Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
352 changes: 352 additions & 0 deletions .github/workflows/docker-build-ecr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,352 @@
name: Docker Build (AWS ECR)

# Reusable workflow for building and pushing Docker images to AWS ECR
# Uses AWS credentials for authentication via OIDC or access keys
# For other registries, use docker-build.yml instead
#
# Usage example (with OIDC):
# jobs:
# docker-build-ecr:
# permissions:
# id-token: write
# contents: read
# uses: your-org/workflows/.github/workflows/docker-build-ecr.yml@main
# with:
# images: '[{"name": "myapp", "dockerfile": "Dockerfile"}]'
# aws-region: us-east-1
# ecr-repository-prefix: my-company
# tag: ${{ github.ref_name }}
# secrets:
# aws-role-arn: ${{ secrets.AWS_ROLE_ARN }}
#
# Usage example (with access keys):
# jobs:
# docker-build-ecr:
# uses: your-org/workflows/.github/workflows/docker-build-ecr.yml@main
# with:
# images: '[{"name": "myapp", "dockerfile": "Dockerfile"}]'
# aws-region: us-east-1
# ecr-repository-prefix: my-company
# tag: ${{ github.ref_name }}
# secrets:
# aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
# aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

on:
workflow_call:
inputs:
images:
description: 'JSON array of image configurations [{"name": "app-name", "dockerfile": "path/to/Dockerfile"}]'
required: true
type: string
base-path:
description: "Base path prepended to dockerfile paths"
required: false
type: string
default: ""
build-context:
description: "Docker build context path"
required: false
type: string
default: "."
aws-region:
description: "AWS region (e.g., us-east-1, ap-northeast-2)"
required: true
type: string
ecr-registry:
description: "ECR registry URL (e.g., 123456789012.dkr.ecr.us-east-1.amazonaws.com). If not provided, will be auto-detected."
required: false
type: string
default: ""
ecr-repository-prefix:
description: "ECR repository prefix (e.g., 'mycompany' for mycompany/app-name)"
required: false
type: string
default: ""
tag:
description: "Docker image tag (e.g., v1.0.0, latest, commit-sha)"
required: true
type: string
tag-prefix:
description: "Tag prefix to remove (e.g., 'v' to convert 'v1.0.0' to '1.0.0')"
required: false
type: string
default: ""
additional-tags:
description: "Additional tags to apply (comma-separated, e.g., 'latest,stable')"
required: false
type: string
default: ""
platforms:
description: "Target platforms (comma-separated, e.g., 'linux/amd64,linux/arm64')"
required: false
type: string
default: "linux/amd64"
enable-provenance:
description: "Enable build provenance attestation"
required: false
type: boolean
default: false
enable-sbom:
description: "Enable SBOM attestation"
required: false
type: boolean
default: false
build-args:
description: "Build arguments (newline-separated KEY=value pairs)"
required: false
type: string
default: ""
create-repository:
description: "Create ECR repository if it doesn't exist"
required: false
type: boolean
default: true
secrets:
aws-role-arn:
description: "AWS IAM role ARN for OIDC authentication (preferred method)"
required: false
aws-access-key-id:
description: "AWS access key ID (alternative to OIDC)"
required: false
aws-secret-access-key:
description: "AWS secret access key (alternative to OIDC)"
required: false

permissions:
contents: read
id-token: write

jobs:
prepare:
name: Prepare Tags
runs-on: ubuntu-latest
outputs:
should-build: ${{ steps.check.outputs.should-build }}
clean-tag: ${{ steps.check.outputs.clean-tag }}
steps:
- name: Validate and process tag
id: check
run: |
TAG="${{ inputs.tag }}"
PREFIX="${{ inputs.tag-prefix }}"

# Check if tag has expected prefix
if [ -n "$PREFIX" ] && [[ ! "$TAG" == "$PREFIX"* ]]; then
echo "⚠️ Tag '$TAG' does not have expected prefix '$PREFIX'. Skipping build."
echo "should-build=false" >> "$GITHUB_OUTPUT"
exit 0
fi

# Remove prefix if present
if [ -n "$PREFIX" ]; then
CLEAN_TAG="${TAG#"$PREFIX"}"
else
CLEAN_TAG="$TAG"
fi

echo "clean-tag=${CLEAN_TAG}" >> "$GITHUB_OUTPUT"
echo "should-build=true" >> "$GITHUB_OUTPUT"
echo "✅ Clean tag: ${CLEAN_TAG}"

build-and-push:
name: Build ${{ matrix.image.name }}
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 60
if: needs.prepare.outputs.should-build == 'true' && fromJson(inputs.images) != null && fromJson(inputs.images)[0] != null
strategy:
matrix:
image: ${{ fromJson(inputs.images) }}
fail-fast: false
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ secrets.aws-role-arn }}
aws-access-key-id: ${{ secrets.aws-access-key-id }}
aws-secret-access-key: ${{ secrets.aws-secret-access-key }}
aws-region: ${{ inputs.aws-region }}

- name: Validate AWS credentials
run: |
if ! aws sts get-caller-identity &>/dev/null; then
echo "❌ Error: AWS credentials are not configured correctly"
echo "Please provide either:"
echo " - aws-role-arn (for OIDC), or"
echo " - aws-access-key-id and aws-secret-access-key"
exit 1
fi
echo "✅ AWS credentials validated"
aws sts get-caller-identity

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Determine ECR registry
id: ecr-registry
env:
PROVIDED_REGISTRY: ${{ inputs.ecr-registry }}
AUTO_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
if [ -n "$PROVIDED_REGISTRY" ]; then
REGISTRY="$PROVIDED_REGISTRY"
echo "Using provided registry: $REGISTRY"
else
REGISTRY="$AUTO_REGISTRY"
echo "Using auto-detected registry: $REGISTRY"
fi
echo "registry=$REGISTRY" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Resolve Dockerfile path
id: paths
run: |
DOCKERFILE="${{ matrix.image.dockerfile }}"
if [ -n "${{ inputs.base-path }}" ]; then
DOCKERFILE="${{ inputs.base-path }}/${DOCKERFILE}"
fi

