From cdf40ca89454bfe40b41368252214d318b1cc4ca Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Tue, 11 Aug 2026 01:41:28 +0200 Subject: [PATCH 1/5] ci: rename publish.yml -> release.yml --- .github/workflows/release.yml | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..004ca5f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Publish Package + +# Publishes @agentage/design-system to npm. Manual (workflow_dispatch) or on a +# release commit to master; never on an ordinary push. Requires an NPM_TOKEN +# repo secret with publish rights to the @agentage scope. + +on: + push: + branches: [master] + paths: + - 'package.json' + workflow_dispatch: + +permissions: + contents: write + id-token: write + +env: + NODE_VERSION: '22' + +jobs: + release-gate: + name: Release gate + runs-on: ubuntu-latest + outputs: + is-release: ${{ steps.check.outputs.is-release }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 5 + - id: check + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "is-release=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + MSG=$(git log -1 --pretty=%s) + if echo "$MSG" | grep -qE "(^Release v|^chore: prepare release|^chore\(release\))"; then + echo "is-release=true" >> "$GITHUB_OUTPUT" + else + echo "is-release=false" >> "$GITHUB_OUTPUT" + echo "Not a release commit - skipping publish." + fi + + publish: + name: Publish to npm + needs: [release-gate] + if: needs.release-gate.outputs.is-release == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - name: Skip if version already on npm + id: ver + run: | + name=$(node -p "require('./package.json').name") + version=$(node -p "require('./package.json').version") + if npm view "${name}@${version}" version >/dev/null 2>&1; then + echo "publish=false" >> "$GITHUB_OUTPUT" + echo "${name}@${version} already published." + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + echo "tag=v${version}" >> "$GITHUB_OUTPUT" + - if: steps.ver.outputs.publish == 'true' + run: npm run verify + - if: steps.ver.outputs.publish == 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public --provenance + - if: steps.ver.outputs.publish == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.ver.outputs.tag }}" -m "Release ${{ steps.ver.outputs.tag }}" + git push origin "${{ steps.ver.outputs.tag }}" From b9169e1de3dd4811fc0eb1b7715b38d1ac5e7855 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Tue, 11 Aug 2026 01:41:29 +0200 Subject: [PATCH 2/5] ci: drop publish.yml (renamed) --- .github/workflows/publish.yml | 80 ----------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 004ca5f..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Publish Package - -# Publishes @agentage/design-system to npm. Manual (workflow_dispatch) or on a -# release commit to master; never on an ordinary push. Requires an NPM_TOKEN -# repo secret with publish rights to the @agentage scope. - -on: - push: - branches: [master] - paths: - - 'package.json' - workflow_dispatch: - -permissions: - contents: write - id-token: write - -env: - NODE_VERSION: '22' - -jobs: - release-gate: - name: Release gate - runs-on: ubuntu-latest - outputs: - is-release: ${{ steps.check.outputs.is-release }} - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 5 - - id: check - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "is-release=true" >> "$GITHUB_OUTPUT"; exit 0 - fi - MSG=$(git log -1 --pretty=%s) - if echo "$MSG" | grep -qE "(^Release v|^chore: prepare release|^chore\(release\))"; then - echo "is-release=true" >> "$GITHUB_OUTPUT" - else - echo "is-release=false" >> "$GITHUB_OUTPUT" - echo "Not a release commit - skipping publish." - fi - - publish: - name: Publish to npm - needs: [release-gate] - if: needs.release-gate.outputs.is-release == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'npm' - registry-url: 'https://registry.npmjs.org' - - run: npm ci - - name: Skip if version already on npm - id: ver - run: | - name=$(node -p "require('./package.json').name") - version=$(node -p "require('./package.json').version") - if npm view "${name}@${version}" version >/dev/null 2>&1; then - echo "publish=false" >> "$GITHUB_OUTPUT" - echo "${name}@${version} already published." - else - echo "publish=true" >> "$GITHUB_OUTPUT" - fi - echo "tag=v${version}" >> "$GITHUB_OUTPUT" - - if: steps.ver.outputs.publish == 'true' - run: npm run verify - - if: steps.ver.outputs.publish == 'true' - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish --access public --provenance - - if: steps.ver.outputs.publish == 'true' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag -a "${{ steps.ver.outputs.tag }}" -m "Release ${{ steps.ver.outputs.tag }}" - git push origin "${{ steps.ver.outputs.tag }}" From 39674fe377fa0c1aa0089f0ecb7f3a65157b0962 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Tue, 11 Aug 2026 01:41:30 +0200 Subject: [PATCH 3/5] ci: rename release-train.yml -> train.yml --- .github/workflows/train.yml | 162 ++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 .github/workflows/train.yml diff --git a/.github/workflows/train.yml b/.github/workflows/train.yml new file mode 100644 index 0000000..c9bc1c2 --- /dev/null +++ b/.github/workflows/train.yml @@ -0,0 +1,162 @@ +name: Release Train + +# Fully automated Friday MINOR release of @agentage/design-system to npm. No human +# step on the happy path. Weekly cadence, always a minor bump (product decision). +# Level 0: no internal @agentage npm deps, npm-only publish (no MCP registry). +# +# GITHUB_TOKEN caveats handled here: its branch pushes don't fire CI on the release +# PR (the in-workflow `npm run verify` plus the one in publish.yml are the gates), +# and its merges don't fire publish.yml's push trigger - so after merging, this +# workflow dispatches publish.yml explicitly. +on: + schedule: + # Single UTC cron (L0 slot). A late-firing cron just means the train + # departs late instead of being gated away. Dependabot opens PRs + # 16:00 UTC Friday; org triage merges from 17:00 UTC. + - cron: '0 19 * * 5' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + dependabot-quiescence: + name: 🤖 Wait for dependabot triage to finish + runs-on: ubuntu-latest + timeout-minutes: 50 + steps: + - name: Wait until this repo has no fresh open dependabot PRs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + # Dependabot merging is owned by the org triage automation + # (.github-private); this job only delays the train while that agent + # may still be working. PRs still open after the wait were deliberately + # left by triage - the train departs without them. + for i in $(seq 1 45); do + OPEN=$(gh pr list -R "$REPO" --author "app/dependabot" --state open --json number --jq 'length') + [ "$OPEN" = "0" ] && { echo "No open dependabot PRs - proceeding."; exit 0; } + echo "poll $i: $OPEN open dependabot PR(s), waiting for triage..." + sleep 60 + done + echo "Dependabot PR(s) still open after wait - releasing without them." | tee -a "$GITHUB_STEP_SUMMARY" + + release: + name: 🚝 Cut minor release + needs: dependabot-quiescence + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '22' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Skip if an open release PR exists + id: guard + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EXISTING=$(gh pr list --state open --json headRefName \ + --jq '[.[] | select(.headRefName | startswith("release/"))] | length') + if [ "$EXISTING" != "0" ]; then + echo "Open release PR already exists - a previous train is stuck. Skipping." | tee -a "$GITHUB_STEP_SUMMARY" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Detect releasable commits since the last tag + id: detect + if: steps.guard.outputs.skip != 'true' + run: | + set -euo pipefail + LAST_TAG=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -1) + RANGE=""; [ -n "$LAST_TAG" ] && RANGE="${LAST_TAG}..HEAD" + echo "Last tag: ${LAST_TAG:-none}" + SUBJECTS=$(git log $RANGE --no-merges --pretty=%s) + # Releasable = anything except docs/ci/plain-chore; chore(deps) counts + # (dependabot bundles ship weekly), chore(release) does not. + RELEASABLE=$(printf '%s\n' "$SUBJECTS" \ + | grep -vE '^(docs|ci)(\([^)]*\))?!?:' \ + | grep -vE '^chore(\([^)]*\))?!?:' || true) + DEPS=$(printf '%s\n' "$SUBJECTS" | grep -E '^chore\(deps' || true) + COUNT=$(printf '%s\n%s\n' "$RELEASABLE" "$DEPS" | grep -c . || true) + if [ "$COUNT" -eq 0 ]; then + echo "No releasable commits since ${LAST_TAG:-repo start} - no release this week." | tee -a "$GITHUB_STEP_SUMMARY" + echo "release=false" >> "$GITHUB_OUTPUT" + else + echo "Found $COUNT releasable commit(s)." + echo "release=true" >> "$GITHUB_OUTPUT" + fi + + - name: Bump minor version (package.json) + id: bump + if: steps.guard.outputs.skip != 'true' && steps.detect.outputs.release == 'true' + run: | + set -euo pipefail + NEW=$(node -e ' + const fs = require("fs"); + const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); + const [a, b] = pkg.version.split(".").map(Number); + const next = `${a}.${b + 1}.0`; + pkg.version = next; + fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); + console.log(next); + ') + npm install --package-lock-only + echo "New version: $NEW" + echo "version=$NEW" >> "$GITHUB_OUTPUT" + + - name: Verify + if: steps.bump.outputs.version + run: npm run verify + + - name: Create + auto-merge release PR + id: pr + if: steps.bump.outputs.version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + VERSION="${{ steps.bump.outputs.version }}" + BRANCH="release/${VERSION}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add package.json package-lock.json + git commit -m "chore(release): ${VERSION}" + git push origin "$BRANCH" + PR_URL=$(gh pr create --base master --head "$BRANCH" \ + --title "chore(release): ${VERSION}" \ + --body "Weekly minor release train. Version ${VERSION} to npm (@agentage/design-system). Publish happens via publish.yml after merge.") + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + sleep 5 + if ! OUT=$(gh pr merge "$PR_URL" --auto --squash 2>&1); then + echo "$OUT" + echo "$OUT" | grep -q "clean status" && gh pr merge "$PR_URL" --squash || exit 1 + fi + + - name: Dispatch publish workflow + if: steps.pr.outputs.pr_url + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # A GITHUB_TOKEN merge never fires publish.yml's push trigger, so + # dispatch it explicitly. Its dispatch path re-verifies, skips if the + # version is already on npm, then publishes to npm and tags the release. + gh workflow run publish.yml --ref master --repo "${{ github.repository }}" + echo "Released ${{ steps.bump.outputs.version }} - publish.yml dispatched." >> "$GITHUB_STEP_SUMMARY" From de30c43cfabb92801a30e4a26acdcce0df75e03b Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Tue, 11 Aug 2026 01:41:32 +0200 Subject: [PATCH 4/5] ci: drop release-train.yml (renamed) --- .github/workflows/release-train.yml | 162 ---------------------------- 1 file changed, 162 deletions(-) delete mode 100644 .github/workflows/release-train.yml diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml deleted file mode 100644 index c9bc1c2..0000000 --- a/.github/workflows/release-train.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: Release Train - -# Fully automated Friday MINOR release of @agentage/design-system to npm. No human -# step on the happy path. Weekly cadence, always a minor bump (product decision). -# Level 0: no internal @agentage npm deps, npm-only publish (no MCP registry). -# -# GITHUB_TOKEN caveats handled here: its branch pushes don't fire CI on the release -# PR (the in-workflow `npm run verify` plus the one in publish.yml are the gates), -# and its merges don't fire publish.yml's push trigger - so after merging, this -# workflow dispatches publish.yml explicitly. -on: - schedule: - # Single UTC cron (L0 slot). A late-firing cron just means the train - # departs late instead of being gated away. Dependabot opens PRs - # 16:00 UTC Friday; org triage merges from 17:00 UTC. - - cron: '0 19 * * 5' - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - actions: write - -jobs: - dependabot-quiescence: - name: 🤖 Wait for dependabot triage to finish - runs-on: ubuntu-latest - timeout-minutes: 50 - steps: - - name: Wait until this repo has no fresh open dependabot PRs - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - # Dependabot merging is owned by the org triage automation - # (.github-private); this job only delays the train while that agent - # may still be working. PRs still open after the wait were deliberately - # left by triage - the train departs without them. - for i in $(seq 1 45); do - OPEN=$(gh pr list -R "$REPO" --author "app/dependabot" --state open --json number --jq 'length') - [ "$OPEN" = "0" ] && { echo "No open dependabot PRs - proceeding."; exit 0; } - echo "poll $i: $OPEN open dependabot PR(s), waiting for triage..." - sleep 60 - done - echo "Dependabot PR(s) still open after wait - releasing without them." | tee -a "$GITHUB_STEP_SUMMARY" - - release: - name: 🚝 Cut minor release - needs: dependabot-quiescence - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout code - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Skip if an open release PR exists - id: guard - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - EXISTING=$(gh pr list --state open --json headRefName \ - --jq '[.[] | select(.headRefName | startswith("release/"))] | length') - if [ "$EXISTING" != "0" ]; then - echo "Open release PR already exists - a previous train is stuck. Skipping." | tee -a "$GITHUB_STEP_SUMMARY" - echo "skip=true" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Detect releasable commits since the last tag - id: detect - if: steps.guard.outputs.skip != 'true' - run: | - set -euo pipefail - LAST_TAG=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -1) - RANGE=""; [ -n "$LAST_TAG" ] && RANGE="${LAST_TAG}..HEAD" - echo "Last tag: ${LAST_TAG:-none}" - SUBJECTS=$(git log $RANGE --no-merges --pretty=%s) - # Releasable = anything except docs/ci/plain-chore; chore(deps) counts - # (dependabot bundles ship weekly), chore(release) does not. - RELEASABLE=$(printf '%s\n' "$SUBJECTS" \ - | grep -vE '^(docs|ci)(\([^)]*\))?!?:' \ - | grep -vE '^chore(\([^)]*\))?!?:' || true) - DEPS=$(printf '%s\n' "$SUBJECTS" | grep -E '^chore\(deps' || true) - COUNT=$(printf '%s\n%s\n' "$RELEASABLE" "$DEPS" | grep -c . || true) - if [ "$COUNT" -eq 0 ]; then - echo "No releasable commits since ${LAST_TAG:-repo start} - no release this week." | tee -a "$GITHUB_STEP_SUMMARY" - echo "release=false" >> "$GITHUB_OUTPUT" - else - echo "Found $COUNT releasable commit(s)." - echo "release=true" >> "$GITHUB_OUTPUT" - fi - - - name: Bump minor version (package.json) - id: bump - if: steps.guard.outputs.skip != 'true' && steps.detect.outputs.release == 'true' - run: | - set -euo pipefail - NEW=$(node -e ' - const fs = require("fs"); - const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); - const [a, b] = pkg.version.split(".").map(Number); - const next = `${a}.${b + 1}.0`; - pkg.version = next; - fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); - console.log(next); - ') - npm install --package-lock-only - echo "New version: $NEW" - echo "version=$NEW" >> "$GITHUB_OUTPUT" - - - name: Verify - if: steps.bump.outputs.version - run: npm run verify - - - name: Create + auto-merge release PR - id: pr - if: steps.bump.outputs.version - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - VERSION="${{ steps.bump.outputs.version }}" - BRANCH="release/${VERSION}" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add package.json package-lock.json - git commit -m "chore(release): ${VERSION}" - git push origin "$BRANCH" - PR_URL=$(gh pr create --base master --head "$BRANCH" \ - --title "chore(release): ${VERSION}" \ - --body "Weekly minor release train. Version ${VERSION} to npm (@agentage/design-system). Publish happens via publish.yml after merge.") - echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - sleep 5 - if ! OUT=$(gh pr merge "$PR_URL" --auto --squash 2>&1); then - echo "$OUT" - echo "$OUT" | grep -q "clean status" && gh pr merge "$PR_URL" --squash || exit 1 - fi - - - name: Dispatch publish workflow - if: steps.pr.outputs.pr_url - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # A GITHUB_TOKEN merge never fires publish.yml's push trigger, so - # dispatch it explicitly. Its dispatch path re-verifies, skips if the - # version is already on npm, then publishes to npm and tags the release. - gh workflow run publish.yml --ref master --repo "${{ github.repository }}" - echo "Released ${{ steps.bump.outputs.version }} - publish.yml dispatched." >> "$GITHUB_STEP_SUMMARY" From 3767a90f0504336d49970e2b85fd887a3394c7dd Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Tue, 11 Aug 2026 01:41:35 +0200 Subject: [PATCH 5/5] ci: dispatch the renamed workflow from train.yml --- .github/workflows/train.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/train.yml b/.github/workflows/train.yml index c9bc1c2..ab2086d 100644 --- a/.github/workflows/train.yml +++ b/.github/workflows/train.yml @@ -158,5 +158,5 @@ jobs: # A GITHUB_TOKEN merge never fires publish.yml's push trigger, so # dispatch it explicitly. Its dispatch path re-verifies, skips if the # version is already on npm, then publishes to npm and tags the release. - gh workflow run publish.yml --ref master --repo "${{ github.repository }}" + gh workflow run release.yml --ref master --repo "${{ github.repository }}" echo "Released ${{ steps.bump.outputs.version }} - publish.yml dispatched." >> "$GITHUB_STEP_SUMMARY"