Push auto-trigger operator runbook
Operator-facing reference and rollout runbook for the push auto-trigger — the mechanism that fires a CI/CD pipeline in-process when a branch is pushed to the bundled git transports (SSH / smart-HTTP). This document is operator-facing; it restates behavior and points at the source so an operator can verify each claim.
| Section | Verb |
|---|---|
| §1 How the push trigger works | understand the mechanism + semantics |
| §2 What the trigger is not — no hook binary required | distinguish triggering from enforcement |
| §3 Loop avoidance: mirror / runner pushes | understand why replicated pushes don't retrigger |
§4 Rollout: enable the gate on develop and staging |
replayable rollout runbook |
| §5 Troubleshooting | diagnose a push that did / didn't trigger |
Implementation-state note.
engine.ScheduleForBranch(the gated scheduler this trigger calls) lives ininternal/cicd/engine.go, and the in-process transport wiring invokesScheduleForBranch(..., "push")on the success branch of receive-pack. Where an exact symbol or call site is named below, verify against the merged implementation before relying on the literal path. The behavior and semantics described here are stable; the exact wiring location may shift.
1. How the push trigger works
1.1 Mechanism
A successful push to a branch over the bundled git transports (SSH on :2222, smart-HTTP on the HTTP port) fires a pipeline schedule in-process — there is no spool directory, no external dispatcher process, and no extra hop. The transport layer, on the success branch of a receive-pack run, calls the engine directly:
engine.ScheduleForBranch(ctx, cfg, repoID, commitSHA, ref, branch, "push")
This is the same in-process post-receive pattern the code-search
reindex already uses: both the HTTP handler
(internal/git/protocol_http.go) and the SSH handler
(internal/git/protocol_ssh.go) run their post-receive work only on a
successful write (receive-pack exit / CGI status < 400). The push
trigger hangs off that same success gate. See the postReceiveIndexer
dispatch in those two files for the established pattern; the schedule
call sits alongside it.
1.2 trigger="push" semantics
The pipeline row records its origin in the trigger column. An
auto-triggered pipeline carries trigger="push". Contrast the manual
REST trigger (POST /api/v1/repos/{owner}/{repo}/pipelines,
internal/api/pipelines.go Trigger), which records trigger="manual".
Both paths funnel through engine.Schedule →
engine.ScheduleForBranch, so they get identical per-job branch
gating (§1.3); the only difference is the recorded trigger value and
how the ref/SHA are supplied (transport-supplied vs. request body).
What to observe: a pipeline created by a push has trigger: "push"
in its API representation; a pipeline created by the REST endpoint has
trigger: "manual".
1.3 Config discovery is per-pushed-ref
The trigger schedules from the vetrix-ci.yml (or .ci/pipeline.yml
/ .vetrix/pipeline.yml) found in the tree of the pushed ref, not
from any single global location. cicd.DiscoverConfig reads the
pipeline file out of the pushed ref's tree, walking the documented
search path (.ci/pipeline.yml → .vetrix/pipeline.yml →
vetrix-ci.yml; see internal/cicd/discovery.go and the discovery
note in pipelines.go).
The operational consequence — which drives the entire rollout runbook
in §4 — is that the config the trigger sees on a push to develop
is whatever lives on develop, and the config it sees on a push to
staging is whatever lives on staging. These are independent
trees. A vetrix-ci.yml edit on one branch has no effect on the other
until the file is propagated to that branch.
1.4 Per-job branch gating
Before any pipeline row is created, ScheduleForBranch filters
cfg.Jobs through parser.JobRunsOnBranch(job, branch) using the
short branch name (e.g. develop, not refs/heads/develop). The
filter rules (internal/cicd/parser/v1.go):
- If a job's
only:is non-empty and the branch matches none of its patterns → the job is dropped. - If the branch matches any pattern in the job's
except:→ the job is dropped. - Otherwise the job runs.
Pattern matching is simple glob: an exact match, *, a prefix*, or a
*suffix (* matches any run of non-slash characters).
If every job is gated out, ScheduleForBranch returns
ErrNoRunnableJobs and creates no pipeline row — the push
succeeds, but nothing is scheduled. This is the intended way to keep a
branch "quiet": gate every job out for it.
What to observe: after an all-gated-out push, there is no new pipeline for that commit (the pipelines list for the repo shows no new row for the pushed SHA), and the push itself still reports success to the client.
2. What the trigger is not — no hook binary required
Auto-triggering does not require any external hook binary. This is the single most common point of operator confusion, so it is called out explicitly.
The push trigger runs in-process inside the server off the transport success path (§1.1). It does not depend on:
VETRIX_HOOK_BINARY- the
cmd/vetrix-hookbinary - the
pre-receive/post-receiveshell scripts ininternal/git/hook_scripts.go
2.1 What the hook binary is for: pre-receive enforcement only
The VETRIX_HOOK_BINARY / cmd/vetrix-hook mechanism exists
solely for pre-receive ENFORCEMENT — rejecting a push before it
lands. That covers checks such as:
- protected-branch rules,
- force-push rejection,
- push-time secret scanning.
It is an admission control path: it decides whether the push is allowed at all. It has nothing to do with scheduling pipelines.
The two are wired at different points in the push lifecycle:
| Pre-receive enforcement | Push auto-trigger | |
|---|---|---|
| Purpose | accept / reject the push | schedule a pipeline after the push lands |
| Runs | before refs are updated | after a successful write |
| Mechanism | pre-receive script execs $VETRIX_HOOK_BINARY |
in-process call to engine.ScheduleForBranch |
| Needs a binary? | yes (else the script logs VETRIX_HOOK_BINARY not set; skipping pre-receive checks and allows the push) |
no |
Operator takeaway. You do not need to set
VETRIX_HOOK_BINARYto get auto-triggering. A deployment withVETRIX_HOOK_BINARYunset still auto-triggers pipelines on push; it just skips pre-receive enforcement (and logs the warning above on every push). Conversely, configuring a hook binary does not, by itself, schedule any pipeline — that is the in-process trigger's job.
3. Loop avoidance: mirror / runner pushes
A push that the platform itself generates — a mirror / replication push or a runner / worker push — must not retrigger a pipeline, or pushes would feed back on themselves. This is satisfied structurally, not by a runtime guard:
-
Mirror / replication pushes (
internal/mirror/push_mirror.go) push to a remote URL with go-git directly. They never go through the bundled SSH / smart-HTTP receive-pack transports, so they never reach the transport success path the trigger hangs off. This is the same structural exclusion that keeps mirror pushes out of thegit.pushanalytics event — see the comment block at the top ofinternal/git/analytics_push_event.go("Mirror / replication pushes … bypass the HTTP / SSH transports entirely … so [the exclusion] is satisfied structurally, without an explicit guard"). -
The trigger only fires from the inbound transport receive-pack success path. Any platform-internal write that does not transit those transports cannot retrigger.
What to observe: trigger a mirror sync (or inspect the mirror sync
log via the mirror admin surface) and confirm no new trigger: "push" pipeline appears on the mirror-source repo as a result of the
replication push. The only trigger: "push" pipelines should be the
ones from genuine inbound developer pushes.
4. Rollout: enable the gate on develop and staging
This is the replayable runbook for enabling the push auto-trigger on
the develop and staging branches by adding the appropriate only:
filter to the gate job in vetrix-ci.yml. Every step names what to
observe.
Why two branches, two edits. Because
DiscoverConfigreadsvetrix-ci.ymlfrom the pushed ref's tree (§1.3), the gate-jobonly:configuration must physically exist on bothdevelopandstaging. Editing it ondevelopalone leavesstagingungated. Step §4.4 propagates the file tostagingas an explicit operator action — do not skip it.
The example below uses a single gate job named build-test that should
run on develop and staging and nowhere else. Adapt the job name to
your repo.
4.0 Pre-flight
Confirm the trigger path is live on this deployment before relying on it:
# A test push to a feature branch should create a trigger="push" pipeline
# (assuming the job is not gated out for that branch).
curl -fsSL "https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/pipelines?per_page=5" \
-H "Authorization: Bearer $VETRIX_OPERATOR_TOKEN"
Observe: the most recent rows show trigger: "push" for recent
inbound pushes. If every recent pipeline is trigger: "manual" and
none are "push", the push-trigger wiring is not active on this
deployment — stop and confirm the build state before continuing.
4.1 Edit the gate job on develop
On a working clone, on the develop branch, set the gate job's only:
to the two target branches:
# vetrix-ci.yml (on develop)
jobs:
build-test:
stage: test
image: golang:1.23
only:
- develop
- staging
script:
- go test ./...
Commit and push the change to develop.
Observe: the push to develop itself creates a trigger: "push"
pipeline (the job's only: now includes develop, so it is runnable):
curl -fsSL "https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/pipelines?per_page=3" \
-H "Authorization: Bearer $VETRIX_OPERATOR_TOKEN"
The newest row should have trigger: "push", ref ending in
develop, and commit_sha equal to the commit you just pushed.
4.2 Confirm a gated-out branch creates no pipeline
Push a no-op commit to a branch that is not in the only: list
(e.g. a throwaway feature/gate-check branch):
Observe: the push succeeds but no new pipeline row appears
for that commit (re-run the pipelines list; the gated branch's SHA is
absent). This is the ErrNoRunnableJobs path (§1.4) — every job is
gated out for that branch, so no pipeline is created. This is the
positive confirmation that the only: filter is taking effect on
develop's config.
4.3 Propagate the config to staging — REQUIRED
staging is still ungated until vetrix-ci.yml carries the same
only: block on staging's tree. Bring the file across, e.g.:
git checkout staging
git checkout develop -- vetrix-ci.yml # take develop's vetrix-ci.yml onto staging
git commit -m "Propagate gate-job only: filter to staging"
git push origin staging
(Use whatever propagation mechanism your branch policy allows — a merge
or cherry-pick of the develop change is equally fine. The
requirement is only that the resulting staging tree contains the same
only: block.)
Observe: the push to staging creates a trigger: "push" pipeline
(the job's only: includes staging, so it is runnable). Confirm via
the pipelines list that the newest row has trigger: "push" and ref
ending in staging.
4.4 Verify both branches are gated
Final confirmation. Push a trivial no-op commit to each branch in turn and confirm a pipeline is created for each:
# After a push to develop, then a push to staging:
curl -fsSL "https://api.gitvetrix.com/api/v1/repos/{owner}/{repo}/pipelines?per_page=10" \
-H "Authorization: Bearer $VETRIX_OPERATOR_TOKEN"
Observe: there is one trigger: "push" pipeline for the develop
commit and one for the staging commit, and pushes to any branch
outside the only: list create no pipeline. If staging
pushes still create no pipeline, §4.3 did not land — re-check that
staging's vetrix-ci.yml actually contains the only: block (the
config is per-ref; a stale staging tree is the most common failure).
4.5 Rollback of the rollout
To disable the gate on a branch, remove that branch from the job's
only: list (or add it to except:) on that branch's tree and
push. The change is per-ref, exactly like enabling it: removing
staging from only: on develop's tree does nothing to staging —
you must edit staging's tree.
Observe: after the rollback push, a subsequent no-op push to the
de-gated branch creates no pipeline (ErrNoRunnableJobs).
5. Troubleshooting
A push did not create a pipeline.
- Is the deployment running the push-trigger wiring? Re-run §4.0 — if
no recent pipeline is
trigger: "push", the trigger is not live. - Is the branch gated out? If every job's
only:excludes the pushed branch (orexcept:includes it),ScheduleForBranchreturnsErrNoRunnableJobsand creates nothing — this is expected (§1.4). Check thevetrix-ci.ymlon the pushed ref's tree, not on another branch (§1.3). - Did the push actually succeed? The trigger only fires on a
successful receive-pack (write, exit
< 400). A push rejected by pre-receive enforcement (§2) never reaches the trigger. - Is a pipeline config present on the pushed ref?
DiscoverConfigmust find.ci/pipeline.yml,.vetrix/pipeline.yml, orvetrix-ci.ymlin that ref's tree; otherwise there is nothing to schedule.
A mirror or runner push created an unexpected pipeline. This should
not happen — those pushes bypass the transports (§3). If you see a
trigger: "push" pipeline you can attribute to a replication or
runner push, treat it as a defect and capture the source repo, ref, and
SHA before filing.
VETRIX_HOOK_BINARY not set warning on every push. This is the
pre-receive enforcement path (§2), not the trigger. It means
enforcement is being skipped; auto-triggering is unaffected. Configure
the hook binary only if you want enforcement.
6. Live devstack dry-run checklist
This section is the replayable operator checklist for the live HTTP+SSH push matrix against a deployed rc devstack. It exists because the in-process automated coverage cannot stand up a real worker, real Docker, or a real receive-pack transport in the unit-test sandbox. Run this against a built & deployed rc devstack with a running worker.
Prerequisites (from §4.0 and the §2 notes):
- rc code built and deployed; one
workerprocess running withRUNNER_IDset and/var/run/docker.sockmounted with the correctgroup_addGID (see the Docker deployment guide §7.1 in the system-docs repo). - A repo reachable over both transports — exercises
ssh://repo-arg owner resolution (the leading-slash fix inparseRepoArg,internal/git/protocol_ssh.go). The fix is not org-specific; it applies equally to user-owned repo SSH clones (orgs are just rows in the users table). Use the canonicalvetrix/vetrixrepo here as the convenient fixture, not a scope limit. - A
vetrix-ci.ymlondevelopandstagingshaped like §4.1: agatejobonly:[develop,staging]and areleasejobonly:[master].
Each row: perform the action, then assert the exact observable.
| # | Action | Exact observable to assert |
|---|---|---|
| 6.1 | git push https://<host>/vetrix/vetrix.git develop (smart-HTTP) |
A new pipeline appears with trigger: "push", ref: refs/heads/develop, commit_sha == the pushed tip. The gate job is present and runs; the release job is absent. |
| 6.2 | git push ssh://git@<host>:2222/vetrix/vetrix.git staging (SSH) |
Same as 6.1 with ref: refs/heads/staging. The ssh:// push succeeds — it does not 404. This exercises ssh:// repo-arg owner resolution: the leading-slash fix in parseRepoArg, internal/git/protocol_ssh.go. The fix is not org-specific: ssh:// URLs send the repo path absolute, so parseRepoArg strips the leading slash before SplitN to avoid an empty owner segment. It applies equally to user-owned repo SSH clones (orgs are just rows in the users table). The vetrix/vetrix repo here is the convenient fixture, not a scope limit. |
| 6.3 | After 6.1/6.2 jobs finish, open the job detail / log view for a finished gate job |
Finished-job logs are retrievable from the durable store, not only the live SSE stream. Logs persist after the job reaches a terminal state. |
| 6.4 | git push <host>/vetrix/vetrix.git v9.9.9 (a tag) |
The push succeeds. No new trigger: "push" pipeline is created (pipelines are branch-scoped). |
| 6.5 | git push <host>/vetrix/vetrix.git :develop (branch delete) |
The delete succeeds. No new pipeline is created. |
| 6.6 | Push to a branch whose vetrix-ci.yml gates every job out (e.g. a feature branch with only release:[master]), or to a repo with no config |
The push succeeds. No pipeline is created (ErrNoRunnableJobs / config-not-found benign skip). |
| 6.7 | Trigger a mirror sync (or runner push) on the repo (§3) | No new trigger: "push" pipeline appears as a result of the replication/runner push. Only genuine inbound developer pushes produce trigger: "push" rows. |
| 6.8 | For 6.1, open the audit log for the repo | A pipeline.trigger audit row exists with source: "push", the correct branch/ref, and the actor set to the pushing user. |
If any row's observable does not hold, capture the repo, ref, SHA, and
the relevant worker / server log lines and file a bug before
continuing.