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
75 changes: 75 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CI Tests

on:
pull_request:
branches:
- '**'
push:
branches:
- main

env:
GO_VERSION: 1.25.3
GOLANGCI_LINT_VERSION: v2.6.2

jobs:
golangci:
name: Golang CI lint
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}
args: -v

unit-tests-race:
name: Unit tests (race)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
- run: go mod download
- run: go test -mod=readonly ./... -count=1 -shuffle=on -race

unit-tests-flaky:
name: Unit tests (flaky)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}
- run: go mod download
- run: go test -mod=readonly ./... -count=16 -shuffle=on

summary:
name: CI Summary
runs-on: ubuntu-24.04
needs: [golangci, unit-tests-race, unit-tests-flaky]
if: always() # Always run, even if earlier jobs fail
steps:
- name: Write CI summary
run: |
{
echo "## 🧪 CI Test Summary"
echo ""
echo "- **Lint:** ${{ needs.golangci.result }}"
echo "- **Unit (race):** ${{ needs.unit-tests-race.result }}"
echo "- **Unit (flaky):** ${{ needs.unit-tests-flaky.result }}"
echo ""
echo "### Commit"
echo "[${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})"
echo ""
echo "Triggered by: @${{ github.actor }}"
echo "Event: \`${{ github.event_name }}\`"
echo ""
echo "[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
} >> "$GITHUB_STEP_SUMMARY"
302 changes: 302 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
name: Manual Release

on:
workflow_dispatch:
inputs:
version:
description: "Release version (e.g. v0.1.2 or 0.1.2)"
required: true
type: string
prerelease:
description: "Mark as pre-release?"
required: false
default: false
type: boolean

permissions:
contents: write

env:
GO_VERSION: 1.25.3
GOLANGCI_LINT_VERSION: v2.6.2

jobs:
initialize:
name: Initialize inputs
runs-on: ubuntu-24.04
outputs:
raw_input: ${{ steps.norm.outputs.raw_input }}
base_version: ${{ steps.norm.outputs.base_version }}
steps:
- name: Normalize version input
id: norm
shell: bash
run: |
set -euo pipefail
RAW_INPUT="${{ github.event.inputs.version }}"
RAW_INPUT="$(echo "$RAW_INPUT" | xargs)"
INPUT_NO_V="${RAW_INPUT#v}"

# final-only semver
if [[ "$INPUT_NO_V" =~ ^([0-9]+(\.[0-9]+){2})$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
else
echo "::error::Input must be final SemVer like 0.1.2 or v0.1.2. Do not include -rc; use prerelease checkbox instead."
exit 1
fi

echo "raw_input=$RAW_INPUT" >> "$GITHUB_OUTPUT"
echo "base_version=$BASE_VERSION" >> "$GITHUB_OUTPUT"

version-bump-check:
name: Version bump check
runs-on: ubuntu-24.04
needs: [initialize]
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Version bump check vs latest FINAL tag
shell: bash
run: |
set -euo pipefail

RAW_INPUT="${{ needs.initialize.outputs.raw_input }}"
BASE_VERSION="${{ needs.initialize.outputs.base_version }}"

# Latest FINAL tag strictly vX.Y.Z (ignore prereleases)
LATEST_FINAL_TAG=$(git tag --list 'v[0-9]*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V | tail -n1 || true)
LATEST_FINAL_TAG=${LATEST_FINAL_TAG:-v0.0.0}
OLD_FINAL_VERSION=${LATEST_FINAL_TAG#v}

echo "Latest FINAL tag: $LATEST_FINAL_TAG"
echo "Requested base: $BASE_VERSION (from input: $RAW_INPUT)"

# Require strictly newer than latest FINAL
if [[ "$BASE_VERSION" == "$OLD_FINAL_VERSION" ]]; then
echo "::error::Requested version $BASE_VERSION must be greater than latest final $OLD_FINAL_VERSION"
exit 1
fi
if [[ "$(printf '%s\n' "$OLD_FINAL_VERSION" "$BASE_VERSION" | sort -V | tail -n1)" != "$BASE_VERSION" ]]; then
echo "::error::Requested version $BASE_VERSION must be greater than latest final $OLD_FINAL_VERSION"
exit 1
fi

echo "OK: $OLD_FINAL_VERSION → $BASE_VERSION"

test-race:
name: Test race
runs-on: ubuntu-24.04
needs: [version-bump-check]
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1

- uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}

- name: Run race tests (1x -race -shuffle)
shell: bash
run: go test -mod=readonly ./... -count=1 -shuffle=on -race

test-flaky:
name: Test flaky
runs-on: ubuntu-24.04
needs: [version-bump-check]
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1

- uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}

- name: Run flaky tests (16x -shuffle)
shell: bash
run: go test -mod=readonly ./... -count=16 -shuffle=on

golangci-lint:
runs-on: ubuntu-24.04
needs: [version-bump-check]
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1

- uses: actions/setup-go@v6
with:
go-version: ${{ env.GO_VERSION }}

- name: lint
uses: golangci/golangci-lint-action@v9
with:
version: ${{ env.GOLANGCI_LINT_VERSION }}
args: -v

release:
name: Tag & release
needs: [initialize, version-bump-check, test-race, test-flaky, golangci-lint]
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.compute_tag.outputs.version }}
tag: ${{ steps.compute_tag.outputs.tag }}
is_prerelease: ${{ steps.compute_tag.outputs.is_prerelease }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Compute tag (final or next -rc.N from input)
id: compute_tag
shell: bash
run: |
set -euo pipefail

PRERELEASE="${{ github.event.inputs.prerelease }}"
BASE_VERSION="${{ needs.initialize.outputs.base_version }}"

final_exists() { git rev-parse "v${BASE_VERSION}" >/dev/null 2>&1; }

next_rc_tag() {
local base="$1"
local maxn
maxn=$(git tag --list "v${base}-rc*" \
| sed -E 's/^.*-rc\.?([0-9]+)$/\1/' \
| grep -E '^[0-9]+$' \
| sort -n | tail -n1 || true)
if [[ -z "${maxn:-}" ]]; then
echo "v${base}-rc.1"
else
echo "v${base}-rc.$((maxn+1))"
fi
}

# If a final already exists for this base, block both prerelease and final
if final_exists; then
if [[ "$PRERELEASE" == "true" ]]; then
echo "::error::Cannot create prerelease: final tag v${BASE_VERSION} already exists"
else
echo "::error::Final tag v${BASE_VERSION} already exists; choose a higher version"
fi
exit 1
fi

if [[ "$PRERELEASE" == "true" ]]; then
TAG="$(next_rc_tag "$BASE_VERSION")"
IS_PRE="true"
else
TAG="v${BASE_VERSION}"
IS_PRE="false"
fi

# Final guard: do not reuse an existing tag
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists"
exit 1
fi

echo "version=$BASE_VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "is_prerelease=$IS_PRE" >> "$GITHUB_OUTPUT"

echo "Computed tag: $TAG (prerelease=$IS_PRE)"

- name: Create Git tag
shell: bash
run: |
set -euo pipefail
TAG="${{ steps.compute_tag.outputs.tag }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists; skipping tag creation."
else
git tag -a "$TAG" -m "chore(release): $TAG"
git push origin "$TAG"
fi

- name: Find previous final tag (-rc.N excluded)
id: prev_final
shell: bash
run: |
set -euo pipefail
PREV=$(git tag --list 'v[0-9]*' \
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
| sort -V | tail -n2 | head -n1 || true)
echo "prev=$PREV" >> "$GITHUB_OUTPUT"

- name: Generate changelog (RC)
id: git-cliff-rc
if: ${{ steps.compute_tag.outputs.is_prerelease == 'true' }}
uses: orhun/git-cliff-action@v4
with:
args: --current --no-exec
env:
GITHUB_REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ github.token }}

