Skip to content

ci: route pr-quality and auto-merge-deps through shared netresearch/.github reusables#605

Merged
CybotTM merged 3 commits into
mainfrom
ci/use-shared-reusables
Jul 23, 2026
Merged

ci: route pr-quality and auto-merge-deps through shared netresearch/.github reusables#605
CybotTM merged 3 commits into
mainfrom
ci/use-shared-reusables

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 23, 2026

Copy link
Copy Markdown
Member

Routes two workflows through shared reusables in
netresearch/.github:

Caller Reusable
.github/workflows/pr-quality.yml pr-quality.yml@main
.github/workflows/auto-merge-deps.yml auto-merge-deps.yml@main

Both callers now have zero step-level uses: — each has a single job that is a uses: of its reusable.
.github/workflows/AGENTS.md is updated for the changed auto-merge-deps.yml scope.
release.when-tagged.yml and release-please.yml are deliberately untouched.


1. pr-quality.yml

How the original blocker was proved resolved

This migration was refused once before: this repo authorizes auto-approval on the author's repository permission (admin/write), while the hub reusable's default test is author_association — which is not a permission (COLLABORATOR is returned for read/triage collaborators as well). The hub has since gained auto-approve-require-write. Proof it actually delivers the property, traced from requirement to API call:

  1. Association gate is bypassed, not merely supplemented. Reusable lines 152-159: the job if ends in ( inputs.auto-approve-require-write || contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) ). With the input true the || short-circuits and author_association is never evaluated.
  2. The permission lookup is the same call this repo made. Reusable lines 172-184 runs gh api "repos/$REPO/collaborators/$PR_AUTHOR/permission" --jq '.permission' 2>/dev/null || echo "none" — byte-identical to the removed line 44 of this repo's old workflow, including the fail-closed || echo "none".
  3. Approval is gated on that lookup. Reusable lines 194-196: inputs.auto-approve-require-write == false || contains(fromJSON('["admin","write"]'), steps.check-permission.outputs.permission). With the input true the left operand is false, so admin/write is mandatory — equivalent to the removed line 48.
  4. No second approval path exists in the reusable. grep -n "createReview\|pr review\|APPROVE" over the reusable returns only the one createReview block at lines 205-211. Nothing else can widen the gate downstream.
  5. The lookup demonstrably works here at runtime (not just statically). PRs chore(ci): adopt node-ci gold-standard reusable #604 and chore(ci): route CodeQL through shared netresearch/.github reusable #603 (author CybotTM, same-repo branches) carry APPROVED reviews from github-actions[bot], while gh run list shows Auto-merge bot PRs = skipped on both of those branches and PR Quality Gates = success. Only the old pr-quality.yml could have produced those approvals — so GET /collaborators/{author}/permission already resolves to admin/write under pull_request_target with GITHUB_TOKEN and this repo's pull-requests: write grant. The migrated job grants a superset (contents: read + pull-requests: write).

Before / after behaviour

Behaviour Before After Delta
Approve author with admin/write repo permission yes (gh pr review --approve) yes (pulls.createReview, same lookup) none — proof above
Approve author with read/triage/no permission no (lookup → none) no (lookup → none, fails closed) none
Approve on author_association alone never never (auto-approve-require-write: true) none
Approve dependabot[bot] / renovate[bot] yes, duplicated by auto-merge-deps.yml dropped here; auto-merge-deps.yml still approves them no effective loss — see below
Approve github-actions[bot] (release-please) step existed but was a no-op dropped no effective loss — see below
Re-approve on synchronize (needed: dismiss_stale_reviews: true) yes yes (trigger preserved) none
Approval blocked when the size-check job fails n/a (no such job) yes — the reusable's auto-approve carries needs: quality-gate new coupling, stated below
Approve draft PRs yes no (reusable guards draft == false) mitigated by adding ready_for_review
Approve PRs from forks by write-collaborators yes no (reusable requires head.repo.full_name == github.repository) narrower, safer; no fork PRs in this repo's recent history
PR size check / large-PR warning none added (quality-gate job) new job, and approval now depends on it
Approval review body empty short explanatory body, no {controls} placeholder so no dead link cosmetic
Top-level permissions: pull-requests: write {} + job-level union tightened
Runner ubuntu-24.04 ubuntu-latest (reusable) none material
Required status checks job names not referenced by branch protection (ci / … only) unchanged none

