Repository navigation
Conversation
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.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Contributor
🔍 PR Validation Summary✅ PR Mergeable — no blocking failures
|
Contributor
🛡️ CodeQL Analysis ResultsLanguages analyzed: Found 3 issue(s): 3 Medium
🔍 View full scan logs | 🛡️ Security tab |
Contributor
🔍 Lint Analysis
|
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.
7 of 13 tasks
guimoreirar
approved these changes
Oct 20, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
GitHub Actions Shared Workflows
Description
Release
developtomain. 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-approvalGitHub 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-validationandjs-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:
fp(A,B)pointing at the run that approved onlyA, then pushB. The record now says only which run to ask:breaking-change-commitsgained anat-shamode, and the approved set is resolved from the approving run's ownhead_sha.superseded, which exits 0. Onlypull_requestruns 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
RADAGAST_AUTHORS. Unset or empty behaves exactly as before, so nothing changes outside LerianStudio.src/security/codeql-{init,analyze}behind the workflows (fix(security): realign the codeql-action refs the bump left behind #931).Test coverage
Three composite suites —
pr-gate-scope(32 cases),breaking-change-commits(28) andcodeowners-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
@developrefs 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 workflowfix: Bug fix in a workflow (incorrect behavior, broken step, wrong condition)docs: Documentation only (README, docs/, inline comments)test: Adding or updating testschore: Dependency bumps, config updates, maintenanceBreaking Changes
None.
Two behaviours of
breaking-change-approval.ymlchanged after its first release and are documented rather than left silent — see Behaviour changes since v1.84:labelsurvives the decision. It is a fact about the change, not a waiting marker; the transient role moved to the newpending_label.has_breaking_changesreports detection out of scope, instead of being forced tofalse.Neither is classified as breaking: the gate shipped in
v1.84.0on 2026-10-15, and none of the umbrellas expose an input forlabel— onlyforce_labelsandskip_labels— so a caller on the normal path could not have configured one. The umbrellas also do not surface the gate'shas_breaking_changes; theirs comes from the unchangedbreaking-change-guardjob.Testing
Every change was validated in its own PR into
develop. In aggregate on this head:actionlint,shellcheck,yamllintandtyposcleanThe 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).
@this-branchor the beta tagCaller repo / workflow run: this repository, through
self-pr-validationon every PR listed above; the gate's own runs on #921 through #924.Related Issues
Supersedes #926.