Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/RELEASE_PROCEDURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Release Procedure & Guardrails

This document establishes the mandatory, step-by-step release procedure for `go-app-kit`.

---

## 1. Immutable Release Principles

Go modules rely on public proxy and checksum services (`proxy.golang.org` and `sum.golang.org`).

- **Never Overwrite or Move Tags**: Once a Git tag is pushed to remote, Go module proxies immediately cache the source archive and its cryptographic checksum (`h1:...`). Retagging the same version name to a different commit will break downstream builds with checksum mismatch errors.
- **Monotonic Releases Only**: If a release fails or needs corrections (such as the historical `v0.3.1` incident where a tag was pushed before docs were updated), a new monotonic version (e.g. `v0.3.2`) must be published.
- **Annotated Tags Only**: All release tags must be created as annotated Git tags (`git tag -a vX.Y.Z -m "Release vX.Y.Z"`). Never use lightweight tags for official releases.

---

## 2. Release Guardrails & Automated Checks

To prevent releasing unverified code or untracked changes, multiple layers of defense are active:

1. **Pre-Tag Verification Script** (`scripts/check_tag_readiness.sh`):
- Verifies the Git working tree is completely clean.
- Validates that `TAG` argument is valid SemVer (e.g. `v0.3.2`).
- Ensures `CHANGELOG.md` has an entry for the version as its top released section.
- Ensures `README.md` published version and installation snippet match.
- Runs `GIT_TAG=$TAG ./scripts/check_version.sh` (validates package count, DDL, docs, symbol inventory, and coverage).
- Runs `./scripts/test_bash_compat.sh` (Bash 3.2 macOS compatibility).
- Runs `go test -race ./...` (concurrency race detector).

2. **Git Pre-Push Hook** (`scripts/git-hooks/pre-push`):
- Intercepts any `git push` attempting to push a tag `refs/tags/v*`.
- Executes `GIT_TAG=$tag ./scripts/check_version.sh`.
- Rejects the push if version gates or test checks fail.

3. **Makefile Targets**:
- `make install-hooks`: Configures Git to use `scripts/git-hooks`.
- `make check-release-readiness TAG=vX.Y.Z`: Runs pre-tag validation.
- `make tag-release TAG=vX.Y.Z`: Runs readiness validation and creates the annotated tag only if all checks pass.

---

## 3. Step-by-Step Release Checklist

### Step 1: Install Git Hooks (One-Time Setup)
```bash
make install-hooks
```

### Step 2: Prepare Release PR on Feature Branch
1. Create a release branch from `main`:
```bash
git checkout -b release/vX.Y.Z
```
2. Update documentation and release metadata:
- **`CHANGELOG.md`**: Add `## [X.Y.Z] - YYYY-MM-DD` at the top of released versions. Document all changes under appropriate categories (`Changed`, `Added`, `Fixed`).
- **`README.md`**: Update current published release header and `go get github.com/umesh0492/go-app-kit@vX.Y.Z` snippet.
- **`SECURITY.md`**: Verify that the version series is marked **Active / Current**.
- **`docs/RELEASE_BASELINE.md`**: Document the release baseline and history.
- **`scripts/check_version.sh`**: Ensure dependency versions and expectations match `go.mod`.
3. Verify changes locally:
```bash
bash scripts/test_bash_compat.sh
bash scripts/check_version.sh
go test -race ./...
```
4. Commit and push the branch:
```bash
git add -A
git commit -m "chore(release): prepare vX.Y.Z release"
git push origin release/vX.Y.Z
```
5. Open a Pull Request into `main` and wait for all CI checks to pass.

### Step 3: Merge PR to `main`
- Merge the Release PR into `main` (Squash or Rebase).

### Step 4: Tag the Release on `main`
1. Switch to `main` and ensure your local branch is synchronized:
```bash
git checkout main
git pull origin main
```
2. Execute the automated release tagging target:
```bash
make tag-release TAG=vX.Y.Z
```
*Note: This command runs `scripts/check_tag_readiness.sh`. It will fail if your working tree is dirty or any version mismatch exists.*

### Step 5: Push the Tag
```bash
git push origin vX.Y.Z
```
*Note: The `pre-push` hook will run a final validation before the tag leaves your machine.*

### Step 6: Verify CI Release Workflow
- Monitor the GitHub Actions **Release** workflow run.
- Ensure that the release manifest and assets are created and published on GitHub.
18 changes: 15 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.3.2] - 2026-09-18

### Changed
- dependencies: Require verified published `github.com/umesh0492/go-libs@v0.2.1` and `github.com/umesh0492/go-fintech-india@v0.2.3`; remove the machine-specific local replacement.
- release: Record immutable module and tag evidence, including the separate lightweight `v0.3.0` artifact, and verify proxy-only isolated consumers before a release.
- release: Reconciled release baseline following v0.3.1 truth-gate trip; published monotonic v0.3.2.
- dependencies: Bumped `github.com/umesh0492/go-libs` to `v0.3.0`.
- outbox: Added `TypeMap()` implementation to test `mockRows` for pgx/v5 5.11.0 compatibility.
- tooling: Added `scripts/check_tag_readiness.sh`, `scripts/git-hooks/pre-push`, and `make tag-release` to structurally prevent untracked changelog releases.

## [0.3.1] - 2026-09-17

### Changed
- ci: Replace all Bash 4+ `mapfile`/`readarray` built-ins with POSIX/Bash 3.2-compatible `while IFS= read -r` array loops in `scripts/check_version.sh` and `scripts/check_coverage.sh`, fixing macOS CI failures.
- ci: Add `scripts/test_bash_compat.sh` — static compatibility guard integrated into CI, release workflow, and Makefile.
- release: Remove fragile zero-retry isolated consumer step from `release.yml`; replaced with `scripts/verify_release.sh` using bounded exponential backoff (up to 12 attempts, per-attempt isolated GOMODCACHE).
- release: Add `scripts/test_verify_release.sh` — unit-tests proxy retry logic, backoff timing, manifest generation, and pre-release SemVer parsing.
- ci: Add `scripts/verify_release_baseline.sh` — post-merge downstream consumer integrity check.
- docs: Add `docs/RELEASE_BASELINE.md` documenting full release procedure and version gates.

## [0.2.1] - 2026-09-17

Expand Down
18 changes: 17 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all fmt-check test test-race cover test-integration lint vulncheck verify verify-release-baseline test-bash-compat tidy build couple decouple workspace-init clean help
.PHONY: all fmt-check test test-race cover test-integration lint vulncheck verify verify-release-baseline test-bash-compat tidy build couple decouple workspace-init install-hooks check-release-readiness tag-release clean help

all: fmt-check verify lint test-race vulncheck build

Expand Down Expand Up @@ -49,6 +49,22 @@ decouple:
couple:
@echo "DEPRECATED: refusing to modify go.mod. Run 'make workspace-init' to use go-libs locally."

