From 948efc7c91d6f965e663219d5aa5523b24ac9672 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Mon, 12 Oct 2026 14:15:23 +0200 Subject: [PATCH] feat(docker-build): make the Docker Hub pull login opt-out and skip it when pushing to Docker Hub The workflow required the `dockerhub-username`/`dockerhub-password` secrets on every call while pushing through `registry` + `username`/`password`. Two distinct concerns were conflated: - the Docker Hub login only exists to lift the anonymous pull rate limit on base images; - `username`/`password` authenticate the push against `registry`. When pushing to Docker Hub, both logins hit the same host, and the second one overwrites the first one. Worse, callers that only pass the push credentials were rejected at workflow validation time, because the Docker Hub secrets were declared `required: true`. Every change is a relaxation, so this is additive rather than breaking. --- .github/workflows/docker-build.yml | 55 +++++++++++++++++++++++++++--- docker-build/README.md | 53 ++++++++++++++++++++-------- docker-build/workflow-sha256 | 2 +- 3 files changed, 91 insertions(+), 19 deletions(-) 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