From 04b1868773f82ba391f9969664740a39e4e2c155 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Thu, 30 Jul 2026 01:07:46 +0200 Subject: [PATCH 1/2] ci: Friday release train (L2, @agentage dep-bump) --- .github/dependabot.yml | 3 - .github/workflows/release-prepare.yml | 216 ------------------------- .github/workflows/release-train.yml | 217 ++++++++++++++++++++++++++ 3 files changed, 217 insertions(+), 219 deletions(-) delete mode 100644 .github/workflows/release-prepare.yml create mode 100644 .github/workflows/release-train.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b1e4ad1..b6d0588 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -14,9 +14,6 @@ updates: all-dependencies: patterns: - '*' - ignore: - - dependency-name: '*' - update-types: ['version-update:semver-major'] - package-ecosystem: 'github-actions' directory: '/' diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml deleted file mode 100644 index 5d3ed7e..0000000 --- a/.github/workflows/release-prepare.yml +++ /dev/null @@ -1,216 +0,0 @@ -name: "\U0001F4E6 Prepare Release" - -on: - workflow_dispatch: - inputs: - bump_type: - description: 'Version bump type' - required: true - default: 'patch' - type: choice - options: - - patch - - minor - - major - auto_merge: - description: 'Auto-merge the release PR after CI passes' - required: false - default: false - type: boolean - -permissions: - contents: write - pull-requests: write - -jobs: - prepare-release: - name: Prepare Release PR - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Verify - run: npm run verify - - - name: Check for existing release PR - id: check_pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - EXISTING_PR=$(gh pr list --head "release/" --state open --json number --jq '.[0].number // empty') - if [ -n "$EXISTING_PR" ]; then - echo "Release PR #$EXISTING_PR already exists, skipping" - echo "skip=true" >> $GITHUB_OUTPUT - else - echo "No existing release PR found" - echo "skip=false" >> $GITHUB_OUTPUT - fi - - - name: Check for commits since last tag - if: steps.check_pr.outputs.skip != 'true' - id: check_commits - run: | - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$LAST_TAG" ]; then - echo "No tags found, will create initial release" - echo "has_commits=true" >> $GITHUB_OUTPUT - exit 0 - fi - COMMIT_COUNT=$(git rev-list --count --no-merges ${LAST_TAG}..HEAD) - echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT - if [ "$COMMIT_COUNT" -eq 0 ]; then - echo "No commits since last tag ($LAST_TAG), skipping" - echo "has_commits=false" >> $GITHUB_OUTPUT - else - echo "Found $COMMIT_COUNT commits since $LAST_TAG" - echo "has_commits=true" >> $GITHUB_OUTPUT - fi - - - name: Bump version - if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' - id: bump - run: | - BUMP_TYPE="${{ github.event.inputs.bump_type }}" - OUTPUT=$(node scripts/bump-version.js $BUMP_TYPE) - echo "$OUTPUT" - NEW_VERSION=$(echo "$OUTPUT" | grep "NEW_VERSION=" | cut -d= -f2) - echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - - - name: Update package-lock.json - if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' - run: npm install --package-lock-only - - - name: Generate changelog with Claude - if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' - id: changelog - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - # Optional Anthropic-compatible endpoint override (e.g. Azure); SDK reads it natively. - ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }} - run: | - CHANGELOG=$(node scripts/generate-changelog.js ${{ steps.bump.outputs.new_version }}) - echo "$CHANGELOG" > changelog-content.md - echo "Generated changelog:" - cat changelog-content.md - - - name: Update CHANGELOG.md - if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' - run: | - CHANGELOG_CONTENT=$(cat changelog-content.md) - node scripts/update-changelog.js "${{ steps.bump.outputs.new_version }}" "$CHANGELOG_CONTENT" - - - name: Create release branch and PR - if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' - id: create_pr - env: - GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} - run: | - VERSION="${{ steps.bump.outputs.new_version }}" - BRANCH_NAME="release/v${VERSION}" - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Re-auth origin so the push is authored by RELEASE_PAT, which fires - # downstream workflows. GITHUB_TOKEN pushes are suppressed by GitHub - # to prevent recursion, which stalls auto-merge on the release PR. - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" - - git checkout -b "$BRANCH_NAME" - git add -A - git commit -m "chore: prepare release v${VERSION} - - - Bump version to ${VERSION} - - Update CHANGELOG.md with release notes" - git push origin "$BRANCH_NAME" - - CHANGELOG_CONTENT=$(cat changelog-content.md) - - PR_URL=$(gh pr create \ - --title "Release v${VERSION}" \ - --body "$(cat <> $GITHUB_OUTPUT - - gh pr edit "$BRANCH_NAME" --add-label "release" 2>/dev/null || true - - - name: Auto-merge release PR - if: steps.create_pr.outputs.pr_url && github.event.inputs.auto_merge == 'true' - env: - GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }} - PR_URL: ${{ steps.create_pr.outputs.pr_url }} - run: | - echo "Auto-merge enabled — enabling GitHub auto-merge" - # `--auto` waits for required checks. When the PR has no required checks, - # GitHub rejects it with `GraphQL: Pull request is in clean status - # (enablePullRequestAutoMerge)` — in that case the PR is already mergeable - # so fall back to an immediate squash-merge. - if ! OUTPUT=$(gh pr merge "$PR_URL" --auto --squash 2>&1); then - echo "$OUTPUT" - if echo "$OUTPUT" | grep -q "clean status"; then - echo "Auto-merge rejected (clean status) — merging directly" - gh pr merge "$PR_URL" --squash - else - exit 1 - fi - else - echo "Auto-merge enabled. PR will merge when all checks pass." - fi - - - name: Summary - if: always() - run: | - if [ "${{ steps.check_pr.outputs.skip }}" == "true" ]; then - echo "## Skipped" >> $GITHUB_STEP_SUMMARY - echo "An open release PR already exists." >> $GITHUB_STEP_SUMMARY - elif [ "${{ steps.check_commits.outputs.has_commits }}" == "false" ]; then - echo "## Skipped" >> $GITHUB_STEP_SUMMARY - echo "No commits since last release tag." >> $GITHUB_STEP_SUMMARY - elif [ -n "${{ steps.bump.outputs.new_version }}" ]; then - echo "## Release PR Created" >> $GITHUB_STEP_SUMMARY - echo "Version: v${{ steps.bump.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY - AUTO_MERGE="${{ github.event.inputs.auto_merge }}" - if [ "$AUTO_MERGE" == "true" ]; then - echo "Auto-merge: **enabled** (will merge when checks pass)" >> $GITHUB_STEP_SUMMARY - else - echo "Auto-merge: **disabled** (manual review required)" >> $GITHUB_STEP_SUMMARY - fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Changelog" >> $GITHUB_STEP_SUMMARY - cat changelog-content.md >> $GITHUB_STEP_SUMMARY 2>/dev/null || true - fi diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml new file mode 100644 index 0000000..0b585ca --- /dev/null +++ b/.github/workflows/release-train.yml @@ -0,0 +1,217 @@ +name: Release Train + +# Fully automated Friday MINOR release of @agentage/cli to npm. No human step on +# the happy path. Weekly cadence, always a minor bump. +# +# Level 2 in the @agentage dependency chain: this repo depends on +# @agentage/memory-core AND @agentage/server-memory, which release earlier the +# same evening (20:00 / 21:00 Prague). This train runs at 22:00 Prague so it picks +# up their freshly published versions. Both scheduled crons fire (one per DST +# offset); the prague-gate job proceeds only when the local hour is 22. +# +# GITHUB_TOKEN caveats handled here: its branch pushes don't fire pr-validation on +# the release PR (the in-workflow `npm run verify` is the CI gate), and its merges +# don't fire publish.yml's push trigger - so after merging, this workflow +# dispatches publish.yml explicitly. +on: + schedule: + # Friday 22:00 Europe/Prague across both DST offsets; the gate job filters. + - cron: '0 20 * * 5' + - cron: '0 21 * * 5' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + prague-gate: + name: Friday 22:00 Prague gate + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + go: ${{ steps.gate.outputs.go }} + steps: + - name: Check local Prague hour (DST-proof) + id: gate + run: | + if [ "${{ github.event_name }}" != "schedule" ]; then + echo "workflow_dispatch - bypassing hour gate." + echo "go=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + HOUR=$(TZ=Europe/Prague date +%H) + if [ "$HOUR" = "22" ]; then + echo "go=true" >> "$GITHUB_OUTPUT" + else + echo "Prague hour is $HOUR, not 22 - wrong DST cron slot, skipping." + echo "go=false" >> "$GITHUB_OUTPUT" + fi + + dependabot-quiescence: + name: Wait for dependabot triage to finish + needs: prague-gate + if: needs.prague-gate.outputs.go == 'true' + 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; 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: [prague-gate, dependabot-quiescence] + if: needs.prague-gate.outputs.go == 'true' + 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: Bump internal @agentage/* dependencies to latest + id: deps + if: steps.guard.outputs.skip != 'true' + run: | + set -euo pipefail + # L2 upgrade: pull in memory-core + server-memory versions released + # earlier tonight. A dep change alone justifies the weekly release. + npx --yes npm-check-updates -u --dep prod,dev --filter "@agentage/*" + npm install + if git diff --quiet -- package.json package-lock.json; then + echo "No @agentage/* dependency changes." + echo "deps_changed=false" >> "$GITHUB_OUTPUT" + else + echo "Internal dependencies bumped." + git --no-pager diff -- package.json | grep -E '@agentage/' || true + echo "deps_changed=true" >> "$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}." + echo "commits=false" >> "$GITHUB_OUTPUT" + else + echo "Found $COUNT releasable commit(s)." + echo "commits=true" >> "$GITHUB_OUTPUT" + fi + + - name: Decide whether to release + id: decide + if: steps.guard.outputs.skip != 'true' + run: | + if [ "${{ steps.deps.outputs.deps_changed }}" = "true" ] || [ "${{ steps.detect.outputs.commits }}" = "true" ]; then + echo "release=true" >> "$GITHUB_OUTPUT" + else + echo "Neither dep changes nor releasable commits - no release this week." | tee -a "$GITHUB_STEP_SUMMARY" + echo "release=false" >> "$GITHUB_OUTPUT" + fi + + - name: Bump minor version + id: bump + if: steps.decide.outputs.release == 'true' + run: | + set -euo pipefail + OUT=$(node scripts/bump-version.js minor) + echo "$OUT" + NEW=$(echo "$OUT" | grep "NEW_VERSION=" | cut -d= -f2) + 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} of @agentage/cli. Publish happens via publish.yml after merge.") + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + sleep 5 + # A GITHUB_TOKEN-created PR fires no PR checks, so --auto is rejected with + # "clean status" - fall back to an immediate squash merge. + 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 + tags. + gh workflow run publish.yml --ref master --repo "${{ github.repository }}" + echo "Released ${{ steps.bump.outputs.version }} - publish.yml dispatched." >> "$GITHUB_STEP_SUMMARY" From 7690de26ef799e1252dfe09b89cf66329276113d Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Thu, 30 Jul 2026 01:11:37 +0200 Subject: [PATCH 2/2] ci: anchor last-tag detection on current version line (stale v0.24.x tags) --- .github/workflows/release-train.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-train.yml b/.github/workflows/release-train.yml index 0b585ca..232ff74 100644 --- a/.github/workflows/release-train.yml +++ b/.github/workflows/release-train.yml @@ -132,7 +132,17 @@ jobs: if: steps.guard.outputs.skip != 'true' run: | set -euo pipefail - LAST_TAG=$(git tag --list 'v[0-9]*' --sort=-v:refname | head -1) + # Anchor on the CURRENT release line: the repo carries stale v0.24.x + # tags from the pre-reboot CLI that sort above the live 0.x line, so + # semver-max would flag every week as releasable forever. Prefer the + # tag of the current package.json version; fall back to the nearest + # ancestor tag. + PKG_TAG="v$(node -p "require('./package.json').version")" + if git rev-parse -q --verify "refs/tags/${PKG_TAG}" >/dev/null; then + LAST_TAG="$PKG_TAG" + else + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || true) + fi RANGE=""; [ -n "$LAST_TAG" ] && RANGE="${LAST_TAG}..HEAD" echo "Last tag: ${LAST_TAG:-none}" SUBJECTS=$(git log $RANGE --no-merges --pretty=%s)