From c1823a8bde70fc0b4ed57a76ac0dd83564d41b57 Mon Sep 17 00:00:00 2001 From: countgitmick Date: Sun, 2 Aug 2026 06:17:06 -0700 Subject: [PATCH 1/2] fix(ci): stop the auto-heal loop stranding every npm Dependabot PR The heal commit that recomputes the fetchYarnDeps hash caused three separate failures, and together they deadlocked PRs #227 to #235. Dependabot stops rebasing a PR as soon as a third party pushes a commit to it. The heal commit is that push. Once #228 merged, the seven remaining npm PRs conflicted and nothing ever cleared them. The heal commit message now carries [dependabot skip], the documented marker that lets Dependabot force-push a rebase over it. Every npm PR rewrites yarn.lock and the same fetchYarnDeps hash line in flake.nix, so two open npm PRs always conflict. A catch-all group puts every npm version update in one PR and removes the race. Security updates keep their own PRs, because the group is scoped to version-updates. A push made with the default GITHUB_TOKEN never starts another workflow run, so the healed head commit carried zero checks. The checkout and merge steps now read AUTOHEAL_TOKEN and fall back to GITHUB_TOKEN when it is unset. The header documents that it has to be a Dependabot secret. Drop the gh pr merge --auto fallback. GitHub refuses to arm auto-merge on a branch with no merge requirement, and development has neither a required check nor a required review, so the call always failed the job. A conflict is now logged and passes, because Dependabot rebases it. --- .github/dependabot.yml | 50 +++++++---------- .../workflows/dependabot-npm-automerge.yml | 54 ++++++++++++++++--- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index bd5b56bf0..b3214952a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,38 +9,26 @@ updates: - "PR: dependencies" open-pull-requests-limit: 20 groups: - babel: + # One PR for every npm version update, not one per package or per tool + # family. + # + # Every npm PR rewrites yarn.lock and the single fetchYarnDeps `hash =` + # line in flake.nix that dependabot-npm-automerge.yml heals. Two open npm + # PRs therefore always conflict on both files. Only the first one can + # merge, and every sibling goes DIRTY the moment it lands. That is what + # stalled PRs #227 through #235 on 2026-08-01: #228 merged, and the other + # seven conflicted inside the same minute. + # + # Grouping removes the race. It also cuts the cost from one full nix + # build per package to one per week. + # + # applies-to defaults to version-updates, and is stated here to make the + # scope explicit: security updates still arrive as their own PRs, and + # this group never holds one back. + npm: + applies-to: version-updates patterns: - - "@babel/*" - - "babel-*" - eslint: - patterns: - - "eslint" - - "eslint-*" - - "@eslint/*" - - "vue-eslint-parser" - - "neostandard" - - "@intlify/eslint-plugin-vue-i18n" - - "@stylistic/eslint-plugin" - stylelint: - patterns: - - "stylelint" - - "stylelint-*" - - "postcss" - - "postcss-*" - - "@double-great/stylelint-a11y" - fortawesome: - patterns: - - "@fortawesome/*" - webpack: - patterns: - - "css-loader" - - "mini-css-extract-plugin" - - "sass" - - "sass-loader" - - "webpack" - - "webpack-*" - - "*-webpack-plugin" + - "*" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/dependabot-npm-automerge.yml b/.github/workflows/dependabot-npm-automerge.yml index 7833b21e8..cc11619ea 100644 --- a/.github/workflows/dependabot-npm-automerge.yml +++ b/.github/workflows/dependabot-npm-automerge.yml @@ -4,6 +4,23 @@ name: Auto-heal and merge Dependabot npm PRs # fails Nix Build and never merges. This workflow heals the hash on the PR # branch, proves the build is green, and auto-merges patch/minor updates. # Major updates are healed (so they are green) but left for manual review. +# +# AUTOHEAL_TOKEN (optional, but strongly recommended) +# +# A push or a merge made with the default GITHUB_TOKEN never starts another +# workflow run. GitHub does this to stop recursive runs, and the exception list +# holds only workflow_dispatch and repository_dispatch. The healed head commit +# therefore carries zero checks, and the branch reads as untested even though +# the heal step below ran a full nix build on it. +# +# Set AUTOHEAL_TOKEN to a GitHub App installation token or a PAT with `repo` +# scope to restore the checks. Store it as a **Dependabot** secret, under +# Settings > Secrets and variables > Dependabot. A run triggered by Dependabot +# reads Dependabot secrets, and it cannot read Actions secrets. Add the same +# value as an Actions secret if the workflow_dispatch path needs it too. +# +# Without the secret the workflow still heals and still merges. Only the check +# runs on the healed commit are missing. on: pull_request: @@ -57,7 +74,9 @@ jobs: - uses: actions/checkout@v7.0.1 with: ref: ${{ steps.pr.outputs.head }} - token: ${{ secrets.GITHUB_TOKEN }} + # See AUTOHEAL_TOKEN in the header. An unset secret is an empty + # string, so this falls back to the default token. + token: ${{ secrets.AUTOHEAL_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: true fetch-depth: 0 @@ -98,7 +117,13 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add flake.nix - git commit -m "build(flake): recompute fetchYarnDeps hash for $HEAD" + # [dependabot skip] is mandatory, not decoration. Dependabot stops + # rebasing a PR as soon as a third party pushes a commit to it, and + # this heal commit is exactly that push. Without the marker, the + # first sibling PR to merge strands every other one in a conflict + # that nothing ever clears. With it, Dependabot force-pushes a + # rebase, and that push re-triggers this workflow to heal again. + git commit -m "build(flake): recompute fetchYarnDeps hash [dependabot skip]" git push origin "HEAD:$HEAD" - name: Flag and fail when the build cannot be made green @@ -118,7 +143,7 @@ jobs: steps.heal.outputs.ok == 'true' && steps.meta.outputs.update-type != 'version-update:semver-major' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.AUTOHEAL_TOKEN || secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} NUM: ${{ steps.pr.outputs.number }} UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} @@ -126,17 +151,30 @@ jobs: gh pr review "$NUM" --repo "$REPO" --approve \ --body "Auto-approved: fetchYarnDeps hash healed and nix build passes ($UPDATE_TYPE)." # GitHub needs a moment to recompute mergeability after the push. - merged=false for _ in 1 2 3 4 5 6; do if gh pr merge "$NUM" --repo "$REPO" --squash; then - merged=true - break + exit 0 fi sleep 10 done - if [ "$merged" != true ]; then - gh pr merge "$NUM" --repo "$REPO" --auto --squash + + # A conflict is not a failure of this workflow, so do not report one. + # The heal commit carries [dependabot skip], so Dependabot rebases + # this PR onto development, and that force-push runs this workflow + # again on the rebased head. + # + # `gh pr merge --auto` is not the fallback here. GitHub refuses to + # arm auto-merge on a branch with no merge requirement, and + # `development` has neither a required status check nor a required + # review. The call returns: + # Pull request Branch does not have required protected branch rules + STATE=$(gh pr view "$NUM" --repo "$REPO" --json mergeable --jq '.mergeable') + if [ "$STATE" = "CONFLICTING" ]; then + echo "PR $NUM conflicts with development. Dependabot will rebase it." + exit 0 fi + echo "::error::PR $NUM did not merge. Mergeable state: $STATE" + exit 1 - name: Leave major updates for manual review if: >- From 0ca5e18fcb8c8e3a3169f44bbaee8f40bf5ef24a Mon Sep 17 00:00:00 2001 From: countgitmick Date: Sun, 2 Aug 2026 06:17:14 -0700 Subject: [PATCH 2/2] chore(ci): drop batch-deps and leave Dependabot as the only npm updater batch-deps ran yarn upgrade every Monday and pushed package.json, yarn.lock and flake.nix straight to development. Dependabot npm runs weekly against the same three files. Two updaters on one lockfile mean that every Monday push turned each open npm PR dirty. The catch-all npm group now does the same job through a reviewed PR with real CI. batch-deps pushed to the default branch with no review at all, which its own header called out as the trade it accepted. --- .github/workflows/batch-deps.yml | 103 ------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 .github/workflows/batch-deps.yml diff --git a/.github/workflows/batch-deps.yml b/.github/workflows/batch-deps.yml deleted file mode 100644 index e9bc669e9..000000000 --- a/.github/workflows/batch-deps.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Batch Dependency Update - -# Verifies and pushes straight to development rather than opening a PR. -# -# It used to open one, and those PRs could never merge: a PR created with the -# default GITHUB_TOKEN does not trigger `pull_request` workflows (GitHub's -# loop prevention), so they were born with no CI, could never go green, and -# piled up until they conflicted with each other. The PR body even said "CI -# must pass before merging" — CI could not run at all. -# -# Pushing directly also keeps working with the fork-PR approval gate set to -# all_external_contributors: this is a first-party workflow, so it is never -# held for approval the way Dependabot's PRs are. -# -# The trade is that nothing downstream reviews this, so the Verify step below -# has to be the gate: lint plus a full nix build before anything is pushed. - -on: - schedule: - # Every Monday at 08:00 UTC - - cron: '0 8 * * 1' - workflow_dispatch: - -permissions: {} - -jobs: - batch-deps: - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - uses: actions/checkout@v7.0.1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Use Node.js 24.x - uses: actions/setup-node@v7 - with: - node-version: 24.x - - - name: Upgrade all compatible deps - run: yarn upgrade 2>&1 - - - name: Check for changes - id: changes - run: | - if git diff --quiet yarn.lock package.json; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Install Nix - if: steps.changes.outputs.changed == 'true' - uses: cachix/install-nix-action@v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - - name: Recompute fetchYarnDeps hash - if: steps.changes.outputs.changed == 'true' - id: nixhash - run: | - # flake.nix pins the yarn offline cache via: - # offlineCache = pkgs.fetchYarnDeps { yarnLock = ./yarn.lock; hash = "sha256-..."; }; - # Replace that hash with a fake one, build, and capture the real hash - # from the fixed-output-derivation mismatch. - FAKE="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - sed -i "s|hash = \"sha256-[^\"]*\"|hash = \"$FAKE\"|" flake.nix - REAL_HASH=$(nix build .#default --no-link 2>&1 | grep -oE 'got: *sha256-[A-Za-z0-9+/=]+' | awk '{print $NF}' || true) - if [ -n "$REAL_HASH" ]; then - sed -i "s|hash = \"$FAKE\"|hash = \"$REAL_HASH\"|" flake.nix - echo "hash_updated=true" >> "$GITHUB_OUTPUT" - else - # Hash unchanged — restore original - git checkout flake.nix - echo "hash_updated=false" >> "$GITHUB_OUTPUT" - fi - - # This pushes straight to development instead of opening a PR, so nothing - # downstream will check it — this step is the only gate that exists. - # Anything that would have failed PR CI has to fail here instead. - # - # `yarn install` re-runs the postinstall patch script, which exits - # non-zero if a patch youtubei.js still needs stops applying. - - name: Verify the upgrade - if: steps.changes.outputs.changed == 'true' - run: | - yarn install --frozen-lockfile - yarn run lint - nix build .#default --no-link - - - name: Push to development - if: steps.changes.outputs.changed == 'true' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add package.json yarn.lock flake.nix - git commit -m "chore(deps): weekly batch dependency upgrade $(date +%Y-%m-%d)" - # development may have moved while the build ran. - git pull --rebase origin development - git push origin HEAD:development