Team git credentials
POST /api/v1/runs/{run}/source-credential releases a credential
for a live claim's sealed source location. A covered GitHub repository uses an
App token restricted to its numeric repository ID and contents: read. The
response uses kind: github_app and carries the token in secret. A typed
repository needs a team subscription before it can receive an App token. It takes no body. The response
contains kind, host, secret, optional username and optional
known_hosts. The host already has the checkout URL, SHA and policy in its
sealed execution input.
GitHub App installation tokens are opaque strings. Both the classic format and longer stateless tokens with dots, underscores and hyphens pass through unchanged. The source host registers the full token and its Basic authorization header with the log masker before invoking git.
The controller derives the credential's host from the sealed source location. The store checks live ancestry, the current claim fence, repository permission and executor enrollment in one transaction. It commits the release audit before the controller opens the envelope. The envelope is bound to the team, host and repository grant, so copying it to another scope does not open it.
A bare machine root cannot release source credentials. Trigger, plan and job
capabilities need source.read and the run's source binding. A job loses source
access when execution starts. Unknown, revoked or ended credentials receive
401. Live authority without the required grant or binding receives 403.
Errors use the shared {"error":{"code","message"}} envelope.
Extra repositories
Team owners manage GitHub App extra-repository lists at
PUT /api/v1/team/github-app/extra-repos. The server stores repository IDs
with the list so a rename cannot transfer access to a reused name. Both the source and extra repositories are bound by numeric ID. The store
checks live installation ownership and coverage before minting. Revoked
coverage of an approved extra returns 409 naming that repository. The request
body cannot add repositories to the token. Extra repositories join the token
only for a verified run of the repository's default branch, so a writer of
another branch cannot read repositories they may lack access to; every other
run's token covers only its own repository.
Store a credential
A team owner stores one credential per repository, under Team > Git
credentials in the dashboard or with
POST /api/v1/team/git-credentials. Live member or verified local authority
with team.admin stores, replaces,
confirms or deletes one, and no route returns its value: the list shows the
host, repository, kind, pinned host key and who stored it. The controller seals
the value under its secrets key, bound to the team, host and repository grant, so a
controller started without SPARKWING_SECRETS_KEY holds none.
A write names repository, the canonical key such as gh:12345 or
git:gitlab.com/team/repo. An administrator can instead set host_wide: true
and leave repository empty to permit every repository on that host. Without
that explicit grant, another repository receives no credential. An exact
repository grant takes precedence over a host-wide grant. Confirm and delete
requests select the grant with ?repository=<key>. Omit that selector only for
a host-wide grant. Resolving a private GitHub slug without the App needs an
explicit host-wide HTTPS credential when the canonical repository ID is not yet
known. A repository-scoped token does not authorize that identity lookup.
GET /api/v1/team/git-credentials requires live member or verified local
authority with runs.read. It lists the authenticated team's metadata and
uses Cache-Control: no-store. Query parameters and caller headers cannot
select another team. Stored secrets are omitted, including when no decryption
key is configured. Machine roots, claims and bootstraps cannot read this list.
Session writes require CSRF proof. Embedded controllers supply
WithCredentialWriter with tenant transactions that recheck authority before
commit. Missing adapters return policy_missing. Mutation responses omit stored
credentials and use Cache-Control: no-store.
A credential is one of these kinds:
- An SSH deploy key (
kind: ssh,private_key), unencrypted, since a runner cannot type a passphrase. When it is stored, the controller opens an ssh handshake to the host (port 22 unlessportsays otherwise), reads the host key the wayssh-keyscandoes, and pins it. The answer shows the key's SHA256 fingerprint, and the credential is unusable until an owner confirms that fingerprint withPOST /api/v1/team/git-credentials/{host}/confirm. Check it against the fingerprint your forge publishes. The controller refuses a host that resolves inside its own network, and it dials the very address it checked, so a name that resolves differently a second time cannot steer the scan inward. - An HTTPS token (
kind: https,token, optionalusername), such as a GitLab or Bitbucket access token or a fine-grained GitHub token. The username defaults tox-access-token; Bitbucket repository access tokens needx-token-auth. Tokens accept up to 4,096 printable ASCII characters without spaces. It is usable once stored.
Prefer a read-only deploy key or token scoped to the repositories the team builds.
A team stores at most 50 credentials.
Who receives it
Every job credential, including a host-wide stored credential or GitHub App
token, requires a matching sealed repository proof and a successful GitHub
compare check at intake. Sparkwing checks that the selected code commit is
reachable from the requested source branch or tag, or the repository's default
branch for a raw commit. GitHub's ahead and identical results pass. behind,
diverged and a missing comparison do not. The result is sealed with the run,
so credential release makes no new GitHub trust decision.
Uploaded code, local checkouts and unproven remote commits receive no team credential, even if they name a connected repository. A historical commit or locked schedule can qualify when the App selects its code and the compare check passes. Non-GitHub jobs cannot establish this proof. Controller-owned intake access used to select source is separate from job credential release.
A live authorized claim receives a credential only on Sparkwing Cloud or a
shared runner. GitHub Actions runners use the workflow's own GITHUB_TOKEN.
Personal machines and here hosts receive no team credential. Host kind is
sealed when the claim is awarded. Runner labels and later enrollment changes
cannot replace that classification. A personal machine uses its owner's Git
configuration and credential helper for its own asserted work.
Each release records the repository, host, run, job, generation, machine, owner, capability prefix and store timestamp. Audit failure returns no credential. Stored credentials require the bound secrets cipher before release. App tokens use the App signing configuration and are returned only after the release audit commits.
Use the credential
The source owner supplies credentials in memory or through a private temporary file. The Git process receives the filtered Git environment and source-owned argv/configuration. It cannot inherit a capability, ambient credential helper, SSH command override or arbitrary Git configuration from the runner's shell. Selected CA and proxy settings follow the shared process policy.
Acquisition-owned files are removed after the fetch. Credentials stay out of clone URLs, execution seals and pipeline environments. Diagnostics use the shared masker. A local owner credential is usable only under the runner owner's repository allow-list.
Pipeline code running as the same OS account can reach that account's credentials while they are in use. Repository scoping and a read-only credential limit the exposed authority.
Use the released credential for the sealed source location. Keep it in memory or a private temporary file and remove that file when git exits. The SSH response carries the confirmed known_hosts entry for the credential's host.
Rotate and revoke
Storing a credential for the same repository grant replaces it; the next release hands
out the new value. Replacing an SSH key keeps its confirmation only while the
host's key is the one already confirmed. Deleting it
(DELETE /api/v1/team/git-credentials/{host}) makes every later release
fail. Nothing the runner held outlives the fetch it was released for.
POST /api/v1/secrets/rotate requires team.admin and reseals the authenticated
team's secrets and git credentials under the new key in one transaction.
Browser sessions require CSRF. An unreadable secret stays listed in skipped.
An unreadable git credential or a sealing failure aborts both sets of changes.
Plaintext and unbound rows stay unreadable. The previous key is still needed
while any secret is skipped.
GET /api/v1/team/git-credentials/releases requires team.admin and returns
the latest 200 releases for the authenticated team. Each row carries host,
repository, run_id, job_id, generation, runner, capability_prefix
and released_at in Unix seconds. Both routes return no-store.
Embedded controllers supply WithCredentialRotator and WithSourceAuditReader
with the declared tenant interfaces. An absent adapter returns policy_missing.