Help us shape the future of Go! Take the 2026 Go Developer Survey

imagecheck

package
v0.10.4 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package imagecheck records what a workflow's derived image was built from, and decides whether it needs a rebuild (design §7.2).

The build's record step writes a Record to the runs bucket, at RecordKey(slug, workflow). The daily check compares it with the base branch's head: the key files (KeyFiles) and the image config (ImageConfigHash) are computed the same way on both sides, from a Tree, which is a checkout directory in the build and a git tree in the check.

Index

Constants

View Source
const (
	Rebuild = "rebuild" // a trigger fired: a build is submitted
	Skip    = "skip"    // nothing changed that a build would pick up
	// RebuildFailedLast: a trigger fired, but the last build of these very
	// inputs failed, or ended without clearing the trigger, so it isn't
	// paid for again until something changes.
	RebuildFailedLast = "rebuild-failed-last"
	// CheckFailed: the check itself could not decide.
	CheckFailed = "check-failed"
	// NotInstalled: head's fugaro.yaml and the installed check job
	// disagree about the workflow (fugaro update fixes it).
	NotInstalled = "not-installed"
)

The decisions of the daily check.

View Source
const (
	ReasonForce           = "force"
	ReasonNoRecord        = "no-record"
	ReasonImageConfig     = "image-config"
	ReasonLockfiles       = "lockfiles"
	ReasonBase            = "base"
	ReasonPaths           = "paths"
	ReasonMaxAge          = "max-age"
	ReasonBuiltCommitGone = "built-commit-gone"
	ReasonLatestDrift     = "latest-drift"

	// ReasonBaseUnknown: the base image's digest couldn't be read, so the
	// base trigger was skipped.
	ReasonBaseUnknown = "base-unknown"
	// ReasonLatestUnknown: the image's latest digest couldn't be read, so
	// the latest-drift trigger was skipped.
	ReasonLatestUnknown = "latest-unknown"
	// ReasonBuildRunning: the last build of these inputs hasn't finished.
	ReasonBuildRunning = "build-running"
	// ReasonLastBuildIneffective: the last build of these inputs ended
	// without failing, but didn't clear the trigger.
	ReasonLastBuildIneffective = "last-build-ineffective"
)

The reasons a decision records. The triggers come first, in the order they are evaluated; the rest are notes that never rebuild alone.

View Source
const CheckVersion = 1

CheckVersion is the CheckState schema version this package writes.

View Source
const RecordVersion = 1

RecordVersion is the Record schema version this package writes.

Variables

View Source
var ErrManifestUnknown = errors.New("the registry has no such manifest")

ErrManifestUnknown means the registry has no such tag or digest.

Functions

func CheckKey

func CheckKey(slug, workflow string) string

CheckKey is the workflow's check.json in the runs bucket.

func FailedBuild

func FailedBuild(status string) bool

FailedBuild reports whether a Cloud Build status is a failure the check backs off from: the build ran and did not succeed. A cancelled or expired build is not one: somebody stopped it, it didn't fail.

func FinalBuild

func FinalBuild(status string) bool

FinalBuild reports whether a Cloud Build status can no longer change.

func ImageConfigHash

func ImageConfigHash(cfg *config.Config, workflow string, tree Tree) (string, error)

ImageConfigHash is the hex SHA-256 of the canonical JSON of the workflow's base, image: settings, dockerfile: and, when that is set, the repository Dockerfile's blob ID in tree.

func KeyFiles

func KeyFiles(cfg *config.Config, workflow string, tree Tree) (map[string]string, error)

KeyFiles are the workflow's cache key files in tree, with their blob IDs: every file its cache key globs match, or, when fugaro.yaml declares no cache, the key of the per-base default (config.DefaultCache).

func MatchPaths

func MatchPaths(globs, files []string) []string

MatchPaths returns the files matching one of globs, in files' order.

func RecordKey

func RecordKey(slug, workflow string) string

RecordKey is the record's object in the runs bucket.

