Skip to content

Commit ccc5bbe

Browse files
committed
Support mult arch image
1 parent 39ea756 commit ccc5bbe

File tree

2 files changed

+190
-49
lines changed

2 files changed

+190
-49
lines changed

.github/workflows/build.yml

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -198,57 +198,10 @@ jobs:
198198
./build.py "${{ matrix.tag.tag }}"
199199
200200
- name: Push image
201-
shell: python {0}
201+
shell: bash
202202
id: push
203203
run: |
204-
import sys, os, json, subprocess
205-
206-
imageName = os.environ["IMAGE_NAME"]
207-
images = json.loads('${{ needs.prepare.outputs.images }}')
208-
digests = {}
209-
for image in images:
210-
for tag in ("${{ matrix.tag.tag }}", "${{ matrix.tag.tag }}-debuggable"):
211-
print(f"tagging {imageName}:{tag} to {image}:{tag}")
212-
proc = subprocess.run(
213-
["docker", "tag", f"{imageName}:{tag}", f"{image}:{tag}"]
214-
)
215-
if proc.returncode != 0:
216-
print(f"failed to tag {imageName}:{tag} to {image}:{tag}")
217-
sys.exit(1)
218-
219-
print(f"pushing {image}:{tag}")
220-
proc = subprocess.run(["docker", "push", f"{image}:{tag}"])
221-
if proc.returncode != 0:
222-
print(f"failed to push {image}:{tag}")
223-
sys.exit(1)
224-
225-
print(f"get {image}:{tag} digest")
226-
proc = subprocess.run(
227-
[
228-
"docker",
229-
"inspect",
230-
f"{image}:{tag}",
231-
"--format",
232-
"{{index .RepoDigests 0}}",
233-
],
234-
capture_output=True,
235-
)
236-
if proc.returncode != 0:
237-
print(f"failed to inspect {image}:{tag}")
238-
sys.exit(1)
239-
240-
digest = proc.stdout.decode().strip().split("@")[1]
241-
if digest:
242-
print(f"got {image}:{tag} digest: {digest}")
243-
else:
244-
print(f"failed to get {image}:{tag} digest")
245-
sys.exit(1)
246-
247-
digests[f"{image}:{tag}"] = digest
248-
249-
githubOutput = open(os.environ["GITHUB_OUTPUT"], "w")
250-
githubOutput.write(f"${{ matrix.tag.outputVar }}={json.dumps(digests)}\n")
251-
githubOutput.close()
204+
./.github/workflows/pushimage.py '${{ matrix.tag.tag }}' '${{ needs.prepare.outputs.images }}'
252205
253206
gen-attests:
254207
name: Generate attestation jobs

.github/workflows/pushimage.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env python3
2+
3+
import sys
4+
import os
5+
import json
6+
import subprocess
7+
import re
8+
from typing import Union
9+
10+
AUTH_FILE = os.path.expanduser("~/.docker/config.json")
11+
imageNameRe = re.compile(r"^(?P<registry>[^/]+)/(?P<image>[^:@]+)$")
12+
13+
14+
def exec(cmd: list[str], capture_output: bool = False) -> Union[int, bytes]:
15+
print(f"\033[1mexecuting {cmd}\033[0m", file=sys.stderr)
16+
proc = subprocess.run(cmd, capture_output=capture_output)
17+
if proc.returncode != 0:
18+
print(f"\033[31mfailed to execute {cmd}\033[0m", file=sys.stderr)
19+
if capture_output:
20+
print(f"\033[31merror: {proc.stderr.decode()}\033[0m", file=sys.stderr)
21+
return proc.returncode
22+
return proc.stdout if capture_output else 0
23+
24+
25+
def pushimage(tag: str, images: list[str]) -> None:
26+
dockerArch = {
27+
"x86_64": "amd64",
28+
"aarch64": "arm64",
29+
}[os.uname().machine]
30+
imageName = os.environ["IMAGE_NAME"]
31+
digests = {}
32+
for image in images:
33+
for t in (tag, f"{tag}-debuggable"):
34+
# retag source image to single arch image
35+
if (
36+
exec(["docker", "tag", f"{imageName}:{t}", f"{image}:{t}-{dockerArch}"])
37+
!= 0
38+
):
39+
print(
40+
f"\033[31mfailed to tag {imageName}:{t} to {image}:{t}-{dockerArch}\033[0m",
41+
file=sys.stderr,
42+
)
43+
continue
44+
45+
# push single arch image
46+
if exec(["docker", "push", f"{image}:{t}-{dockerArch}"]) != 0:
47+
print(
48+
f"\033[31mfailed to push {image}:{t}-{dockerArch}\033[0m",
49+
file=sys.stderr,
50+
)
51+
continue
52+
53+
# get single arch image digest
54+
singleImageInfoBytes = exec(
55+
["docker", "image", "inspect", f"{image}:{t}-{dockerArch}"],
56+
capture_output=True,
57+
)
58+
if isinstance(singleImageInfoBytes, bytes):
59+
singleImageInfo = json.loads(singleImageInfoBytes.decode())
60+
else:
61+
print(
62+
f"\033[31mfailed to get single arch image digest {image}:{t}-{dockerArch}\033[0m",
63+
file=sys.stderr,
64+
)
65+
continue
66+
67+
# find this build digest
68+
thisBuildDigest = None
69+
for repoDigest in singleImageInfo[0]["RepoDigests"]:
70+
if repoDigest.startswith(f"{image}@"):
71+
thisBuildDigest = repoDigest.split("@")[1]
72+
break
73+
74+
# add single arch image digest to digests
75+
digests[f"{image}:{t}-{dockerArch}"] = thisBuildDigest
76+
77+
# get multi arch image digest
78+
multiManifestBytes = exec(
79+
["docker", "manifest", "inspect", f"{image}:{t}"],
80+
capture_output=True,
81+
)
82+
if isinstance(multiManifestBytes, bytes):
83+
multiManifest = json.loads(multiManifestBytes.decode())
84+
if (
85+
multiManifest["mediaType"]
86+
!= "application/vnd.oci.image.index.v1+json"
87+
and multiManifest["mediaType"]
88+
!= "application/vnd.docker.distribution.manifest.list.v2+json"
89+
):
90+
# it's a single arch image, make it a index
91+
if (
92+
exec(
93+
[
94+
"docker",
95+
"manifest",
96+
"create",
97+
f"{image}:{t}",
98+
f"{image}@{thisBuildDigest}",
99+
]
100+
)
101+
!= 0
102+
):
103+
print(
104+
f"\033[31mfailed to create manifest {image}:{t}\033[0m",
105+
file=sys.stderr,
106+
)
107+
continue
108+
109+
multiManifestBytes = exec(
110+
["docker", "manifest", "inspect", f"{image}:{t}"],
111+
capture_output=True,
112+
)
113+
if isinstance(multiManifestBytes, bytes):
114+
multiManifest = json.loads(multiManifestBytes.decode())
115+
else:
116+
print(
117+
f"\033[31mfailed to get multi arch image digest {image}:{t}\033[0m",
118+
file=sys.stderr,
119+
)
120+
continue
121+
122+
archImages = {
123+
manifest["platform"]["architecture"]: manifest["digest"]
124+
for manifest in multiManifest["manifests"]
125+
}
126+
archImages[dockerArch] = thisBuildDigest
127+
128+
if exec(["docker", "manifest", "rm", f"{image}:{t}"]) != 0:
129+
print(
130+
f"\033[31mfailed to delete manifest {image}:{t}\033[0m",
131+
file=sys.stderr,
132+
)
133+
continue
134+
135+
if (
136+
exec(
137+
[
138+
"docker",
139+
"manifest",
140+
"create",
141+
f"{image}:{t}",
142+
*[f"{image}@{digest}" for digest in archImages.values()],
143+
]
144+
)
145+
!= 0
146+
):
147+
print(
148+
f"\033[31mfailed to create manifest {image}:{t}\033[0m",
149+
file=sys.stderr,
150+
)
151+
continue
152+
# fixme: debug
153+
exec(["docker", "manifest", "inspect", f"{image}:{t}"])
154+
if exec(["docker", "manifest", "push", f"{image}:{t}"]) != 0:
155+
print(f"\033[31mfailed to push {image}:{t}\033[0m", file=sys.stderr)
156+
continue
157+
else:
158+
# image may not exist, just override it
159+
if (
160+
exec(
161+
[
162+
"docker",
163+
"manifest",
164+
"create",
165+
f"{image}:{t}",
166+
f"{image}@" + singleManifest["config"]["digest"],
167+
]
168+
)
169+
!= 0
170+
):
171+
print(
172+
f"\033[31mfailed to create manifest {image}:{t}\033[0m",
173+
file=sys.stderr,
174+
)
175+
continue
176+
if exec(["docker", "manifest", "push", f"{image}:{t}"]) != 0:
177+
print(f"\033[31mfailed to push {image}:{t}\033[0m", file=sys.stderr)
178+
continue
179+
180+
githubOutput = open(os.environ["GITHUB_OUTPUT"], "w")
181+
githubOutput.write(f"${{ matrix.tag.outputVar }}={json.dumps(digests)}\n")
182+
githubOutput.close()
183+
184+
185+
if __name__ == "__main__":
186+
tag = sys.argv[1]
187+
images = json.loads(sys.argv[2])
188+
pushimage(tag, images)

0 commit comments

Comments
 (0)