# Validate Dockerfile exists
if [ ! -f "$DOCKERFILE" ]; then
echo "❌ Error: Dockerfile not found at: $DOCKERFILE"
exit 1
fi

echo "dockerfile=$DOCKERFILE" >> "$GITHUB_OUTPUT"
echo "✅ Dockerfile found: $DOCKERFILE"

- name: Determine ECR repository name
id: ecr-repo
env:
IMAGE_NAME: ${{ matrix.image.name }}
REPO_PREFIX: ${{ inputs.ecr-repository-prefix }}
run: |
if [ -n "$REPO_PREFIX" ]; then
ECR_REPO="${REPO_PREFIX}/${IMAGE_NAME}"
else
ECR_REPO="${IMAGE_NAME}"
fi
echo "repository=$ECR_REPO" >> "$GITHUB_OUTPUT"
echo "📦 ECR repository: $ECR_REPO"

- name: Create ECR repository if needed
if: inputs.create-repository
env:
ECR_REPO: ${{ steps.ecr-repo.outputs.repository }}
AWS_REGION: ${{ inputs.aws-region }}
run: |
if aws ecr describe-repositories --repository-names "$ECR_REPO" --region "$AWS_REGION" &>/dev/null; then
echo "✅ ECR repository '$ECR_REPO' already exists"
else
echo "📦 Creating ECR repository '$ECR_REPO'..."
aws ecr create-repository \
--repository-name "$ECR_REPO" \
--region "$AWS_REGION" \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
echo "✅ ECR repository created"
fi

- name: Generate Docker metadata
id: meta
env:
REGISTRY: ${{ steps.ecr-registry.outputs.registry }}
ECR_REPO: ${{ steps.ecr-repo.outputs.repository }}
CLEAN_TAG: ${{ needs.prepare.outputs.clean-tag }}
ADDITIONAL_TAGS: ${{ inputs.additional-tags }}
run: |
# Primary tag
TAGS="${REGISTRY}/${ECR_REPO}:${CLEAN_TAG}"

# Add 'latest' tag for semantic version releases (x.y.z format)
if [[ "$CLEAN_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
TAGS="${TAGS},${REGISTRY}/${ECR_REPO}:latest"
echo "📌 Auto-adding 'latest' tag for semver release"
fi

# Add additional tags if specified
if [ -n "$ADDITIONAL_TAGS" ]; then
IFS=',' read -ra EXTRA_TAGS <<< "$ADDITIONAL_TAGS"
for extra_tag in "${EXTRA_TAGS[@]}"; do
extra_tag=$(echo "$extra_tag" | xargs) # trim whitespace
if [ -n "$extra_tag" ]; then
TAGS="${TAGS},${REGISTRY}/${ECR_REPO}:${extra_tag}"
fi
done
fi

echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
echo "📦 Generated tags:"
echo "$TAGS" | tr ',' '\n' | sed 's/^/ - /'

- name: Build and push Docker image
id: build-push
uses: docker/build-push-action@v6
with:
context: ${{ inputs.build-context }}
file: ${{ steps.paths.outputs.dockerfile }}
platforms: ${{ inputs.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: ${{ inputs.build-args }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: ${{ inputs.enable-provenance }}
sbom: ${{ inputs.enable-sbom }}

- name: Scan image with Amazon ECR
if: success()
env:
ECR_REPO: ${{ steps.ecr-repo.outputs.repository }}
IMAGE_TAG: ${{ needs.prepare.outputs.clean-tag }}
AWS_REGION: ${{ inputs.aws-region }}
run: |
echo "🔍 Triggering ECR image scan..."
aws ecr start-image-scan \
--repository-name "$ECR_REPO" \
--image-id imageTag="$IMAGE_TAG" \
--region "$AWS_REGION" || echo "⚠️ Image scan could not be started (may already be in progress)"

- name: Generate build summary
if: always()
run: |
{
echo "### 🐳 Docker Build Results (AWS ECR): ${{ matrix.image.name }}"
echo ""
echo "**ECR Repository:** \`${{ steps.ecr-registry.outputs.registry }}/${{ steps.ecr-repo.outputs.repository }}\`"
echo "**Dockerfile:** \`${{ steps.paths.outputs.dockerfile }}\`"
echo "**AWS Region:** \`${{ inputs.aws-region }}\`"
echo "**Platforms:** \`${{ inputs.platforms }}\`"
echo ""
echo "#### Tags"
echo "\`\`\`"
echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'
echo "\`\`\`"
echo ""

if [ "${{ steps.build-push.outcome }}" = "success" ]; then
echo "**Status:** ✅ Build and push successful"
echo "**Digest:** \`${{ steps.build-push.outputs.digest }}\`"
echo ""
echo "**ECR Scan:** 🔍 Image scan triggered (check ECR console for results)"
else
echo "**Status:** ❌ Build failed"
fi

if [ "${{ inputs.enable-provenance }}" = "true" ]; then
echo "**Provenance:** ✅ Enabled"
fi
if [ "${{ inputs.enable-sbom }}" = "true" ]; then
echo "**SBOM:** ✅ Enabled"
fi
} >> "$GITHUB_STEP_SUMMARY"
Loading