Skip to main content
Version: v5.7.0

Git remote

Included from Basic tier

JSON:API type: git_remote.

The v2 OpenAPI document declares this resource's schema; the resource-field coverage gate holds this page to it.

A git remote is a project-scoped repository registration: the URL plus the credentials templates and template versions use to fetch it.

Endpoints​

All under /api/projects/{project_code}/git_remotes:

VerbPathBehaviour
GET…/git_remotesIndex — each remote gated by read on the project's …/git_remotes rule path. meta.authorisations reports update/delete on that path, plus default_periodic_fetch_interval (added only when the project itself is readable). Accepts the shared Ransack filter grammar; the sensitive columns are excluded from the allowlist, so filter[user_*], filter[password_*] and filter[ssh_key_data_*] are rejected 400. Default order: created_at ascending, id ascending.
POST…/git_remotesCreate — project readable plus update on …/git_remotes, 404-on-deny. 201, or 422 with the model errors. A credentialed create on a server with no encryption keys configured answers 503 (see below).
GET…/git_remotes/{id}Show — a missing project, a remote outside the project, or a read denial all answer 404.
PATCH/PUT…/git_remotes/{id}Update — same 422 model-error and credentialed-write 503 contract as create.
DELETE…/git_remotes/{id}Destroy — but an archived remote still referenced by changes keeps its row and has its credentials stripped instead: 200 with the meta message "This Git remote cannot be deleted as it is used by a change, its credentials have been removed." A hard delete answers 204.

A remote is also reachable as a sideloaded resource: include=git_remote on the project's templates index or a single-template show carries it in the compound document's included, serialized exactly as the show above — masked credentials and all.

Writes on a server with no encryption keys (503)​

Credentials are encrypted before they are stored, so a create or update that supplies a credential (user, password, passphrase or ssh_key_data) on a server with no encryption keys configured cannot be honoured. It answers 503 Service Unavailable with the named detail git remote credentials cannot be stored: no encryption keys are configured on this server — a known, operator-fixable configuration state (set OPSCHAIN_PRIMARY_KEY and OPSCHAIN_KEY_DERIVATION_SALT), deliberately not a 500. Credential-free writes and destroys never encrypt anything, so they never answer this 503.

Attributes​

AttributeTypeMeaning
namestringThe remote's name, unique within the project; how templates reference it (git_remote_name).
urlstringThe fetch URL as stored — any userinfo was stripped on write, so exposing it leaks nothing. A transport URL: http://, https://, ssh://, git:// or the scp-like user@host:path form. A filesystem path or a file:// URL is refused by name on create and on a URL change (Url must be an http(s)://, ssh://, git:// or user@host:path remote, a 422 on v1 / invalid_argument with the url field violation on v2) before any connectivity probe, unless the server's deploy-authoritative security.allow_local_git_remotes admits it (opschain docs config). At most 2048 characters: a longer value is refused by name on create and update (Url is too long (maximum is 2048 characters), the same shapes); the stored column is unbounded text.
public_urlstring, nullableA display URL override; null when unset. Bounded like url (Public url is too long (maximum is 2048 characters)).
archivedbooleanArchive flag (update-only in effect: create permits it and then ignores it).
disabledbooleanDisable flag (nullable column, coalesced false on read).
periodic_fetch_intervalinteger, nullablePer-remote override of the periodic fetch cadence in seconds (60–86400); null means the remote inherits the default reported in the index meta (project settings override → global settings override → compiled default 900) — "The periodic fetch" below has the rule. A supplied null clears the override.
created_at / updated_atstring (ISO8601, 6 fractional digits)Row timestamps.
last_fetched_atstring (ISO8601), nullableWhen the periodic fetch sweep last fetched this remote; null until the first sweep. Written on every sweep fetch, quiet or not.
last_fetch_outcomestring, nullableHow the last periodic fetch went: ok, failed (see last_fetch_error), or skipped (the remote stopped being active between the due test and the fetch). Null until the first sweep.
last_fetch_errorstring, nullableThe credential-free cause of the last failed periodic fetch (the same text the error:git_remote:fetch event carries); null after a successful fetch.
consecutive_failuresintegerHow many periodic fetches in a row have failed; 0 after a success. A rising count with the same last_fetch_error is one outage, not many.
next_fetch_atstring (ISO8601), nullableComputed — when the remote is next due: last_fetched_at plus its effective interval. Null while the remote is archived, disabled, switched off by a non-positive effective interval, or has never been fetched (due at the next sweep).
floating_versionsintegerComputed — the unarchived template versions on this remote with float_git_rev set, the versions the sweep repins when their revision's tip moves.
last_repin_atstring (ISO8601), nullableWhen the sweep last repinned a floating template version on this remote; null when it never has.
watch_default_branchbooleanWhether the fetch watches the remote's default branch for ref moves (default true): with it on, a push to the default branch records info:git_remote:ref_moved with no trigger or floating version naming the branch; a busy repository whose default branch never rests turns it off per remote — "Ref-move events" below. Writable on create and update.
last_ref_move_atstring (ISO8601), nullableComputed — when a watched ref of this remote last moved (the newest recorded move in the ref snapshot); null while none has.
userstring, nullableMasked. "{{hidden}}" when a username credential is stored, null when none is.
ssh_key_datastring, nullableMasked. "{{hidden}}" when an SSH key is stored, null otherwise.
passwordstring, nullableMasked. "{{hidden}}" when a password/passphrase credential is stored and the url is not an SSH url; null otherwise.
passphrasestring, nullableMasked. The same stored credential presented as a passphrase when the url is an SSH url (explicit ssh:// scheme, or the schemeless scp-like user@host:path form); null otherwise.
credential_refstring, nullableThe shared git credential this remote references (write-accepted on v1, exposed on v2): fetch/clone resolve the credential at use time, so updating the credential applies to every referencing remote on its next use. Null when the remote stores inline credentials (or none) — a remote never holds both.

Credential redaction semantics​

Stored credential values are encrypted at rest and never materialised by this read path at all: the reader selects only a boolean presence per column, and the serializer maps presence to the placeholder {{hidden}} and absence to null. password and passphrase are two views of one stored column, split on whether the URL is SSH. On writes the three-way distinction is load-bearing: an omitted credential key preserves the stored value, while a key supplied as null or "" clears it — so echoing a read payload (all {{hidden}}) back on an update would overwrite credentials; send only the keys you mean to change.

Shared credentials (credential_ref)​

Instead of inline values, a write may reference a shared git credential by id — or by name within the remote's scope (the project's own registry, then the global one). The referenced credential must live at or above the remote's node: its project, or the global registry; another project's credential is refused. A write carrying both a credential_ref and inline credential values (URL userinfo included) is refused (Credential ref cannot be combined with inline credentials - supply one or the other); supplying a reference clears any stored inline values (the remote flips to the reference), and supplying inline values while a reference is in place is refused until the reference is cleared (credential_ref: "") in the same write. The reverse migration is the ExtractGitRemoteCredential action — see opschain docs resources/git_credential.

