Skip to content

ci(api-review-gate): publish the gate as a commit status - #3440

Draft
myasnikovdaniil wants to merge 2 commits into
mainfrom
ci/narrow-api-gate-review-trigger
Draft

myasnikovdaniil wants to merge 2 commits into
mainfrom
ci/narrow-api-gate-review-trigger

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #3262 — targets ci/fork-pr-e2e and reuses its commit-status publishing infrastructure. Merge after it.

The problem

The API Review Gate's merge context was its job name. That inherits two GitHub behaviours which together make it both unenforceable and self-poisoning, and the second one is not widely known — it is why this PR replaced its own first approach.

A required job that skips counts as PASSED by branch protection. Any path that skips the evaluating job hands out a free pass. This is the same hole that let fork PRs merge with e2e never having run (#3257), which #3262 fixes for E2E Tests.

Check runs are not deduplicated per name. Every workflow run gets its own check suite, so GET /commits/{sha}/check-runs?filter=latest returns a byte-identical set to filter=all — filter=latest only collapses re-runs within a suite. Two runs on one head SHA therefore leave two verdicts standing for one context name, and the aggregate is worst-of, so a single cancelled run poisons the context until the next push. Measured on this repo: 41d1153c carries four check runs named Require API owner review for sizeable API changes (one cancelled, three success); 0c14d6cf carries E2E Tests as cancelled at 12:51:22 and skipped at 12:51:23, with every other context green and a rollup of FAILURE — the later skipped did not supersede the earlier cancelled.

Both problems are live today, not hypothetical. 41d1153c and 4b1966c5 each show a pull_request run cancelled by a pull_request_review run seconds later: workflow-level concurrency kills the anchor before any job-level if: is evaluated, so a review bot commenting during the ~30s evaluation leaves a stale cancelled as that SHA's only api-gate verdict.

The fix

Publish the gate as a commit status. The evaluating job is renamed off the gate context, and API Review Gate is published by a dedicated reporting job — api-gate-report for same-repo PRs, and a privileged workflow_run (api-review-gate-fork.yaml) for forks. Commit statuses are deduplicated per context with the newest winning (verified: 0c14d6cf has six CodeRabbit statuses; the combined status returns exactly one), which buys the invariant the whole design rests on: skipped and cancelled runs post nothing, so declining to post can only preserve a verdict some real run reached — never invent one.

Stop re-evaluating on review-bot comments. Plain commented reviews — all the bots ever post — cannot move a verdict that counts only APPROVED reviews, so they skip the evaluation instead of rebuilding the detector each time. This is now safe on its own terms: the reporting job skips in lockstep and writes nothing, leaving the previous status untouched. It no longer depends on skip-to-green, nor on any assumption about how duplicate check runs are resolved.

Split the concurrency group so a run that computes no verdict cannot cancel one that does. Bot-comment runs get their own key; pushes and actionable reviews share eval and legitimately supersede each other. This also fixes the pre-existing cancellation defect above, which no amount of if: tuning addresses.

Fork PRs need the privileged half. A fork's GITHUB_TOKEN is read-only on both the pull_request and the pull_request_review trigger — the restriction is not pull_request-specific — so no run of the gate workflow can post a status for a fork. The companion runs from the default branch after the gate run completes. It checks nothing out, builds nothing, and executes no fork-authored code: its only inputs are the triggering run's conclusion and head SHA, both base-repo payload data, with the SHA regex-validated before it reaches the API. That is why this is a workflow_run split and not a pull_request_target run.

Testing

  • actionlint clean on both workflows; yaml.safe_load parses; folded concurrency and if: expressions verified to render as intended single-line strings.
  • zizmor (pre-commit, --offline) passes — but note its dangerous-triggers and template-injection audits are disabled repo-wide in .github/zizmor.yml, so that pass is not meaningful evidence for a new privileged workflow. Audited manually against both instead: no checkout, no fork code executed, every payload value routed through env: rather than shell interpolation, and statuses: write confined to the reporting jobs (the evaluating job keeps contents: read + pull-requests: read).
  • The GitHub behaviours the design depends on were measured, not assumed — the commits and API calls are quoted above and are reproducible read-only.

Not yet exercised live, and the reason this is a draft: the fork companion cannot run until it is on the default branch (workflow_run only runs from there), the same constraint that keeps #3262 in draft. The same-repo path is exercisable normally. The commented-review skip only truly exercises when a bot comments on a live PR.

Repo-admin prerequisite

For the gate to block a merge, branch protection must require the API Review Gate status context — not a job name, and no app-id pin. This is a repository setting outside this diff. Until it is set the status is advisory, exactly as the job name was, so this PR is not a change in enforcement — only in whether enforcement is possible and correct once switched on.

NONE

@github-actions github-actions Bot added size/S This PR changes 10-29 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review labels Jul 23, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot dosubot Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt labels Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The workflows now isolate fork pull-request execution from same-repository execution. Fork builds export OCI archives for privileged workflows. Separate jobs publish API Review Gate and E2E Tests commit statuses after validation and testing.

Changes

Fork workflow isolation

Layer / File(s) Summary
Export fork build artifacts
.github/workflows/pull-requests.yaml, hack/common-envs.mk, hack/common-envs_test.bats, packages/core/talos/Makefile
Fork builds export single-image OCI archives and upload them as artifacts. Same-repository builds retain registry publishing. Tests cover both paths.
Report same-repository gate statuses
.github/workflows/api-review-gate.yaml, .github/workflows/pull-requests.yaml
API review evaluation and E2E execution are separated from required commit-status reporting. Commented reviews do not replace existing API verdicts.
Run the fork API gate
.github/workflows/api-review-gate-fork.yaml
A privileged workflow validates fork run data and publishes the API Review Gate commit status.
Run and report fork E2E tests
.github/workflows/e2e-fork.yaml, docs/agents/e2e-testing.md
The privileged workflow validates fork metadata and artifacts, publishes images, runs isolated E2E tests, collects reports, and publishes the E2E Tests status. Documentation describes the workflow split.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

  • cozystack/cozystack#3262: Shares the fork E2E workflow, OCI export, status reporting, documentation, and Talos changes.
  • cozystack/cozystack#3449: Also changes E2E execution and full-e2e label handling in .github/workflows/pull-requests.yaml.

Suggested labels: kind/bug

Suggested reviewers: lllamnyp, kvaps, lexfrei

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the API Review Gate’s commit-status publication change, which is a central part of the pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/narrow-api-gate-review-trigger

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/api-review-gate.yaml:
- Around line 41-43: Update the workflow’s concurrency group so non-actionable
pull_request_review events use a distinct group, preventing skipped runs from
cancelling the active pull_request gate. Preserve the existing shared group for
actionable events and keep the job condition around github.event_name and
github.event.review.state unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d12930e4-b50a-41ca-9c1b-8de396135fd9

📥 Commits

Reviewing files that changed from the base of the PR and between 18e459a and f44eedf.

📒 Files selected for processing (1)
  • .github/workflows/api-review-gate.yaml

Comment thread .github/workflows/api-review-gate.yaml
@IvanHunters

Copy link
Copy Markdown
Collaborator

Review summary

The noise problem this PR fixes is real, and the intent is right. But the safety argument in the PR description only covers the case where the push-anchor run passed; it does not cover the case where the anchor run failed — which is exactly the case the gate exists to enforce. Flagging one blocking-severity concern to verify before merge.

[CRITICAL — please verify] A commented-review skip supersedes a failing anchor status on the same commit, turning the required gate green

The PR description reasons only about the pass case: "the real success from the pull_request run on the head SHA remains." Now consider a sizeable API change with no owner approval:

  1. Push commit X (sizeable API change, no approval). pull_request: synchronize → run A evaluates → reports the check Require API owner review for sizeable API changes = failure. Correct: red, merge blocked.
  2. A review bot (CodeRabbit / dosubot — both named in the PR body, both active here) posts a plain comment on X. pull_request_review: submitted, state=commented → run B. The new job-level if: → skipped → GitHub creates a check run with the same context name and conclusion skipped, timestamped after run A.
  3. Branch protection evaluates the latest check run per context on the head SHA. That is now run B (skipped), which required-status-check evaluation treats as non-blocking / satisfied — the very property this PR relies on for the pass case.
  4. Net: the earlier failure is superseded by skipped. The required gate shows green and the sizeable API change becomes mergeable without an API-owner approval.

This is not a rare race — it's the normal flow. Review bots comment automatically right after a push, so on essentially every sizeable API PR with a bot enabled, the failing gate would be overwritten by a skipped shortly after it fails.

Regression vs. current main: before this change, the commented-review run executes the full job and re-reports the real verdict (failure again), so the terminal status after a trailing comment is always a real evaluation and the gate stays enforced. After this change, a trailing commented event leaves skipped as the authoritative status.

Secondary interaction with concurrency (same root cause): the top-level concurrency group is shared between the pull_request anchor and pull_request_review triggers with cancel-in-progress: true. Workflow-level concurrency cancels the in-progress run before the job if: is evaluated. So a commented review landing during the ~30s anchor run cancels that run and then skips its own job — same outcome: no real evaluation is the terminal state.

Load-bearing assumption to verify empirically before merging (don't take my word for it): "for one check-context name on one commit, the most-recent check run wins, and a later skipped run supersedes an earlier failure." This is standard GitHub required-status-check behavior, but the whole safety of this change hinges on it and it wasn't tested. Concrete ~5-minute repro: on a scratch PR that fails this gate, post a plain review comment and confirm whether the required check flips from red to green (skipped). "YAML parses clean" doesn't exercise any of this. Impact is also conditional on this check actually being a required status check in branch protection (which the workflow header states is its purpose).

Direction for a fix (the tension is inherent — needs a design choice, not a one-liner)

The pull_request_review trigger exists specifically so an approval can flip the required context green without a new push, which means review-triggered runs must write that same context. Any path that writes skipped to that context can therefore supersede a real failure. Options worth weighing:

  • Keep re-running fully on all review events and cut cost differently — e.g. cache/prebuild api-gate so the commented re-run is cheap instead of skipped, while still re-reporting the true verdict.
  • On commented events, run a minimal step that re-reports the anchor's current conclusion into the same context instead of skipping, so the context is never downgraded to skipped.
  • Don't have review-triggered runs share the required context at all, and re-drive the anchor context another way on approval — larger redesign.

Non-blocking notes

  • The inline comments (workflow header and the on: block) are genuinely good — clear intent, helpful for the next maintainer.
  • The boolean logic of the if: is correct for its stated intent: pull_request always runs; approved / changes_requested / dismissed still re-run (review.state on a dismissed action is "dismissed", not "commented"); only commented skips.
  • The underlying problem (a dozen cancelled runs per PR from bot-comment churn) is real and worth fixing — the objection is to how, not whether.

The gate's merge context was its job name, which inherits two GitHub
behaviours that together make it both unenforceable and self-poisoning.

A required *job* that skips counts as PASSED by branch protection, so
any path that skips the evaluating job hands out a free pass — the hole
that let fork PRs merge with e2e never run (#3257). And check runs are
not deduplicated per name: every workflow run gets its own check suite,
so `GET /commits/{sha}/check-runs?filter=latest` returns the same set as
`filter=all`, and the rollup is worst-of. Two runs on one head SHA
therefore leave two verdicts standing for one context name, and a
cancelled run poisons the context until the next push. Commit statuses
are deduplicated per context, newest wins.

Rename the evaluating job off the gate context and publish "API Review
Gate" as a commit status: `api-gate-report` for same-repo PRs, and a
privileged workflow_run for forks, whose GITHUB_TOKEN is read-only on
both the pull_request and the pull_request_review trigger and so can
never post a status itself. Skipped and cancelled runs post nothing, so
declining to post can only ever preserve a verdict some real run
reached — it can never invent one.

That invariant is what makes it safe to stop re-evaluating on the plain
`commented` reviews the review bots post: such a review cannot move a
verdict that counts only APPROVED reviews, and the run that skips now
leaves the previous status untouched instead of overwriting it.

Also split the concurrency group so a run that computes no verdict
cannot cancel one that does. A bot commenting during the ~30s
evaluation used to cancel it and then contribute nothing, leaving a
cancelled run as that SHA's only api-gate verdict (41d1153, 4b1966c).

For the gate to actually block a merge, main branch protection must
require the "API Review Gate" status context; until then it is
advisory, as the job name was.

Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
The "E2E Tests" commit-status pattern in §10 now has a second consumer
in api-review-gate.yaml, and the two GitHub behaviours that make it
load-bearing were not written down anywhere.

Check runs are not deduplicated per name across workflow runs — each run
gets its own check suite, so filter=latest returns the same set as
filter=all and the aggregate is worst-of, meaning a cancelled run
poisons a context until the next push — while commit statuses are
deduplicated per context with the newest winning. And the fork
read-only token restriction covers pull_request_review as well as
pull_request, so an approval on a fork PR cannot publish a status
either, which is why both gates need a privileged workflow_run half
rather than only handling the push path.

Assisted-By: Claude <[email protected]>
Signed-off-by: Myasnikov Daniil <[email protected]>
@myasnikovdaniil
myasnikovdaniil force-pushed the ci/narrow-api-gate-review-trigger branch from f44eedf to 179d59a Compare August 3, 2026 12:30
@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files and removed size/S This PR changes 10-29 lines, ignoring generated files labels Aug 3, 2026
@myasnikovdaniil myasnikovdaniil changed the title ci(api-review-gate): skip review-bot comment events ci(api-review-gate): publish the gate as a commit status Aug 3, 2026
@myasnikovdaniil
myasnikovdaniil changed the base branch from main to ci/fork-pr-e2e August 3, 2026 12:34
@myasnikovdaniil
myasnikovdaniil marked this pull request as draft August 3, 2026 12:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/core/talos/Makefile (1)

17-17: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider removing a stale archive before the copy.

skopeo copy to an existing oci-archive: destination can fail or append a second manifest, which then breaks the single-image assumption in e2e-fork.yaml. A repeated make image-talos in the same workspace hits this. Add an rm -f before the copy.

♻️ Proposed change
-	$(if $(strip $(OCI_EXPORT_DIR)),mkdir -p "$(OCI_EXPORT_DIR)"; skopeo copy "$$SRC" "oci-archive:$(OCI_EXPORT_DIR)/talos.oci.tar:$(IMAGE_TAG)",skopeo copy "$$SRC" docker://$(REGISTRY)/talos:$(IMAGE_TAG)); \
+	$(if $(strip $(OCI_EXPORT_DIR)),mkdir -p "$(OCI_EXPORT_DIR)"; rm -f "$(OCI_EXPORT_DIR)/talos.oci.tar"; skopeo copy "$$SRC" "oci-archive:$(OCI_EXPORT_DIR)/talos.oci.tar:$(IMAGE_TAG)",skopeo copy "$$SRC" docker://$(REGISTRY)/talos:$(IMAGE_TAG)); \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/talos/Makefile` at line 17, Update the OCI export branch in the
Makefile command to remove the existing talos.oci.tar archive with rm -f
immediately before invoking skopeo copy, while leaving the registry-copy branch
unchanged.
hack/common-envs_test.bats (1)

24-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pass COZYSTACK_VERSION=0 in these three tests to keep them hermetic.

hack/common-envs.mk runs a version probe at parse time. When COZYSTACK_VERSION is empty, it executes git remote add upstream … and git fetch upstream --tags (lines 128-132). Tests 4 and 5 already set COZYSTACK_VERSION=0, and e2e-fork.yaml sets it for the same reason. Tests 1-3 omit it, so a shallow or tagless checkout makes them mutate git remotes and reach the network.

♻️ Proposed change
-  out=$(make -n -C packages/system/cozystack-controller image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc BUILDER=b)
+  out=$(make -n -C packages/system/cozystack-controller image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)

Apply the same addition to the capi-providers-cpprovider and talos invocations.

Also applies to: 37-37, 51-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/common-envs_test.bats` at line 24, Set COZYSTACK_VERSION=0 on the three
hermetic make invocations in hack/common-envs_test.bats, including the shown
system/cozystack-controller command and the capi-providers-cpprovider and talos
invocations. Preserve the existing arguments and command structure while
preventing parse-time version probing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/api-review-gate-fork.yaml:
- Around line 44-67: Bind fork status publication to the triggering pull
request: in .github/workflows/api-review-gate-fork.yaml lines 44-67, grant
pull-requests: read, query pull requests associated with HEAD_SHA, and publish
nothing unless an open request has head.sha === HEAD_SHA and head.repo.full_name
matching github.event.workflow_run.head_repository.full_name; in
.github/workflows/e2e-fork.yaml lines 143-152, extend the existing
assoc.data.find predicate with those same SHA and repository checks so all
decisions and status targets use the triggering fork’s pull request.

In @.github/workflows/e2e-fork.yaml:
- Around line 304-322: Update the “Derive trusted image allowlist (base tree)”
step so cozystack-operator is always included, matching the installer targets
invoked by the fork build even when image-operator’s prereq output is skipped.
Prefer deriving the name from the same installer/image targets; otherwise
explicitly add cozystack-operator to _allow.txt while preserving the existing
archive-derived entries and validation.

In `@docs/agents/e2e-testing.md`:
- Line 103: Update the section 10 heading to refer to the “E2E Tests” commit
status instead of a check-run, keeping the existing same-repo versus fork
workflow wording unchanged.

---

Nitpick comments:
In `@hack/common-envs_test.bats`:
- Line 24: Set COZYSTACK_VERSION=0 on the three hermetic make invocations in
hack/common-envs_test.bats, including the shown system/cozystack-controller
command and the capi-providers-cpprovider and talos invocations. Preserve the
existing arguments and command structure while preventing parse-time version
probing.

In `@packages/core/talos/Makefile`:
- Line 17: Update the OCI export branch in the Makefile command to remove the
existing talos.oci.tar archive with rm -f immediately before invoking skopeo
copy, while leaving the registry-copy branch unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 11484404-e518-44c5-b22f-068c661b9230

📥 Commits

Reviewing files that changed from the base of the PR and between f44eedf and 179d59a.

📒 Files selected for processing (8)
  • .github/workflows/api-review-gate-fork.yaml
  • .github/workflows/api-review-gate.yaml
  • .github/workflows/e2e-fork.yaml
  • .github/workflows/pull-requests.yaml
  • docs/agents/e2e-testing.md
  • hack/common-envs.mk
  • hack/common-envs_test.bats
  • packages/core/talos/Makefile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/core/talos/Makefile (1)

17-17: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider removing a stale archive before the copy.

skopeo copy to an existing oci-archive: destination can fail or append a second manifest, which then breaks the single-image assumption in e2e-fork.yaml. A repeated make image-talos in the same workspace hits this. Add an rm -f before the copy.

♻️ Proposed change
-	$(if $(strip $(OCI_EXPORT_DIR)),mkdir -p "$(OCI_EXPORT_DIR)"; skopeo copy "$$SRC" "oci-archive:$(OCI_EXPORT_DIR)/talos.oci.tar:$(IMAGE_TAG)",skopeo copy "$$SRC" docker://$(REGISTRY)/talos:$(IMAGE_TAG)); \
+	$(if $(strip $(OCI_EXPORT_DIR)),mkdir -p "$(OCI_EXPORT_DIR)"; rm -f "$(OCI_EXPORT_DIR)/talos.oci.tar"; skopeo copy "$$SRC" "oci-archive:$(OCI_EXPORT_DIR)/talos.oci.tar:$(IMAGE_TAG)",skopeo copy "$$SRC" docker://$(REGISTRY)/talos:$(IMAGE_TAG)); \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/talos/Makefile` at line 17, Update the OCI export branch in the
Makefile command to remove the existing talos.oci.tar archive with rm -f
immediately before invoking skopeo copy, while leaving the registry-copy branch
unchanged.
hack/common-envs_test.bats (1)

24-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pass COZYSTACK_VERSION=0 in these three tests to keep them hermetic.

hack/common-envs.mk runs a version probe at parse time. When COZYSTACK_VERSION is empty, it executes git remote add upstream … and git fetch upstream --tags (lines 128-132). Tests 4 and 5 already set COZYSTACK_VERSION=0, and e2e-fork.yaml sets it for the same reason. Tests 1-3 omit it, so a shallow or tagless checkout makes them mutate git remotes and reach the network.

♻️ Proposed change
-  out=$(make -n -C packages/system/cozystack-controller image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc BUILDER=b)
+  out=$(make -n -C packages/system/cozystack-controller image OCI_EXPORT_DIR=/tmp/ocitest IMAGE_TAG=pr-1-abc COZYSTACK_VERSION=0 BUILDER=b)

Apply the same addition to the capi-providers-cpprovider and talos invocations.

Also applies to: 37-37, 51-51

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/common-envs_test.bats` at line 24, Set COZYSTACK_VERSION=0 on the three
hermetic make invocations in hack/common-envs_test.bats, including the shown
system/cozystack-controller command and the capi-providers-cpprovider and talos
invocations. Preserve the existing arguments and command structure while
preventing parse-time version probing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/api-review-gate-fork.yaml:
- Around line 44-67: Bind fork status publication to the triggering pull
request: in .github/workflows/api-review-gate-fork.yaml lines 44-67, grant
pull-requests: read, query pull requests associated with HEAD_SHA, and publish
nothing unless an open request has head.sha === HEAD_SHA and head.repo.full_name
matching github.event.workflow_run.head_repository.full_name; in
.github/workflows/e2e-fork.yaml lines 143-152, extend the existing
assoc.data.find predicate with those same SHA and repository checks so all
decisions and status targets use the triggering fork’s pull request.

In @.github/workflows/e2e-fork.yaml:
- Around line 304-322: Update the “Derive trusted image allowlist (base tree)”
step so cozystack-operator is always included, matching the installer targets
invoked by the fork build even when image-operator’s prereq output is skipped.
Prefer deriving the name from the same installer/image targets; otherwise
explicitly add cozystack-operator to _allow.txt while preserving the existing
archive-derived entries and validation.

In `@docs/agents/e2e-testing.md`:
- Line 103: Update the section 10 heading to refer to the “E2E Tests” commit
status instead of a check-run, keeping the existing same-repo versus fork
workflow wording unchanged.

---

Nitpick comments:
In `@hack/common-envs_test.bats`:
- Line 24: Set COZYSTACK_VERSION=0 on the three hermetic make invocations in
hack/common-envs_test.bats, including the shown system/cozystack-controller
command and the capi-providers-cpprovider and talos invocations. Preserve the
existing arguments and command structure while preventing parse-time version
probing.

In `@packages/core/talos/Makefile`:
- Line 17: Update the OCI export branch in the Makefile command to remove the
existing talos.oci.tar archive with rm -f immediately before invoking skopeo
copy, while leaving the registry-copy branch unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 11484404-e518-44c5-b22f-068c661b9230

📥 Commits

Reviewing files that changed from the base of the PR and between f44eedf and 179d59a.

📒 Files selected for processing (8)
  • .github/workflows/api-review-gate-fork.yaml
  • .github/workflows/api-review-gate.yaml
  • .github/workflows/e2e-fork.yaml
  • .github/workflows/pull-requests.yaml
  • docs/agents/e2e-testing.md
  • hack/common-envs.mk
  • hack/common-envs_test.bats
  • packages/core/talos/Makefile
🛑 Comments failed to post (3)
.github/workflows/api-review-gate-fork.yaml (1)

44-67: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Privileged publishers do not bind the status SHA to an open pull request from the triggering fork. Both workflows publish a required merge-gate status using only workflow_run.head_sha. Neither proves that the SHA is the head of an open pull request whose head repository equals github.event.workflow_run.head_repository.full_name. A fork can point a branch at a commit that another open pull request also uses as its head, so the verdict computed for the fork branch lands on the shared SHA. Commit statuses are deduplicated per context and newest wins, so the fork verdict replaces the verdict a real run reached.

  • .github/workflows/api-review-gate-fork.yaml#L44-L67: add pull-requests: read, look up the pull requests associated with HEAD_SHA, and post nothing unless an open pull request exists with head.sha === HEAD_SHA and head.repo.full_name === github.event.workflow_run.head_repository.full_name.
  • .github/workflows/e2e-fork.yaml#L143-L152: extend the existing assoc.data.find(...) predicate with p.head.sha === HEAD_SHA and p.head.repo?.full_name === github.event.workflow_run.head_repository.full_name, so the docs-only decision, the TIA inputs, and the status target all come from the triggering fork's own pull request.
📍 Affects 2 files
  • .github/workflows/api-review-gate-fork.yaml#L44-L67 (this comment)
  • .github/workflows/e2e-fork.yaml#L143-L152
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/api-review-gate-fork.yaml around lines 44 - 67, Bind fork
status publication to the triggering pull request: in
.github/workflows/api-review-gate-fork.yaml lines 44-67, grant pull-requests:
read, query pull requests associated with HEAD_SHA, and publish nothing unless
an open request has head.sha === HEAD_SHA and head.repo.full_name matching
github.event.workflow_run.head_repository.full_name; in
.github/workflows/e2e-fork.yaml lines 143-152, extend the existing
assoc.data.find predicate with those same SHA and repository checks so all
decisions and status targets use the triggering fork’s pull request.
.github/workflows/e2e-fork.yaml (1)

304-322: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the installer image targets and what the derivation would emit.
fd -t f 'Makefile' packages/core/installer --exec cat -n {}
rg -n 'image-operator|^image:|image-packages' packages/core/installer

Repository: cozystack/cozystack

Length of output: 2743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the workflow derivation and push sections.
fd -t f 'e2e-fork.yaml' .github/workflows --exec sh -c '
  echo "--- $1 ---"
  sed -n "300,370p" "$1" | cat -n
' sh {}

# Inspect helper functions used in the installer Makefile image target.
fd -t f 'common-envs.mk|pre-checks.sh|Dockerfile' --exec sh -c '
  echo "--- $1 ---"
  wc -l "$1"
  sed -n "1,220p" "$1" | cat -n
' sh {}

# Simulate the make -n derivation behavior as shell (read-only from sources).
python3 - <<'PY'
from pathlib import Path
text = Path("packages/core/installer/Makefile").read_text()
print("Makefile contains image operator line:", "IMAGE=\"$(REGISTRY)/cozystack-operator" in text)
print("make derivation command present:", "make -n" in Path(".github/workflows/e2e-fork.yaml").read_text())
print("operator image name literal:", "cozystack-operator" in text)
PY

Repository: cozystack/cozystack

Length of output: 50375


🌐 Web query:

GNU make -n rule execution order prerequisities target recipe error handling

💡 Result:

In GNU make, the execution flow and error handling follow specific mechanisms: Rule Execution and Prerequisites GNU make processes targets by first identifying the dependency graph [1]. When you request a target, make evaluates its prerequisites to determine if the target is out of date (i.e., if the target does not exist or is older than any of its prerequisites) [1]. It recursively processes these prerequisites, ensuring they are built before the recipe for the main target is executed [2][1]. The order in which rules appear in the makefile does not determine execution order, except that the first non-pattern target in the first makefile becomes the default goal [1]. Error Handling in Recipes When a recipe line (a shell command) returns a non-zero exit status, make treats it as an error [2][3]. By default, make stops executing the current rule and immediately terminates, returning a non-zero status [2][4]. You can modify this behavior using several methods: - Ignore Errors for a Specific Line: Prefix a recipe line with a hyphen - (e.g., -rm file.o). This causes make to ignore any non-zero exit status for that specific command [2][3]. - Global Ignore: Run make with the -i or --ignore-errors flag to ignore all errors in all recipes [2][4]. Alternatively, you can define the special target.IGNORE in the makefile [2][3]. - Continue Processing: Run make with the -k or --keep-going flag. This instructs make to continue building other independent prerequisites even after a failure occurs, rather than exiting immediately [2][4]. - Cleanup on Failure: To prevent corrupted files from remaining with an updated timestamp after a failed recipe, use the special target.DELETE_ON_ERROR [2][3]. When enabled, make will delete a target if its recipe fails after it has begun modifying the file [2][3]. Expansion Timing Note that make differentiates between variable expansion and prerequisite evaluation [5]. Prerequisites are typically expanded during the first phase (parsing) of the makefile, whereas variables used within recipes are expanded during the second phase (when the recipe is executed) [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- installer Makefile image target ---"
sed -n '1,35p' packages/core/installer/Makefile | cat -n

echo "--- prereq files/scripts that image: depends on ---"
git ls-files packages/core/installer/pre-checks packages/*/hack/pre-checks.sh hack/pre-checks.sh | xargs -r ls -la

echo "--- make dry-run if make is available ---"
if command -v make >/dev/null 2>&1; then
  make -n -C packages/core/installer image OCI_EXPORT_DIR=/oci REGISTRY=r COZYSTACK_VERSION=0 | sed -n '1,80p'
fi

Repository: cozystack/cozystack

Length of output: 1625


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- make dry-run output ---"
make -n -C packages/core/installer image OCI_EXPORT_DIR=/oci REGISTRY=r COZYSTACK_VERSION=0 | sed -n '1,100p'

echo "--- pipeline output for current dry-run string ---"
make -n -C packages/core/installer image OCI_EXPORT_DIR=/oci REGISTRY=r COZYSTACK_VERSION=0 | \
  grep -oE '(dest=/oci/|oci-archive:/oci/)[a-z0-9._-]+\.oci\.tar' || true

Repository: cozystack/cozystack

Length of output: 227


Add cozystack-operator to the image allowlist.

The derivation only scans make -n image output for dest=.../.oci.tar, but the installer image target builds the operator first with image-tags(cozystack-operator, ...) and writes that image name to pr-oci-installer. A failed or skipped pre-checks prereq stops the recipe for image-operator, so the operator archive can be excluded while the push still creates r/cozystack-operator@.... Derive from the same targets the fork build invokes, or include cozystack-operator reliably.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/e2e-fork.yaml around lines 304 - 322, Update the “Derive
trusted image allowlist (base tree)” step so cozystack-operator is always
included, matching the installer targets invoked by the fork build even when
image-operator’s prereq output is skipped. Prefer deriving the name from the
same installer/image targets; otherwise explicitly add cozystack-operator to
_allow.txt while preserving the existing archive-derived entries and validation.
docs/agents/e2e-testing.md (1)

103-103: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the section heading: the gate is a commit status.

The heading says check-run, but the section states the opposite and explains why a check run is wrong. The heading is the first thing a reader sees, so it inverts the invariant.

📝 Proposed change
-### 10. The E2E workflow split: same-repo vs fork, and the "E2E Tests" check-run
+### 10. The E2E workflow split: same-repo vs fork, and the "E2E Tests" commit status
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

### 10. The E2E workflow split: same-repo vs fork, and the "E2E Tests" commit status
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/agents/e2e-testing.md` at line 103, Update the section 10 heading to
refer to the “E2E Tests” commit status instead of a check-run, keeping the
existing same-repo versus fork workflow wording unchanged.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Thanks — the core of this was right and it changed the PR substantially. Two parts of the stated mechanism didn't survive checking, though, and since you explicitly asked not to take your word for it, here is what I measured. All of it is read-only against real commits in this repo, so no scratch PR was needed.

The load-bearing assumption does not hold — check runs are not deduplicated per name. You asked to verify that "for one check-context name on one commit, the most-recent check run wins." It does not. Each workflow run gets its own check suite, and GET /commits/{sha}/check-runs?filter=latest returns a byte-identical set to filter=all; filter=latest only collapses re-runs within a suite. Commit 41d1153c currently carries four check runs named Require API owner review for sizeable API changes — one cancelled and three success, in four different suites. Nothing supersedes anything.

And the aggregate is worst-of, not latest-wins. Commit 0c14d6cf is precisely the shape this PR would produce: E2E Tests cancelled at 12:51:22, then E2E Tests skipped at 12:51:23. Every other context on that commit is success or skipped, and the rollup state is FAILURE. The later skipped did not rescue the earlier cancelled. So the bypass in step 4 of your walkthrough — skipped superseding failure and turning the gate green — is not the behaviour; if anything the risk inverts into a false red that no further review event can clear.

But your conclusion was right, for a different reason, and it's the reason the PR got rebuilt. The real regression is that a commented review landing inside the ~30s evaluation window produces zero real evaluations: workflow-level concurrency cancels the anchor before the job if: is even considered, and the replacement run then skips. That half of your comment is not only correct, it is already happening on main — 41d1153c and 4b1966c5 both show a pull_request run cancelled by a pull_request_review run seconds later. Under worst-of aggregation that stale cancelled sticks to the context until the next push. So the concurrency interaction you filed as secondary is actually the primary defect here, and it predates this PR.

What the PR does now. Rather than tuning the if:, the gate is no longer a job name at all. It follows the pattern #3262 established for E2E Tests: the merge context is a commit status, published by a dedicated reporting job, and skipped or cancelled runs post nothing. That inverts the whole argument — statuses are deduplicated per context with the newest winning (verified: 0c14d6cf has six CodeRabbit statuses and the combined status returns exactly one), so declining to post preserves whatever verdict a real run last reached and can never invent one. This is what makes the commented skip safe on its own terms instead of relying on skip-to-green. The concurrency group is also split so a run that computes no verdict cannot cancel one that does.

Two consequences worth flagging since they weren't in the original PR either. A fork PR cannot publish this status from any trigger — the read-only-token restriction covers pull_request_review as well as pull_request, which I'd assumed was pull_request-only — so forks need the privileged workflow_run half, added here. And this PR is now stacked on #3262 to reuse that infrastructure rather than duplicate it, so it should land after it.

Your three suggested directions all pointed at this; option 2 in particular is close to what the reporting job does, just expressed as "post nothing" instead of "re-post the previous conclusion", which needs no lookup and cannot race. Thanks for pushing back on the "YAML parses clean" testing section — that was the actual weak point.

Base automatically changed from ci/fork-pr-e2e to main August 8, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants