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
215 changes: 215 additions & 0 deletions .github/workflows/autoupdate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
name: Autoupdate
on:
schedule:
- cron: "0 6 * * 1"
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:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repo
uses: actions/checkout@v4
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@v4
with:
node-version: 24

- name: Autoupdate
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: 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: Check for diff vs main on remote branch
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 <<EOF
Automated dependency update.

- autoupdater outcome: success
- target version: v${{ steps.pkg.outputs.version }}

PR-checks (CI workflow) must be green before merge.
EOF
)
PR_URL=$(gh pr create \
--base main \
--head "$AUTOUPDATE_BRANCH" \
--title "chore(deps): autoupdate v${{ steps.pkg.outputs.version }}" \
--body "$BODY")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"

- name: Open draft 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 <<EOF
The dependency autoupdater failed during \`builds-and-checks\`.

Run log: $RUN_URL

A Claude session has been dispatched to push fixes onto this branch
so the following commands all exit 0:

npm run typecheck
npm run format:check
npm run build
npm test

See **Actions → Claude** for progress. The CI workflow will
re-run on each new commit to confirm a green state before merge.
EOF
)
PR_URL=$(gh pr create \
--base main \
--head "$AUTOUPDATE_BRANCH" \
--title "chore(deps): autoupdate (needs claude fix)" \
--body "$BODY" \
--draft)
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"

- name: Trigger CI on autoupdate branch (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 test.yml --ref "$AUTOUPDATE_BRANCH" || true

- name: Dispatch Claude to fix the failed autoupdate
if: steps.pr_failure.outputs.pr_url != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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 run_url="$RUN_URL"

- name: Post info comment on the failure PR
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: |
COMMENT=$(cat <<EOF
Claude has been dispatched to fix this PR. See [Actions → Claude](${{ github.server_url }}/${{ github.repository }}/actions/workflows/claude.yml) for progress.

Failing autoupdate run: $RUN_URL
EOF
)
gh pr comment "$PR_URL" --body "$COMMENT"

- 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
94 changes: 94 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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
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:
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: read
steps:
- name: Checkout repo
uses: actions/checkout@v4
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 }}
RUN_URL: ${{ github.event.inputs.run_url }}
run: |
{
echo 'prompt<<PROMPT_EOF'
cat <<EOF
The dependency autoupdater failed on branch \`$BRANCH\` (run: $RUN_URL).

Push commits to this branch until the following exit
with code 0 in your local working tree, observed via the Bash tool —
not inferred:

npm run typecheck
npm run format:check
npm run build
npm test

(This repo uses prettier + tsc — there is no separate \`lint\` script.)

Hard rules:
- Run those commands yourself before every push. Do not push
if any of them is red.
- If \`npm install\` or \`npm ci\` is needed, run it first with
\`--no-audit --no-fund\` and confirm exit 0.
- Limit edits to compatibility shims (types, renamed exports,
breaking-change adjustments). Do NOT change product logic.
- Do NOT bump the package version.
- Mind the existing CLAUDE.md guardrails: Zod \`z.ZodType\`
collection trick, no \`.max()\` on Zod arrays in Anthropic
structured output.
- When all are green, push and stop. CI will re-verify on the PR.

See CLAUDE.md in the repo root for the full project conventions.
EOF
echo PROMPT_EOF
} >> "$GITHUB_OUTPUT"

- name: Run Claude Code
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ steps.prep.outputs.prompt }}
claude_args: |
--allowedTools "Edit,Write,MultiEdit,Bash(npm:*),Bash(npx:*),Bash(node:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(git fetch:*),Bash(git restore:*),Bash(git checkout:*),Bash(rm:*),Bash(mkdir:*),Bash(cat:*),Bash(ls:*)"
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: '22'

Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@ on:

jobs:
test:
name: Test
name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
node-version: ['22', '24']
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
node-version: ${{ matrix.node-version }}
cache: npm

- name: Install dependencies
Expand Down
30 changes: 29 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,40 @@ After **any** change to `.ts` files in `src/` or `tests/`, run:

```bash
npm run typecheck
npm run format:check
npm run build
npm test
```

Do not consider a task done until `tsc --noEmit` exits cleanly. If the change touches runtime behavior, also run `npm test`.
Do not consider a task done until all four exit cleanly. Run them with the `Bash` tool, do not infer success from "the change looks right".

If `npm install` is needed (e.g. lockfile changed), run it with `--no-audit --no-fund` and ensure it returned 0 before running checks.

## Conventions

- Use `.ts` extensions in relative imports (project relies on `--experimental-strip-types`).
- Zod v4 is used; when passing heterogeneous schemas through a shared array/iterable, type the collection as `z.ZodType` to avoid union-narrowing errors.
- Anthropic's native structured output rejects `maxItems` on arrays — never add `.max()` to Zod arrays that flow into structured output. The guard test in `tests/anthropic-schema-compat.test.ts` enforces this.

## Boundaries

- Do not bump the package `version` manually. Versioning is handled by the autoupdate flow / maintainer on release.
- Do not edit `.github/workflows/release.yml` unless explicitly asked — it is the npm-trusted-publisher release pipeline.
- Do not push to `main` directly. Always work on the existing branch you were summoned to.

## When you are working on an autoupdate PR

- Branch will be `chore/autoupdate-<run_id>`.
- Goal: bring `npm run typecheck && npm run format:check && npm run build && npm test` to green.
- Push compatibility fixes onto this branch. Each push re-runs the `CI` workflow automatically once the PR is open.
- If a fix is impossible without changing product behavior, stop and leave a comment explaining what's blocked rather than guessing.

## CI quirks specific to this repo

This repo follows the unified `autoupdate-with-claude` baseline (same template across siblings). Several workarounds are intentional:
- `autoupdate.yml` uses `GITHUB_TOKEN` and explicitly dispatches `test.yml` (the `CI` workflow) after PR creation, because events created via `GITHUB_TOKEN` don't trigger `pull_request` workflows.
- `autoupdate.yml` dispatches `claude.yml` directly via `workflow_dispatch` instead of relying on an `@claude` PR comment.
- Releases stay wired through `release.yml` (npm Trusted Publisher), which fires via `workflow_run` after a successful CI on `main`. There is no `release-on-version-bump.yml` here — it would conflict with the existing tag/publish chain.
- All actions pinned to the `@v4` line because the runner image currently lacks `externals/node24`, breaking post-cleanup of `@v5/@v6` actions.

Do **not** "fix" any of the above by replacing dispatch calls with comment-based mentions, or by bumping action versions back to `@v5/@v6`.
Loading