name: Release on: push: tags: - "v*" env: REGISTRY: gitea-http.taildb3494.ts.net IMAGE_NAME: will/unitforge jobs: validate-release: runs-on: ubuntu-latest outputs: version: ${{ steps.extract.outputs.version }} is_prerelease: ${{ steps.extract.outputs.is_prerelease }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Extract version info id: extract run: | VERSION=${GITHUB_REF#refs/tags/} echo "version=$VERSION" >> $GITHUB_OUTPUT # Check if this is a pre-release (contains alpha, beta, rc, etc.) if [[ $VERSION =~ (alpha|beta|rc|pre) ]]; then echo "is_prerelease=true" >> $GITHUB_OUTPUT else echo "is_prerelease=false" >> $GITHUB_OUTPUT fi echo "Releasing version: $VERSION" echo "Is pre-release: ${{ steps.extract.outputs.is_prerelease }}" - name: Validate version format run: | if [[ ! "${{ steps.extract.outputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then echo "Invalid version format: ${{ steps.extract.outputs.version }}" echo "Version must follow semantic versioning (e.g., v1.0.0, v1.0.0-alpha.1)" exit 1 fi test-and-build: needs: validate-release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v3 with: version: "latest" - name: Set up Python run: uv python install 3.11 - name: Install dependencies run: | uv venv uv pip install -e ".[dev]" - name: Run full test suite run: | source .venv/bin/activate make check-all - name: Build Python package run: | source .venv/bin/activate make build - name: Upload build artifacts uses: actions/upload-artifact@v3 with: name: python-package path: dist/ build-container: needs: [validate-release, test-and-build] runs-on: ubuntu-latest outputs: image-digest: ${{ steps.build.outputs.digest }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: driver-opts: network=host - name: Log in to Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.CONTAINER_REGISTRY_USERNAME }} password: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}},enable=${{ !needs.validate-release.outputs.is_prerelease }} type=raw,value=latest,enable=${{ !needs.validate-release.outputs.is_prerelease }} - name: Verify vendor assets run: | assets=( "frontend/static/vendor/bootstrap/css/bootstrap.min.css" "frontend/static/vendor/bootstrap/js/bootstrap.bundle.min.js" "frontend/static/vendor/fontawesome/css/all.min.css" "frontend/static/vendor/fontawesome/webfonts/fa-solid-900.woff2" "frontend/static/img/osi-logo.svg" ) for asset in "${assets[@]}"; do if [ ! -f "$asset" ]; then echo "Error: Missing required asset: $asset" exit 1 fi done echo "All vendor assets verified" - name: Build and push release image id: build uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | BUILDKIT_INLINE_CACHE=1 VERSION=${{ needs.validate-release.outputs.version }} security-scan: needs: [validate-release, build-container] runs-on: ubuntu-latest steps: - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }} format: "sarif" output: "trivy-results.sarif" severity: "CRITICAL,HIGH" - name: Upload Trivy scan results uses: github/codeql-action/upload-sarif@v2 if: always() with: sarif_file: "trivy-results.sarif" - name: Fail on critical vulnerabilities uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }} format: "table" exit-code: 1 severity: "CRITICAL" create-release: needs: [validate-release, test-and-build, build-container, security-scan] runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Download build artifacts uses: actions/download-artifact@v3 with: name: python-package path: dist/ - name: Generate changelog id: changelog run: | # Extract changelog for this version VERSION=${{ needs.validate-release.outputs.version }} # Create release notes cat > release-notes.md << EOF ## UnitForge $VERSION ### Container Images - **Multi-arch support**: linux/amd64, linux/arm64 - **Registry**: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$VERSION\` - **Digest**: \`${{ needs.build-container.outputs.image-digest }}\` ### Installation #### Docker \`\`\`bash docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$VERSION docker run -p 8000:8000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$VERSION \`\`\` #### Python Package \`\`\`bash pip install unitforge==$VERSION \`\`\` ### Verification All container images are scanned for security vulnerabilities and signed for authenticity. EOF - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: body_path: release-notes.md files: | dist/* prerelease: ${{ needs.validate-release.outputs.is_prerelease }} generate_release_notes: true tag_name: ${{ needs.validate-release.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish-package: needs: [validate-release, create-release] runs-on: ubuntu-latest if: ${{ !needs.validate-release.outputs.is_prerelease }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v3 with: version: "latest" - name: Set up Python run: uv python install 3.11 - name: Download build artifacts uses: actions/download-artifact@v3 with: name: python-package path: dist/ - name: Publish to PyPI run: | uv pip install twine twine upload dist/* --non-interactive env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} deploy-production: needs: [validate-release, create-release, security-scan] runs-on: ubuntu-latest if: ${{ !needs.validate-release.outputs.is_prerelease }} environment: production steps: - name: Deploy to production run: | echo "🚀 Deploying UnitForge ${{ needs.validate-release.outputs.version }} to production" echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }}" # Add your production deployment commands here # Examples: # - Update Kubernetes manifests # - Update Helm charts # - Trigger deployment pipeline # - Update Docker Swarm services echo "✅ Production deployment completed" notify-release: needs: [validate-release, create-release, deploy-production] runs-on: ubuntu-latest if: always() steps: - name: Notify release completion run: | if [[ "${{ needs.deploy-production.result }}" == "success" ]]; then echo "🎉 UnitForge ${{ needs.validate-release.outputs.version }} has been successfully released!" echo "📦 Container image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }}" echo "🌐 Production deployment: ✅ Complete" else echo "⚠️ UnitForge ${{ needs.validate-release.outputs.version }} release completed with issues" echo "📦 Container image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.validate-release.outputs.version }}" echo "🌐 Production deployment: ❌ Failed or skipped" fi # Add notification logic here (Slack, Discord, email, etc.)