Release #59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/... | |
| test-integration-codex: | |
| name: "Integration: OpenAI Codex" | |
| needs: [lint, test-unit] | |
| runs-on: ubuntu-latest | |
| env: | |
| SORTIE_CODEX_TEST: "1" | |
| CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }} | |
| steps: | |
| - name: Verify Codex secrets are configured | |
| run: | | |
| missing=() | |
| [ -z "$CODEX_API_KEY" ] && missing+=("CODEX_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 Codex 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 OpenAI Codex CLI | |
| run: npm install -g @openai/codex | |
| - name: Run integration tests | |
| run: go test -race -count=1 -v -timeout 300s -run 'Integration' ./internal/agent/codex/... | |
| release: | |
| name: Tag & Release | |
| needs: | |
| - lint | |
| - test-unit | |
| - test-integration-jira | |
| - test-integration-github | |
| - test-e2e-github | |
| - test-integration-claude | |
| - test-integration-copilot | |
| - test-integration-codex | |
| 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 build date | |
| run: echo "BUILD_DATE=$(date -u +%Y-%m-%d)" >> "$GITHUB_ENV" | |
| - 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@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.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 }} | |
| BUILD_DATE=${{ env.BUILD_DATE }} | |
| 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 |