The periodic fetch​

The server keeps every active remote's mirror warm and every floating template version on its branch without anyone pressing Fetch revision. Once a minute the cron leader (one replica at a time, whichever holds the scheduler lease; nothing runs while the system is quiesced) sweeps for remotes that are due — unarchived, not disabled, on an unarchived project, with a positive effective interval that has elapsed since last_fetched_at (a remote never fetched is due at once) — and, oldest fetch first, up to two hundred per pass:

  1. Fetches the mirror (git fetch --prune) under the remote's credential — the inline credentials or the referenced shared credential, resolved at fetch time — and records the outcome on the remote: last_fetched_at, last_fetch_outcome, last_fetch_error, consecutive_failures. A failure records error:git_remote:fetch (with cause and consecutive_failures); a fetch that moved any ref records info:git_remote:fetched (never a quiet one). A remote that stays down is retried at its interval and the streak grows.
  2. Repins the floating versions: for every unarchived, unlocked template version on the remote with float_git_rev set, no refresh in progress and a resolved commit, the stored revision is resolved in the fetched mirror; when the tip differs from the sealed commit the version takes the manual Fetch revision path — pack pins re-resolved (explicit pins kept), the pinned assets' active windows rolled onto the new commit, their action catalogs refreshed — under the git identity policy as the version's creator, and info:template_version:rev_repinned — on the chain of the info:git_remote:ref_moved event the same fetch recorded for the branch (the branch a floating version follows is always watched), its ref_moved_event_id naming the move — (from → to) goes on the record and last_repin_at is stamped. A repin the sweep cannot perform records error:template_version:float_refresh and the version's float_refresh_error, cleared by the next successful repin (opschain docs resources/template).

The effective interval of a remote is, in order: its own periodic_fetch_interval; else the owning project's settings document git_remote.periodic_fetch_interval; else the global settings document's git_remote.periodic_fetch_interval; else 900 seconds (opschain docs settings-reference). The index meta's default_periodic_fetch_interval reports the inherited value. A non-positive inherited value switches the periodic fetch off for the remotes inheriting it (next_fetch_at reads null); a disabled or archived remote is never fetched.

