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
248 changes: 248 additions & 0 deletions .github/workflows/autoupdate.yml
Original file line number Diff line number Diff line change
@@ -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 <<EOF
Automated dependency update.

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

PR-checks 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" \
--assignee siarheidudko \
--reviewer siarheidudko)
echo "pr_url=$PR_URL" >> "$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 <<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

Claude will leave a status comment on this PR when it finishes.
The Claude workflow re-dispatches PR-checks once it is done, so the
checks below reflect its pushed fixes.
EOF
)
PR_URL=$(gh pr create \
--base main \
--head "$AUTOUPDATE_BRANCH" \
--title "chore(deps): autoupdate (needs claude fix)" \
--body "$BODY" \
--assignee siarheidudko \
--reviewer siarheidudko)
echo "pr_url=$PR_URL" >> "$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
Loading
Loading