feat(19.3-03): summer.yaml build.tags and the summercms_s3 tag that links the S3 uploads driver

- Manifest.Tags from build.tags (^[a-z0-9_]+$, duplicates dropped), round-tripped by SaveManifest
- summer build passes -tags=<list> through goBuildArgs and prints the build tags line
- attach: s3blob imported only by s3_on.go (//go:build summercms_s3); an untagged binary
  refuses an s3:// bucket_url naming the tag, build.tags and -tags
- go mod tidy adds the AWS SDK v2 modules as indirect requirements
- storage, modules, scaffolding, configuration and scaling docs plus the lagoon README
This commit is contained in:
Jakub Zych
2026-10-15 18:07:05 +02:00
parent b1fab9930b
commit 2382040b25
15 changed files with 346 additions and 18 deletions

View File

@@ -46,7 +46,10 @@ func App(ctx context.Context, appDir string, out io.Writer) error {
return fmt.Errorf("build: create bin dir: %w", err)
}
cmd := exec.CommandContext(ctx, "go", "build", "-o", binRel, ".")
if len(m.Tags) > 0 {
fmt.Fprintf(out, "build tags: %s\n", strings.Join(m.Tags, ","))
}
cmd := exec.CommandContext(ctx, "go", goBuildArgs(binRel, m.Tags)...)
cmd.Dir = appDir
cmd.Stdout = out
cmd.Stderr = out
@@ -59,6 +62,17 @@ func App(ctx context.Context, appDir string, out io.Writer) error {
return nil
}
// goBuildArgs returns the go command arguments for summer build: build, the
// -tags flag when the manifest lists build tags, then -o binRel and the
// application package. The tags travel as one exec argument, never a shell.
func goBuildArgs(binRel string, tags []string) []string {
args := []string{"build"}
if len(tags) > 0 {
args = append(args, "-tags="+strings.Join(tags, ","))
}
return append(args, "-o", binRel, ".")
}
func generate(appDir string, m Manifest) error {
mainSrc, err := generateMain(m)
if err != nil {

View File

@@ -13,13 +13,18 @@ import (
const manifestName = "summer.yaml"
var pluginIDRe = regexp.MustCompile(`^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$`)
var (
pluginIDRe = regexp.MustCompile(`^[a-z][a-z0-9]*\.[a-z][a-z0-9]*$`)
buildTagRe = regexp.MustCompile(`^[a-z0-9_]+$`)
)
// Manifest is the ordered app plugin list used to generate imports.
// Manifest is the ordered app plugin list used to generate imports, plus
// the Go build tags summer build passes to go build (summer.yaml build.tags).
type Manifest struct {
Module string
Binary string
Plugins []Plugin
Tags []string
}
// Plugin is one compiled plugin module in manifest order.
@@ -76,10 +81,67 @@ func LoadManifest(path string) (Manifest, error) {
seenMod[p.Module] = struct{}{}
m.Plugins = append(m.Plugins, p)
}
tags, err := loadBuildTags(tree["build"])
if err != nil {
return Manifest{}, err
}
m.Tags = tags
return m, nil
}
// SaveManifest writes summer.yaml with plugins in slice order.
// loadBuildTags reads the optional build mapping and its tags list. Entries
// are trimmed and validated; duplicates are dropped keeping the first.
func loadBuildTags(raw any) ([]string, error) {
if raw == nil {
return nil, nil
}
section := asMap(raw)
if section == nil {
return nil, fmt.Errorf("build: build must be a mapping (build.tags)")
}
rawTags, ok := section["tags"]
if !ok || rawTags == nil {
return nil, nil
}
list, ok := rawTags.([]any)
if !ok {
return nil, fmt.Errorf("build: build.tags must be a list of tags")
}
var tags []string
seen := make(map[string]struct{}, len(list))
for i, item := range list {
s, ok := item.(string)
if !ok {
return nil, fmt.Errorf("build: build.tags[%d] must be a string", i)
}
tag := strings.TrimSpace(s)
if err := ValidateBuildTag(tag); err != nil {
return nil, fmt.Errorf("build: build.tags[%d]: %w", i, err)
}
if _, dup := seen[tag]; dup {
continue
}
seen[tag] = struct{}{}
tags = append(tags, tag)
}
return tags, nil
}
// ValidateBuildTag requires a Go build tag of lowercase letters, digits and
// underscores, so a summer.yaml value can never smuggle a flag into go build.
func ValidateBuildTag(tag string) error {
if tag == "" {
return fmt.Errorf("build tag is empty")
}
if !buildTagRe.MatchString(tag) {
return fmt.Errorf("build tag %q must match ^[a-z0-9_]+$", tag)
}
return nil
}
// SaveManifest writes summer.yaml with plugins in slice order and, when
// present, the build.tags list.
func SaveManifest(path string, m Manifest) error {
if err := validateModulePath(m.Module); err != nil {
return fmt.Errorf("build: module: %w", err)
@@ -101,6 +163,16 @@ func SaveManifest(path string, m Manifest) error {
fmt.Fprintf(&b, " - id: %s\n", p.ID)
fmt.Fprintf(&b, " module: %s\n", p.Module)
}
if len(m.Tags) > 0 {
b.WriteString("build:\n")
b.WriteString(" tags:\n")
for i, tag := range m.Tags {
if err := ValidateBuildTag(tag); err != nil {
return fmt.Errorf("build: build.tags[%d]: %w", i, err)
}
fmt.Fprintf(&b, " - %s\n", tag)
}
}
if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
return fmt.Errorf("build: write %s: %w", path, err)
}

View File

@@ -0,0 +1,70 @@
package build
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
func TestManifestBuildTagsSmoke(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, manifestName)
body := "module: git.golem15.com/acme/acme\n" +
"binary: acme\n" +
"plugins:\n" +
" - id: acme.blog\n" +
" module: git.golem15.com/acme/acme/plugins/blog\n" +
"build:\n" +
" tags:\n" +
" - summercms_s3\n" +
" - \" summercms_s3 \"\n"
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
m, err := LoadManifest(path)
if err != nil {
t.Fatal(err)
}
if !slices.Equal(m.Tags, []string{"summercms_s3"}) {
t.Fatalf("Tags = %q, want [summercms_s3]", m.Tags)
}
if err := SaveManifest(path, m); err != nil {
t.Fatal(err)
}
again, err := LoadManifest(path)
if err != nil {
t.Fatal(err)
}
if !slices.Equal(again.Tags, m.Tags) || len(again.Plugins) != 1 {
t.Fatalf("round trip = %+v, want tags %q and one plugin", again, m.Tags)
}
args := goBuildArgs("bin/acme", again.Tags)
tagAt := slices.Index(args, "-tags=summercms_s3")
outAt := slices.Index(args, "-o")
if tagAt < 0 || outAt < 0 || tagAt > outAt {
t.Fatalf("goBuildArgs = %q, want -tags=summercms_s3 before -o", args)
}
if plain := goBuildArgs("bin/acme", nil); !slices.Equal(plain, []string{"build", "-o", "bin/acme", "."}) {
t.Fatalf("goBuildArgs without tags = %q", plain)
}
for _, bad := range []string{"summercms-s3", "summercms s3"} {
if err := ValidateBuildTag(bad); err == nil {
t.Fatalf("ValidateBuildTag(%q) accepted", bad)
}
raw := "module: git.golem15.com/acme/acme\nbinary: acme\nbuild:\n tags:\n - \"" + bad + "\"\n"
if err := os.WriteFile(path, []byte(raw), 0o644); err != nil {
t.Fatal(err)
}
if _, err := LoadManifest(path); err == nil || !strings.Contains(err.Error(), "build.tags") {
t.Fatalf("LoadManifest with tag %q: err = %v, want a build.tags error", bad, err)
}
}
if err := SaveManifest(path, Manifest{Module: "git.golem15.com/acme/acme", Binary: "acme", Tags: []string{"a b"}}); err == nil {
t.Fatal("SaveManifest accepted an invalid tag")
}
}