func RunningBuild

func RunningBuild(status string) bool

RunningBuild reports whether a Cloud Build status is not final yet.

Types

type CheckState

type CheckState struct {
	Version   int       `json:"version"`
	CheckedAt time.Time `json:"checked_at"`
	Decision  string    `json:"decision"`
	Reasons   []string  `json:"reasons"`
	Error     string    `json:"error,omitempty"`
	// BuildID is the last build a check submitted, and BuildInputs what
	// it was built from; both carry over until the next submission.
	BuildID     string             `json:"build_id,omitempty"`
	BuildInputs *InputsFingerprint `json:"build_inputs,omitempty"`
	// LastBuildStatus is BuildID's status as this check read it (the
	// status at submission when this check submitted it), and LastBuildAt
	// when it was submitted.
	LastBuildStatus string     `json:"last_build_status,omitempty"`
	LastBuildAt     *time.Time `json:"last_build_at,omitempty"`
}

CheckState is the last check of one workflow, as the check job writes it to builds/<slug>/<workflow>/check.json.

func ParseCheckState

func ParseCheckState(data []byte) (*CheckState, error)

ParseCheckState decodes a check.json. Unknown fields are ignored.

func (*CheckState) Marshal

func (s *CheckState) Marshal() ([]byte, error)

Marshal encodes s as check.json.

type CloneOptions

type CloneOptions struct {
	URL, Branch string
	// Dir is the directory to clone into; it must not exist yet.
	Dir string
	// Env is git's whole environment, credential variables included
	// (gitops.CredentialVars), so no token reaches an argv.
	Env []string
}

CloneOptions say what to clone and how.

type Decision

type Decision struct {
	Workflow string   `json:"workflow"`
	Decision string   `json:"decision"`
	Reasons  []string `json:"reasons"`
	// Error is why the check failed (CheckFailed only).
	Error string `json:"error,omitempty"`
	// Inputs are what a build submitted now would be built from.
	Inputs InputsFingerprint `json:"inputs"`
}

Decision is one workflow's decision.

func Decide

func Decide(_ context.Context, in Inputs) Decision

Decide evaluates the workflow's rebuild triggers, in order, recording every one that fires, and then the back-off, unless forced: a rebuild whose inputs are those the last build ran with is RebuildFailedLast when that build ended (failed, or finished without clearing the trigger), and Skip while it is still running. It makes no calls: the caller reads everything first.

type GitError

type GitError struct {
	Args   string // the subcommand
	Err    error
	Stderr string
}

GitError is a failed git command.

func (*GitError) Error

func (e *GitError) Error() string

func (*GitError) Unwrap

func (e *GitError) Unwrap() error

type GitTree

type GitTree struct {
	// contains filtered or unexported fields
}

GitTree is a Tree over one commit of a git repository: the check's blobless, no-checkout clone (Clone), which holds every commit and tree of the branch and fetches a blob from the remote only when ReadFile asks for it, or the build's checkout (Open). Either way its blob IDs and contents are git's own, never a working tree's.

func Clone

func Clone(ctx context.Context, o CloneOptions) (*GitTree, error)

Clone makes a blobless, no-checkout, single-branch clone of o.Branch and reads its head's tree. ctx bounds every later git call of the tree.

func Open

func Open(ctx context.Context, dir, rev string, env []string) (*GitTree, error)

Open is a Tree over commit rev of the repository at dir, with git's environment env (nil inherits fugaro's). It reads what git stores, so its blob IDs equal the check's even where the working tree's bytes differ (an eol attribute, a smudge filter), and files git doesn't track are not in it.

func (*GitTree) BlobID

func (g *GitTree) BlobID(path string) (string, error)

BlobID is the blob ID of the regular file at path in the head tree.

func (*GitTree) Changed

func (g *GitTree) Changed(from, to string, globs []string) (bool, []string, error)

Changed reports whether a file matching one of globs differs between the commits from and to, and which files do. It compares trees only: rename detection, which would fetch blobs, is off.

