Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 0 additions & 65 deletions .github/workflows/renovate.yml

This file was deleted.

214 changes: 206 additions & 8 deletions .github/workflows/upgrade-check.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
# Reusable consumer upgrade radar — things a consuming repo owns that its
# updater doesn't cover here, each in its own reviewed PR:
# flutter-sdk — the Flutter SDK pin (.fvmrc + example/.fvmrc) → latest stable.
# lockfiles — pubspec.lock files refreshed to latest in-range.
# whuppi/ci refs (workflows AND composite action.yml) are bumped by Renovate
# (renovate.yml) — it reads composite action.yml, which Dependabot can't
# (dependabot/dependabot-core#6704). The shared tool/binary pins (fvm, Chrome,
# bore, zizmor, actionlint) reach consumers through a whuppi/ci release +
# Renovate bumping the whuppi/ci refs; whuppi/ci owns their versions via
# self-upgrade.yml.
# flutter-sdk — the Flutter SDK pin (.fvmrc + example/.fvmrc) → latest stable.
# lockfiles — pubspec.lock files refreshed to latest in-range.
# composite-refs — the dependabot-core#6704 blind spot (opt-in): every
# whuppi/ci ref (workflow AND composite, kept uniform so the
# pin never splits) → latest release, plus third-party
# actions pinned inside composite action.yml → latest SHA.
#
# Dependabot still owns pub deps and third-party actions in workflow FILES; the
# composite-refs job owns only what Dependabot can't see (dependabot never reads
# `uses:` inside composite action.yml, and a whuppi/ci group bump splits the pin
# across workflow-vs-composite). A consumer opts in with `sweepActions: true`
# and passes CI_ACTIONS_TOKEN — a Workflows-scope PAT, because GITHUB_TOKEN
# can't write .github/workflows/ — and drops `whuppi/ci*` from its Dependabot
# github-actions group.
#
# The shared tool/binary pins (fvm, Chrome, bore, zizmor, actionlint, pinact)
# reach consumers through a whuppi/ci release + the composite-refs whuppi/ci
# bump; whuppi/ci owns their versions via self-upgrade.yml.
name: Upgrade Check

on:
Expand All @@ -21,6 +31,17 @@ on:
type: string
required: false
default: 'dev'
sweepActions:
# Opt in to the composite-refs job. Off by default so a consumer that
# hasn't set up CI_ACTIONS_TOKEN keeps its old Dependabot-only shape.
type: boolean
required: false
default: false
secrets:
CI_ACTIONS_TOKEN:
# Workflows-scope PAT for the composite-refs job's push (GITHUB_TOKEN
# can't write .github/workflows/). Only needed when sweepActions: true.
required: false

permissions: {}

Expand Down Expand Up @@ -208,3 +229,180 @@ jobs:
else
gh pr edit "$EXISTING_PR" --title "chore: refresh lockfiles" --body "$PR_BODY"
fi

# ── Composite + workflow action refs (the dependabot-core#6704 gap) ──
# Opt-in (sweepActions: true). Two bumps Dependabot can't reach:
# 1. every whuppi/ci ref across .github — workflows AND composite
# action.yml — swept to the latest release, so the pin never splits
# (a Dependabot group bump moves only the workflow refs, and
# pin-availability then rejects the split);
# 2. third-party actions pinned inside composite action.yml (e.g.
# actions/cache frozen at an old SHA) → latest SHA, via pinact.
# Dependabot keeps pub + third-party actions in workflow FILES; this owns only
# the composite blind spot + the uniform whuppi/ci bump. The push needs
# CI_ACTIONS_TOKEN (Workflows-scope PAT) — GITHUB_TOKEN can't write
# .github/workflows/. github.token (contents:read) still authenticates
# pinact's SHA-resolving API reads.
composite-refs:
name: Composite action refs
if: ${{ inputs.sweepActions }}
runs-on: ${{ inputs.runner }}
timeout-minutes: 15
permissions:
contents: read # pinact's GitHub API reads; the PAT does every write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ inputs.branch }}
persist-credentials: false
# whuppi/ci itself — for tool/versions.env (PINACT_VERSION). Stamped to
# the release tag by self-release.yml, same as the pr-checks gate does.
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: whuppi/ci
ref: main
path: .whuppi-ci
persist-credentials: false

