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-train.yml b/.github/workflows/release-train.yml new file mode 100644 index 0000000..25f9836 --- /dev/null +++ b/.github/workflows/release-train.yml @@ -0,0 +1,222 @@ +name: Release Train + +# Fully automated Friday MINOR release of @agentage/server-memory to npm. No human +# step on the happy path. Weekly cadence, always a minor bump. +# +# Level 1 (dep-driven): this package wraps @agentage/memory-core, whose own train +# releases at 20:00 Prague. This one runs at 21:00 so it can pull the fresh core. +# Before the version bump it upgrades every @agentage/* dependency to latest; a dep +# change ALONE justifies releasing (the whole point of L1). The frozen 6-tool +# contract means `npm run verify` against the bumped memory-core is the integration +# gate - a red verify fails the train without releasing (the PR stays open). +# +# GITHUB_TOKEN caveats handled here: its branch pushes don't fire pr-validation on +# the release PR (the in-workflow `npm run verify` plus the one in publish.yml are +# the CI gates), and its merges don't fire publish.yml's push trigger - so after +# merging, this workflow dispatches publish.yml explicitly. +on: + schedule: + # Friday 21:00 Europe/Prague, both DST offsets; the gate job filters. + - cron: '0 19 * * 5' + - cron: '0 20 * * 5' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + actions: write + +jobs: + prague-gate: + name: 🕘 Friday 21: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" = "21" ]; then + echo "go=true" >> "$GITHUB_OUTPUT" + else + echo "Prague hour is $HOUR, not 21 - 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 - never merge them here. + 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: 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: Bump internal @agentage/* dependencies to latest + id: deps + if: steps.guard.outputs.skip != 'true' + run: | + set -euo pipefail + # L1: pull the fresh @agentage/* line (memory-core released at 20:00). + 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 "changed=false" >> "$GITHUB_OUTPUT" + else + echo "Internal @agentage/* dependencies changed." + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Decide whether to release + id: decide + if: steps.guard.outputs.skip != 'true' + run: | + # A dep change alone justifies releasing (L1); so do releasable commits. + if [ "${{ steps.detect.outputs.commits }}" = "true" ] || [ "${{ steps.deps.outputs.changed }}" = "true" ]; then + echo "release=true" >> "$GITHUB_OUTPUT" + else + echo "Neither releasable commits nor dep changes - no release this week." | tee -a "$GITHUB_STEP_SUMMARY" + echo "release=false" >> "$GITHUB_OUTPUT" + fi + + - name: Bump minor version (package.json) + id: bump + if: steps.guard.outputs.skip != 'true' && steps.decide.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" + + # Verify against the bumped memory-core is the integration gate; a red verify + # fails the train here, before any PR is merged. + - 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 (L1). Version ${VERSION}, with @agentage/* dependencies bumped to latest. 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 + 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"