feat: publish Gamertan Tend preview source
Sanitized root snapshot from private source commit a72903c63e1753f9e6ffbf40453c0830bdfc05c5 and tree 295641e67eef5979da76746d8ae271249568263e. Private development history and workflows are excluded by the exact allowlist. AI-assisted: OpenAI Codex helped implement, test, and audit this preview. Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package packager
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"debug/buildinfo"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gamertan.com/tend/internal/config"
|
||||
"gamertan.com/tend/internal/process"
|
||||
"gamertan.com/tend/internal/provenance"
|
||||
)
|
||||
|
||||
var versionPattern = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+-preview\.[1-9][0-9]*$`)
|
||||
|
||||
type Result struct {
|
||||
Artifact string `json:"artifact"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Commit string `json:"commit"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
type Manifest struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
Service string `json:"service"`
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
SourceEpoch int64 `json:"source_date_epoch"`
|
||||
GOOS string `json:"goos"`
|
||||
GOARCH string `json:"goarch"`
|
||||
CGOEnabled bool `json:"cgo_enabled"`
|
||||
Binary string `json:"binary"`
|
||||
BinarySHA256 string `json:"binary_sha256"`
|
||||
GoVersion string `json:"go_version"`
|
||||
ModulePath string `json:"module_path"`
|
||||
ModuleVersion string `json:"module_version"`
|
||||
}
|
||||
type buildRecord struct {
|
||||
GoVersion string `json:"go_version"`
|
||||
Path string `json:"path"`
|
||||
Main moduleRecord `json:"main"`
|
||||
Deps []moduleRecord `json:"dependencies"`
|
||||
Settings []settingRecord `json:"settings"`
|
||||
}
|
||||
type moduleRecord struct {
|
||||
Path string `json:"path"`
|
||||
Version string `json:"version"`
|
||||
Sum string `json:"sum,omitempty"`
|
||||
}
|
||||
type settingRecord struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
type spdxDocument struct {
|
||||
SPDXVersion string `json:"spdxVersion"`
|
||||
DataLicense string `json:"dataLicense"`
|
||||
SPDXID string `json:"SPDXID"`
|
||||
Name string `json:"name"`
|
||||
DocumentNamespace string `json:"documentNamespace"`
|
||||
CreationInfo spdxCreation `json:"creationInfo"`
|
||||
Packages []spdxPackage `json:"packages"`
|
||||
Relationships []spdxRelationship `json:"relationships"`
|
||||
}
|
||||
type spdxCreation struct {
|
||||
Created string `json:"created"`
|
||||
Creators []string `json:"creators"`
|
||||
}
|
||||
type spdxPackage struct {
|
||||
Name string `json:"name"`
|
||||
SPDXID string `json:"SPDXID"`
|
||||
VersionInfo string `json:"versionInfo"`
|
||||
DownloadLocation string `json:"downloadLocation"`
|
||||
FilesAnalyzed bool `json:"filesAnalyzed"`
|
||||
LicenseConcluded string `json:"licenseConcluded"`
|
||||
LicenseDeclared string `json:"licenseDeclared"`
|
||||
}
|
||||
type spdxRelationship struct {
|
||||
SPDXElementID string `json:"spdxElementId"`
|
||||
RelationshipType string `json:"relationshipType"`
|
||||
RelatedSPDXElement string `json:"relatedSpdxElement"`
|
||||
}
|
||||
|
||||
func Package(ctx context.Context, runner process.Runner, cfg config.Config, sourceDir, outDir, version string) (Result, error) {
|
||||
if !versionPattern.MatchString(version) {
|
||||
return Result{}, errors.New("version must use vX.Y.Z-preview.N")
|
||||
}
|
||||
if !filepath.IsAbs(sourceDir) || !filepath.IsAbs(outDir) {
|
||||
return Result{}, errors.New("source and output directories must be absolute")
|
||||
}
|
||||
source, err := provenance.Inspect(ctx, runner, sourceDir, cfg.Build.Branch)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := verifyModules(ctx, runner, sourceDir); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
work, err := os.MkdirTemp("", "tend-package-")
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
defer os.RemoveAll(work)
|
||||
first := filepath.Join(work, "first", cfg.Build.Binary)
|
||||
second := filepath.Join(work, "second", cfg.Build.Binary)
|
||||
if err := build(ctx, runner, cfg, source, sourceDir, version, first); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := build(ctx, runner, cfg, source, sourceDir, version, second); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
firstSHA, err := fileSHA(first)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
secondSHA, err := fileSHA(second)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if firstSHA != secondSHA {
|
||||
return Result{}, errors.New("two clean builds were not byte-identical")
|
||||
}
|
||||
record, err := readBuildRecord(first)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if record.Main.Path == "" {
|
||||
return Result{}, errors.New("built binary has no main module provenance")
|
||||
}
|
||||
if err := verifyBuildSettings(record, source.Commit); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
manifest := Manifest{SchemaVersion: 1, Service: cfg.Service.Name, Version: version, Commit: source.Commit, SourceEpoch: source.Epoch, GOOS: "linux", GOARCH: "amd64", CGOEnabled: false, Binary: cfg.Build.Binary, BinarySHA256: firstSHA, GoVersion: record.GoVersion, ModulePath: record.Main.Path, ModuleVersion: record.Main.Version}
|
||||
bundle := filepath.Join(work, "bundle")
|
||||
if err := os.Mkdir(bundle, 0o700); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := copyFile(first, filepath.Join(bundle, cfg.Build.Binary), 0o755); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := writeJSON(filepath.Join(bundle, "RELEASE.json"), manifest, 0o644); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := writeJSON(filepath.Join(bundle, "BUILDINFO.json"), record, 0o644); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := writeJSON(filepath.Join(bundle, "SBOM.spdx.json"), makeSPDX(cfg, version, source, record), 0o644); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := writeSums(bundle, []string{cfg.Build.Binary, "BUILDINFO.json", "RELEASE.json", "SBOM.spdx.json"}); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := os.MkdirAll(outDir, 0o755); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
artifactName := fmt.Sprintf("%s-%s-linux-amd64.tar.gz", cfg.Service.Name, strings.TrimPrefix(version, "v"))
|
||||
artifact := filepath.Join(outDir, artifactName)
|
||||
if err := writeArchive(artifact, bundle, source.Epoch); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
artifactSHA, err := fileSHA(artifact)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := os.WriteFile(artifact+".sha256", []byte(artifactSHA+" "+artifactName+"\n"), 0o644); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return Result{Artifact: artifact, SHA256: artifactSHA, Commit: source.Commit, Version: version}, nil
|
||||
}
|
||||
|
||||
func verifyModules(ctx context.Context, runner process.Runner, dir string) error {
|
||||
out, err := runner.Run(ctx, dir, map[string]string{"GOWORK": "off", "GOFLAGS": "-mod=readonly"}, "go", "list", "-m", "-json", "all")
|
||||
if err != nil {
|
||||
return fmt.Errorf("list modules: %w", err)
|
||||
}
|
||||
dec := json.NewDecoder(strings.NewReader(string(out)))
|
||||
count := 0
|
||||
for {
|
||||
var module struct {
|
||||
Path, Version string
|
||||
Main bool
|
||||
Replace *json.RawMessage
|
||||
}
|
||||
if err := dec.Decode(&module); errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("decode module graph: %w", err)
|
||||
}
|
||||
count++
|
||||
if module.Replace != nil {
|
||||
return fmt.Errorf("module %s uses a replacement", module.Path)
|
||||
}
|
||||
if !module.Main && module.Version == "" {
|
||||
return fmt.Errorf("module %s is not pinned", module.Path)
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return errors.New("module graph is empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func build(ctx context.Context, runner process.Runner, cfg config.Config, source provenance.Source, dir, version, output string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(output), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
ldflags := []string{"-s", "-w"}
|
||||
date := time.Unix(source.Epoch, 0).UTC().Format(time.RFC3339)
|
||||
pairs := [][2]string{{cfg.Build.VersionSymbol, version}, {cfg.Build.CommitSymbol, source.Commit}, {cfg.Build.DateSymbol, date}}
|
||||
for _, pair := range pairs {
|
||||
if pair[0] != "" {
|
||||
ldflags = append(ldflags, "-X", pair[0]+"="+pair[1])
|
||||
}
|
||||
}
|
||||
args := []string{"build", "-mod=readonly", "-trimpath", "-buildvcs=true", "-ldflags", strings.Join(ldflags, " "), "-o", output, cfg.Build.Package}
|
||||
env := map[string]string{"GOWORK": "off", "GOFLAGS": "-mod=readonly", "GOOS": "linux", "GOARCH": "amd64", "CGO_ENABLED": "0", "SOURCE_DATE_EPOCH": fmt.Sprint(source.Epoch)}
|
||||
if _, err := runner.Run(ctx, dir, env, "go", args...); err != nil {
|
||||
return fmt.Errorf("build candidate: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readBuildRecord(path string) (buildRecord, error) {
|
||||
info, err := buildinfo.ReadFile(path)
|
||||
if err != nil {
|
||||
return buildRecord{}, fmt.Errorf("read Go build info: %w", err)
|
||||
}
|
||||
record := buildRecord{GoVersion: info.GoVersion, Path: info.Path, Main: moduleRecord{Path: info.Main.Path, Version: info.Main.Version, Sum: info.Main.Sum}}
|
||||
for _, dep := range info.Deps {
|
||||
if dep.Replace != nil {
|
||||
return buildRecord{}, fmt.Errorf("built binary contains replacement for %s", dep.Path)
|
||||
}
|
||||
record.Deps = append(record.Deps, moduleRecord{Path: dep.Path, Version: dep.Version, Sum: dep.Sum})
|
||||
}
|
||||
for _, setting := range info.Settings {
|
||||
record.Settings = append(record.Settings, settingRecord{Key: setting.Key, Value: setting.Value})
|
||||
}
|
||||
sort.Slice(record.Deps, func(i, j int) bool { return record.Deps[i].Path < record.Deps[j].Path })
|
||||
sort.Slice(record.Settings, func(i, j int) bool { return record.Settings[i].Key < record.Settings[j].Key })
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func verifyBuildSettings(record buildRecord, commit string) error {
|
||||
settings := map[string]string{}
|
||||
for _, setting := range record.Settings {
|
||||
settings[setting.Key] = setting.Value
|
||||
}
|
||||
for key, expected := range map[string]string{"vcs.revision": commit, "vcs.modified": "false", "GOOS": "linux", "GOARCH": "amd64", "CGO_ENABLED": "0"} {
|
||||
if settings[key] != expected {
|
||||
return fmt.Errorf("build setting %s is %q, expected %q", key, settings[key], expected)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func makeSPDX(cfg config.Config, version string, source provenance.Source, record buildRecord) spdxDocument {
|
||||
created := time.Unix(source.Epoch, 0).UTC().Format("2006-01-02T15:04:05Z")
|
||||
modules := append([]moduleRecord{record.Main}, record.Deps...)
|
||||
doc := spdxDocument{SPDXVersion: "SPDX-2.3", DataLicense: "CC0-1.0", SPDXID: "SPDXRef-DOCUMENT", Name: cfg.Service.Name + "-" + version, DocumentNamespace: "https://gamertan.com/tend/sbom/" + source.Commit + "/" + cfg.Service.Name, CreationInfo: spdxCreation{Created: created, Creators: []string{"Tool: gamertan.com/tend"}}}
|
||||
for i, module := range modules {
|
||||
id := fmt.Sprintf("SPDXRef-Package-%d", i+1)
|
||||
versionInfo := module.Version
|
||||
if versionInfo == "" {
|
||||
versionInfo = source.Commit
|
||||
}
|
||||
doc.Packages = append(doc.Packages, spdxPackage{Name: module.Path, SPDXID: id, VersionInfo: versionInfo, DownloadLocation: "NOASSERTION", FilesAnalyzed: false, LicenseConcluded: "NOASSERTION", LicenseDeclared: "NOASSERTION"})
|
||||
doc.Relationships = append(doc.Relationships, spdxRelationship{SPDXElementID: "SPDXRef-DOCUMENT", RelationshipType: "DESCRIBES", RelatedSPDXElement: id})
|
||||
}
|
||||
return doc
|
||||
}
|
||||
func writeJSON(path string, value any, mode os.FileMode) error {
|
||||
b, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b = append(b, '\n')
|
||||
return os.WriteFile(path, b, mode)
|
||||
}
|
||||
func writeSums(dir string, names []string) error {
|
||||
sort.Strings(names)
|
||||
var b strings.Builder
|
||||
for _, name := range names {
|
||||
sum, err := fileSHA(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(&b, "%s %s\n", sum, name)
|
||||
}
|
||||
return os.WriteFile(filepath.Join(dir, "SHA256SUMS"), []byte(b.String()), 0o644)
|
||||
}
|
||||
func writeArchive(path, bundle string, epoch int64) error {
|
||||
tmp := path + ".tmp"
|
||||
_ = os.Remove(tmp)
|
||||
file, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok := false
|
||||
defer func() {
|
||||
_ = file.Close()
|
||||
if !ok {
|
||||
_ = os.Remove(tmp)
|
||||
}
|
||||
}()
|
||||
gz := gzip.NewWriter(file)
|
||||
gz.Header.ModTime = time.Unix(0, 0)
|
||||
gz.Header.OS = 255
|
||||
tw := tar.NewWriter(gz)
|
||||
entries, err := os.ReadDir(bundle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Type().IsRegular() {
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
root := filepath.Base(bundle)
|
||||
if err := tw.WriteHeader(&tar.Header{Name: root + "/", Typeflag: tar.TypeDir, Mode: 0o755, ModTime: time.Unix(epoch, 0), Uid: 0, Gid: 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range names {
|
||||
data, err := os.ReadFile(filepath.Join(bundle, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := int64(0o644)
|
||||
if name != "BUILDINFO.json" && !strings.HasSuffix(name, ".json") && name != "SHA256SUMS" {
|
||||
mode = 0o755
|
||||
}
|
||||
if err := tw.WriteHeader(&tar.Header{Name: root + "/" + name, Typeflag: tar.TypeReg, Mode: mode, Size: int64(len(data)), ModTime: time.Unix(epoch, 0), Uid: 0, Gid: 0}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tw.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
return err
|
||||
}
|
||||
ok = true
|
||||
return nil
|
||||
}
|
||||
func copyFile(source, target string, mode os.FileMode) error {
|
||||
in, err := os.Open(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
_ = out.Close()
|
||||
return err
|
||||
}
|
||||
if err := out.Sync(); err != nil {
|
||||
_ = out.Close()
|
||||
return err
|
||||
}
|
||||
return out.Close()
|
||||
}
|
||||
func fileSHA(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user