// 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, which indicates // the process is not running inside GitHub Actions. // // ci := build.DetectCI() // if ci == nil { // // running locally, skip CI-specific output // } // if ci != nil && ci.IsTag { // // upload release assets // } func DetectCI() *CIContext { return detectGitHubContext(true) } // DetectGitHubMetadata returns GitHub CI metadata when the standard environment // variables are present, even if GITHUB_ACTIONS is unset. // // This is useful for metadata emission paths that only need the GitHub ref/SHA // shape and should not be coupled to a specific runner environment. func DetectGitHubMetadata() *CIContext { return detectGitHubContext(false) } func detectGitHubContext(requireActions bool) *CIContext { if requireActions && core.Env("GITHUB_ACTIONS") == "" { return nil } 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, } populateGitHubContext(ctx) return ctx } func populateGitHubContext(ctx *CIContext) { if ctx == nil { return } // ShortSHA is first 7 chars of SHA. runes := []rune(ctx.SHA) if len(runes) >= 7 { ctx.ShortSHA = string(runes[:7]) } else { ctx.ShortSHA = ctx.SHA } // Derive owner from "owner/repo" format. if ctx.Repo != "" { parts := core.SplitN(ctx.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(ctx.Ref, tagPrefix) { ctx.IsTag = true ctx.Tag = core.TrimPrefix(ctx.Ref, tagPrefix) } else if core.HasPrefix(ctx.Ref, branchPrefix) { ctx.Branch = core.TrimPrefix(ctx.Ref, branchPrefix) } } // 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", build.Target{OS: "linux", Arch: "amd64"}, 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, target Target, ci *CIContext) error { meta := artifactMeta{ Name: buildName, OS: target.OS, Arch: target.Arch, } 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 }