Skip to content

Release Process

GoGatoZ follows a gitflow branching model with fully automated releases. The version number lives in the branch name — there is no version constant to bump in code.

Branch Cut from Merges into Purpose
main Production-ready code. Every commit is releasable; tags live here.
develop main main (via release/*) Integration branch for the next release.
feature/*, fix/* develop develop Day-to-day work.
release/vX.Y.Z develop main and develop Stabilise a release.
hotfix/vX.Y.Z main main and develop Urgent production fix.

A branch-policy.yml workflow validates every PR:

Source branch Allowed target
feature/*, fix/* develop only
release/*, hotfix/*, develop main
Any develop
Anything else main blocked

The end-to-end flow — feature work lands on develop, a release/vX.Y.Z branch builds the versioned image, and the merge into main triggers the full release automation:

graph TB
    subgraph Feature["Feature Development"]
        A["feature/* branch"] -->|"PR"| B["develop"]
    end

    subgraph Build["Release Build"]
        B -->|"cut release/vX.Y.Z<br/>PR + merge"| M["main"]
        M -->|"tag-release.yml<br/>auto-tags vX.Y.Z"| R["release.yml"]
        R --> R1["GoReleaser binaries<br/>ghcr.io/mr-pmillz/gogatoz:vX.Y.Z<br/>+ GitHub Release + changelog"]
    end

    subgraph Sync["Branch Sync"]
        M -.->|"back-merge<br/>main → develop"| B
    end

How the long-lived (develop, main) and ephemeral (release/*, hotfix/*) branches relate over a release cycle:

gitGraph
    commit id: "init"
    branch develop
    checkout develop
    commit id: "feat A"
    commit id: "feat B"
    branch release/v0.8.0
    checkout release/v0.8.0
    commit id: "release prep"
    checkout main
    merge release/v0.8.0 tag: "v0.8.0"
    checkout develop
    merge main id: "back-merge"
    checkout main
    branch hotfix/v0.8.1
    checkout hotfix/v0.8.1
    commit id: "urgent fix"
    checkout main
    merge hotfix/v0.8.1 tag: "v0.8.1"
    checkout develop
    merge main id: "back-merge hotfix"

A branch merge never triggers the release directly. tag-release.yml validates the merged branch, then pushes the annotated tag that release.yml consumes — using the GitHub App token, because a tag pushed by the default GITHUB_TOKEN would not trigger a downstream workflow:

sequenceDiagram
    actor Dev as Engineer
    participant GH as GitHub (branch)
    participant AT as tag-release.yml
    participant WF as release.yml
    participant GHCR as GHCR + GitHub Release

    Dev->>GH: merge release/vX.Y.Z into main
    GH->>AT: merged PR event
    Note over AT: extract semver<br/>from branch name
    AT->>GH: push annotated tag (GitHub App token)
    Note right of AT: App token, not GITHUB_TOKEN —<br/>else no downstream trigger
    GH->>WF: tag event (vX.Y.Z)
    WF->>WF: GoReleaser cross-compile<br/>+ git-cliff changelog
    WF->>GHCR: push multi-arch images<br/>+ publish GitHub Release

GoGatoZ does not keep a version constant in source. The version is resolved at build time from one of three sources (in order):

  1. GoReleaser ldflags — injected during the release build (cmd.version, cmd.commit, cmd.date).
  2. Module build info — set automatically by go install github.com/mr-pmillz/gogatoz@vX.Y.Z.
  3. Fallbackdev / none / unknown for plain go build.

The version is chosen exactly once: when you name the release branch (release/v0.8.0).

Use the GitHub UI / CLI toggle in each step to follow whichever path you prefer — the choice syncs across every step.

  1. Land features on develop. Merge each feature/* or fix/* PR into develop. Verify CI is green.

  2. Create the release branch from develop.

    • Navigate to Code → Branches → New branch.
    • Name it release/v0.8.0, source from develop.
  3. Stabilise the release branch. Only release-blocking fixes go on the release branch — no new features. CI runs on every push to release/**.

  4. Open a PR into main and wait for CI to pass.

    • Pull requests → New pull request: base main, compare release/v0.8.0.
    • Wait for CI (build, lint, test, branch-policy) to pass.
  5. Merge the PR. This triggers the full automation chain:

    1. tag-release.yml detects the merged release/* branch, extracts the semver from the branch name, and pushes an annotated v0.8.0 tag using a GitHub App token.
    2. release.yml fires on the new v* tag:
      • GoReleaser cross-compiles binaries for Linux, macOS, and Windows (amd64 + arm64).
      • Multi-arch container images are pushed to ghcr.io/mr-pmillz/gogatoz.
      • git-cliff generates release notes from conventional commits.
      • A GitHub Release is published with the binaries and changelog.
      • Build provenance is attested for both archives and container images.

    Click Merge pull request on the release PR.

  6. Back-merge main into develop. Sync the changelog commit and any release-branch fixes back to develop.

    • Pull requests → New pull request: base develop, compare main.
    • Click Merge pull request.

When a single release needs to bundle several in-flight feature branches or open PRs, create the release branch first, then retarget each feature PR so they all merge into it. The release branch carries the combined work into main.

  1. Create the release branch from develop.

    Branches → New branch: name release/v0.8.0, source develop.

  2. Retarget each feature PR’s base from develop to release/v0.8.0.

    • For each open PR: open it → click Edit (next to the PR title) → change the base dropdown from develop to release/v0.8.0Change base.
    • For a feature branch with no PR yet: Pull requests → New pull request, set base release/v0.8.0, compare feature/foo.
  3. Merge each PR into the release branch. Resolve conflicts on release/v0.8.0 as they surface.

    Click Merge pull request on each retargeted PR.

  4. Open the single release PR into main and merge it. This is Release Steps step 4 onward: the merge triggers tag-release.ymlrelease.yml.

    Pull requests → New pull request: base main, compare release/v0.8.0; wait for CI, then Merge pull request.

For urgent production fixes that cannot wait for the next release:

  1. Create the hotfix branch from main.

    Branches → New branch: name hotfix/v0.8.1, source main.

  2. Fix, commit, and push the hotfix.

    Edit files directly on the hotfix/v0.8.1 branch in GitHub, or push from your local clone.

  3. Open a PR into main and merge it. The same automation chain fires (tag-release.ymlrelease.yml → changelog → GitHub Release).

    • Pull requests → New pull request: base main, compare hotfix/v0.8.1.
    • Wait for CI, then Merge pull request.
  4. Back-merge main into develop.

    • Pull requests → New pull request: base develop, compare main.
    • Click Merge pull request.

If the automation fails, you can always tag manually:

Terminal window
git tag -a v0.8.0 -m "Release v0.8.0"
git push origin v0.8.0

This works because release.yml triggers on any v* tag push regardless of how it was created.

Workflow Trigger Purpose
ci.yml Push to main / develop / release/** / hotfix/**; PR to main or develop Build, lint, test, coverage
tag-release.yml Merged PR into main from release/v* or hotfix/* Auto-tag vX.Y.Z
changelog.yml Push to release/v* or hotfix/v* Regenerate CHANGELOG.md on the release branch
release.yml Tag v* GoReleaser + GHCR images + GitHub Release + attestation
branch-policy.yml PR events Enforce branch targeting rules
docs.yml Push to main Build + deploy Astro docs to GitHub Pages

Before this flow works, the repository needs:

  1. GitHub App — create a GitHub App with Contents: write permission, install it on the repository, and add its credentials as repository secrets:
    • GOGATOZ_APP_ID — the App’s numeric ID.
    • GOGATOZ_APP_PRIVATE_KEY — the App’s PEM private key.
  2. Branch protection bypass — add the GitHub App to the branch/tag protection bypass list so it can push tags and commits to protected main.
  3. Changelog token — the App token is reused by git-cliff to link PRs and authors in the changelog.