Skip to content

The Dependency Rejuvenation aka Bump Versions #1

The Dependency Rejuvenation aka Bump Versions

The Dependency Rejuvenation aka Bump Versions #1

Workflow file for this run

name: The Dependency Rejuvenation aka Bump Versions
on:
workflow_dispatch:
inputs:
include_major:
description: 'Also attempt major bumps (monthly cadence forces this on the first Sunday)'
type: boolean
default: false
schedule:
# Every Sunday at 18:00 UTC. Minor + patch every week; major bumps
# stack onto the same run (and the same PR) on the first Sunday of the
# month, so we never open two competing deps PRs against one lockfile.
- cron: '0 18 * * 0'
permissions:
contents: read
jobs:
bump:
name: Refresh Dependency Pins
runs-on: ubuntu-latest
concurrency:
group: packages
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: write
timeout-minutes: 240
steps:
- name: Summoning the GitHub App Token
uses: actions/create-github-app-token@v2
id: generate-token
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
permission-issues: write
- name: Secure the Evidence aka Checkout Code
uses: actions/checkout@v4
with:
token: ${{ steps.generate-token.outputs.token }}
- name: Setup Deno Environment
uses: ./.github/actions/setup-deno
- name: Forge i18n Artifacts Once
working-directory: projects/client
run: deno task i18n:prepare
- name: Decide Major Cadence
id: cadence
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "major=${{ inputs.include_major }}" >> "$GITHUB_OUTPUT"
exit 0
fi
DAY=$(date -u +%d)
if [ "$DAY" -le 7 ]; then
echo "major=true" >> "$GITHUB_OUTPUT"
else
echo "major=false" >> "$GITHUB_OUTPUT"
fi
- name: Enumerate Minor Candidates
id: enum_minor
working-directory: projects/client
run: |
set -euo pipefail
deno run -A npm:npm-check-updates@latest \
-p npm \
--target minor \
--jsonUpgraded > /tmp/minors.json
echo "::group::minor candidates"
cat /tmp/minors.json
echo "::endgroup::"
COUNT=$(jq 'length' /tmp/minors.json)
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
- name: Bisect Minor Candidates
if: steps.enum_minor.outputs.count != '0'
env:
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
run: |
./.github/scripts/ncu-bisect.sh /tmp/minors.json
cp /tmp/passing.txt /tmp/passing-minor.txt
cp /tmp/failing.txt /tmp/failing-minor.txt
- name: Enumerate Major-Only Candidates
id: enum_major
if: steps.cadence.outputs.major == 'true'
working-directory: projects/client
run: |
set -euo pipefail
deno run -A npm:npm-check-updates@latest \
-p npm --target latest --jsonUpgraded > /tmp/all.json
deno run -A npm:npm-check-updates@latest \
-p npm --target minor --jsonUpgraded > /tmp/minor.json
# Majors = all-upgrades minus minor-upgrades (by key), computed
# against the already-minor-bumped tree so we never re-offer a pin
# the minor pass just moved.
jq -n \
--slurpfile all /tmp/all.json \
--slurpfile minor /tmp/minor.json \
'($all[0] // {}) as $a | ($minor[0] // {}) as $m
| $a | to_entries
| map(select($m[.key] == null))
| from_entries' > /tmp/majors.json
echo "::group::major candidates"
cat /tmp/majors.json
echo "::endgroup::"
COUNT=$(jq 'length' /tmp/majors.json)
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
- name: Bisect Major Candidates
if: steps.cadence.outputs.major == 'true' && steps.enum_major.outputs.count != '0'
env:
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
run: |
./.github/scripts/ncu-bisect.sh /tmp/majors.json
cp /tmp/passing.txt /tmp/passing-major.txt
cp /tmp/failing.txt /tmp/failing-major.txt
- name: Detect Adopted vs Reverted
id: detect
run: |
set -euo pipefail
if [ -s /tmp/passing-minor.txt ] || [ -s /tmp/passing-major.txt ]; then
echo "has_passing=true" >> "$GITHUB_OUTPUT"
else
echo "has_passing=false" >> "$GITHUB_OUTPUT"
fi
if [ -s /tmp/failing-major.txt ]; then
echo "has_failing_major=true" >> "$GITHUB_OUTPUT"
else
echo "has_failing_major=false" >> "$GITHUB_OUTPUT"
fi
if [ -s /tmp/passing-major.txt ]; then
echo "title=chore(deps): bump minor, patch & major versions" >> "$GITHUB_OUTPUT"
else
echo "title=chore(deps): bump minor & patch versions" >> "$GITHUB_OUTPUT"
fi
{
echo 'labels<<LABELS_EOF'
echo 'dependencies'
echo 'automated'
[ -s /tmp/passing-major.txt ] && echo 'major-bump'
echo 'LABELS_EOF'
} >> "$GITHUB_OUTPUT"
- name: Build PR Body
if: steps.detect.outputs.has_passing == 'true'
id: pr_body
run: |
{
echo "Automated dependency bump. Each candidate was tried independently with reinstall + typecheck + tests + minimal build (\`build:doctor\`). Failures were reverted automatically; only green bumps reach this PR."
echo ""
echo "## Minor & patch"
echo ""
if [ -s /tmp/passing-minor.txt ]; then
while IFS='=' read -r NAME VERSION; do
[ -z "$NAME" ] && continue
echo "- \`$NAME\` → \`$VERSION\`"
done < /tmp/passing-minor.txt
else
echo "_None adopted this run._"
fi
if [ -s /tmp/failing-minor.txt ]; then
echo ""
echo "### Reverted"
echo ""
while IFS='=' read -r NAME VERSION; do
[ -z "$NAME" ] && continue
echo "- \`$NAME\` → \`$VERSION\`"
done < /tmp/failing-minor.txt
echo ""
echo "_Reverted candidates stay on their current pin; investigate or pin them out before the next run._"
fi
if [ -s /tmp/passing-major.txt ] || [ -s /tmp/failing-major.txt ]; then
echo ""
echo "## Major"
echo ""
if [ -s /tmp/passing-major.txt ]; then
while IFS='=' read -r NAME VERSION; do
[ -z "$NAME" ] && continue
echo "- \`$NAME\` → \`$VERSION\`"
done < /tmp/passing-major.txt
else
echo "_None adopted this run._"
fi
if [ -s /tmp/failing-major.txt ]; then
echo ""
echo "### Skipped (tracked in companion issue)"
echo ""
while IFS='=' read -r NAME VERSION; do
[ -z "$NAME" ] && continue
echo "- \`$NAME\` → \`$VERSION\`"
done < /tmp/failing-major.txt
fi
fi
} > /tmp/pr-body.md
{
echo 'body<<PRBODY_EOF'
cat /tmp/pr-body.md
echo 'PRBODY_EOF'
} >> "$GITHUB_OUTPUT"
- name: Forge the Pull Request
if: steps.detect.outputs.has_passing == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.generate-token.outputs.token }}
branch: chore/deps
delete-branch: true
base: main
commit-message: ${{ steps.detect.outputs.title }}
title: ${{ steps.detect.outputs.title }}
body: ${{ steps.pr_body.outputs.body }}
labels: ${{ steps.detect.outputs.labels }}
reviewers: |
seferturan
vladjerca
assignees: |
seferturan
- name: Open Issue for Reverted Majors
if: steps.detect.outputs.has_failing_major == 'true'
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
set -euo pipefail
{
echo "Automated major-bump run could not promote the following packages. Each was attempted in isolation against a clean baseline and reverted on failure."
echo ""
echo "## Reverted majors"
echo ""
while IFS='=' read -r NAME VERSION; do
[ -z "$NAME" ] && continue
echo "### \`$NAME\` → \`$VERSION\`"
echo ""
echo "<details><summary>Last 80 log lines</summary>"
echo ""
echo '```'
tail -n 80 "/tmp/bisect-logs/rejected-${NAME//\//__}.log" 2>/dev/null || echo "(log missing)"
echo '```'
echo "</details>"
echo ""
done < /tmp/failing-major.txt
echo ""
echo "_Triage path:_ bump locally, investigate the failure, either land a fix or pin the package with a reason."
echo ""
echo "Opened by \`packages.yml\` run \`${{ github.run_id }}\`."
} > /tmp/issue-body.md
DATE=$(date -u +%Y-%m-%d)
gh issue create \
--repo "${{ github.repository }}" \
--title "chore(deps): major bumps that need manual triage ($DATE)" \
--body-file /tmp/issue-body.md \
--assignee seferturan \
--label dependencies \
--label automated \
--label major-bump