name: 🚀✨ Build on: workflow_dispatch: inputs: build_mode: description: 'Build mode' required: true default: 'rebuild' type: choice options: - rebuild - version_bump variants: description: 'Which variants to build' required: true default: 'both' type: choice options: - both - latest - edge release_type: description: 'Release type (only for version_bump mode)' required: false default: 'patch' type: choice options: - major - minor - patch schedule: - cron: '30 18 * * 0' - cron: '0 12 */3 * *' push: tags: - 'v*.*.*' permissions: contents: read env: GHCR_REGISTRY: ghcr.io GHCR_IMAGE_NAME: ${{ github.repository_owner }}/onion-relay DOCKERHUB_IMAGE_NAME: r3bo0tbx1/onion-relay jobs: determine-version: name: 🏷️ Determine Version and Build Type runs-on: ubuntu-latest permissions: contents: read outputs: version: ${{ steps.version.outputs.version }} build_type: ${{ steps.version.outputs.build_type }} is_release: ${{ steps.version.outputs.is_release }} build_date: ${{ steps.version.outputs.build_date }} short_sha: ${{ steps.version.outputs.short_sha }} build_variants: ${{ steps.version.outputs.build_variants }} steps: - name: 📥 Checkout Repository uses: actions/checkout@v5 with: fetch-depth: 0 - name: 🔍 Detect Version and Build Type id: version run: | set -e echo "🔍 Determining version context..." BUILD_VARIANTS="both" # Default: build both variants if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then VERSION="${GITHUB_REF#refs/tags/v}" BUILD_TYPE="release" IS_RELEASE="true" BUILD_VARIANTS="both" echo "🏷️ Release tag detected: v${VERSION}" elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then BUILD_MODE="${{ github.event.inputs.build_mode }}" BUILD_VARIANTS="${{ github.event.inputs.variants }}" LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0") if [[ "${BUILD_MODE}" == "rebuild" ]]; then # Rebuild mode: Use last release version (same as weekly) VERSION="${LATEST_TAG#v}" BUILD_TYPE="manual-rebuild" IS_RELEASE="false" echo "🔄 Manual rebuild of last release: ${VERSION} (with updated packages)" echo " Variants: ${BUILD_VARIANTS}" else # Version bump mode: Create new version with suffix VERSION="${LATEST_TAG#v}-manual-${GITHUB_RUN_NUMBER}" BUILD_TYPE="manual" IS_RELEASE="false" echo "👤 Manual build version: ${VERSION}" echo " Variants: ${BUILD_VARIANTS}" fi elif [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then # Scheduled rebuild: Determine which schedule based on time LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0") VERSION="${LATEST_TAG#v}" IS_RELEASE="false" CURRENT_HOUR=$(date -u +%H) if [[ "${CURRENT_HOUR}" == "18" ]]; then # Weekly rebuild (Sundays 18:30 UTC): Build stable only BUILD_TYPE="weekly" BUILD_VARIANTS="latest" echo "📅 Weekly rebuild of last release: ${VERSION} (stable variant with updated packages)" else # Edge-only rebuild (Every 3 days at 12:00 UTC): Build edge only BUILD_TYPE="edge-rebuild" BUILD_VARIANTS="edge" echo "⚡ Edge-only rebuild of last release: ${VERSION} (edge variant with updated packages)" fi else # Fallback (shouldn't happen) LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v1.0.0") VERSION="${LATEST_TAG#v}" BUILD_TYPE="unknown" IS_RELEASE="false" BUILD_VARIANTS="both" echo "⚠️ Unknown trigger: ${VERSION}" fi BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') SHORT_SHA=$(git rev-parse --short HEAD) echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "build_type=${BUILD_TYPE}" >> "$GITHUB_OUTPUT" echo "is_release=${IS_RELEASE}" >> "$GITHUB_OUTPUT" echo "build_date=${BUILD_DATE}" >> "$GITHUB_OUTPUT" echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT" echo "build_variants=${BUILD_VARIANTS}" >> "$GITHUB_OUTPUT" - name: 📋 Version Information run: | echo "📦 Build Info:" echo " 🏷️ Version: ${{ steps.version.outputs.version }}" echo " 📝 Build Type: ${{ steps.version.outputs.build_type }}" echo " ✅ Release: ${{ steps.version.outputs.is_release }}" echo " 📅 Date: ${{ steps.version.outputs.build_date }}" echo " 🔑 SHA: ${{ steps.version.outputs.short_sha }}" build-and-push: name: 🏗️ Multi-Arch Build and Push (${{ matrix.variant.name }}) runs-on: ubuntu-latest needs: determine-version permissions: contents: read packages: write if: | github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' strategy: fail-fast: false matrix: variant: - name: stable dockerfile: Dockerfile suffix: "" is_latest: "true" base: "Alpine 3.22.2" push_dockerhub: "true" - name: edge dockerfile: Dockerfile.edge suffix: "-edge" is_latest: "false" base: "Alpine edge" push_dockerhub: "true" steps: - name: 📥 Checkout Repository uses: actions/checkout@v5 - name: 🎯 Check if variant should be built id: should_build run: | BUILD_VARIANTS="${{ needs.determine-version.outputs.build_variants }}" VARIANT_NAME="${{ matrix.variant.name }}" # Determine if this variant should be built SHOULD_BUILD="false" if [ "$BUILD_VARIANTS" = "both" ]; then SHOULD_BUILD="true" elif [ "$BUILD_VARIANTS" = "latest" ] && [ "$VARIANT_NAME" = "stable" ]; then SHOULD_BUILD="true" elif [ "$BUILD_VARIANTS" = "edge" ] && [ "$VARIANT_NAME" = "edge" ]; then SHOULD_BUILD="true" fi echo "should_build=${SHOULD_BUILD}" >> "$GITHUB_OUTPUT" echo "🔍 Variant: ${VARIANT_NAME}, Build Variants: ${BUILD_VARIANTS}, Should Build: ${SHOULD_BUILD}" - name: 🎯 Verify Tools Directory if: steps.should_build.outputs.should_build == 'true' run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📝 Pre-Build: Verifying Tools" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" if [ ! -d "tools" ]; then echo "❌ tools/ directory not found" exit 1 fi TOOL_COUNT=0 for file in tools/*; do [ -f "$file" ] || continue filename=$(basename "$file") if head -1 "$file" 2>/dev/null | grep -q "^#!/bin/sh"; then echo "✅ $filename (busybox sh)" TOOL_COUNT=$((TOOL_COUNT + 1)) fi done echo "" if [ $TOOL_COUNT -lt 1 ]; then echo "❌ Build blocked: No tools found in tools/" exit 1 else echo "🎉 Found $TOOL_COUNT diagnostic tools (busybox-only, no .sh extension)" fi - name: 🔧 Normalize scripts before build if: steps.should_build.outputs.should_build == 'true' run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "🔧 Normalizing Line Endings and Permissions" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" sudo apt-get update -qq && sudo apt-get install -y dos2unix echo "📄 Processing main scripts..." for script in docker-entrypoint.sh healthcheck.sh Dockerfile Dockerfile.edge; do if [ -f "$script" ]; then dos2unix "$script" 2>/dev/null || true echo " ✅ Normalized: $script" fi done echo "" echo "📁 Processing tools/*..." if [ -d "tools" ]; then find tools -type f -exec dos2unix {} \; 2>/dev/null || true TOOL_COUNT=$(find tools -type f | wc -l) echo " ✅ Normalized: $TOOL_COUNT tool scripts" fi echo "" echo "🔐 Setting execute permissions..." chmod +x docker-entrypoint.sh healthcheck.sh 2>/dev/null || true [ -d "tools" ] && chmod +x tools/* 2>/dev/null || true echo " ✅ Permissions verified" echo "" echo "🎉 Normalization complete" - name: 🐳 Login to Docker Hub if: steps.should_build.outputs.should_build == 'true' && matrix.variant.push_dockerhub == 'true' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: 📦 Login to GitHub Container Registry if: steps.should_build.outputs.should_build == 'true' uses: docker/login-action@v3 with: registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: 🖥️ Set up QEMU if: steps.should_build.outputs.should_build == 'true' uses: docker/setup-qemu-action@v3 with: platforms: arm64,amd64 - name: 🔨 Set up Docker Buildx if: steps.should_build.outputs.should_build == 'true' uses: docker/setup-buildx-action@v3 - name: 🏷️ Generate Docker Tags if: steps.should_build.outputs.should_build == 'true' id: tags run: | VERSION="${{ needs.determine-version.outputs.version }}" BUILD_TYPE="${{ needs.determine-version.outputs.build_type }}" VARIANT_NAME="${{ matrix.variant.name }}" SUFFIX="${{ matrix.variant.suffix }}" IS_LATEST="${{ matrix.variant.is_latest }}" PUSH_DOCKERHUB="${{ matrix.variant.push_dockerhub }}" echo "🏷️ Generating tags for variant: ${VARIANT_NAME}" echo " Version: ${VERSION}${SUFFIX}" echo " Build Type: ${BUILD_TYPE}" echo " Is Latest: ${IS_LATEST}" echo " Push to Docker Hub: ${PUSH_DOCKERHUB}" echo "" TAGS=() # Always add GHCR versioned tag TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${VERSION}${SUFFIX}") if [ "$BUILD_TYPE" = "release" ]; then # New release: Add special tags if [ "$IS_LATEST" = "true" ]; then # Stable variant gets :latest TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest") else # Edge variant gets :edge TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:edge") fi # Add Docker Hub tags if [ "$PUSH_DOCKERHUB" = "true" ]; then if [ "$IS_LATEST" = "true" ]; then # Stable: versioned tag + :latest TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION}") TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:latest") else # Edge: only :edge (no versioned tag for Docker Hub) TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:edge") fi fi elif [ "$BUILD_TYPE" = "weekly" ] || [ "$BUILD_TYPE" = "manual-rebuild" ] || [ "$BUILD_TYPE" = "edge-rebuild" ]; then # Weekly rebuild, manual rebuild, or edge-only rebuild: Update version tag with fresh packages if [ "$IS_LATEST" = "true" ]; then TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:latest") else TAGS+=("${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:edge") fi if [ "$PUSH_DOCKERHUB" = "true" ]; then if [ "$IS_LATEST" = "true" ]; then # Stable: versioned tag + :latest TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION}") TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:latest") else # Edge: only :edge (no versioned tag for Docker Hub) TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:edge") fi fi else # Manual/validated builds: version tag only if [ "$PUSH_DOCKERHUB" = "true" ]; then if [ "$IS_LATEST" = "true" ]; then TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:${VERSION}") else # Edge manual builds: only :edge for Docker Hub TAGS+=("${{ env.DOCKERHUB_IMAGE_NAME }}:edge") fi fi fi TAGS_STR=$(IFS=','; echo "${TAGS[*]}") echo "tags=${TAGS_STR}" >> "$GITHUB_OUTPUT" echo "📋 Generated tags:" for tag in "${TAGS[@]}"; do echo " - ${tag}" done - name: 🚀 Build and Push Multi-Arch Image if: steps.should_build.outputs.should_build == 'true' uses: docker/build-push-action@v6 with: context: . file: ./${{ matrix.variant.dockerfile }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.tags.outputs.tags }} build-args: | BUILD_DATE=${{ needs.determine-version.outputs.build_date }} BUILD_VERSION=${{ needs.determine-version.outputs.version }} cache-from: type=gha,scope=${{ matrix.variant.name }} cache-to: type=gha,mode=max,scope=${{ matrix.variant.name }} labels: | org.opencontainers.image.title=Tor Guard Relay (${{ matrix.variant.name }}) org.opencontainers.image.description=Hardened Tor Guard Relay (${{ matrix.variant.base }}) org.opencontainers.image.version=${{ needs.determine-version.outputs.version }}${{ matrix.variant.suffix }} org.opencontainers.image.created=${{ needs.determine-version.outputs.build_date }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} sbom: true provenance: true - name: 📋 Generate SBOM (CycloneDX & SPDX) if: steps.should_build.outputs.should_build == 'true' && needs.determine-version.outputs.is_release == 'true' run: | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📋 Generating Software Bill of Materials (SBOM)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Install syft for SBOM generation curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin VERSION="${{ needs.determine-version.outputs.version }}" SUFFIX="${{ matrix.variant.suffix }}" VARIANT="${{ matrix.variant.name }}" IMAGE="${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}:${VERSION}${SUFFIX}" echo "📦 Generating SBOM for ${VARIANT} variant" echo " Image: ${IMAGE}" echo "" # Generate CycloneDX JSON echo "📄 Generating CycloneDX JSON format..." syft "${IMAGE}" -o cyclonedx-json > "sbom-${VARIANT}-cyclonedx-v${VERSION}.json" echo " ✅ sbom-${VARIANT}-cyclonedx-v${VERSION}.json" # Generate CycloneDX XML echo "📄 Generating CycloneDX XML format..." syft "${IMAGE}" -o cyclonedx-xml > "sbom-${VARIANT}-cyclonedx-v${VERSION}.xml" echo " ✅ sbom-${VARIANT}-cyclonedx-v${VERSION}.xml" # Generate SPDX JSON echo "📄 Generating SPDX JSON format..." syft "${IMAGE}" -o spdx-json > "sbom-${VARIANT}-spdx-v${VERSION}.json" echo " ✅ sbom-${VARIANT}-spdx-v${VERSION}.json" # Generate SPDX tag-value echo "📄 Generating SPDX tag-value format..." syft "${IMAGE}" -o spdx-tag-value > "sbom-${VARIANT}-spdx-v${VERSION}.spdx" echo " ✅ sbom-${VARIANT}-spdx-v${VERSION}.spdx" # Generate human-readable table echo "📄 Generating human-readable table..." syft "${IMAGE}" -o table > "sbom-${VARIANT}-table-v${VERSION}.txt" echo " ✅ sbom-${VARIANT}-table-v${VERSION}.txt" echo "" echo "✅ SBOM generation complete for ${VARIANT} variant" echo "" echo "📊 Package Statistics:" jq '.components | length' "sbom-${VARIANT}-cyclonedx-v${VERSION}.json" | xargs echo " Total packages:" - name: 📤 Upload SBOM Artifacts if: steps.should_build.outputs.should_build == 'true' && needs.determine-version.outputs.is_release == 'true' uses: actions/upload-artifact@v4 with: name: sbom-${{ matrix.variant.name }}-v${{ needs.determine-version.outputs.version }} path: | sbom-${{ matrix.variant.name }}-*.json sbom-${{ matrix.variant.name }}-*.xml sbom-${{ matrix.variant.name }}-*.spdx sbom-${{ matrix.variant.name }}-*.txt retention-days: 90 release-notes: name: 📝 Generate Release Notes runs-on: ubuntu-latest needs: [determine-version, build-and-push] permissions: contents: write if: needs.determine-version.outputs.is_release == 'true' steps: - name: 📥 Checkout Repository uses: actions/checkout@v5 - name: 📝 Generate Notes run: | VERSION="${{ needs.determine-version.outputs.version }}" GHCR_IMAGE="${{ env.GHCR_REGISTRY }}/${{ env.GHCR_IMAGE_NAME }}" DOCKERHUB_IMAGE="${{ env.DOCKERHUB_IMAGE_NAME }}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "📝 Generating Release Notes for v${VERSION}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # Try to extract from CHANGELOG.md first CHANGELOG_FOUND=0 if [ -f CHANGELOG.md ]; then echo "🔍 Checking CHANGELOG.md for v${VERSION}..." awk -v version="${VERSION}" ' $0 ~ "^##[[:space:]]*(\\[v?" version "\\]|v" version ")([[:space:]]*-.*)?$" {p=1; next} p && /^##[[:space:]]*\[/ && !($0 ~ version) {p=0} p ' CHANGELOG.md > tmp_notes.txt sed -i '/^$/N;/^\n$/D' tmp_notes.txt 2>/dev/null || true if [ -s tmp_notes.txt ]; then echo "✅ Found changelog section for v${VERSION} in CHANGELOG.md" CHANGELOG_FOUND=1 echo "## 🧅 Tor Guard Relay v${VERSION}" > release_notes.md echo "" >> release_notes.md cat tmp_notes.txt >> release_notes.md else echo "⚠️ No changelog section found for v${VERSION} in CHANGELOG.md" fi else echo "⚠️ CHANGELOG.md not found" fi # Fall back to auto-generated notes from commits if [ "$CHANGELOG_FOUND" = "0" ]; then echo "📋 Auto-generating release notes from commits..." if [ -x scripts/release/generate-release-notes.sh ]; then # Use auto-generation script chmod +x scripts/release/generate-release-notes.sh ./scripts/release/generate-release-notes.sh --format github "${VERSION}" > release_notes.md echo "✅ Auto-generated release notes from conventional commits" else # Simple fallback echo "## 🧅 Tor Guard Relay v${VERSION}" > release_notes.md echo "" >> release_notes.md echo "### Changes" >> release_notes.md echo "" >> release_notes.md git log --pretty=format:"- %s (\`%h\`) by %an" "$(git describe --tags --abbrev=0)..HEAD" >> release_notes.md || echo "- Initial release" >> release_notes.md echo "" >> release_notes.md echo "⚠️ **Note:** Release notes were auto-generated from commit history." >> release_notes.md echo "For detailed changes, see the commit history below." >> release_notes.md echo "✅ Generated basic release notes from commit history" fi fi # Append Docker images and SBOM info echo "" >> release_notes.md echo "---" >> release_notes.md echo "" >> release_notes.md echo "### 🐳 Docker Images" >> release_notes.md echo "" >> release_notes.md echo "**Stable Variant (Recommended for Production)**" >> release_notes.md echo "" >> release_notes.md echo "- Base: Alpine 3.22.2" >> release_notes.md echo "- Proven stability with weekly security updates" >> release_notes.md echo "" >> release_notes.md echo "\`\`\`bash" >> release_notes.md echo "# From GitHub Container Registry (GHCR)" >> release_notes.md echo "docker pull ${GHCR_IMAGE}:${VERSION}" >> release_notes.md echo "docker pull ${GHCR_IMAGE}:latest" >> release_notes.md echo "" >> release_notes.md echo "# From Docker Hub" >> release_notes.md echo "docker pull ${DOCKERHUB_IMAGE}:${VERSION}" >> release_notes.md echo "docker pull ${DOCKERHUB_IMAGE}:latest" >> release_notes.md echo "\`\`\`" >> release_notes.md echo "" >> release_notes.md echo "**Edge Variant (Testing Only)**" >> release_notes.md echo "" >> release_notes.md echo "- Base: Alpine edge (bleeding edge)" >> release_notes.md echo "- ⚠️ **NOT recommended for production** - faster updates, less stable" >> release_notes.md echo "" >> release_notes.md echo "\`\`\`bash" >> release_notes.md echo "# From GitHub Container Registry (GHCR) - versioned + simple tags" >> release_notes.md echo "docker pull ${GHCR_IMAGE}:${VERSION}-edge" >> release_notes.md echo "docker pull ${GHCR_IMAGE}:edge" >> release_notes.md echo "" >> release_notes.md echo "# From Docker Hub - simple tag only" >> release_notes.md echo "docker pull ${DOCKERHUB_IMAGE}:edge" >> release_notes.md echo "\`\`\`" >> release_notes.md echo "" >> release_notes.md echo "### 📋 Software Bill of Materials (SBOM)" >> release_notes.md echo "" >> release_notes.md echo "This release includes comprehensive SBOM files for **both variants** for supply chain security:" >> release_notes.md echo "" >> release_notes.md echo "- **CycloneDX**: JSON and XML formats (stable + edge)" >> release_notes.md echo "- **SPDX**: JSON and tag-value formats (stable + edge)" >> release_notes.md echo "- **Human-readable**: Table format (stable + edge)" >> release_notes.md echo "" >> release_notes.md echo "Download SBOM files from the release assets below (prefixed with \`stable-\` or \`edge-\`)." >> release_notes.md echo "" >> release_notes.md echo "---" >> release_notes.md echo "" >> release_notes.md echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/$(git describe --tags --abbrev=0 2>/dev/null || echo 'v1.0.0')...v${VERSION}" >> release_notes.md echo "" echo "✅ Release notes generation complete" - name: 📦 Download SBOM Artifacts (Stable) uses: actions/download-artifact@v4 with: name: sbom-stable-v${{ needs.determine-version.outputs.version }} path: ./sbom - name: 📦 Download SBOM Artifacts (Edge) uses: actions/download-artifact@v4 with: name: sbom-edge-v${{ needs.determine-version.outputs.version }} path: ./sbom - name: 🏷️ Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: v${{ needs.determine-version.outputs.version }} name: "🧅 Tor Guard Relay v${{ needs.determine-version.outputs.version }}" body_path: release_notes.md files: | sbom/*.json sbom/*.xml sbom/*.spdx sbom/*.txt env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}