Skip to content

feat(release): develop to main - #941

Merged
bedatty merged 81 commits into
mainfrom
develop
Oct 20, 2026
Merged

bedatty merged 81 commits into
mainfrom
develop

Conversation

@bedatty

@bedatty bedatty commented Oct 20, 2026

Copy link
Copy Markdown
Contributor
Lerian

GitHub Actions Shared Workflows


Description

Release develop to main. Reopened from #926, which was closed after @fredcamaral reproduced two ways past the breaking-change gate at that head. Both are fixed and merged; this is the same release with those fixes in it.

Breaking-change approval gate

The headline of the release: a pull request carrying breaking-change commits is held at the breaking-approval GitHub Environment until a human approves it, so a major version bump is a decision rather than a side effect. #907, #908, #909, #911, #912, #913, #914, #915, #918, #919, #921.

Built from six composites, and wired into pr-validation, go-pr-validation and js-pr-validation.

The review that followed

@radagast-lerian and @fredcamaral reviewed the gate across five rounds. Everything they raised is either fixed or recorded — nothing is carried into this release unanswered. The two security findings first:

  • A forged record could reuse someone else's approval (fix(pr-validation): close two paths past the breaking-change gate #940). The reuse lookup took the fingerprint from the record itself, so it established that an approval happened on this branch and nothing about what it covered. Since the fingerprint derives from public commit SHAs, a member could post a record carrying fp(A,B) pointing at the run that approved only A, then push B. The record now says only which run to ask: breaking-change-commits gained an at-sha mode, and the approved set is resolved from the approving run's own head_sha.
  • A dispatched run could pass a cancelled approval as superseded (fix(pr-validation): close two paths past the breaking-change gate #940). The newer-run check counted any newer run on the branch, so dispatching one by hand and cancelling the pending approval produced superseded, which exits 0. Only pull_request runs count now.

And the correctness ones: approval scope checked before the Environment is scheduled (#932), cancellation requiring established supersession (#932), truncated commit lists refused rather than answered (#932), a reused approval bound to the pull request that earned it (#930), acknowledgement feedback surviving both approval and a disabled gate (#935), multiple approvers serialised into one valid output value (#939).

Also in this release

Test coverage

Three composite suites — pr-gate-scope (32 cases), breaking-change-commits (28) and codeowners-reviewers (22) — were invoked by no workflow at all and now run in self CI (#936). The workflow suite grew from 91 to 113 cases, and the gate's error paths are executed rather than grepped for.

After this merges, the gate's composites move off @develop refs to a released channel — the last open item from @radagast-lerian's review, which needed this release to have something to point at.

Type of Change

  • feat: New workflow or new input/output/step in an existing workflow
  • fix: Bug fix in a workflow (incorrect behavior, broken step, wrong condition)
  • docs: Documentation only (README, docs/, inline comments)
  • test: Adding or updating tests
  • chore: Dependency bumps, config updates, maintenance

Breaking Changes

None.

Two behaviours of breaking-change-approval.yml changed after its first release and are documented rather than left silent — see Behaviour changes since v1.84:

  1. label survives the decision. It is a fact about the change, not a waiting marker; the transient role moved to the new pending_label.
  2. has_breaking_changes reports detection out of scope, instead of being forced to false.

Neither is classified as breaking: the gate shipped in v1.84.0 on 2026-10-15, and none of the umbrellas expose an input for label — only force_labels and skip_labels — so a caller on the normal path could not have configured one. The umbrellas also do not surface the gate's has_breaking_changes; theirs comes from the unchanged breaking-change-guard job.

Testing

Every change was validated in its own PR into develop. In aggregate on this head:

  • 113 workflow integration tests, 60 detector cases, 32 gate-scope, 28 commit-list, 22 CODEOWNERS — all passing
  • actionlint, shellcheck, yamllint and typos clean
  • both security findings encoded as tests and verified by mutation: removing the fingerprint comparison makes the forged case pass again, and removing the event filter makes the dispatched cases pass as superseded again

The gate was also exercised end to end on real pull requests: rejection closing the PR (#921), approval surviving later commits (#922, #923), and a clean PR passing untouched (#924).

  • YAML syntax validated locally
  • Triggered a real workflow run on a caller repository using @this-branch or the beta tag
  • Verified all existing inputs still work with default values
  • Confirmed no secrets or tokens are printed in logs
  • Checked that unrelated workflows are not affected

Caller repo / workflow run: this repository, through self-pr-validation on every PR listed above; the gate's own runs on #921 through #924.

Related Issues

Supersedes #926.

lerian-studio and others added 30 commits October 15, 2026 19:56
The waiting comment opened with a siren and spent four paragraphs
explaining itself. A breaking change is often deliberate, and a comment
that shouts teaches people to scroll past it — which is the one thing a
gate cannot afford.

Now: what was found, a link straight to the decision, and who can make
it. Everything else — the scope rule, the acknowledgement state, how to
undo the break — is one small line at the bottom. Eighteen lines down to
nine.

Four things are links now where there were none: the offending commit,
the pending deployment, the reviewers, and the docs. "Who can approve
this" was the obvious next question and the answer was buried in
Environment settings nobody thinks to open.

Reviewers render as links, never as @mentions: the pending deployment
already notifies exactly those people.
Addresses the CodeRabbit review on #907.

environment-protection now reads back who the Environment is actually
gated by and exposes it, and the reporter prefers that over the
configured list. The two differ whenever the Environment already existed
or was edited since — and naming the wrong approvers is worse than
naming none, which is what the comment does when neither is available.

Also from the review:

  the `reviewers` description still said "rendered as GitHub mentions",
  written before the switch to links and never updated

  the README gave `docs-url` as "the gate's docs page" where .coderabbit.yml
  asks for input tables to match action.yml exactly

  the example's links all pointed at `(#)`

  the no-bare-mention test asserted one forbidden spelling, so it passed
  by accident and would keep passing if the mention were written any
  other way. It now checks that every `@${owner}/` sits inside Markdown
  link text — verified by injecting a bare mention and watching it fail
… unknown

Addresses the second CodeRabbit review on #907.

The composite read the Environment twice: once for the protection rules,
once for the reviewers, with the second call swallowing failure. That
allowed a state of "protected, reviewers unknown" — and an empty
reviewer list is indistinguishable from "this Environment has none", so
the reporter fell back to the configured list and could name people who
cannot actually approve. Which is the very thing the previous commit set
out to fix.

The suggestion was to carry the read failure as a distinct status.
Removing the state is better than reporting it: one GET now answers both
questions, so either both are known or the step fails. It also drops a
call to an API this job already talks to twice.

Test pins it to exactly one read.
Three things, from reviewing the comment on #906 in place.

The call to action is now a shields.io badge wrapped in the link, which
is the only thing in Markdown that reads as a button. That puts a third
party in the path of a release control, so the alt text carries the
whole call to action: a blocked image leaves a described link, not a
broken rectangle. Setting badge-url to empty drops the dependency and
falls back to a heading link.

The "not intentional?" example now shows this pull request's own commit
subjects in a diff block instead of a made-up feat(scope)!: subject. It
matches only the `!` form, because that is the form the example
illustrates — a view, never a verdict. The canonical rule stays in the
detector, and with nothing matched the wording goes generic.

And only one approval request stays open. Every push queued another
pending deployment while the old ones kept sitting in the reviewers'
queue, each tied to a commit that is no longer the head — #906 had two
at once. Approving one of those approves a revision nobody is merging.
A concurrency group keyed to the pull request cancels the previous
request, which is also what expires an approval: the yes was given to a
specific revision and the next one has to earn its own.

The cancelled run still reaches the verdict, so it now checks whether it
still covers the head and leaves the pull request alone when it does
not. Otherwise it would overwrite the live comment with "the approval
did not complete".
The alert block said what happened in prose, which meant reading it to
know. A 💥 in an `##` heading inside the block makes the state legible
from the timeline, before anyone opens anything — and the table gains
marks on the three rows that matter, so it can be scanned instead of
read.

Each state now gets the alert that fits it: IMPORTANT while a decision
is pending, NOTE once approved, CAUTION on a rejection that closed the
pull request, WARNING when the decision was never recorded. Colour and
icon come from GitHub; the emoji says what the block is about.
Addresses the CodeRabbit review on #909.

The state assertions searched the whole reporter script, so every alert
and heading was present somewhere no matter which branch used it.
Swapping two states' alerts would have kept the suite green while every
comment said the wrong thing.

The suggested fix pulled each `case` branch out with a regex anchored to
the exact indentation. That trades one fragility for another — any
reindentation breaks the pattern and the test reports a problem that is
not there — and it still reads the source rather than the output.

So the tests now run the composite and read the Markdown it produced.
`render_reporter(state)` executes the body with `gh` stubbed, and each
state is checked for its own alert and mark *and* for the absence of the
other three; exclusivity is what catches a swap. Verified by swapping
IMPORTANT for NOTE on `waiting` and watching it fail.

The link assertions moved the same way: they matched shell fragments
like `${SERVER_URL}/${REPO}/commit/...` and now match rendered URLs, so
the no-bare-mention rule holds however the composite spells it.
Addresses the third CodeRabbit review on #909.

The state test read the rendered comment but checked `[!IMPORTANT]` and
`## 💥 …` as separate strings. A GitHub alert only renders inside a
blockquote, so dropping the `>` would leave both assertions green while
the comment rendered as plain text — getting the content right and the
presentation wrong, which is the thing this PR is about.

Now asserted as adjacent quoted lines. Verified by removing the `>` from
`waiting` and watching it fail.
…fails

PR Size, Auto Labels and PR Metadata were gated on
`needs.blocking-checks.result == 'success'`, so a wrong title or a
missing signature took them down too — the summary reported three
`skipped` rows next to the failure.

That came from the 2-tier fail-fast model and saved a runner at the
expense of the pull request that most needed the help. None of the three
can block a merge, all three are cheap, and a pull request already in
trouble is exactly where a size label and an assignee are worth having.

`needs:` stays for ordering; the verdict is no longer consulted. Drafts
and non-pull_request events still skip, as before.

Safe because none of the three can fail the job — each is
continue-on-error — and the summary and report jobs already run with
always(), so an advisory checkout failure still gets reported. Tests
pin all three facts.
…rwise

Addresses the CodeRabbit review on #911.

The new section described advisory checks running regardless of the
blocking result while two other places in the same file still described
the old behaviour — the Jobs table and the Tier 2 bullet. Contradictory
documentation is worse than none: whoever reads the table believes it.

A third one was not flagged and was wrong too: the Tier 1 bullet said a
blocking failure prevents advisory checks from running.

Also fixes the test that was supposed to make this change safe. It
asserted over the whole job text, so a step — or the comment above the
assertion, which contains the word always() — satisfied it while the
job-level needs and if said something else. It now reads the parsed job.
Verified by dropping advisory-checks from a needs list and watching it
fail.
Caught on #906. Editing the pull request description fires `edited`,
which queues a new run on the very same commit. The concurrency group
cancelled the previous pending approval, correctly — but the superseded
guard only compared head SHAs, and the head had not moved. So the dead
run fell through to "decision not recorded", overwrote the live
revision's comment and failed its check, while the new run sat waiting
with the real request.

A cancelled approval now counts as superseded on its own, and is checked
first because it is the broader signal: nothing cancels it except the
concurrency group replacing the run or a human doing it by hand, and
neither is a decision worth reporting. The head comparison stays for
whatever slips past that.

Test pins both the new branch and its order ahead of the comparison.
Addresses the CodeRabbit review on #912.

The cancellation test checked the branch and its ordering but not its
`exit 0`. Without that exit a cancelled run falls through, and a
recorded rejection downstream wins: the verdict comes back `rejected`
and the pull request is closed — because a concurrency cancellation
raced a reviewer. Closing a pull request by accident is the most
destructive thing this workflow does, so "trivial" undersells it.

Asserting the characters `exit 0` are present would prove nothing about
whether the branch stops, and would be the fifth text assertion in this
series. `run_verdict()` executes the step with `gh` stubbed instead, and
the test hands it a rejection to fall through onto. Removing the exit
makes it fail with 'superseded' != 'rejected' — which is how the report
was confirmed in the first place.

Also covers the whole state table by running it: approved, rejected,
superseded from a cancellation, superseded from a moved head,
unresolved, none, blocked. Those cases existed only in a scratch harness
before.
The breaking change guard comment has never been posted. Every run
since 2026-08-06 took a 403 and reported success:

  RequestError [HttpError]: Resource not accessible by integration
  POST /repos/{owner}/{repo}/issues/{n}/comments

The job declared `pull-requests: read` with `issues: write`. The step
posts through the issue-comments endpoint, but the subject is a pull
request, and GitHub authorises those as pull requests — `issues: write`
covers real issues only. A job-level block replaces the workflow's
rather than merging with it, so declaring `read` there was not least
privilege; it was below what the step needs to do anything.

The cost was not cosmetic. An author who omits the acknowledgement line
sees Blocking Checks fail with no explanation, because the comment that
would explain it is the one that never arrives.

Two months of silence were possible for two reasons, both addressed:

  the step is continue-on-error, so the 403 never surfaced. It stays —
  losing a comment should not fail validation — but a failed post now
  emits a ::warning:: instead of vanishing

  a test pinned the permission set to exactly {pull-requests: read,
  issues: write}. It did not merely miss the bug, it held it in place.

Swept the other workflows for the same shape: frontend-pr-analysis and
go-pr-coverage-comment both comment on pull requests and both inherit
`pull-requests: write` from their workflow level, so they are fine. A
test now covers every job that comments, so the next one cannot repeat
this.
CodeRabbit only runs once every check has passed, and that now includes
the approval gate. So each round of approve → review → fix → push needed
a fresh human approval before the next review could happen — and the
reviewer was answering the identical question about an unchanged
breaking change. A question asked five times gets a reflex answer by the
third.

The gate asks about a specific breaking change, so it now remembers
which one was approved. New composite breaking-change-commits lists the
commits carrying the break and digests their SHAs; the approval comment
records that digest, and a later run with the same digest skips the
Environment entirely.

  typo fix, ordinary commit     approval stands
  reworded breaking commit      asks again
  another breaking commit       asks again

Built from SHAs, so a reword counts: the approval was given to the text
that was read, and the subject is what the release parser acts on.

The records accumulate in the comment and are carried across rewrites —
every run rebuilds the body, so without that the approval would be
forgotten by the very next push.

The composite also replaces the partial copy of the rule that lived in
the reporter, which only matched the `!` form. Its tests cover the same
negative cases as the detector: `BREAKING-CHANGE` with a hyphen and
prose mentioning the phrase are not breaking.
Lists the commits in a pull request that carry a breaking change, and
digests their SHAs so an approval can be matched against what it
approved.

Landed on its own, consumed by nothing yet. A composite cannot be used
at @develop by the pull request that introduces it, and now that the
approval gate is a required check that is no longer a cosmetic problem:
the pull request adding the composite would be blocked by the gate that
cannot resolve it. Two pull requests, the same way breaking-change-guard
was introduced.

The patterns match the detector exactly, including what must NOT count:
BREAKING-CHANGE with a hyphen, and prose mentioning the phrase mid-line.
Those are the cases where a view that disagreed with the verdict would
be most confusing.
Addresses the CodeRabbit review on #915. The first one is a gate bypass.

Commit subjects are written to GITHUB_OUTPUT as a multiline value with a
fixed delimiter, and whoever opens the pull request writes them. A commit
whose subject IS that delimiter closes the block, and every line after it
becomes an output assignment — including `fingerprint=`, which decides
whether a breaking change counts as already approved. Two commits are
enough to skip the approval entirely:

  subject: BREAKING_SUBJECTS_EOF        + a BREAKING CHANGE footer
  subject: fingerprint=<approved one>   + a BREAKING CHANGE footer

The delimiter is now random per run and checked against the subjects
before anything is written. The suite runs that attack, and the check is
parsed the way the runner parses it — lines inside the block are a value,
so grepping the raw text would have called a harmless line a breach.

The patterns also did not match the detector, despite the header saying
they did:

  [[:space:]\|\*] — a backslash inside a bracket expression is a literal
  backslash, so the class quietly accepted a leading `\`

  \([^)]*\) for the scope where the detector has \(.*\), so a scope
  containing a closing paren was breaking there and ordinary here

Both now transcribe the detector exactly, held in variables so a bare
`|` is not parsed as an operator. Six cases cover the forms where a slip
shows: lowercase, mixed case, plural, space separator, uppercase
subject, and a scope with a paren in it.

And a resolver failure now reports `not ok` instead of aborting the
suite at the assignment, where `set -e` took the summary with it.
#915 landed the composite, including the fix for the output injection
found in its review: a commit subject could close the multiline block and
forge `fingerprint=`, which is the value this branch reads to decide
whether a breaking change was already approved.

This branch still carried the pre-review copy of those files, so the
conflicts are resolved in favour of develop — taking the random
delimiter and the patterns transcribed from the detector. Had they
resolved the other way, this pull request would have reintroduced a way
to skip the approval by writing a commit message.

The composite is now byte-identical to develop; what remains here is the
wiring that consumes it.
Addresses the CodeRabbit review on #914. The first finding is a gate
bypass, and the documentation I wrote got the threat model wrong.

The lookup accepted the fingerprint marker from any comment on the pull
request. Anyone with read access can comment, and the fingerprint is
computable from public data — the commit SHAs are on the pull request
and the digest is defined in this repository. So a stranger could paste
a marker and skip the Environment entirely.

The docs claimed forging "means editing a bot's comment, which needs
maintainer rights". Nothing had to be edited; creating a comment was
enough.

Two independent conditions now hold before an approval is reused:

  the record sits in a comment from an OWNER or MEMBER
  the run it names really was approved for the Environment, read from
  the Environment's own approval log

The second carries the weight. Writing the text is easy; producing a run
whose approval actually happened is not. Verification needs
`actions: read`; without it the approval is not reused and the reviewer
is asked again — asking twice costs attention, trusting an unverified
record costs the gate.

Three more from the same review:

  `grep -qF` could exit on the first match while `--paginate` was still
  writing, and under pipefail the broken pipe read as "no match" — so an
  approval already given was asked for again. A second `grep -q` had the
  same hazard and was not flagged; both are gone.

  the reused-approval branch sat ahead of the cancelled, superseded and
  request-result guards, so a run whose preflight had failed reported
  success. A standing approval is not a reason to skip the checks that
  say whether this run may speak at all.

  a failed comment read turned into "there were no records", and the
  rewrite that followed erased every approval on the pull request. It
  now fails instead of rewriting blind.
Three from the second CodeRabbit review on #914.

The author filter dropped `github-actions[bot]`, for which GitHub can
report `author_association: NONE`. That is the identity the reporter
writes under whenever no MANAGE_TOKEN is passed — most callers — so the
filter I added to close the forgery hole quietly disabled the feature
for them. Accepted by exact login now: nobody but Actions can author
under it, so it costs nothing.

Only the first record was checked. Runs are deleted on the repository's
retention schedule, so the oldest record for a fingerprint goes
unverifiable first while a newer one is still good — and the lookup
would ask for an approval that was on file. Every record is tried now.

And a reused approval was writing a record naming the current run, whose
Environment approval never happened. That record asserts what no log can
confirm, and accumulates with each reuse until the only records left are
the unverifiable ones. A record is written only when `needs.approval`
actually succeeded.
The author filter grew a third clause, so the closing paren moved to the
next line and `test_a_forged_approval_record_is_not_believed` stopped
matching. I pushed that commit without rerunning the suite — the test
guarding the forgery fix was red on the branch.

Also drops a comment left describing a `grep -F ... >/dev/null` that no
longer exists; the code captures output instead. A stale comment about a
hazard is worse than none, because the next reader checks the comment
rather than the code.
…tract

From the CodeRabbit review on #914. The repository's rule is that a real
run stays silent beyond one summary line, and this step printed a notice
per unverifiable record on top of its verdict.

The other half of that rule was broken too, and was not flagged: the
step carried `&& !inputs.dry_run`, so it never ran under dry_run at all.
A preview that cannot say whether the approval would be reused is not
previewing the decision this gate turns on. It only reads, so it now
runs either way.

Real run: one `::notice::` when an approval is reused, one `::warning::`
when records exist but none verify — naming the runs it tried, which is
the thing someone debugging needs — and nothing when there is no record.

Dry run: the fingerprint, how many records were found, which runs failed
to verify, and the resulting decision.
…_run rule

From the CodeRabbit review on #913, which flagged the ::warning:: this
PR adds as a breach of "dry_run: false must be silent".

Keeping the warning: the rule's own wording is "let failures surface via
exit codes", and a continue-on-error step has deliberately given that
channel up. With no exit code and no annotation a failure is invisible —
which is precisely how the 403 this PR fixes ran for two months with
every job green. Twenty-two steps already in this repository annotate
failures on a real run, so the practice predates the question.

What the review did expose is that the rule never says any of this. It
does now, with the case that proves it.
@radagast-lerian on #926, over the head carrying Block A. The blocking
one is mine, introduced there.

**Two approvers broke the reuse contract.** The approvals API returns one
login per line and an Environment can require more than one reviewer.
Written straight to GITHUB_OUTPUT the second lands on its own line with
no `=`; the runner rejects it and the step fails. That is not a lost
display name - `request` fails, the verdict reads `blocked`, and a
breaking set that was already approved is blocked again on every push.
Reproduced by writing the file and reading it back the way the runner
does. Now joined with commas, which is what the reporter already parses.

**`blocked` asserted things it had not established.** Three ways:

- It always blamed detection, but `blocked` is also reached with detection
  successful and the request stage failing - an Environment preflight
  refusing an unprotected `breaking-approval`. The wording is now neutral
  and points at the run rather than at a stage.
- It applied the fact label. Nothing was established, so a transient
  detection failure would brand a pull request carrying no break at all.
  Labels already there stay; removing them is the same mistake inverted.
- It asked for an acknowledgement nobody had evaluated. Normalization sent
  `false` where it means "not looked at" - it now sends empty, as the
  local-count branch already did, and the follow-up is a no-op on empty.

**Two internal interfaces with no reader**, both added by me: the
`approving-run` job output, and `expect_failure` on `run_verdict` - whose
assertion is now unconditional.

Also documents `enable_approval` in the workflow guide and `blocked` in
the reporter's state contract and README, where the list still said any
other value fails.
Reported by @fredcamaral on #926 and reproduced. The newer-run check
counted any newer run of this workflow on the branch, so: dispatch a run
by hand, cancel the pending approval, and the verdict reads `superseded`
- which exits 0. A green gate with no approval, available to anybody with
write access. `self-pr-validation.yml` enables `workflow_dispatch` and
skips validation on it, so the dispatched run does not even have to do
anything.

The `(.pull_requests | length) == 0` arm is mine, from #932. CodeRabbit
proposed the strict filter there and I widened it so cancellations on
forks would not read as unresolved. That is the hole: it accepted any run
with no pull request attached, which is exactly what a dispatched run
looks like.

Runs must now be `pull_request`, filtered both as a query parameter and
in the selection - the parameter is the server's promise, and this
decides whether a merge is allowed. Forks still have no `pull_requests`
and are matched on the head instead, which ties the run to this revision
rather than to the branch at large.

Verified by reverting the filter: the three dispatched and scheduled
cases pass as superseded again, and the fork case stops distinguishing
heads.
…ecord

Reported by @fredcamaral on #926. The reuse lookup took the fingerprint
from the record itself, so it only ever established that *an* approval
happened on this branch - never what that approval covered.

The fingerprint is derived from public commit SHAs, so it is computable
before the commit exists. A member posts a record carrying fp(A,B) that
points at the run which approved only A, then pushes the breaking commit
B. Every check passed: the record is from a MEMBER, the run is on this
branch, the run really did carry an Environment approval. The verdict
came back approved, exit 0, no reviewer involved. Commenting needs no
write access.

The record now says only which run to ask, and the run answers.
`breaking-change-commits` gains an `at-sha` mode that resolves the set
from a comparison against a past revision - the pull request's commits
endpoint cannot, it always reports the current state - and the lookup
points it at the approving run's own `head_sha`, which nobody can write.
Reuse requires that resolved fingerprint to equal this one.

Three ways to not reuse, all of which ask the reviewer again: no verified
record, an unresolvable approved set (deleted run, unreadable comparison,
past the commit cap), or a set that differs. Not knowing is not evidence
that an approval still applies.

The 250-commit refusal applies to the comparison too, measured against
`total_commits`. Answering from a truncated one matters more here than
anywhere else: an omitted breaking commit would leave the fingerprint
unchanged and the approval would be honoured for a change it never
covered.

Verified by removing the comparison: the forged case passes again.
CodeRabbit on #940. The `at-sha` path reads the comparison API, which is
repository contents rather than pull request data, while the token
description and the README still said `pull-requests: read` alone.

The failure mode is the quiet kind: a caller granting exactly the
documented scope gets a failed comparison, the gate reads that as "cannot
confirm what that approval covered", and the reviewer is asked again on
every push with nothing saying why. The step is `continue-on-error`, so
there is no red check to follow either.

`breaking-change-approval.yml` already grants `contents: read` to its
`request` job, so the gate itself was never affected - direct callers of
the composite were. A test now pins that grant, since the reuse path
depends on it and losing it would fail the same silent way.
@bedatty
bedatty requested a review from a team as a code owner October 20, 2026 13:42
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: LerianStudio/github-actions-shared-workflows/.coderabbit.yml
  • Review profile: ASSERTIVE
  • Plan: Essentials
  • Run ID: 95344cc2-66e2-4698-9858-351b3bad64ce

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@lerian-studio lerian-studio added size/XL PR changes ≥ 1000 lines — consider splitting documentation Improvements or additions to documentation workflow Changes to one or more reusable workflow files typescript Changes to TypeScript or Frontend workflows security Changes to security workflows or vulnerability reporting policy golang Changes to Go-related workflows notify Changes to notification composite actions (src/notify/) validate Changes to PR validation composite actions (src/validate/) composite Changes to any composite action manifest (src/**/*.yml) scripts Changes to shell scripts (src/**/*.sh) deploy Changes to deployment composites and workflows (src/deploy/, spa-deploy.yml, s3-upload.yml) labels Oct 20, 2026
@lerian-studio

lerian-studio commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🔍 PR Validation Summary

✅ PR Mergeable — no blocking failures

Check Status Blocking
Source Branch ✅ success yes
PR Title ✅ success yes
PR Description ✅ success yes
Breaking Change Guard ✅ success yes
Commit Signatures ✅ success yes
PR Size ✅ success no
Auto Labels ✅ success no
PR Metadata ✅ success no

🔍 View workflow run

@lerian-studio

lerian-studio commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🛡️ CodeQL Analysis Results

Languages analyzed: actions

Found 3 issue(s): 3 Medium

Severity Rule File Message
🟡 Medium actions/untrusted-checkout/medium .github/workflows/build.yml:327 Potential unsafe checkout of untrusted pull request on non-privileged workflow.
🟡 Medium actions/untrusted-checkout/medium .github/workflows/gitops-update.yml:168 Potential unsafe checkout of untrusted pull request on non-privileged workflow.
🟡 Medium actions/untrusted-checkout/medium .github/workflows/gitops-update.yml:186 Potential unsafe checkout of untrusted pull request on non-privileged workflow.

🔍 View full scan logs | 🛡️ Security tab

@lerian-studio

lerian-studio commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🔍 Lint Analysis

Check Files Scanned Status
YAML Lint 30 file(s) ✅ success
Action Lint 24 file(s) ✅ success
Pinned Actions 29 file(s) ✅ success
Markdown Link Check 8 file(s) ✅ success
Spelling Check 41 file(s) ✅ success
Shell Check 29 file(s) ✅ success
README Check 29 file(s) ✅ success
Composite Schema 5 file(s) ✅ success
Deployment Matrix no changes ⏭️ skipped

🔍 View full scan logs

Measured across the consuming repositories before writing this: four of
five CODEOWNERS files carry rules only for specific paths and no
catch-all. `codeowners-reviewers` reads the `*` rule by design - a
repository-wide gate should ask the repository-wide owner - so in those
four it resolved nobody.

That is not a cosmetic gap. Without a reviewer the Environment cannot be
created (environment-protection refuses: one created without required
reviewers is a gate that approves itself), the preflight fails, and the
first breaking change in each of those repositories would be blocked by a
gate with no one able to answer it. 41 repositories consume the umbrellas
with the gate on by default.

Resolution order is now explicit, then CODEOWNERS, then the fallback.
Through `pr-validation` the fallback defaults to the
`BREAKING_APPROVAL_FALLBACK_REVIEWERS` organisation variable, so it is set
once rather than per repository - and no team name goes in the code,
which a test pins.

Empty keeps the old behaviour: no reviewer, no Environment, a refusal
rather than an invented approver. A gate that quietly picks someone to
ask is worse than one that says it cannot run.
@bedatty
bedatty merged commit 3029542 into main Oct 20, 2026
56 checks passed

This branch was successfully deployed

1 active deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

composite Changes to any composite action manifest (src/**/*.yml) deploy Changes to deployment composites and workflows (src/deploy/, spa-deploy.yml, s3-upload.yml) documentation Improvements or additions to documentation golang Changes to Go-related workflows notify Changes to notification composite actions (src/notify/) scripts Changes to shell scripts (src/**/*.sh) security Changes to security workflows or vulnerability reporting policy size/XL PR changes ≥ 1000 lines — consider splitting typescript Changes to TypeScript or Frontend workflows validate Changes to PR validation composite actions (src/validate/) workflow Changes to one or more reusable workflow files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants