Documentation
¶
Overview ¶
Package pubgh verifies GitHub Actions artifact handoff metadata, signed release bundles, and the draft GitHub Release publication state machine.
VerifyHandoff is a pure check over the ArtifactMeta port. It confirms that an artifact exists, belongs to the expected workflow run, has not expired, and reports the caller-supplied digest. It does not download the artifact and does not recompute the Actions ZIP transport digest.
VerifyBundle reconciles a distribution directory against its checksums.txt claim and then verifies the detached Sigstore bundle through BlobVerifier. Local checks run before the signature check.
Publish binds a git tag to github.sha, refuses unexpected assets, uploads the closed expected set onto the matching draft, converges GitHub-reported digests, and optionally leaves draft state. It never creates a release, re-drafts a public one, or deletes an asset.
Index ¶
- Variables
- type ArtifactDigest
- type ArtifactID
- type ArtifactMeta
- type ArtifactMetadata
- type Asset
- type AssetPath
- type AssetReplacer
- type AssetsView
- type BlobVerification
- type BlobVerifier
- type Bundle
- type BundleEntry
- type CommitSHA
- type Handoff
- type PollPolicy
- type PublishInput
- type PublishResult
- type Publisher
- type RefResolver
- type Release
- type ReleaseID
- type ReleaseReader
- type Repository
- type RunID
- type SleepFunc
- type TrustPolicy
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoDraft reports that no release for the tag appeared before the // draft poll budget expired. ErrNoDraft = errors.New("no draft release for the tag") // ErrAmbiguousRelease reports that the tag resolves to more than one // GitHub Release. ErrAmbiguousRelease = errors.New("tag does not uniquely resolve to one release") // ErrUnexpectedAsset reports that the release already contains an asset // name outside the expected closed set. Unexpected assets are refused // and are never deleted. ErrUnexpectedAsset = errors.New("release contains an unexpected asset") // ErrIndeterminate reports that a non-draft release exists for the tag // but its assets do not match the expected closed set exactly, so // [Publish] cannot tell whether a prior run completed successfully. ErrIndeterminate = errors.New("release state is indeterminate") )
Sentinel errors classified for Publish.
var ( // ErrRetryable reports a transient GitHub API failure that VerifyHandoff retries. ErrRetryable = errors.New("retryable github error") // ErrHandoffMismatch reports that observed artifact metadata failed the tuple check. ErrHandoffMismatch = errors.New("handoff mismatch") )
Sentinel errors classified for the handoff check.
Functions ¶
This section is empty.
Types ¶
type ArtifactDigest ¶
type ArtifactDigest string
ArtifactDigest is a lowercase sha256:<64 hex> digest.
The only constructor is ParseArtifactDigest, which accepts an optional sha256: prefix and normalizes hex to lowercase. The zero value is invalid.
func ParseArtifactDigest ¶
func ParseArtifactDigest(raw string) (ArtifactDigest, error)
ParseArtifactDigest constructs an ArtifactDigest from a SHA-256 hex string.
An optional sha256: prefix is accepted. Uppercase hex is normalized to lowercase. The returned value always uses the sha256: prefix.
func (ArtifactDigest) String ¶
func (d ArtifactDigest) String() string
String returns the canonical sha256:<hex> digest.
type ArtifactID ¶
type ArtifactID int64
ArtifactID is a positive GitHub Actions artifact identifier.
The only constructors are ParseArtifactID and ArtifactIDFromInt. The zero value is invalid.
func ArtifactIDFromInt ¶
func ArtifactIDFromInt(value int64) (ArtifactID, error)
ArtifactIDFromInt constructs an ArtifactID from a positive safe integer.
func ParseArtifactID ¶
func ParseArtifactID(raw string) (ArtifactID, error)
ParseArtifactID constructs an ArtifactID from a decimal string.
func (ArtifactID) String ¶
func (id ArtifactID) String() string
String returns the decimal identifier.
type ArtifactMeta ¶
type ArtifactMeta interface {
// Get returns metadata for id in repository.
//
// Callers must not assume Get downloads the archive or recomputes
// its digest.
Get(ctx context.Context, repository Repository, id ArtifactID) (ArtifactMetadata, error)
}
ArtifactMeta fetches Actions artifact metadata.
type ArtifactMetadata ¶
type ArtifactMetadata struct {
// ID is the observed artifact identifier.
ID ArtifactID
// Name is the observed artifact name.
Name string
// Digest is the GitHub-reported digest. The zero value means GitHub
// reported no digest.
Digest ArtifactDigest
// SizeBytes is the reported archive size.
SizeBytes int64
// HasRun reports whether workflow-run metadata was present.
HasRun bool
// Run is the workflow run that produced the artifact. It is only
// valid when HasRun is true.
Run RunID
// ExpiresAt is the reported expiry instant. The zero value means
// GitHub reported no expiry.
ExpiresAt time.Time
// Expired reports GitHub's expired flag.
Expired bool
}
ArtifactMetadata is the observed Actions artifact tuple.
Values are produced by ArtifactMeta.Get. A zero ArtifactMetadata is invalid and must not be treated as a successful lookup.
func VerifyHandoff ¶
func VerifyHandoff( ctx context.Context, meta ArtifactMeta, expected Handoff, sleep SleepFunc, ) (ArtifactMetadata, error)
VerifyHandoff confirms that the artifact metadata matches expected.
The artifact must exist, belong to expected.Run, not be expired, include workflow-run metadata, and report expected.Digest after normalization. A cancelled context.Context fails before the port is called. A nil context or port is rejected. Failures name what mismatched and never include credentials.
Get is called at most four times. Failures wrapping ErrRetryable wait 1s, then 2s, then 4s between attempts. Context cancellation returns immediately. Absent, authentication, and malformed responses are never retried. A nil sleep uses a context-aware timer.
type Asset ¶
type Asset struct {
// Name is the asset file name on the release.
Name string
// Digest is the GitHub-reported digest, "sha256:<64 hex>". It is empty
// until GitHub finishes processing the upload.
Digest string
// State is the GitHub-reported asset state. It is "uploaded" when the
// asset is ready.
State string
}
Asset is one GitHub Release asset observed through ReleaseReader.
type AssetPath ¶
type AssetPath string
AssetPath is a local filesystem path handed to AssetReplacer.
type AssetReplacer ¶
type AssetReplacer interface {
// Replace uploads expected onto the release identified by tag.
Replace(ctx context.Context, repository Repository, tag rel.Tag, expected []AssetPath) error
}
AssetReplacer uploads the expected local assets onto a draft release.
Clobber semantics live in the adapter. Unexpected existing assets must already have been refused by Publish before Replace is called.
type AssetsView ¶
type AssetsView struct {
// Assets are the GitHub-reported release assets, in API order.
Assets []Asset
}
AssetsView is the set of assets currently attached to a release.
type BlobVerification ¶
type BlobVerification struct {
// Payload is the name inside the distribution directory, for example
// checksums.txt.
Payload string
// Bundle is the name inside the distribution directory, for example
// checksums.txt.sigstore.json.
Bundle string
// Identity is the exact certificate identity URL.
Identity string
// Issuer is the OIDC issuer URL.
Issuer string
}
BlobVerification is one detached-bundle verification request.
type BlobVerifier ¶
type BlobVerifier interface {
// Verify checks request.Payload against request.Bundle using the
// exact identity and issuer. Implementations must not mutate the
// distribution directory.
Verify(ctx context.Context, request BlobVerification) error
}
BlobVerifier verifies a detached Sigstore bundle against a payload.
type Bundle ¶
type Bundle struct {
// Payloads are the checksummed release payloads, in checksums.txt order.
Payloads []BundleEntry
// Controls are exactly checksums.txt then checksums.txt.sigstore.json.
Controls []BundleEntry
}
Bundle is a closed, checksummed release distribution.
Payloads follow checksums.txt order. Controls are always checksums.txt then checksums.txt.sigstore.json.
func BuildBundle ¶
BuildBundle performs the closed-set reconciliation of claim against fsys.
Every claimed payload must be a regular file whose digest matches. Both control files must exist as regular files and must not appear in the claim. The directory must contain nothing else: no extra file, directory, symlink, or irregular entry. The closed-set scan uses each fs.DirEntry mode so a symlink is refused rather than followed. Payload digests are then checked with stage.VerifyBundle. Control digests are streamed with io.Copy into sha256.New. A nil filesystem is rejected. The first offending entry is named in the error.
func VerifyBundle ¶
func VerifyBundle( ctx context.Context, fsys fs.FS, verifier BlobVerifier, trust TrustPolicy, ) (Bundle, error)
VerifyBundle parses checksums.txt, builds the closed bundle, and verifies the Sigstore signature last.
Local checks run first: stage.ParseChecksums, then BuildBundle, then TrustPolicy.Normalize. The signature check runs only after every local check has passed. A nil context.Context, filesystem, or verifier is rejected. A cancelled context fails before the filesystem is read.
type BundleEntry ¶
type BundleEntry struct {
// Name is the flat file name inside the distribution directory.
Name string
// Digest is the lowercase SHA-256 hex digest with no prefix.
Digest stage.Digest
}
BundleEntry is one named digest inside a closed release bundle.
type CommitSHA ¶
type CommitSHA string
CommitSHA is a 40-digit lowercase Git commit object ID.
The only constructor is ParseCommitSHA. The zero value is invalid.
func ParseCommitSHA ¶
ParseCommitSHA constructs a CommitSHA from a 40-digit hex string.
Surrounding space is trimmed. Uppercase hex is normalized to lowercase.
type Handoff ¶
type Handoff struct {
// Repository is the repository that owns the artifact.
Repository Repository
// Run is the workflow run that must own the artifact.
Run RunID
// Artifact is the expected artifact identifier.
Artifact ArtifactID
// Digest is the expected GitHub-reported artifact digest.
Digest ArtifactDigest
}
Handoff is the expected Actions artifact metadata tuple.
The only constructor is NewHandoff. The zero value is invalid.
func NewHandoff ¶
func NewHandoff(repository Repository, run RunID, artifact ArtifactID, digest ArtifactDigest) (Handoff, error)
NewHandoff constructs a Handoff from already-validated domain values.
type PollPolicy ¶
type PollPolicy struct {
// Attempts is the maximum number of lookups, including the first.
Attempts int
// Wait is the pause after each failed lookup, including the last.
Wait time.Duration
}
PollPolicy is a bounded lookup budget.
A zero Attempts or Wait field is replaced by the matching field from DefaultDraftPolicy or DefaultAssetPolicy.
func DefaultAssetPolicy ¶
func DefaultAssetPolicy() PollPolicy
DefaultAssetPolicy returns the GR-16 asset-convergence budget: 12 attempts, 1s apart.
func DefaultDraftPolicy ¶
func DefaultDraftPolicy() PollPolicy
DefaultDraftPolicy returns the GR-06 draft-discovery budget: 24 attempts, 5s apart.
type PublishInput ¶
type PublishInput struct {
// Repository is the GitHub owner/name that owns the release.
Repository Repository
// Tag is the git tag bound to the draft release.
Tag rel.Tag
// Commit is the workflow's github.sha. The tag must resolve to it.
Commit CommitSHA
// Expected is the closed bundle rebuilt from the distribution
// directory. Payload and control names carry their hex digests.
Expected Bundle
// Assets are the local paths to upload, in [Bundle] order.
Assets []AssetPath
// Undraft is false for --no-undraft: converge and stop while still
// a draft.
Undraft bool
// Draft is the draft-discovery budget. The zero value selects
// [DefaultDraftPolicy].
Draft PollPolicy
// Asset is the asset-convergence budget. The zero value selects
// [DefaultAssetPolicy].
Asset PollPolicy
// Sleep waits between retryable GitHub calls and between poll
// attempts. Nil selects a context-aware timer.
Sleep SleepFunc
}
PublishInput is the closed input to Publish.
type PublishResult ¶
type PublishResult struct {
// ReleaseID is the GitHub Release identifier.
ReleaseID int64 `json:"release_id"`
// Tag is the git tag bound to the release.
Tag string `json:"tag"`
// URL is the GitHub html_url of the release.
URL string `json:"url"`
// Draft reports the release draft state after the run.
Draft bool `json:"draft"`
// Assets are the converged asset names, sorted.
Assets []string `json:"assets"`
}
PublishResult is the JSON document produced by a successful Publish.
func Publish ¶
func Publish( ctx context.Context, input PublishInput, reader ReleaseReader, replacer AssetReplacer, publisher Publisher, resolver RefResolver, ) (PublishResult, error)
Publish binds a tag to github.sha, uploads the expected assets onto the matching draft, converges them, and optionally leaves draft state.
The order is fail-closed at every step:
- Validate the input and reject a nil context or port.
- Resolve the tag and fail unless it equals PublishInput.Commit.
- Find the draft. Absence after the budget is ErrNoDraft. More than one release for the tag is ErrAmbiguousRelease.
- A non-draft release is a rerun after a completed publication. If Undraft is false, that is ErrIndeterminate: a draft-only publication was requested but the release is already public. If Undraft is true and the assets match the expected set exactly (same names and count, every asset uploaded with a nonempty digest, every digest equal to "sha256:" plus the expected hex), return success with Draft false. Any other difference is ErrIndeterminate. This branch never creates, re-drafts, uploads, or deletes.
- Read assets once before uploading and refuse any existing name outside the expected set with ErrUnexpectedAsset. Unexpected assets are never deleted.
- Replace every expected asset path. Clobber lives in the adapter.
- Converge under the asset policy. Incomplete readiness (count below expected, missing digest, or a state other than uploaded) is retried. A duplicate name, unexpected name, count above expected, or digest mismatch fails immediately.
- If Undraft, publish and require the final Get to report Draft false. A Publish, Get, or draft-state failure after the undraft call is ErrIndeterminate because the remote release may already have left draft. Otherwise require the release to still be a draft.
- Return the release URL and the sorted converged asset names.
Transient failures classified ErrRetryable are retried with the same bounded helper used by VerifyHandoff (four attempts, 1s/2s/4s). Tag and SHA mismatch, unexpected assets, and digest mismatches are never retried. A cancelled context.Context fails immediately.
type Publisher ¶
type Publisher interface {
// Publish marks the release public. It must not re-draft a public
// release and must not create a release.
Publish(ctx context.Context, repository Repository, release ReleaseID) error
}
Publisher leaves draft state on a populated release.
type RefResolver ¶
type RefResolver interface {
// Resolve returns the commit object ID named by tag.
Resolve(ctx context.Context, tag rel.Tag) (CommitSHA, error)
}
RefResolver resolves a git tag to the commit it currently names.
type Release ¶
type Release struct {
// ID is the GitHub Release identifier.
ID ReleaseID
// Tag is the git tag bound to the release.
Tag rel.Tag
// Draft reports whether the release is still a draft.
Draft bool
// URL is the GitHub html_url of the release.
URL string
}
Release is one GitHub Release observed through ReleaseReader.
type ReleaseID ¶
type ReleaseID int64
ReleaseID is a positive GitHub Release identifier.
The only constructors are ParseReleaseID and ReleaseIDFromInt. The zero value is invalid.
func ParseReleaseID ¶
ParseReleaseID constructs a ReleaseID from a decimal string.
func ReleaseIDFromInt ¶
ReleaseIDFromInt constructs a ReleaseID from a positive safe integer.
type ReleaseReader ¶
type ReleaseReader interface {
// FindDraft returns the unique release whose tag_name equals tag.
//
// The adapter lists once. Zero matches is [ErrNoDraft] immediately.
// More than one match is [ErrAmbiguousRelease]. Otherwise it returns
// the release with its real Draft flag; it does not wait for a draft
// and does not refuse a public release. [Publish] retries [ErrNoDraft]
// under [PublishInput.Draft].
FindDraft(ctx context.Context, repository Repository, tag rel.Tag) (Release, error)
// WaitAssets returns the current assets on release after one list pass.
//
// The adapter does not judge readiness, count, or digests. [Publish]
// re-reads under [PublishInput.Asset] until the expected set matches.
// Implementations must not mutate the release.
WaitAssets(ctx context.Context, repository Repository, release ReleaseID) (AssetsView, error)
// Get returns the current release metadata for id.
Get(ctx context.Context, repository Repository, release ReleaseID) (Release, error)
}
ReleaseReader observes GitHub Releases and their assets.
Implementations must not create, undraft, or delete a release, and must not delete an asset. FindDraft and WaitAssets perform one paginated snapshot each; they take no poll budget. Publish owns the 24×5s draft budget and the 12×1s asset-convergence budget.
type Repository ¶
type Repository struct {
// Owner is the account or organization that owns the repository.
Owner string
// Name is the repository name.
Name string
}
Repository is a GitHub owner/name pair.
The only constructor is ParseRepository. The zero value is invalid.
func ParseRepository ¶
func ParseRepository(raw string) (Repository, error)
ParseRepository constructs a Repository from an owner/name pair.
type RunID ¶
type RunID int64
RunID is a positive GitHub Actions workflow run identifier.
The only constructors are ParseRunID and RunIDFromInt. The zero value is invalid.
func ParseRunID ¶
ParseRunID constructs a RunID from a decimal string.
func RunIDFromInt ¶
RunIDFromInt constructs a RunID from a positive safe integer.
type TrustPolicy ¶
type TrustPolicy struct {
// Identity is the exact certificate identity URL. It is required.
Identity string
// Issuer is the OIDC issuer. An empty value defaults to
// https://token.actions.githubusercontent.com.
Issuer string
}
TrustPolicy is the exact Sigstore identity a release bundle must carry.
func (TrustPolicy) Normalize ¶
func (p TrustPolicy) Normalize() (TrustPolicy, error)
Normalize fills the default issuer and rejects an empty or non-URL identity or issuer.
Identity must be an absolute https URL. An empty issuer becomes https://token.actions.githubusercontent.com. The issuer, after defaulting, must also be an absolute https URL.