func (*GitTree) Dir

func (g *GitTree) Dir() string

Dir is the clone's directory.

func (*GitTree) Glob

func (g *GitTree) Glob(pattern string) ([]string, error)

Glob returns the head tree's regular files matching pattern.

func (*GitTree) Head

func (g *GitTree) Head() string

Head is the branch's head commit.

func (*GitTree) IsAncestor

func (g *GitTree) IsAncestor(commit string) (bool, error)

IsAncestor reports whether commit is an ancestor of the head (or the head itself). A commit the clone doesn't have is not: it is gone from the branch, after a force-push. The clone doesn't fetch it.

func (*GitTree) ReadFile

func (g *GitTree) ReadFile(path string) ([]byte, error)

ReadFile reads a regular file of the head tree: one git cat-file, which fetches that blob on demand. A file is read at most once.

type ImageStatus

type ImageStatus struct {
	BuiltAt      time.Time `json:"built_at"`
	AgeS         int64     `json:"age_s"`
	SourceCommit string    `json:"source_commit"`
	BaseRef      string    `json:"base_ref"`
	BaseDigest   string    `json:"base_digest"`
	Image        string    `json:"image"`
	ImageDigest  string    `json:"image_digest"`
	BuildID      string    `json:"build_id"`
}

ImageStatus is the part of a record fugaro image status shows.

type Inputs

type Inputs struct {
	// Config is head's fugaro.yaml, and Workflow the workflow decided.
	Config   *config.Config
	Workflow string
	// Record is the workflow's image.json; nil when there is none.
	Record *Record
	// LastCheck is the previous check.json; nil when there is none.
	LastCheck *CheckState
	// LastBuildStatus is the Cloud Build status of LastCheck's build.
	LastBuildStatus string
	// Head is the base branch's head commit, and Tree its tree.
	Head string
	Tree Tree
	// BuiltCommitReachable: the record's commit is an ancestor of Head.
	BuiltCommitReachable bool
	// Changed reports whether a file matching one of globs changed
	// between the commits from and to, and which.
	Changed func(from, to string, globs []string) (bool, []string, error)
	// BaseRef is the base image the build would be given, and BaseDigest
	// its current digest ("" when it couldn't be read).
	BaseRef, BaseDigest string
	// LatestDigest is the digest the image's latest tag points at (""
	// when unknown); LatestMissing means the tag doesn't exist.
	LatestDigest  string
	LatestMissing bool
	// TemplateSalt is gcp.TemplateSalt of the build a rebuild would run;
	// "" leaves the salt out of the image-config trigger.
	TemplateSalt string
	Now          time.Time
	// Rebuild is the workflow's rebuild: block, with defaults filled in.
	Rebuild config.Rebuild
	Force   bool
}

Inputs are what one workflow's decision is made from.

type InputsFingerprint

type InputsFingerprint struct {
	SourceCommit    string            `json:"source_commit"`
	ImageConfigHash string            `json:"image_config_hash"`
	KeyFiles        map[string]string `json:"key_files"`
	BaseDigest      string            `json:"base_digest"`
}

InputsFingerprint is what a build would be built from: the back-off compares it with the one the last failed build ran with.

func (InputsFingerprint) Equal

Equal reports whether f and o are the same inputs.

type Record