A manual Fetch revision (templates version upsert --fetch, the GUI's Fetch revision) still works exactly as before and does not touch the sweep's record — last_fetched_at is the sweep's own.

Ref-move events​

The fetch is the one source of truth for "a ref moved": after every successful fetch — scheduled, hinted by a webhook, or manual — the server compares the remote's watched refs against the ref snapshot it kept from the previous fetch and records one info:git_remote:ref_moved per watched ref that moved, carrying the SHAs it observed (opschain docs events). A ref nobody watches is silent, so a busy repository does not fill the ledger. The watched set is the union of:

  • refs/heads/<default branch> — the remote's HEAD as the mirror recorded it — unless the remote's watch_default_branch is off;
  • every ref a floating template version on the remote follows (float_git_rev);
  • every branch or tag an enabled trigger names: a trigger whose events carries info:git_remote:ref_moved declares the refs it cares about through its conditions (event.data.remote eq, event.data.ref eq | in | contains, event.data.kind eq), bounded by its scope path; one with no ref condition watches every branch and tag of the remotes it names. Other refs (refs/pull/*, notes) are never watched.

The set is recomputed on every fetch, so a new trigger takes effect on the remote's next fetch. A ref the trigger names that already exists is seeded into the snapshot silently (no event for a branch that was already there); a ref the fetch itself brought records created; a watched ref that is gone records deleted and leaves the snapshot; a ref no longer watched leaves the snapshot without an event. The remote's first fetch after upgrading seeds the whole snapshot and records nothing.

What change means. fast_forward — the new tip descends from the previous one (commits counts what it brought); forced — it does not: history was rewritten, and warn:git_remote:forced_push is recorded beside the event every time, listener or not; created and deleted as the names say. Coalescing: moves of one ref observed inside the remote's effective fetch interval extend the open ref_moved event — the first old_sha, the latest new_sha, commits summed, extended: true and fetch recording every cause (scheduled+hint) — so ten commits pushed in a minute are one deployable state; a forced move or a deletion always opens its own event, and a fast-forward after a forced move does too.

Fetching now, and webhooks as hints. GitRemoteService/FetchGitRemote (opschain git-remotes fetch <project>/<name>, update on the project's …/git_remotes rule path) fetches the remote at once over the same lane and waits for it; an archived or disabled remote is refused as not found. A verified inbound-webhook delivery is a hint that takes that lane without waiting: a delivery to a connection whose git_remote names this remote, or a GitHub push delivery naming this remote's repository, schedules the fetch now — the payload's claimed SHA is discarded; the fetch decides. Hinted fetches are rate-limited per remote: one in flight, hints that arrive during it coalesce into one follow-up fetch, and hinted fetches stay at least thirty seconds apart (deferred, never dropped), so a hostile or misconfigured sender causes at most a couple of fetches a minute. Every fetched / ref_moved event names its cause in fetch: scheduled, hint or manual. opschain docs inbound-webhooks has the delivery side.

GitRemoteRef — the ref snapshot​

GitRemoteService/ListGitRemoteRefs (v2; opschain git-remotes refs <project>/<name>) reads the snapshot, ref name ascending, authorised like a read of the remote:

GitRemoteRef fieldMeaning
refThe full ref name (refs/heads/main, refs/tags/v1.2.0).
shaThe commit the ref pointed at when the platform last fetched it.
kindbranch or tag.
observed_atWhen the platform last observed the ref — every fetch, moved or not.
last_changeThe last recorded move: fast_forward, forced, created or deleted; unset while the ref has not moved since it was first watched.
last_moved_atWhen that move was observed; unset with last_change.

Health summary​

GitRemoteService/SummarizeGitRemoteHealth (v2) is the operations read over the sweep, estate-wide: one GitRemoteHealthRow per unarchived remote whose project /git_remotes rule path the caller may read, plus GitRemoteHealthTotals over those rows. failing_only keeps only the rows whose last fetch failed; the totals always count every visible remote. Compositions without the PostgreSQL git-remote estate answer ERROR_REASON_STORE_POSTGRES_REQUIRED.

GitRemoteHealthRow fieldMeaning
id, project_code, nameThe remote's identity.
disabledWhether the remote is disabled (never fetched while it is).
last_fetched_at, last_fetch_outcome, last_fetch_error, consecutive_failures, next_fetch_at, floating_versions, last_repin_atAs on the resource above.
repins_24hFloating repins the sweep performed on this remote in the last 24 hours (counted from the info:template_version:rev_repinned events).
dueWhether the remote is due now — never fetched, or its effective interval has elapsed.
GitRemoteHealthTotals fieldMeaning
remotesVisible unarchived remotes.
dueOf those, due now.
failingOf those, whose last periodic fetch failed.
repins_24hFloating repins across the visible remotes in the last 24 hours.
floating_versionsFloating template versions across the visible remotes.
NameMeaning
projectTo-one project (the owning project).
links.self/api/projects/<code>/git_remotes/<id>.
links.project/api/projects/<code>.

The v2 service — GitRemoteService​

ListGitRemotes / GetGitRemote (by id or name) / CreateGitRemote / UpdateGitRemote / DeleteGitRemote carry this surface typed, with the same {{hidden}} credential masking, the default_periodic_fetch_interval meta, the credentials-removed delete arm (which also clears a credential_ref), the keyless-write 503 typed as unavailable + ERROR_REASON_SERVER_NOT_CONFIGURED, credential_ref on the payloads and writes, the periodic-fetch health fields and watch_default_branch on every GitRemote; SummarizeGitRemoteHealth is the estate-wide health read above and ListGitRemoteRefs the ref snapshot. See opschain docs api-v2.

  • opschain docs resources/template — templates pin a git remote by name.
  • opschain docs resources/git_credential — the shared credential registry remotes reference via credential_ref.