go-build/pkg/build/ci.go
Snider 77aaac226c feat(build): discovery enrichment, build options, CI output format
- Add docs/python/rust project detection + DiscoverFull()
- Add BuildOptions with ComputeOptions, webkit tag injection, garble/NSIS/WebView2
- Add CI output: GitHub annotations, DetectCI, ArtifactName, WriteArtifactMeta
- Config struct additions for obfuscate/nsis/webview2
- Full Good/Bad/Ugly test coverage

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-31 19:33:31 +01:00

183 lines
5.1 KiB
Go

// Package build provides project type detection and cross-compilation for the Core build system.
// This file handles CI environment detection and GitHub Actions output formatting.
package build
import (
"encoding/json"
"dappco.re/go/core"
io_interface "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
)
// CIContext holds environment information detected from a GitHub Actions run.
//
// ci := build.DetectCI()
// if ci != nil {
// fmt.Println(ci.ShortSHA) // "abc1234"
// }
type CIContext struct {
// Ref is the full git ref (GITHUB_REF).
// ci.Ref // "refs/tags/v1.2.3"
Ref string
// SHA is the full commit hash (GITHUB_SHA).
// ci.SHA // "abc1234def5678..."
SHA string
// ShortSHA is the first 7 characters of SHA.
// ci.ShortSHA // "abc1234"
ShortSHA string
// Tag is the tag name when the ref is a tag ref.
// ci.Tag // "v1.2.3"
Tag string
// IsTag is true when the ref is a tag ref (refs/tags/...).
// ci.IsTag // true
IsTag bool
// Branch is the branch name when the ref is a branch ref.
// ci.Branch // "main"
Branch string
// Repo is the owner/repo string (GITHUB_REPOSITORY).
// ci.Repo // "dappcore/core"
Repo string
// Owner is the repository owner derived from Repo.
// ci.Owner // "dappcore"
Owner string
}
// artifactMeta is the structure written to artifact_meta.json.
type artifactMeta struct {
Name string `json:"name"`
OS string `json:"os"`
Arch string `json:"arch"`
Ref string `json:"ref,omitempty"`
SHA string `json:"sha,omitempty"`
Tag string `json:"tag,omitempty"`
Branch string `json:"branch,omitempty"`
IsTag bool `json:"is_tag"`
Repo string `json:"repo,omitempty"`
}
// FormatGitHubAnnotation formats a build message as a GitHub Actions annotation.
//
// s := build.FormatGitHubAnnotation("error", "main.go", 42, "undefined: foo")
// // "::error file=main.go,line=42::undefined: foo"
//
// s := build.FormatGitHubAnnotation("warning", "pkg/build/ci.go", 10, "unused import")
// // "::warning file=pkg/build/ci.go,line=10::unused import"
func FormatGitHubAnnotation(level, file string, line int, message string) string {
return core.Sprintf("::%s file=%s,line=%d::%s", level, file, line, message)
}
// DetectCI reads GitHub Actions environment variables and returns a populated CIContext.
// Returns nil if GITHUB_ACTIONS is not set or GITHUB_SHA is empty — indicating not in CI.
//
// ci := build.DetectCI()
// if ci == nil {
// // running locally, skip CI-specific output
// }
// if ci != nil && ci.IsTag {
// // upload release assets
// }
func DetectCI() *CIContext {
sha := core.Env("GITHUB_SHA")
if sha == "" {
return nil
}
ref := core.Env("GITHUB_REF")
repo := core.Env("GITHUB_REPOSITORY")
ctx := &CIContext{
Ref: ref,
SHA: sha,
Repo: repo,
}
// ShortSHA is first 7 chars of SHA.
runes := []rune(sha)
if len(runes) >= 7 {
ctx.ShortSHA = string(runes[:7])
} else {
ctx.ShortSHA = sha
}
// Derive owner from "owner/repo" format.
if repo != "" {
parts := core.SplitN(repo, "/", 2)
if len(parts) == 2 {
ctx.Owner = parts[0]
}
}
// Classify ref as tag or branch.
const tagPrefix = "refs/tags/"
const branchPrefix = "refs/heads/"
if core.HasPrefix(ref, tagPrefix) {
ctx.IsTag = true
ctx.Tag = core.TrimPrefix(ref, tagPrefix)
} else if core.HasPrefix(ref, branchPrefix) {
ctx.Branch = core.TrimPrefix(ref, branchPrefix)
}
return ctx
}
// ArtifactName generates a canonical artifact filename from the build name, CI context, and target.
// Format: {name}_{OS}_{ARCH}_{TAG|SHORT_SHA}
// When ci is nil or has no tag or SHA, only the name and target are used.
//
// name := build.ArtifactName("core", ci, build.Target{OS: "linux", Arch: "amd64"})
// // "core_linux_amd64_v1.2.3" (when ci.IsTag)
// // "core_linux_amd64_abc1234" (when ci != nil, not a tag)
// // "core_linux_amd64" (when ci is nil)
func ArtifactName(buildName string, ci *CIContext, target Target) string {
base := core.Join("_", buildName, target.OS, target.Arch)
if ci == nil {
return base
}
var version string
if ci.IsTag && ci.Tag != "" {
version = ci.Tag
} else if ci.ShortSHA != "" {
version = ci.ShortSHA
}
if version == "" {
return base
}
return core.Concat(base, "_", version)
}
// WriteArtifactMeta writes an artifact_meta.json file to path.
// The file contains the build name, target OS/arch, and CI metadata if available.
//
// err := build.WriteArtifactMeta(io.Local, "dist/artifact_meta.json", "core", ci)
// // writes: {"name":"core","os":"linux","arch":"amd64","tag":"v1.2.3","is_tag":true,...}
func WriteArtifactMeta(fs io_interface.Medium, path string, buildName string, ci *CIContext) error {
meta := artifactMeta{
Name: buildName,
}
if ci != nil {
meta.Ref = ci.Ref
meta.SHA = ci.SHA
meta.Tag = ci.Tag
meta.Branch = ci.Branch
meta.IsTag = ci.IsTag
meta.Repo = ci.Repo
}
data, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return coreerr.E("build.WriteArtifactMeta", "failed to marshal artifact meta", err)
}
if err := fs.Write(path, string(data)); err != nil {
return coreerr.E("build.WriteArtifactMeta", "failed to write artifact meta", err)
}
return nil
}