diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 613f005..181c95f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -20,7 +20,7 @@ on: workflow_call: inputs: images: - description: "Image repositories to push, one per line (registry.digitalocean.com/... and/or ghcr.io/...)" + description: "Image repositories to push, one per line (registry.digitalocean.com/..., ghcr.io/... and/or docker.io/...)" type: string required: true platforms: @@ -91,10 +91,10 @@ on: description: "DigitalOcean token or DOCR robot with write access; used when an image lives in registry.digitalocean.com" required: false DOCKERHUB_USERNAME: - description: "Docker Hub user for dhi.io base images; only read with dhi-login" + description: "Docker Hub user for dhi.io base images and docker.io pushes; only read when configured" required: false DOCKERHUB_TOKEN: - description: "Docker Hub token for dhi.io base images; only read with dhi-login" + description: "Docker Hub token for dhi.io base images and docker.io pushes; only read when configured" permissions: contents: read @@ -161,6 +161,13 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to Docker Hub + if: ${{ contains(inputs.images, 'docker.io/') }} + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 @@ -186,7 +193,7 @@ jobs: image="${image//[[:space:]]/}" [ -n "$image" ] || continue case "$image" in - registry.digitalocean.com/* | ghcr.io/*) ;; + registry.digitalocean.com/* | ghcr.io/* | docker.io/*) ;; *) echo "error: unsupported image repository: $image" >&2 exit 1 @@ -273,6 +280,13 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Log in to Docker Hub + if: ${{ contains(inputs.images, 'docker.io/') }} + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Buildx uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 diff --git a/README.md b/README.md index 18840d7..84e53ee 100644 --- a/README.md +++ b/README.md @@ -690,7 +690,8 @@ jobs: Builds one image per entry in `platforms`, pushes every platform by digest to each repository in `images`, then joins those digests into a single multi-arch manifest per repository. It logs in only where an image needs it: `registry.digitalocean.com` when a DOCR image is listed, `ghcr.io` through -the workflow token, and `dhi.io` when `dhi-login: true` and the Docker Hub secrets are provided. +the workflow token, `docker.io` when Docker Hub images are listed (using `DOCKERHUB_USERNAME` and +`DOCKERHUB_TOKEN`), and `dhi.io` when `dhi-login: true` and the Docker Hub secrets are provided. Both craft web images publish this way. `font-preview-service` fetches its font sources into the build context first, so it passes a `pre-build-command`; `artcraft-services` builds one platform on a diff --git a/tests/test_docker_build.py b/tests/test_docker_build.py new file mode 100644 index 0000000..b2657ef --- /dev/null +++ b/tests/test_docker_build.py @@ -0,0 +1,50 @@ +"""Verify the shared docker-build workflow handles Docker Hub alongside DOCR and GHCR.""" + +from pathlib import Path +import unittest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = ROOT / ".github/workflows/docker-build.yml" +WORKFLOW = yaml.load(WORKFLOW_PATH.read_text(), Loader=yaml.BaseLoader) + + +class DockerBuildWorkflowTests(unittest.TestCase): + def test_workflow_interface(self): + call = WORKFLOW["on"]["workflow_call"] + self.assertIn("images", call["inputs"]) + self.assertIn("DOCKERHUB_USERNAME", call["secrets"]) + self.assertIn("DOCKERHUB_TOKEN", call["secrets"]) + + def test_dockerhub_login_in_build_job(self): + steps = WORKFLOW["jobs"]["build"]["steps"] + dh_step = next( + (s for s in steps if s.get("name") == "Log in to Docker Hub"), + None, + ) + self.assertIsNotNone(dh_step, "Build job missing 'Log in to Docker Hub' step") + self.assertIn("docker.io/", dh_step["if"]) + self.assertEqual(dh_step["with"]["username"], "${{ secrets.DOCKERHUB_USERNAME }}") + self.assertEqual(dh_step["with"]["password"], "${{ secrets.DOCKERHUB_TOKEN }}") + + def test_dockerhub_login_in_merge_job(self): + steps = WORKFLOW["jobs"]["merge"]["steps"] + dh_step = next( + (s for s in steps if s.get("name") == "Log in to Docker Hub"), + None, + ) + self.assertIsNotNone(dh_step, "Merge job missing 'Log in to Docker Hub' step") + self.assertIn("docker.io/", dh_step["if"]) + self.assertEqual(dh_step["with"]["username"], "${{ secrets.DOCKERHUB_USERNAME }}") + self.assertEqual(dh_step["with"]["password"], "${{ secrets.DOCKERHUB_TOKEN }}") + + def test_build_step_accepts_docker_io_repository(self): + steps = WORKFLOW["jobs"]["build"]["steps"] + build_step = next(s for s in steps if s.get("name") == "Build and push by digest") + script = build_step["run"] + self.assertIn("docker.io/*", script) + self.assertIn("registry.digitalocean.com/* | ghcr.io/* | docker.io/*", script) + + +if __name__ == "__main__": + unittest.main()