diff --git a/docker-ci-image-workflows/README.md b/docker-ci-image-workflows/README.md new file mode 100644 index 0000000..3e88f94 --- /dev/null +++ b/docker-ci-image-workflows/README.md @@ -0,0 +1,170 @@ + +# Docker CI build + +This repository contains reusable GitHub Actions located in `./global/docker-golden-image-workflows` that implement a docker build system with pr test image. + +## 🏗️ Architecture + +The workflows follow a strict promotion workflow: + +``` +PR Build → MA (Main) + ↓ ↓ +pr-* ma-* +``` + +## 📦 Available Composite Actions + +These actions are designed to be used as steps within your own jobs. + +1. **build-pr** - Builds Docker images for pull requests. +2. **build-and-promote-ma** - Builds golden images on main merge. + +## 🚀 Usage in Your Repository (using Composite Actions) + +Composite actions are more flexible as they allow you to define your own jobs and runners. + +### Example: `.github/workflows/ci-cd.yml` + +```yaml +jobs: + pr-build: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: YOUR-ORG/REPO-NAME/global/docker-golden-image-workflows/build-pr@main + with: + registry_password: ${{ secrets.GITHUB_TOKEN }} + + main-build: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: YOUR-ORG/REPO-NAME/global/docker-golden-image-workflows/build-and-promote-ma@main + with: + registry_password: ${{ secrets.GITHUB_TOKEN }} +``` + + + + +### Step 2: Configure Repository Settings + +1. **Enable GitHub Packages** + - Go to repository Settings → Packages + - Ensure package visibility is set appropriately + +2. **Set Up Production Approval** (Optional) + - Go to Settings → Environments + - Create environment: `production-approval` + - Enable "Required reviewers" + - Add reviewers who can approve production deployments + +3. **Configure Secrets** (if using private registry) + - Go to Settings → Secrets and variables → Actions + - Add `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` if not using GHCR + +## 🔧 Configuration Options + +### Common Inputs + +All actions support these inputs: + +| Input | Description | Default | Required | +|-------|-------------|---------|----------| +| `dockerfile_path` | Path to Dockerfile | `./Dockerfile` | No | +| `docker_context` | Docker build context | `.` | No | +| `registry` | Container registry URL | `ghcr.io` | No | +| `image_name` | Image name | Repository name | No | +| `build_args` | Build arguments (comma-separated) | `''` | No | +| `platforms` | Target platforms | `linux/amd64` | No | + +### Build Arguments Example + +```yaml +with: + build_args: 'NODE_ENV=production,API_VERSION=v1,BUILD_DATE=2024-01-15' +``` + +### Multi-Platform Builds + +```yaml +with: + platforms: 'linux/amd64,linux/arm64' +``` + +### Custom Registry + +```yaml +with: + registry: 'docker.io' + image_name: 'myorg/myapp' +secrets: + registry_username: ${{ secrets.DOCKER_USERNAME }} + registry_password: ${{ secrets.DOCKER_PASSWORD }} +``` + +## 🏷️ Tagging Strategy + +### PR Images +- `pr-{number}` - PR number reference +- `pr-{branch}-{sha}` - Branch and commit reference + +### MA (Main) Images +- `v{version}` - Semantic version (e.g., v0.1.2) [Primary] +- `latest` - Latest build on main +- `ma-latest` - Latest MA build (alias) +- `ma-{sha}` - Commit SHA reference +- `ma-{timestamp}` - Timestamped version (YYYYMMDD-HHmmss) + +## 📊 Action Outputs + +### build-and-promote-ma +- `image-digest` - SHA256 digest of the built image +- `image-tag` - Primary tag applied to the image +- `full-image` - Complete image reference with digest +- `version` - Version tag (e.g., v0.1.2) used for GitHub Release + +## 🔐 Security Best Practices + +1. **Use GitHub Environments** for approval gates +2. **Restrict workflow permissions** to minimum required +3. **Enable branch protection** on main branch +4. **Use CODEOWNERS** for workflow file changes +5. **Audit promotion manifests** regularly +6. **Use image digests** for production deployments + +## 🎯 Example: Complete Deployment Flow + +```bash +# 1. Developer creates PR +git checkout -b feature/new-feature +git push origin feature/new-feature +# → PR workflow builds pr-123 image + +# 2. PR merged to main +git checkout main +git merge feature/new-feature +git push origin main +# → Main workflow builds v0.1.2 and latest +``` + +## 🆘 Troubleshooting + +### Image not found +- Verify the tag exists in GitHub Container Registry +- Check workflow logs for build failures +- Ensure GITHUB_TOKEN has package write permissions + +### Approval not working +- Verify environment is configured correctly +- Check required reviewers are set +- Ensure reviewers have appropriate permissions + +## 📝 Versioning + +This actions repository follows semantic versioning. Use specific versions in production: + +```yaml +uses: YOUR-ORG/REPO-NAME/global/docker-golden-image-workflows/build-and-promote-ma@v1.0.0 +``` diff --git a/docker-ci-image-workflows/build-and-promote-ma/action.yml b/docker-ci-image-workflows/build-and-promote-ma/action.yml new file mode 100644 index 0000000..51d3d0e --- /dev/null +++ b/docker-ci-image-workflows/build-and-promote-ma/action.yml @@ -0,0 +1,242 @@ +name: 'Docker Golden Image: Build and Promote to MA' +description: 'Builds and pushes a Docker image with MA tags and exposes digest & tag information.' + +inputs: + dockerfile_path: + description: 'Path to Dockerfile' + required: false + default: './Dockerfile' + docker_context: + description: 'Docker build context path' + required: false + default: '.' + registry: + description: 'Container registry URL' + required: false + default: 'ghcr.io' + image_name: + description: 'Image name (defaults to repository name)' + required: false + default: '' + build_args: + description: 'Docker build arguments (comma-separated KEY=VALUE pairs)' + required: false + default: '' + platforms: + description: 'Target platforms for build' + required: false + default: 'linux/amd64' + registry_username: + description: 'Registry username' + required: false + registry_password: + description: 'Registry password' + required: true + +outputs: + image-digest: + description: 'Image digest' + value: ${{ steps.build.outputs.digest }} + image-tag: + description: 'Image tag' + value: ${{ steps.meta.outputs.version }} + full-image: + description: 'Full image reference' + value: ${{ steps.output.outputs.full-image }} + version: + description: 'Release version' + value: ${{ steps.version.outputs.version }} + +runs: + using: 'composite' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set image name + id: image + shell: bash + run: | + if [ -z "${{ inputs.image_name }}" ]; then + echo "name=${{ github.repository }}" >> $GITHUB_OUTPUT + else + echo "name=${{ inputs.image_name }}" >> $GITHUB_OUTPUT + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry_username || github.actor }} + password: ${{ inputs.registry_password }} + + - name: Parse build args + id: build-args + shell: bash + run: | + if [ -n "${{ inputs.build_args }}" ]; then + echo "args<> $GITHUB_OUTPUT + echo "${{ inputs.build_args }}" | tr ',' '\n' >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "args=" >> $GITHUB_OUTPUT + fi + + - name: Generate version + id: semantic-release-config + shell: bash + run: | + cat > .releaserc.json <> $GITHUB_OUTPUT + echo "notes=No automated release notes available." >> $GITHUB_OUTPUT + else + echo "version=v$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "notes<> $GITHUB_OUTPUT + echo "${{ steps.semantic-release.outputs.new_release_notes }}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ steps.image.outputs.name }} + tags: | + type=raw,value=latest + type=raw,value=${{ steps.version.outputs.version }} + type=raw,value=ma-{{date 'YYYYMMDD-HHmmss'}} + + - name: Build and push MA image + id: build + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.docker_context }} + file: ${{ inputs.dockerfile_path }} + platforms: ${{ inputs.platforms }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: ${{ steps.build-args.outputs.args }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Set outputs + id: output + shell: bash + run: | + echo "full-image=${{ inputs.registry }}/${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT + + - name: Create main image manifest + shell: bash + run: | + cat > main-image-manifest.json < 0))'), + "commit_sha": "${{ github.sha }}", + "commit_message": $(echo '${{ github.event.head_commit.message }}' | jq -R -s .), + "built_at": "${{ github.event.head_commit.timestamp }}", + "built_by": "${{ github.actor }}", + "workflow_run_id": "${{ github.run_id }}", + "workflow_run_number": "${{ github.run_number }}" + } + EOF + cat main-image-manifest.json + + - name: Create GitHub Release + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const version = '${{ steps.version.outputs.version }}'; + const notes = `${{ steps.version.outputs.notes }}`; + const manifest = JSON.parse(fs.readFileSync('main-image-manifest.json', 'utf8')); + + try { + await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: version, + name: `Release ${version}`, + body: `## Main Deployment\n\n` + + `### Changelog\n` + + `${notes}\n\n` + + `--- \n\n` + + `**Repository:** ${{ github.repository }}\n` + + `**Image Digest:** \`${manifest.digest}\`\n` + + `**Commit:** ${{ github.sha }}\n` + + `**Built by:** ${{ github.actor }}\n` + + `**Built at:** ${manifest.built_at}\n\n` + + `### Available Tags:\n` + + manifest.tags.map(tag => `- \`${tag}\``).join('\n'), + draft: false, + prerelease: false + }); + } catch (error) { + console.log('Release creation failed (tag may already exist):', error.message); + } + + - name: Upload manifest as artifact + uses: actions/upload-artifact@v4 + with: + name: main-builded-manifest.zip + path: main-image-manifest.json + retention-days: 90 + + - name: Summary + shell: bash + run: | + echo "### ✅ MA Image Built and Promoted" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY + echo "**Environment:** MA (Main/Staging)" >> $GITHUB_STEP_SUMMARY + echo "**Version:** \`${{ steps.version.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Digest:** \`${{ steps.build.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Tags:**" >> $GITHUB_STEP_SUMMARY + echo '${{ steps.meta.outputs.tags }}' | while read tag; do + [ -n "$tag" ] && echo "- \`$tag\`" >> $GITHUB_STEP_SUMMARY + done diff --git a/docker-ci-image-workflows/build-pr/action.yml b/docker-ci-image-workflows/build-pr/action.yml new file mode 100644 index 0000000..5ae1994 --- /dev/null +++ b/docker-ci-image-workflows/build-pr/action.yml @@ -0,0 +1,110 @@ +name: 'Docker Golden Image: Build PR Image' +description: 'Builds and pushes a Docker image for a Pull Request and comments on the PR with tags.' + +inputs: + dockerfile_path: + description: 'Path to Dockerfile' + required: false + default: './Dockerfile' + docker_context: + description: 'Docker build context path' + required: false + default: '.' + registry: + description: 'Container registry URL' + required: false + default: 'ghcr.io' + image_name: + description: 'Image name (defaults to repository name)' + required: false + default: '' + build_args: + description: 'Docker build arguments (comma-separated KEY=VALUE pairs)' + required: false + default: '' + platforms: + description: 'Target platforms for build' + required: false + default: 'linux/amd64' + registry_username: + description: 'Registry username' + required: false + registry_password: + description: 'Registry password' + required: true + +runs: + using: 'composite' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set image name + id: image + shell: bash + run: | + if [ -z "${{ inputs.image_name }}" ]; then + echo "name=${{ github.repository }}" >> $GITHUB_OUTPUT + else + echo "name=${{ inputs.image_name }}" >> $GITHUB_OUTPUT + fi + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.registry_username || github.actor }} + password: ${{ inputs.registry_password }} + + - name: Parse build args + id: build-args + shell: bash + run: | + if [ -n "${{ inputs.build_args }}" ]; then + echo "args<> $GITHUB_OUTPUT + echo "${{ inputs.build_args }}" | tr ',' '\n' >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + echo "args=" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ steps.image.outputs.name }} + tags: | + type=ref,event=pr,prefix=pr- + type=sha,prefix=pr-{{branch}}- + + - name: Build and push PR image + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.docker_context }} + file: ${{ inputs.dockerfile_path }} + platforms: ${{ inputs.platforms }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: ${{ steps.build-args.outputs.args }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Comment PR with image tags + uses: actions/github-script@v7 + with: + script: | + const tags = `${{ steps.meta.outputs.tags }}`.split('\n'); + const comment = `### 🐳 Docker Image Built Successfully\n\n` + + `**Image Tags:**\n${tags.map(tag => `- \`${tag}\``).join('\n')}\n\n` + + `**Pull command:**\n\`\`\`bash\ndocker pull ${tags[0]}\n\`\`\``; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); diff --git a/docker-ci-image-workflows/examples/simple-app/.github/workflows/ci-cd.yml b/docker-ci-image-workflows/examples/simple-app/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..ed69363 --- /dev/null +++ b/docker-ci-image-workflows/examples/simple-app/.github/workflows/ci-cd.yml @@ -0,0 +1,29 @@ + +name: CI/CD Pipeline + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + # Build PR images + pr-build: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Build and promote PR image (composite) + uses: pagopa/github-actions-template/docker-golden-image-workflows/build-pr@main + with: + registry_password: ${{ secrets.GITHUB_TOKEN }} + + # Build MA images on main + main-build: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Build and promote MA image (composite) + uses: pagopa/github-actions-template/docker-golden-image-workflows/build-and-promote-ma@main + with: + registry_password: ${{ secrets.GITHUB_TOKEN }} diff --git a/docker-ci-image-workflows/examples/simple-app/Dockerfile b/docker-ci-image-workflows/examples/simple-app/Dockerfile new file mode 100644 index 0000000..b7d5958 --- /dev/null +++ b/docker-ci-image-workflows/examples/simple-app/Dockerfile @@ -0,0 +1,21 @@ + +# Example Dockerfile - Replace with your actual application Dockerfile + +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci --only=production + +COPY ../.. . + +FROM node:20-alpine + +WORKDIR /app + +COPY --from=builder /app . + +EXPOSE 3000 + +CMD ["node", "index.js"] diff --git a/docker-ci-image-workflows/examples/simple-app/README.md b/docker-ci-image-workflows/examples/simple-app/README.md new file mode 100644 index 0000000..55bcb69 --- /dev/null +++ b/docker-ci-image-workflows/examples/simple-app/README.md @@ -0,0 +1,57 @@ + +# Example: Simple Application CI/CD + +This example shows how to use the golden image workflows in a single, unified CI/CD pipeline. + +## Features + +- **Automatic PR builds** when pull requests are created +- **Automatic MA builds** when code is merged to main +- +## Usage + +### Automatic Builds + +1. **Create a PR** - Automatically builds `pr-*` images +2. **Merge to main** - Automatically builds `ma-*` images + +### Manual Actions + +Go to **Actions** → **CI/CD Pipeline** → **Run workflow** + +## Setup + +1. Copy this workflow to your repository: + ```bash + mkdir -p .github/workflows + cp examples/simple-app/.github/workflows/ci-cd.yml .github/workflows/ + ``` + +2. Update the workflow to reference your golden-image-workflows repository: + ```yaml + uses: pagopa/REPO/.github/workflows/docker-build-pr.yml@main + ``` + +3. Configure production approval environment (optional): + - Settings → Environments → New environment + - Name: `production-approval` + - Add required reviewers + +4. Commit and push: + ```bash + git add .github/workflows/ci-cd.yml + git commit -m "Add CI/CD pipeline" + git push + ``` + +## Workflow Diagram + +``` +┌─────────────┐ +│ PR Open │──→ Build pr-* image +└─────────────┘ + +┌─────────────┐ +│ Merge Main │──→ Build ma-* image +└─────────────┘ +```