Skip to content

Release

Release #51

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Release version (semver, e.g. 1.0.0 or 1.0.0-rc.1 — no 'v' prefix)"
required: true
type: string
dry_run:
description: "Dry run — run all steps but skip tag creation and release publish"
required: false
type: boolean
default: false
permissions:
contents: write
# Only one release pipeline at a time.
concurrency:
group: release
cancel-in-progress: false
jobs:
validate:
name: Validate inputs
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
VERSION='${{ inputs.version }}'
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$ ]]; then
echo "::error::Invalid version format: '${VERSION}'. Expected semver (e.g. 1.0.0 or 1.0.0-rc.1)"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Check tag does not already exist
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Verify tag is new
run: |
TAG='${{ inputs.version }}'
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag ${TAG} already exists. Delete it first or choose a different version."
exit 1
fi
echo "✓ Tag ${TAG} is available"
- name: Verify CHANGELOG.md contains release version
run: |
VERSION='${{ inputs.version }}'
if ! grep -qP "^## \[${VERSION}\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md has no entry for version ${VERSION}. Add a '## [${VERSION}] - YYYY-MM-DD' section before releasing."
exit 1
fi
echo "✓ CHANGELOG.md contains [${VERSION}]"
lint:
name: Lint
needs: [validate]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Run linter
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: latest
test-unit:
name: Unit tests
needs: [validate]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Run unit tests
run: go test -race -count=1 ./...
test-integration-jira:
name: "Integration: Jira"
needs: [lint, test-unit]
runs-on: ubuntu-latest
env:
SORTIE_JIRA_TEST: "1"
SORTIE_JIRA_ENDPOINT: ${{ secrets.SORTIE_JIRA_ENDPOINT }}
SORTIE_JIRA_API_KEY: ${{ secrets.SORTIE_JIRA_API_KEY }}
SORTIE_JIRA_PROJECT: ${{ secrets.SORTIE_JIRA_PROJECT }}
steps:
- name: Verify Jira secrets are configured
run: |
missing=()
[ -z "$SORTIE_JIRA_ENDPOINT" ] && missing+=("SORTIE_JIRA_ENDPOINT")
[ -z "$SORTIE_JIRA_API_KEY" ] && missing+=("SORTIE_JIRA_API_KEY")
[ -z "$SORTIE_JIRA_PROJECT" ] && missing+=("SORTIE_JIRA_PROJECT")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required secrets: ${missing[*]}"
echo "Configure these in Settings → Secrets and variables → Actions"
exit 1
fi
echo "✓ All Jira secrets are present"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Run integration tests
run: go test -race -count=1 -v -run 'Integration' ./internal/tracker/jira/...
test-integration-github:
name: "Integration: GitHub"
needs: [lint, test-unit]
runs-on: ubuntu-latest
env:
SORTIE_GITHUB_TEST: "1"
SORTIE_GITHUB_TOKEN: ${{ secrets.SORTIE_GITHUB_TOKEN }}
SORTIE_GITHUB_PROJECT: sortie-ai/sortie-test
steps:
- name: Verify GitHub secrets are configured
run: |
missing=()
[ -z "$SORTIE_GITHUB_TOKEN" ] && missing+=("SORTIE_GITHUB_TOKEN")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required secrets: ${missing[*]}"
echo "Configure these in Settings → Secrets and variables → Actions"
exit 1
fi
echo "✓ All GitHub secrets are present"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Run integration tests
run: go test -race -count=1 -v -run 'Integration' ./internal/scm/github/...
test-e2e-github:
name: "E2E: GitHub orchestrator"
needs: [lint, test-unit]
runs-on: ubuntu-latest
env:
SORTIE_GITHUB_E2E: "1"
SORTIE_GITHUB_TOKEN: ${{ secrets.SORTIE_GITHUB_TOKEN }}
SORTIE_GITHUB_PROJECT: sortie-ai/sortie-test
steps:
- name: Verify GitHub token secret is configured
run: |
missing=()
[ -z "$SORTIE_GITHUB_TOKEN" ] && missing+=("SORTIE_GITHUB_TOKEN")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required secrets: ${missing[*]}"
echo "Configure SORTIE_GITHUB_TOKEN in Settings → Secrets and variables → Actions"
exit 1
fi
echo "✓ GitHub E2E token secret is present"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Run E2E tests
run: go test -race -count=1 -v -timeout 120s -run 'TestGitHubIntegration' ./internal/orchestrator/...
test-integration-claude:
name: "Integration: Claude Code"
needs: [lint, test-unit]
runs-on: ubuntu-latest
env:
SORTIE_CLAUDE_TEST: "1"
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
steps:
- name: Verify Claude Code secrets are configured
run: |
missing=()
[ -z "$ANTHROPIC_API_KEY" ] && missing+=("ANTHROPIC_API_KEY")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required secrets: ${missing[*]}"
echo "Configure these in Settings → Secrets and variables → Actions"
exit 1
fi
echo "✓ All Claude Code secrets are present"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "lts/*"
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
- name: Run integration tests
run: go test -race -count=1 -v -timeout 300s -run 'Integration' ./internal/agent/claude/...
test-integration-copilot:
name: "Integration: GitHub Copilot CLI"
needs: [lint, test-unit]
runs-on: ubuntu-latest
env:
SORTIE_COPILOT_TEST: "1"
GH_TOKEN: ${{ secrets.SORTIE_COPILOT_TOKEN }}
steps:
- name: Verify Copilot CLI secrets are configured
run: |
missing=()
[ -z "$GH_TOKEN" ] && missing+=("SORTIE_COPILOT_TOKEN")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required secrets: ${missing[*]}"
echo "Configure these in Settings → Secrets and variables → Actions"
exit 1
fi
echo "✓ All Copilot CLI secrets are present"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Set up Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: "22"
- name: Install GitHub Copilot CLI
run: npm install -g @github/copilot
- name: Run integration tests
run: go test -race -count=1 -v -timeout 300s -run 'Integration' ./internal/agent/copilot/...
release:
name: Tag & Release
needs:
- lint
- test-unit
- test-integration-jira
- test-integration-github
- test-e2e-github
- test-integration-claude
- test-integration-copilot
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Install Syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
- name: Create and push tag
if: ${{ !inputs.dry_run }}
run: |
TAG='${{ inputs.version }}'
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Release ${TAG}"
git push origin "$TAG"
echo "### Tagged \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY"
- name: Create local tag (dry run)
if: ${{ inputs.dry_run }}
run: |
TAG='${{ inputs.version }}'
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Dry run ${TAG}"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
with:
distribution: goreleaser
version: "~> v2"
args: release ${{ inputs.dry_run && '--skip=publish --skip=announce' || '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
- name: Release summary
if: ${{ !inputs.dry_run }}
run: |
echo "### Release ${{ inputs.version }} published" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Artifacts:**" >> "$GITHUB_STEP_SUMMARY"
echo "- \`sortie_${{ inputs.version }}_linux_amd64.tar.gz\`" >> "$GITHUB_STEP_SUMMARY"
echo "- \`sortie_${{ inputs.version }}_linux_arm64.tar.gz\`" >> "$GITHUB_STEP_SUMMARY"
echo "- \`sortie_${{ inputs.version }}_darwin_amd64.tar.gz\`" >> "$GITHUB_STEP_SUMMARY"
echo "- \`sortie_${{ inputs.version }}_darwin_arm64.tar.gz\`" >> "$GITHUB_STEP_SUMMARY"
echo "- \`sortie_${{ inputs.version }}_windows_amd64.zip\`" >> "$GITHUB_STEP_SUMMARY"
echo "- \`sortie_${{ inputs.version }}_windows_arm64.zip\`" >> "$GITHUB_STEP_SUMMARY"
echo "- \`checksums.txt\`" >> "$GITHUB_STEP_SUMMARY"
echo "- SBOM manifests (\`*.sbom.json\`)" >> "$GITHUB_STEP_SUMMARY"
- name: Dry run summary
if: ${{ inputs.dry_run }}
run: |
echo "### Dry run complete for ${{ inputs.version }}" >> "$GITHUB_STEP_SUMMARY"
echo "No tag was created and no release was published." >> "$GITHUB_STEP_SUMMARY"
docker:
name: Docker image
needs: [release]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Determine pre-release
id: prerelease
run: |
VERSION='${{ inputs.version }}'
if [[ "$VERSION" == *-* ]]; then
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to GHCR
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.version }}
type=semver,pattern={{major}},value=${{ inputs.version }},enable=${{ steps.prerelease.outputs.is_prerelease == 'false' }}
- name: Build and push
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ !inputs.dry_run }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
build-args: |
VERSION=${{ inputs.version }}
REVISION=${{ github.sha }}
provenance: mode=max
sbom: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate artifact attestation
if: ${{ !inputs.dry_run }}
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Docker summary
run: |
echo "### Docker image" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "${{ inputs.dry_run }}" == "true" ]; then
echo "Multi-platform image built but **not pushed** (dry run)." >> "$GITHUB_STEP_SUMMARY"
else
echo "Published to \`ghcr.io/${{ github.repository }}\`:" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo '${{ steps.meta.outputs.tags }}' >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "**Platforms:** linux/amd64, linux/arm64" >> "$GITHUB_STEP_SUMMARY"
echo "**Digest:** \`${{ steps.build.outputs.digest }}\`" >> "$GITHUB_STEP_SUMMARY"
fi