diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 57c7a752a..7504476ab 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "genie", - "version": "5.260724.1", + "version": "5.260725.2", "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 806dbdd34..ab2dd8889 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -1029,47 +1029,82 @@ jobs: timeout-minutes: 10 environment: # No reviewers: dev/homolog stay automatic. The environment is restricted - # to main and holds the sole credential (RELEASE_MANIFESTS_TOKEN) allowed - # to push the .well-known manifests through main branch protection. + # to main and holds the sole credential (the RELEASE_MANIFESTS App key) + # allowed to push the .well-known manifests through the main ruleset. name: release-manifests permissions: contents: read # ----------------------------------------------------------------------- - # HUMAN SETUP (one-time) — the manifest push authenticates with a - # fine-grained PAT, NOT a deploy key. Deploy keys are DISABLED org-wide - # (repos/automagik-dev/genie/keys POST → 422 "Deploy keys are disabled"), - # so the old ssh-key model silently fell back to github-actions[bot]'s - # token, whose push to protected main 403s. That broke 4 releases. + # HUMAN SETUP (one-time) — the manifest push authenticates as a GitHub + # App, NOT a PAT and NOT a deploy key. + # + # History: deploy keys are DISABLED org-wide (repos/automagik-dev/genie/keys + # POST → 422 "Deploy keys are disabled"), so the ssh-key model silently fell + # back to github-actions[bot], whose push to protected main 403s. Replacing + # it with a fine-grained PAT did not help either: the `main` ruleset + # (id 9203218) carries a `pull_request` rule with an EMPTY bypass_actors + # list, so *every* direct push to main is rejected GH013 regardless of the + # token's contents:write scope. A ruleset bypass is required, and an App is + # the narrowest identity that can hold one — granting it to a RepositoryRole + # would hand every admin direct push to main. # # To provision (repo/org admin): - # 1. Create a FINE-GRAINED PAT scoped to automagik-dev/genie ONLY, with - # Repository permissions → Contents: Read and write (nothing else). - # 2. Store it as the environment secret RELEASE_MANIFESTS_TOKEN in the - # `release-manifests` GitHub environment (Settings → Environments). - # 3. Allow the PAT's identity through main branch protection (add it to - # the bypass/allow list for "Restrict who can push to matching - # branches"), or the push will 403 exactly as the deploy key did. - # 4. Delete the now-unused RELEASE_MANIFESTS_DEPLOY_KEY environment - # secret and any orphaned deploy key. + # 1. Create an org-owned GitHub App (suggested name: genie-release-bot) + # with Repository permissions → Contents: Read and write (nothing + # else). Generate a private key. + # 2. Install it on automagik-dev/genie ONLY (no other repositories). + # 3. Store its credentials as environment secrets in the + # `release-manifests` environment (Settings → Environments): + # RELEASE_MANIFESTS_APP_ID and RELEASE_MANIFESTS_APP_PRIVATE_KEY. + # 4. Add the App as a bypass actor on the `main` ruleset: + # gh api repos/automagik-dev/genie/rulesets/9203218 -X PUT \ + # -f 'bypass_actors[][actor_type=Integration]' \ + # -F 'bypass_actors[][actor_id]=' \ + # -f 'bypass_actors[][bypass_mode]=always' + # Without this the push fails GH013 exactly as the PAT did. + # 5. Delete the now-unused RELEASE_MANIFESTS_TOKEN and any stale + # RELEASE_MANIFESTS_DEPLOY_KEY environment secret. + # + # The App token is minted per run and expires in ~1h, so no long-lived + # push credential to main exists anywhere. # (Kept in the workflow header because docs/ is a submodule; mirror this # into docs/release-process.mdx when that submodule is next updated.) # ----------------------------------------------------------------------- steps: - - name: Preflight — require the manifest push token + - name: Preflight — require the manifest push App credentials env: - RELEASE_MANIFESTS_TOKEN: ${{ secrets.RELEASE_MANIFESTS_TOKEN }} + RELEASE_MANIFESTS_APP_ID: ${{ secrets.RELEASE_MANIFESTS_APP_ID }} + RELEASE_MANIFESTS_APP_PRIVATE_KEY: ${{ secrets.RELEASE_MANIFESTS_APP_PRIVATE_KEY }} shell: bash run: | set -euo pipefail - if [[ -z "${RELEASE_MANIFESTS_TOKEN}" ]]; then - echo "::error::RELEASE_MANIFESTS_TOKEN is not set in the 'release-manifests' environment." >&2 - echo "::error::Without it actions/checkout silently falls back to github-actions[bot], whose push to protected main is rejected 403 (misreported below as a CAS race)." >&2 - echo "::error::Fix: create a fine-grained PAT (contents:read+write on automagik-dev/genie only), store it as the RELEASE_MANIFESTS_TOKEN environment secret, allow it through main branch protection, then delete the stale RELEASE_MANIFESTS_DEPLOY_KEY. See this job's HUMAN SETUP header." >&2 + missing=0 + if [[ -z "${RELEASE_MANIFESTS_APP_ID:-}" ]]; then + echo "::error::RELEASE_MANIFESTS_APP_ID is not set in the 'release-manifests' environment." >&2 + missing=1 + fi + if [[ -z "${RELEASE_MANIFESTS_APP_PRIVATE_KEY:-}" ]]; then + echo "::error::RELEASE_MANIFESTS_APP_PRIVATE_KEY is not set in the 'release-manifests' environment." >&2 + missing=1 + fi + if [[ "${missing}" -ne 0 ]]; then + echo "::error::Without App credentials actions/checkout silently falls back to github-actions[bot], whose push to protected main is rejected (misreported below as a CAS race)." >&2 + echo "::error::Fix: create an org-owned App with contents:read+write on automagik-dev/genie only, store its id/private key as the RELEASE_MANIFESTS_APP_ID and RELEASE_MANIFESTS_APP_PRIVATE_KEY environment secrets, and add the App as an Integration bypass actor on the 'main' ruleset. See this job's HUMAN SETUP header." >&2 exit 1 fi + + - name: Mint a short-lived App token for the manifest push + id: manifest_app_token + uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v3.2.0 + with: + app-id: ${{ secrets.RELEASE_MANIFESTS_APP_ID }} + private-key: ${{ secrets.RELEASE_MANIFESTS_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: - token: ${{ secrets.RELEASE_MANIFESTS_TOKEN }} + token: ${{ steps.manifest_app_token.outputs.token }} persist-credentials: true - name: Download the exact pre-endorsed candidate manifest bytes @@ -1147,7 +1182,7 @@ jobs: fi printf '%s\n' "$push_output" >&2 if printf '%s' "$push_output" | grep -qiE 'protected branch|GH006|permission to .* denied|not authorized|403|forbidden|remote rejected'; then - echo "::error::manifest push to main was REJECTED for lack of permission — this is NOT a concurrent-update CAS race. RELEASE_MANIFESTS_TOKEN lacks contents:write on ${GITHUB_REPOSITORY} or is not allowed through main branch protection. See this job's HUMAN SETUP header." >&2 + echo "::error::manifest push to main was REJECTED for lack of permission — this is NOT a concurrent-update CAS race. Either the RELEASE_MANIFESTS App lacks contents:write on ${GITHUB_REPOSITORY}, or it is not a bypass actor on the 'main' ruleset (an empty bypass_actors list rejects every direct push GH013). See this job's HUMAN SETUP header." >&2 exit 1 fi echo "::warning ::release-manifest.cas_retry attempt ${attempt}/5: non-fast-forward from a concurrent main update; recomputing" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b058cee9..8eca175f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -189,7 +189,7 @@ jobs: source_ci_run_id: ${{ inputs.source_ci_run_id }} draft: false # Without inherit, the called workflow's secrets context is empty and the - # manifests job sees RELEASE_MANIFESTS_TOKEN as unset even though the - # release-manifests environment holds it (proven on the v5.260720.10 - # stable run). Local trusted workflow — inherit is safe. + # manifests job sees RELEASE_MANIFESTS_APP_ID/_PRIVATE_KEY as unset even + # though the release-manifests environment holds them (proven on the + # v5.260720.10 stable run). Local trusted workflow — inherit is safe. secrets: inherit diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 322d677b9..a62967971 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -441,5 +441,6 @@ jobs: source_branch: ${{ needs.auto-version.outputs.source_branch }} source_ci_run_id: ${{ needs.auto-version.outputs.source_ci_run_id }} # Propagate secrets through the call chain so release-publish.yml's - # manifests job can read RELEASE_MANIFESTS_TOKEN (see release.yml publish). + # manifests job can read the RELEASE_MANIFESTS App credentials (see + # release.yml publish). secrets: inherit diff --git a/package.json b/package.json index d06acd4f7..601ea3c0c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@automagik/genie", - "version": "5.260724.1", + "version": "5.260725.2", "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 acb3ea05e..5c986d956 100644 --- a/plugins/genie/.claude-plugin/plugin.json +++ b/plugins/genie/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260724.1", + "version": "5.260725.2", "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 1c4f30830..3ea67a4e6 100644 --- a/plugins/genie/.codex-plugin/plugin.json +++ b/plugins/genie/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "genie", - "version": "5.260724.1", + "version": "5.260725.2", "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 90bf78ec8..40e911a3c 100644 --- a/plugins/genie/package.json +++ b/plugins/genie/package.json @@ -1,6 +1,6 @@ { "name": "genie-plugin", - "version": "5.260724.1", + "version": "5.260725.2", "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 b683d4460..1f0956a14 100644 --- a/plugins/hermes-genie/plugin.yaml +++ b/plugins/hermes-genie/plugin.yaml @@ -1,5 +1,5 @@ name: genie -version: 5.260724.1 +version: 5.260725.2 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 056e01535..ad8c21d99 100644 --- a/scripts/release-docs.test.ts +++ b/scripts/release-docs.test.ts @@ -480,11 +480,18 @@ describe('Group E release and documentation contracts', () => { expect(workflow).not.toContain('/immutable-releases'); expect(workflow).not.toContain('--clobber'); expect(workflow).toContain('name: release-manifests'); - // Manifest push authenticates with a fine-grained PAT, not a deploy key - // (deploy keys are disabled org-wide; the ssh-key model silently fell back - // to the bot token and 403'd on protected main). The stale deploy-key - // secret must be gone. - expect(workflow).toContain('token: ${{ secrets.RELEASE_MANIFESTS_TOKEN }}'); + // Manifest push authenticates as a GitHub App — not a deploy key (disabled + // org-wide; the ssh-key model silently fell back to the bot token and 403'd + // on protected main) and not a fine-grained PAT (which still cannot push: + // the `main` ruleset carries a pull_request rule with an EMPTY bypass_actors + // list, so every direct push is rejected GH013 regardless of token scope). + // Only an App identity can hold that ruleset bypass, and its token is minted + // per run rather than stored. Both stale credentials must be gone. + expect(workflow).toContain('uses: actions/create-github-app-token@'); + expect(workflow).toContain('app-id: ${{ secrets.RELEASE_MANIFESTS_APP_ID }}'); + expect(workflow).toContain('private-key: ${{ secrets.RELEASE_MANIFESTS_APP_PRIVATE_KEY }}'); + expect(workflow).toContain('token: ${{ steps.manifest_app_token.outputs.token }}'); + expect(workflow).not.toContain('token: ${{ secrets.RELEASE_MANIFESTS_TOKEN }}'); expect(workflow).not.toContain('RELEASE_MANIFESTS_DEPLOY_KEY: ${{'); expect(workflow).not.toContain('ssh-key: ${{ secrets.RELEASE_MANIFESTS_DEPLOY_KEY }}'); expect(workflow).toContain('[release-manifest]');