- name: Prepare branch
id: branch
env:
# The PAT, not GITHUB_TOKEN: `gh auth setup-git` wires it into git's
# credential helper so the push can write .github/workflows/.
GH_TOKEN: ${{ secrets.CI_ACTIONS_TOKEN }}
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::sweepActions is on but CI_ACTIONS_TOKEN was not passed. Add it under the caller's secrets:." >&2
exit 1
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
gh auth setup-git
gh label create upgrade-ci-refs --description "Auto: whuppi/ci + composite action ref bump" --color "1d76db" 2>/dev/null || true
EXISTING_PR=$(gh pr list --label upgrade-ci-refs --state open --json number --jq '.[0].number // empty')
echo "existing_pr=$EXISTING_PR" >> "$GITHUB_OUTPUT"
if [ -n "$EXISTING_PR" ]; then
git fetch origin chore/composite-refs 2>/dev/null || true
git checkout -B chore/composite-refs origin/chore/composite-refs 2>/dev/null || git checkout -b chore/composite-refs
else
git checkout -b chore/composite-refs
fi

- name: Sweep every whuppi/ci ref to the latest release
id: sweep
env:
GH_TOKEN: ${{ secrets.CI_ACTIONS_TOKEN }}
run: |
set -euo pipefail
latest="$(gh api repos/whuppi/ci/releases/latest --jq '.tag_name' 2>/dev/null || true)"
# latest is interpolated into the sed below — reject anything but an
# exact vX.Y.Z tag, or a crafted release name becomes sed injection.
if ! printf '%s' "$latest" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::implausible whuppi/ci release tag: '$latest'" >&2
exit 1
fi
# Every .github file pinning a whuppi/ci ref to a version tag —
# workflows AND composite action.yml. `@main` refs (intentionally
# floating) carry no @v and are left untouched. .whuppi-ci is our own
# transient checkout of whuppi/ci — never sweep or stage it.
files=()
while IFS= read -r f; do files+=("$f"); done \
< <(grep -rlE 'whuppi/ci/[^@[:space:]]*@v[0-9.]+' .github || true)
if [ ${#files[@]} -eq 0 ]; then
echo "No versioned whuppi/ci refs found — nothing to sweep."
else
for f in "${files[@]}"; do
sed -i.bak -E "s#(whuppi/ci/[^@[:space:]]*)@v[0-9.]+#\1@${latest}#g" "$f"
rm -f "$f.bak"
done
echo "swept ${#files[@]} whuppi/ci ref file(s) → $latest"
fi
# Hand the exact rewritten paths to the commit step so it stages only
# those (never a blanket add).
{
echo "files<<SWEPT_EOF"
printf '%s\n' "${files[@]}"
echo "SWEPT_EOF"
} >> "$GITHUB_OUTPUT"

- name: Pin third-party actions inside composites (pinact)
id: pinact
env:
# Read-only API for resolving tag → SHA. The write is the push below.
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# shellcheck disable=SC1091
source .whuppi-ci/tool/versions.env
go install "github.com/suzuki-shunsuke/pinact/v4/cmd/pinact@v$PINACT_VERSION"
PATH="$(go env GOPATH)/bin:$PATH"
# Composite action.yml ONLY — Dependabot owns third-party actions in
# workflow files, so pinact stays out of .github/workflows here to
# avoid double-bumping. whuppi/ci refs are excluded (the sweep above
# keeps them as version tags, not SHAs).
comp=()
while IFS= read -r f; do comp+=("$f"); done \
< <(find .github/actions \( -name action.yml -o -name action.yaml \) 2>/dev/null || true)
if [ ${#comp[@]} -eq 0 ]; then
echo "No composite action.yml — nothing to pin."
else
pinact run --update --exclude '^whuppi/ci/' "${comp[@]}"
fi
# Stage any composite that pinact rewrote.
changed=()
while IFS= read -r f; do changed+=("$f"); done \
< <(git diff --name-only -- '.github/actions/**/action.yml' '.github/actions/**/action.yaml' 2>/dev/null || true)
{
echo "files<<PINNED_EOF"
printf '%s\n' "${changed[@]}"
echo "PINNED_EOF"
} >> "$GITHUB_OUTPUT"

- name: Commit + open or update the PR
env:
GH_TOKEN: ${{ secrets.CI_ACTIONS_TOKEN }}
EXISTING_PR: ${{ steps.branch.outputs.existing_pr }}
BASE: ${{ inputs.branch }}
SWEPT_FILES: ${{ steps.sweep.outputs.files }}
PINNED_FILES: ${{ steps.pinact.outputs.files }}
run: |
# Stage only the files the two steps rewrote — never a blanket add,
# and never .whuppi-ci (our transient whuppi/ci checkout).
printf '%s\n%s\n' "$SWEPT_FILES" "$PINNED_FILES" | while IFS= read -r f; do
[ -n "$f" ] && git add -- "$f"
done
git commit -m "ci: bump whuppi/ci + composite action refs" || {
echo "Refs already current — nothing to bump."
exit 0
}
if [ -n "$EXISTING_PR" ]; then
git push origin chore/composite-refs
else
git push --force origin chore/composite-refs
fi

PR_BODY="$(cat <<'EOF'
Bumps the refs Dependabot can't reach inside composite `action.yml`
(dependabot/dependabot-core#6704):

- **whuppi/ci refs** across `.github` — workflows AND vendored composite
actions — swept uniform to the latest whuppi/ci release, so the pin
never splits (a Dependabot group bump moves only the workflow refs,
and pin-availability rejects the split).
- **Third-party actions inside composites** (e.g. `actions/cache`)
pinned to the latest SHA via pinact.

Dependabot still owns pub deps and third-party actions in workflow files.

Add `ready-to-test` for the full cross-target run, then merge once green.

_Auto-generated by upgrade-check.yml_
EOF
)"

if [ -z "$EXISTING_PR" ]; then
gh pr create --base "$BASE" --head chore/composite-refs \
--title "ci: bump whuppi/ci + composite action refs" --label upgrade-ci-refs --draft --body "$PR_BODY"
else
gh pr edit "$EXISTING_PR" --title "ci: bump whuppi/ci + composite action refs" --body "$PR_BODY"
fi
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ Releases are cut from the top heading here by `self-release.yml`; consumers pin
an exact version and upgrade through grouped Dependabot PRs. Versioning rules
live in the README. Newest first.

## 2.3.0

- Reverted Renovate (added in 2.2.0). Deleted the reusable `renovate.yml`. The
self-hosted Renovate machine — a dashboard issue, a per-consumer `renovate.json5`,
a status-check/token-scope surface — was far more than the one gap that
actually bit us: composite `action.yml` refs Dependabot can't see
([dependabot-core#6704](https://github.com/dependabot/dependabot-core/issues/6704)).
- Closed that gap in the existing radar instead. New opt-in `composite-refs` job
in the reusable `upgrade-check.yml`: sweeps every whuppi/ci ref across `.github`
(workflows AND composites) to the latest release — uniform, so the pin never
splits — and pins third-party actions inside composites to the latest SHA via
`pinact`. Dependabot keeps pub deps + third-party actions in workflow files;
the two never overlap. A consumer opts in with `sweepActions: true` +
`CI_ACTIONS_TOKEN` and adds `whuppi/ci*` to its Dependabot `ignore`.
- Added `pinact` to the pinned tool supply chain (`PINACT_VERSION`), owned by
`self-upgrade.yml` like actionlint/zizmor.
- Renamed the org secret `RENOVATE_TOKEN` → `CI_ACTIONS_TOKEN` (same
Workflows-scope PAT; `GITHUB_TOKEN` still can't write `.github/workflows/`).
`secrets.sh`'s `org` scope stays — it's generic.

## 2.2.0

- Added a reusable `renovate.yml` — self-hosted Renovate that each consumer calls
Expand Down
2 changes: 1 addition & 1 deletion deploy/.deploy/secrets.json.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"github": {
"release_token": "FINE_GRAINED_PAT_contents+workflows_RW_on_whuppi/ci",
"renovate_token": "FINE_GRAINED_PAT_contents+workflows+PRs+issues_RW_on_ALL_whuppi_repos"
"ci_actions_token": "FINE_GRAINED_PAT_contents+workflows+PRs+issues_RW_on_ALL_whuppi_repos (composite-refs sweep; org secret CI_ACTIONS_TOKEN)"
}
}
3 changes: 2 additions & 1 deletion deploy/.deploy/secrets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ cmd_set() {
ghn="$key"
if [ "$env" = "org" ]; then
# Org-wide secret, visible to every whuppi repo — used by reusable
# workflows running in a consumer's context (e.g. Renovate).
# workflows running in a consumer's context (e.g. the composite-refs
# sweep's CI_ACTIONS_TOKEN).
gh secret set "$ghn" --org "$ORG" --visibility all --body "$value"
echo "✓ GitHub: org $ORG → $ghn (all repos)"
else
Expand Down
28 changes: 16 additions & 12 deletions docs/UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,25 @@ consumer onboarding.
| Thing | Bumped by | You do |
|---|---|---|
| Tool/binary pins (`tool/versions.env`) | `self-upgrade.yml` daily PR | Review the hashes, merge, cut a release |
| Consumer deps + every action `uses:` ref — pub, **and workflows AND composite `action.yml`** (third-party + whuppi/ci) | **Renovate** (`renovate.yml` reusable, one thin wrapper per consumer) | Review + merge the bump PRs it opens |
| Consumers' Flutter SDK + lockfiles, pre-Renovate | reusable `upgrade-check.yml` — for consumers not yet migrated to Renovate | Nothing here |
| Consumer pub deps + third-party actions in **workflow files** | consumer's **Dependabot** (ignoring `whuppi/ci*`) | Review + merge its grouped PRs |
| Every **whuppi/ci ref** (workflow + composite, uniform) + third-party actions **inside composites** | the `composite-refs` job in the reusable `upgrade-check.yml` (opt-in) | Review + merge the one bump PR it opens |
| Consumers' Flutter SDK + lockfiles | reusable `upgrade-check.yml` | Nothing here |

**Why Renovate, not Dependabot.** Dependabot's github-actions updater never reads
`uses:` refs inside composite `action.yml`
**The composite blind spot (`composite-refs`).** Dependabot's github-actions
updater never reads `uses:` refs inside composite `action.yml`
([dependabot/dependabot-core#6704](https://github.com/dependabot/dependabot-core/issues/6704),
open) — a consumer's vendored `make-target/action.yml`, and any third-party action
pinned inside a composite, is invisible to it. A grouped bump moves only the
workflow-level refs and splits the pin, which the `pin-availability` gate then
rejects. Renovate reads composite `action.yml`, so it keeps every whuppi/ci ref
uniform (workflow and composite alike) and bumps the third-party actions hidden in
composites. Each consumer runs `renovate.yml` (a thin wrapper calling the reusable,
same shape as this one) and drops its Dependabot `github-actions` + `pub` config
once migrated. The token is a whuppi org secret (`RENOVATE_TOKEN`); Renovate must
write `.github/workflows/`, which `GITHUB_TOKEN` can't.
pinned inside a composite, is invisible to it. A grouped whuppi/ci bump moves only
the workflow-level refs and splits the pin, which the `pin-availability` gate then
rejects. The opt-in `composite-refs` job owns exactly that gap: it sweeps every
whuppi/ci ref across `.github` (workflows AND composites) to the latest release —
uniform by construction — and pins third-party actions inside composites to the
latest SHA with `pinact`. Dependabot keeps pub deps + third-party actions in
workflow FILES; the two never overlap. A consumer opts in by calling the reusable
with `sweepActions: true`, passing `CI_ACTIONS_TOKEN`, and adding `whuppi/ci*` to
its Dependabot `ignore`. The token is a whuppi org secret (`CI_ACTIONS_TOKEN`, a
Workflows-scope PAT) — the sweep writes `.github/workflows/`, which `GITHUB_TOKEN`
can't.

A pin bump merged to `main` reaches nobody until a release is cut — merging
and releasing are separate, deliberate steps.
Expand Down
Loading