release

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package release implements the `huan release` command: cross-compile huan for a set of target platforms, archive each with LICENSE/README, compute sha256 checksums, and emit a JSON manifest into /release/{version}/.

Scope (per ADR 0004 + docs/progress/release-command.md):

  • Pure local artifact production. No git tag, no push, no remote upload.
  • Operator huan (the binary running `huan release`) is decoupled from artifact huans (the binaries being packaged). Artifacts are always freshly compiled via `go build` with cross-compile flags.
  • Five standard targets by default; `--targets` flag for overrides.
  • Reproducible: -trimpath + CGO_ENABLED=0 + no wall-clock ldflags => same commit + same Go version produces byte-identical binaries.

Logging uses the cross-cutting internal/observability package (shared with internal/deploy). Report/Artifact/Failure types are release-specific and stay in this package — their shapes differ from deploy's analogues.

Index

Constants

This section is empty.

Variables

View Source
var StandardTargets = []Target{
	{OS: "darwin", Arch: "amd64"},
	{OS: "darwin", Arch: "arm64"},
	{OS: "linux", Arch: "amd64"},
	{OS: "linux", Arch: "arm64"},
	{OS: "windows", Arch: "amd64"},
}

StandardTargets is the default platform matrix: darwin (amd64+arm64), linux (amd64+arm64), windows (amd64). Covers ~99% of Go CLI users per Hugo/Caddy precedent. Override with the --targets flag.

Functions

func ArchiveName

func ArchiveName(t Target, version string) string

ArchiveName returns the release archive filename for a target. Unix targets use tar.gz; windows uses zip (Windows users expect zip and many older tar implementations on Windows mishandle permissions).

func BinaryName

func BinaryName(t Target) string

