diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 44e0a37..15208e2 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -36,6 +36,10 @@ on: description: "Generate & sign a keyless SLSA build provenance attestation for the pushed image (the CALLER must grant id-token: write and attestations: write)" default: false type: boolean + dockerhub-login: + description: "Log in to Docker Hub before the build, to avoid anonymous pull rate limits on base images. Ignored when `push: true` and `registry` already points at Docker Hub, since the push login covers that host" + default: true + type: boolean security-scan: description: "Enable Trivy Security Scan" default: true @@ -70,9 +74,9 @@ on: type: string secrets: dockerhub-username: - required: true + required: false dockerhub-password: - required: true + required: false username: required: false password: @@ -89,9 +93,11 @@ jobs: # avoid shell injection through string interpolation env: - PLATFORM: ${{ inputs.platform }} - OCI_IMAGE: ${{ inputs.image-name }}:${{ inputs.image-tag }} IMAGE_TAG: ${{ inputs.image-tag }} + OCI_IMAGE: ${{ inputs.image-name }}:${{ inputs.image-tag }} + PLATFORM: ${{ inputs.platform }} + PUSH: ${{ inputs.push }} + REGISTRY: ${{ inputs.registry }} REPORT_MODE: ${{ inputs.security-report }} steps: @@ -116,6 +122,43 @@ jobs: echo "✅ Security report mode validation passed: $REPORT_MODE" + # Resolves whether the Docker Hub pull login is still needed, and fails fast + # when the push credentials required by `push: true` are missing. + - name: Validate registry credentials + id: validate-credentials + env: + DOCKERHUB_LOGIN: ${{ inputs.dockerhub-login }} + DOCKERHUB_USERNAME: ${{ secrets.dockerhub-username }} + DOCKERHUB_PASSWORD: ${{ secrets.dockerhub-password }} + REGISTRY_USERNAME: ${{ secrets.username }} + REGISTRY_PASSWORD: ${{ secrets.password }} + run: | + set -euo pipefail + + if [ "$PUSH" = "true" ] && { [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_PASSWORD" ]; }; then + echo "❌ Error: push is enabled but 'username'/'password' are missing" + echo "Pass the secrets of the target registry ($REGISTRY), or set push: false" + exit 1 + fi + + NEEDS_DOCKERHUB_LOGIN=false + if [ "$DOCKERHUB_LOGIN" != "true" ]; then + echo "✅ Docker Hub login disabled by input" + elif [ "$PUSH" = "true" ] && { [ "$REGISTRY" = "docker.io" ] || [ "$REGISTRY" = "index.docker.io" ] || [ "$REGISTRY" = "registry-1.docker.io" ]; }; then + # The Docker Registry login below would authenticate the same host as + # Docker Hub login and would just overwrite credentials in ~/.docker/config.json. + # So pushing to Docker Hub does not require the Docker Hub secrets at all. + echo "✅ Skipping the Docker Hub login: pushing to $REGISTRY already authenticates that host" + elif [ -z "$DOCKERHUB_USERNAME" ] || [ -z "$DOCKERHUB_PASSWORD" ]; then + echo "❌ Error: dockerhub-login is enabled but 'dockerhub-username'/'dockerhub-password' are missing" + echo "Pass the Docker Hub secrets, or set dockerhub-login: false" + exit 1 + else + NEEDS_DOCKERHUB_LOGIN=true + fi + + echo "needs-dockerhub-login=$NEEDS_DOCKERHUB_LOGIN" >> "$GITHUB_OUTPUT" + - name: Checkout Repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -131,7 +174,10 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + # Only needed to raise the pull rate limit on Docker Hub hosted base images + # when push `registry` is not Docker Hub. - name: Login to Docker Hub + if: ${{ steps.validate-credentials.outputs.needs-dockerhub-login == 'true' }} uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ secrets.dockerhub-username }} @@ -279,6 +325,7 @@ jobs: run: docker push "$OCI_IMAGE" - name: Record the pushed image tag + if: ${{ inputs.push }} run: | echo "$IMAGE_TAG" > image-tag.txt diff --git a/docker-build/README.md b/docker-build/README.md index 694d463..64f2d4b 100644 --- a/docker-build/README.md +++ b/docker-build/README.md @@ -6,13 +6,13 @@ This reusable GitHub Actions workflow automates the process of building and push ## ✨ Features -- 🔐 Securely authenticates with a Docker Registry using best practices -- 🏗️ Builds optimized Docker images from a specified Dockerfile -- 🏷️ Intelligently tags and pushes images to a Docker Registry +- 🔐 Securely authenticates with a Docker Registry using best practices, using credentials stored as GitHub Secrets +- 🧭 Keeps the Docker Hub pull login and the registry push login separate, and skips the former when the target registry already is Docker Hub +- 🏭 Builds optimized Docker images from a specified Dockerfile +- 🔖 Intelligently tags and pushes images to a Docker Registry - 🔎 Scan for vulnerabilities with Trivy - 👍 Lint dockerfile - 📜 Generates & signs a keyless SLSA build provenance attestation (optional) -- 🛡️ Handles authentication securely using GitHub Secrets - 🚀 Optimizes build performance with layer caching - 📦 Supports AMD64 and ARM64 platforms (one per workflow run) - 📥 Optionally pulls a build artifact from an earlier job into the build context, so a compiled binary or jar can be built once and copied into the image @@ -29,6 +29,7 @@ This reusable GitHub Actions workflow automates the process of building and push | `build-args` | Docker build arguments (multiline format: `KEY1=value1\nKEY2=value2`) | No | `""` | | `context` | Path to Docker Build Context | No | `"."` | | `dockerfile` | Path to the Dockerfile to build (e.g., './Dockerfile', './docker/Dockerfile') | No | `"Dockerfile"` | +| `dockerhub-login` | Log in to Docker Hub before the build, to avoid anonymous pull rate limits on base images. Ignored when `push: true` and `registry` already points at Docker Hub, since the push login covers that host | No | `true` | | `hadolint` | Enable Hadolint | No | `true` | | `image-name` | Name of Docker Image (e.g., 'myimage', 'myorg/myimage') | true | - | | `image-tag` | Tag to apply to the built image (e.g., 'latest', 'v1.2.3') | No | `"latest"` | @@ -44,10 +45,17 @@ This reusable GitHub Actions workflow automates the process of building and push | Name | Description | Required | | --- | --- | --- | -| `dockerhub-username` | Username for Docker Hub authentication | Yes | -| `dockerhub-password` | Token for Docker Hub authentication (with read-only permissions) | Yes | | `username` | Username for Docker Registry authentication | When `push: true` | | `password` | Password or Personal Access Token for Docker registry authentication (with appropriate permissions) | When `push: true` | +| `dockerhub-username` | Username for Docker Hub authentication (pull-only, used when `dockerhub-login: true`) | When a Docker Hub login is still needed | +| `dockerhub-password` | Token for Docker Hub authentication (with read-only permissions) | When a Docker Hub login is still needed | + +The two pairs serve different purposes and are **not** interchangeable: + +- `username` / `password` authenticate the **push** against `registry`, and are the only credentials needed when `push: true`. +- `dockerhub-username` / `dockerhub-password` only raise the **pull** rate limit on base images (`FROM golang:…`, `FROM alpine:…`). + +When `push: true` and `registry` already points at Docker Hub, the push login covers that host, so the workflow **skips** the Docker Hub login and the `dockerhub-*` secrets are not needed. A second login would only overwrite the first in `~/.docker/config.json`. Set `dockerhub-login: false` to skip the pull login explicitly, e.g. when every base image comes from a private mirror. ## 📤 Outputs @@ -59,6 +67,8 @@ The built image tag, digest and checksum are visible on the workflow run summary ## 💻 Example Usage +Pushing to Docker Hub only needs the push credentials, because that login already covers pulling base images from Docker Hub: + ```yaml name: Build and Push Docker Image @@ -80,26 +90,38 @@ jobs: - name: Build and Push Docker Image uses: iExecBlockchainComputing/github-actions-workflows/.github/workflows/docker-build.yml@main # ⚠️ use tagged version here with: - image-name: "username/my-image" + image-name: "docker.io/username/my-image" + registry: "docker.io" + push: true dockerfile: "Dockerfile" build-args: | BUILD_VERSION=1.0.0 NODE_ENV=production secrets: - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN_PULL_ONLY }} username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PAT }} ``` +Pushing to another registry, such as a private Nexus, keeps the Docker Hub pull login separate so base images are still fetched above the anonymous rate limit: + +```yaml +- name: Build and Push Docker Image + uses: iExecBlockchainComputing/github-actions-workflows/.github/workflows/docker-build.yml@main # ⚠️ use tagged version here + with: + image-name: "docker-regis.iex.ec/my-image" + registry: "docker-regis.iex.ec" + push: true + secrets: + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN_PULL_ONLY }} + username: ${{ secrets.NEXUS_USERNAME }} + password: ${{ secrets.NEXUS_PASSWORD }} +``` + ## 📝 Notes -- 🔒 Ensure your Docker Registry credentials are stored securely as GitHub Secrets -- 🔄 The workflow will automatically handle the Docker build and push process -- 🏷️ You can specify any valid Docker tag format in the `tag` input +- 🔖 You can specify any valid Docker tag format in the `image-tag` input - 📤 When `push: true`, the applied tag is also saved as a `docker-image-tag` artifact, so `docker-promote` can promote the exact image CI built for a commit rather than reconstruct its tag -- 📅 Consider using dynamic tags based on git tags, commit SHAs, or dates -- 🧪 For testing purposes, you can use the `--dry-run` flag in your own implementation - 📜 When `attest: true`, the attestation is only generated if `push: true`, and the **caller** workflow must grant the following permissions: ```yaml permissions: @@ -110,6 +132,9 @@ jobs: ## 🛠️ Troubleshooting - If you encounter authentication issues, verify your Docker Registry credentials are correct and have appropriate permissions +- ❌ **`push is enabled but 'username'/'password' are missing`** — pass the credentials of `registry`, or set `push: false` +- ❌ **`dockerhub-login is enabled but 'dockerhub-username'/'dockerhub-password' are missing`** — the pull login is requested but no credential was given; pass them, or set `dockerhub-login: false` when base images come from a private mirror. Not raised when pushing to Docker Hub, since that login is skipped as redundant +- ❌ **Base image pull fails with a rate limit error** — set `dockerhub-login: true` and pass the `dockerhub-*` secrets - For build failures, check your Dockerfile syntax and ensure all referenced files exist - Large images may take longer to push - consider optimizing your Dockerfile with multi-stage builds - If you need to debug the build process, you can add the `ACTIONS_STEP_DEBUG` secret set to `true` in your repository diff --git a/docker-build/workflow-sha256 b/docker-build/workflow-sha256 index dd143ec..d792990 100644 --- a/docker-build/workflow-sha256 +++ b/docker-build/workflow-sha256 @@ -1 +1 @@ -eda9395589dd24e9b931f836a515d6733a82df11438abc850ae0220d9c49fc9e +38bec74c032cfdf66b75251458deb6ef8c0ba1fbc10307049d3311fa8538648f