Approval is now coupled to the quality gate — stated plainly

The reusable's auto-approve job declares needs: quality-gate. Consequences, which an earlier revision of this description glossed as merely "additive":

  • If quality-gate fails (e.g. pulls.listFiles errors, or the checkout step fails), auto-approve is skipped and the PR is not auto-approved. That failure mode did not exist before.
  • quality-gate is itself guarded by draft == false, so on a draft PR both jobs are skipped — the ready_for_review trigger below is what recovers from that.
  • core.warning() for a large PR does not fail the job, so an oversized PR still gets approved; only a genuine job failure blocks approval.

This is accepted, not refuted: the shared standard runs the size check ahead of approval in every consuming repo, and this repo now gets the same ordering.

Why dropping the bot branches loses nothing

Trigger change

ready_for_review is added to types. The reusable guards both jobs with draft == false; marking a PR ready fires neither opened nor synchronize, so without this a draft PR would stay unapproved until its next push — a regression the migration itself would introduce. The spelling is validated by actionlint, which rejects an unknown activity type for pull_request_target and lists ready_for_review among the accepted ones (negative control below). All pre-existing types (opened, synchronize, reopened) are preserved, as are the workflow name and the (absent) concurrency.

actions/checkout under pull_request_target

The old file carried a comment forbidding actions/checkout; the reusable's quality-gate job contains one. The invariant that comment protects — untrusted PR code executing with a write-scoped token — is preserved:

  • The only step after the checkout is actions/github-script with an inline script; nothing from the working tree is executed, so the checked-out ref is not a code-execution vector either way.
  • actions/checkout is called without ref, so it resolves github.ref, which for pull_request_target is the base branch, not the PR head.
  • It sets persist-credentials: false, so no token is left in .git/config.

2. auto-merge-deps.yml

The release-please arm is removed

The old job's if had three arms; the third matched github-actions[bot] carrying the label autorelease: pending, i.e. a release-please PR. Its scope, measured rather than assumed (gh pr list --state all --limit 300, filtered on head branch):

  • Exactly one genuine release-please PR has ever been opened in this repo — #466 (release-please--branches--main--components--node-magento-eqp, 2026-02-16, merged). The two other "release-please" head-branch matches are Renovate PRs bumping the release-please action (chore(deps): update googleapis/release-please-action action to v4.4.1 #529, chore(deps): update googleapis/release-please-action action to v5 #540), which the dependabot/renovate arms handle anyway.
  • Nothing since — five months, while release-please.yml kept running success (most recently 2026-07-20). Releases here are cut by pushing a signed v* tag, handled by release.when-tagged.yml.
  • An org-wide code search for googleapis/release-please-action matches only this repo and its sibling, out of 191 — nothing else in the org uses it.

The arm's approval half was a no-op by construction: Auto-approve PR was explicitly skipped for github-actions[bot], and the comment deferred approval to "pr-quality.yml using APPROVE_TOKEN" — a path that does not exist (no such secret, see above) and that the pr-quality.yml migration in this same PR removes outright, so that comment pointed at a capability that is gone. On #466 the approval came manually from CybotTM.

Its merge half did fire, once: gh run list shows auto-merge-deps.yml running on that branch on 2026-02-16 (three success, one earlier failure). So the honest statement is not "this code never ran" but "this code has run for one PR in the repo's history, five months ago, on a release path the repo no longer uses".

Removing it is a deliberate owner decision, not an oversight, and it is not reimplemented in the shared workflow — netresearch/.github#261 ("RECOMMEND CLOSE: allow-release-please arm is unreachable") was closed for that reason. If release-please is ever revived here, --auto on its PRs comes back as an explicit change rather than as an unused branch nobody remembers. release-please.yml itself is out of scope for this PR and is untouched.

