diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33af423..17b20623 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,12 @@ version: 2 # Maintenance mode: ViTransfer takes security patches weekly and little else. -# Everything targets dev; the weekly-security-release workflow promotes dev to -# main. +# Version updates target dev; the weekly-security-release workflow promotes dev +# to main. +# +# Security updates do NOT follow target-branch — Dependabot raises those against +# the default branch, main. sync-main-to-dev.yml carries them back to dev so the +# release train does not keep shipping a lockfile the advisory already covers. # # Major version updates are ignored everywhere: in maintenance mode a runtime # or framework major is not worth the churn. The release run's @@ -21,16 +25,17 @@ updates: prefix: deps prefix-development: deps-dev groups: - # One PR a week for routine churn, one for advisories. + # One PR a week for routine churn. There is deliberately no + # security-updates group here: `target-branch` applies to version updates + # only, so Dependabot opens security PRs against the default branch and + # ignores this file's groups and commit prefix entirely. A group here + # would read as if advisories arrive on dev, and they do not. npm: applies-to: version-updates patterns: ["*"] update-types: - patch - minor - npm-security: - applies-to: security-updates - patterns: ["*"] ignore: - dependency-name: "*" update-types: diff --git a/.github/workflows/docker-integration-tests.yml b/.github/workflows/docker-integration-tests.yml index c30f1ee3..4ea262f9 100644 --- a/.github/workflows/docker-integration-tests.yml +++ b/.github/workflows/docker-integration-tests.yml @@ -6,6 +6,12 @@ name: Docker Integration Tests # # No paths filter on PRs: "Test Summary" is a required check on dev, and a # required check that never runs leaves auto-merge pending forever. +# +# PRs to main are tested too. Setting `target-branch` in dependabot.yml turns +# off security updates from that config, so GitHub opens them against the +# default branch instead — main. Those PRs used to get CodeQL and nothing else, +# which is how a browserslist advisory sat on main untested while the release +# train kept building the vulnerable lockfile from dev. on: push: branches: [dev] @@ -18,7 +24,7 @@ on: - '.github/workflows/docker-*.yml' - '.github/workflows/test-dev-*.yml' pull_request: - branches: [dev] + branches: [dev, main] workflow_dispatch: permissions: diff --git a/.github/workflows/sync-main-to-dev.yml b/.github/workflows/sync-main-to-dev.yml new file mode 100644 index 00000000..319088a0 --- /dev/null +++ b/.github/workflows/sync-main-to-dev.yml @@ -0,0 +1,84 @@ +name: Sync main to dev + +# main moves without dev twice, and only one of them matters. The release train +# pushing the version bump and changelog is by design — dev never carries +# VERSION, so it lags every week and that is fine. A Dependabot security PR +# landing is not: those target the default branch (see dependabot.yml), so dev — +# the branch every release is cut from — keeps a lockfile the advisory already +# covers while main looks patched. That is how v1.3.5 built a vulnerable tree +# after the fix was already sitting on main, and how the release then hit a +# lockfile conflict waiting to happen the next time dev touched the same file. +# +# So this reports on what is stranded, not on how far behind dev is. Counting +# commits would file an issue after every release, weekly, for the state that is +# supposed to be true. +# +# It opens an issue rather than a PR: dev requires the "Test Summary" check, and +# a PR opened with GITHUB_TOKEN does not trigger `pull_request` workflows, so +# the check would never run and the PR would block forever. There is no PAT. + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + sync: + name: Check dev is in step + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Report anything stranded on main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git fetch origin main dev --quiet + + # Release commits are authored by github-actions[bot] and dev is meant + # to lag them. Everything else on main is a patch that never reached + # the branch the next release builds from. + stranded=$(git log --format='%an %h %s' origin/dev..origin/main \ + | grep -v '^github-actions\[bot\] ' || true) + + # One open issue at a time, closed again once dev catches up, so a + # stale one can never mask the next strand. + open_issue=$(gh issue list --label sync --state open --json number -q '.[0].number') + + if [ -z "$stranded" ]; then + echo "Nothing stranded: dev has every patch main does." + if [ -n "$open_issue" ]; then + gh issue close "$open_issue" --comment "dev has caught up with main." + fi + exit 0 + fi + + echo "Stranded on main:" + echo "$stranded" + + if [ -n "$open_issue" ]; then + echo "Issue #$open_issue is already tracking this." + exit 0 + fi + + # Its own label, not `ci`: that one is the release-failure report's, + # and sharing it would make either issue suppress the other. + gh label create sync \ + --description "A patch on main has not reached dev" \ + --color 1d76db --force + + gh issue create \ + --title "Patches on main have not reached dev" \ + --label sync \ + --body "$(printf '%s\n\n```\n%s\n```\n\n%s\n' \ + "These commits are on main but not on dev. The weekly release is cut from dev, so until they are carried back it keeps building without them — and the release's own merge will conflict as soon as dev touches the same files." \ + "$stranded" \ + "Carry them over with \`git checkout dev && git merge origin/main && git push origin dev\`.")" diff --git a/.github/workflows/weekly-security-release.yml b/.github/workflows/weekly-security-release.yml index ec25e1b1..c3b45a96 100644 --- a/.github/workflows/weekly-security-release.yml +++ b/.github/workflows/weekly-security-release.yml @@ -112,8 +112,13 @@ jobs: run: | npm ci --legacy-peer-deps --ignore-scripts # Majors and unfixable advisories never reach auto-merge, so this is - # the backstop that makes them visible rather than silently skipped. - npm audit --audit-level=high || echo "::warning::npm audit reports unresolved advisories — review before approving" + # the backstop that catches them rather than silently skipping them. + # + # It fails rather than warns: the Dockerfile runs the same gate during + # `build`, which is after `publish` has merged dev and pushed the + # version bump. A warning here bought nothing and left main carrying + # release commits for a version that never tagged or shipped. + npm audit --audit-level=high # Individual PRs are tested against dev as it was when they opened, not # against the final merged state. With no human approving the release, this