BinaryName returns the binary filename for a target. Windows needs the .exe suffix (without it, Windows won't execute); other platforms use the bare name.

func ChecksumsFilename

func ChecksumsFilename(version string) string

ChecksumsFilename returns the checksums file name (shasum-compatible).

func ChecksumsLine

func ChecksumsLine(filename, sha256 string) string

ChecksumsLine renders one shasum-compatible line: "<sha256> <filename>\n". The two-space separator matches `shasum -a 256` output so users can verify with `shasum -a 256 -c file.txt`.

func CreateArchive

func CreateArchive(outFile string, target Target, members []ArchiveMember) error

CreateArchive dispatches to CreateTarGZ or CreateZip based on target OS.

func CreateTarGZ

func CreateTarGZ(outFile string, members []ArchiveMember) error

CreateTarGZ writes members as a flat (no wrapping dir) gzipped tar to outFile. The output is deterministic: no directory entries, no mtime in tar headers (zero time used so two runs produce identical bytes).

Per ADR 0004 §6: archive contents are flat — binary, LICENSE, READMEs at the tarball root. This matches Hugo/Caddy convention so users can `tar xzf huan_*.tar.gz -C ~/bin` without an intermediate dir.

func CreateZip

func CreateZip(outFile string, members []ArchiveMember) error

CreateZip writes members as a flat zip archive. Zip format is used for Windows targets (no permission info preserved; Windows doesn't need execute bit on .exe files).

Like CreateTarGZ, the output is deterministic: modification time is zeroed so two runs produce identical bytes.

func ManifestFilename

func ManifestFilename(version string) string

ManifestFilename returns the JSON manifest file name.

func OutDir

func OutDir(outRoot, version string) string

OutDir returns the absolute path to the per-version release directory. outRoot is typically <sourceDir>/release.

func SHA256File

func SHA256File(path string) (string, error)

SHA256File returns the hex-encoded sha256 of the file at path. Reads in 32KB chunks to handle large files without buffering entire content.

func ValidateVersion

func ValidateVersion(v string) error

ValidateVersion returns an error if v is not a canonical semver string. Empty strings, strings with leading "v", strings with whitespace, and non-semver like "latest" or "dev" are all rejected.

Examples:

"0.1.0"        → nil
"0.1.0-rc1"    → nil
"0.1.0+b.5"    → nil
""             → error "empty version"
"v0.1.0"       → error "leading v not allowed"
"0.1"          → error "not semver"
"0.1.0\n"      → error "contains whitespace"
"latest"       → error "not semver"

func WriteChecksumsFile

func WriteChecksumsFile(outDir, version string, artifacts []Artifact) (string, error)

WriteChecksumsFile writes a shasum-compatible file containing one line per artifact. Lines are sorted by filename for deterministic output. The file is written atomically (write to temp, then rename) to avoid leaving a partially-written checksums file if the process is interrupted.

outDir must exist; the file is named via ChecksumsFilename(version).

func WriteManifest

func WriteManifest(outDir, version string, report *Report) (string, error)

WriteManifest serializes report as JSON to <outDir>/<ManifestFilename>. Output is deterministic: field order is fixed (struct declaration order), and json.Marshal with sorted map keys (none here, but defensive) is stable.

Atomic write (temp + rename) so partial writes never appear on disk.

Types

type ArchiveMember

type ArchiveMember struct {
	Name string // path inside the archive (e.g. "huan", "LICENSE")
	Path string // source path on disk
	Mode os.FileMode
}

ArchiveMember is a single file to pack into the archive.

type Artifact

type Artifact struct {
	Name   string `json:"name"`   // e.g. "huan_0.1.0_darwin_arm64.tar.gz"
	SHA256 string `json:"sha256"` // hex-encoded sha256 of the file bytes
	Size   int64  `json:"size"`   // file size in bytes
	Binary string `json:"binary"` // binary name inside the archive ("huan" or "huan.exe")
	OS     string `json:"os"`     // target OS (matches Target.OS)
	Arch   string `json:"arch"`   // target arch (matches Target.Arch)
}

Artifact describes one file in the release output.

type Builder

type Builder interface {
	Build(ctx context.Context, target Target, outPath string) error
}

Builder abstracts the "compile huan for one target" step so unit tests can inject a mock without invoking the real `go build`. Production callers use GoBuildBuilder; tests use MockBuilder.

type Failure

type Failure struct {
	Target Target `json:"target"` // darwin/arm64 etc.
	Phase  string `json:"phase"`  // "compile" / "archive" / "checksum" / "manifest"
	Error  string `json:"error"`
}

Failure captures a per-target error during the release pipeline.

type GoBuildBuilder

type GoBuildBuilder struct {
	SourceDir string // project root containing cmd/huan
	Logger    *observability.Logger
}

GoBuildBuilder invokes `go build` with cross-compile flags per ADR 0004 §7. Flags applied to every target:

CGO_ENABLED=0          # static binary, cross-compile friendly
-trimpath              # strip local paths (reproducible + privacy)
-ldflags="-s -w"       # strip symbol table + DWARF (~30% size reduction)
GOOS=<target.OS>
GOARCH=<target.Arch>

The resulting binary is byte-identical across runs given the same source + Go version (verifiable via the determinism integration test).

func (*GoBuildBuilder) Build

func (b *GoBuildBuilder) Build(ctx context.Context, target Target, outPath string) error

Build compiles huan for target into outPath. The source directory must contain cmd/huan (the main package). outPath's parent directory must already exist.

type MockBuilder

type MockBuilder struct {
	FailTargets map[Target]error // targets that should fail
	BuildLog    []Target         // record of Build calls, in order
	Content     []byte           // bytes to write as the "binary" (default = mock target name)
}

MockBuilder is the test double for Builder. It writes a small deterministic "binary" file for each requested target, optionally returning a per-target failure. The build log lets tests assert which targets were attempted and in what order.

func (*MockBuilder) Build

func (m *MockBuilder) Build(_ context.Context, target Target, outPath string) error

Build writes a deterministic mock binary to outPath, or returns the configured failure for this target.

type Options

type Options struct {
	Version   string   // canonical semver (no leading v), e.g. "0.1.0"
	OutDir    string   // absolute path to /release/{version}/
	Targets   []Target // platforms to compile for; must be non-empty
	SourceDir string   // project root, used to locate LICENSE/README and main pkg
	DryRun    bool     // when true, build to a temp dir, never touch OutDir
	TraceID   string   // optional; auto-generated when empty
}

Options configures a release invocation.

type Report

type Report struct {
	TraceID    string     `json:"trace_id"`
	Version    string     `json:"version"`
	GoVersion  string     `json:"go_version"`
	GitSHA     string     `json:"git_sha,omitempty"`
	GitDirty   bool       `json:"git_dirty,omitempty"`
	BuildTime  string     `json:"build_time"`
	OutDir     string     `json:"out_dir"`
	Targets    []string   `json:"targets"`
	Artifacts  []Artifact `json:"artifacts"`
	DurationMs int64      `json:"duration_ms"`
	DryRun     bool       `json:"dry_run"`
	Failures   []Failure  `json:"failures,omitempty"`
}

Report is the machine-readable outcome of a release invocation.

func ParseManifest

func ParseManifest(path string) (*Report, error)

ParseManifest reads + unmarshals a manifest JSON file. Used by future tooling (e.g. `huan upgrade`) to inspect what a release contains. Currently exercised by golden tests; left exported for symmetry.

func Release

func Release(ctx context.Context, opts Options, builder Builder, logger *observability.Logger) (*Report, error)

Release runs the full release pipeline:

  1. Validate inputs (version semver, LICENSE present, targets non-empty).
  2. For each target: build → archive → checksum (continue on per-target failure, collect into Report.Failures per ADR 0004 §15 / Q15 A1).
  3. Write checksums.txt + manifest.json atomically.
  4. If !opts.DryRun: move artifacts to opts.OutDir (overwriting expected files, leaving any extra files the operator may have added — Q15 B1).
  5. Always: clean up the temp work dir.

ctx cancellation (Ctrl-C / signal.NotifyContext) propagates to all `go build` subprocesses via the Builder (Q15 C1).

On success, returns a populated Report. On validation failure, returns (nil, error) before touching the filesystem. On per-target failure, returns (report, nil) — the report's Failures slice is the truth.

type Target

type Target struct {
	OS   string // e.g. "darwin", "linux", "windows"
	Arch string // e.g. "amd64", "arm64"
}

Target describes a GOOS/GOARCH pair to compile for.

func HostTarget

func HostTarget() Target

HostTarget returns the current host's Target (useful for `--targets=current` and for the smoke-test path).

func (Target) String

func (t Target) String() string

String returns "os/arch" (e.g. "darwin/arm64") for log lines and errors.

Jump to

Keyboard shortcuts

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