Files
git.stella-ops.org/.gitea/workflows/release-suite.yml
2025-12-26 18:11:06 +02:00

684 lines
25 KiB
YAML

# .gitea/workflows/release-suite.yml
# Full suite release pipeline with Ubuntu-style versioning (YYYY.MM)
# Sprint: SPRINT_20251226_005_CICD
name: Suite Release
on:
workflow_dispatch:
inputs:
version:
description: 'Suite version (YYYY.MM format, e.g., 2026.04)'
required: true
type: string
codename:
description: 'Release codename (e.g., Nova, Orion, Pulsar)'
required: true
type: string
channel:
description: 'Release channel'
required: true
type: choice
options:
- edge
- stable
- lts
default: edge
skip_tests:
description: 'Skip test execution (use with caution)'
type: boolean
default: false
dry_run:
description: 'Dry run (build but do not publish)'
type: boolean
default: false
push:
tags:
- 'suite-*' # e.g., suite-2026.04
env:
DOTNET_VERSION: '10.0.100'
DOTNET_NOLOGO: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
REGISTRY: git.stella-ops.org
NUGET_SOURCE: https://git.stella-ops.org/api/packages/stella-ops.org/nuget/index.json
jobs:
# ===========================================================================
# PARSE TAG (for tag-triggered builds)
# ===========================================================================
parse-tag:
name: Parse Tag
runs-on: ubuntu-22.04
if: github.event_name == 'push'
outputs:
version: ${{ steps.parse.outputs.version }}
codename: ${{ steps.parse.outputs.codename }}
channel: ${{ steps.parse.outputs.channel }}
steps:
- name: Parse version from tag
id: parse
run: |
TAG="${{ github.ref_name }}"
# Expected format: suite-{YYYY.MM} or suite-{YYYY.MM}-{codename}
if [[ "$TAG" =~ ^suite-([0-9]{4}\.(04|10))(-([a-zA-Z]+))?$ ]]; then
VERSION="${BASH_REMATCH[1]}"
CODENAME="${BASH_REMATCH[4]:-TBD}"
# Determine channel based on month (04 = LTS, 10 = feature)
MONTH="${BASH_REMATCH[2]}"
if [[ "$MONTH" == "04" ]]; then
CHANNEL="lts"
else
CHANNEL="stable"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "codename=$CODENAME" >> "$GITHUB_OUTPUT"
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
echo "Parsed: version=$VERSION, codename=$CODENAME, channel=$CHANNEL"
else
echo "::error::Invalid tag format. Expected: suite-YYYY.MM or suite-YYYY.MM-codename"
exit 1
fi
# ===========================================================================
# VALIDATE
# ===========================================================================
validate:
name: Validate Release
runs-on: ubuntu-22.04
needs: [parse-tag]
if: always() && (needs.parse-tag.result == 'success' || needs.parse-tag.result == 'skipped')
outputs:
version: ${{ steps.resolve.outputs.version }}
codename: ${{ steps.resolve.outputs.codename }}
channel: ${{ steps.resolve.outputs.channel }}
dry_run: ${{ steps.resolve.outputs.dry_run }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve inputs
id: resolve
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
VERSION="${{ needs.parse-tag.outputs.version }}"
CODENAME="${{ needs.parse-tag.outputs.codename }}"
CHANNEL="${{ needs.parse-tag.outputs.channel }}"
DRY_RUN="false"
else
VERSION="${{ github.event.inputs.version }}"
CODENAME="${{ github.event.inputs.codename }}"
CHANNEL="${{ github.event.inputs.channel }}"
DRY_RUN="${{ github.event.inputs.dry_run }}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "codename=$CODENAME" >> "$GITHUB_OUTPUT"
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
echo "dry_run=$DRY_RUN" >> "$GITHUB_OUTPUT"
echo "=== Suite Release Configuration ==="
echo "Version: $VERSION"
echo "Codename: $CODENAME"
echo "Channel: $CHANNEL"
echo "Dry Run: $DRY_RUN"
- name: Validate version format
run: |
VERSION="${{ steps.resolve.outputs.version }}"
if ! [[ "$VERSION" =~ ^[0-9]{4}\.(04|10)$ ]]; then
echo "::error::Invalid version format. Expected YYYY.MM where MM is 04 or 10 (e.g., 2026.04)"
exit 1
fi
- name: Validate codename
run: |
CODENAME="${{ steps.resolve.outputs.codename }}"
if [[ -z "$CODENAME" || "$CODENAME" == "TBD" ]]; then
echo "::warning::No codename provided, release will use 'TBD'"
elif ! [[ "$CODENAME" =~ ^[A-Z][a-z]+$ ]]; then
echo "::warning::Codename should be capitalized (e.g., Nova, Orion)"
fi
# ===========================================================================
# RUN TESTS (unless skipped)
# ===========================================================================
test-gate:
name: Test Gate
runs-on: ubuntu-22.04
needs: [validate]
if: github.event.inputs.skip_tests != 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Restore
run: dotnet restore src/StellaOps.sln
- name: Build
run: dotnet build src/StellaOps.sln -c Release --no-restore
- name: Run Release Tests
run: |
dotnet test src/StellaOps.sln \
--filter "Category=Unit|Category=Architecture|Category=Contract" \
--configuration Release \
--no-build \
--logger "trx;LogFileName=release-tests.trx" \
--results-directory ./TestResults
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: release-test-results
path: ./TestResults
retention-days: 14
# ===========================================================================
# BUILD MODULES (matrix strategy)
# ===========================================================================
build-modules:
name: Build ${{ matrix.module }}
runs-on: ubuntu-22.04
needs: [validate, test-gate]
if: always() && needs.validate.result == 'success' && (needs.test-gate.result == 'success' || needs.test-gate.result == 'skipped')
strategy:
fail-fast: false
matrix:
module:
- name: Authority
project: src/Authority/StellaOps.Authority.WebService/StellaOps.Authority.WebService.csproj
- name: Attestor
project: src/Attestor/StellaOps.Attestor.WebService/StellaOps.Attestor.WebService.csproj
- name: Concelier
project: src/Concelier/StellaOps.Concelier.WebService/StellaOps.Concelier.WebService.csproj
- name: Scanner
project: src/Scanner/StellaOps.Scanner.WebService/StellaOps.Scanner.WebService.csproj
- name: Policy
project: src/Policy/StellaOps.Policy.Gateway/StellaOps.Policy.Gateway.csproj
- name: Signer
project: src/Signer/StellaOps.Signer.WebService/StellaOps.Signer.WebService.csproj
- name: Excititor
project: src/Excititor/StellaOps.Excititor.WebService/StellaOps.Excititor.WebService.csproj
- name: Gateway
project: src/Gateway/StellaOps.Gateway.WebService/StellaOps.Gateway.WebService.csproj
- name: Scheduler
project: src/Scheduler/StellaOps.Scheduler.WebService/StellaOps.Scheduler.WebService.csproj
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Determine module version
id: version
run: |
MODULE_NAME="${{ matrix.module.name }}"
MODULE_LOWER=$(echo "$MODULE_NAME" | tr '[:upper:]' '[:lower:]')
# Try to read version from version.txt, fallback to 1.0.0
VERSION_FILE="src/${MODULE_NAME}/version.txt"
if [[ -f "$VERSION_FILE" ]]; then
MODULE_VERSION=$(cat "$VERSION_FILE" | tr -d '[:space:]')
else
MODULE_VERSION="1.0.0"
fi
echo "module_version=$MODULE_VERSION" >> "$GITHUB_OUTPUT"
echo "module_lower=$MODULE_LOWER" >> "$GITHUB_OUTPUT"
echo "Module: $MODULE_NAME, Version: $MODULE_VERSION"
- name: Restore
run: dotnet restore ${{ matrix.module.project }}
- name: Build
run: |
dotnet build ${{ matrix.module.project }} \
--configuration Release \
--no-restore \
-p:Version=${{ steps.version.outputs.module_version }}
- name: Pack NuGet
run: |
dotnet pack ${{ matrix.module.project }} \
--configuration Release \
--no-build \
-p:Version=${{ steps.version.outputs.module_version }} \
-p:PackageVersion=${{ steps.version.outputs.module_version }} \
--output out/packages
- name: Push NuGet
if: needs.validate.outputs.dry_run != 'true'
run: |
for nupkg in out/packages/*.nupkg; do
if [[ -f "$nupkg" ]]; then
echo "Pushing: $nupkg"
dotnet nuget push "$nupkg" \
--source "${{ env.NUGET_SOURCE }}" \
--api-key "${{ secrets.GITEA_TOKEN }}" \
--skip-duplicate
fi
done
- name: Upload NuGet artifacts
uses: actions/upload-artifact@v4
with:
name: nuget-${{ matrix.module.name }}
path: out/packages/*.nupkg
retention-days: 30
if-no-files-found: ignore
# ===========================================================================
# BUILD CONTAINERS
# ===========================================================================
build-containers:
name: Container ${{ matrix.module }}
runs-on: ubuntu-22.04
needs: [validate, build-modules]
if: needs.validate.outputs.dry_run != 'true'
strategy:
fail-fast: false
matrix:
module:
- authority
- attestor
- concelier
- scanner
- policy
- signer
- excititor
- gateway
- scheduler
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Build and push container
uses: docker/build-push-action@v5
with:
context: .
file: devops/docker/Dockerfile.platform
target: ${{ matrix.module }}
push: true
tags: |
${{ env.REGISTRY }}/stella-ops.org/${{ matrix.module }}:${{ needs.validate.outputs.version }}
${{ env.REGISTRY }}/stella-ops.org/${{ matrix.module }}:${{ needs.validate.outputs.channel }}
${{ env.REGISTRY }}/stella-ops.org/${{ matrix.module }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
labels: |
org.opencontainers.image.title=StellaOps ${{ matrix.module }}
org.opencontainers.image.version=${{ needs.validate.outputs.version }}
org.opencontainers.image.description=StellaOps ${{ needs.validate.outputs.version }} ${{ needs.validate.outputs.codename }}
org.opencontainers.image.source=https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
org.opencontainers.image.revision=${{ github.sha }}
# ===========================================================================
# BUILD CLI (multi-platform)
# ===========================================================================
build-cli:
name: CLI (${{ matrix.runtime }})
runs-on: ubuntu-22.04
needs: [validate, test-gate]
if: always() && needs.validate.result == 'success' && (needs.test-gate.result == 'success' || needs.test-gate.result == 'skipped')
strategy:
fail-fast: false
matrix:
runtime:
- linux-x64
- linux-arm64
- win-x64
- osx-x64
- osx-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
include-prerelease: true
- name: Install cross-compilation tools
if: matrix.runtime == 'linux-arm64'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends binutils-aarch64-linux-gnu
- name: Publish CLI
run: |
dotnet publish src/Cli/StellaOps.Cli/StellaOps.Cli.csproj \
--configuration Release \
--runtime ${{ matrix.runtime }} \
--self-contained true \
-p:Version=${{ needs.validate.outputs.version }}.0 \
-p:PublishSingleFile=true \
-p:PublishTrimmed=true \
-p:EnableCompressionInSingleFile=true \
--output out/cli/${{ matrix.runtime }}
- name: Create archive
run: |
VERSION="${{ needs.validate.outputs.version }}"
RUNTIME="${{ matrix.runtime }}"
CODENAME="${{ needs.validate.outputs.codename }}"
cd out/cli/$RUNTIME
if [[ "$RUNTIME" == win-* ]]; then
zip -r "../stellaops-cli-${VERSION}-${CODENAME}-${RUNTIME}.zip" .
else
tar -czvf "../stellaops-cli-${VERSION}-${CODENAME}-${RUNTIME}.tar.gz" .
fi
- name: Upload CLI artifacts
uses: actions/upload-artifact@v4
with:
name: cli-${{ needs.validate.outputs.version }}-${{ matrix.runtime }}
path: |
out/cli/*.zip
out/cli/*.tar.gz
retention-days: 90
# ===========================================================================
# BUILD HELM CHART
# ===========================================================================
build-helm:
name: Helm Chart
runs-on: ubuntu-22.04
needs: [validate]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Helm
run: |
curl -fsSL https://get.helm.sh/helm-v3.16.0-linux-amd64.tar.gz | \
tar -xzf - -C /tmp
sudo install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm
- name: Lint Helm chart
run: helm lint devops/helm/stellaops
- name: Package Helm chart
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
helm package devops/helm/stellaops \
--version "$VERSION" \
--app-version "$VERSION" \
--destination out/helm
- name: Upload Helm chart
uses: actions/upload-artifact@v4
with:
name: helm-chart-${{ needs.validate.outputs.version }}
path: out/helm/*.tgz
retention-days: 90
# ===========================================================================
# GENERATE RELEASE MANIFEST
# ===========================================================================
release-manifest:
name: Release Manifest
runs-on: ubuntu-22.04
needs: [validate, build-modules, build-cli, build-helm]
if: always() && needs.validate.result == 'success'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Generate release manifest
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
CHANNEL="${{ needs.validate.outputs.channel }}"
mkdir -p out/release
cat > out/release/suite-${VERSION}.yaml << EOF
apiVersion: stellaops.org/v1
kind: SuiteRelease
metadata:
version: "${VERSION}"
codename: "${CODENAME}"
channel: "${CHANNEL}"
date: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
gitSha: "${{ github.sha }}"
gitRef: "${{ github.ref }}"
spec:
modules:
authority: "1.0.0"
attestor: "1.0.0"
concelier: "1.0.0"
scanner: "1.0.0"
policy: "1.0.0"
signer: "1.0.0"
excititor: "1.0.0"
gateway: "1.0.0"
scheduler: "1.0.0"
platforms:
- linux-x64
- linux-arm64
- win-x64
- osx-x64
- osx-arm64
artifacts:
containers: "${{ env.REGISTRY }}/stella-ops.org/*:${VERSION}"
nuget: "${{ env.NUGET_SOURCE }}"
helm: "stellaops-${VERSION}.tgz"
EOF
echo "=== Release Manifest ==="
cat out/release/suite-${VERSION}.yaml
- name: Generate checksums
run: |
VERSION="${{ needs.validate.outputs.version }}"
cd artifacts
find . -type f \( -name "*.nupkg" -o -name "*.tgz" -o -name "*.zip" -o -name "*.tar.gz" \) \
-exec sha256sum {} \; > ../out/release/SHA256SUMS-${VERSION}.txt
echo "=== Checksums ==="
cat ../out/release/SHA256SUMS-${VERSION}.txt
- name: Upload release manifest
uses: actions/upload-artifact@v4
with:
name: release-manifest-${{ needs.validate.outputs.version }}
path: out/release
retention-days: 90
# ===========================================================================
# CREATE GITEA RELEASE
# ===========================================================================
create-release:
name: Create Gitea Release
runs-on: ubuntu-22.04
needs: [validate, build-modules, build-containers, build-cli, build-helm, release-manifest]
if: needs.validate.outputs.dry_run != 'true'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Prepare release assets
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
mkdir -p release-assets
# Copy CLI archives
find artifacts -name "*.zip" -exec cp {} release-assets/ \;
find artifacts -name "*.tar.gz" -exec cp {} release-assets/ \;
# Copy Helm chart
find artifacts -name "*.tgz" -exec cp {} release-assets/ \;
# Copy manifest and checksums
find artifacts -name "suite-*.yaml" -exec cp {} release-assets/ \;
find artifacts -name "SHA256SUMS-*.txt" -exec cp {} release-assets/ \;
ls -la release-assets/
- name: Generate release notes
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
CHANNEL="${{ needs.validate.outputs.channel }}"
cat > release-notes.md << 'EOF'
## StellaOps ${{ needs.validate.outputs.version }} "${{ needs.validate.outputs.codename }}"
### Release Information
- **Version:** ${{ needs.validate.outputs.version }}
- **Codename:** ${{ needs.validate.outputs.codename }}
- **Channel:** ${{ needs.validate.outputs.channel }}
- **Date:** $(date -u +%Y-%m-%d)
- **Git SHA:** ${{ github.sha }}
### Included Modules
| Module | Version | Container |
|--------|---------|-----------|
| Authority | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/authority:${{ needs.validate.outputs.version }}` |
| Attestor | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/attestor:${{ needs.validate.outputs.version }}` |
| Concelier | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/concelier:${{ needs.validate.outputs.version }}` |
| Scanner | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/scanner:${{ needs.validate.outputs.version }}` |
| Policy | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/policy:${{ needs.validate.outputs.version }}` |
| Signer | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/signer:${{ needs.validate.outputs.version }}` |
| Excititor | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/excititor:${{ needs.validate.outputs.version }}` |
| Gateway | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/gateway:${{ needs.validate.outputs.version }}` |
| Scheduler | 1.0.0 | `${{ env.REGISTRY }}/stella-ops.org/scheduler:${{ needs.validate.outputs.version }}` |
### CLI Downloads
| Platform | Download |
|----------|----------|
| Linux x64 | `stellaops-cli-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.codename }}-linux-x64.tar.gz` |
| Linux ARM64 | `stellaops-cli-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.codename }}-linux-arm64.tar.gz` |
| Windows x64 | `stellaops-cli-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.codename }}-win-x64.zip` |
| macOS x64 | `stellaops-cli-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.codename }}-osx-x64.tar.gz` |
| macOS ARM64 | `stellaops-cli-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.codename }}-osx-arm64.tar.gz` |
### Installation
#### Helm
```bash
helm install stellaops ./stellaops-${{ needs.validate.outputs.version }}.tgz
```
#### Docker Compose
```bash
docker compose -f devops/compose/docker-compose.yml up -d
```
---
See [CHANGELOG.md](CHANGELOG.md) for detailed changes.
EOF
- name: Create Gitea release
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
VERSION="${{ needs.validate.outputs.version }}"
CODENAME="${{ needs.validate.outputs.codename }}"
CHANNEL="${{ needs.validate.outputs.channel }}"
# Determine if prerelease
PRERELEASE_FLAG=""
if [[ "$CHANNEL" == "edge" ]]; then
PRERELEASE_FLAG="--prerelease"
fi
gh release create "suite-${VERSION}" \
--title "StellaOps ${VERSION} ${CODENAME}" \
--notes-file release-notes.md \
$PRERELEASE_FLAG \
release-assets/*
# ===========================================================================
# SUMMARY
# ===========================================================================
summary:
name: Release Summary
runs-on: ubuntu-22.04
needs: [validate, build-modules, build-containers, build-cli, build-helm, release-manifest, create-release]
if: always()
steps:
- name: Generate Summary
run: |
echo "## Suite Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Release Information" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ needs.validate.outputs.version }} |" >> $GITHUB_STEP_SUMMARY
echo "| Codename | ${{ needs.validate.outputs.codename }} |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${{ needs.validate.outputs.channel }} |" >> $GITHUB_STEP_SUMMARY
echo "| Dry Run | ${{ needs.validate.outputs.dry_run }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Job Results" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Build Modules | ${{ needs.build-modules.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Containers | ${{ needs.build-containers.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build CLI | ${{ needs.build-cli.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Build Helm | ${{ needs.build-helm.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Release Manifest | ${{ needs.release-manifest.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Create Release | ${{ needs.create-release.result || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY
- name: Check for failures
if: contains(needs.*.result, 'failure')
run: |
echo "::error::One or more release jobs failed"
exit 1