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 ¶
- Variables
- func ArchiveName(t Target, version string) string
- func BinaryName(t Target) string
- func ChecksumsFilename(version string) string
- func ChecksumsLine(filename, sha256 string) string
- func CreateArchive(outFile string, target Target, members []ArchiveMember) error
- func CreateTarGZ(outFile string, members []ArchiveMember) error
- func CreateZip(outFile string, members []ArchiveMember) error
- func ManifestFilename(version string) string
- func OutDir(outRoot, version string) string
- func SHA256File(path string) (string, error)
- func ValidateVersion(v string) error
- func WriteChecksumsFile(outDir, version string, artifacts []Artifact) (string, error)
- func WriteManifest(outDir, version string, report *Report) (string, error)
- type ArchiveMember
- type Artifact
- type Builder
- type Failure
- type GoBuildBuilder
- type MockBuilder
- type Options
- type Report
- type Target
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
ChecksumsFilename returns the checksums file name (shasum-compatible).
func ChecksumsLine ¶
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 ¶
ManifestFilename returns the JSON manifest file name.
func OutDir ¶
OutDir returns the absolute path to the per-version release directory. outRoot is typically <sourceDir>/release.
func SHA256File ¶
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 ¶
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 ¶
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 ¶
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 ¶
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).
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.
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 ¶
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:
- Validate inputs (version semver, LICENSE present, targets non-empty).
- For each target: build → archive → checksum (continue on per-target failure, collect into Report.Failures per ADR 0004 §15 / Q15 A1).
- Write checksums.txt + manifest.json atomically.
- If !opts.DryRun: move artifacts to opts.OutDir (overwriting expected files, leaving any extra files the operator may have added — Q15 B1).
- 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).