No description
Find a file
Heiko Schlittermann (HS12-RIPE) eaf583426f Merge branch 'feature/tag-message-in-release' (close #43)
Include annotated tag message in Forgejo release body.

* feature/tag-message-in-release:
  refactor: improve composeReleaseBody design and fix signed-tag handling
  feat: include annotated tag message in release body
2026-05-10 09:26:34 +02:00
.claude refactor: rename GOGOGO_SKIP_VERIFY to GOGOGO_RELEASE_SKIP_VERIFY 2026-05-06 16:24:18 +02:00
.gemini ai: add .gemini 2026-05-02 09:04:19 +02:00
internal refactor: improve composeReleaseBody design and fix signed-tag handling 2026-05-10 09:22:05 +02:00
.gogogo.conf feat: add native .deb/.rpm packaging 2026-05-09 23:49:43 +02:00
.golangci.yml lint: new config 2026-04-14 22:12:55 +02:00
go.mod feat: add native .deb/.rpm packaging 2026-05-09 23:49:43 +02:00
go.sum feat: add native .deb/.rpm packaging 2026-05-09 23:49:43 +02:00
LICENSE.txt new: first PoC 2024-10-02 00:04:36 +02:00
main.go feat: cobra CLI with shell completion for --commit 2026-04-13 20:45:04 +02:00
README.md feat: include annotated tag message in release body 2026-05-10 09:09:24 +02:00

go · go · go

Reproducible release builder for Go modules on Forgejo / Gitea.
Publish what others can build. Verify what your users will get.

License Go Status Targets


What is it

gogogo clones a Go module straight from your VCS host, verifies the clone byte-for-byte against the public module proxy, builds across every OS/arch you list, and uploads the artifacts as a release on Forgejo or Gitea — all from a single config file next to your go.mod.

The point is reproducibility: the binary you publish is built from the same bytes that any user gets via go install.

┌────────────────────────────────────────────────────────────────────┐
│   $ gogogo release                                                 │
│   ✓ cloned go.example.com/foo @ v1.2.3                             │
│   ✓ verified clone hash matches module proxy                       │
│   ✓ built foo-linux-amd64  foo-linux-arm64  foo-darwin-arm64       │
│   ✓ uploaded release v1.2.3 to forgejo.example.com                 │
│   ✓ cleaned up superseded pre-releases                             │
└────────────────────────────────────────────────────────────────────┘

Why

Bit-for-bit reproducible GOWORK=off, fresh go mod init, go build -trimpath. Nothing from your local checkout leaks in.
Hash-verified against the proxy The clone is checked against golang.org/x/mod/zip hashes from the Go module proxy before any binary is built.
Cross-arch in parallel Every OS/arch target builds concurrently.
VCS-stamped binaries Builds run inside a real git checkout, so vcs.revision and vcs.time end up in the binary.
Private-repo aware Token is auto-injected into git/go mod download via GIT_CONFIG_* — including across redirect chains.
Self-cleaning Pre-releases superseded by a full release are removed automatically.

Quick start

go install go.schlittermann.de/heiko/gogogo@latest

cd /path/to/your/module          # the directory with go.mod
gogogo config default > .gogogo.conf
$EDITOR .gogogo.conf             # set repo.{apiurl,owner,name,token}, build.targets

# at HEAD on a signed semver tag:
gogogo release

That's the whole workflow. Ahead of pushing, try gogogo release --dry-run to see exactly what would happen.


How it works

flowchart LR
    L[Local checkout]:::soft -. ref check .-> CL
    R[(Remote / Forgejo)] -->|clone @ ref| CL[Verified clone]
    P[(Go module proxy)] <-->|hash compare| CL
    CL -->|go build -trimpath| B[Cross-arch binaries]
    B -->|upload| REL[(Release on Forgejo)]
    CL -->|stamps vcs.revision / vcs.time| B
    classDef soft fill:#f4f4f4,stroke:#bbb,color:#666;
  1. Clone — shallow-clone at the requested ref. Stable releases must point to the same commit locally and remotely.
  2. Verify — compute the module zip hash (golang.org/x/mod/zip) and compare with what go mod download -json reports. Mismatch = abort.
  3. Buildgo build -trimpath runs inside the clone, so the Go toolchain stamps real VCS info into every binary.
  4. Upload — publish as a Forgejo/Gitea release, then run retention cleanup.

If repo config is missing or --local is set, gogogo falls back to a temp-dir build via the module cache (go get module@version). That path can't stamp VCS metadata.


Configuration

.gogogo.conf lives next to your go.mod. Generate the annotated default with gogogo config --default.

Field Description
build.commands Subdirectories of commands to build (empty = module root)
build.targets OS/arch pairs, e.g. linux/amd64 (empty = current platform)
build.test.skip Skip tests before building
repo.apiurl Forgejo/Gitea API URL, e.g. https://git.example.com/api/v1
repo.owner Repository owner / organization
repo.name Repository name
repo.token API token (PAT) — see secret for syntax
repo.restrict30x Permitted redirect-target prefixes (optional)
env Extra environment variables passed to the build

Tip

If repo.token is unset, gogogo falls back to netrc:<host>. Run go tool dist list for the full target catalog.


Commands

gogogo [flags] <command> [flags]

  -v, --verbose         progress and status messages
      --debug LEVEL     1=HTTP requests/status, 2=full bodies (implies --verbose)
      --expose-secrets  show credentials in cleartext in debug output (dangerous)
      --version         print version and exit
      --help            help for any command

Commands:
  config       show configuration
  release      build and upload a release
  status       list existing releases
  completion   generate shell completion script
gogogo config — inspect and migrate configuration
gogogo config default            # built-in default config
gogogo config parsed             # effective config (defaults + .gogogo.conf)
gogogo config migrate            # print migrated config with new sections added
gogogo config migrate --write    # update .gogogo.conf in place

migrate adds any sections present in the default config but absent from your .gogogo.conf, as commented-out examples. The operation is idempotent.

gogogo release — build and upload
  --commit COMMITISH   what to release (default: auto-detect from HEAD)
                         (empty)  use tag at HEAD if present, else
                                  pseudo-version from current branch
                         @        current local branch
                         +        default branch HEAD
                         latest   latest tag on default branch (via proxy)
  --dry-run            show what would happen, no builds, no uploads
  --force              replace an existing release with the same tag
  --local              build only, do not upload
  --out DIR            copy artifacts to DIR (also writes release-info.json)
  --skip-verify        skip module-proxy verification for stable releases
                         (env: GOGOGO_RELEASE_SKIP_VERIFY)
  --timeout DURATION   total timeout for entire build+upload (default 2m)
                         (env: GOGOGO_RELEASE_TIMEOUT)
  --verify             during --dry-run, actually run the verification

When --commit is omitted, gogogo inspects HEAD:

  • Tagged with a semver tag → that tag is used (after a remote-reachability check).
  • Untagged → the current branch name is used, producing a pseudo-version pre-release.
  • In both cases, HEAD must be reachable from a remote tracking branch — push first.
gogogo status — list releases
gogogo status            # tabular
gogogo status --json     # machine-readable
gogogo status --limit 10 # cap the count (0 = all)

Shows tag, name, asset count, creation time, and draft/pre flags.

gogogo completion — shell completion
# bash
source <(gogogo completion bash)

# zsh
gogogo completion zsh > "${fpath[1]}/_gogogo"

# fish
gogogo completion fish | source

The --commit flag completes to git tags, branches, and the special tokens @, +, latest.


Module proxy verification

For stable releases (semver-tagged, e.g. v1.2.0 or v2.0.0-rc.1), gogogo runs go mod download -json module@version in a temp dir and compares the proxy-reported hash with the clone's content hash. This guarantees the release matches what users will get via go get.

For branch builds (pseudo-versions), proxy verification is skipped automatically — the commit SHA already anchors the build, and remote reachability was confirmed during cloning.

Note

Verification is skipped only when --skip-verify is passed or GOGOGO_RELEASE_SKIP_VERIFY is set. In that case the version is derived from the highest semver tag pointing at HEAD.


Authentication & private repositories

A single token (repo.token in .gogogo.conf) is used for two independent purposes:

  1. Cloning — injected as https://token:<TOKEN>@host/owner/repo.git.
  2. Forgejo API — authenticates release uploads and tag deletion.

For module-proxy verification, the Go toolchain runs separately, so it needs its own credentials. gogogo automatically wires the token through via GIT_CONFIG_* — verification works out of the box for private repos.

Vanity domain + redirect chain

A common Go-vanity setup chains a vanity domain → a legacy git host → the actual Forgejo:

sequenceDiagram
    participant U as gogogo
    participant V as go.example.com<br/>(vanity)
    participant G as git.example.com<br/>(legacy)
    participant F as forgejo.example.com<br/>(real)

    U->>V: GET /org/repo?go-import=1
    V-->>U: meta refresh → git.example.com/org/repo
    U->>G: HEAD /org/repo
    G-->>U: 301 forgejo.example.com/org/repo
    U->>F: clone with credentials for forgejo.example.com
    Note over U,F: Token is sent only to the final host —<br/>git does not forward credentials across redirects.

gogogo resolves the chain before injecting the token, so the credential always lands on the final host — both for cloning and for the GIT_CONFIG_* URL rewriting passed to go mod download.

Minimal private-repo config

# .gogogo.conf
env:
  GOPRIVATE: "go.example.com"

GOPRIVATE tells Go to skip the public proxy and checksum DB. gogogo handles token injection and redirect resolution automatically.

Advanced: explicit GIT_CONFIG_* for unusual setups
# .gogogo.conf
env:
  GOPRIVATE: "go.example.com"
  GOPROXY: "direct"
  GIT_CONFIG_COUNT: "1"
  GIT_CONFIG_KEY_0: "url.https://token:${FORGEJO_TOKEN}@forgejo.example.com/.insteadOf"
  GIT_CONFIG_VALUE_0: "https://go.example.com/"
Variable Purpose
GOPRIVATE Bypass the proxy for matching modules
GONOSUMCHECK Skip checksum verification for matching modules
GOPROXY Override the module proxy (e.g. direct)
GIT_CONFIG_COUNT Number of process-scoped Git config entries
GIT_CONFIG_KEY_N Git config key (e.g. url....insteadOf)
GIT_CONFIG_VALUE_N Git config value

User-provided entries are preserved; gogogo's auto-injected entry is appended after them.

Important

--skip-verify is for the rare case where verification is fundamentally impossible (air-gapped, broken vanity-domain DNS). For everyday private-repo work, just set GOPRIVATE.


Major-version subdirectory modules

gogogo supports the Go major-version subdirectory layout (e.g. v2/ with its own go.mod). When the configured module ends with /vN (N ≥ 2) and the clone has vN/go.mod, gogogo automatically:

  • Hashes only the subdirectory (matching what the proxy serves)
  • Runs go list inside the subdirectory
  • Treats build.commands as relative to the subdirectory

Release lifecycle

Releasing a tag

  1. Create and push a signed semver tag on your default branch.
  2. Run gogogo release — the tag at HEAD is auto-detected.

If the tag is annotated (e.g. created via gogogo set-tag), its message is used as the release body on Forgejo. When package registry uploads are configured, install instructions are appended after a separator.

Releasing a branch (pre-release)

gogogo release                       # uses current branch
gogogo release --commit my-feature   # explicit

Untagged HEAD produces a Go pseudo-version pre-release automatically.

Retention policy

flowchart TD
    NEW[New release published]
    NEW --> KIND{kind?}
    KIND -->|pre-release| P1["Strip assets from older pre-releases<br/>(keep the immediate previous one)"]
    KIND -->|pre-release| P2[Full releases untouched]
    KIND -->|full release| F1[Delete all older pre-releases entirely<br/>release object + git tag]
    KIND -->|full release| F2["Strip assets from older full releases<br/>(keep the immediate previous one)"]

"Previous" is always defined by semver order, not upload time.


Native packages

gogogo can automatically generate native distribution packages from your binaries. This makes it easy for users on Debian/Ubuntu, Fedora/RHEL, and other distributions to install your tool via their system package manager.

Binary packages

For Linux targets, gogogo generates .deb (Debian/Ubuntu) and .rpm (Fedora/RHEL) packages from the built binaries. Each package includes:

  • Binary installed to /usr/bin/<name>
  • Metadata from .gogogo.conf or auto-detected from LICENSE/README
  • Reproducible — uses the same VCS timestamp as the binary

Source packages

gogogo can also generate source packages (.dsc for Debian, .src.rpm for RPM) with:

  • Vendored dependenciesgo mod vendor bundles all deps, so the source builds offline
  • Changelog — formatted per distribution conventions
  • Copyright — from your LICENSE file

Source packages let distributions rebuild your tool with their own toolchain or security patches.

Configuration

Enable packaging in .gogogo.conf:

# Generate binary packages for linux targets
packages: [deb, rpm]

# Optional: customize package metadata and build behavior
formats:
  common:
    maintainer: "Your Name <you@example.com>"
    description: "My tool does X"
    homepage: "https://example.com"
    license: "Apache-2.0"
  
  deb:
    section: "utils"           # Debian section (default: misc)
    priority: "optional"       # Debian priority (default: optional)
    depends: ["libc6 (>= 2.31)"] # Additional runtime dependencies
  
  rpm:
    group: "Development/Tools" # RPM group (default: empty)
    requires: ["glibc"]        # Additional runtime dependencies
  
  source:
    enabled: true              # Generate source packages (default: true)
    vendor: true               # Run go mod vendor in source (default: true)

Note

Packaging gracefully skips if the required tools are absent. .deb requires dpkg-source, .rpm requires rpmbuild. Both are optional — gogogo still builds and uploads binaries even if packaging tools are missing.

How it works

After each release build completes:

  1. Binary packages — for each Linux/arch target, gogogo creates a .deb and .rpm wrapping the built binary.
  2. Source packages — once per release, generates a source .deb (.dsc + tarballs) and source .rpm (.src.rpm) with vendored dependencies.
  3. Upload — all packages are included in the Forgejo release alongside the raw binaries.

Verbosity

gogogo is quiet by default — only errors are printed.

Flag Effect
--verbose (-v) One-line status per action; progress bars on TTYs; richer error hints.
--debug 1 Above + HTTP request lines and status codes.
--debug 2 Above + full request and response bodies (verbose; secrets are redacted).

Roadmap

  • APT/YUM repository publishing (gogogo repo subcommand) — host your packages on a Debian or RPM repository server.
  • Winget + Scoop manifests for Windows releases — auto-publish installers to package registries.

License

Apache-2.0 — see LICENSE.txt.