No description
Find a file
Heiko Schlittermann (HS12-RIPE) 0c253e3190
All checks were successful
nagonag (Push) / nagonag (push) Successful in 3m21s
remove nagonag artifacts
2026-05-30 09:02:03 +02:00
.agents ci: add codex instructions 2026-05-20 14:31:33 +02:00
.claude fix: consistent Source: field across .changes and binary .deb control ai:claude-sonnet-4-6 2026-05-24 12:07:38 +02:00
.forgejo/workflows cicd: new workflow 2026-05-21 22:54:40 +02:00
.gemini Update docs: replace stale modver reference with internal/version 2026-05-15 15:01:29 +02:00
internal build: reject unknown strip levels instead of silent default 2026-05-30 08:19:24 +02:00
.gitignore new: doctor mode and debian release number (closes #38, closes #46) 2026-05-12 23:35:26 +02:00
.gogogo.conf Fix Debian package description 2026-05-24 15:52:40 +02:00
.golangci.yml lint: new config 2026-04-14 22:12:55 +02:00
DESIGN.md build: reject unknown strip levels instead of silent default 2026-05-30 08:19:24 +02:00
go-list-raw.json deps(master): update minor/patch dependencies 2026-05-30 09:00:37 +02:00
go.mod deps(master): update minor/patch dependencies 2026-05-30 09:00:37 +02:00
go.sum deps(master): update minor/patch dependencies 2026-05-30 09:00:37 +02:00
LICENSE.txt new: first PoC 2024-10-02 00:04:36 +02:00
main.go ai: slight improvements 2026-05-23 09:43:26 +02:00
minor_patch_updates.json deps(master): update minor/patch dependencies 2026-05-30 09:00:37 +02:00
README.md feat: XDG cache staging dir, checksum dedup, payload purge for dupload ai:claude-sonnet-4-6 2026-05-24 12:07:39 +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


Contents


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 publish                                                 │
│   ✓ 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 build.targets and any non-detected repo fields

# at HEAD on a signed semver tag:
gogogo publish

Ahead of a real run, gogogo publish --dry-run shows what would happen without building or uploading.


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 -->|package linux targets| PKG[Deb and RPM packages]
    B -->|publish.blob| PB[publish.blob]
    PKG -->|publish.deb| PD[publish.deb]
    PKG -->|publish.rpm| PR[publish.rpm]
    PB -->|default| RR[releases]
    PB -->|empty list| NONE[no destinations]
    PB -->|explicit names| DESTS[publish.destinations]
    PD -->|default| RP[packages]
    PD -->|empty list| NONE
    PD -->|explicit names| DESTS
    PR -->|default| RP
    PR -->|empty list| NONE
    PR -->|explicit names| DESTS
    RR -->|built-in| TREL[type: forgejo-release]
    RP -->|built-in| TPKG[type: forgejo-packages]
    TREL --> REL[(Forgejo release)]
    DESTS -->|named destination| XREL[type: forgejo-release]
    DESTS -->|named destination| XPKG[type: forgejo-packages]
    DESTS -->|named destination| XDUP[type: dupload]
    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. Package & publish — route blobs, Debian artifacts, and RPM artifacts through publish.blob, publish.deb, and publish.rpm, using built-in defaults or explicit destinations from publish.destinations, then run retention cleanup.

Each publish.* route accepts default, [], or an explicit destination list. Built-in destination names are releases and packages; explicit names resolve through publish.destinations to a type such as forgejo-release, forgejo-packages, or dupload.

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 and isn't bit-for-bit reproducible across runs.


Configuration

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

Field Description
version Config schema version; current new-style config uses 2
build.commands Subdirectories of commands to build (empty = module root)
build.targets OS/arch pairs, e.g. linux/amd64 (empty = current platform)
build.strip Linker strip level: all (default, -s -w), symbols, dwarf, none
build.test.skip Skip tests before building
repo.remote Git remote used to derive repo settings (default: origin)
repo.apiurl Forgejo/Gitea API URL; derived from repo.remote when unset
repo.owner Repository owner / organization; derived from repo.remote when unset
repo.name Repository name; derived from repo.remote when unset
repo.token API token — defaults to netrc:<remote-host> when unset
security.trusted_redirects Permitted redirect-target prefixes that may receive forwarded request headers
artifacts.packages Multi-package split: absent = one package, '*' = one per cmd/, or explicit list (see below)
artifacts.blob.extra Extra files to publish as blobs (list of {src, name})
artifacts.common.name Override the source/package name derived from the module path's last component
artifacts.common.maintainer Package maintainer name and email; auto-detects from git committer when unset
artifacts.deb.lintian Run lintian on generated .deb packages (default: true)
artifacts.deb.systemd Auto-generate maintainer scripts for systemd/sysusers/tmpfiles assets (default: true)
artifacts.deb.scripts Explicit maintainer scripts (preinst, postinst, prerm, postrm); disables auto-generation
artifacts.deb.conffiles Paths registered as dpkg conffiles
artifacts.rpm.vendor RPM Vendor tag
artifacts.rpm.repository RPM Repository tag
publish.* Artifact destination routing
release.branch Branch where release tags may be created (empty = repository default branch)
env Extra environment variables passed to the build

Tip

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

Minimal real-world example

A typical project needs only a handful of fields — everything else is auto-detected from go.mod and the origin remote:

version: 2

build:
  targets:
    - linux/amd64
    - linux/arm64
    - darwin/arm64
    - windows/amd64

artifacts:
  common:
    maintainer: "Jane Doe <jane@example.com>"
    description: "My Go tool — short one-liner"
    homepage: "https://example.com/mytool"
    license: "Apache-2.0"

  deb:
    enabled: true   # .deb packages for Linux targets

  rpm:
    enabled: true   # .rpm packages for Linux targets

repo.* fields are omitted here: gogogo derives the Forgejo API URL, owner, and repo name from git remote get-url origin, and picks up the token from ~/.netrc. Add them explicitly only if auto-detection fails or you use a non-standard remote.

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)
      --no-network      disable network access globally
      --version         print version and exit
      --help            help for any command

