22
33namespace Webdevops \Build ;
44
5+ use function array_filter ;
6+ use function array_values ;
7+ use function dirname ;
8+ use function implode ;
59use function str_replace ;
610
711class GithubJobBuilder
812{
9-
10- public function getJobDescription (array $ node ): array
13+ /**
14+ * @return array<string, array<string, mixed>>
15+ */
16+ public function getJobsDescription (array $ node ): array
1117 {
1218 $ serverSpec = $ this ->serverSpec ($ node );
1319 $ structuredTests = $ this ->structuredTests ($ node );
1420
21+ $ jobId = GithubJobBuilder::toJobId ($ node ['name ' ]);
22+ $ needs = ($ node ['parent ' ] ?? null ) ? GithubJobBuilder::toJobId ($ node ['parent ' ]) . '_publish ' : 'validate-automation ' ;
23+
24+ $ pushTags = [];
25+ $ pushTags [] = '-t " ' . $ node ['id ' ] . '" ' ;
26+ $ pushTags [] = '-t "ghcr.io/ ' . $ node ['id ' ] . '" ' ;
27+ foreach ($ node ['aliases ' ] as $ alias ) {
28+ $ pushTags [] = '-t " ' . $ alias . '" ' ;
29+ $ pushTags [] = '-t "ghcr.io/ ' . $ alias . '" ' ;
30+ }
1531 return [
16- 'name ' => $ node ['name ' ],
17- 'needs ' => [
18- ($ node ['parent ' ] ?? null ) ? GithubJobBuilder::toJobId ($ node ['parent ' ]) : 'validate-automation ' ,
19- ],
20- 'runs-on ' => 'ubuntu-latest ' ,
21- 'container ' => 'webdevops/dockerfile-build-env ' ,
22- 'steps ' => array_values (
23- array_filter (
24- [
25- ['uses ' => 'actions/checkout@v4 ' ],
26- // ['uses' => 'docker/setup-qemu-action@v3'], // only needed for ARM builds
27- ['uses ' => 'docker/setup-buildx-action@v3 ' ],
28- [
29- 'name ' => 'Build x64 ' ,
30- 'uses ' => 'docker/build-push-action@v6 ' ,
31- 'with ' => [
32- 'context ' => dirname (str_replace (__DIR__ . '/../../ ' , '' , $ node ['file ' ])),
33- 'load ' => true ,
34- 'tags ' => 'ghcr.io/webdevops/ ' . $ node ['image ' ] . ': ' . $ node ['tag ' ] . ',webdevops/ ' . $ node ['image ' ] . ': ' . $ node ['tag ' ],
35- 'platforms ' => 'linux/amd64 ' ,
32+ $ jobId => [
33+ 'strategy ' => [
34+ 'fail-fast ' => false ,
35+ 'matrix ' => [
36+ 'include ' => [
37+ [
38+ 'arch ' => 'amd64 ' ,
39+ 'runner ' => 'ubuntu-24.04 ' ,
40+ 'platform ' => 'linux/amd64 ' ,
3641 ],
37- ],
38- $ serverSpec ? [
39- 'name ' => 'run serverspec ' ,
40- 'run ' => implode ("\n" , $ serverSpec ),
41- ] : null ,
42- $ structuredTests ? [
43- 'name ' => 'run structure-test ' ,
44- 'run ' => implode ("\n" , $ structuredTests ),
45- ] : null ,
46- [
47- 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
48- 'name ' => 'Login to ghcr.io ' ,
49- 'uses ' => 'docker/login-action@v3 ' ,
50- 'with ' => [
51- 'registry ' => 'ghcr.io ' ,
52- 'username ' => '${{ github.actor }} ' ,
53- 'password ' => '${{ secrets.GITHUB_TOKEN }} ' ,
42+ [
43+ 'arch ' => 'arm64 ' ,
44+ 'runner ' => 'ubuntu-24.04-arm ' ,
45+ 'platform ' => 'linux/arm64 ' ,
5446 ],
5547 ],
48+ ],
49+ ],
50+ 'name ' => $ node ['name ' ] . ' (${{ matrix.arch }}) ' ,
51+ 'needs ' => $ needs ,
52+ 'runs-on ' => '${{ matrix.runner }} ' ,
53+ 'container ' => 'webdevops/dockerfile-build-env ' ,
54+ 'steps ' => array_values (
55+ array_filter (
5656 [
57- // login after the build so the rate limit of github is used and not from our login Token.
58- 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
59- 'name ' => 'Login to hub.docker.com ' ,
60- 'uses ' => 'docker/login-action@v3 ' ,
61- 'with ' => [
62- 'username ' => '${{ secrets.DOCKERHUB_USERNAME }} ' ,
63- 'password ' => '${{ secrets.DOCKERHUB_TOKEN }} ' ,
57+ ['uses ' => 'actions/checkout@v6 ' ],
58+ ['uses ' => 'docker/setup-buildx-action@v3 ' ],
59+ [
60+ 'name ' => 'Build (load locally) ' ,
61+ 'uses ' => 'docker/build-push-action@v6 ' ,
62+ 'with ' => [
63+ 'context ' => dirname (str_replace (__DIR__ . '/../../ ' , '' , $ node ['file ' ])),
64+ 'platforms ' => '${{ matrix.platform }} ' ,
65+ 'load ' => true ,
66+ 'tags ' => 'ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-${{ matrix.arch }}- ' . $ node ['tag ' ],
67+ 'cache-from ' => 'type=gha ' ,
68+ 'cache-to ' => 'type=gha,mode=max ' ,
69+ 'build-args ' => implode ("\n" , [
70+ 'TARGETARCH=${{ matrix.arch }} ' ,
71+ ]),
72+ ],
6473 ],
65- ],
66- [
67- 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
68- 'name ' => 'Push ' ,
69- // 'name' => 'Build ARM + Push',
70- 'uses ' => 'docker/build-push-action@v6 ' ,
71- 'with ' => [
72- 'context ' => dirname (str_replace (__DIR__ . '/../../ ' , '' , $ node ['file ' ])),
73- 'push ' => true ,
74- 'tags ' => 'ghcr.io/webdevops/ ' . $ node ['image ' ] . ': ' . $ node ['tag ' ] . ',webdevops/ ' . $ node ['image ' ] . ': ' . $ node ['tag ' ],
75- 'platforms ' => 'linux/amd64 ' ,
76- // 'platforms' => 'linux/amd64,linux/arm64', // ARM not ready yet
74+ $ serverSpec ? [
75+ 'name ' => 'run serverspec ' ,
76+ 'run ' => implode ("\n" , $ serverSpec ),
77+ ] : null ,
78+ $ structuredTests ? [
79+ 'name ' => 'run structure-test ' ,
80+ 'run ' => implode ("\n" , $ structuredTests ),
81+ ] : null ,
82+ [
83+ 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
84+ 'name ' => 'Login to ghcr.io ' ,
85+ 'uses ' => 'docker/login-action@v3 ' ,
86+ 'with ' => [
87+ 'registry ' => 'ghcr.io ' ,
88+ 'username ' => '${{ github.actor }} ' ,
89+ 'password ' => '${{ secrets.GITHUB_TOKEN }} ' ,
90+ ],
91+ ],
92+ [
93+ 'name ' => 'Push arch image ' ,
94+ 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
95+ 'run ' => 'docker push "ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-${{ matrix.arch }}"- ' . $ node ['tag ' ],
7796 ],
7897 ],
79- ] ,
98+ ) ,
8099 ),
81- ),
100+ ],
101+ $ jobId . '_publish ' => [
102+ 'name ' => $ node ['name ' ] . ' - Publish ' ,
103+ 'runs-on ' => 'ubuntu-latest ' ,
104+ 'needs ' => $ jobId ,
105+ 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
106+ 'steps ' => [
107+ ['uses ' => 'docker/setup-buildx-action@v3 ' ],
108+ [
109+ 'name ' => 'Login to ghcr.io ' ,
110+ 'uses ' => 'docker/login-action@v3 ' ,
111+ 'with ' => [
112+ 'registry ' => 'ghcr.io ' ,
113+ 'username ' => '${{ github.actor }} ' ,
114+ 'password ' => '${{ secrets.GITHUB_TOKEN }} ' ,
115+ ],
116+ ],
117+ [
118+ 'name ' => 'Login to hub.docker.com ' ,
119+ 'uses ' => 'docker/login-action@v3 ' ,
120+ 'with ' => [
121+ 'username ' => '${{ secrets.DOCKERHUB_USERNAME }} ' ,
122+ 'password ' => '${{ secrets.DOCKERHUB_TOKEN }} ' ,
123+ ],
124+ ],
125+ [
126+ 'name ' => 'Create and push multi-arch manifest ' ,
127+ 'run ' =>
128+ // we need the retry loop here because sometimes docker hub returns errors when pushing manifests (especially if pushed to the same image multiple times in a short time frame)
129+ implode ("\n" , [
130+ 'set -euo pipefail ' ,
131+ 'for i in 1 2 3 4 5 6 7 8 9 10; do ' ,
132+ ' ' . implode (" \\\n " , [
133+ 'docker buildx imagetools create ' ,
134+ ...$ pushTags ,
135+ '"ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-amd64- ' . $ node ['tag ' ] . '" ' ,
136+ '"ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-arm64- ' . $ node ['tag ' ] . '" && exit 0 ' ,
137+ ]),
138+ ' sleep $((i*i)) ' ,
139+ 'done ' ,
140+ 'exit 1 ' ,
141+ ]),
142+ ],
143+ ],
144+ ],
82145 ];
83146 }
84147
@@ -98,14 +161,13 @@ private function serverSpec(array $node): array
98161 return [];
99162 }
100163
101- // $testDockerfile = uniqid('Dockerfile_', true);
102164 $ testDockerfile = 'Dockerfile_test ' ;
103165 $ specConfig = $ node ['serverspec ' ];
104166 $ specConfig ['DOCKERFILE ' ] = $ testDockerfile ;
105167 $ encodedJsonConfig = base64_encode (json_encode ($ specConfig ));
106168 $ script = [
107169 'cd tests/serverspec ' ,
108- 'echo "FROM ' . $ node ['id ' ] . '" >> ' . $ testDockerfile ,
170+ 'echo "FROM ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-${{ matrix.arch }}"- ' . $ node [ ' tag ' ] . ' >> ' . $ testDockerfile ,
109171 'echo "COPY conf/ /" >> ' . $ testDockerfile ,
110172 ];
111173 $ script [] = 'bundle install ' ;
@@ -119,9 +181,9 @@ private function structuredTests(array $node): array
119181 if (file_exists (__DIR__ . '/../../tests/structure-test/ ' . $ node ['image ' ] . '/test.yaml ' )) {
120182 $ script [] = 'cd tests/structure-test ' ;
121183 if (file_exists (__DIR__ . '/../../tests/structure-test/ ' . $ node ['image ' ] . '/ ' . $ node ['tag ' ] . '/test.yaml ' )) {
122- $ script [] = '/usr/local/bin/container-structure-test test --image ' . $ node ['name ' ] . ' --config ' . $ node ['image ' ] . '/test.yaml --config ' . $ node ['image ' ] . '/ ' . $ node ['tag ' ] . '/test.yaml ' ;
184+ $ script [] = '/usr/local/bin/container-structure-test test --image ghcr.io/webdevops/ ' . $ node ['image ' ] . ' :sha-${{ github.sha }}-${{ matrix.arch }}- ' . $ node [ ' tag ' ] . ' --config ' . $ node ['image ' ] . '/test.yaml --config ' . $ node ['image ' ] . '/ ' . $ node ['tag ' ] . '/test.yaml ' ;
123185 } else {
124- $ script [] = '/usr/local/bin/container-structure-test test --image ' . $ node ['name ' ] . ' --config ' . $ node ['image ' ] . '/test.yaml ' ;
186+ $ script [] = '/usr/local/bin/container-structure-test test --image ghcr.io/webdevops/ ' . $ node ['image ' ] . ' :sha-${{ github.sha }}-${{ matrix.arch }}- ' . $ node [ ' tag ' ] . ' --config ' . $ node ['image ' ] . '/test.yaml ' ;
125187 }
126188 }
127189 return $ script ;
@@ -133,7 +195,7 @@ public function getValidationConfig(): array
133195 'name ' => 'Validate Automation ' ,
134196 'runs-on ' => 'ubuntu-latest ' ,
135197 'steps ' => [
136- ['uses ' => 'actions/checkout@v4 ' ],
198+ ['uses ' => 'actions/checkout@v6 ' ],
137199 [
138200 'name ' => 'Validate that template/* are used to generate Dockerfiles ' ,
139201 'run ' => implode ("\n" , [
0 commit comments