From 8b6d21e3d1caf0647a3e1893bc97301cdfc44cb4 Mon Sep 17 00:00:00 2001 From: Baptiste Fernandez Date: Mon, 29 Jun 2026 14:57:44 +0100 Subject: [PATCH] Require Tessl auth for skill review --- README.md | 8 ++++++-- action.yml | 9 ++++++++- src/main.test.ts | 18 +++++++++++++++++- src/main.ts | 15 +++++++++++++-- src/skill-review.ts | 14 +++++++++++--- 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 63e345e..3beee14 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A GitHub Action that automatically reviews `SKILL.md` files changed in a pull request and posts the results as a PR comment. -**No authentication required.** This action runs `tessl skill review` locally — no Tessl account or API token needed. The only token used is the GitHub-provided `GITHUB_TOKEN` for posting PR comments. +This action requires a Tessl API token. Store the token as a GitHub repository secret, for example `TESSL_TOKEN`, and pass it with the `tessl-token` input. ## Usage @@ -23,9 +23,11 @@ jobs: steps: - uses: actions/checkout@v4 - uses: tesslio/skill-review@main + with: + tessl-token: ${{ secrets.TESSL_TOKEN }} ``` -That's it. Any PR that modifies a `SKILL.md` file will get an automated review comment. +Any PR that modifies a `SKILL.md` file will get an automated review comment. ## Inputs @@ -34,6 +36,7 @@ That's it. Any PR that modifies a `SKILL.md` file will get an automated review c | `path` | Root path to search for SKILL.md files | `.` | | `comment` | Whether to post results as a PR comment | `true` | | `fail-threshold` | Minimum score (0-100) to pass. Set to `0` to never fail. | `0` | +| `tessl-token` | Tessl API token used to authenticate review requests. Store it as a GitHub secret. | unset | ### Setting a quality gate @@ -42,6 +45,7 @@ To enforce a minimum skill quality score, set `fail-threshold`: ```yaml - uses: tesslio/skill-review@main with: + tessl-token: ${{ secrets.TESSL_TOKEN }} fail-threshold: 70 ``` diff --git a/action.yml b/action.yml index 9e3a9d0..7dc80b7 100644 --- a/action.yml +++ b/action.yml @@ -13,10 +13,15 @@ inputs: fail-threshold: description: 'Minimum average score (0-100) to pass the check. Set to 0 to never fail.' default: '0' + tessl-token: + description: 'Tessl API token used to authenticate review requests. Store this as a GitHub secret, for example secrets.TESSL_TOKEN.' + required: false runs: using: 'composite' steps: - - uses: tesslio/setup-tessl@v1 + - uses: tesslio/setup-tessl@v2 + with: + token: ${{ inputs.tessl-token }} - uses: oven-sh/setup-bun@v2 - run: bun install --production --frozen-lockfile shell: bash @@ -27,4 +32,6 @@ runs: INPUT_PATH: ${{ inputs.path }} INPUT_COMMENT: ${{ inputs.comment }} INPUT_FAIL_THRESHOLD: ${{ inputs.fail-threshold }} + TESSL_API_TOKEN: ${{ inputs.tessl-token }} + TESSL_TOKEN: ${{ inputs.tessl-token }} GITHUB_TOKEN: ${{ github.token }} diff --git a/src/main.test.ts b/src/main.test.ts index cf4243b..7060a25 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -43,7 +43,7 @@ mock.module('@actions/github', () => ({ // Import after mock registration const { getChangedSkillFiles } = await import('./changed-files.ts'); -const { runSkillReview, extractJson } = await import('./skill-review.ts'); +const { runSkillReview, extractJson, isAuthErrorMessage } = await import('./skill-review.ts'); const { postOrUpdateComment } = await import('./comment.ts'); const { parseThreshold } = await import('./main.ts'); @@ -292,6 +292,22 @@ describe('runSkillReview', () => { expect(result.score).toBe(-1); }); + test('auth failure with threshold 0 still fails', async () => { + // @ts-expect-error mock assignment + Bun.spawn = makeMockSpawn('', '✘ 401 Unauthorized', 1); + + const result = await runSkillReview('skills/test/SKILL.md', 0); + expect(result.passed).toBe(false); + expect(result.score).toBe(-1); + expect(result.error).toContain('401 Unauthorized'); + }); + + test('detects login-required auth failures', () => { + expect(isAuthErrorMessage('Skill review requires you to be logged in. Run tessl login to log in.')).toBe(true); + expect(isAuthErrorMessage('✘ 401 Unauthorized')).toBe(true); + expect(isAuthErrorMessage('some validation error')).toBe(false); + }); + test('malformed JSON output (unclosed brace)', async () => { // @ts-expect-error mock assignment Bun.spawn = makeMockSpawn('{ broken json !!!', '', 0); diff --git a/src/main.ts b/src/main.ts index 009b3c2..182b16d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import * as core from '@actions/core'; import { getChangedSkillFiles } from './changed-files.ts'; import { postOrUpdateComment } from './comment.ts'; import type { SkillReviewResult } from './skill-review.ts'; -import { runSkillReview } from './skill-review.ts'; +import { isAuthErrorMessage, runSkillReview } from './skill-review.ts'; const CONCURRENCY_LIMIT = 5; @@ -53,7 +53,18 @@ async function main(): Promise { } } - // 5. Check threshold + // 5. Auth failures are infrastructure failures, not score failures. + // `fail-threshold: 0` disables score gating only; it must not hide missing auth. + const authFailures = results.filter((r) => isAuthErrorMessage(r.error)); + if (authFailures.length > 0) { + const summary = authFailures.map((r) => ` ${r.skillPath}`).join('\n'); + core.setFailed( + `Tessl authentication failed for ${authFailures.length} skill(s). Configure the tessl-token input with a Tessl API token stored in a GitHub secret.\n${summary}`, + ); + return; + } + + // 6. Check threshold if (threshold > 0) { const failed = results.filter((r) => !r.passed); if (failed.length > 0) { diff --git a/src/skill-review.ts b/src/skill-review.ts index b4fe675..98ec0ae 100644 --- a/src/skill-review.ts +++ b/src/skill-review.ts @@ -111,6 +111,13 @@ export interface SkillReviewResult { error?: string; } +export function isAuthErrorMessage(message: string | undefined): boolean { + if (!message) return false; + return /requires you to be logged in|run tessl login|401 unauthorized|authentication required|not authenticated/i.test( + message, + ); +} + export async function runSkillReview( skillFilePath: string, threshold: number, @@ -129,15 +136,16 @@ export async function runSkillReview( const exitCode = await proc.exited; if (exitCode !== 0) { + const error = stderr || stdout || `Process exited with code ${exitCode}`; console.warn( - `tessl skill review failed for ${skillFilePath} (exit code ${exitCode}): ${stderr}`, + `tessl skill review failed for ${skillFilePath} (exit code ${exitCode}): ${error}`, ); return { skillPath: skillFilePath, - passed: threshold === 0, + passed: threshold === 0 && !isAuthErrorMessage(error), score: -1, output: '', - error: stderr || `Process exited with code ${exitCode}`, + error, }; }