From dc846edd045893e7e60ee0405db1fb32bcdd0f97 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 08:28:16 +0000 Subject: [PATCH] feat: onboard agent-web and agent-web-react to the autoupdate flow Both publish only when someone bumps package.json by hand. Their release.yml is fine -- it fires on workflow_run after CI on main and its history shows that trigger working. What they lack is the flow that produces a version bump in the first place: neither has autoupdate.yml or claude.yml, and neither was in the template system at all. Adds configs for both, so they get the same weekly dependency update, patch bump, Claude compatibility fix and gated auto-merge as the other nine. Their existing release.yml then publishes on its own. Two mechanics were needed to fit repos that already own their CI: - SKIP_PR_CHECKS: no pr-checks.yml is generated for them. The existing test.yml (agent-web) and ci.yml (agent-web-react) stay authoritative. - The autoupdate "Trigger PR checks" step no longer hardcodes pr-checks.yml; it renders from `ci_workflows`, the key claude.yml already used. As a side effect `agent` now dispatches both of its gates (pr-checks.yml and test.yml) rather than only the first. Both are also in SKIP_RELEASE_ON_BUMP: they release through their own release.yml on CI success, not through a version-bump tag chain, so no release-on-version-bump.yml and no tag/deploy step is generated. Checks run before any publish, twice over: the merge gate requires every check run on the PR to be green, and release.yml then runs only on `workflow_run` of CI with conclusion == 'success'. Both repos' CI is a real suite -- typecheck, format:check, build, test, plus the demo build for agent-web-react. The other eight repos render byte-identical. --- .github/workflows/autoupdate.yml | 248 +++++++++++++++++++++++++++++++ .github/workflows/claude.yml | 158 ++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 .github/workflows/autoupdate.yml create mode 100644 .github/workflows/claude.yml diff --git a/.github/workflows/autoupdate.yml b/.github/workflows/autoupdate.yml new file mode 100644 index 0000000..e12e787 --- /dev/null +++ b/.github/workflows/autoupdate.yml @@ -0,0 +1,248 @@ +name: Autoupdate +on: + schedule: + - cron: "0 23 * * 5" + workflow_dispatch: + workflow_call: +concurrency: + group: "${{ github.workflow }} @ ${{ github.ref }}" + cancel-in-progress: false +permissions: + contents: write + actions: write + pull-requests: write + issues: write +env: + AUTOUPDATE_BRANCH: chore/autoupdate-${{ github.run_id }} +jobs: + update: + name: Autoupdate dependencies + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + - name: Configure git + run: | + git config user.email "siarhei@dudko.dev" + git config user.name "Siarhei Dudko" + - name: Create autoupdate branch + run: | + git checkout -b "$AUTOUPDATE_BRANCH" + git push -u origin "$AUTOUPDATE_BRANCH" + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Run autoupdater + id: autoupdate + continue-on-error: true + uses: siarheidudko/autoupdater@v6 + with: + author-email: "siarhei@dudko.dev" + author-name: "Siarhei Dudko" + working-directory: ${{ github.workspace }} + ref: ${{ github.repository }} + branch: ${{ env.AUTOUPDATE_BRANCH }} + builds-and-checks: | + npm run typecheck + npm run format:check + npm run build + npm test + debug: "true" + ignore-packages: | + @types/node + - name: Persist autoupdater work on failure + if: steps.autoupdate.outcome == 'failure' + run: | + if [ -n "$(git status --porcelain)" ]; then + git add -A + git commit -m "chore(deps): autoupdater partial update" + fi + git push --force-with-lease origin "HEAD:$AUTOUPDATE_BRANCH" || true + - name: Fallback baseline update if branch still empty vs main + if: steps.autoupdate.outcome == 'failure' + run: | + git fetch origin main + if git diff --quiet origin/main; then + npx --yes npm-check-updates -u || true + npm install --no-audit --no-fund || true + if [ -n "$(git status --porcelain)" ]; then + git add -A + git commit -m "chore(deps): npm-check-updates baseline (autoupdater fallback)" + git push origin "HEAD:$AUTOUPDATE_BRANCH" + fi + fi + - name: Bump patch version on the failure path + if: steps.autoupdate.outcome == 'failure' + run: | + # The autoupdater bumps the version itself, but only on the success + # path. Neither the persist step nor the npm-check-updates fallback + # touches it, so without this the dependency update merges into the + # default branch, release-on-version-bump sees no version change and + # never fires, and the update is never published. + git fetch origin main + git show origin/main:package.json > /tmp/base-package.json + BASE=$(node -p "require('/tmp/base-package.json').version") + CURRENT=$(node -p "require('$PWD/package.json').version") + if [ "$BASE" != "$CURRENT" ]; then + echo "version already bumped: $BASE -> $CURRENT" + else + npm --no-git-tag-version --ignore-scripts version patch + NEW=$(node -p "require('$PWD/package.json').version") + git add package.json package-lock.json + git commit -m "chore(release): v$NEW" + git push origin "HEAD:$AUTOUPDATE_BRANCH" + echo "bumped $BASE -> $NEW" + fi + - name: Ensure labels exist + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh label create autoupdate --color "0e8a16" --description "Automated dependency update PRs" --force || true + gh label create needs-claude --color "d4c5f9" --description "Needs Claude GitHub App to fix" --force || true + - name: Detect diff vs main + id: diff + if: always() + run: | + git fetch origin main "$AUTOUPDATE_BRANCH" + if git diff --quiet "origin/main" "origin/$AUTOUPDATE_BRANCH"; then + echo "has_diff=false" >> "$GITHUB_OUTPUT" + else + echo "has_diff=true" >> "$GITHUB_OUTPUT" + fi + - name: Read package version from branch + id: pkg + if: steps.diff.outputs.has_diff == 'true' + run: | + VERSION=$(git show "origin/$AUTOUPDATE_BRANCH:package.json" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>console.log(JSON.parse(s).version))") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Open PR (autoupdater succeeded) + id: pr_success + if: | + steps.autoupdate.outcome == 'success' && + steps.autoupdate.outputs.updated == 'true' && + steps.diff.outputs.has_diff == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BODY=$(cat <> "$GITHUB_OUTPUT" + - name: Open PR (autoupdater failed) + id: pr_failure + if: steps.autoupdate.outcome == 'failure' && steps.diff.outputs.has_diff == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + BODY=$(cat <> "$GITHUB_OUTPUT" + - name: Trigger PR checks (GITHUB_TOKEN can't auto-trigger pull_request) + if: steps.pr_success.outputs.pr_url != '' || steps.pr_failure.outputs.pr_url != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh workflow run ci.yml --ref "$AUTOUPDATE_BRANCH" || true + - name: Wait for PR checks and merge (autoupdater succeeded) + id: merge_pr + if: steps.pr_success.outputs.pr_url != '' + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ steps.pr_success.outputs.pr_url }} + run: | + # Poll the check-runs API instead of `gh pr checks --watch`. + # The pull_request run GitHub queues for a bot-authored PR is gated on + # manual approval and sits in `action_required` forever. It surfaces in + # the status rollup that `gh pr checks` reads, so `--watch` waited on a + # check that can never complete and this step hung until the job timed + # out, never reaching `gh pr merge`. Gated runs create no check runs, so + # this view sees only jobs that actually execute -- among them the run + # the previous step dispatched explicitly. + SHA=$(gh pr view "$PR_URL" --json headRefOid --jq .headRefOid) + for i in $(seq 1 40); do + RUNS=$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/check-runs" --jq .check_runs) + TOTAL=$(jq length <<<"$RUNS") + PENDING=$(jq '[.[] | select(.status != "completed")] | length' <<<"$RUNS") + [ "$TOTAL" -gt 0 ] && [ "$PENDING" -eq 0 ] && break + sleep 30 + done + FAILED=$(jq '[.[] | select(.conclusion != null and (.conclusion | IN("success","neutral","skipped") | not))] | length' <<<"$RUNS") + echo "checks: total=$TOTAL pending=$PENDING failed=$FAILED" + if [ "$TOTAL" -eq 0 ] || [ "$PENDING" -ne 0 ] || [ "$FAILED" -ne 0 ]; then + echo "not merging: checks are not green" + exit 1 + fi + gh pr merge "$PR_URL" --squash --delete-branch + + - name: Dispatch Claude to fix the failed autoupdate + if: steps.pr_failure.outputs.pr_url != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ steps.pr_failure.outputs.pr_url }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + gh workflow run claude.yml --ref main \ + -f branch="$AUTOUPDATE_BRANCH" \ + -f pr_url="$PR_URL" \ + -f run_url="$RUN_URL" + - name: Add labels to PR + if: steps.pr_success.outputs.pr_url != '' || steps.pr_failure.outputs.pr_url != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ steps.pr_success.outputs.pr_url || steps.pr_failure.outputs.pr_url }} + run: | + if [ "${{ steps.pr_failure.outputs.pr_url }}" != "" ]; then + gh pr edit "$PR_URL" --add-label autoupdate --add-label needs-claude || true + else + gh pr edit "$PR_URL" --add-label autoupdate || true + fi + - name: Cleanup branch when nothing to ship + if: always() && steps.diff.outputs.has_diff != 'true' + run: | + git push origin --delete "$AUTOUPDATE_BRANCH" || true diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 0000000..246d61e --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,158 @@ +name: Claude +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] + issues: + types: [opened, assigned] + workflow_dispatch: + inputs: + branch: + description: "Branch Claude should operate on (used by autoupdate flow)" + required: true + type: string + pr_url: + description: "PR URL where Claude should post a status comment" + required: false + type: string + run_url: + description: "URL of the failing autoupdate run, included in the prompt for context" + required: false + type: string +concurrency: + group: "${{ github.workflow }} @ ${{ github.event.issue.number || github.event.pull_request.number || github.event.inputs.branch }}" + cancel-in-progress: false +jobs: + claude: + name: Run Claude + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + issues: write + id-token: write + actions: write + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.branch || github.ref }} + fetch-depth: 1 + - name: Prepare autoupdate-fix prompt + if: github.event_name == 'workflow_dispatch' + id: prep + env: + BRANCH: ${{ github.event.inputs.branch }} + PR_URL: ${{ github.event.inputs.pr_url }} + RUN_URL: ${{ github.event.inputs.run_url }} + run: | + { + echo 'prompt<" + + The comment must state plainly: + - Which command(s) failed initially (or "all green on first run"). + - What changes you made (or "no fix needed"). + - Whether you pushed any commits, and the SHA(s) if so. + + Do NOT exit without posting this comment. The maintainer relies + on it to know what happened without reading the action log. + + See CLAUDE.md in the repo root for the full project conventions. + EOF + echo PROMPT_EOF + } >> "$GITHUB_OUTPUT" + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + allowed_bots: "*" + prompt: ${{ steps.prep.outputs.prompt }} + claude_args: | + --allowedTools "Edit,Write,MultiEdit,Bash(git:*),Bash(gh:*),Bash(npm:*),Bash(npx:*),Bash(node:*),Bash(rm:*),Bash(mkdir:*),Bash(cat:*),Bash(ls:*),Bash(echo:*),Bash(grep:*),Bash(find:*),Bash(sed:*),Bash(awk:*),Bash(head:*),Bash(tail:*),Bash(diff:*),Bash(mv:*),Bash(cp:*),Bash(touch:*)" + # Claude pushes with GITHUB_TOKEN, so its push fires no `push` run and the + # `pull_request` run GitHub queues for it stays stuck in `action_required`. + # Dispatch the checks explicitly — `workflow_dispatch` is the only trigger + # that is not gated. Runs even if the step above failed, so the PR ends up + # with a red check instead of no checks at all. + - name: Re-dispatch PR checks on the branch Claude pushed to + if: always() && github.event_name == 'workflow_dispatch' && github.event.inputs.branch != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ github.event.inputs.branch }} + run: | + git fetch origin "$BRANCH" + echo "Dispatching checks for $BRANCH @ $(git rev-parse --short "origin/$BRANCH")" + gh workflow run ci.yml --ref "$BRANCH" || true + # Without this the autoupdate flow has no ending: the success path merges + # itself, but a PR that needed a Claude fix stayed open forever even once + # its checks were green, so these PRs just piled up week after week. + - name: Merge once the dispatched checks are green + if: always() && github.event_name == 'workflow_dispatch' && github.event.inputs.pr_url != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.inputs.pr_url }} + run: | + # Poll check-runs, not `gh pr checks`: the approval-gated pull_request + # run shows up as a permanently-pending check in the status rollup but + # produces no check runs, so this view sees only jobs that really ran. + SHA=$(gh pr view "$PR_URL" --json headRefOid --jq .headRefOid) + for i in $(seq 1 40); do + RUNS=$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/check-runs" --jq .check_runs) + TOTAL=$(jq length <<<"$RUNS") + PENDING=$(jq '[.[] | select(.status != "completed")] | length' <<<"$RUNS") + [ "$TOTAL" -gt 0 ] && [ "$PENDING" -eq 0 ] && break + sleep 30 + done + FAILED=$(jq '[.[] | select(.conclusion != null and (.conclusion | IN("success","neutral","skipped") | not))] | length' <<<"$RUNS") + echo "checks: total=$TOTAL pending=$PENDING failed=$FAILED" + if [ "$TOTAL" -eq 0 ] || [ "$PENDING" -ne 0 ] || [ "$FAILED" -ne 0 ]; then + echo "leaving the PR open for review: checks are not green" + exit 0 + fi + gh pr merge "$PR_URL" --squash --delete-branch