SonarQube: fix tag to 26.3.0.120487-community #51
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
| # BeeCompose OCI Artifact Publishing | |
| # | |
| # This workflow publishes Docker Compose stacks as OCI artifacts to GitHub Container Registry. | |
| # Services can be deployed directly from GHCR using: | |
| # docker compose -f oci://ghcr.io/beevelop/<service>:<version> up -d | |
| # | |
| # Triggers: | |
| # - Push to main: Publishes ALL services with 'latest' tag (ensures consistency) | |
| # - Manual dispatch: Publish specific service with optional version override | |
| # - Release: Publishes all services with release tag (e.g., v1.0.0) | |
| name: Publish OCI Artifacts | |
| on: | |
| push: | |
| branches: [main] | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| service: | |
| description: 'Service to publish (empty for all changed)' | |
| required: false | |
| type: string | |
| version_override: | |
| description: 'Override version tag (e.g., v1.0.0)' | |
| required: false | |
| type: string | |
| dry_run: | |
| description: 'Dry run - validate only, do not publish' | |
| required: false | |
| default: false | |
| type: boolean | |
| env: | |
| REGISTRY: ghcr.io | |
| REGISTRY_NAMESPACE: beevelop | |
| # Prevent concurrent publishing runs to avoid race conditions | |
| concurrency: | |
| group: publish-oci-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Default permissions (restrictive) - jobs override as needed | |
| permissions: | |
| contents: read | |
| jobs: | |
| # ============================================================================ | |
| # Job 1: Detect Changed Services | |
| # ============================================================================ | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| outputs: | |
| services: ${{ steps.detect.outputs.services }} | |
| service_count: ${{ steps.detect.outputs.service_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Detect changed services | |
| id: detect | |
| run: | | |
| set -euo pipefail | |
| echo "=== Detecting changed services ===" | |
| # If specific service provided via workflow dispatch | |
| if [[ -n "${{ inputs.service }}" ]]; then | |
| if [[ -d "services/${{ inputs.service }}" ]]; then | |
| echo "Manual trigger for: ${{ inputs.service }}" | |
| echo 'services=["${{ inputs.service }}"]' >> $GITHUB_OUTPUT | |
| echo "service_count=1" >> $GITHUB_OUTPUT | |
| exit 0 | |
| else | |
| echo "::error::Service '${{ inputs.service }}' not found" | |
| exit 1 | |
| fi | |
| fi | |
| # For push to main or release: publish ALL services | |
| # This ensures consistency - all OCI artifacts are always in sync with main | |
| # Change detection via HEAD~1 is unreliable when multiple commits are pushed | |
| if [[ "${{ github.event_name }}" == "push" || "${{ github.event_name }}" == "release" ]]; then | |
| echo "${{ github.event_name }} trigger - publishing all services" | |
| SERVICES=$(find services -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | sort | jq -R -s -c 'split("\n") | map(select(. != ""))') | |
| COUNT=$(echo "$SERVICES" | jq 'length') | |
| echo "services=$SERVICES" >> $GITHUB_OUTPUT | |
| echo "service_count=$COUNT" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Fallback for other event types (shouldn't reach here normally) | |
| echo "::error::Unexpected event type: ${{ github.event_name }}" | |
| exit 1 | |
| # ============================================================================ | |
| # Job 2: Validate OCI Compatibility | |
| # ============================================================================ | |
| validate: | |
| name: Validate | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.service_count != '0' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| service: ${{ fromJson(needs.detect-changes.outputs.services) }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Validate Compose syntax | |
| working-directory: services/${{ matrix.service }} | |
| run: | | |
| echo "=== Validating ${{ matrix.service }} ===" | |
| # Check compose syntax | |
| docker compose config > /dev/null | |
| echo "✓ Compose syntax valid" | |
| - name: Check OCI compatibility | |
| working-directory: services/${{ matrix.service }} | |
| run: | | |
| # Check for bind mounts (except docker.sock which is allowed for traefik) | |
| # Use awk to detect bind mounts and check if source is docker.sock | |
| CONFIG=$(docker compose config 2>/dev/null) | |
| # Find bind mounts that are NOT docker.sock | |
| # The config format has 'type: bind' followed by 'source: <path>' on next lines | |
| BINDS=$(echo "$CONFIG" | awk ' | |
| /type: bind/ { in_bind=1; bind_line=NR; next } | |
| in_bind && /source:/ { | |
| if ($2 !~ /docker\.sock/) { | |
| print "Bind mount at line " bind_line ": " $0 | |
| } | |
| in_bind=0 | |
| } | |
| /^[^ ]/ { in_bind=0 } | |
| ') | |
| if [[ -n "$BINDS" ]]; then | |
| echo "::error::Service contains bind mounts (not OCI compatible):" | |
| echo "$BINDS" | |
| exit 1 | |
| fi | |
| echo "✓ No bind mounts (OCI compatible)" | |
| # ============================================================================ | |
| # Job 3: Publish OCI Artifacts | |
| # ============================================================================ | |
| publish: | |
| name: Publish | |
| needs: [detect-changes, validate] | |
| if: ${{ !inputs.dry_run && needs.detect-changes.outputs.service_count != '0' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| service: ${{ fromJson(needs.detect-changes.outputs.services) }} | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Determine version | |
| id: version | |
| run: | | |
| # Version priority: | |
| # 1. Release event: use git tag | |
| # 2. Manual dispatch with override: use provided version | |
| # 3. Push to main: use 'latest' | |
| if [[ "${{ github.event_name }}" == "release" ]]; then | |
| VERSION="${{ github.event.release.tag_name }}" | |
| elif [[ -n "${{ inputs.version_override }}" ]]; then | |
| VERSION="${{ inputs.version_override }}" | |
| # Normalize: add 'v' prefix if starts with number | |
| [[ "$VERSION" =~ ^[0-9] ]] && VERSION="v$VERSION" | |
| else | |
| VERSION="latest" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Publishing version: $VERSION" | |
| - name: Publish OCI artifact | |
| working-directory: services/${{ matrix.service }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| IMAGE="${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.service }}" | |
| echo "=== Publishing ${IMAGE}:${VERSION} ===" | |
| # Publish the determined version tag | |
| # --with-env includes environment variables with defaults in the OCI artifact | |
| # -y auto-confirms prompts in non-interactive CI environment | |
| # Using 'yes |' to pipe 'y' responses for bind mount prompts (workaround for docker/compose#13148) | |
| yes | docker compose publish --with-env -y "${IMAGE}:${VERSION}" | |
| echo "✓ Published ${IMAGE}:${VERSION}" | |
| # On release, also publish 'latest' tag alongside the version tag | |
| if [[ "${{ github.event_name }}" == "release" ]]; then | |
| yes | docker compose publish --with-env -y "${IMAGE}:latest" | |
| echo "✓ Published ${IMAGE}:latest" | |
| fi | |
| - name: Generate summary | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| IMAGE="${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ matrix.service }}" | |
| cat >> $GITHUB_STEP_SUMMARY << EOF | |
| ### ✅ Published: ${{ matrix.service }} | |
| | Property | Value | | |
| |----------|-------| | |
| | Service | \`${{ matrix.service }}\` | | |
| | Version | \`${VERSION}\` | | |
| | Image | \`${IMAGE}\` | | |
| **Deploy with:** | |
| \`\`\`bash | |
| docker compose -f oci://${IMAGE}:${VERSION} up -d | |
| \`\`\` | |
| EOF | |
| # ============================================================================ | |
| # Job 4: Summary | |
| # ============================================================================ | |
| summary: | |
| name: Summary | |
| needs: [detect-changes, validate, publish] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate pipeline summary | |
| run: | | |
| cat >> $GITHUB_STEP_SUMMARY << 'EOF' | |
| ## OCI Publishing Summary | |
| | Metric | Value | | |
| |--------|-------| | |
| | Services detected | ${{ needs.detect-changes.outputs.service_count }} | | |
| | Validation | ${{ needs.validate.result }} | | |
| | Publishing | ${{ needs.publish.result }} | | |
| ### Usage | |
| Deploy any published service: | |
| ```bash | |
| # Create environment file | |
| cat > .env << 'ENVEOF' | |
| COMPOSE_PROJECT_NAME=myapp | |
| SERVICE_DOMAIN=app.example.com | |
| # Add service-specific variables... | |
| ENVEOF | |
| # Deploy from OCI | |
| docker compose -f oci://ghcr.io/beevelop/<service>:<version> --env-file .env up -d | |
| ``` | |
| ### Available Services | |
| Browse all artifacts at: https://github.com/orgs/beevelop/packages | |
| EOF |