Commands:
  config       show configuration (subcommands: default, parsed, migrate, packages, auto)
  doctor       run pre-flight checks
  build        run build stage only
  pack         run build + package stages (subcommand: populate)
  publish      run build + package + publish stages
  tag          create the next annotated release tag
  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 new-style config to stdout
gogogo config packages           # scaffold artifacts.packages: block from cmd/* discovery
gogogo config auto               # generate a pre-filled .gogogo.conf from project state

migrate reads the existing .gogogo.conf and prints a new rich commented config. Values matching defaults or auto-detected settings are commented out; non-default user values stay active.

packages generates an artifacts.packages: block. Use --shorthand to emit the packages: '*' one-liner, or --in-place to splice the block directly into .gogogo.conf.

auto analyses the current project (module path, git remote, README, LICENSE, systemd units) and emits a pre-filled .gogogo.conf. By default writes to stdout; use --write to update .gogogo.conf in place (a .bak backup is kept by default).

gogogo tag — create the next release tag
  --approve              approve a guessed or major release for creation
  --bump LEVEL           create the next release tag with LEVEL (patch, minor, or major)
  --dry-run              print the changelog without creating the tag
  --force                replace an existing tag
  --push                 push the tag to the tracking remote after creation
  --gpg-sign MODE        GPG-sign the tag: auto (default), true, or false

If you omit --bump, gogogo guesses from commits since the latest stable release tag. The heuristics are deliberately simple:

  • feat: or feat(scope): means minor
  • feat!: / feat(scope)!: or BREAKING CHANGE in the body means major
  • everything else falls back to patch

Pre-release and pseudo-version tags are ignored when looking for the baseline release. --bump patch and --bump minor imply --approve; --bump major and guessed releases require --approve before anything is created.

Tag creation must run on release.branch. If unset, gogogo uses the repository's default branch, usually main or master. --dry-run can be used from any branch; --force overrides the branch guard.

