Files
goget/docs/development.md

7.5 KiB

Development

This guide covers the day-to-day workflow for working on goget — building, testing, releasing, and contributing.

Requirements

  • Go 1.26+
  • Platform: Linux (amd64, arm64, riscv64, loong64) or FreeBSD (amd64, arm64, riscv64)
  • just — task runner (brew install just / apt install just / pacman -S just)
  • groff (optional) — for just man manpage validation

All Go source files carry the build constraint //go:build linux || freebsd. goget does not build or run on macOS or Windows.

First-time Setup

git clone https://codeberg.org/petrbalvin/goget.git
cd goget
just install        # go mod download
just test           # verify everything works on your machine
just build          # produce bin/goget

A successful just build produces a static bin/goget binary with no runtime dependencies beyond the Go standard library and golang.org/x/ packages.

Task Reference

Every task is defined in justfile and is the canonical way to interact with the project.

Task What it does
just List all recipes (default)
just install go mod download
just run go run ./cmd/goget (development loop)
just build gofmt -l check + go vet ./... + build bin/goget
just test go test -race -count=1 ./...
just uninstall Remove bin/ and coverage.out
just man Validate man/goget.1 with groff (skips if absent)
just install-man Install manpage to ~/.local/share/man/man1/ (gzipped, XDG)
just uninstall-man Remove installed manpage

just build is strict: it runs gofmt -l (any output is a failure) plus go vet ./.... Both must report zero warnings before the binary compiles. This is exactly what CI checks; running it locally first saves a round-trip.

Development Loop

graph TD
    Edit[Edit source in cmd/, internal/, pkg/]
    Test[just test]
    Vet[just build runs gofmt + vet + build]
    Man[just man - groff validation]
    Commit[Conventional Commits message]
    Push[Push to development branch]
    CI[Forgejo CI: test.yml]

    Edit --> Test
    Test -->|fail| Edit
    Test --> Vet
    Vet -->|fail| Edit
    Vet --> Man
    Man -->|fail| Edit
    Man --> Commit
    Commit --> Push
    Push --> CI
    CI -->|fail| Edit
    CI -->|pass| Merge[Merge / open PR]

Typical iteration:

# Make a focused change.
$EDITOR cmd/goget/flags.go

# Quick check that compile + format + vet pass.
just build

# Run the relevant tests with the race detector.
just test

# Validate any manpage edits.
just man

Building from Source

just build does three things in order:

  1. Copies man/goget.1 into cmd/goget/man-page-data/ — the manpage is embedded into the binary via //go:embed, and //go:embed can only reference files within the embedding package's directory.
  2. Runs gofmt -l over cmd/, internal/, and pkg/. Any output is a formatting violation that breaks the build.
  3. Runs go vet ./... — must report zero warnings.
  4. Compiles bin/goget with -ldflags "-s -w" (strips DWARF).

The resulting binary is fully static. ldd bin/goget should report not a dynamic executable on Linux.

Testing

just test           # full suite, race detector, count=1
go test ./internal/protocol/gemini/...   # focused
go test -run TestFoo ./internal/core/    # one test

Conventions:

  • Tests live next to the code as *_test.go.
  • Use table-driven tests for parsers, validators, and protocol handlers.
  • Cover both happy path and error path.
  • Test names follow TestFunctionName_Scenario.
  • Fuzz tests use testing.F and live next to the code they exercise.

The race detector is always on in CI. New code that introduces a data race must be fixed, not silenced.

Manpage Authoring

The single source of truth is man/goget.1 (troff). Editing this file is what most users eventually read via man goget and what --generate-man-page prints.

Workflow:

$EDITOR man/goget.1
just man            # validates the troff
just build          # copies the file into cmd/goget/man-page-data/
just test           # cmd/goget/man_page_test.go asserts every CLI flag
                    # is documented and the embedded source still parses

cmd/goget/man_page_test.go enforces documentation coverage — adding a new flag to defineFlags without documenting it in man/goget.1 breaks the build.

Logging

The project uses internal/log for structured output. Prefer log.Infof, log.Errorf, etc. over ad-hoc fmt.Fprintf(os.Stderr, ...) or cli.Print*. The backlog tracks a migration of the remaining ~140 legacy callsites to internal/log.

Branching & Releases

graph LR
    Feature[Feature branches]
    Development[development]
    Main[main]
    Tag[vX.Y.Z tag]
    Release[Forgejo release]

    Feature --> Development
    Development --> Main
    Main --> Tag
    Tag --> Release
  • development — the only branch that receives day-to-day work. All pull requests target development.
  • main — only changes when development is merged for a release. Never commit directly to main.
  • TagsvX.Y.Z tags trigger the release workflow.

Cutting a Release

  1. Bump Version in internal/core/version.go.
  2. Move the top ## [development] section in CHANGELOG.md to ## [X.Y.Z] — YYYY-MM-DD, inserting a fresh empty ## [development] section above it.
  3. Ensure CI is green on development.
  4. Merge developmentmain.
  5. Tag and push:
    git tag -a vX.Y.Z -m "goget vX.Y.Z"
    git push origin main
    git push origin vX.Y.Z
    
  6. The release workflow builds binaries for all seven platform targets, extracts the matching CHANGELOG.md section, and publishes the release on Codeberg.

CI

Two Forgejo Actions workflows live under .forgejo/workflows/:

test.yml

Triggered on every push to development and on every pull request targeting development.

Job What it does
vet gofmt -l check + go vet ./... (no output = pass)
test go test -race -count=1 ./... + coverage gate

Run the same locally before opening a PR:

go vet ./...
just test

release.yml

Triggered by pushing a v* tag. Verifies that the tag matches internal/core.Version, runs the quality gate, builds static binaries for all seven targets, and publishes the release.

Requires a FORGEJO_TOKEN secret with release-creation permissions on the runner.

Code Style

  • Follow Effective Go.
  • Keep lines under 100 characters.
  • camelCase for locals, PascalCase for exported symbols.
  • Prefix every file with a one-line comment describing its purpose.
  • Use //go:build linux || freebsd on every source file.
  • Use long CLI flags only (--header, not -H).
  • Stick to the Go standard library and golang.org/x/. External dependencies require an explicit justification.
  • Error messages: English, lowercase, no trailing period, always with context (e.g. open config: permission denied).
  • Run gofmt before committing.

Reporting Bugs

Open an issue at https://codeberg.org/petrbalvin/goget/issues with:

  • goget --version output.
  • Operating system and architecture.
  • The full command used and its output.
  • Expected vs actual behaviour.

For security issues, email opensource@petrbalvin.org instead of opening a public issue.