feat: native .deb/.rpm packaging + config migrate subcommand #41

Open
heiko wants to merge 19 commits from package-deb into master
Owner

Summary

  • Native packaging (Phases 1–6): pure-Go .deb and .rpm binary builders wired into the release flow; source-package generators (dpkg-source/rpmbuild when available); Debian changelog and RPM %changelog formatters; arch/version conversion helpers
  • config migrate (issue #40): new subcommand that appends missing top-level sections and missing leaf attributes within existing sections as commented examples — append-only, idempotent
  • Docs (issues #39, Phase 7): document CLI flags, env vars, and packaging feature in README
  • Refactor/simplify: consolidate version-conversion helpers into packager, deduplicate metadata resolution with pkgMeta, add derefBool helper in config

Test plan

  • go build ./... — clean
  • go test ./... — all pass
  • golangci-lint run ./internal/cmd/migrate/... ./internal/packager/... — 0 issues
  • gogogo config migrate on a partial config appends missing attributes as comments
  • Re-running config migrate is a no-op (idempotency)
  • gogogo release with packages: [deb] in config produces .deb asset

🤖 Generated with Claude Code

## Summary - **Native packaging** (Phases 1–6): pure-Go `.deb` and `.rpm` binary builders wired into the release flow; source-package generators (`dpkg-source`/`rpmbuild` when available); Debian changelog and RPM `%changelog` formatters; arch/version conversion helpers - **`config migrate`** (issue #40): new subcommand that appends missing top-level sections *and* missing leaf attributes within existing sections as commented examples — append-only, idempotent - **Docs** (issues #39, Phase 7): document CLI flags, env vars, and packaging feature in README - **Refactor/simplify**: consolidate version-conversion helpers into `packager`, deduplicate metadata resolution with `pkgMeta`, add `derefBool` helper in config ## Test plan - [x] `go build ./...` — clean - [x] `go test ./...` — all pass - [ ] `golangci-lint run ./internal/cmd/migrate/... ./internal/packager/...` — 0 issues - [ ] `gogogo config migrate` on a partial config appends missing attributes as comments - [ ] Re-running `config migrate` is a no-op (idempotency) - [ ] `gogogo release` with `packages: [deb]` in config produces `.deb` asset 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reorganized around quick-start at the top, mermaid diagrams for the
build pipeline, redirect-resolution flow, and retention policy. Added
GitHub-style alerts, collapsible command details, and a Roadmap
section flagging upcoming Debian/RPM packaging support.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends Config with Packages []string and a Formats struct (Common,
Deb, RPM, Source) so users can opt into native packaging via:

  packages:
    - deb
    - rpm

New internal/packager package adds DebArch / RPMArch (GOARCH ->
distro arch, linux only) and DebVersion / RPMVersion (semver tag ->
distro version). Pre-releases sort correctly: 1.0.0~rc.1 in Debian,
Release 0.1.rc.1 in RPM per Fedora convention. Build metadata is
preserved for Debian and dropped for RPM (semver explicitly excludes
build metadata from precedence).

No behavior change yet; consumers land in later phases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two new sub-packages, both pure stdlib — no nfpm, no go-git, no new
external runtime deps for binary .deb generation.

internal/packager/ar/ — writer for the ar(1) variant Debian uses.
API mirrors archive/tar (WriteHeader / Write / Close), supports the
short-name layout that .deb requires (debian-binary, control.tar.gz,
data.tar.gz). 60-byte header with the standard layout: name, mtime,
uid, gid, mode, size, "`\n" terminator. Round-trips through the
system ar(1) command in tests when available.

internal/packager/deb/ — Build(io.Writer, *Spec) writes a complete
.deb: ar archive over [debian-binary, control.tar.gz, data.tar.gz].
Sorts files for determinism, emits parent-directory tar entries,
generates the control file (Description folding per Policy 5.6.13),
md5sums (sorted), and Installed-Size. Tested against dpkg-deb -I/-c
when available; verified lintian-clean on format (open content
findings — changelog, copyright, mtime — land in Phase 6 wiring).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 of the native packaging feature. Adds internal/packager/rpm/
with a Build(w, *Spec) function that maps gogogo's Spec struct to an
rpmpack.RPMMetaData and RPMFile list, producing a valid binary RPM.
Includes table-driven unit tests plus an rpm(1) integration test that
is skipped when the tool is absent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 4 of the native packaging feature. Adds internal/packager/changelog/
with Collect() (git-log-based entry harvester) and FormatDeb()/FormatRPM()
renderers. No external dependencies beyond the existing internal/exec shim.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 5 of the native packaging feature. Adds internal/packager/source/
with BuildDeb (dpkg-source 3.0/quilt, orig tarball + debian dir) and
BuildRPM (rpmbuild -bs with templated .spec). Both shell out gracefully —
returning (true, nil) when the tool is absent. Optional go mod vendor
makes tarballs self-contained for offline builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After binary builds complete, generate native packages (deb, rpm) for
linux targets and source packages (once per release). The packaging is
gracefully skipped if tools are absent. Packages are added to the release
assets for upload to Forgejo.

Also uses VCS metadata (vcs.time) for reproducible builds when available.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Move Debian + RPM packaging from Roadmap to documented feature. Add
comprehensive guide on:
- Binary packages (.deb/.rpm) for Linux targets
- Source packages (.dsc/.src.rpm) with vendored dependencies
- Configuration examples and requirements
- How graceful fallback works when tools are absent

Update Roadmap to reflect only deferred items (APT/YUM repos, Winget+Scoop).

Also verifies the implementation compiles and all tests pass.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Phase 6 had several critical wiring defects:

1. cf.Packages was never read — every release built deb+rpm regardless
   of the user's config. Now respects [deb, rpm] and rejects unknown
   format names with a clear error.

2. The temp output directory was deleted with `defer os.RemoveAll`
   inside packageBuilds, which fired *before* publishRelease read the
   asset filenames for upload. Caller now owns the dir lifetime so it
   outlives publishRelease.

3. packageSource generated source artefacts but returned an empty
   asset list — .dsc / .src.rpm files were written to disk and then
   immediately discarded. Now snapshots outDir before/after and
   registers the new files as assets.

4. Version handling was hardcoded as `version+"-1"`, which produces
   invalid Debian versions for pre-releases (e.g. "1.0.0-rc.1-1"
   instead of "1.0.0~rc.1-1"). Now uses packager.DebVersion /
   packager.RPMVersion which already handle this correctly.

5. Architecture mapping was duplicated locally with a 5-arch subset
   that returned "unknown" for anything else. Now uses the canonical
   packager.DebArch / packager.RPMArch helpers.

6. The License field was populated with the entire LICENSE file body
   (truncated to a "See LICENSE file" placeholder when over 100 bytes).
   Now detects an SPDX-License-Identifier comment in main.go/doc.go,
   or falls back to a short LICENSE first line.

7. RPM filenames used "<name>-<version>-1.<arch>.rpm" with the raw
   version which broke for pre-releases. Now uses the (Version,
   Release) pair from packager.RPMVersion.

8. defer-Close-then-explicit-Close pattern cleaned up.

Also adds package_test.go with tests for gating, format validation,
asset materialisation, license detection, and synopsis extraction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two bugs shipped together because the cert-proxy dogfood test surfaced
both at once:

1. The packaging hook lived only in executeFromClone, so --local (which
   takes the executeFromModCache path) silently dropped packages on the
   floor — producing binaries in --out but no .deb / .rpm. Extracted
   runPackaging() onto *Release and invoke from both paths.
   executeFromModCache uses filepath.Dir(goenv.GOMOD) as the source
   directory for README/LICENSE auto-detection.

2. Multi-command modules generated multiple binary packages with the
   same filename (path.Base(cf.Module) was used for every command),
   so they overwrote each other and publishRelease then tried to copy
   each duplicate filename twice — the first copy removed the source
   file. Now name the package after the binary (res.Name) so a module
   with cmd/foo and cmd/bar produces foo_*.deb and bar_*.deb.

Verified by running:
  cd cert-proxy && gogogo release --local --out /tmp/out
which now produces four valid .debs (client/server × amd64/arm64),
each with the correct binary at /usr/bin/<binary>.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements gogogo config migrate to help users update .gogogo.conf with
newly-added configuration sections (packages, formats, etc.) in commented
form for review. The operation is idempotent and uses a hybrid YAML parsing
approach for robustness.

Changes:
- New internal/cmd/migrate package with schema parsing, diffing, and emission
- Refactored config command to use subcommands: default, parsed, migrate
- All migrations are idempotent via comment-grep pattern matching
- 8 comprehensive tests covering schema, diffing, and migrations
- Optional --write flag to update .gogogo.conf in-place with .bak backup

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
README.md: config subcommands are `default`/`parsed`/`migrate`, not
`--default`/`--parsed` flags; document `config migrate`.

.gogogo.conf: enable deb packaging so gogogo releases itself with a
.deb alongside the binary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add missing global flag --expose-secrets and release flag --timeout
(with GOGOGO_RELEASE_TIMEOUT env var) to the README.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Issue #40: enhance 'gogogo config migrate' to surface missing leaf
attributes within sections that already exist, not just whole missing
sections. The operation remains append-only and idempotent.

- schema.go: add Field type + buildFieldSchema() via reflection on
  config.Config to enumerate all expected field paths
- diff.go: add missingAttributes() to detect leaf attributes whose parent
  section exists but the attribute is absent; also check raw text for
  idempotency markers (# path.to.key:)
- emit.go: rename appendUnits to appendMissing, combine whole-section and
  attribute-level gaps into single output block; add extractSubSectionLines,
  findSectionBounds, and dedent helpers to extract examples from default.yml
  for nested paths
- migrate.go: update Run, NeedsUpdate, CheckIdempotent to call both
  missingUnits and missingAttributes
- migrate_test.go: fix assertion to match updated message text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add four test cases for the new attribute-level detection in issue #40:

- TestMissingAttributes_PartialSection: verifies detection of missing
  leaf attributes within existing sections (e.g., build.commands when
  build: is present)
- TestMissingAttributes_Idempotent: confirms that comment markers
  (# path.to.key:) are treated as present, preventing re-reporting
  after migration
- TestMissingAttributes_NoneWhenComplete: ensures fully-populated config
  returns no missing attributes
- TestMissingAttributes_SkipsWholeMissing: verifies that attributes under
  entirely-missing top-level sections are NOT reported (missingUnits
  covers those)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Cache fieldSchema and defaultLines as package-level vars (was rebuilt
  via reflection on every Run/NeedsUpdate/CheckIdempotent call, and
  config.Default was re-split per missing attribute)
- Extract diffConfig() helper to consolidate the
  missingUnits + missingAttributes pattern across Run, NeedsUpdate,
  and CheckIdempotent
- Extract writeCommented() to deduplicate the inner-line emit loop in
  appendMissing (attrs and units shared identical bodies)
- Drop appendUnits backward-compat shim (its only caller was
  TestCheckIdempotent, now updated to call appendMissing directly)
- Simplify redundant condition in extractSubSectionLines
  (HasPrefix subsumes the equality check)
- Strengthen TestMissingAttributes tests:
  - PartialSection now keys on full dotted paths instead of leaf-only
    (which passed by coincidence of unique leaf names today)
  - Idempotent and SkipsWholeMissing now include positive assertions
    so a regression that returns nil unconditionally is caught
  - Rename NoneWhenComplete -> NoneWhenAllPathsSpecified to better
    describe what's tested (key presence, not value completeness)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add packager.DebUpstreamVersion and private debUpstream helper;
  changelog and source packages now delegate to packager instead of
  carrying identical local version-conversion functions (~60 lines removed)
- Add pkgMeta struct in release/package.go; resolve README synopsis and
  SPDX license once per release run rather than once per binary
- Add config.derefBool helper; simplify SourceEnabled/SourceVendor
- Remove redundant TestDebianVersion/TestRPMVersion/TestDebVersion from
  changelog and source test files (covered by packager/version_test.go)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Author
Owner

The handling of config migrate is rubbish.

  1. It adds module even module exists, but is commented as #module (note the missing whitespace)
  2. It appends after the Vim modeline, that breaks Vim-like editors
  3. It adds the "full" options which are there already in a structured way

Conclusion: we should re-think and maybe only append new options we introduced (commented, if we want them to be opted in) or uncommented, if they need opt-out. E.g. packaging I'd consider as opt-in.

So probably we need to have a config version, current v0, now, with the new features v1, which means, the config subcommand appends the new options only, if the config is <v1

Not sure, if that makes sense.

The handling of `config migrate` is rubbish. 1. It adds `module` even `module` exists, but is commented as `#module` (note the missing whitespace) 2. It appends _after_ the Vim modeline, that breaks Vim-like editors 3. It adds the "full" options which are there already in a structured way Conclusion: we should re-think and maybe only *append* new options we introduced (commented, if we want them to be opted in) or uncommented, if they need opt-out. E.g. packaging I'd consider as opt-in. So probably we need to have a config version, current v0, now, with the new features v1, which means, the config subcommand appends the new options only, if the config is <v1 Not sure, if that makes sense.
When a config has no command specified, the builder sets the package name
to "." (representing the current directory). The package naming logic would
then create a debian package literally named "." instead of falling back to
the module basename.

Fix by treating "." the same as an empty string when determining the
package name — both cases now fall back to the module basename.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This pull request has changes conflicting with the target branch.
  • README.md
  • internal/config/config.go
  • internal/config/default.yml
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin package-deb:package-deb
git switch package-deb

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch master
git merge --no-ff package-deb
git switch package-deb
git rebase master
git switch master
git merge --ff-only package-deb
git switch package-deb
git rebase master
git switch master
git merge --no-ff package-deb
git switch master
git merge --squash package-deb
git switch master
git merge --ff-only package-deb
git switch master
git merge package-deb
git push origin master
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!41
No description provided.