gogogo build, gogogo pack, gogogo publish
  build       build
  pack        build + package  (subcommand: pack populate — scaffold debian/* and rpm/ stubs)
  publish     build + package + publish

Shared stage flags:
  --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)
  --out DIR            output directory; artifacts written to DIR/{blob,deb,rpm}/
                       required for build and pack; optional for publish
  --sign               GPG-sign release binaries (default: auto from git user.signingkey)
  --no-lintian         skip lintian check on generated .deb packages
  --skip-doctor        skip release pre-flight checks
  --skip-verify        skip module-proxy verification for stable releases
                       (env: GOGOGO_RELEASE_SKIP_VERIFY)
  --timeout DURATION   total timeout for staged operation (default 2m)
                       (env: GOGOGO_RELEASE_TIMEOUT)
  --allow-unclean      allow --no-network builds from a dirty worktree

Build/pack-only flags:
  --keep-tmp           keep temporary build directories instead of cleaning up

Publish-only flags:
  --dry-run            show what would happen, no builds, no uploads
  --replace            delete and recreate an existing Forgejo release;
                       for Debian, the pool cannot replace so the revision
                       auto-bumps and a warning is emitted (see Re-publishing
                       and Debian revisions below)
  --local              build only, do not upload
  --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 build, gogogo pack, and gogogo publish all run the same pre-flight checks as gogogo doctor unless you pass --skip-doctor.

--no-network builds from the local checkout, uses a tag at HEAD or derives a local pseudo-version, disables clone/proxy/upload operations, and for publish must be combined with --local. By default it requires a clean worktree; pass --allow-unclean to build uncommitted changes anyway.

gogogo pack populate scaffolds editable debian/ and rpm/ stub files (with text/template placeholders) so you can fully control package layout. Use --force to overwrite existing stubs.

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

repo.token is used for cloning (https://token:<TOKEN>@host/…) and Forgejo API calls. For module-proxy verification the Go toolchain runs separately; gogogo wires the token via GIT_CONFIG_* automatically.

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 publish — the tag at HEAD is auto-detected.

Releasing a branch (pre-release)

gogogo publish                       # uses current branch
gogogo publish --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.

Re-publishing and Debian revisions

A semver tag (e.g. v0.0.12) corresponds to a single upstream release, but packaging fixes (wrong dependency, broken postinst, etc.) often need to be shipped without touching the source. gogogo handles this with a separate tag namespace and an automatic revision counter:

  • Receipt tag deb/v<X>-<N> — after every successful Debian publish, gogogo writes a GPG-signed annotated tag like deb/v0.0.12-3 at the same commit, with message gogogo: debian packaging revision N for v<X>. The next publish reads this and bumps to 0.0.12-4. The tag namespace is non-semver, so it never participates in release-tag selection. Requires user.signingkey to be set in the local git config (same key gogogo uses for signing binaries).
  • Auto-bump — running gogogo publish after a packaging-fix commit produces 0.0.12-(N+1) without any flag. A verbose log line reports the bump: # deb revision 4 for v0.0.12 (previous: 3).
  • No collision in the Debian pool — the Debian archive format (reprepro, dak, aptly, mini-dak) enforces "one checksum per filename" in pool/. Because each revision has a unique filename, gogogo's revisions land cleanly without conflict.

Re-publishing to an existing Forgejo release

When a Forgejo release for the upstream tag already exists from a prior publish, gogogo publish (without --replace) merges the new artifacts into the existing release:

warning: release for tag v0.0.12 already exists; adding new artifacts (use --replace to recreate)
warning: release asset xr-invoiced-linux-amd64 already exists; skipping (use --replace to overwrite)
  • Assets whose names don't already exist in the release are uploaded normally.
  • Assets whose names match an existing one are skipped with a per-asset warning. This preserves the prior binary and only adds the new revision-suffixed .deb/.dsc/.debian.tar.gz.
  • The release's notes, description, and metadata are preserved.

--replace semantics

--replace is the explicit override. Per transport:

  • Forgejo release — the existing release and its tag are deleted, then a fresh release is created with the new artifacts. Use this when you want to overwrite the binary blobs on the release page or reset the release notes.
  • Debian pool — cannot be replaced (archive-format invariant, not a tool quirk). The revision auto-bumps and a warning is emitted: warning: debian pool rejects same-revision re-uploads; bumping v0.0.12-3 → v0.0.12-4 and writing deb/v0.0.12-4.
  • Had no effect — if --replace was given but neither side actually replaced anything: warning: --replace had no effect; nothing to replace at v0.0.12.

Auto-pruning old revision artifacts

When new revision-N artifacts are uploaded to an existing Forgejo release that still contains revision-(N-1) .deb/.dsc/.debian.tar.gz assets, those obsolete files are auto-pruned so the release shows only the latest revision. The binary blob and .orig.tar.gz (no revision suffix) are not touched. This only applies on the default publish path — --replace already starts from a clean slate.


Native packages (DEB, RPM)

For Linux targets, gogogo generates .deb (Debian/Ubuntu) and .rpm (Fedora/RHEL) packages alongside the raw binaries. Binary packages install to /usr/bin/<name> with metadata from .gogogo.conf; source packages (.dsc/.src.rpm) include vendored dependencies for distribution rebuilds.

Configuration

Enable packaging in .gogogo.conf:

artifacts:
  common:
    maintainer: "Your Name <you@example.com>"
    description: "My tool does X"
    homepage: "https://example.com"
    license: "Apache-2.0"

  deb:
    enabled: true              # Generate .deb packages for Linux targets
    source: false              # Generate Debian source artifacts
    source_vendor: true        # Run go mod vendor in source artifacts
    section: "utils"           # Debian section (default: misc)
    priority: "optional"       # Debian priority (default: optional)
    depends: ["libc6 (>= 2.31)"] # Additional runtime dependencies
    files:                     # Extra package files; src supports globs and rsync -r directory semantics
      - src: packaging/hooks/  # trailing slash installs directory contents
        dst: /usr/lib/mytool/hooks/
      - src: packaging/*.service # dst inferred as /lib/systemd/system/<name>
      - src: packaging/mytool.tmpfiles # installs as /usr/lib/tmpfiles.d/mytool.conf

  rpm:
    enabled: true              # Generate RPM packages for Linux targets
    source: false              # Generate source RPM artifacts
    source_vendor: true        # Run go mod vendor in source artifacts
    group: "Development/Tools" # RPM group (default: empty)
    requires: ["glibc"]        # Additional runtime dependencies

publish:
  blob: default                # default: Forgejo/Gitea release assets
  deb:
    - packages                 # Forgejo/Gitea Debian package registry
    - debian_upload            # extra dupload destination
  rpm: default                 # default: Forgejo/Gitea RPM package registry

  destinations:
    debian_upload:
      type: dupload
      host: myrepo             # nickname from ~/.dupload.conf
      keyid: ABC123            # optional debsign key
      configfile: ~/.dupload.conf.local # optional alternate dupload config
      dir: ~/.dupload-uploads/mytool    # optional staging dir outside the module root

Note

Packaging gracefully skips source-package generation if required tools are absent. Debian source artifacts require dpkg-source; source RPMs require rpmbuild. Binary .deb and .rpm artifacts are still built without those source tools.

Note

Debian source package routing: Debian source artifacts (.dsc, .orig.tar.*, .debian.tar.*, .diff.gz) are automatically classified and routed alongside binary .deb files. With --outdir, they go to outdir/deb/ (not outdir/blobs/). Both binary and source packages are sent to dupload destinations. Forgejo's Debian package registry receives only binary .deb files, not source sidecars.

Warning

Debian source packages are currently uploaded as release artifacts, not to a deb-src repository. Source-package generation is disabled by default until Forgejo supports Debian source repositories in the package registry; see issue #45.

Multi-binary packages

artifacts.packages controls how cmd/* binaries are split across .deb/.rpm packages.

Value Behaviour
absent (default) all binaries in one package named after the module
'*' one package per cmd/* binary, named after the binary
explicit list full control: name, binary assignment, per-package deb/rpm overrides
artifacts:
  packages:
    - name: myapp-server
      binaries: [myapp-server]
      deb:
        depends: ["libsystemd0"]
    - name: myapp-client
      binaries: [myapp-client]

Entries in binaries may be bare names (myapp-server) or full Go package paths (cmd/myapp-server) — both are accepted.

Use gogogo config packages to generate this block from cmd/* discovery, or gogogo config packages --shorthand for the packages: '*' one-liner.

Use gogogo pack populate to scaffold editable debian/ and rpm/ template files when you need full control over the package layout.

Using dupload

type: dupload adds a Debian publishing path that stages packages and uploads them via the dupload tool — for example, to an incoming queue or a self-hosted APT repository front-end. It works alongside or instead of Forgejo's built-in package registry.

Prerequisites

apt install devscripts dupload   # debsign comes from devscripts

gogogo doctor checks for both tools and fails with an actionable hint if either is missing. You do not need to run doctor manually; gogogo publish runs it automatically.

How the upload works

For each release that produces .deb files, gogogo:

  1. Creates a staging directory — default $XDG_CACHE_HOME/gogogo/dupload-uploads/<package-name> (typically ~/.cache/gogogo/dupload-uploads/<package-name>), or the path in dir. The directory is created if it does not exist and must be outside the module root.
  2. Copies artifacts — each .deb and any Debian source-package sidecars (.dsc, .tar.gz, etc.) are copied in flat; the original build directory is left untouched.
  3. Writes a .changes file<name>_<version>.changes is generated from the staged files plus metadata taken from .gogogo.conf:
    • Maintainer / Changed-By from artifacts.common.maintainer
    • Description from artifacts.common.description
    • Distribution is the space-joined list of artifacts.deb.distributions (default stable) so reprepro can land one .changes in multiple suites atomically
    • Section and Priority from artifacts.deb.section / artifacts.deb.priority
    • Checksums-Sha1, Checksums-Sha256, and Files computed from the staged files
  4. Signs with debsign — runs debsign [-k<keyid>] <changes> in the staging directory. The key must be available in the local GPG keyring. Signing happens during the pack phase; the dupload step is upload-only.
  5. Uploads with dupload — before calling dupload, gogogo checks a SHA256 sidecar (.changes.gogogo-sha256) left by the last successful upload of this .changes file. If the signed content is identical, the upload is skipped. Otherwise any stale dupload upload record (.upload) is removed so dupload re-uploads cleanly. On success, the sidecar is updated and payload files (.deb, .dsc, .changes, etc.) are purged from the staging directory; the sidecar itself is kept for future dedup checks.

~/.dupload.conf examples

dupload supports several transport methods. Choose the one that matches your repository's incoming queue:

# ~/.dupload.conf

# scp — most common for private repos and Debian Mentors work
$cfg{'myrepo'} = {
  fqdn     => 'repo.example.com',
  method   => 'scp',
  incoming => '/srv/incoming/debian',
  login    => 'upload',
};

# ftp — legacy, but still used by some self-hosted reprepro setups
$cfg{'myftp'} = {
  fqdn     => 'repo.example.com',
  method   => 'ftp',
  incoming => '/queue/incoming',
  login    => 'anonymous',
};

# local — useful for testing; copies to a local directory
$cfg{'local'} = {
  fqdn     => 'localhost',
  method   => 'local',
  incoming => '/var/spool/incoming/debian',
};

Refer to man dupload.conf for the full option set, including dinstall_runs, post_upload_command, and preupload.

Full configuration example

# .gogogo.conf

artifacts:
  common:
    maintainer: "Jane Doe <jane@example.com>"
    description: "My tool does X"

  deb:
    enabled: true
    distributions: ["stable"]   # written into the .changes Distribution field
    section: "utils"
    priority: "optional"

publish:
  deb:
    - packages                   # Forgejo Debian package registry
    - debian_upload              # additionally upload via dupload

  destinations:
    debian_upload:
      type: dupload
      host: myrepo               # nickname from ~/.dupload.conf
      keyid: ABC123              # GPG key for debsign (optional; omit to use default key)
      configfile: ~/.dupload.conf.local  # alternate config file (optional)
      dir: ~/.dupload-uploads/mytool    # staging dir outside the module root (optional)

To upload to both the Forgejo package registry and an external repository via dupload, list both destinations:

publish:
  deb:
    - packages        # built-in: Forgejo/Gitea Debian package registry
    - debian_upload   # custom destination defined in publish.destinations

  destinations:
    debian_upload:
      type: dupload
      host: myrepo    # nickname from ~/.dupload.conf
      keyid: ABC123   # optional GPG key for debsign

packages is the built-in alias for type: forgejo-packages; releases is the built-in alias for type: forgejo-release. Neither needs to be defined under destinations. To skip Forgejo and upload only via dupload:

publish:
  deb:
    - debian_upload   # [] would disable all deb publishing

Reference

Field Required Default Description
type yes Must be dupload
host yes Nickname matching a stanza in ~/.dupload.conf
keyid no GPG default key Key passed as -k<keyid> to debsign
configfile no ~/.dupload.conf Alternate dupload config file
dir no $XDG_CACHE_HOME/gogogo/dupload-uploads/<package-name> Staging directory; ~/ is expanded; must be outside module root

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.