diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 083d81fd5..292f40cd2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260725.5", + "version": "5.260725.8", "source": "./plugins/genie", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, wish them into plans, make with parallel agents, ship as one team. A coding genie that grows with your project." } diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 4eb139a2a..6687eb53f 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -187,7 +187,6 @@ jobs: VERSION: ${{ inputs.version }} CHANNEL: ${{ inputs.channel }} SOURCE_SHA: ${{ inputs.source_sha }} - TRIGGER_SHA: ${{ inputs.trigger_sha }} SOURCE_BRANCH: ${{ inputs.source_branch }} SOURCE_CI_RUN_ID: ${{ inputs.source_ci_run_id }} CONTROL_SHA: ${{ github.sha }} diff --git a/.github/workflows/sign-attest.yml b/.github/workflows/sign-attest.yml index b7463efca..761d8288a 100644 --- a/.github/workflows/sign-attest.yml +++ b/.github/workflows/sign-attest.yml @@ -337,7 +337,6 @@ jobs: SOURCE_SHA: ${{ inputs.source_sha }} SOURCE_BRANCH: ${{ inputs.source_branch }} SOURCE_CI_RUN_ID: ${{ inputs.source_ci_run_id }} - TRIGGER_SHA: ${{ inputs.trigger_sha }} CONTROL_SHA: ${{ github.sha }} RUN_ID: ${{ github.run_id }} RUN_ATTEMPT: ${{ github.run_attempt }} @@ -380,6 +379,10 @@ jobs: SOURCE_SHA: ${{ inputs.source_sha }} SOURCE_BRANCH: ${{ inputs.source_branch }} SOURCE_CI_RUN_ID: ${{ inputs.source_ci_run_id }} + # Required by release-generic-provenance.sh verify-exact on the + # automated channels: it binds the provenance's triggering + # workflow_run head to this value and exits 2 when it is absent. + TRIGGER_SHA: ${{ inputs.trigger_sha }} CONTROL_SHA: ${{ github.sha }} run: | set -euo pipefail diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 628620d09..784e83b9d 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -180,8 +180,14 @@ jobs: run: | PREFIX="${{ steps.context.outputs.prefix }}" TODAY=$(date -u +%y%m%d) - EXISTING=$(git tag --list "v${PREFIX}.${TODAY}.*" | wc -l) - BUILD_NUMBER=$((EXISTING + 1)) + # Highest existing build number + 1, never the count: a gap (a tag + # deleted, or a build number burned by a failed release) would make + # count+1 collide with an existing tag, and the push failure below is + # reported as a benign race, silently dropping the release. + HIGHEST=$(git tag --list "v${PREFIX}.${TODAY}.*" \ + | sed -n "s|^v${PREFIX}\.${TODAY}\.\([0-9][0-9]*\)$|\1|p" \ + | sort -n | tail -1) + BUILD_NUMBER=$(( ${HIGHEST:-0} + 1 )) VERSION="${PREFIX}.${TODAY}.${BUILD_NUMBER}" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "build_number=${BUILD_NUMBER}" >> "$GITHUB_OUTPUT" @@ -313,7 +319,14 @@ jobs: # in CI logs and detector-greppable. The next merge's run catches # up. SHORT_SHA="${TRIGGER_SHA:0:7}" - if ! git push --atomic origin "HEAD:refs/heads/dev" "refs/tags/v${VERSION}"; then + if ! push_output=$(git push --atomic origin "HEAD:refs/heads/dev" "refs/tags/v${VERSION}" 2>&1); then + printf '%s\n' "$push_output" >&2 + # A tag collision is a version-derivation defect, not a race: exiting + # 0 here would drop the release with a green run and no diagnosis. + if printf '%s' "$push_output" | grep -qiE 'already exists|cannot lock ref|tag .* exists'; then + echo "::error ::release-version.tag-collision v${VERSION} already exists; version derivation picked a used build number" + exit 1 + fi echo "::notice ::release-race.next-tag-pin-skipped.detected dev advanced past triggering SHA ${SHORT_SHA}; tag push skipped — next merge will republish" exit 0 fi diff --git a/package.json b/package.json index b28ad27f0..9aa6ff05f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260725.5", + "version": "5.260725.8", "description": "Collaborative terminal toolkit for human + AI workflows. NOTE: npm distribution discontinued 2026-05-09 — install via `curl -fsSL https://raw.githubusercontent.com/automagik-dev/genie/main/install.sh | bash` (cosign + SLSA verified). See https://automagik.dev/genie/release-process", "license": "MIT", diff --git a/plugins/genie/.claude-plugin/plugin.json b/plugins/genie/.claude-plugin/plugin.json index d777687b8..789ffd7f7 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260725.5", + "version": "5.260725.8", "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.", "author": { "name": "Namastex Labs" diff --git a/plugins/genie/.codex-plugin/plugin.json b/plugins/genie/.codex-plugin/plugin.json index e11fd0610..507107a1e 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260725.5", + "version": "5.260725.8", "description": "Plan, execute, review, and ship software with Genie workflows in Codex.", "author": { "name": "Namastex Labs", diff --git a/plugins/genie/package.json b/plugins/genie/package.json index ccfd9c2ab..3f132366d 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260725.5", + "version": "5.260725.8", "private": true, "description": "Runtime dependencies for genie bundled CLIs", "license": "MIT", diff --git a/plugins/hermes-genie/plugin.yaml b/plugins/hermes-genie/plugin.yaml index 1f6bc9d12..b38f694f4 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260725.5 +version: 5.260725.8 description: "Native Hermes surface for Genie orchestration: read-only status plus work-plan and review-plan gap tools that the MCP board surface does not cover, hooks, commands, and a thin cockpit skill. Board/task truth comes from the genie MCP tools." provides_tools: # Default surface: exactly the three gap tools the MCP board surface does not cover. diff --git a/scripts/release-docs.test.ts b/scripts/release-docs.test.ts index ad8c21d99..53bf83aed 100644 --- a/scripts/release-docs.test.ts +++ b/scripts/release-docs.test.ts @@ -463,6 +463,63 @@ describe('Group E release and documentation contracts', () => { expect(read('src/genie.ts')).toContain(".command('__install-promote', { hidden: true })"); }); + test('the admitting CI head is plumbed to provenance verification end to end', () => { + // The generic SLSA provenance embeds the workflow_run that TRIGGERED the + // release, whose head is the commit CI ran on — one BEFORE the + // [auto-version] bump the tarballs are built from. Verification must bind + // that field to the trigger commit, never to source_sha (they can never be + // equal, which silently broke every dev release on 2026-07-25). If any link + // in this chain drops trigger_sha, exact verification either mis-binds or + // hard-fails on an empty value, so the whole wiring is pinned here. + const version = read('.github/workflows/version.yml'); + const release = read('.github/workflows/release.yml'); + const signAttest = read('.github/workflows/sign-attest.yml'); + const publish = read('.github/workflows/release-publish.yml'); + const verifier = read('scripts/release-generic-provenance.sh'); + + expect(version).toContain('trigger_sha: ${{ github.event.workflow_run.head_sha }}'); + expect(release).toContain('trigger_sha:'); + expect(release).toContain('trigger_sha: ${{ inputs.trigger_sha }}'); + expect(signAttest).toContain('trigger_sha:'); + + // Substring checks are NOT enough: the first attempt at this fix put + // TRIGGER_SHA in the native-predicate step, whose script never reads it, + // while the step that actually runs verify-exact had none — a whole-file + // toContain() passed anyway and the next release died with the same silent + // exit 2. Assert the value reaches the env of the step that CONSUMES it. + const stepEnvFor = (workflow: string, needle: string): string => { + const lines = workflow.split('\n'); + let start = -1; + for (let i = 0; i < lines.length; i += 1) { + if (/^\s+- name:/.test(lines[i])) start = i; + if (lines[i].includes(needle) && start >= 0) { + let end = start + 1; + while (end < lines.length && !/^\s+- name:/.test(lines[end])) end += 1; + return lines.slice(start, end).join('\n'); + } + } + throw new Error(`no step found containing ${needle}`); + }; + + const verifyStep = stepEnvFor(signAttest, 'release-generic-provenance.sh verify-exact'); + expect(verifyStep).toContain('TRIGGER_SHA:'); + expect(verifyStep).toContain('${{ inputs.trigger_sha }}'); + + const gateStep = stepEnvFor(publish, 'generic_expected_sha'); + expect(gateStep).toContain('TRIGGER_SHA:'); + + // The verifier compares the trigger commit, and refuses to run exact + // verification without a well-formed one. + expect(verifier).toContain('$run.head_sha == $trigger_sha'); + expect(verifier).not.toContain('$run.head_sha == $source_sha'); + + // Stable records source_sha directly; automated channels cannot, so the + // generic descriptor is compared per channel. + expect(publish).toContain('generic_expected_sha="$SOURCE_SHA"'); + expect(publish).toContain('generic_expected_sha="$TRIGGER_SHA"'); + expect(publish).toContain('.sourceSha == $genericSourceSha'); + }); + test('release create and promotion paths retain the one-time convergence caveat', () => { const workflow = read('.github/workflows/release-publish.yml'); const helper = read('scripts/reconcile-release-note.sh');