The Dependency Rejuvenation aka Bump Versions #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |