statewrite

package
v0.8.1-dryrun.28 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package statewrite provides the shared optimistic-lock retry used by every finalize verb (orchestrate, promote, rollback, hotfix) when it commits the manifest to the trunk branch through the GitHub Contents REST API.

Concurrent finalize jobs for different environments mutate disjoint keys in the same manifest file. They do not conflict semantically, but they collide on the file blob SHA: the second writer to PUT with a now-stale SHA gets an HTTP 409 ("does not match <sha>") and its state is dropped. CommitWithRetry closes that race by expressing the write as a read-modify-write that re-reads the current manifest, re-applies the caller's mutation on top of whatever the other writer committed, and re-PUTs, retrying on 409.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CommitWithRetry

func CommitWithRetry(opts Options) error

CommitWithRetry performs an optimistic-locked read-modify-write of the manifest at opts.Ref. It fetches the current manifest and blob SHA, applies opts.Mutate to the current bytes, and PUTs with that SHA. On a 409 conflict it re-fetches, re-applies the mutation on top of the now-current manifest, and re-PUTs, up to maxAttempts with a staggered backoff. It returns the last conflict (or any non-409 error) when the bound is exhausted, so a genuinely stuck write still surfaces.

Because Mutate is re-applied against the freshly fetched manifest, two finalize jobs that each set only their own env's ci.state.<env> keys merge: the loser re-reads the winner's committed keys and re-applies its own on top, so the final manifest carries both.

func IsConflict

func IsConflict(err error) bool

IsConflict reports whether err is (or wraps) a Contents API 409 conflict. It recognizes both the typed ConflictError and the raw gh-CLI 409 body, which carries "does not match" and a "409" status, so a client that forwards the gh error verbatim still triggers a retry.

Types

type ConflictError

type ConflictError struct {
	// Err is the underlying transport or CLI error, surfaced when the retry
	// bound is exhausted.
	Err error
}

ConflictError reports an optimistic-lock (HTTP 409) failure from the Contents API, the signal CommitWithRetry retries on. Clients wrap the API's 409 in this type so the retry loop does not have to string-match raw gh output.

func (*ConflictError) Error

func (e *ConflictError) Error() string

func (*ConflictError) Unwrap

func (e *ConflictError) Unwrap() error

Unwrap exposes the wrapped error to errors.Is/As.

type ContentsClient

type ContentsClient interface {
	GetContent(repo, path, ref string) (content []byte, sha string, err error)
	PutContent(repo, path, ref, sha, message string, content []byte, author Identity) error
}

ContentsClient is the minimal GitHub Contents API surface the retry loop needs. The production implementation shells out to the gh CLI; tests inject a fake that returns a 409 on the first PUT and succeeds on the second.

GetContent returns the current file bytes and its blob SHA at ref. A file that does not yet exist returns an empty SHA and a nil error so the first write creates it. PutContent writes content at ref using sha for the optimistic lock (empty sha creates the file); it returns an error satisfying IsConflict when the blob SHA no longer matches.

func NewGHClient

func NewGHClient() ContentsClient

NewGHClient returns the production ContentsClient backed by the gh CLI.

type Identity added in v0.5.0

type Identity struct {
	// Name is the commit author/committer name. Empty falls back to the bot default.
	Name string
	// Email is the commit author/committer email. Empty falls back to the bot default.
	Email string
}

Identity is the name and email stamped as both the author and the committer of a Contents API state commit. State writers populate it from the manifest git config (GetGitUserName/GetGitUserEmail) so automated commits are attributed to the bot identity rather than the token owner that GitHub would otherwise use.

func (Identity) OrDefault added in v0.5.0

func (id Identity) OrDefault() Identity

OrDefault returns the identity with any empty field filled from the bot default, so a commit is always attributed to a concrete identity and behavior is never worse than before this attribution was threaded through. It is exported so other state writers that build their own Contents API request can resolve the same identity without duplicating the bot defaults.

type Mutate

type Mutate func(current []byte) ([]byte, error)

Mutate applies a caller's state change to the current manifest bytes and returns the new bytes to write. It MUST be re-appliable: CommitWithRetry calls it again after re-fetching the manifest on a 409, so it must derive the new bytes purely from the current bytes it is handed (for example, parse them, set only this env's ci.state.<env> keys, and re-marshal) rather than from a stale in-memory snapshot. Re-fetching picks up the other writer's committed keys, and re-applying preserves both.

type Options

type Options struct {
	// Client performs the Contents API get/put. Required.
	Client ContentsClient
	// Repo is the "owner/name" repository slug. Required.
	Repo string
	// Path is the repo-relative manifest path. Required.
	Path string
	// Ref is the branch the write targets (the trunk branch). Required.
	Ref string
	// Message is the commit message for the write. Required.
	Message string
	// Mutate derives the bytes to write from the current manifest bytes. It is
	// re-applied on every retry. Required.
	Mutate Mutate
	// Author is the identity stamped as both author and committer of the state
	// commit. Callers populate it from the manifest git config so the commit is
	// attributed to the bot identity. An empty field falls back to the
	// github-actions[bot] default.
	Author Identity
	// Sleep is called between retries. Defaults to time.Sleep; tests inject a
	// no-op so no real time passes.
	Sleep func(time.Duration)
}

Options carries the inputs CommitWithRetry needs. Required identity fields are explicit; Sleep is optional and defaults to time.Sleep.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL