Skip to content

chore(ci): adopt node-ci gold-standard reusable#604

Merged
CybotTM merged 2 commits into
mainfrom
chore/adopt-node-ci
Jul 20, 2026
Merged

chore(ci): adopt node-ci gold-standard reusable#604
CybotTM merged 2 commits into
mainfrom
chore/adopt-node-ci

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 20, 2026

Copy link
Copy Markdown
Member

Pilots the org node-ci gold-standard meta-reusable (netresearch/.github#248). Replaces lint.and.build.yml + codeql.yml + security.yml with one ci.yml calling node-ci.yml (lint/type-check/test/build + node-audit + codeql + gitleaks + dependency-review). References the reusable's @feat/node-gold-standard branch for validation; flips to @main once #248 merges. Required checks (🔎 Lint, 🔨 Build) will be updated to the new ci / … names once the run reports them.

Replace lint.and.build.yml + codeql.yml + security.yml with one ci.yml
calling the org node-ci reusable (lint/type-check/test/build + node-audit
+ codeql + gitleaks + dependency-review). Piloting against the reusable's
feature branch; flips to @main once netresearch/.github#248 merges.

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
@gemini-code-assist

Copy link
Copy Markdown

Note

Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported.

github-actions[bot]
github-actions Bot previously approved these changes Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

  • .github/workflows/security.yml

Signed-off-by: Sebastian Mendel <sebastian.mendel@netresearch.de>
@sonarqubecloud

Copy link
Copy Markdown

@CybotTM
CybotTM merged commit 1aa46c0 into main Jul 20, 2026
17 checks passed
@CybotTM
CybotTM deleted the chore/adopt-node-ci branch July 20, 2026 22:51
CybotTM added a commit that referenced this pull request Jul 23, 2026
…github reusables (#605)

Routes two workflows through shared reusables in
[`netresearch/.github`](https://github.com/netresearch/.github):

| Caller | Reusable |
|---|---|
| `.github/workflows/pr-quality.yml` |
[`pr-quality.yml@main`](https://github.com/netresearch/.github/blob/main/.github/workflows/pr-quality.yml)
|
| `.github/workflows/auto-merge-deps.yml` |
[`auto-merge-deps.yml@main`](https://github.com/netresearch/.github/blob/main/.github/workflows/auto-merge-deps.yml)
|

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](https://github.com/netresearch/.github/blob/main/.github/workflows/pr-quality.yml#L152-L159):
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](https://github.com/netresearch/.github/blob/main/.github/workflows/pr-quality.yml#L172-L184)
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](https://github.com/netresearch/.github/blob/main/.github/workflows/pr-quality.yml#L194-L196):
`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 #604 and #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`](https://github.com/netresearch/.github/blob/main/.github/workflows/pr-quality.yml#L144).
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

- `auto-merge-deps.yml` also approves `dependabot[bot]` and
`renovate[bot]` — before this PR in its own inline `Auto-approve PR`
step, after this PR in the reusable's `Approve PR` step, whose job `if`
admits exactly those two logins. `gh pr view <n> --json reviews` shows
PRs #602, #597 and #587 each carrying **2 `APPROVED` reviews from
`github-actions[bot]`**, and #601 carrying **4** — one per workflow, per
run. Removing one of two duplicates leaves the approval intact.
- The `Auto-approve (bot via APPROVE_TOKEN)` step was **already a
no-op**: `APPROVE_TOKEN` exists neither as a repo secret (`gh secret
list` → empty) nor among the org secrets visible to this repo, so it
fell back to `GITHUB_TOKEN`, which cannot approve its own bot's PRs, and
the failure was swallowed by `|| true`. Empirical confirmation:
release-please PR #466 (`github-actions[bot]`, label `autorelease:
pending`) was approved **manually by `CybotTM`**, not by the workflow.

### 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](#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* (#529, #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](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 (#466) is a
genuine release-please branch and two (#529, #540) are Renovate action
bumps; `gh run list --workflow=auto-merge-deps.yml` filtered the same
way → four runs, all on #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.
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