diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ac6f54..ddd7fd8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,8 +53,8 @@ jobs: matrix: node: ["24", "26"] # 24 = current LTS floor (node:sqlite); 26 = latest stable steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node }} cache: npm @@ -86,7 +86,7 @@ jobs: npx -y @anthropic-ai/claude-code@latest plugin validate plugin --strict npx -y @anthropic-ai/claude-code@latest plugin validate .claude-plugin/marketplace.json --strict # Hand the lcov report to the sonar job so it doesn't re-install + re-test. - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: matrix.node == '24' with: name: coverage-lcov @@ -103,8 +103,8 @@ jobs: runs-on: windows-latest timeout-minutes: 20 # the launcher tests spawn real children; a hang must not cost 6 hours steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: "24" # the floor install.ps1 enforces cache: npm @@ -155,22 +155,22 @@ jobs: env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # full history for accurate blame / new-code detection - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version cache: npm - run: npm ci # node_modules → type-aware analysis (resolves dependency types) # Reuse the check job's coverage instead of re-running the whole test suite. - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: coverage-lcov path: coverage # Cache the scanner CLI + analyzer/JRE so it isn't re-downloaded each run. - name: Cache SonarQube scanner - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar @@ -181,7 +181,7 @@ jobs: run: echo "PKG_VERSION=$(node -p "require('./package.json').version")" >> "$GITHUB_ENV" - name: SonarQube Cloud scan if: ${{ env.SONAR_TOKEN != '' }} - uses: SonarSource/sonarqube-scan-action@v8 + uses: SonarSource/sonarqube-scan-action@e805d875275f728fac1885b413934be6d7c63f65 # v8.4.1 with: args: -Dsonar.projectVersion=${{ env.PKG_VERSION }} @@ -205,10 +205,10 @@ jobs: contents: write # rolling `next` tag + prerelease id-token: write # npm provenance / OIDC trusted publishing (tokenless) steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # tags → last release → canary base version - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version registry-url: https://registry.npmjs.org @@ -255,27 +255,28 @@ jobs: # A separate marketplace, so the plugin keeps its name: it installs as # devcoach@ultimaphoenix-beta with the same tools/commands/skill as the release. The entry is an # `archive` source (the prerelease's plugin zip + sha256): that zip is the only artifact whose - # launcher pins the canary npm version. Pushes with a write deploy key scoped to the beta repo - # alone (secret BETA_MARKETPLACE_DEPLOY_KEY in the claude-marketplace environment) — the token - # that guards the release marketplace never leaves that job. Kept out of `prerelease` because - # an `environment:` key would change its npm OIDC subject. + # launcher pins the canary npm version. Pushes like the release-time jobs below (CI_DEVCOACH_TOKEN + # in the `publishing` environment — see the comment above `homebrew`). Kept out of `prerelease` + # because an `environment:` key would change its npm OIDC subject. marketplace-beta: needs: prerelease if: ${{ needs.prerelease.result == 'success' }} runs-on: ubuntu-latest environment: - name: claude-marketplace + name: publishing url: https://github.com/UltimaPhoenix/claude-plugins-marketplace-beta env: - DEPLOY_KEY: ${{ secrets.BETA_MARKETPLACE_DEPLOY_KEY }} + HAS_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN != '' }} GH_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version - name: Pin the canary in UltimaPhoenix/claude-plugins-marketplace-beta - if: ${{ env.DEPLOY_KEY != '' }} + if: ${{ env.HAS_TOKEN == 'true' }} + env: + GIT_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN }} run: | set -euo pipefail VERSION="${{ needs.prerelease.outputs.version }}" @@ -287,11 +288,8 @@ jobs: gh release download next -p "$ASSET" --dir dl SHA="$(sha256sum "dl/$ASSET" | cut -d' ' -f1)" URL="https://github.com/UltimaPhoenix/dev-coach/releases/download/next/${ASSET}" - mkdir -p ~/.ssh && chmod 700 ~/.ssh - printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/beta_deploy_key && chmod 600 ~/.ssh/beta_deploy_key - ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts 2>/dev/null - export GIT_SSH_COMMAND="ssh -i ~/.ssh/beta_deploy_key -o IdentitiesOnly=yes" - git clone --depth 1 git@github.com:UltimaPhoenix/claude-plugins-marketplace-beta.git mkt + . scripts/ci/git-token-auth.sh + git clone --depth 1 https://github.com/UltimaPhoenix/claude-plugins-marketplace-beta.git mkt node scripts/update-beta-marketplace.mjs "$VERSION" "$URL" "$SHA" mkt/.claude-plugin/marketplace.json cd mkt git config user.name "github-actions[bot]" @@ -305,6 +303,10 @@ jobs: if: github.event_name == 'workflow_dispatch' needs: check runs-on: ubuntu-latest + # CI_DEVCOACH_TOKEN is a secret of the `publishing` environment (see the publishing jobs): + # the bump reads it from there, and the environment's branch policy admits exactly the refs a + # release is dispatched from (develop, main). + environment: publishing # Serialize releases: a second dispatch queues behind the first instead of racing it # (rapid re-triggers were stacking version bumps). concurrency: @@ -317,16 +319,16 @@ jobs: tag: ${{ steps.bump.outputs.tag }} version: ${{ steps.bump.outputs.version }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Full history + tags so the preflight guards (last tag, "nothing to release") work. fetch-depth: 0 - # Admin PAT (fine-grained, dev-coach only, Contents: RW) so the pushes to the - # protected develop + main branches bypass branch protection (enforce_admins is off). + # The admin's fine-grained PAT (Contents: RW; see the publishing jobs) so the pushes to + # the protected develop + main branches bypass branch protection (enforce_admins is off). # The default GITHUB_TOKEN runs as github-actions[bot], which is not an admin # and would be rejected by the required CLA check. - token: ${{ secrets.RELEASE_TOKEN }} - - uses: actions/setup-node@v7 + token: ${{ secrets.CI_DEVCOACH_TOKEN }} + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version - name: Preflight — release from develop (or main for a hotfix) @@ -411,10 +413,10 @@ jobs: id-token: write # npm provenance / OIDC trusted publishing (tokenless) deployments: write # record an "npm" deployment for the Deployments dashboard steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.bump.outputs.tag || github.ref }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version registry-url: https://registry.npmjs.org @@ -430,7 +432,7 @@ jobs: # `environment:` key on purpose: an environment would change the OIDC subject claim and break # trusted publishing unless mirrored in npmjs.com's trusted-publisher settings. - name: Record npm deployment - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { version } = require(`${process.env.GITHUB_WORKSPACE}/package.json`); @@ -459,10 +461,10 @@ jobs: MCPB_CERT_PEM: ${{ secrets.MCPB_CERT }} MCPB_KEY_PEM: ${{ secrets.MCPB_KEY }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.bump.outputs.tag || github.ref }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version cache: npm @@ -496,7 +498,7 @@ jobs: cp dist-standalone/devcoach-*-standalone.zip dist-mcpb/ # Checksums over every release asset (.mcpb + zip + SBOM); run inside the dir so paths are bare. (cd dist-mcpb && shasum -a 256 *.mcpb *.zip *.cdx.json > SHASUMS256.txt) - - uses: softprops/action-gh-release@v3 + - uses: softprops/action-gh-release@8a0cd651e5b2365bd640854a3fa5dfc70e4875f3 # v3.0.4 with: tag_name: ${{ needs.bump.outputs.tag || github.ref_name }} generate_release_notes: true @@ -506,22 +508,36 @@ jobs: dist-mcpb/devcoach.cdx.json dist-mcpb/SHASUMS256.txt - # ── Bump the Homebrew tap formula (skips when HOMEBREW_TAP_TOKEN unset) ───── + # ── Publishing jobs: the jobs that write INTO other UltimaPhoenix repos ───────────────────── + # The pipeline has ONE credential, CI_DEVCOACH_TOKEN: a fine-grained PAT with Contents RW on + # dev-coach (the `bump` job's pushes past the branch protection) and on the destination repos + # homebrew-tap, scoop-bucket, claude-plugins-marketplace, claude-plugins-marketplace-beta. It is + # a secret of the `publishing` environment, and every job that uses it (`bump` included) runs + # in that environment, whose deployment branch policy (develop, main, v* tags) refuses the job + # on any other ref before it starts. In these jobs the token is read in the ONE step that pushes + # (`secrets` is not available in a step's `if`, hence the job-level HAS_TOKEN boolean), and + # scripts/ci/git-token-auth.sh hands it to git as a header: never in a URL, never on disk. + # Every action is pinned to a commit SHA (Dependabot bumps them, comment included): a moved + # tag is how CI secrets have actually been stolen (tj-actions, March 2025). + + # ── Bump the Homebrew tap formula (skips when CI_DEVCOACH_TOKEN unset) ───── homebrew: needs: [publish, bump] if: ${{ always() && needs.publish.result == 'success' }} runs-on: ubuntu-latest environment: - name: homebrew-tap + name: publishing url: https://github.com/UltimaPhoenix/homebrew-tap/blob/main/Formula/devcoach.rb env: - TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + HAS_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN != '' }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.bump.outputs.tag || github.ref }} - name: Update devcoach.rb in UltimaPhoenix/homebrew-tap - if: ${{ env.TAP_TOKEN != '' }} + if: ${{ env.HAS_TOKEN == 'true' }} + env: + GIT_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN }} run: | set -euo pipefail VERSION="${{ needs.bump.outputs.version || github.ref_name }}" @@ -532,7 +548,8 @@ jobs: for _ in $(seq 1 60); do curl -fsSL "$URL" -o pkg.tgz && break || sleep 15; done [ -s pkg.tgz ] || { echo "::error::npm never served $URL — re-run this job once the tarball is available."; exit 1; } SHA="$(shasum -a 256 pkg.tgz | awk '{print $1}')" - git clone --depth 1 "https://x-access-token:${TAP_TOKEN}@github.com/UltimaPhoenix/homebrew-tap.git" tap + . scripts/ci/git-token-auth.sh + git clone --depth 1 https://github.com/UltimaPhoenix/homebrew-tap.git tap mkdir -p tap/Formula node scripts/homebrew-formula.mjs "$VERSION" "$SHA" > tap/Formula/devcoach.rb cd tap @@ -545,26 +562,29 @@ jobs: # ── Scoop bucket: pin the standalone zip in UltimaPhoenix/scoop-bucket (skips when token unset) ── # The Windows twin of the Homebrew job: bucket/devcoach.json points at the release's standalone # zip with the sha256 taken from the release's own SHASUMS256.txt; `scoop install devcoach` - # installs Scoop's nodejs-lts next to it and shims devcoach.cmd. + # installs Scoop's nodejs-lts next to it and shims devcoach.cmd. The bucket is shared by every + # UltimaPhoenix project — this job adds and commits its own manifest only. scoop: needs: [release, bump] if: ${{ always() && needs.release.result == 'success' }} runs-on: ubuntu-latest environment: - name: scoop-bucket + name: publishing url: https://github.com/UltimaPhoenix/scoop-bucket/blob/main/bucket/devcoach.json env: - BUCKET_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} + HAS_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN != '' }} GH_TOKEN: ${{ github.token }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.bump.outputs.tag || github.ref }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version - name: Update bucket/devcoach.json in UltimaPhoenix/scoop-bucket - if: ${{ env.BUCKET_TOKEN != '' }} + if: ${{ env.HAS_TOKEN == 'true' }} + env: + GIT_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN }} run: | set -euo pipefail VERSION="${{ needs.bump.outputs.version || github.ref_name }}" @@ -573,7 +593,8 @@ jobs: gh release download "v${VERSION}" -p SHASUMS256.txt --dir dl SHA="$(awk -v a="$ASSET" '$2 == a { print $1 }' dl/SHASUMS256.txt)" [ -n "$SHA" ] || { echo "::error::${ASSET} is not listed in the release's SHASUMS256.txt"; exit 1; } - git clone --depth 1 "https://x-access-token:${BUCKET_TOKEN}@github.com/UltimaPhoenix/scoop-bucket.git" bucket + . scripts/ci/git-token-auth.sh + git clone --depth 1 https://github.com/UltimaPhoenix/scoop-bucket.git bucket mkdir -p bucket/bucket node scripts/scoop-manifest.mjs "$VERSION" "$SHA" > bucket/bucket/devcoach.json cd bucket @@ -589,24 +610,27 @@ jobs: if: ${{ always() && needs.publish.result == 'success' }} runs-on: ubuntu-latest environment: - name: claude-marketplace + name: publishing url: https://github.com/UltimaPhoenix/claude-plugins-marketplace env: - MARKETPLACE_TOKEN: ${{ secrets.CLAUDE_PLUGIN_MARKETPLACE }} + HAS_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN != '' }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.bump.outputs.tag || github.ref }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version - name: Pin devcoach in UltimaPhoenix/claude-plugins-marketplace - if: ${{ env.MARKETPLACE_TOKEN != '' }} + if: ${{ env.HAS_TOKEN == 'true' }} + env: + GIT_TOKEN: ${{ secrets.CI_DEVCOACH_TOKEN }} run: | set -euo pipefail VERSION="${{ needs.bump.outputs.version || github.ref_name }}" VERSION="${VERSION#v}" - git clone --depth 1 "https://x-access-token:${MARKETPLACE_TOKEN}@github.com/UltimaPhoenix/claude-plugins-marketplace.git" mkt + . scripts/ci/git-token-auth.sh + git clone --depth 1 https://github.com/UltimaPhoenix/claude-plugins-marketplace.git mkt # Surgical merge: updates only the devcoach entry, leaving other plugins untouched. node scripts/update-marketplace.mjs "$VERSION" "v${VERSION}" mkt/.claude-plugin/marketplace.json cd mkt @@ -631,10 +655,10 @@ jobs: name: mcp-registry url: https://registry.modelcontextprotocol.io/v0/servers?search=io.github.UltimaPhoenix/devcoach steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.bump.outputs.tag || github.ref }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version - name: Install mcp-publisher diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 6a12aa4..5030612 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -22,7 +22,7 @@ jobs: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' - uses: contributor-assistant/github-action@v2.6.1 + uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ed35586..ac48d39 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,15 +24,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main path: release - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: develop path: next - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: release/.node-version cache: npm @@ -48,7 +48,7 @@ jobs: DOCS_NEXT: "1" run: npm ci && npm run build - run: cp -r next/website/build release/website/build/next - - uses: actions/upload-pages-artifact@v5 + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: release/website/build @@ -60,4 +60,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/update-screenshots.yml b/.github/workflows/update-screenshots.yml index d845707..4d45489 100644 --- a/.github/workflows/update-screenshots.yml +++ b/.github/workflows/update-screenshots.yml @@ -10,14 +10,14 @@ jobs: screenshots: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Admin PAT so the screenshots commit can push to the protected branch it was # dispatched on — run it on develop (the default GITHUB_TOKEN bot is blocked by the # required CLA check). The commit message already carries [skip ci], so this push # doesn't re-trigger CI (nor a canary). token: ${{ secrets.RELEASE_TOKEN }} - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version cache: npm diff --git a/CLAUDE.md b/CLAUDE.md index 5a8f86b..d813180 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -451,7 +451,8 @@ PATH (`HKCU\Environment` + `WM_SETTINGCHANGE`), writes `uninstall.ps1` + an Apps then runs `devcoach install`. Windows PowerShell 5.1 and 7, x64 + ARM64, no admin, no execution policy (`irm | iex` runs in memory; `.cmd` launchers, never `.ps1` shims). The Scoop bucket (`UltimaPhoenix/scoop-bucket`, `bucket/devcoach.json` from `scripts/scoop-manifest.mjs`, pushed by -the `scoop` CI job with `SCOOP_BUCKET_TOKEN`) `depends` on Scoop's `nodejs-lts` and shims +the `scoop` CI job — the bucket is shared by every UltimaPhoenix project, one manifest per app) +`depends` on Scoop's `nodejs-lts` and shims `devcoach.cmd` — Scoop never shims a `.js`. winget was studied and deferred: it only takes exe/msi/ msix (a `.cmd`/`.js` portable is rejected), i.e. a Node SEA + code signing. @@ -484,9 +485,17 @@ layout `devcoach-plugin-.zip` (root `marketplace.json` + `plugin/`) and the p inert) — and the `marketplace-beta` job pins the **archive** zip (archive source + sha256, via `scripts/update-beta-marketplace.mjs`) in `UltimaPhoenix/claude-plugins-marketplace-beta` — a **separate** marketplace so the plugin keeps its name (`devcoach@ultimaphoenix-beta`, same -tools/commands/skill as the release; `doctor` warns when both channels are enabled); it pushes with -a write deploy key scoped to the beta repo (`BETA_MARKETPLACE_DEPLOY_KEY`, `claude-marketplace` -environment), not the release marketplace token. The docs site +tools/commands/skill as the release; `doctor` warns when both channels are enabled). **CI +credentials**: the pipeline has ONE secret, `CI_DEVCOACH_TOKEN` — the admin's fine-grained PAT +(Contents RW on dev-coach + the four destination repos: homebrew-tap, scoop-bucket, +claude-plugins-marketplace, claude-plugins-marketplace-beta), stored only in the `publishing` +environment. `bump` (pushes past the branch protection) and the four publishing jobs +(`marketplace-beta`, `homebrew`, `scoop`, `marketplace`) all run in that environment, whose +deployment branch policy (develop, main, `v*`) refuses them on any other ref. In the publishing +jobs the token lives in the `env` of the one step that pushes and reaches git as a header via +`scripts/ci/git-token-auth.sh` (never in a URL, never on disk); every `uses:` is pinned to a +commit SHA with the version as a comment (Dependabot's `github-actions` ecosystem bumps both). +`npm` and `mcp-registry` stay tokenless (OIDC). The docs site deploys both branches from one Pages artifact: `main` at `/dev-coach/`, `develop` at `/dev-coach/next/` (`DOCS_NEXT=1` build: noindex, no sitemap, banner). Then `publish` (`npm publish`, OIDC diff --git a/scripts/ci/git-token-auth.sh b/scripts/ci/git-token-auth.sh new file mode 100644 index 0000000..e2242a5 --- /dev/null +++ b/scripts/ci/git-token-auth.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Source this in a publishing step: . scripts/ci/git-token-auth.sh +# +# Hands $GIT_TOKEN to every git command of the step as an HTTP header, through git's +# environment configuration (GIT_CONFIG_COUNT/KEY/VALUE, git >= 2.31). The token is never part of +# a URL, never written to the clone's .git/config, and its base64 form is masked in the job log. +# Only the step that sources this file ever sees the token: the jobs keep the secret in that +# step's `env`, not the job's. +: "${GIT_TOKEN:?GIT_TOKEN is not set}" +_auth="$(printf 'x-access-token:%s' "$GIT_TOKEN" | base64 | tr -d '\n')" +echo "::add-mask::$_auth" +export GIT_CONFIG_COUNT=1 +export GIT_CONFIG_KEY_0="http.https://github.com/.extraheader" +export GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $_auth" +unset _auth