- name: Generate changelog (final release)
id: git-cliff-final
if: ${{ steps.compute_tag.outputs.is_prerelease == 'false' }}
uses: orhun/git-cliff-action@v4
with:
args: ${{ steps.prev_final.outputs.prev != '' &&
format('{0}..{1} --no-exec', steps.prev_final.outputs.prev, steps.compute_tag.outputs.tag) ||
'--current --no-exec' }}
env:
GITHUB_REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ github.token }}

- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.compute_tag.outputs.tag }}
name: threadsafe ${{ steps.compute_tag.outputs.tag }}
body: ${{ steps.compute_tag.outputs.is_prerelease == 'true'
&& steps.git-cliff-rc.outputs.content
|| steps.git-cliff-final.outputs.content }}
prerelease: ${{ steps.compute_tag.outputs.is_prerelease }}
env:
GITHUB_TOKEN: ${{ github.token }}

summary:
name: Release summary
runs-on: ubuntu-24.04
needs: [initialize, version-bump-check, test-race, test-flaky, golangci-lint, release]
if: ${{ always() }} # ensure this job runs even if dependencies failed
steps:
- name: Write release summary
run: |
{
echo "## 🚀 Release Summary"
echo ""
echo "### Job Status"
echo "- Initialize: ${{ needs.initialize.result }}"
echo "- Version bump: ${{ needs['version-bump-check'].result }}"
echo "- Tests (race): ${{ needs['test-race'].result }}"
echo "- Tests (flaky): ${{ needs['test-flaky'].result }}"
echo "- Lint: ${{ needs.golangci-lint.result }}"
echo "- Release: ${{ needs.release.result }}"
echo ""
# Only print release details/links if the release job succeeded and produced outputs
if [ "${{ needs.release.result }}" = "success" ] && [ -n "${{ needs.release.outputs.tag }}" ]; then
echo "### Release"
echo "- **Version:** \`${{ needs.release.outputs.version }}\`"
echo "- **Tag:** \`${{ needs.release.outputs.tag }}\`"
echo "- **Prerelease:** \`${{ needs.release.outputs.is_prerelease }}\`"
echo "- **Release page:** [${{ needs.release.outputs.tag }}](${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ needs.release.outputs.tag }})"
else
echo "> Release not created (see job statuses above)."
fi
echo ""
echo "Triggered by: @${{ github.actor }}"
echo "Commit: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})"
echo "[View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
} >> "$GITHUB_STEP_SUMMARY"