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
|
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 :
|
Par défaut la commande :
|
||||||
|
|
||||||
- lit `mcp.toml` (en remontant les répertoires parents)
|
- lit `mcp.toml` (en remontant les répertoires parents)
|
||||||
|
|
@ -64,10 +70,59 @@ Options principales :
|
||||||
- `--build-dir`
|
- `--build-dir`
|
||||||
- `--goos`
|
- `--goos`
|
||||||
- `--goarch`
|
- `--goarch`
|
||||||
|
- `--target` (`os/arch`, ex: `linux/amd64`)
|
||||||
- `--version`
|
- `--version`
|
||||||
- `--version-var`
|
- `--version-var`
|
||||||
- `--ldflag` (répétable)
|
- `--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
|
## Packages
|
||||||
|
|
||||||
- `bootstrap` : couche CLI optionnelle avec sous-commandes communes (`setup`, `mcp`, `config show|test`, `update`, `version`) et hooks métier explicites.
|
- `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_prefix = "token"
|
||||||
token_env_names = ["GITEA_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]
|
[environment]
|
||||||
known = ["MY_MCP_PROFILE", "MY_MCP_URL", "MY_MCP_TOKEN"]
|
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].main_package` : package main à builder (ex: `./cmd/my-mcp`).
|
||||||
- `[build].output_dir` : répertoire de sortie des artefacts (défaut `build`).
|
- `[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].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.
|
- `[environment].known` : variables d'environnement connues du projet.
|
||||||
- `[secret_store].backend_policy` : politique de secret store (`auto`, `kwallet-only`, `keyring-any`, `env-only`).
|
- `[secret_store].backend_policy` : politique de secret store (`auto`, `kwallet-only`, `keyring-any`, `env-only`).
|
||||||
- `[profiles].default` : profil recommandé par défaut.
|
- `[profiles].default` : profil recommandé par défaut.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -16,6 +17,43 @@ import (
|
||||||
|
|
||||||
type stringListFlag []string
|
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 {
|
func (f *stringListFlag) String() string {
|
||||||
return strings.Join(*f, ",")
|
return strings.Join(*f, ",")
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +64,13 @@ func (f *stringListFlag) Set(value string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBuild(args []string, stdout, stderr io.Writer) 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) {
|
if shouldShowHelp(args) {
|
||||||
printBuildHelp(stdout)
|
printBuildHelp(stdout)
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -34,15 +79,8 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
||||||
fs := flag.NewFlagSet("build", flag.ContinueOnError)
|
fs := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
defaultGOOS := strings.TrimSpace(os.Getenv("GOOS"))
|
defaultGOOS := resolveDefaultGOOS()
|
||||||
if defaultGOOS == "" {
|
defaultGOARCH := resolveDefaultGOARCH()
|
||||||
defaultGOOS = runtime.GOOS
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultGOARCH := strings.TrimSpace(os.Getenv("GOARCH"))
|
|
||||||
if defaultGOARCH == "" {
|
|
||||||
defaultGOARCH = runtime.GOARCH
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifestDir string
|
var manifestDir string
|
||||||
var binaryName string
|
var binaryName string
|
||||||
|
|
@ -50,6 +88,7 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
||||||
var buildDir string
|
var buildDir string
|
||||||
var goos string
|
var goos string
|
||||||
var goarch string
|
var goarch string
|
||||||
|
var target string
|
||||||
var version string
|
var version string
|
||||||
var versionVar string
|
var versionVar string
|
||||||
var gocache 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(&buildDir, "build-dir", "", "Répertoire de sortie (override [build].output_dir)")
|
||||||
fs.StringVar(&goos, "goos", defaultGOOS, "GOOS cible")
|
fs.StringVar(&goos, "goos", defaultGOOS, "GOOS cible")
|
||||||
fs.StringVar(&goarch, "goarch", defaultGOARCH, "GOARCH 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(&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(&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")
|
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(), ", "))
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
projectDir := filepath.Dir(manifestPath)
|
if strings.TrimSpace(target) != "" {
|
||||||
|
parsedTarget, err := parseBuildTarget(target)
|
||||||
binaryName = firstNonEmpty(binaryName, file.BinaryName)
|
if err != nil {
|
||||||
if binaryName == "" {
|
return err
|
||||||
return errors.New("binary name is required (set binary_name in mcp.toml or --binary)")
|
}
|
||||||
|
goos = parsedTarget.GOOS
|
||||||
|
goarch = parsedTarget.GOARCH
|
||||||
}
|
}
|
||||||
|
|
||||||
mainPackage = firstNonEmpty(mainPackage, file.Build.MainPackage)
|
goos = firstNonEmpty(goos, resolveDefaultGOOS())
|
||||||
if mainPackage == "" {
|
goarch = firstNonEmpty(goarch, resolveDefaultGOARCH())
|
||||||
mainPackage = fmt.Sprintf("./cmd/%s", binaryName)
|
version = resolveBuildVersion(version, cfg.ProjectDir)
|
||||||
}
|
versionVar = cfg.VersionVar
|
||||||
|
|
||||||
buildDir = firstNonEmpty(buildDir, file.Build.OutputDir, "build")
|
outputPath, err := buildOutputPath(cfg.ProjectDir, cfg.BuildDir, cfg.BinaryName, goos, goarch)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -120,10 +161,10 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
||||||
if len(ldflags) > 0 {
|
if len(ldflags) > 0 {
|
||||||
cmdArgs = append(cmdArgs, "-ldflags", strings.Join(ldflags, " "))
|
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 := exec.Command("go", cmdArgs...)
|
||||||
cmd.Dir = projectDir
|
cmd.Dir = cfg.ProjectDir
|
||||||
cmd.Stdout = stdout
|
cmd.Stdout = stdout
|
||||||
cmd.Stderr = stderr
|
cmd.Stderr = stderr
|
||||||
cmd.Env = withEnvOverrides(os.Environ(), map[string]string{
|
cmd.Env = withEnvOverrides(os.Environ(), map[string]string{
|
||||||
|
|
@ -143,10 +184,210 @@ func runBuild(args []string, stdout, stderr io.Writer) error {
|
||||||
return nil
|
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) {
|
func printBuildHelp(w io.Writer) {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
w,
|
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,
|
toolName,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -232,3 +473,11 @@ func firstNonEmpty(values ...string) string {
|
||||||
}
|
}
|
||||||
return ""
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"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) {
|
func TestBuildHelp(t *testing.T) {
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
@ -125,6 +228,9 @@ func TestBuildHelp(t *testing.T) {
|
||||||
if !strings.Contains(output, "version_var") {
|
if !strings.Contains(output, "version_var") {
|
||||||
t.Fatalf("build help should mention manifest build config: %q", output)
|
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) {
|
func writeBuildFixture(t *testing.T, projectDir, binaryName, goModContent, manifestContent, mainContent string) {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ type Build struct {
|
||||||
MainPackage string `toml:"main_package"`
|
MainPackage string `toml:"main_package"`
|
||||||
OutputDir string `toml:"output_dir"`
|
OutputDir string `toml:"output_dir"`
|
||||||
VersionVar string `toml:"version_var"`
|
VersionVar string `toml:"version_var"`
|
||||||
|
Targets []BuildTarget `toml:"targets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BuildTarget struct {
|
||||||
|
OS string `toml:"os"`
|
||||||
|
Arch string `toml:"arch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Environment struct {
|
type Environment struct {
|
||||||
|
|
@ -172,6 +178,7 @@ func (b *Build) normalize() {
|
||||||
b.MainPackage = strings.TrimSpace(b.MainPackage)
|
b.MainPackage = strings.TrimSpace(b.MainPackage)
|
||||||
b.OutputDir = strings.TrimSpace(b.OutputDir)
|
b.OutputDir = strings.TrimSpace(b.OutputDir)
|
||||||
b.VersionVar = strings.TrimSpace(b.VersionVar)
|
b.VersionVar = strings.TrimSpace(b.VersionVar)
|
||||||
|
b.Targets = normalizeBuildTargets(b.Targets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Environment) normalize() {
|
func (e *Environment) normalize() {
|
||||||
|
|
@ -243,3 +250,30 @@ func normalizeStringList(values []string) []string {
|
||||||
}
|
}
|
||||||
return normalized
|
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 "
|
main_package = " ./cmd/my-mcp "
|
||||||
output_dir = " build "
|
output_dir = " build "
|
||||||
version_var = " main.version "
|
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]
|
[environment]
|
||||||
known = [" MCP_PROFILE ", "", "MCP_TOKEN"]
|
known = [" MCP_PROFILE ", "", "MCP_TOKEN"]
|
||||||
|
|
@ -197,6 +209,12 @@ description = " Client MCP interne "
|
||||||
if file.Build.VersionVar != "main.version" {
|
if file.Build.VersionVar != "main.version" {
|
||||||
t.Fatalf("build version var = %q", file.Build.VersionVar)
|
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" {
|
if file.SecretStore.BackendPolicy != "auto" {
|
||||||
t.Fatalf("secret store policy = %q", file.SecretStore.BackendPolicy)
|
t.Fatalf("secret store policy = %q", file.SecretStore.BackendPolicy)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -665,6 +665,9 @@ token_env_names = ["{{.ReleaseTokenEnv}}"]
|
||||||
main_package = "./cmd/{{.BinaryName}}"
|
main_package = "./cmd/{{.BinaryName}}"
|
||||||
output_dir = "build"
|
output_dir = "build"
|
||||||
version_var = "main.version"
|
version_var = "main.version"
|
||||||
|
[[build.targets]]
|
||||||
|
os = "linux"
|
||||||
|
arch = "amd64"
|
||||||
|
|
||||||
[environment]
|
[environment]
|
||||||
known = [{{- range $index, $value := .KnownEnvironmentVariables}}{{if $index}}, {{end}}"{{$value}}"{{- end}}]
|
known = [{{- range $index, $value := .KnownEnvironmentVariables}}{{if $index}}, {{end}}"{{$value}}"{{- end}}]
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,9 @@ func TestGenerateCreatesRecommendedSkeleton(t *testing.T) {
|
||||||
"[environment]",
|
"[environment]",
|
||||||
"[profiles]",
|
"[profiles]",
|
||||||
"version_var = \"main.version\"",
|
"version_var = \"main.version\"",
|
||||||
|
"[[build.targets]]",
|
||||||
|
"os = \"linux\"",
|
||||||
|
"arch = \"amd64\"",
|
||||||
"backend_policy = \"auto\"",
|
"backend_policy = \"auto\"",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(string(manifestContent), snippet) {
|
if !strings.Contains(string(manifestContent), snippet) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue