Multi-package Debian/RPM: schema + scaffolders + user-template rendering #56

Merged
heiko merged 11 commits from worktree-multi-deb into dev 2026-05-17 20:07:07 +02:00
Owner

Summary

Adds full multi-package Debian/RPM support to gogogo. Three layers land in this PR:

  1. YAML schema layer — polymorphic artifacts.packages: field (absent / '*' / list) with per-package merge and validation.
  2. Generator + rendering layer — source-package builders emit multi-stanza debian/control and multi-%package -n <name>.spec files. Both honor user-edited templates in debian/* and rpm/<name>.spec via text/template rendering against a stable DebContext/RPMContext.
  3. CLI scaffolding — two new subcommands:
    • gogogo config packages — scaffold the artifacts.packages: YAML block from cmd/* discovery (replaces the misnamed first version of pack populate).
    • gogogo pack populate — scaffold editable starter files into debian/ and rpm/ for users who want to customise the source-package shape beyond what the YAML schema covers.

Behavior change

Existing multi-binary projects without an artifacts.packages: declaration now produce one combined .deb named after the module, instead of one .deb per cmd/* binary. Set packages: '*' (or use gogogo config packages --shorthand --in-place) to preserve the prior per-binary split.

CLI surface

gogogo config packages                       # print packages: block to stdout
gogogo config packages --shorthand           # one-liner: `packages: '*'`
gogogo config packages --in-place --force    # splice into .gogogo.conf

gogogo pack populate                         # scaffold editable debian/* + rpm/* stubs
gogogo pack populate --force                 # overwrite existing stubs

gogogo --version                             # `<cmd> <ver> <exe-path>` (new format)

User-template contract

User-edited debian/control, debian/rules, debian/changelog, debian/source/format, and rpm/<name>.spec files are rendered through text/template at build time against:

  • DebContextSource, Maintainer, Tag, DebVersion, Packages[], Synopsis, LongDescIndented, ChangelogDate, etc.
  • RPMContextName, RPMVersion, RPMRelease, TarName, Packages[], Group, Changelog, etc.

Field names form a public stability contract. missingkey=error makes typos like {{ .Versoin }} fail loud with the offending file path in the error.

Test plan

  • go test ./... — all packages pass
  • go vet ./... clean
  • golangci-lint run — no new issues in any added file (intentional misspellings + fmt.Fprintf progress lines marked with //nolint)
  • Manual: gogogo --version, gogogo config packages --help, gogogo pack populate end-to-end on a fixture project
  • Source .dsc rebuild produces the same multi-package set as direct .deb build (multi-stanza debian/control + per-package go build)
  • rpmbuild -bs on the generated <name>.spec works for both single-package (no -n) and multi-package (%package -n per entry) projects

Commits

1058573 suppress intentional lint warnings: nolint markers for typo+errcheck
9276d5f source/rpm: multi-package %package -n + user-file render path
44d992b pack populate: scaffold editable debian/* and rpm/ stub files
a4ec9e1 source: render user-edited debian/* files through text/template
12d2083 pack populate → config packages (rename only)
4bce628 config/migrate: split artifacts() into per-block render helpers
b55048b cli: --version prints '<cmd> <ver> <exe-path>'
367aa26 config: keep explanatory comments in migrate output, drop 'Example config' preamble
9647a74 config migrate: import existing v2 artifacts.* settings
0ac9515 Expand multi-package test coverage
9063add Add multi-package support for Debian and RPM artifacts

Known limitations (CHANGELOG-worthy)

  1. Behavior change: existing multi-binary projects start producing one combined .deb by default. Use packages: '*' to preserve the prior per-binary split.
  2. Union merge semantics on per-package list overrides mean an entry can't remove a globally-declared dep. If needed in practice, a depends_remove: escape hatch can land separately.
  3. Template stability: once shipped, DebContext/RPMContext field names are committed — renames are breaking changes for users with custom debian/* or rpm/<name>.spec templates.

ius:ai:claude-opus-4-7

## Summary Adds full multi-package Debian/RPM support to gogogo. Three layers land in this PR: 1. **YAML schema layer** — polymorphic `artifacts.packages:` field (absent / `'*'` / list) with per-package merge and validation. 2. **Generator + rendering layer** — source-package builders emit multi-stanza `debian/control` and multi-`%package -n` `<name>.spec` files. Both honor user-edited templates in `debian/*` and `rpm/<name>.spec` via `text/template` rendering against a stable `DebContext`/`RPMContext`. 3. **CLI scaffolding** — two new subcommands: - `gogogo config packages` — scaffold the `artifacts.packages:` YAML block from `cmd/*` discovery (replaces the misnamed first version of `pack populate`). - `gogogo pack populate` — scaffold editable starter files into `debian/` and `rpm/` for users who want to customise the source-package shape beyond what the YAML schema covers. ## Behavior change Existing multi-binary projects without an `artifacts.packages:` declaration now produce **one combined `.deb`** named after the module, instead of one `.deb` per `cmd/*` binary. Set `packages: '*'` (or use `gogogo config packages --shorthand --in-place`) to preserve the prior per-binary split. ## CLI surface ```bash gogogo config packages # print packages: block to stdout gogogo config packages --shorthand # one-liner: `packages: '*'` gogogo config packages --in-place --force # splice into .gogogo.conf gogogo pack populate # scaffold editable debian/* + rpm/* stubs gogogo pack populate --force # overwrite existing stubs gogogo --version # `<cmd> <ver> <exe-path>` (new format) ``` ## User-template contract User-edited `debian/control`, `debian/rules`, `debian/changelog`, `debian/source/format`, and `rpm/<name>.spec` files are rendered through `text/template` at build time against: - `DebContext` — `Source`, `Maintainer`, `Tag`, `DebVersion`, `Packages[]`, `Synopsis`, `LongDescIndented`, `ChangelogDate`, etc. - `RPMContext` — `Name`, `RPMVersion`, `RPMRelease`, `TarName`, `Packages[]`, `Group`, `Changelog`, etc. Field names form a public stability contract. `missingkey=error` makes typos like `{{ .Versoin }}` fail loud with the offending file path in the error. ## Test plan - [x] `go test ./...` — all packages pass - [x] `go vet ./...` clean - [x] `golangci-lint run` — no new issues in any added file (intentional misspellings + `fmt.Fprintf` progress lines marked with `//nolint`) - [x] Manual: `gogogo --version`, `gogogo config packages --help`, `gogogo pack populate` end-to-end on a fixture project - [x] Source `.dsc` rebuild produces the same multi-package set as direct `.deb` build (multi-stanza `debian/control` + per-package `go build`) - [x] `rpmbuild -bs` on the generated `<name>.spec` works for both single-package (no `-n`) and multi-package (`%package -n` per entry) projects ## Commits ``` 1058573 suppress intentional lint warnings: nolint markers for typo+errcheck 9276d5f source/rpm: multi-package %package -n + user-file render path 44d992b pack populate: scaffold editable debian/* and rpm/ stub files a4ec9e1 source: render user-edited debian/* files through text/template 12d2083 pack populate → config packages (rename only) 4bce628 config/migrate: split artifacts() into per-block render helpers b55048b cli: --version prints '<cmd> <ver> <exe-path>' 367aa26 config: keep explanatory comments in migrate output, drop 'Example config' preamble 9647a74 config migrate: import existing v2 artifacts.* settings 0ac9515 Expand multi-package test coverage 9063add Add multi-package support for Debian and RPM artifacts ``` ## Known limitations (CHANGELOG-worthy) 1. **Behavior change**: existing multi-binary projects start producing one combined `.deb` by default. Use `packages: '*'` to preserve the prior per-binary split. 2. **Union merge semantics** on per-package list overrides mean an entry can't *remove* a globally-declared dep. If needed in practice, a `depends_remove:` escape hatch can land separately. 3. **Template stability**: once shipped, `DebContext`/`RPMContext` field names are committed — renames are breaking changes for users with custom `debian/*` or `rpm/<name>.spec` templates. ius:ai:claude-opus-4-7
Introduce artifacts.packages: as a polymorphic YAML field with three
modes: absent (new default — all binaries combined into one package
named after the module), '*' (one package per cmd/* binary — the prior
implicit behavior, now explicit), and an explicit list of {name,
binaries, deb, rpm, files} entries with per-package overrides under
union-merge semantics for list fields.

This is a deliberate behavior change for existing multi-binary projects:
without a config edit, two cmd/* binaries that previously produced two
identically-configured .debs now produce one combined .deb. Set
packages: '*' to preserve the prior split, or use an explicit list for
differentiation (per-package depends, scripts, files, etc.).

Source-package generation (.dsc) follows the same mode and produces a
matching multi-stanza debian/control with one go build per entry when
applicable, so .dsc rebuilds match the direct .deb build output.

Validation enforces unique names, Debian package-name shape, binary
existence in build.commands or as the module basename, no double-claim
across packages, and well-formed file entries — but only in Explicit
mode; Combined and PerBinary need no per-entry rules.

Add gogogo pack populate subcommand to scaffold the packages: block
from cmd/* discovery: stdout by default (safe path), with --shorthand
for the packages: '*' one-liner, --in-place to splice into .gogogo.conf
(refuses if the key is already present), and --force to overwrite an
existing block. auto.detectCommands is exported as auto.DetectCommands
so populate can reuse the same discovery logic.

ius:ai:claude-opus-4-7
Add tests targeting the behavior gaps left by the initial multi-package
landing: integration, edge cases, and isolation invariants.

config/packages_test.go — nil *bool doesn't override non-nil global,
empty list doesn't erase global, per-package scripts win only when set,
round-trip stability for Explicit and PerBinary modes via marshal +
re-parse.

cmd/release/packages_test.go — end-to-end YAML through config.New →
validation → resolvePackages, asserting per-package depends merge
union'd with global depends; invalid configs (duplicate names, unknown
binaries) caught by Validate before they reach the resolver;
per-package depends actually reach deb.Spec.Depends; explicit script
paths override systemd auto-generation; multi-arch × multi-package
produces N×M correctly-named .debs; sourcePackageEntries falls back
to ./cmd/<bin> for binaries not in cf.Build.Commands.

pack/source/deb_test.go — multi-stanza debian/control isolates depends
per stanza (no cross-leakage between Package: blocks); multi-package
debian/rules builds each package's binaries only into its own install
root.

cmd/pack/populate_test.go — in-place fails cleanly when no artifacts:
block exists; force-overwrite preserves surrounding content (top-level
comments, publish: block, user-set depends); realistic cert-proxy-style
config splices correctly with nested structure intact.

ius:ai:claude-opus-4-7
Previously the migrator only read legacy v1 Formats.* and Packages
fields, so running `gogogo config migrate` against a current v2
.gogogo.conf silently dropped user-set artifacts.deb.depends,
artifacts.rpm.group, artifacts.common.maintainer, and similar v2
fields — they were re-rendered as commented defaults.

Introduce migrator.effective() that returns an Artifacts struct
combining v1 Formats.* (the original migration source) with v2
Artifacts.* overlaid where set. Precedence is "v2 wins where set"
because these are explicit user values, not per-package overrides
that need union semantics.

artifacts() now reads from this effective struct, so user-set v2
fields round-trip through Migrated() while legacy v1 configs still
migrate correctly. Includes the v1→v2 derivations:
  - Packages []string ("deb", "rpm") → per-format Enabled bools
  - Formats.Source → Deb.Source / RPM.Source / SourceVendor
  - Formats.Deb.* / Formats.RPM.* / Formats.Common.* → Artifacts.*

Per-format source toggles now emit independently — deb.source and
rpm.source are no longer linked to a single v1 Source switch when
v2 sets them separately.

ius:ai:claude-opus-4-7
The migrator previously emitted a bare list of keys with no context —
useful as a parser but unhelpful as a re-rendered config users might
read or edit. Each field rendering now precedes its key with the same
explanatory comment present in default.yml, matching the example
config's structure for blobs, deb, rpm, common, repo, security, env,
release, build, and publish blocks.

Also remove the literal "Example config for gogogo" line from both
default.yml and the migrator's preamble — the rendered output is the
user's actual config (with user values inlined), not an "example".
Keep the schema-version header and the "defaults are commented out"
guidance, which are still accurate framing.

default.yml gains a commented packages: section documenting the
three-mode multi-package field added by the parent commit, so the
example config covers the feature.

ius:ai:claude-opus-4-7
Replace the module-path-based version line ("go.schlittermann.de/heiko/gogogo v1.0.0")
with a more human-readable form: the command's basename, the version,
and the full path to the executable on disk.

The basename + path lets users disambiguate when multiple gogogo
binaries are installed (e.g. one from `go install`, one from a .deb
package, one from a worktree build) — `which gogogo` answers the
"which one" question, --version now does it inline.

Falls back to os.Args[0] when os.Executable() fails, so the version
line stays useful even in pathological environments.

ius:ai:claude-opus-4-7
The artifacts() function had grown to ~85 lines mixing blobs, deb, rpm,
common, and packages rendering. Split into renderBlobsBlock,
renderDebBlock, renderRPMBlock, and renderCommonBlock, each ~15-25
lines. The dispatcher artifacts() is now a 9-line outline that names
the rendering order.

Output is unchanged (existing tests pin the comment + value text
verbatim); this is purely a structural simplification.

ius:ai:claude-opus-4-7
The original "pack populate" was scaffolding a YAML `packages:` block for
.gogogo.conf — not the debian/* file tree the original design called
for. Rename frees up `pack populate` for the real file-tree scaffolder
(landing in a follow-up commit). The YAML scaffolder moves to
`gogogo config packages`, sibling to `config migrate` / `config default`.

Mechanical changes only:
- internal/cmd/pack/populate.go → internal/cmd/config/packages.go
  (package pack → package configcmd to avoid clash with internal/config)
- pack.Populate → configcmd.PackagesBlock; same for Options type
- TestPopulate_* → TestPackagesBlock_*
- Error prefix "populate:" → "config packages:"
- Cobra wiring: drop newPackPopulateCmd from internal/cli/release.go,
  add newConfigPackagesCmd to internal/cli/config.go

No behavior change. Existing tests pass with renamed functions; new CLI
surface verified manually.

ius:ai:claude-opus-4-7
writeDebianDir now honors user-edited debian/source/format, control,
rules, copyright, and changelog files when they're present in the
source tree (already copied into pkgDir by copyTree). The contents are
rendered through text/template against a new DebContext exposing
Source, Maintainer, Tag, DebVersion, Packages, etc.

When the file is absent the existing generator runs as before — so
projects without any debian/ directory in their source produce
byte-identical output (existing tests verify this).

Template parsing uses missingkey=error so user typos like
{{ .Versoin }} fail loud with a clear file-locating error. The
DebContext field names form a public stability contract — once
shipped, renaming them breaks user templates.

This is groundwork for the new `pack populate` subcommand (next
commit) which scaffolds editable stub files containing these
placeholders.

ius:ai:claude-opus-4-7
`gogogo pack populate` now does what the original design intended: it
writes editable starter files into the project's debian/ and rpm/
directories. Each file is a text/template that the build renders against
a DebContext / RPMContext at package-creation time.

Scaffolded files:
- debian/source/format (literal "3.0 (native)")
- debian/control       (Source/Maintainer/{{ range .Packages }})
- debian/rules         (per-package go build per .Packages entry, 0755)
- debian/changelog     (Source/DebVersion/Tag/ChangelogDate)
- rpm/<module>.spec    (multi-stanza-ready; full multi-package %package -n
                       support lands in a follow-up commit)

Existing files are skipped silently with a "skip <name> (exists; --force
overwrites)" notice; --force overwrites them. The skip behavior keeps
re-running safe — users can pull in new stubs after a gogogo upgrade
without losing local edits to existing files.

A new exported source.RenderDebTemplate provides the entry point that
tests (and pack populate's own end-to-end render test) use to verify
stub templates flow through the renderer cleanly without leftover
{{ }} markers. Combined with the missingkey=error setting from the
prior commit, typos like {{ .Versoin }} fail loudly at build time.

ius:ai:claude-opus-4-7
Rewrite the RPM source-package generator to support multi-package
projects with %package -n stanzas, one per binary package, plus
matching %files -n blocks listing each package's binaries.

The single-package case (where a binary package's name equals the
source name) emits the implicit form (%files, no -n) to avoid the
"package <name> already exists" error rpmbuild produces when you try
to declare %package -n with the same name as the source.

writeRPMSpec now also honors a user-edited rpm/<name>.spec file in
the source tree: if present, it's used as the template source instead
of the built-in builtinSpecTemplate. Both paths share the new
RPMContext via newRPMContext() — a stability contract whose field
names (Name, RPMVersion, Tag, Commit, Packages, etc.) are committed
to be stable across releases.

The new exported source.RenderRPMTemplate parallels RenderDebTemplate
and is the entry point used by pack populate's stub-render test.

The rpm/<module>.spec stub emitted by pack populate is updated to
mirror the new template shape so users can edit it freely and have
their changes flow through cleanly.

ius:ai:claude-opus-4-7
- internal/pack/source/template.go: //nolint:misspell on a comment that
  uses {{ .Versoin }} as a literal example of the typo the
  missingkey=error setting catches.
- internal/pack/source/deb_test.go: //nolint:misspell on the same typo
  used as a test fixture for that error path.
- internal/cmd/pack/populate.go: //nolint:errcheck on two fmt.Fprintf
  progress lines. These are user-facing progress notes; if the output
  writer fails, there's no recovery and no useful place to log.

ius:ai:claude-opus-4-7
heiko changed title from Add multi-package support for Debian and RPM artifacts to Multi-package Debian/RPM: schema + scaffolders + user-template rendering 2026-05-17 10:36:06 +02:00
heiko changed title from Multi-package Debian/RPM: schema + scaffolders + user-template rendering to WIP: Multi-package Debian/RPM: schema + scaffolders + user-template rendering 2026-05-17 10:43:02 +02:00
Reconcile two parallel debian-side designs:
- Master's ada172c overlay: copies user-edited debian/* metadata
  files (scripts, conffiles) and extracts extra Depends/Recommends/
  Suggests/Conflicts/Replaces from a VCS debian/control into the
  generated binary .deb spec.
- This branch's user-template-render: rewrites user-edited debian/
  control/rules/changelog/copyright/source/format through text/template
  against a stable DebContext at build time.

The two paths now compose: writeOrRenderDeb handles the gogogo-managed
generated set; overlayDebianDir copies in everything else (scripts,
install files, etc.) afterwards. newDebSpec's per-package merged
Depends list is then unioned with the values extracted from
debian/control via parseDebianControlFields, so users get all three
sources (YAML config, per-package override, debian/control) folded
together.

Also adopts master's renames that swept the codebase since branch:
- Artifacts.Blobs → Artifacts.Blob (singular, parallel to Deb/RPM)
- Publish.Blobs → Publish.Blob
- Built-in destinations: repo_release → releases, repo_packages → packages
- Deb.SystemdEnabled() → Deb.IsSystemdEnabled()
- Source.SourceEnabled() → Source.IsEnabled()

And master's mtime-zero fallback for the mod-cache build path (uses
time.Now() rather than Unix epoch to keep lintian happy).

ius:ai:claude-opus-4-7
heiko changed target branch from master to dev 2026-05-17 14:11:04 +02:00
heiko force-pushed worktree-multi-deb from 32e68a5b36 to d7d1d11ba6 2026-05-17 14:11:05 +02:00 Compare
heiko changed title from WIP: Multi-package Debian/RPM: schema + scaffolders + user-template rendering to Multi-package Debian/RPM: schema + scaffolders + user-template rendering 2026-05-17 20:06:45 +02:00
heiko deleted branch worktree-multi-deb 2026-05-17 20:09:09 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
heiko/gogogo!56
No description provided.