Make publish failures terminal - #652
Conversation
|
Reviewer notes:
|
There was a problem hiding this comment.
Pull request overview
This PR restructures the fabro-workflow pipeline to introduce a dedicated, always-present publish phase (between conclude and finalize) and makes publish-related failures (final push, remote head verification, PR creation) produce a terminal run failure (publish_failed). It also extends PR-created events to record the verified remote head SHA and updates GitHub App token permissions to support workflow-file writes.
Changes:
- Add a new publish pipeline phase that re-pushes the final run branch and (optionally) opens a PR only after verifying the remote branch head SHA.
- Introduce
FailureReason::PublishFailedacross Rust, OpenAPI, and the TS client, and classify publish errors as terminal. - Extend pull request created event payloads with
head_sha, and requestworkflows: writepermissions for GitHub App tokens.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| lib/packages/fabro-api-client/src/models/failure-reason.ts | Add publish_failed to the generated TS failure-reason tokens. |
| lib/foundation/fabro-types/src/status.rs | Add FailureReason::PublishFailed to shared Rust status vocabulary. |
| lib/foundation/fabro-types/src/run_event/misc.rs | Add optional head_sha to PR-created event props. |
| lib/foundation/fabro-api/tests/status_round_trip.rs | Assert OpenAPI/Rust JSON token parity for publish_failed. |
| lib/components/fabro-workflow/src/pipeline/types.rs | Introduce PublishOutcome/Published and rename PR options to publish options. |
| lib/components/fabro-workflow/src/pipeline/pull_request.rs | Require remote head SHA verification before creating a PR; plumb expected head SHA through request/response types. |
| lib/components/fabro-workflow/src/pipeline/publish.rs | New publish phase: final push + optional PR creation; emit events and convert failures into publish errors. |
| lib/components/fabro-workflow/src/pipeline/mod.rs | Wire the new conclude + publish steps into pipeline exports. |
| lib/components/fabro-workflow/src/pipeline/finalize.rs | Split finalize into conclude() (collect diff/result) and finalize() (persist + emit terminal), and map publish errors to publish_failed. |
| lib/components/fabro-workflow/src/operations/start.rs | Update run lifecycle sequencing to initialize -> execute -> conclude -> publish -> finalize. |
| lib/components/fabro-workflow/src/event/events.rs | Add head_sha to internal PullRequestCreated event and constructor. |
| lib/components/fabro-workflow/src/event/convert.rs | Convert internal head_sha string into optional API field. |
| lib/components/fabro-workflow/src/error.rs | Add Error::Publish plus helpers, retryability classification, and failure-detail support. |
| lib/components/fabro-workflow/README.md | Update docs to reflect the new pipeline phase ordering. |
| lib/components/fabro-store/src/run_state.rs | Update tests/fixtures to include persisted PR head_sha. |
| lib/components/fabro-github/src/lib.rs | Request workflows: write for push tokens and add branch_head_sha() helper for remote head verification. |
| lib/apps/fabro-server/src/server/tests.rs | Update tests to account for PR head SHA verification and storage. |
| lib/apps/fabro-server/src/server/handler/pull_requests.rs | Require final_git_commit_sha and enforce remote head SHA verification before server-side PR creation. |
| lib/apps/fabro-server/src/server.rs | Treat publish failures distinctly and map them to FailureReason::PublishFailed. |
| lib/apps/fabro-server/src/install.rs | Add workflows: write to the GitHub App manifest and assert it in tests. |
| lib/apps/fabro-server/src/demo/mod.rs | Add parsing support for the new publish_failed failure reason. |
| lib/apps/fabro-cli/tests/it/cmd/pr_view.rs | Update CLI integration test fixtures for PR head_sha. |
| lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs | Update CLI progress rendering tests for PR head_sha. |
| lib/apps/fabro-cli/src/commands/run/run_progress/event.rs | Update event parsing tests for PR head_sha. |
| docs/public/integrations/github.mdx | Document workflows: write permission and the new publish-stage behavior/guarantees. |
| docs/public/api-reference/fabro-api.yaml | Add publish_failed to the API failure reason enum. |
| docs/internal/events.md | Document head_sha in legacy PR-linked event properties. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Follow-up cleanup on the publish-failures change.
Error model:
- Collapse `Error::{Engine, Publish, Handler}` into one `Error::Stage` with an
`ErrorStage` discriminator. The three shared a field shape and had to be
edited together in four match groups; nine near-identical constructors
become two private helpers.
- Add `Error::failure_reason()`, replacing the same error -> FailureReason
mapping written out in four places.
- Publish errors are now terminal. Publish runs once, after execution, so no
caller could ever act on the retryable classification.
Publish phase:
- Fix: a branch that was pushed is now still reported when pull request
creation fails afterwards. `PublishOutcome` records what happened and
carries the error separately, instead of hiding both behind a `Result`.
- Drop `PublishOutcome::NoChanges`, which no consumer distinguished from
`Published { pr_url: None }`.
- Move publish onto `Concluded` as methods and replace three near-identical
precondition guards with one `publish_target()`.
Pull requests:
- `maybe_open_pull_request` -> `open_pull_request` returning the record
directly. Both callers already reject empty diffs, so the `Ok(None)` path
was unreachable.
- Drop `CreatedPullRequest.head_sha`, which echoed back its own input.
GitHub client:
- Delete `branch_exists`, which had no callers and duplicated
`branch_head_sha`. Give `branch_head_sha` the `_with_client` split every
sibling has and port the tests to `MockHttpClient`.
- Collapse the copy-pasted credential match in `resolve_clone_credentials`.
Events:
- `PullRequestCreated.head_sha` is `Option<String>` instead of using an empty
string to mean absent.
- Centralize the run-branch refspec in `lifecycle::git::push_run_branch`, so
`git.push` reports a branch name from both emitters as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Ran a simplification pass over this branch (three review agents: reuse, quality, efficiency). Pushed as One real bug fixed: if the final push succeeded but PR creation then failed, Duplication this branch introduced, collapsed:
Dead code removed:
Other: One behavior change worth a look: Left alone, flagging for you:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
lib/components/fabro-workflow/src/pipeline/finalize.rs:552
final_git_commit_shais being derived fromgit.base_shawhenlast_git_shais missing.base_shais the diff base, not the final commit, and using it here can cause publish/PR remote-head verification to compare against the wrong SHA.
let final_git_commit_sha = options.last_git_sha.clone().or_else(|| {
run_options
.git
.as_ref()
.and_then(|git| git.base_sha.clone())
});
lib/components/fabro-workflow/src/pipeline/pull_request.rs:501
- Remote branch head verification happens after
build_pr_content(...)(which can invoke the LLM). When the branch is stale/missing, this pays LLM latency/cost before failing. Consider verifying the remote head first, then building PR content once the branch is confirmed.
let remote_head = github_app::branch_head_sha(&req.github, &owner, &repo, req.head_branch)
.await
.map_err(|err| format!("failed to verify remote branch head: {err:#}"))?;
…ate replica lag
Three follow-ups from the efficiency review of the publish pipeline.
Check the branch before spending an LLM call:
`open_pull_request` generated the PR title and body first and only then
verified the remote branch pointed at the run's final commit. Every stale
branch therefore cost a full content generation before failing. The
verification is the cheap check, so it now runs first.
Tolerate GitHub read-after-write lag:
`GET /repos/{owner}/{repo}/branches/{branch}` is replica-served and can briefly
report the previous commit, or 404 for a branch that is new on the remote,
right after the push publish just made. It was read once with no retry. Since
publish failures are terminal, a replica that had not caught up yet would
discard a fully successful run. It is now read up to three times.
These two land together on purpose: the LLM call was the only thing buying
slack against the race, so reordering without the retry would have made it
more likely.
Keep commit SHAs out of failure classification:
`classify_failure_reason` substring-matches bare "500", "502", "503" and "504"
as transient-infra hints. Both publish messages embed a commit SHA, and a
40-char hex string contains one of those often enough to matter, so a
deterministic failure could be reported as transient. Long hex runs are now
masked before matching; the three-digit status codes those hints look for are
too short to be affected. The hex regex is shared with
`normalize_failure_reason`, which already had its own copy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Follow-up from the efficiency review — pushed as 1. The branch check now runs before the LLM call. 2. The branch read tolerates GitHub replica lag. These two land together deliberately: the LLM call was the only thing buying slack against that race, so reordering without the retry would have made it more likely. 3. Commit SHAs no longer contaminate failure classification. Test coverage: the stale-branch test now asserts the branch was read 3 times and that neither the LLM nor PR creation was called. I did not add a test for "recovers on attempt 2" — it needs timing-dependent mock swapping, which seemed a worse trade than the call-count assertion. Unrelated, but it blocked verification — filter = "package(fabro-server)" # 5s x 4 = 20s
filter = "package(fabro-server) & test(all_spec_routes_are_routable)" # 15s x 4 = 60sthe general rule shadows the specific one, and Still open from the review, not addressed here:
|
… GitHub twin Both from Copilot review feedback on #652. Do not fall back to `base_sha` for `final_git_commit_sha`: `base_sha` is where the run started, not what it produced. When a run made commits but no SHA was tracked, the conclusion reported the base commit as the run's final commit — a durable, API-exposed field — and publish then checked the pushed branch against it, failing a branch that was pushed correctly. The SHA is now only required where it is actually used: verifying the remote head before opening a pull request. Pushing never needed it, since the refspec sends whatever the branch points at. A run with no tracked SHA therefore still pushes its branch and succeeds; it fails only if a pull request is requested, where an unverifiable head is a real problem. Route branch names with slashes in the GitHub twin: Run branches are `fabro/run/<id>`. GitHub routes the branch as the remainder of the path, but the twin declared a single-segment `{branch}` capture, so every real run branch 404'd against it. Now a wildcard, with a test covering the slashed case that the existing single-segment tests missed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
This prevents a run from finishing green and opening a PR from a stale checkpoint when GitHub rejects the implementation push.
Testing