A repository therefore declares the workflow files preloop owns. preloop
runs those from webhook deliveries and ignores the rest; GitHub still owns the
rest and creates their runs itself.
The model
- Ownership is a set of paths per repository, e.g.
.github/workflows/ci.yml. It is stored in the engine’s config file ([owned_workflows], keyed by"owner/repo") and can be changed live through the API below; a change applies to the next delivery and survives a restart. - No record means “own everything”. A repository the operator never configured keeps the historical behaviour: preloop runs every workflow a delivery matches. Migration is opt-in per repository.
- An empty record owns nothing.
PUTwith{"workflows": []}is a valid, explicit “preloop runs no workflows here”;DELETEremoves the record and restores “own everything”. - Only webhook-driven runs are filtered. A native
POST /api/v1/runsthat names an explicit workflow (what a localpreloop rundoes) is an intentional local submission and is never skipped. Scheduled workflows are only registered for owned files, so a schedule the operator left on GitHub does not silently fire on preloop too. - preloop never edits the repository. It holds
contents: read,checks: writeandactions: readon the GitHub App and never needsworkflows: write. Anything that edits.github/workflows/runs on the operator’s machine with the operator’s git andghcredentials.
INFO with the repository, the path, and
reason="not_owned"; the webhook delivery still completes successfully.
Disable the moved workflows on GitHub
Ownership only tells preloop which workflows to run. To stop GitHub from running them as well, disable each moved workflow on GitHub with the customer’s own credentials:runs-on to a preloop label, so a re-enabled workflow on GitHub
queues jobs that its hosted runners cannot pick up instead of silently
double-running them; the rewrite is a safety net, not a substitute for
disabling the workflow.
Drift
A workflow can be disabled on GitHub and later re-enabled (by a person or a policy reset). preloop still owns it, so GitHub creates runs for it too: without theruns-on rewrite the two systems double-run it; with the rewrite
GitHub queues jobs no GitHub-hosted runner can satisfy. Either way the
migration is no longer what the operator intended, so the drift watcher
reports it.
preloop checks GitHub’s GET /repos/{owner}/{repo}/actions/workflows for every
repository with a record, using an installation token scoped to exactly
actions: read. It reports, per workflow:
The watcher runs every
owned_workflows_drift_interval_secs seconds (default
900 — 15 minutes; 0 disables it). It logs one WARN per transition into
drift, records when the episode started, and keeps that timestamp until the
workflow is disabled again. A GitHub outage is logged, leaves the recorded
state untouched, and shows as unknown; it never fabricates drift and never
panics.
API
All routes require the native bearer token (authorization: Bearer …), like
/api/v1/secrets.
404 when the repository has none and therefore still owns
everything):
.github/workflows/, end in .yml or
.yaml, and are deduplicated; traversal, absolute paths, backslashes and
nested directories are rejected with 400:
200 with every owned workflow unknown, drift: false,
and an error string:
404 is returned for /drift when the repository has no record: there is
nothing to compare.
Config file
The API persists the same table the engine reads at startup. Editing the file by hand has the same effect on the engine’s next start, like every other policy table (and a malformed path fails startup rather than being silently ignored):PRELOOP_OWNED_WORKFLOWS_DRIFT_INTERVAL_SECS overrides the interval key.
Troubleshooting
- A moved workflow still runs on GitHub.
GET …/driftshowsgithub_state: "active"anddrift: true; rungh workflow disable. - A workflow that should run on preloop does not. Check the record lists
the exact path GitHub reports (
.github/workflows/<file>.yml). Workflow evaluation logs carryreason="not_owned"when the ownership filter skipped it. - Everything runs on preloop again. The record was deleted (or the
repository key is misspelled).
GET /api/v1/owned-workflowsshows only records that exist. - Drift always says
unknown. The engine has no GitHub App installation covering that repository, so it cannot mint theactions: readtoken. Drift checking does not fall back to a wider PAT.
