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
107 changes: 26 additions & 81 deletions .github/workflows/autoupdate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,17 @@ jobs:
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: |
- uses: actions/checkout@v5
with: { ref: main, token: "${{ secrets.GITHUB_TOKEN }}", fetch-depth: 0 }
- run: |
git config user.email "siarhei@dudko.dev"
git config user.name "Siarhei Dudko"

- name: Create autoupdate branch
run: |
- 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
- uses: actions/setup-node@v6
with: { node-version: 24 }
- id: autoupdate
continue-on-error: true
uses: siarheidudko/autoupdater@v6
with:
Expand All @@ -59,18 +46,14 @@ jobs:
debug: "true"
ignore-packages: |
@types/node

- name: Persist autoupdater work on failure
if: steps.autoupdate.outcome == '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'
- if: steps.autoupdate.outcome == 'failure'
run: |
git fetch origin main
if git diff --quiet origin/main; then
Expand All @@ -82,16 +65,12 @@ jobs:
git push origin "HEAD:$AUTOUPDATE_BRANCH"
fi
fi

- name: Ensure labels exist
env:
- 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
- id: diff
if: always()
run: |
git fetch origin main "$AUTOUPDATE_BRANCH"
Expand All @@ -100,20 +79,13 @@ jobs:
else
echo "has_diff=true" >> "$GITHUB_OUTPUT"
fi

- name: Read package version from branch
id: pkg
- 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'
- 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: |
Expand All @@ -126,15 +98,9 @@ jobs:
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")
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
- id: pr_failure
if: steps.autoupdate.outcome == 'failure' && steps.diff.outputs.has_diff == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -153,37 +119,21 @@ jobs:
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.
See **Actions → Claude** for progress.
EOF
)
PR_URL=$(gh pr create \
--base main \
--head "$AUTOUPDATE_BRANCH" \
--title "chore(deps): autoupdate (needs claude fix)" \
--body "$BODY" \
--draft)
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 != ''
- 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 != ''
run: gh workflow run test.yml --ref "$AUTOUPDATE_BRANCH" || true
- 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 != ''
run: gh workflow run claude.yml --ref main -f branch="$AUTOUPDATE_BRANCH" -f run_url="$RUN_URL"
- if: steps.pr_failure.outputs.pr_url != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ steps.pr_failure.outputs.pr_url }}
Expand All @@ -196,9 +146,7 @@ jobs:
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 != ''
- 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 }}
Expand All @@ -208,8 +156,5 @@ jobs:
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
- if: always() && steps.diff.outputs.has_diff != 'true'
run: git push origin --delete "$AUTOUPDATE_BRANCH" || true
28 changes: 8 additions & 20 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,8 @@ on:
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
branch: { required: true, type: string }
run_url: { 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
Expand All @@ -38,14 +32,11 @@ jobs:
id-token: write
actions: read
steps:
- name: Checkout repo
uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch || github.ref }}
fetch-depth: 1

- name: Prepare autoupdate-fix prompt
if: github.event_name == 'workflow_dispatch'
- if: github.event_name == 'workflow_dispatch'
id: prep
env:
BRANCH: ${{ github.event.inputs.branch }}
Expand All @@ -72,21 +63,18 @@ jobs:
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.
- Limit edits to compatibility shims.
- 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.
- When all are green, push and stop.

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

- name: Run Claude Code
uses: anthropics/claude-code-action@v1
- uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
allowed_bots: "*"
Expand Down
76 changes: 8 additions & 68 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,63 +1,37 @@
name: Release

# Triggered after CI completes on main. The workflow_run trigger guarantees
# we only publish commits that already passed tests on main, while keeping
# release as a separate workflow file so it can be wired to the npm trusted
# publisher (Settings -> Packages -> Trusted Publishers): point npm at this
# workflow filename (`release.yml`) and the `release` environment below.
on:
workflow_run:
workflows: [CI]
branches: [main]
types: [completed]
# Manual trigger - publish from a specific tag, branch or commit SHA.
# Leave `ref` empty to publish from main HEAD.
workflow_dispatch:
inputs:
ref:
description: 'Tag, branch or commit SHA to publish (e.g. v0.1.0). Empty = main HEAD.'
type: string
required: false
default: ''

permissions:
# Needed to push the version tag back to the repository.
contents: write
# Required for npm provenance + npm trusted-publisher OIDC exchange.
id-token: write

jobs:
release:
name: Publish to npm
runs-on: ubuntu-latest
# Skip when CI failed; workflow_dispatch has no workflow_run context, so
# the second clause lets manual runs through.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
environment: release
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
# Resolution order:
# 1. workflow_dispatch input (manual republish at a specific ref)
# 2. workflow_run head SHA (the commit that just passed CI)
# 3. github.ref (defensive default; should not be hit)
ref: ${{ inputs.ref != '' && inputs.ref || github.event.workflow_run.head_sha || github.ref }}
# Full history so `git push origin <tag>` succeeds.
fetch-depth: 0

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

- name: Install dependencies
run: npm ci

- name: Resolve package metadata
id: meta
node-version: '24'
- run: npm ci
- id: meta
run: |
set -euo pipefail
NAME=$(node -p "require('./package.json').name")
Expand All @@ -68,9 +42,7 @@ jobs:
echo "version=$VERSION"
echo "tag=$TAG"
} >>"$GITHUB_OUTPUT"

- name: Skip if version already on npm
id: check
- id: check
run: |
set -euo pipefail
if npm view "${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }}" version >/dev/null 2>&1; then
Expand All @@ -80,63 +52,31 @@ jobs:
echo "publish=true" >>"$GITHUB_OUTPUT"
echo "::notice::Will publish ${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }}."
fi

# `npm publish` runs `prepublishOnly` (typecheck && test && build), so
# we don't pre-build here.
#
# We publish via `npx -y npm@11.5.1` rather than the system npm:
# Trusted Publishing requires npm >= 11.5.1, and Node 22 LTS still
# bundles npm 10.x. corepack-based shims have proven flaky on hosted
# runners (the activate step doesn't always land first in PATH), so
# we pin npm explicitly for this single command.
#
# Auth strategy:
# - Preferred: npm Trusted Publishing (OIDC). No secret needed; npm
# verifies this workflow + environment against the package's
# Trusted Publisher config on npmjs.com.
# - Bootstrap / fallback: NPM_TOKEN repo secret. Used automatically
# when present (NODE_AUTH_TOKEN empty -> OIDC is attempted;
# non-empty -> classic token wins). Set NPM_TOKEN as a Granular
# Access Token if TP is not yet configured.
- name: Publish to npm
if: steps.check.outputs.publish == 'true'
- if: steps.check.outputs.publish == 'true'
run: |
set -euo pipefail
echo "Publishing with:"
npx -y npm@11.5.1 --version
npx -y npm@11.5.1 publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create git tag and GitHub release
if: steps.check.outputs.publish == 'true'
- if: steps.check.outputs.publish == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.meta.outputs.tag }}"

# Idempotency: a previous publish that succeeded on npm but failed
# to push the tag (or a manual republish at the same ref) should
# not crash the job - just reuse the existing tag/release.
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::notice::GitHub release $TAG already exists; skipping tag/release creation."
exit 0
fi

git config user.name "Siarhei Dudko"
git config user.email "siarhei@dudko.dev"

if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::notice::Git tag $TAG already exists locally; reusing without re-tagging."
else
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
fi

# Auto-generated notes diff against the previous release tag - on
# the first run for a repo there's no prior tag, so gh falls back
# to the full commit history; both cases are handled internally.
gh release create "$TAG" \
--title "$TAG" \
--generate-notes \
Expand Down
Loading