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/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 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: >-