Skip to content

Commit 6811ff6

Browse files
authored
Add normalization to docker-bake action (#744)
1 parent b3bb878 commit 6811ff6

4 files changed

Lines changed: 97 additions & 26 deletions

File tree

.github/actions/docker-bake/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Builds per-platform images from `templates.yml` using `docker buildx bake`, uplo
77
- Authenticates to Docker Hub and/or GHCR when credentials are supplied.
88
- Runs `docker/bake-action@v6` to build or push the image targets.
99
- Reuses BuildKit cache from GHCR when credentials are provided (while still priming the GitHub Actions cache as a fallback).
10+
- Registry cache export is enabled only when `push=true`.
11+
- GHCR cache/image namespace parts are normalized for valid registry references.
1012
- Persists the bake metadata as an artifact so the merge job can create multi-arch manifests.
1113

1214
## Inputs

.github/actions/docker-bake/action.yml

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,22 @@ inputs:
1313
required: false
1414
default: ""
1515
docker-username:
16-
description: "dockerhub username"
16+
description: "Docker Hub username"
1717
default: ""
1818
required: false
1919
docker-password:
20-
description: "dockerhub password"
20+
description: "Docker Hub password"
2121
required: false
2222
ghcr-username:
23-
description: "ghcr username"
23+
description: "GHCR username"
2424
default: ""
2525
required: false
2626
ghcr-password:
27-
description: "ghcr password"
27+
description: "GHCR password"
2828
default: ""
2929
required: false
3030
push:
31-
description: "Push digests to repo"
31+
description: "Push digests to registry"
3232
default: "true"
3333
required: false
3434

@@ -55,19 +55,47 @@ runs:
5555
- name: Set up Docker Buildx
5656
uses: docker/setup-buildx-action@v3
5757

58+
- name: Normalize registry ref parts
59+
id: refs
60+
shell: bash
61+
run: |
62+
set -euo pipefail
63+
ghcr_login_user="${{ inputs.ghcr-username }}"
64+
if [[ -z "$ghcr_login_user" ]]; then
65+
ghcr_login_user="${{ github.actor }}"
66+
fi
67+
68+
ghcr_owner="${{ inputs.ghcr-username }}"
69+
# Dependabot usernames include brackets (e.g. dependabot[bot]) which are
70+
# invalid in image/cache references; fall back to repo owner namespace.
71+
if [[ -z "$ghcr_owner" || "$ghcr_owner" == *"["* || "$ghcr_owner" == *"]"* ]]; then
72+
ghcr_owner="${{ github.repository_owner }}"
73+
fi
74+
ghcr_owner="$(printf '%s' "$ghcr_owner" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')"
75+
if [[ -z "$ghcr_owner" ]]; then
76+
ghcr_owner="$(printf '%s' "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//')"
77+
fi
78+
79+
family="${{ inputs.family }}"
80+
distro="${{ inputs.distro }}"
81+
echo "ghcr_login_user=$ghcr_login_user" >> "$GITHUB_OUTPUT"
82+
echo "ghcr_owner=$ghcr_owner" >> "$GITHUB_OUTPUT"
83+
echo "family=${family,,}" >> "$GITHUB_OUTPUT"
84+
echo "distro=${distro,,}" >> "$GITHUB_OUTPUT"
85+
5886
- name: Login to Docker Hub
59-
if: ${{ inputs.docker-password }}
87+
if: ${{ inputs.docker-username && inputs.docker-password }}
6088
uses: docker/login-action@v3
6189
with:
6290
username: ${{ inputs.docker-username }}
6391
password: ${{ inputs.docker-password }}
6492

6593
- name: Log in to GHCR
66-
if: ${{ inputs.ghcr-password }}
94+
if: ${{ inputs.ghcr-password && steps.refs.outputs.ghcr_login_user }}
6795
uses: docker/login-action@v3
6896
with:
6997
registry: ghcr.io
70-
username: ${{ inputs.ghcr-username }}
98+
username: ${{ steps.refs.outputs.ghcr_login_user }}
7199
password: ${{ inputs.ghcr-password }}
72100

73101
- name: Set up Python
@@ -94,18 +122,6 @@ runs:
94122
key="${plat//\//-}"
95123
echo "platform_key=$key" >> "$GITHUB_OUTPUT"
96124
97-
- name: Normalize registry ref parts
98-
id: refs
99-
shell: bash
100-
run: |
101-
set -euo pipefail
102-
ghcr_owner="${{ inputs.ghcr-username }}"
103-
family="${{ inputs.family }}"
104-
distro="${{ inputs.distro }}"
105-
echo "ghcr_owner=${ghcr_owner,,}" >> "$GITHUB_OUTPUT"
106-
echo "family=${family,,}" >> "$GITHUB_OUTPUT"
107-
echo "distro=${distro,,}" >> "$GITHUB_OUTPUT"
108-
109125
- name: Compute bake variables
110126
id: gen
111127
shell: bash
@@ -115,9 +131,9 @@ runs:
115131
--family "${{ inputs.family }}"
116132
--distro "${{ inputs.distro }}"
117133
--platform "${{ steps.detect.outputs.platform }}"
118-
${{ inputs.ghcr-password && format('--ghcr-username "{0}"', inputs.ghcr-username) || '' }}
134+
${{ inputs.ghcr-password && format('--ghcr-username "{0}"', steps.refs.outputs.ghcr_owner) || '' }}
119135
${{ inputs.docker-password && format('--docker-username "{0}"', inputs.docker-username) || '' }}
120-
--digest
136+
${{ inputs.push == 'true' && '--digest' || '' }}
121137
)
122138
output=$("${cmd[@]}")
123139
echo "$output"
@@ -133,7 +149,7 @@ runs:
133149
${{ steps.gen.outputs.release }}-*.platform=${{ steps.detect.outputs.platform }}
134150
*.cache-to=type=gha,mode=max,scope=${{ steps.gen.outputs.group }}
135151
*.cache-from=type=gha,scope=${{ steps.gen.outputs.group }}
136-
${{ (inputs.push && inputs.ghcr-password && steps.refs.outputs.ghcr_owner) && format('*.cache-to=type=registry,ref=ghcr.io/{0}/{1}:{2}-{3}-buildcache,mode=max', steps.refs.outputs.ghcr_owner, steps.refs.outputs.family, steps.refs.outputs.distro, steps.detect.outputs.platform_key) || '' }}
152+
${{ (inputs.push == 'true' && inputs.ghcr-password && steps.refs.outputs.ghcr_owner) && format('*.cache-to=type=registry,ref=ghcr.io/{0}/{1}:{2}-{3}-buildcache,mode=max', steps.refs.outputs.ghcr_owner, steps.refs.outputs.family, steps.refs.outputs.distro, steps.detect.outputs.platform_key) || '' }}
137153
${{ (inputs.ghcr-password && steps.refs.outputs.ghcr_owner) && format('*.cache-from=type=registry,ref=ghcr.io/{0}/{1}:{2}-{3}-buildcache', steps.refs.outputs.ghcr_owner, steps.refs.outputs.family, steps.refs.outputs.distro, steps.detect.outputs.platform_key) || '' }}
138154
${{ steps.gen.outputs.set_lines }}
139155

.github/actions/docker-merge/merge_manifests.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,19 @@ def main() -> int:
220220
args = parse_args()
221221
metadata_paths = resolve_metadata_paths(args.metadata_list)
222222
target_map = collect_targets(metadata_paths)
223-
release_targets = ensure_release_targets(
224-
args.family, args.distro, target_map
225-
)
223+
try:
224+
release_targets = ensure_release_targets(
225+
args.family, args.distro, target_map
226+
)
227+
except ValueError:
228+
if args.dry_run:
229+
print(
230+
"[merge] No release targets found in metadata during dry-run; "
231+
"skipping manifest merge."
232+
)
233+
write_output("created_tags", "{}")
234+
return 0
235+
raise
226236

227237
created_tags: Dict[str, List[str]] = {}
228238
prefix = f"{args.family}-{args.distro}-"
@@ -255,6 +265,13 @@ def main() -> int:
255265
created_tags[stage_name] = tags
256266

257267
if not created_tags:
268+
if args.dry_run:
269+
print(
270+
"[merge] No mergeable digest refs found during dry-run; "
271+
"skipping manifest creation."
272+
)
273+
write_output("created_tags", "{}")
274+
return 0
258275
raise ValueError("No manifests were created.")
259276

260277
write_output("created_tags", json.dumps(created_tags))

.github/actions/docker-merge/tests/test_merge_manifests.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
import importlib.util
77
import json
8+
import os
89
import tempfile
910
import unittest
1011
from pathlib import Path
12+
from unittest.mock import patch
1113

1214

1315
def load_module():
@@ -139,6 +141,40 @@ def test_collect_targets_with_real_metadata_fixtures(self):
139141
},
140142
)
141143

144+
def test_main_dry_run_succeeds_when_release_targets_missing(self):
145+
payload = {
146+
"other-release-base": {
147+
"image.name": "ghcr.io/acme/other:base",
148+
"containerimage.digest": "sha256:abc",
149+
}
150+
}
151+
metadata_path = self._write_metadata(payload)
152+
153+
with tempfile.NamedTemporaryFile("w", delete=False) as out:
154+
output_path = out.name
155+
self.addCleanup(lambda: os.path.exists(output_path) and os.unlink(output_path))
156+
157+
argv = [
158+
"merge_manifests.py",
159+
"--family",
160+
"gz",
161+
"--distro",
162+
"harmonic-cuda",
163+
"--metadata-list",
164+
str(metadata_path),
165+
"--gh-owner",
166+
"athackst",
167+
"--dry-run",
168+
]
169+
with patch("sys.argv", argv), patch.dict(
170+
os.environ, {"GITHUB_OUTPUT": output_path}, clear=False
171+
):
172+
exit_code = MERGE_MODULE.main()
173+
174+
self.assertEqual(exit_code, 0)
175+
output_text = Path(output_path).read_text(encoding="utf-8")
176+
self.assertIn("created_tags={}", output_text)
177+
142178

143179
if __name__ == "__main__":
144180
unittest.main()

0 commit comments

Comments
 (0)