feat: add CI build matrix planning from manifest targets
This commit is contained in:
parent
845d20541b
commit
f5e52463f2
7 changed files with 512 additions and 32 deletions
67
README.md
67
README.md
|
|
@ -47,6 +47,12 @@ les projets consommateurs :
|
|||
go run gitea.lclr.dev/AI/mcp-framework/cmd/mcp-framework@latest build
|
||||
```
|
||||
|
||||
Et une commande de planification de matrice CI :
|
||||
|
||||
```bash
|
||||
go run gitea.lclr.dev/AI/mcp-framework/cmd/mcp-framework@latest build plan --format github
|
||||
```
|
||||
|
||||
Par défaut la commande :
|
||||
|
||||
- lit `mcp.toml` (en remontant les répertoires parents)
|
||||
|
|
@ -64,10 +70,59 @@ Options principales :
|
|||
- `--build-dir`
|
||||
- `--goos`
|
||||
- `--goarch`
|
||||
- `--target` (`os/arch`, ex: `linux/amd64`)
|
||||
- `--version`
|
||||
- `--version-var`
|
||||
- `--ldflag` (répétable)
|
||||
|
||||
`build plan` options principales :
|
||||
|
||||
- `--manifest-dir`
|
||||
- `--binary`
|
||||
- `--package`
|
||||
- `--build-dir`
|
||||
- `--version-var`
|
||||
- `--format` (`json`, `github`, `gitlab`)
|
||||
|
||||
Exemple GitHub Actions (matrix dynamique) :
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: plan
|
||||
run: echo "matrix=$(mcp-framework build plan --format github | tr -d '\n')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: [plan]
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.plan.outputs.matrix) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: mcp-framework build --target "${{ matrix.target }}" --version "${{ github.ref_name }}"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
path: ${{ matrix.artifact_path }}
|
||||
```
|
||||
|
||||
Exemple GitLab CI (snippet matrix généré) :
|
||||
|
||||
```bash
|
||||
mcp-framework build plan --format gitlab > matrix.yml
|
||||
```
|
||||
|
||||
Puis inclure le snippet `parallel.matrix` dans le job de build et exécuter :
|
||||
|
||||
```bash
|
||||
mcp-framework build --target "${GOOS}/${GOARCH}" --version "${CI_COMMIT_TAG}"
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
- `bootstrap` : couche CLI optionnelle avec sous-commandes communes (`setup`, `mcp`, `config show|test`, `update`, `version`) et hooks métier explicites.
|
||||
|
|
@ -161,6 +216,17 @@ token_header = "Authorization"
|
|||
token_prefix = "token"
|
||||
token_env_names = ["GITEA_TOKEN"]
|
||||
|
||||
[build]
|
||||
main_package = "./cmd/my-mcp"
|
||||
output_dir = "build"
|
||||
version_var = "main.version"
|
||||
[[build.targets]]
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
[[build.targets]]
|
||||
os = "darwin"
|
||||
arch = "arm64"
|
||||
|
||||
[environment]
|
||||
known = ["MY_MCP_PROFILE", "MY_MCP_URL", "MY_MCP_TOKEN"]
|
||||
|
||||
|
|
@ -196,6 +262,7 @@ Champs supportés :
|
|||
- `[build].main_package` : package main à builder (ex: `./cmd/my-mcp`).
|
||||
- `[build].output_dir` : répertoire de sortie des artefacts (défaut `build`).
|
||||
- `[build].version_var` : variable Go ciblée par `-X` pour injecter la version (`main.version`, `gitlab.../internal/app.Version`, etc.).
|
||||
- `[[build.targets]]` : cibles de compilation (`os`, `arch`) exploitées par `mcp-framework build plan`.
|
||||
- `[environment].known` : variables d'environnement connues du projet.
|
||||
- `[secret_store].backend_policy` : politique de secret store (`auto`, `kwallet-only`, `keyring-any`, `env-only`).
|
||||
- `[profiles].default` : profil recommandé par défaut.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
|
@ -16,6 +17,43 @@ import (
|
|||
|
||||
type stringListFlag []string
|
||||
|
||||
type buildTarget struct {
|
||||
GOOS string
|
||||
GOARCH string
|
||||
}
|
||||
|
||||
type buildConfig struct {
|
||||
ProjectDir string
|
||||
BinaryName string
|
||||
MainPackage string
|
||||
BuildDir string
|
||||
VersionVar string
|
||||
ManifestTargets []manifestpkg.BuildTarget
|
||||
}
|
||||
|
||||
type buildConfigOverrides struct {
|
||||
BinaryName string
|
||||
MainPackage string
|
||||
BuildDir string
|
||||
VersionVar string
|
||||
}
|
||||
|
||||
type buildPlanTarget struct {
|
||||
GOOS string `json:"goos"`
|
||||
GOARCH string `json:"goarch"`
|
||||
Target string `json:"target"`
|
||||
ArtifactName string `json:"artifact_name"`
|
||||
ArtifactPath string `json:"artifact_path"`
|
||||
}
|
||||
|
||||
type buildPlanOutput struct {
|
||||
BinaryName string `json:"binary_name"`
|
||||
MainPackage string `json:"main_package"`
|
||||
BuildDir string `json:"build_dir"`
|
||||
VersionVar string `json:"version_var"`
|
||||
Targets []buildPlanTarget `json:"targets"`
|
||||
}
|
||||
|
||||
func (f *stringListFlag) String() string {
|
||||
return strings.Join(*f, ",")
|
||||
}
|
||||
|
|
@ -26,6 +64,13 @@ func (f *stringListFlag) Set(value string) error {
|
|||
}
|
||||
|
||||
func runBuild(args []string, stdout, stderr io.Writer) error {
|
||||
if len(args) > 0 {
|
||||
switch strings.TrimSpace(args[0]) {
|
||||
case "plan":
|
||||
return runBuildPlan(args[1:], stdout, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
if shouldShowHelp(args) {
|
||||
printBuildHelp(stdout)
|
||||
return nil
|
||||
|
|
@ -34,15 +79,8 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
|||
fs := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
defaultGOOS := strings.TrimSpace(os.Getenv("GOOS"))
|
||||
if defaultGOOS == "" {
|
||||
defaultGOOS = runtime.GOOS
|
||||
}
|
||||
|
||||
defaultGOARCH := strings.TrimSpace(os.Getenv("GOARCH"))
|
||||
if defaultGOARCH == "" {
|
||||
defaultGOARCH = runtime.GOARCH
|
||||
}
|
||||
defaultGOOS := resolveDefaultGOOS()
|
||||
defaultGOARCH := resolveDefaultGOARCH()
|
||||
|
||||
var manifestDir string
|
||||
var binaryName string
|
||||
|
|
@ -50,6 +88,7 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
|||
var buildDir string
|
||||
var goos string
|
||||
var goarch string
|
||||
var target string
|
||||
var version string
|
||||
var versionVar string
|
||||
var gocache string
|
||||
|
|
@ -61,6 +100,7 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
|||
fs.StringVar(&buildDir, "build-dir", "", "Répertoire de sortie (override [build].output_dir)")
|
||||
fs.StringVar(&goos, "goos", defaultGOOS, "GOOS cible")
|
||||
fs.StringVar(&goarch, "goarch", defaultGOARCH, "GOARCH cible")
|
||||
fs.StringVar(&target, "target", "", "Cible au format os/arch (override --goos/--goarch)")
|
||||
fs.StringVar(&version, "version", "", "Version injectée (par défaut VERSION env, puis git describe, puis dev)")
|
||||
fs.StringVar(&versionVar, "version-var", "", "Variable cible pour -X (override [build].version_var)")
|
||||
fs.StringVar(&gocache, "gocache", strings.TrimSpace(os.Getenv("GOCACHE")), "Valeur GOCACHE pour go build")
|
||||
|
|
@ -75,30 +115,31 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
|||
return fmt.Errorf("unexpected argument(s): %s", strings.Join(fs.Args(), ", "))
|
||||
}
|
||||
|
||||
file, manifestPath, err := manifestpkg.LoadDefault(manifestDir)
|
||||
cfg, err := resolveBuildConfig(manifestDir, buildConfigOverrides{
|
||||
BinaryName: binaryName,
|
||||
MainPackage: mainPackage,
|
||||
BuildDir: buildDir,
|
||||
VersionVar: versionVar,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectDir := filepath.Dir(manifestPath)
|
||||
|
||||
binaryName = firstNonEmpty(binaryName, file.BinaryName)
|
||||
if binaryName == "" {
|
||||
return errors.New("binary name is required (set binary_name in mcp.toml or --binary)")
|
||||
if strings.TrimSpace(target) != "" {
|
||||
parsedTarget, err := parseBuildTarget(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
goos = parsedTarget.GOOS
|
||||
goarch = parsedTarget.GOARCH
|
||||
}
|
||||
|
||||
mainPackage = firstNonEmpty(mainPackage, file.Build.MainPackage)
|
||||
if mainPackage == "" {
|
||||
mainPackage = fmt.Sprintf("./cmd/%s", binaryName)
|
||||
}
|
||||
goos = firstNonEmpty(goos, resolveDefaultGOOS())
|
||||
goarch = firstNonEmpty(goarch, resolveDefaultGOARCH())
|
||||
version = resolveBuildVersion(version, cfg.ProjectDir)
|
||||
versionVar = cfg.VersionVar
|
||||
|
||||
buildDir = firstNonEmpty(buildDir, file.Build.OutputDir, "build")
|
||||
goos = firstNonEmpty(goos, runtime.GOOS)
|
||||
goarch = firstNonEmpty(goarch, runtime.GOARCH)
|
||||
version = resolveBuildVersion(version, projectDir)
|
||||
versionVar = firstNonEmpty(versionVar, file.Build.VersionVar, "main.version")
|
||||
|
||||
outputPath, err := buildOutputPath(projectDir, buildDir, binaryName, goos, goarch)
|
||||
outputPath, err := buildOutputPath(cfg.ProjectDir, cfg.BuildDir, cfg.BinaryName, goos, goarch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -120,10 +161,10 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
|||
if len(ldflags) > 0 {
|
||||
cmdArgs = append(cmdArgs, "-ldflags", strings.Join(ldflags, " "))
|
||||
}
|
||||
cmdArgs = append(cmdArgs, "-o", outputPath, mainPackage)
|
||||
cmdArgs = append(cmdArgs, "-o", outputPath, cfg.MainPackage)
|
||||
|
||||
cmd := exec.Command("go", cmdArgs...)
|
||||
cmd.Dir = projectDir
|
||||
cmd.Dir = cfg.ProjectDir
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
cmd.Env = withEnvOverrides(os.Environ(), map[string]string{
|
||||
|
|
@ -143,10 +184,210 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runBuildPlan(args []string, stdout, stderr io.Writer) error {
|
||||
if shouldShowHelp(args) {
|
||||
printBuildPlanHelp(stdout)
|
||||
return nil
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("build plan", flag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
|
||||
var manifestDir string
|
||||
var binaryName string
|
||||
var mainPackage string
|
||||
var buildDir string
|
||||
var versionVar string
|
||||
var format string
|
||||
|
||||
fs.StringVar(&manifestDir, "manifest-dir", ".", "Répertoire de départ pour trouver mcp.toml")
|
||||
fs.StringVar(&binaryName, "binary", "", "Nom du binaire (override binary_name)")
|
||||
fs.StringVar(&mainPackage, "package", "", "Package main à builder (override [build].main_package)")
|
||||
fs.StringVar(&buildDir, "build-dir", "", "Répertoire de sortie (override [build].output_dir)")
|
||||
fs.StringVar(&versionVar, "version-var", "", "Variable cible pour -X (override [build].version_var)")
|
||||
fs.StringVar(&format, "format", "json", "Format de sortie: json|github|gitlab")
|
||||
|
||||
if err := fs.Parse(args); err != nil {
|
||||
_ = stderr
|
||||
return fmt.Errorf("parse build plan flags: %w", err)
|
||||
}
|
||||
|
||||
if fs.NArg() > 0 {
|
||||
return fmt.Errorf("unexpected argument(s): %s", strings.Join(fs.Args(), ", "))
|
||||
}
|
||||
|
||||
cfg, err := resolveBuildConfig(manifestDir, buildConfigOverrides{
|
||||
BinaryName: binaryName,
|
||||
MainPackage: mainPackage,
|
||||
BuildDir: buildDir,
|
||||
VersionVar: versionVar,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targets, err := resolveBuildTargets(cfg.ManifestTargets, resolveDefaultGOOS(), resolveDefaultGOARCH())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
planTargets := make([]buildPlanTarget, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
outputPath, err := buildOutputPath(cfg.ProjectDir, cfg.BuildDir, cfg.BinaryName, target.GOOS, target.GOARCH)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
planTargets = append(planTargets, buildPlanTarget{
|
||||
GOOS: target.GOOS,
|
||||
GOARCH: target.GOARCH,
|
||||
Target: formatBuildTarget(target.GOOS, target.GOARCH),
|
||||
ArtifactName: filepath.Base(outputPath),
|
||||
ArtifactPath: formatArtifactPath(cfg.ProjectDir, outputPath),
|
||||
})
|
||||
}
|
||||
|
||||
switch strings.ToLower(strings.TrimSpace(format)) {
|
||||
case "json":
|
||||
return renderJSON(stdout, buildPlanOutput{
|
||||
BinaryName: cfg.BinaryName,
|
||||
MainPackage: cfg.MainPackage,
|
||||
BuildDir: cfg.BuildDir,
|
||||
VersionVar: cfg.VersionVar,
|
||||
Targets: planTargets,
|
||||
})
|
||||
case "github":
|
||||
include := make([]map[string]string, 0, len(planTargets))
|
||||
for _, target := range planTargets {
|
||||
include = append(include, map[string]string{
|
||||
"goos": target.GOOS,
|
||||
"goarch": target.GOARCH,
|
||||
"target": target.Target,
|
||||
"artifact_name": target.ArtifactName,
|
||||
"artifact_path": target.ArtifactPath,
|
||||
})
|
||||
}
|
||||
return renderJSON(stdout, map[string]any{"include": include})
|
||||
case "gitlab":
|
||||
return renderGitLabMatrix(stdout, planTargets)
|
||||
default:
|
||||
return fmt.Errorf("unsupported build plan format %q (expected json, github or gitlab)", format)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveBuildConfig(manifestDir string, overrides buildConfigOverrides) (buildConfig, error) {
|
||||
file, manifestPath, err := manifestpkg.LoadDefault(manifestDir)
|
||||
if err != nil {
|
||||
return buildConfig{}, err
|
||||
}
|
||||
|
||||
projectDir := filepath.Dir(manifestPath)
|
||||
binaryName := firstNonEmpty(overrides.BinaryName, file.BinaryName)
|
||||
if binaryName == "" {
|
||||
return buildConfig{}, errors.New("binary name is required (set binary_name in mcp.toml or --binary)")
|
||||
}
|
||||
|
||||
mainPackage := firstNonEmpty(overrides.MainPackage, file.Build.MainPackage)
|
||||
if mainPackage == "" {
|
||||
mainPackage = fmt.Sprintf("./cmd/%s", binaryName)
|
||||
}
|
||||
|
||||
return buildConfig{
|
||||
ProjectDir: projectDir,
|
||||
BinaryName: binaryName,
|
||||
MainPackage: mainPackage,
|
||||
BuildDir: firstNonEmpty(overrides.BuildDir, file.Build.OutputDir, "build"),
|
||||
VersionVar: firstNonEmpty(overrides.VersionVar, file.Build.VersionVar, "main.version"),
|
||||
ManifestTargets: append([]manifestpkg.BuildTarget(nil), file.Build.Targets...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func resolveBuildTargets(manifestTargets []manifestpkg.BuildTarget, defaultGOOS, defaultGOARCH string) ([]buildTarget, error) {
|
||||
if len(manifestTargets) == 0 {
|
||||
defaultTarget, err := parseBuildTarget(formatBuildTarget(defaultGOOS, defaultGOARCH))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []buildTarget{defaultTarget}, nil
|
||||
}
|
||||
|
||||
targets := make([]buildTarget, 0, len(manifestTargets))
|
||||
for _, manifestTarget := range manifestTargets {
|
||||
target, err := parseBuildTarget(formatBuildTarget(manifestTarget.OS, manifestTarget.Arch))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func parseBuildTarget(value string) (buildTarget, error) {
|
||||
raw := strings.TrimSpace(value)
|
||||
parts := strings.Split(raw, "/")
|
||||
if len(parts) != 2 {
|
||||
return buildTarget{}, fmt.Errorf("invalid target %q (expected os/arch)", value)
|
||||
}
|
||||
|
||||
goos := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
goarch := strings.ToLower(strings.TrimSpace(parts[1]))
|
||||
if goos == "" || goarch == "" {
|
||||
return buildTarget{}, fmt.Errorf("invalid target %q (expected non-empty os/arch)", value)
|
||||
}
|
||||
|
||||
return buildTarget{GOOS: goos, GOARCH: goarch}, nil
|
||||
}
|
||||
|
||||
func formatBuildTarget(goos, goarch string) string {
|
||||
return strings.ToLower(strings.TrimSpace(goos)) + "/" + strings.ToLower(strings.TrimSpace(goarch))
|
||||
}
|
||||
|
||||
func formatArtifactPath(projectDir, outputPath string) string {
|
||||
rel, err := filepath.Rel(projectDir, outputPath)
|
||||
if err == nil && rel != "" && !strings.HasPrefix(rel, "..") && !filepath.IsAbs(rel) {
|
||||
return filepath.ToSlash(rel)
|
||||
}
|
||||
return filepath.ToSlash(outputPath)
|
||||
}
|
||||
|
||||
func renderJSON(w io.Writer, payload any) error {
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(payload)
|
||||
}
|
||||
|
||||
func renderGitLabMatrix(w io.Writer, targets []buildPlanTarget) error {
|
||||
if _, err := fmt.Fprintln(w, "parallel:"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(w, " matrix:"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, target := range targets {
|
||||
if _, err := fmt.Fprintf(w, " - GOOS: %q\n", target.GOOS); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, " GOARCH: %q\n", target.GOARCH); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printBuildHelp(w io.Writer) {
|
||||
fmt.Fprintf(
|
||||
w,
|
||||
"Usage:\n %s build [flags]\n\nFlags:\n --manifest-dir Répertoire de départ pour trouver mcp.toml (défaut: .)\n --binary Override de binary_name\n --package Override de [build].main_package\n --build-dir Override de [build].output_dir\n --goos GOOS cible (défaut: env GOOS ou runtime)\n --goarch GOARCH cible (défaut: env GOARCH ou runtime)\n --version Version injectée (défaut: env VERSION, git describe, puis dev)\n --version-var Variable cible pour -X (défaut: [build].version_var puis main.version)\n --gocache Valeur GOCACHE pour go build\n --ldflag Option additionnelle pour -ldflags (répéter si besoin)\n\nManifest optional ([build]):\n main_package = \"./cmd/<binary>\"\n output_dir = \"build\"\n version_var = \"main.version\"\n",
|
||||
"Usage:\n %s build [flags]\n %s build plan [flags]\n\nBuild flags:\n --manifest-dir Répertoire de départ pour trouver mcp.toml (défaut: .)\n --binary Override de binary_name\n --package Override de [build].main_package\n --build-dir Override de [build].output_dir\n --goos GOOS cible (défaut: env GOOS ou runtime)\n --goarch GOARCH cible (défaut: env GOARCH ou runtime)\n --target Cible os/arch (ex: linux/amd64), prioritaire sur --goos/--goarch\n --version Version injectée (défaut: env VERSION, git describe, puis dev)\n --version-var Variable cible pour -X (défaut: [build].version_var puis main.version)\n --gocache Valeur GOCACHE pour go build\n --ldflag Option additionnelle pour -ldflags (répéter si besoin)\n\nBuild plan flags:\n --manifest-dir Répertoire de départ pour trouver mcp.toml\n --binary Override de binary_name\n --package Override de [build].main_package\n --build-dir Override de [build].output_dir\n --version-var Override de [build].version_var\n --format json|github|gitlab\n\nManifest optional ([build]):\n main_package = \"./cmd/<binary>\"\n output_dir = \"build\"\n version_var = \"main.version\"\n [[build.targets]]\n os = \"linux\"\n arch = \"amd64\"\n",
|
||||
toolName,
|
||||
toolName,
|
||||
)
|
||||
}
|
||||
|
||||
func printBuildPlanHelp(w io.Writer) {
|
||||
fmt.Fprintf(
|
||||
w,
|
||||
"Usage:\n %s build plan [flags]\n\nFlags:\n --manifest-dir Répertoire de départ pour trouver mcp.toml\n --binary Override de binary_name\n --package Override de [build].main_package\n --build-dir Override de [build].output_dir\n --version-var Override de [build].version_var\n --format json|github|gitlab (défaut: json)\n",
|
||||
toolName,
|
||||
)
|
||||
}
|
||||
|
|
@ -232,3 +473,11 @@ func firstNonEmpty(values ...string) string {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveDefaultGOOS() string {
|
||||
return firstNonEmpty(os.Getenv("GOOS"), runtime.GOOS)
|
||||
}
|
||||
|
||||
func resolveDefaultGOARCH() string {
|
||||
return firstNonEmpty(os.Getenv("GOARCH"), runtime.GOARCH)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
|
@ -110,6 +111,108 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRunBuildSupportsTargetFlag(t *testing.T) {
|
||||
projectDir := t.TempDir()
|
||||
writeBuildFixture(t, projectDir, "target-flag", `
|
||||
module example.com/target-flag
|
||||
|
||||
go 1.25.0
|
||||
`, `
|
||||
binary_name = "target-flag"
|
||||
`, `
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
fmt.Print(version)
|
||||
}
|
||||
`)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
err := run([]string{
|
||||
"build",
|
||||
"--manifest-dir", projectDir,
|
||||
"--target", runtime.GOOS + "/" + runtime.GOARCH,
|
||||
"--version", "2.0.0",
|
||||
}, &stdout, &stderr)
|
||||
if err != nil {
|
||||
t.Fatalf("run returned error: %v (stderr: %s)", err, stderr.String())
|
||||
}
|
||||
|
||||
artifactPath := artifactPath(projectDir, "build", "target-flag", runtime.GOOS, runtime.GOARCH)
|
||||
output, err := exec.Command(artifactPath).Output()
|
||||
if err != nil {
|
||||
t.Fatalf("run artifact: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(string(output)) != "2.0.0" {
|
||||
t.Fatalf("artifact output = %q, want %q", strings.TrimSpace(string(output)), "2.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunBuildPlanGithubFormat(t *testing.T) {
|
||||
projectDir := t.TempDir()
|
||||
writeBuildFixture(t, projectDir, "matrix-mcp", `
|
||||
module example.com/matrix-mcp
|
||||
|
||||
go 1.25.0
|
||||
`, `
|
||||
binary_name = "matrix-mcp"
|
||||
|
||||
[build]
|
||||
output_dir = "dist"
|
||||
[[build.targets]]
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
[[build.targets]]
|
||||
os = "darwin"
|
||||
arch = "arm64"
|
||||
`, `
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
`)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
err := run([]string{
|
||||
"build", "plan",
|
||||
"--manifest-dir", projectDir,
|
||||
"--format", "github",
|
||||
}, &stdout, &stderr)
|
||||
if err != nil {
|
||||
t.Fatalf("run returned error: %v (stderr: %s)", err, stderr.String())
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Include []struct {
|
||||
GOOS string `json:"goos"`
|
||||
GOARCH string `json:"goarch"`
|
||||
Target string `json:"target"`
|
||||
ArtifactPath string `json:"artifact_path"`
|
||||
} `json:"include"`
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode github matrix: %v", err)
|
||||
}
|
||||
if len(payload.Include) != 2 {
|
||||
t.Fatalf("include length = %d, want 2", len(payload.Include))
|
||||
}
|
||||
|
||||
if payload.Include[0].Target != "linux/amd64" {
|
||||
t.Fatalf("first target = %q", payload.Include[0].Target)
|
||||
}
|
||||
if payload.Include[0].ArtifactPath != "dist/matrix-mcp-linux-amd64" {
|
||||
t.Fatalf("first artifact path = %q", payload.Include[0].ArtifactPath)
|
||||
}
|
||||
if payload.Include[1].Target != "darwin/arm64" {
|
||||
t.Fatalf("second target = %q", payload.Include[1].Target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHelp(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
|
@ -125,6 +228,9 @@ func TestBuildHelp(t *testing.T) {
|
|||
if !strings.Contains(output, "version_var") {
|
||||
t.Fatalf("build help should mention manifest build config: %q", output)
|
||||
}
|
||||
if !strings.Contains(output, "build plan") {
|
||||
t.Fatalf("build help should mention build plan: %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func writeBuildFixture(t *testing.T, projectDir, binaryName, goModContent, manifestContent, mainContent string) {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,15 @@ type Update struct {
|
|||
}
|
||||
|
||||
type Build struct {
|
||||
MainPackage string `toml:"main_package"`
|
||||
OutputDir string `toml:"output_dir"`
|
||||
VersionVar string `toml:"version_var"`
|
||||
MainPackage string `toml:"main_package"`
|
||||
OutputDir string `toml:"output_dir"`
|
||||
VersionVar string `toml:"version_var"`
|
||||
Targets []BuildTarget `toml:"targets"`
|
||||
}
|
||||
|
||||
type BuildTarget struct {
|
||||
OS string `toml:"os"`
|
||||
Arch string `toml:"arch"`
|
||||
}
|
||||
|
||||
type Environment struct {
|
||||
|
|
@ -172,6 +178,7 @@ func (b *Build) normalize() {
|
|||
b.MainPackage = strings.TrimSpace(b.MainPackage)
|
||||
b.OutputDir = strings.TrimSpace(b.OutputDir)
|
||||
b.VersionVar = strings.TrimSpace(b.VersionVar)
|
||||
b.Targets = normalizeBuildTargets(b.Targets)
|
||||
}
|
||||
|
||||
func (e *Environment) normalize() {
|
||||
|
|
@ -243,3 +250,30 @@ func normalizeStringList(values []string) []string {
|
|||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func normalizeBuildTargets(values []BuildTarget) []BuildTarget {
|
||||
if len(values) == 0 {
|
||||
return values
|
||||
}
|
||||
|
||||
normalized := values[:0]
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
target := BuildTarget{
|
||||
OS: strings.ToLower(strings.TrimSpace(value.OS)),
|
||||
Arch: strings.ToLower(strings.TrimSpace(value.Arch)),
|
||||
}
|
||||
if target.OS == "" || target.Arch == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := target.OS + "/" + target.Arch
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
normalized = append(normalized, target)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,18 @@ latest_release_url = "https://example.com/latest"
|
|||
main_package = " ./cmd/my-mcp "
|
||||
output_dir = " build "
|
||||
version_var = " main.version "
|
||||
[[build.targets]]
|
||||
os = " Linux "
|
||||
arch = " AMD64 "
|
||||
[[build.targets]]
|
||||
os = "darwin"
|
||||
arch = "arm64"
|
||||
[[build.targets]]
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
[[build.targets]]
|
||||
os = ""
|
||||
arch = "amd64"
|
||||
|
||||
[environment]
|
||||
known = [" MCP_PROFILE ", "", "MCP_TOKEN"]
|
||||
|
|
@ -197,6 +209,12 @@ description = " Client MCP interne "
|
|||
if file.Build.VersionVar != "main.version" {
|
||||
t.Fatalf("build version var = %q", file.Build.VersionVar)
|
||||
}
|
||||
if !slices.Equal(file.Build.Targets, []BuildTarget{
|
||||
{OS: "linux", Arch: "amd64"},
|
||||
{OS: "darwin", Arch: "arm64"},
|
||||
}) {
|
||||
t.Fatalf("build targets = %#v", file.Build.Targets)
|
||||
}
|
||||
if file.SecretStore.BackendPolicy != "auto" {
|
||||
t.Fatalf("secret store policy = %q", file.SecretStore.BackendPolicy)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -665,6 +665,9 @@ token_env_names = ["{{.ReleaseTokenEnv}}"]
|
|||
main_package = "./cmd/{{.BinaryName}}"
|
||||
output_dir = "build"
|
||||
version_var = "main.version"
|
||||
[[build.targets]]
|
||||
os = "linux"
|
||||
arch = "amd64"
|
||||
|
||||
[environment]
|
||||
known = [{{- range $index, $value := .KnownEnvironmentVariables}}{{if $index}}, {{end}}"{{$value}}"{{- end}}]
|
||||
|
|
|
|||
|
|
@ -90,6 +90,9 @@ func TestGenerateCreatesRecommendedSkeleton(t *testing.T) {
|
|||
"[environment]",
|
||||
"[profiles]",
|
||||
"version_var = \"main.version\"",
|
||||
"[[build.targets]]",
|
||||
"os = \"linux\"",
|
||||
"arch = \"amd64\"",
|
||||
"backend_policy = \"auto\"",
|
||||
} {
|
||||
if !strings.Contains(string(manifestContent), snippet) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue