Skip to content
Merged
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
140 changes: 82 additions & 58 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 }}

Expand All @@ -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
Expand Down Expand Up @@ -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 }}"
Expand All @@ -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]"
Expand All @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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`);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 }}"
Expand All @@ -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
Expand All @@ -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 }}"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cla.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Loading
Loading