With that arm gone the remaining logic — approve dependabot[bot]/renovate[bot], then gh pr merge --auto — is exactly what the shared reusable does, so the file becomes a caller.

Merge strategy is pinned, not auto-detected

The reusable auto-detects squash-first:

if .allow_squash_merge then "--squash"
elif .allow_merge_commit then "--merge"
elif .allow_rebase_merge then "--rebase"
else "--squash" end

gh api repos/netresearch/node-magento-eqp returns {"merge":true,"squash":false,"rebase":true}, so the detector would resolve to --merge, whereas the inline job ran gh pr merge --auto --rebase. The caller therefore passes merge-strategy: rebase, which the reusable turns into STRATEGY="--rebase" before the detector branch is reached.

Before / after behaviour

Behaviour Before After Delta
Approve + auto-merge dependabot[bot] / renovate[bot] yes yes (reusable job if admits exactly those two logins) none
Handle github-actions[bot] + autorelease: pending matched the job if, approval deliberately skipped, then gh pr merge --auto --rebase not matched removed — fired for one PR in repo history (#466, 2026-02-16); owner-decided drop, evidence above
Merge strategy --rebase (hardcoded) --rebase (merge-strategy: rebase) none
Merge gating merge everything the if matched same (safe-updates-only left at its default false) none
dependabot/fetch-metadata step ran for Dependabot; its output steps.metadata was never referenced only runs under safe-updates-only, i.e. not here none — the step had no effect
Trigger pull_request_target: with types: [opened, synchronize, reopened] pull_request_target: — and those three types are exactly GitHub's default activity set none
Top-level permissions: contents: write, pull-requests: write {} + job-level contents: write + pull-requests: write tightened; the job grant equals the reusable's job permissions exactly
Runner / timeout ubuntu-24.04, no timeout ubuntu-latest, timeout-minutes: 5 (reusable) none material
Workflow name Auto-merge bot PRs Auto-merge dependency PRs (matches the org templates; it no longer handles non-dependency bots) not a required status check — branch protection requires only ci / … contexts
actions/checkout under pull_request_target forbidden by comment, absent still absent — the reusable has no checkout none

Permissions

The reusable's job declares contents: write + pull-requests: write. A caller granting less startup-fails before any job runs, so the caller job grants exactly that set.


Verification

Run in this branch's working tree, and nothing beyond this is claimed:

  • actionlint .github/workflows/auto-merge-deps.yml .github/workflows/pr-quality.yml → exit 0.
  • Negative control for the trigger fix: the same actionlint on a copy with ready_for_review misspelled → exit 1, invalid activity type … available types are … "ready_for_review" ….
  • yq '[.jobs[].steps[]?.uses]' .github/workflows/auto-merge-deps.yml[] (no step-level uses); yq '.on, .permissions, .jobs' confirms the single job, its permissions, and with: {merge-strategy: rebase}.
  • gh api repos/netresearch/node-magento-eqp --jq '{merge:.allow_merge_commit,squash:.allow_squash_merge,rebase:.allow_rebase_merge}'{"merge":true,"squash":false,"rebase":true} — the basis for pinning the strategy.
  • gh api repos/netresearch/node-magento-eqp/branches/main/protection → required contexts are ci / TS Check / Lint, ci / TS Check / Check types, ci / Test / test (node 24), ci / Build / build; required_approving_review_count: 1, dismiss_stale_reviews: true. No job renamed or removed here is a required context.
  • gh pr view 602|601|597|587 --json reviews → 2, 4, 2, 2 APPROVED reviews from github-actions[bot] (the duplicate-approval claim above).
  • gh pr list --state all --limit 300 --json number,headRefName filtered on head branch → three matches, of which one (chore(main): release 4.1.0 #466) is a genuine release-please branch and two (chore(deps): update googleapis/release-please-action action to v4.4.1 #529, chore(deps): update googleapis/release-please-action action to v5 #540) are Renovate action bumps; gh run list --workflow=auto-merge-deps.yml filtered the same way → four runs, all on chore(main): release 4.1.0 #466's branch, 2026-02-16.
  • Both reusables read in full from netresearch/.github@main (84499c7), plus this repo's release-please.yml and branches/main/protection.

Correction carried forward: the first commit message on the auto-merge-deps.yml change asserted that no release-please PR had ever been opened here. That is wrong — #466 exists, and the arm's merge half ran on it. The follow-up commit 7da85b4 states the accurate scope in the file itself; the removal decision is unchanged.

Not verified: neither migrated file can be exercised by this PR. pull_request_target always runs the workflow definition from the base branch, so the runs on this PR execute the old files from main. The new definitions first run after merge. The remote uses: references and their inputs were therefore checked by reading netresearch/.github@main directly — auto-approve-require-write (boolean), auto-approve-message (string) and merge-strategy (string) all exist with those names and types — not by execution.

Replace the hand-rolled auto-approve job with a single `uses:` of
netresearch/.github/.github/workflows/pr-quality.yml@main. The workflow now
has zero step-level `uses:`.

`auto-approve-require-write: true` is required: the reusable's default
authorization test is `author_association`, which is not a permission
(COLLABORATOR is returned for read/triage collaborators too). The opt-in
replaces that pre-filter with the same
`GET /repos/{repo}/collaborators/{author}/permission` lookup this repo used
before and approves only on `admin`/`write`.

`ready_for_review` is added to the trigger types to compensate for the
reusable's `draft == false` guard — otherwise a PR opened as a draft would
stay unapproved until its next push.

Top-level permissions are `{}`; the calling job grants exactly the union of
the reusable's job permissions (contents: read, pull-requests: write).

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
github-actions[bot]
github-actions Bot previously approved these changes Jul 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
See the Details below.

License Issues

.github/workflows/pr-quality.yml

PackageVersionLicenseIssue Type
netresearch/.github/.github/workflows/pr-quality.ymlmainNullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
actions/netresearch/.github/.github/workflows/pr-quality.yml main UnknownUnknown

Scanned Files

  • .github/workflows/pr-quality.yml

CybotTM added 2 commits July 23, 2026 09:14
…ed reusable

The release-please arm (author github-actions[bot] + label
'autorelease: pending') is dead code: no release-please PR has ever been
opened in this repo, and releases are cut manually from a signed tag via
release.when-tagged.yml. Its approve step was additionally a deliberate
no-op, and the comment pointing at an APPROVE_TOKEN path in pr-quality.yml
described a capability that no longer exists.

With that arm removed the job does nothing the shared reusable does not
already do, so the workflow becomes a caller of
netresearch/.github/.github/workflows/auto-merge-deps.yml@main.

merge-strategy is pinned to "rebase": the reusable auto-detects squash-first,
but this repo has allow_squash_merge=false / allow_merge_commit=true /
allow_rebase_merge=true, so the detector would pick --merge whereas the
inline job ran `gh pr merge --auto --rebase`.

The caller job grants contents: write + pull-requests: write, matching the
reusable's job-level permissions exactly (a smaller set would startup_failure).

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
…se-please arm

Corrects the previous commit message, which claimed no release-please PR had
ever been opened here. Measured with `gh pr list --state all --limit 300`
filtered on head branch: exactly one genuine release-please PR exists (#466,
2026-02-16, merged), and `gh run list` shows auto-merge-deps.yml did run on
that branch. The other two "release-please" head-branch matches are Renovate
PRs bumping the release-please action.

The approval half of the removed arm was still a no-op by construction, and
#466 was approved manually. Nothing has come from release-please in the five
months since. The removal decision is unchanged; only the justification is
now stated accurately, in the file itself.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@CybotTM CybotTM changed the title ci(pr-quality): route through shared netresearch/.github reusable ci: route pr-quality and auto-merge-deps through shared netresearch/.github reusables Jul 23, 2026
@sonarqubecloud

Copy link
Copy Markdown

@CybotTM
CybotTM merged commit 4341959 into main Jul 23, 2026
17 checks passed
@CybotTM
CybotTM deleted the ci/use-shared-reusables branch July 23, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant