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
3 changes: 0 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ updates:
all-dependencies:
patterns:
- '*'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']

- package-ecosystem: 'github-actions'
directory: '/'
Expand Down
222 changes: 222 additions & 0 deletions .github/workflows/release-train.yml
Original file line number Diff line number Diff line change
@@ -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"