diff --git a/.github/workflows/ambient-version-agent.yml b/.github/workflows/ambient-version-agent.yml deleted file mode 100644 index 35c6e8d..0000000 --- a/.github/workflows/ambient-version-agent.yml +++ /dev/null @@ -1,154 +0,0 @@ -name: Ambient Version Agent - -# An AI agent (Claude Code) that reviews the automated Next.js bump PRs created by -# nextjs-version-check.yml. For each open `nextjs-*` PR it: reads the new Next.js -# release notes, runs the full e2e suite, writes/adjusts e2e tests for any new -# cache-component behavior, and then either approves + merges (clean) or comments -# its findings and leaves the PR for a human (genuine regression / new issue). -# -# SETUP REQUIRED (see docs/ambient-agent.md): -# - Secret ANTHROPIC_API_KEY (or wire Claude Code OAuth). -# - An approval-capable identity: the default GITHUB_TOKEN CANNOT approve PRs, -# so either install the Claude GitHub App, or set secret AGENT_GITHUB_TOKEN to -# a PAT / fine-grained token with "Pull requests: read & write". - -on: - workflow_dispatch: - inputs: - pr_number: - description: "PR number to review (optional; otherwise all open nextjs-* PRs)" - required: false - type: string - schedule: - # ~2h after nextjs-version-check (00:00 UTC) so the bump PR exists first. - - cron: "0 2 * * *" - -# Avoid two agent runs racing on the same PR. -concurrency: - group: ambient-version-agent-${{ github.event.inputs.pr_number || 'all' }} - cancel-in-progress: false - -jobs: - review: - runs-on: ubuntu-latest - permissions: - contents: write # push test commits to the PR branch - pull-requests: write # comment / approve / merge - id-token: write - - services: - redis: - image: redis:7-alpine - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Check configuration - id: cfg - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - run: | - # Skip cleanly (no failed runs) until the agent is configured. - if [ -z "$ANTHROPIC_API_KEY" ]; then - echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "::notice::ANTHROPIC_API_KEY not set โ€” ambient agent is dormant. See docs/ambient-agent.md." - else - echo "enabled=true" >> "$GITHUB_OUTPUT" - fi - - - name: Find target PRs - id: targets - if: steps.cfg.outputs.enabled == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - if [ -n "${{ github.event.inputs.pr_number }}" ]; then - echo "prs=${{ github.event.inputs.pr_number }}" >> "$GITHUB_OUTPUT" - else - # All open PRs from auto-update branches. - PRS=$(gh pr list --state open --json number,headRefName \ - --jq '[.[] | select(.headRefName | startswith("nextjs-")) | .number] | join(" ")') - echo "prs=$PRS" >> "$GITHUB_OUTPUT" - fi - echo "Target PRs: $(grep prs= "$GITHUB_OUTPUT" | cut -d= -f2-)" - - - name: Setup pnpm - if: steps.targets.outputs.prs != '' - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - if: steps.targets.outputs.prs != '' - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "pnpm" - - - name: Install dependencies - if: steps.targets.outputs.prs != '' - run: pnpm install - - - name: Install Playwright browsers - if: steps.targets.outputs.prs != '' - run: pnpm --filter e2e-test-app exec playwright install --with-deps chromium - - - name: Run ambient agent - if: steps.targets.outputs.prs != '' - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - # An approval-capable token. GITHUB_TOKEN cannot approve PRs, so prefer a - # PAT in AGENT_GITHUB_TOKEN (or rely on the Claude GitHub App identity). - github_token: ${{ secrets.AGENT_GITHUB_TOKEN || github.token }} - claude_args: >- - --model claude-opus-4-8 - --allowedTools "Bash,Edit,Write,Read,Grep,Glob,WebFetch" - prompt: | - You are the release-quality agent for @mrjasonroy/cache-components-cache-handler, - a Next.js 16+ cache handler. A daily automation opens PRs from branches named - `nextjs-` that bump the package + example apps to a new Next.js version. - - Review these open auto-update PR(s): #${{ steps.targets.outputs.prs }} - - For EACH PR, work on its branch and do the following, in order: - - 1. Check out the PR branch (`gh pr checkout `). - 2. Read the diff and identify the new Next.js version being adopted. - 3. Fetch the Next.js release notes/changelog for that version (WebFetch - https://github.com/vercel/next.js/releases and the relevant tag) and note any - changes to caching, "use cache", cacheLife/cacheTag, revalidateTag/updateTag, - PPR, or the DataCacheHandler contract. - 4. Run the full local test suite: - - `pnpm install && pnpm build` - - `pnpm --filter @mrjasonroy/cache-components-cache-handler test` (unit) - - e2e against memory AND redis (redis is available at redis://localhost:6379): - `CACHE_HANDLER=redis REDIS_URL=redis://localhost:6379 pnpm --filter e2e-test-app build` - then `... pnpm --filter e2e-test-app test:e2e`. Repeat with the memory handler. - 5. If the new Next.js version introduces or changes cache-component behavior that - our suite does not cover, WRITE or adjust e2e tests (and unit tests where apt) - to cover it. Follow the existing test conventions (see tests/e2e/*.spec.ts and - the condition-based caching-wait helpers; isolate tags per test). Commit and - push the new tests to the PR branch with a clear message. - 6. Re-run the suite to confirm green. - - DECISION (be rigorous โ€” never approve on red): - - If everything passes and coverage is adequate: post a concise summary comment of - what changed and what you verified, then APPROVE the PR - (`gh pr review --approve`) and merge it (`gh pr merge --squash`). - - If there is a genuine failure, regression, or behavior change that needs a human: - post a detailed comment explaining exactly what broke (with logs), do NOT approve, - and leave the PR open. Add the label "needs-human" if it exists. - - Constraints: - - Only touch test files, the bumped version files, and lockfiles. Do not change - library source to force tests green โ€” a real regression must be surfaced, not hidden. - - Keep commits scoped and conventional. Run `pnpm format` and `pnpm lint` before committing. diff --git a/.github/workflows/nextjs-version-check.yml b/.github/workflows/nextjs-version-check.yml index 54e4f8f..aa0ab2d 100644 --- a/.github/workflows/nextjs-version-check.yml +++ b/.github/workflows/nextjs-version-check.yml @@ -32,6 +32,13 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + # Use the release PAT so the branch push + PR are authored by a real + # identity. A PR opened by GITHUB_TOKEN does NOT trigger CI (ci.yml's + # required checks never run) and a bot-merge does NOT fire the post-merge + # tag/publish chain โ€” both are GitHub's anti-recursion rule. Falls back to + # GITHUB_TOKEN when RELEASE_PAT is unset (PR opens, but you merge by hand). + token: ${{ secrets.RELEASE_PAT || github.token }} - name: Setup pnpm uses: pnpm/action-setup@v4 @@ -201,7 +208,8 @@ jobs: if: steps.check_version.outputs.needs_update == 'true' && steps.branch_check.outputs.branch_exists != 'true' id: create_pr env: - GH_TOKEN: ${{ github.token }} + # PAT so the PR triggers ci.yml (the required checks that gate the merge). + GH_TOKEN: ${{ secrets.RELEASE_PAT || github.token }} run: | MEMORY_STATUS="${{ steps.test_memory.outcome }}" REDIS_STATUS="${{ steps.test_redis.outcome }}" @@ -240,19 +248,26 @@ jobs: echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT echo "all_tests_passed=$ALL_TESTS_PASSED" >> $GITHUB_OUTPUT - - name: Hand off to the ambient agent + - name: Enable auto-merge if: steps.check_version.outputs.needs_update == 'true' && steps.branch_check.outputs.branch_exists != 'true' env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ secrets.RELEASE_PAT }} run: | - # The ambient agent (Claude Code, ambient-version-agent.yml) reviews this PR: - # runs the full suite, writes tests for any new cache behavior, then approves + - # merges if clean or flags it for a human. We dispatch it explicitly because a - # PR opened by GITHUB_TOKEN does not trigger pull_request workflows, and - # workflow_dispatch is exempt from that anti-recursion rule. - PR_NUMBER=$(gh pr view nextjs-${{ steps.check_version.outputs.latest_version }} --json number --jq .number) - if gh workflow run ambient-version-agent.yml -f pr_number="$PR_NUMBER"; then - echo "๐Ÿค– Dispatched ambient agent for PR #$PR_NUMBER" - else - echo "โš ๏ธ Could not dispatch the agent; its scheduled run will pick this PR up." + BRANCH="nextjs-${{ steps.check_version.outputs.latest_version }}" + + # No agent, no human: the PR ships itself once the required checks pass + # (lint-and-typecheck, unit-tests, and test-summary โ€” which is green only if + # the full e2e matrix passes: memory, redis, valkey, elasticache). The e2e + # suite IS the reviewer. + if [ -z "$GH_TOKEN" ]; then + echo "::warning::RELEASE_PAT not set โ€” PR opened, but auto-merge is off." + echo "Add a RELEASE_PAT secret (Contents + Pull requests: write) for" + echo "hands-off releases, or merge $BRANCH yourself once CI is green." + echo "See docs/auto-release.md." + exit 0 fi + + # Auto-merge must be enabled by the PAT (a real identity): a merge performed + # via GITHUB_TOKEN would not fire tag-on-version-merge.yml. + gh pr merge "$BRANCH" --squash --auto --delete-branch + echo "๐Ÿš€ Auto-merge enabled for $BRANCH โ€” it ships the moment CI is green." diff --git a/docs/ambient-agent.md b/docs/ambient-agent.md deleted file mode 100644 index e851df2..0000000 --- a/docs/ambient-agent.md +++ /dev/null @@ -1,67 +0,0 @@ -# Ambient Version Agent - -An AI agent (Claude Code) that turns the daily Next.js bump from a *deterministic -re-run of the existing tests* into an *actual review of the new version* โ€” -reading release notes, writing fresh e2e tests for new cache-component behavior, -and deciding whether to ship. - -## The autonomous loop - -``` -nextjs-version-check.yml (daily cron) - โ””โ”€ detects new Next.js โ†’ branch nextjs- โ†’ bump โ†’ quick e2e โ†’ opens PR - โ””โ”€ dispatches โ–ถ ambient-version-agent.yml - โ””โ”€ Claude Code, per PR: - 1. checks out the branch - 2. reads the Next.js release notes - 3. runs the full suite (unit + e2e: memory & redis) - 4. writes/adjusts e2e tests for new behavior, pushes them - 5. clean โ†’ comments summary, APPROVES, squash-merges - issue โ†’ comments findings, leaves PR open (no approval) - โ””โ”€ merge โ†’ tag-on-version-merge.yml โ†’ publish.yml (OIDC) - โ””โ”€ npm publish + GitHub release (contributors credited) -``` - -Everything downstream of the merge is already automated and tokenless (see -[publishing](./publishing/AUTOMATED_RELEASE.md)). The agent closes the one gap -that required a human: **judging and merging** the bump. - -## One-time setup - -1. **`ANTHROPIC_API_KEY`** repo secret โ€” the agent's model access. - `gh secret set ANTHROPIC_API_KEY` - -2. **An approval-capable identity.** GitHub forbids the default `GITHUB_TOKEN` - from *approving* pull requests, so the agent needs its own identity: - - **Recommended:** install the **Claude GitHub App** on this repo โ€” the action - then acts as `claude[bot]` and can approve/comment/merge. *or* - - Set **`AGENT_GITHUB_TOKEN`** to a PAT / fine-grained token (account other - than the PR author) with **Pull requests: read & write** and **Contents: - read & write**. The workflow uses it via the `github_token` input. - -3. Keep branch protection's **1 required approval** โ€” that's the gate the agent - satisfies. Required status checks still apply, so the agent's merge only goes - through once CI is also green. - -## Triggers - -- **Automatic:** `nextjs-version-check.yml` dispatches the agent for each new - bump PR. -- **Scheduled fallback:** runs daily (~02:00 UTC) and processes any open - `nextjs-*` PR that wasn't handled. -- **Manual:** `gh workflow run ambient-version-agent.yml -f pr_number=`. - -## Guardrails - -- The agent may only edit tests, version files, and lockfiles โ€” it must **not** - change library source to force tests green. A real regression is surfaced (PR - left open + comment), never hidden. -- It must not approve on red. -- Concurrency is limited per-PR so two runs don't fight. - -## Status - -v0 scaffold. Validate with a manual run against a real bump PR before relying on -it unattended, and tune the prompt/allowed-tools as you learn what it gets right. -The action version (`anthropics/claude-code-action@v1`) and model id may need -pinning to whatever is current. diff --git a/docs/auto-release.md b/docs/auto-release.md new file mode 100644 index 0000000..e447982 --- /dev/null +++ b/docs/auto-release.md @@ -0,0 +1,71 @@ +# Autonomous releases + +This package keeps itself current with Next.js with **no human and no paid +services**. The end-to-end loop: + +``` +nextjs-version-check.yml (daily cron) + detects a new Next.js โ†’ branch nextjs- โ†’ bump next + package version + โ†’ run e2e (memory + redis) โ†’ open a PR (as the RELEASE_PAT identity) + โ””โ”€ ci.yml runs the required checks on the PR: + lint-and-typecheck ยท unit-tests ยท test-summary + (test-summary is green only if the full e2e matrix passes: + memory, redis, valkey, elasticache) + โ””โ”€ auto-merge is enabled on the PR + checks green โ†’ it squash-merges itself โ†’ branch deleted + โ””โ”€ tag-on-version-merge.yml โ†’ tag v โ†’ dispatches publish.yml + โ””โ”€ npm publish (OIDC, tokenless) + GitHub release +``` + +**The e2e suite is the reviewer.** If the matrix is green the bump ships; if it +goes red the PR just sits open and waits for you. There is no AI in this loop โ€” +an earlier "ambient agent" design was removed because the test suite already +encodes the merge gate. + +## One-time setup: `RELEASE_PAT` + +GitHub deliberately stops the built-in `GITHUB_TOKEN` from driving a release: +a PR opened by `GITHUB_TOKEN` does **not** trigger CI, and a merge performed by +it does **not** fire the post-merge tag/publish chain (the anti-recursion rule). +So the bot needs to act as a real identity โ€” one free, long-lived token does it. + +1. Create a **fine-grained personal access token** + (GitHub โ†’ Settings โ†’ Developer settings โ†’ Fine-grained tokens): + - **Repository access:** only `cache-components-cache-handler` + - **Expiration:** the longest available (or no expiration) + - **Permissions:** **Contents: Read and write** + **Pull requests: Read and write** + (nothing else โ€” no admin, no workflows) + +2. Add it as a repo secret named **`RELEASE_PAT`**: + ```bash + gh secret set RELEASE_PAT + ``` + +That's it. It costs nothing and there is no second token to rotate (npm publish +uses OIDC trusted publishing โ€” see [publishing](./publishing/AUTOMATED_RELEASE.md)). + +### Without the PAT + +The loop degrades gracefully: `nextjs-version-check` still opens the bump PR, but +auto-merge stays off and the workflow logs a warning. Merge the PR yourself once +CI is green โ€” the tag and publish still fire automatically from your merge. + +## Branch protection + +`main` requires the status checks `lint-and-typecheck`, `unit-tests`, and +`test-summary`, and does **not** require an approving review โ€” the checks are the +gate, and no bot can give itself an approval. Required conversation resolution +stays on (only matters when a human leaves review comments). Auto-merge and +delete-branch-on-merge are enabled at the repo level. + +To re-add a human approval gate later: + +```bash +gh api -X PUT repos/{owner}/{repo}/branches/main/protection --input - <<'JSON' +{ "required_status_checks": { "strict": false, + "contexts": ["lint-and-typecheck", "unit-tests", "test-summary"] }, + "enforce_admins": false, + "required_pull_request_reviews": { "required_approving_review_count": 1 }, + "restrictions": null, "required_conversation_resolution": true } +JSON +```