type Record struct {
	Version  int       `json:"version"`
	Repo     string    `json:"repo"` // owner/name
	Workflow string    `json:"workflow"`
	BuiltAt  time.Time `json:"built_at"`
	BuildID  string    `json:"build_id"`

	SourceCommit string `json:"source_commit"`
	// SourceCommitTime is the source commit's committer time. The gate
	// compares these, since the build's shallow clone has no history.
	SourceCommitTime time.Time `json:"source_commit_time"`
	BaseBranch       string    `json:"base_branch"`

	KeyFiles        map[string]string `json:"key_files"` // path → git blob ID
	ImageConfigHash string            `json:"image_config_hash"`

	BaseRef     string `json:"base_ref"`     // the base image as the build was given it
	BaseDigest  string `json:"base_digest"`  // sha256:… of the base the build ran FROM
	Image       string `json:"image"`        // the image, without a tag
	ImageDigest string `json:"image_digest"` // sha256:… that latest was promoted to

	FugaroVersion string `json:"fugaro_version"`
	// SealedBudgetToken is true when the fugaro that rendered this record
	// reads the sealed budget token (docs/design/budget-token-channel.md
	// §7.1). That fugaro is the BASE image's own binary, which runs the image
	// build's render step (images/derived/cloudbuild.yaml): the field says
	// only that the base image's own build step recorded it. It does not say
	// which fugaro the launched image runs: a dockerfile: workflow's image is
	// the repository's to build, its fugaro included, and a job runs the
	// repository's :latest, which no launch compares with ImageDigest (nor
	// does base_ref's release gate). The launcher's seal gate trusts it as
	// far as that goes; a record without it says nothing either way.
	SealedBudgetToken bool   `json:"sealed_budget_token,omitempty"`
	TemplateSalt      string `json:"template_salt"`
	// Adopted is reserved for a record written for an image the build
	// did not make; builds always write false.
	Adopted bool `json:"adopted"`
}

Record is what one derived image was built from, as the build's record step writes it to builds/<slug>/<workflow>/image.json.

func ParseRecord

func ParseRecord(data []byte) (*Record, error)

ParseRecord decodes a record. Unknown fields are ignored, so a record a newer fugaro wrote still reads.

func (*Record) NewerThan

func (r *Record) NewerThan(o *Record) bool

NewerThan reports whether r describes a newer build than o: its source commit's committer time is later, or it is the same commit built later. Committer times need not follow history (clock skew, a rebase that keeps an old time); the check corrects a wrong answer, since a record whose commit isn't the branch head counts as changed.

type Registry

type Registry struct {
	// HTTP reads the anonymous registries; nil is a plain client.
	HTTP *http.Client
	// Google reads Artifact Registry; nil is an ADC client, made on first
	// use.
	Google *http.Client
	// Endpoint is a registry host's base URL; nil is https://<host>.
	Endpoint func(host string) string
	// contains filtered or unexported fields
}

Registry reads image digests with a HEAD of the manifest (the OCI distribution API). Artifact Registry (*-docker.pkg.dev) is read with Google's Application Default Credentials: the job account's metadata server token in the check job, the operator's ADC locally. Any other registry, ghcr.io included, is read anonymously, with a pull token from the registry's own token endpoint when it asks for one.

func (*Registry) Digest

func (r *Registry) Digest(ctx context.Context, ref string) (string, error)

Digest is the digest ref's manifest has now: ref itself when it is pinned by digest. A missing tag is ErrManifestUnknown.

type Status

type Status struct {
	Image  *ImageStatus `json:"image"`
	Check  *CheckState  `json:"check"`
	Errors []string     `json:"errors,omitempty"`
}

Status is what the runs bucket says about one workflow's image: its record and its last check, each nil when there is none.

func ReadStatus

func ReadStatus(ctx context.Context, b *blobx.Bucket, slug, workflow string, now time.Time) (Status, error)

ReadStatus reads the workflow's image.json and check.json. An object that is missing leaves its part nil; one that can't be read or parsed does too, with the problem in Errors. Only the bucket failing to answer is an error.

type Tree

type Tree interface {
	// BlobID is git's object ID of the file at path.
	BlobID(path string) (string, error)
	// Glob returns the files matching a doublestar pattern, as cache key
	// globs match them.
	Glob(pattern string) ([]string, error)
	ReadFile(path string) ([]byte, error)
}

Tree is a repository tree at one commit. Production uses GitTree for both sides, the build's checkout (Open) and the check's clone (Clone), so both read git's own blob IDs. Paths are slash-separated and relative to the tree's root. A missing path is fs.ErrNotExist.

Jump to

Keyboard shortcuts

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