# .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