Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ jobs:
ARTIFACTS_DIR: ${{ github.workspace }}/dist/artifacts
GH_TOKEN: ${{ github.token }}

dispatch-add-auth-eval:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This job sends clerk_cli_released, which is already a generic event. The job and script names describe what clerk-evals does with it today, not what this workflow does.

clerk/skills has the same kind of sender, and it's named after its event: dispatch-skills-updated.yml and dispatch-skills-update.mjs. It sends one event to clerk, dashboard, and clerk-evals, so adding a receiver is a one-line change.

Could we match that here? dispatch-cli-released for the job and scripts/dispatch-cli-released.ts for the script, with the step name and the docs/releasing.md paragraph to match.

Same idea for the secret while it doesn't exist yet: CLERK_CLI_DISPATCH_TOKEN would follow the CLERK_SKILLS_DISPATCH_TOKEN naming. Your call on that one.

That way none of it is tied to evals, and anything interested in CLI releases can use this.

needs: [versioning, publish-npm]
if: needs.versioning.outputs.release_created == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
- name: Notify clerk-evals of the stable release
run: bun scripts/dispatch-evals.ts
env:
CLERK_CLI_VERSION: ${{ needs.versioning.outputs.version }}
CLERK_EVALS_DISPATCH_TOKEN: ${{ secrets.CLERK_EVALS_DISPATCH_TOKEN }}

publish-github:
needs: [versioning, build, sign-macos, smoke-test, publish-npm]
runs-on: ubuntu-latest
Expand Down Expand Up @@ -175,7 +193,7 @@ jobs:
always() &&
needs.versioning.outputs.release_created == 'true' &&
contains(needs.*.result, 'failure')
needs: [versioning, build, sign-macos, smoke-test, publish-npm, publish-github, homebrew]
needs: [versioning, build, sign-macos, smoke-test, publish-npm, publish-github, homebrew, dispatch-add-auth-eval]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If publishing succeeds and the dispatch fails, this posts "Stable release failed" while notify-stable-success still announces the release. An expired token is enough to trigger it.

Maybe give the dispatch its own failure alert instead, like the notify-failure job in the skills sender? Adding the dispatch to notify-stable-success's needs would also fix the mismatch, but then an expired token would hold back the release announcement.

uses: ./.github/workflows/notify-failure.yml
with:
workflow-name: Stable release
Expand Down
3 changes: 3 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,15 @@ The releaser accepts these flags:

All publishes are idempotent -- the script checks the registry before publishing and skips already-published versions, and a publish that npm rejects because the version already exists counts as done. The check reads the per-version document (`registry.npmjs.org/<name>/<version>`), which the registry CDN does not cache, but npm can still take several minutes to serve a new version back. Before publishing the `clerk` wrapper, the script waits up to two minutes for every platform package to be readable; if one is still unreadable after that, it logs a warning and publishes the wrapper anyway, because npm already accepted every platform publish. If one platform publish fails, the others still run to completion before the job exits, so a re-run only has to publish what is actually missing.

After a stable npm publish succeeds, `dispatch-add-auth-eval` sends `clerk_cli_released` with that version to `clerk/clerk-evals` without waiting for npm's `latest` tag to catch up. Canary and snapshot releases do not send this event. The job uses `CLERK_EVALS_DISPATCH_TOKEN`, a dedicated Actions secret with Contents: read and write on only `clerk/clerk-evals`; the default `GITHUB_TOKEN` stays read-only. A dispatch failure triggers the stable-release failure notification. The eval receiver waits for the version to become `clerk@latest`, pins a `clerk/skills` SHA, and records both versions with its results; a receiver failure is reported from `clerk-evals`, not from the CLI release workflow.

#### Environment Variables

The releaser script and publish workflow steps use these environment variables:

- `ARTIFACTS_DIR` -- path to directory containing compiled binaries from the build job (defaults to `./dist/artifacts`)
- `GH_TOKEN` -- GitHub token used by the releaser to create tags and releases (stable only)
- `CLERK_EVALS_DISPATCH_TOKEN` -- dedicated token for the stable release's `clerk/clerk-evals` repository dispatch (not used by npm publishing)

#### npm Authentication

Expand Down
35 changes: 35 additions & 0 deletions scripts/dispatch-evals.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, test } from "bun:test";
import { assertStableVersion, sendCliReleaseDispatch } from "./dispatch-evals.ts";

describe("stable CLI eval dispatch", () => {
test("rejects canary and snapshot versions", () => {
expect(() => assertStableVersion("3.5.0-canary.1")).toThrow("stable");
expect(() => assertStableVersion("3.5.0-snapshot.1")).toThrow("stable");
expect(() => assertStableVersion("3.5.0")).not.toThrow();
});

test("sends the released version as a repository dispatch", async () => {
let request: { url: string; init?: RequestInit } | undefined;
await sendCliReleaseDispatch("3.5.0", "test-token", async (url, init) => {
request = { url, init };
return new Response(null, { status: 204 });
});
expect(request?.url).toBe("https://api.github.com/repos/clerk/clerk-evals/dispatches");
expect(request?.init?.method).toBe("POST");
expect(JSON.parse(String(request?.init?.body))).toEqual({
event_type: "clerk_cli_released",
client_payload: { version: "3.5.0" },
});
});

test("requires the dedicated credential and reports API rejection", async () => {
await expect(sendCliReleaseDispatch("3.5.0", "")).rejects.toThrow("CLERK_EVALS_DISPATCH_TOKEN");
await expect(
sendCliReleaseDispatch(
"3.5.0",
"test-token",
async () => new Response('{"message":"Forbidden"}', { status: 403 }),
),
).rejects.toThrow("HTTP 403");
});
});
44 changes: 44 additions & 0 deletions scripts/dispatch-evals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/** Notify the add-auth eval receiver after a stable CLI release is published. */

type Fetch = (input: string, init?: RequestInit) => Promise<Response>;

export function assertStableVersion(version: string): void {
if (!/^\d+\.\d+\.\d+$/.test(version)) {
throw new Error(`Expected a stable Clerk CLI version, got ${JSON.stringify(version)}`);
}
}

export async function sendCliReleaseDispatch(
version: string,
token: string,
fetchImpl: Fetch = fetch,
): Promise<void> {
assertStableVersion(version);
if (!token) throw new Error("CLERK_EVALS_DISPATCH_TOKEN is not configured");

const response = await fetchImpl("https://api.github.com/repos/clerk/clerk-evals/dispatches", {
method: "POST",
headers: {
accept: "application/vnd.github+json",
authorization: `Bearer ${token}`,
"content-type": "application/json",
"x-github-api-version": "2022-11-28",
},
body: JSON.stringify({ event_type: "clerk_cli_released", client_payload: { version } }),
signal: AbortSignal.timeout(15_000),
});
if (response.status !== 204) {
const detail = (await response.text().catch(() => "")).slice(0, 200);
throw new Error(
`clerk-evals dispatch returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`,
);
}
}

if (import.meta.main) {
const version = process.env.CLERK_CLI_VERSION ?? "";
const token = process.env.CLERK_EVALS_DISPATCH_TOKEN ?? "";
if (!token) throw new Error("CLERK_EVALS_DISPATCH_TOKEN is not configured");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sendCliReleaseDispatch already checks this, and the test covers it there.

Suggested change
if (!token) throw new Error("CLERK_EVALS_DISPATCH_TOKEN is not configured");

await sendCliReleaseDispatch(version, token);
console.log(`Sent clerk_cli_released for clerk@${version} to clerk/clerk-evals`);
}
Loading