Skip to content

fix(ci): defer a trigger until the commit is checkable - #372

Open
aliasfoxkde wants to merge 2 commits into
mainfrom
fix/ci-trigger-commit-presence-20261106
Open

aliasfoxkde wants to merge 2 commits into
mainfrom
fix/ci-trigger-commit-presence-20261106

Conversation

@aliasfoxkde

Copy link
Copy Markdown
Owner

The defect

The CI trigger endpoint accepted any non-zero new_hash and answered 202.
Two trigger paths race for every push:

  1. the GitHub relay (.github/workflows/gitforge.yml) POSTs the trigger
    as soon as GitHub sees the push — before the mirror has received the
    objects;
  2. the mirror's receive path triggers a few seconds later, with the
    objects present.

Path 1 created a pipeline run whose workspace clone failed with
fatal: unable to read tree <sha>. That run was persisted as failed:
terminal, visible, and useless. Path 2 then created a second, real run for
the same commit — so every push produced one doomed run and one good one.

The fix

The endpoint now probes the repository's bare storage before publishing.

Availability is decided the way the workspace clone decides it: the commit
must be reachable from refs/heads/* or refs/tags/*, because the clone
fetches exactly those namespaces. A commit present in the object store but
parked under another namespace is still unreachable at checkout — object
existence alone is not enough, and the new
a_commit_only_a_non_fetched_namespace_reaches_is_unreachable test pins
that distinction.

An undecidable probe (no durable store, unregistered repository, git
itself failing) is not treated as unavailable. The probe decides the one
failure mode it can decide and must never silence a trigger that would
have worked.

A trigger for an unavailable commit is recorded durably and answered 202:

{"status":"queued","deferred":true,"reason":"commit_not_available","trigger_id":"..."}

The existing durable sweep then probes again and publishes once the objects
arrive. The mirror's own trigger for the same commit dedups onto the same
row
, so exactly one run is created. The deferral is bounded at 30
minutes; after that the row fails visibly instead of waiting forever.

No new subsystem: the durable trigger queue and the sweep that already
exist for restart/ring-loss recovery carry the deferral.

Verification

Fedora, CARGO_TARGET_DIR=~/gitforge-gf14-target:

cargo fmt --all -- --check                     -> clean
cargo clippy -p ci --all-targets -- -D warnings -> clean
cargo test -p ci
  -> 79 lib tests, 2 ci_trigger_flow tests, 3 ci_trigger_settlement tests, 0 failed

Regressions

Test Layer Pins
test_trigger_for_an_absent_commit_is_deferred_and_creates_no_run integration, HTTP boundary 202 + deferred: true + reason: commit_not_available; the durable row stays pending with no run; no pipeline_runs row exists
a_commit_on_a_branch_is_available unit a commit on refs/heads/main is available
an_unknown_commit_is_missing unit a hash the store never held is missing
a_commit_only_a_non_fetched_namespace_reaches_is_unreachable unit object-store presence is not checkout reachability
test_trigger_requires_token_and_runs_committed_pipeline integration, existing the happy path still creates exactly one run from the committed pipeline

🤖 Generated with Claude Code

aliasfoxkde and others added 2 commits November 6, 2026 05:56
The trigger endpoint accepted any non-zero new_hash, recorded it, and
published a run. When the commit was not in the repository's storage the
run's workspace clone failed with `fatal: unable to read tree <sha>` and
the run was persisted as failed — a doomed run created for every push.

Two trigger paths race for every push: an external relay that learns of
the push before the mirror received the objects, and the mirror's own
receive path, which arrives later. The relay's trigger was the one that
lost the race, so it produced the doomed run while the mirror's trigger
produced the real one behind it.

The endpoint now probes the repository's storage before it publishes. The
probe decides availability the way the workspace clone does: the commit
must exist AND be reachable from a branch or tag, because a clone fetches
refs/heads/* and refs/tags/* and nothing else. A commit parked under
another namespace is present in the object store and still unreachable at
checkout, so object existence alone is not enough.

A trigger for a commit that is not available yet is recorded durably and
deferred: the response is 202 queued with `deferred: true`, and the
durable sweep publishes it once the objects arrive. The mirror's trigger
for the same commit dedups onto that row, so exactly one run is created.
The deferral is bounded — after 30 minutes the row fails visibly with a
cause instead of lingering forever.

An undecidable probe (no durable store, an unregistered repository, a
git failure) does not defer: the probe decides the one failure mode it
can decide, and a probe that cannot answer must not silence a trigger
that would have worked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The trigger endpoint accepted any non-zero new_hash and answered 202, so a
relay trigger that reached the service before the mirror held the objects
created a run whose workspace clone failed with 'unable to read tree'.
That run was persisted as failed: terminal, visible, and useless.

The endpoint now probes the repository's bare storage before publishing.
Availability is decided the way the workspace clone decides it: the commit
must be reachable from refs/heads/* or refs/tags/*, because the clone
fetches those and nothing else. A commit present in the object store but
parked under another namespace is still unreachable at checkout.

An undecidable probe (no durable store, unregistered repository, git
failure) is not treated as unavailable: the probe decides the one failure
mode it can decide and must not silence a trigger that would have worked.

A trigger for an unavailable commit is recorded durably and answered 202
with 'deferred': true. The durable sweep probes again and publishes once
the objects arrive; the mirror's own trigger for the same commit dedups
onto the same row, so exactly one run is created. The deferral is bounded
at 30 minutes, after which the row fails visibly instead of waiting
forever.

Regression: test_trigger_for_an_absent_commit_is_deferred_and_creates_no_run
asserts the 202/deferred contract, the pending durable row, and that no
pipeline run exists for the absent commit.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 6, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 67a67dbb-da0d-4f22-a1a3-f94f8b0c5a5b
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown

🤖 AI Code Review Results

AI review skipped: provider 'anthropic' is not configured; deterministic gates remain authoritative.

✅ No critical issues found.

AI Code Review powered by GitForge

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant