Skip to content

Commit 49f92e3

Browse files
wip publish multiarch images
1 parent 6bc77d1 commit 49f92e3

File tree

2 files changed

+216
-7
lines changed

2 files changed

+216
-7
lines changed

‎.github/workflows/dump.yml‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ jobs:
2828
FORCE_UPDATE: ${{inputs.FORCE_UPDATE }}
2929
steps:
3030
- name: Setup node
31-
uses: actions/setup-node@v3
31+
uses: actions/setup-node@v4
3232
with:
33-
node-version: 20
33+
node-version: 24
3434
- name: Create Script
3535
run: |
3636
var fs = require("fs");
@@ -115,10 +115,11 @@ jobs:
115115
}
116116
117117
var arm = ${{tojson(contains(matrix.runner, 'arm')) }}
118+
var archSuffix = (arm ? "-arm64" : "-amd64");
119+
120+
var friendlyTag = envMap["ImageOS"] && envMap["ImageVersion"] ? envMap["ImageOS"] + "-" + process.env.TAG + "-" + envMap["ImageVersion"] + archSuffix : null;
121+
var latestTag = envMap["ImageOS"] && envMap["ImageVersion"] ? envMap["ImageOS"] + "-" + process.env.TAG + "-latest" + archSuffix : null;
118122
119-
var friendlyTag = envMap["ImageOS"] && envMap["ImageVersion"] ? envMap["ImageOS"] + "-" + process.env.TAG + "-" + envMap["ImageVersion"] + (arm ? "-arm64" : "") : null;
120-
var latestTag = envMap["ImageOS"] && envMap["ImageVersion"] ? envMap["ImageOS"] + "-" + process.env.TAG + "-latest" + (arm ? "-arm64" : ""): null;
121-
122123
var labels ={
123124
"org.opencontainers.image.authors": ${{toJson(format('{0}/{1}', github.server_url, github.repository_owner)) }},
124125
"org.opencontainers.image.created": new Date().toISOString(),
@@ -127,7 +128,7 @@ jobs:
127128
"org.opencontainers.image.ref.name": ("ghcr.io/" + ${{toJson(github.repository_owner) }} + "/runner-images").toLowerCase(),
128129
"org.opencontainers.image.revision": ${{toJson(github.sha) }},
129130
"org.opencontainers.image.source": ${{toJson(format('{0}/{1}/tree/{2}', github.server_url, github.repository, github.sha)) }},
130-
"org.opencontainers.image.title": arm ? "runner-image-arm64" : "runner-image-amd64",
131+
"org.opencontainers.image.title": "runner-image" + archSuffix,
131132
"org.opencontainers.image.url": ${{toJson(format('{0}/{1}/tree/{2}', github.server_url, github.repository, github.sha)) }},
132133
"org.opencontainers.image.vendor": ${{toJson(github.repository_owner) }},
133134
"org.opencontainers.image.version": friendlyTag
@@ -244,7 +245,7 @@ jobs:
244245
console.log(strmanifest);
245246
246247
await refresh();
247-
var tag = process.env.TAG + (process.env.SUFFIX || '') + (arm ? "-arm64" : "");
248+
var tag = process.env.TAG + (process.env.SUFFIX || '') + archSuffix;
248249
if(envMap["ImageOS"]){
249250
tag = envMap["ImageOS"] + "-" + tag;
250251
}
@@ -254,6 +255,8 @@ jobs:
254255
} });
255256
console.log(JSON.stringify(resp.status));
256257
258+
var tags = [ tag ];
259+
257260
if(friendlyTag){
258261
await refresh();
259262
resp = await fetch("https://ghcr.io/v2/${{github.repository_owner }}/runner-images/manifests/" + friendlyTag,{method: "GET", redirect: "manual", credentials: "include", headers:{
@@ -275,8 +278,12 @@ jobs:
275278
"Authorization": "Bearer " + token
276279
} });
277280
console.log(JSON.stringify(resp.status));
281+
282+
tags.push(friendlyTag, latestTag);
278283
}
279284
}
285+
286+
fs.appendFileSync("tags-${{matrix.runner }}.txt", tags.join("\n") + "\n");
280287
281288
process.exitCode = 0;
282289
} catch(ex){
@@ -810,3 +817,35 @@ jobs:
810817
${{tojson(steps.jvm.outputs) }}
811818
]
812819
shell: bash
820+
- uses: actions/upload-artifact@v4
821+
if: always()
822+
with:
823+
path: tags-${{matrix.runner }}.txt
824+
name: tags-${{matrix.runner }}
825+
upload-manifests:
826+
if: (!cancelled())
827+
needs: dump
828+
permissions: write-all
829+
steps:
830+
- name: Checkout
831+
uses: actions/checkout@v5
832+
- name: Download All Artifacts
833+
uses: actions/download-artifact@v5
834+
with:
835+
path: tags
836+
pattern: tags-*
837+
merge-multiple: true
838+
- name: Merge tags.txt
839+
run: cat tags/*.txt > tags.txt
840+
- name: Dump tags
841+
run: cat tags.txt
842+
- name: Docker Login
843+
run: docker login -u ${{github.actor }} -p ${{github.token }} ghcr.io
844+
- name: Setup node
845+
uses: actions/setup-node@v4
846+
with:
847+
node-version: 24
848+
- name: Npm deps
849+
run: npm i @actions/exec
850+
- name: Run Merge
851+
run: node ./check-composite-up-to-date.js

‎check-composite-up-to-date.js‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env node
2+
3+
constfs=require('fs/promises');
4+
constpath=require('path');
5+
constexec=require('@actions/exec');
6+
7+
constTAG_FILE=process.env.TAGS_FILE||'tags.txt';
8+
9+
asyncfunctionrun(cmd,args,opts={}){
10+
constres=awaitexec.getExecOutput(cmd,args,{
11+
ignoreReturnCode: opts.ignoreReturnCode||false,
12+
silent: true,
13+
});
14+
returnres;
15+
}
16+
17+
functionparseNameAndTag(ref){
18+
constlastSlash=ref.lastIndexOf('/');
19+
consttagSep=ref.indexOf(':',lastSlash+1);
20+
if(tagSep===-1)return{name: ref,tag: ''};
21+
return{name: ref.slice(0,tagSep),tag: ref.slice(tagSep+1)};
22+
}
23+
24+
functionderiveBaseRef(archRef){
25+
const{ name, tag }=parseNameAndTag(archRef);
26+
if(!tag)returnnull;
27+
if(tag.endsWith('-amd64'))return`${name}:${tag.slice(0,-'-amd64'.length)}`;
28+
if(tag.endsWith('-arm64'))return`${name}:${tag.slice(0,-'-arm64'.length)}`;
29+
returnnull;
30+
}
31+
32+
functionarchFromTag(tag){
33+
if(tag.endsWith('-amd64'))return'amd64';
34+
if(tag.endsWith('-arm64'))return'arm64';
35+
returnnull;
36+
}
37+
38+
asyncfunctiongetSourceDigest(ref){
39+
constres=awaitrun('docker',['buildx','imagetools','inspect',ref,'--format','{{.Digest}}'],{ignoreReturnCode: true});
40+
if(res.exitCode!==0)thrownewError(`Failed to inspect source image: ${ref}`);
41+
constout=res.stdout.trim();
42+
if(!out.startsWith('sha256:'))thrownewError(`Unexpected digest for ${ref}: ${out}`);
43+
returnout;
44+
}
45+
46+
asyncfunctiongetCompositeManifest(baseRef){
47+
constres=awaitrun('docker',['buildx','imagetools','inspect',baseRef,'--format','{{json .Manifest}}'],{ignoreReturnCode: true});
48+
if(res.exitCode!==0)returnnull;
49+
try{
50+
returnJSON.parse(res.stdout.trim());
51+
}catch{
52+
returnnull;
53+
}
54+
}
55+
56+
functioncompositeDigestMap(manifestObj){
57+
if(!manifestObj?.manifests)returnnull;
58+
constmap={};
59+
for(constmofmanifestObj.manifests){
60+
constarch=m?.platform?.architecture;
61+
constdigest=m?.digest;
62+
if(arch&&digest)map[arch]=digest;
63+
}
64+
returnmap;
65+
}
66+
67+
asyncfunctionrecreateComposite(baseRef,sources){
68+
constargs=['buildx','imagetools','create','-t',baseRef, ...sources];
69+
constres=awaitrun('docker',args,{ignoreReturnCode: true});
70+
if(res.exitCode!==0)thrownewError(`Failed to create composite for ${baseRef}`);
71+
}
72+
73+
asyncfunctionprocessGroup(baseRef,archRefs){
74+
console.log(`\n==> Processing ${baseRef}`);
75+
76+
constsources=[];
77+
constsourceDigests={};
78+
for(constarchof['amd64','arm64']){
79+
constref=archRefs[arch];
80+
if(ref){
81+
constdigest=awaitgetSourceDigest(ref);
82+
sourceDigests[arch]=digest;
83+
sources.push(ref);
84+
console.log(` Source ${arch} digest: ${digest}`);
85+
}
86+
}
87+
88+
if(sources.length===0){
89+
console.warn(` Skipping: no valid arch variants for ${baseRef}`);
90+
return{ baseRef,status: 'skipped'};
91+
}
92+
93+
constmanifestObj=awaitgetCompositeManifest(baseRef);
94+
letneedsCreate=false;
95+
letreason='';
96+
97+
if(!manifestObj){
98+
needsCreate=true;
99+
reason='not found';
100+
}else{
101+
constmap=compositeDigestMap(manifestObj);
102+
if(!map){
103+
needsCreate=true;
104+
reason='not a manifest list';
105+
}else{
106+
for(constarchofObject.keys(sourceDigests)){
107+
if(map[arch]!==sourceDigests[arch]){
108+
needsCreate=true;
109+
reason='outdated digests';
110+
break;
111+
}
112+
}
113+
}
114+
}
115+
116+
if(needsCreate){
117+
console.log(` Creating manifest list (${reason}) -> ${baseRef}`);
118+
awaitrecreateComposite(baseRef,sources);
119+
return{ baseRef,status: 'updated', reason };
120+
}else{
121+
console.log(` Up to date: ${baseRef}`);
122+
return{ baseRef,status: 'ok'};
123+
}
124+
}
125+
126+
asyncfunctionmain(){
127+
constfilePath=path.resolve(process.cwd(),TAG_FILE);
128+
constcontent=awaitfs.readFile(filePath,'utf8');
129+
constlines=content.split(/\r?\n/).map(s=>s.trim()).filter(Boolean);
130+
131+
constgroups=newMap();
132+
for(constlineoflines){
133+
const{ tag }=parseNameAndTag(line);
134+
constarch=archFromTag(tag);
135+
constbaseRef=deriveBaseRef(line);
136+
if(!arch||!baseRef){
137+
console.warn(`Skipping invalid line: ${line}`);
138+
continue;
139+
}
140+
constg=groups.get(baseRef)||{};
141+
g[arch]=line;
142+
groups.set(baseRef,g);
143+
}
144+
145+
if(groups.size===0){
146+
console.error('No valid tags found in tags.txt');
147+
process.exitCode=2;
148+
return;
149+
}
150+
151+
letupdated=0;
152+
letskipped=0;
153+
for(const[baseRef,archRefs]ofgroups.entries()){
154+
try{
155+
constres=awaitprocessGroup(baseRef,archRefs);
156+
if(res.status==='updated')updated++;
157+
if(res.status==='skipped')skipped++;
158+
}catch(e){
159+
console.error(`Error processing ${baseRef}: ${e.message}`);
160+
process.exitCode=1;
161+
}
162+
}
163+
164+
console.log(`\nSummary: ${groups.size} composites checked, ${updated} updated, ${skipped} skipped.`);
165+
}
166+
167+
main().catch(err=>{
168+
console.error(err);
169+
process.exit(1);
170+
});

0 commit comments

Comments
(0)