# Install git hooks to enforce release and pre-push verification
install-hooks:
@git config core.hooksPath scripts/git-hooks || (mkdir -p .git/hooks && cp scripts/git-hooks/* .git/hooks/)
@chmod +x scripts/git-hooks/* 2>/dev/null || true
@echo "✅ Git hooks configured to scripts/git-hooks"

# Verify that the repository is completely ready for a release tag (usage: make check-release-readiness TAG=v0.3.2)
check-release-readiness:
@if [ -z "$(TAG)" ]; then echo "❌ TAG is required. Usage: make check-release-readiness TAG=vX.Y.Z" >&2; exit 1; fi
./scripts/check_tag_readiness.sh $(TAG)

# Check release readiness and create an annotated git tag (usage: make tag-release TAG=v0.3.2)
tag-release: check-release-readiness
git tag -a $(TAG) -m "Release $(TAG)"
@echo "✅ Successfully created annotated release tag $(TAG)."

# Clean build and test artifacts
clean:
rm -f invoice_service examples/invoice_service/invoice_service coverage.out coverage.out.packages .packages coverage.txt coverage.html *.test
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# go-app-kit

> Current main-line published release: `v0.2.1`. The lightweight `v0.3.0` tag points to an earlier commit and remains an immutable, separate artifact. See [Release Baseline](docs/RELEASE_BASELINE.md) before selecting a release or publishing a reconciliation.
> Current main-line published release: `v0.3.2`. The lightweight `v0.3.0` tag points to an earlier commit and remains an immutable, separate artifact. See [Release Baseline](docs/RELEASE_BASELINE.md) before selecting a release or publishing a reconciliation.

[![CI](https://github.com/umesh0492/go-app-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/umesh0492/go-app-kit/actions/workflows/ci.yml)
[![Code Quality: golangci-lint](https://img.shields.io/badge/code%20quality-golangci--lint-brightgreen?logo=go)](https://golangci-lint.run/)
Expand Down Expand Up @@ -53,7 +53,7 @@ While [`go-libs`](https://github.com/umesh0492/go-libs) provides low-level, zero
### Standalone Import
When consuming `go-app-kit` in your microservice:
```bash
go get github.com/umesh0492/go-app-kit@v0.2.1
go get github.com/umesh0492/go-app-kit@v0.3.2
```

### Multi-Module Local Development (`go.work`)
Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

| Version Series | Security Updates | Status |
| -------------- | ----------------------- | -------------------- |
| 0.2.x | :white_check_mark: Yes | **Active / Current** |
| 0.1.x | :white_check_mark: Yes | Maintenance |
| 0.3.x | :white_check_mark: Yes | **Active / Current** |
| 0.2.x | :white_check_mark: Yes | Maintenance |
| 0.1.x | :x: No | End-of-Life |
| < 0.1.0 | :x: No | End-of-Life |

---
Expand Down
24 changes: 16 additions & 8 deletions docs/RELEASE_BASELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,26 @@ altering local refs.
| --- | --- | --- | --- | --- | --- |
| `github.com/umesh0492/go-app-kit` | `v0.2.1` | annotated (`277ad0741cd723d8c2c445b24dec5608cf1a1c3b`) | `bbb29710fd5622023b9b30bd3c9151d4d5517832` | `11c92c0eabf36fefe0b0a38eceebe8d3f311cda8b72e27edf479a3a64192d325` | `h1:fIcEZxtHfIsYSfaoZPN5+xMGRh6WGltgBMNmkix+mx0=` |
| `github.com/umesh0492/go-app-kit` | `v0.3.0` | lightweight | `a4501b6ee165c475811e6e3c43dab9b20907d25e` | `52036d15c805f2f10f795e868abbe32248f71e210c6f4213524f2a464c1806f4` | `h1:vxh+SnwkUot1nLkWKDUBuhLfEPI877ABFxjOfYYdTiY=` |
| `github.com/umesh0492/go-libs` | `v0.2.1` | annotated (verified in its baseline) | `b75acc6d82e47189ef174a4ea80134fed8cd392f` | `d519b6624138503f1cbe8f3de071f0fe685b521ad5f1f9634a0a85a1992fc9db` | `h1:9Fzm2GZMkd+rnFAFOd5MURnOorqM98O4H/zcVUb6uoY=` |
| `github.com/umesh0492/go-app-kit` | `v0.3.1` | lightweight | `815823212936738de790eba35118c36acdecfea1` | cached proxy entry (release truth-gate failure) | cached sumdb entry |
| `github.com/umesh0492/go-libs` | `v0.3.0` | annotated (verified in its baseline) | `69e9b0857997380907ad53ec220d36baadbe53e3` | verified in go-libs baseline | `h1:B6rPK2M7OdXc5a3Q8xsxUvJawgf/C4nJVG1KvHwpyXg=` |
| `github.com/umesh0492/go-fintech-india` | `v0.2.3` | lightweight (verified in its baseline) | `55888b8fb529bb6379eb96f9a04ce649b7a08b90` | `17b5e66a4f1164cabca95e4bfef849b956162552f32db7e2e7ea6d57b30f8504` | `h1:f2QB8HdhsxaCRFvu0HWQmu55rqPtxA6/kliAYIYNqtU=` |

The `v0.3.0` tag is a legitimate immutable published artifact, but it points to
a4501b6, which predates main's `v0.2.0` and annotated `v0.2.1` commits. It is
not a release of the current main-line source. Consumers of the source checkout
must not be told to use it, and a release must not overwrite it.

The `v0.3.1` tag is an immutable remote artifact pointing to commit `8158232`. It tripped
the release workflow truth gate because commit 8158232 was tagged before CHANGELOG.md
and README.md were updated to v0.3.1. Because Go module proxies cache tags immutably,
v0.3.1 cannot be re-pointed or overwritten; it remains an immutable entry, and
reconciliation is published monotonically as `v0.3.2`.

## Dependency baseline

The current source requires only verified published dependencies:

- `github.com/umesh0492/go-libs v0.2.1`
- `github.com/umesh0492/go-libs v0.3.0`
- `github.com/umesh0492/go-fintech-india v0.2.3`

There are no `replace` directives in `go.mod`. Use a caller-owned `go.work` file
Expand Down Expand Up @@ -57,10 +64,11 @@ on failure. It creates a missing GitHub release or replaces only the
`release-manifest.json` asset on an existing release; tags and other assets are
never changed.

## Next release recommendation
## Current release baseline (v0.3.2)

The source baseline removes an absolute local replacement, upgrades the direct
fintech dependency, and adds release verification. It must be published as a
new version; do not reuse `v0.2.1` or `v0.3.0`. Because `v0.3.0` already exists,
the next monotonic version is **`v0.3.1` or later**. A `v0.3.1` patch release is
appropriate if compatibility checks remain green.
Following the immutable remote caching of `v0.3.1` at commit `8158232`, this repository
reconciled its release baseline to **`v0.3.2`**.
The release includes:
- Upgrading `github.com/umesh0492/go-libs` to `v0.3.0`.
- Implementing `TypeMap() *pgtype.Map` on `*mockRows` in `outbox/outbox_test.go` for pgx/v5 5.11.0 compatibility.
- Introducing structural release guardrails: `scripts/check_tag_readiness.sh`, `scripts/git-hooks/pre-push`, and `make tag-release` to prevent untracked or unverified tags from being cut.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/testcontainers/testcontainers-go v0.44.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.44.0
github.com/umesh0492/go-fintech-india v0.2.3
github.com/umesh0492/go-libs v0.2.1
github.com/umesh0492/go-libs v0.3.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqo
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
github.com/umesh0492/go-fintech-india v0.2.3 h1:f2QB8HdhsxaCRFvu0HWQmu55rqPtxA6/kliAYIYNqtU=
github.com/umesh0492/go-fintech-india v0.2.3/go.mod h1:UPKoeW7JKYkR0nQtY4rW1dmJ9oI20RzDOeE4s7fDrgc=
github.com/umesh0492/go-libs v0.2.1 h1:9Fzm2GZMkd+rnFAFOd5MURnOorqM98O4H/zcVUb6uoY=
github.com/umesh0492/go-libs v0.2.1/go.mod h1:R7gQaadUNwpnavd5P96ThNbhYyUUKyVbfKCR/mu29/o=
github.com/umesh0492/go-libs v0.3.0 h1:B6rPK2M7OdXc5a3Q8xsxUvJawgf/C4nJVG1KvHwpyXg=
github.com/umesh0492/go-libs v0.3.0/go.mod h1:R7gQaadUNwpnavd5P96ThNbhYyUUKyVbfKCR/mu29/o=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down
5 changes: 5 additions & 0 deletions outbox/outbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/umesh0492/go-app-kit/outbox"
)

Expand Down Expand Up @@ -1023,6 +1024,10 @@ func (m *mockRows) Conn() *pgx.Conn {
return nil
}

func (m *mockRows) TypeMap() *pgtype.Map {
return nil
}

func reflectAssign(dest any, src any) error {
if dest == nil {
return errors.New("nil destination")
Expand Down
71 changes: 71 additions & 0 deletions scripts/check_tag_readiness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail

# Validates repository, documentation, and test state before creating or pushing a release tag.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"

readonly SEMVER_PATTERN='[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?'

fail() {
printf '\033[0;31mERROR: %s\033[0m\n' "$*" >&2
exit 1
}

tag="${1:-}"
if [[ -z "${tag}" ]]; then
fail "Usage: $0 <vX.Y.Z>"
fi

if [[ ! "${tag}" =~ ^v${SEMVER_PATTERN}$ ]]; then
fail "Tag must be valid SemVer prefixed with 'v' (e.g. v0.3.2): ${tag}"
fi

version="${tag#v}"

echo "========================================================"
echo "Checking release tag readiness for: ${tag}"
echo "========================================================"

# 1. Validates that the working tree is clean.
echo "-> Checking working tree status..."
if [[ -n "$(git status --porcelain)" ]]; then
git status --short >&2
fail "Working tree has uncommitted or untracked changes. Commit or stash them before releasing."
fi

# 2. Validates that CHANGELOG.md has an entry for this exact version as its top released section.
echo "-> Validating CHANGELOG.md top released version..."
top_changelog_version="$(grep -E "^## \[${SEMVER_PATTERN}\]" CHANGELOG.md | head -n1 | sed -E "s/^## \[(${SEMVER_PATTERN})\].*/\1/")"
if [[ "${top_changelog_version}" != "${version}" ]]; then
fail "Top CHANGELOG.md release entry is v${top_changelog_version:-none}, but expected v${version}"
fi

# 3. Validates that README.md has this exact version.
echo "-> Validating README.md version references..."
readme_pub="$(sed -n -E "s/^> Current main-line published release: \`v(${SEMVER_PATTERN})\`\..*/\1/p" README.md | head -n1)"
readme_inst="$(sed -n -E "s|.*github\.com/umesh0492/go-app-kit@v(${SEMVER_PATTERN}).*|\1|p" README.md | head -n1)"
if [[ "${readme_pub}" != "${version}" ]]; then
fail "README.md published release header is v${readme_pub:-none}, but expected v${version}"
fi
if [[ "${readme_inst}" != "${version}" ]]; then
fail "README.md installation snippet is v${readme_inst:-none}, but expected v${version}"
fi

# 4. Runs GIT_TAG=$1 ./scripts/check_version.sh.
echo "-> Running check_version.sh for ${tag}..."
GIT_TAG="${tag}" ./scripts/check_version.sh

# 5. Runs ./scripts/test_bash_compat.sh.
echo "-> Running test_bash_compat.sh..."
./scripts/test_bash_compat.sh

# 6. Runs go test -race ./...
echo "-> Running go test -race ./..."
go test -race ./...

echo "========================================================"
printf '\033[0;32m✅ Release readiness confirmed! Tag %s is ready to be created and pushed.\033[0m\n' "${tag}"
echo "========================================================"
2 changes: 1 addition & 1 deletion scripts/check_version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ fi
if grep -qE '^[[:space:]]*replace[[:space:]]+' go.mod; then
fail 'go.mod contains a replace directive; use a caller-owned go.work for local companion development'
fi
[[ "${golibs_version}" == 'v0.2.1' ]] || fail "go.mod must require go-libs v0.2.1, got ${golibs_version:-none}"
[[ "${golibs_version}" == 'v0.3.0' ]] || fail "go.mod must require go-libs v0.3.0, got ${golibs_version:-none}"
[[ "${fintech_version}" == 'v0.2.3' ]] || fail "go.mod must require go-fintech-india v0.2.3, got ${fintech_version:-none}"

# Bash 3.2 has indexed arrays but not mapfile/readarray. Populate arrays with
Expand Down
40 changes: 40 additions & 0 deletions scripts/git-hooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail

# Git pre-push hook: intercepts git push attempting to push a tag refs/tags/v*
# Verifies version, changelog, and test gates via check_version.sh.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
cd "${ROOT_DIR}"

remote="$1"
url="$2"

readonly ZERO="0000000000000000000000000000000000000000"

while read -r local_ref local_sha remote_ref remote_sha; do
# Skip deletion of refs
if [[ "${local_sha}" == "${ZERO}" || "${local_ref}" == "(delete)" ]]; then
continue
fi

tag=""
if [[ "${remote_ref}" =~ ^refs/tags/(v[0-9]+.*) ]]; then
tag="${BASH_REMATCH[1]}"
elif [[ "${local_ref}" =~ ^refs/tags/(v[0-9]+.*) ]]; then
tag="${BASH_REMATCH[1]}"
fi

if [[ -n "${tag}" ]]; then
echo "🔒 [pre-push hook] Release tag detected: ${tag}. Running release verification gates..."
if ! GIT_TAG="${tag}" ./scripts/check_version.sh; then
echo "❌ [pre-push hook] Push aborted! Version and release checks failed for ${tag}." >&2
echo " Run './scripts/check_tag_readiness.sh ${tag}' to diagnose and verify release requirements." >&2
exit 1
fi
echo "✅ [pre-push hook] Release tag ${tag} verified successfully."
fi
done

exit 0
Loading