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+ // even run if previous job skipped
53+ 'if ' => '${{ !failure() }} ' ,
54+ 'runs-on ' => '${{ matrix.runner }} ' ,
55+ 'container ' => 'webdevops/dockerfile-build-env ' ,
56+ 'steps ' => array_values (
57+ array_filter (
5658 [
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 }} ' ,
59+ ['uses ' => 'actions/checkout@v6 ' ],
60+ ['uses ' => 'docker/setup-buildx-action@v3 ' ],
61+ [
62+ 'name ' => 'Build (load locally) ' ,
63+ 'uses ' => 'docker/build-push-action@v6 ' ,
64+ 'with ' => [
65+ 'context ' => dirname (str_replace (__DIR__ . '/../../ ' , '' , $ node ['file ' ])),
66+ 'platforms ' => '${{ matrix.platform }} ' ,
67+ 'load ' => true ,
68+ 'tags ' => 'ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-${{ matrix.arch }}- ' . $ node ['tag ' ],
69+ 'cache-from ' => 'type=gha ' ,
70+ 'cache-to ' => 'type=gha,mode=max ' ,
71+ 'build-args ' => implode ("\n" , [
72+ 'TARGETARCH=${{ matrix.arch }} ' ,
73+ ]),
74+ ],
6475 ],
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
76+ $ serverSpec ? [
77+ 'name ' => 'run serverspec ' ,
78+ 'run ' => implode ("\n" , $ serverSpec ),
79+ ] : null ,
80+ $ structuredTests ? [
81+ 'name ' => 'run structure-test ' ,
82+ 'run ' => implode ("\n" , $ structuredTests ),
83+ ] : null ,
84+ [
85+ 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
86+ 'name ' => 'Login to ghcr.io ' ,
87+ 'uses ' => 'docker/login-action@v3 ' ,
88+ 'with ' => [
89+ 'registry ' => 'ghcr.io ' ,
90+ 'username ' => '${{ github.actor }} ' ,
91+ 'password ' => '${{ secrets.GITHUB_TOKEN }} ' ,
92+ ],
93+ ],
94+ [
95+ 'name ' => 'Push arch image ' ,
96+ 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
97+ 'run ' => 'docker push "ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-${{ matrix.arch }}"- ' . $ node ['tag ' ],
7798 ],
7899 ],
79- ] ,
100+ ) ,
80101 ),
81- ),
102+ ],
103+ $ jobId . '_publish ' => [
104+ 'name ' => $ node ['name ' ] . ' - Publish ' ,
105+ 'runs-on ' => 'ubuntu-latest ' ,
106+ 'needs ' => $ jobId ,
107+ 'if ' => '${{github.ref == \'refs/heads/master \'}} ' ,
108+ 'steps ' => [
109+ ['uses ' => 'docker/setup-buildx-action@v3 ' ],
110+ [
111+ 'name ' => 'Login to ghcr.io ' ,
112+ 'uses ' => 'docker/login-action@v3 ' ,
113+ 'with ' => [
114+ 'registry ' => 'ghcr.io ' ,
115+ 'username ' => '${{ github.actor }} ' ,
116+ 'password ' => '${{ secrets.GITHUB_TOKEN }} ' ,
117+ ],
118+ ],
119+ [
120+ 'name ' => 'Login to hub.docker.com ' ,
121+ 'uses ' => 'docker/login-action@v3 ' ,
122+ 'with ' => [
123+ 'username ' => '${{ secrets.DOCKERHUB_USERNAME }} ' ,
124+ 'password ' => '${{ secrets.DOCKERHUB_TOKEN }} ' ,
125+ ],
126+ ],
127+ [
128+ 'name ' => 'Create and push multi-arch manifest ' ,
129+ 'run ' =>
130+ // 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)
131+ implode ("\n" , [
132+ 'set -euo pipefail ' ,
133+ 'for i in 1 2 3 4 5 6 7 8 9 10; do ' ,
134+ ' ' . implode (" \\\n " , [
135+ 'docker buildx imagetools create ' ,
136+ ...$ pushTags ,
137+ '"ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-amd64- ' . $ node ['tag ' ] . '" ' ,
138+ '"ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-arm64- ' . $ node ['tag ' ] . '" && exit 0 ' ,
139+ ]),
140+ ' sleep $((i*i)) ' ,
141+ 'done ' ,
142+ 'exit 1 ' ,
143+ ]),
144+ ],
145+ ],
146+ ],
82147 ];
83148 }
84149
@@ -98,14 +163,13 @@ private function serverSpec(array $node): array
98163 return [];
99164 }
100165
101- // $testDockerfile = uniqid('Dockerfile_', true);
102166 $ testDockerfile = 'Dockerfile_test ' ;
103167 $ specConfig = $ node ['serverspec ' ];
104168 $ specConfig ['DOCKERFILE ' ] = $ testDockerfile ;
105169 $ encodedJsonConfig = base64_encode (json_encode ($ specConfig ));
106170 $ script = [
107171 'cd tests/serverspec ' ,
108- 'echo "FROM ' . $ node ['id ' ] . '" >> ' . $ testDockerfile ,
172+ 'echo "FROM ghcr.io/webdevops/ ' . $ node ['image ' ] . ':sha-${{ github.sha }}-${{ matrix.arch }}"- ' . $ node [ ' tag ' ] . ' >> ' . $ testDockerfile ,
109173 'echo "COPY conf/ /" >> ' . $ testDockerfile ,
110174 ];
111175 $ script [] = 'bundle install ' ;
@@ -119,9 +183,9 @@ private function structuredTests(array $node): array
119183 if (file_exists (__DIR__ . '/../../tests/structure-test/ ' . $ node ['image ' ] . '/test.yaml ' )) {
120184 $ script [] = 'cd tests/structure-test ' ;
121185 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 ' ;
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 --config ' . $ node ['image ' ] . '/ ' . $ node ['tag ' ] . '/test.yaml ' ;
123187 } else {
124- $ script [] = '/usr/local/bin/container-structure-test test --image ' . $ node ['name ' ] . ' --config ' . $ node ['image ' ] . '/test.yaml ' ;
188+ $ 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 ' ;
125189 }
126190 }
127191 return $ script ;
@@ -133,7 +197,7 @@ public function getValidationConfig(): array
133197 'name ' => 'Validate Automation ' ,
134198 'runs-on ' => 'ubuntu-latest ' ,
135199 'steps ' => [
136- ['uses ' => 'actions/checkout@v4 ' ],
200+ ['uses ' => 'actions/checkout@v6 ' ],
137201 [
138202 'name ' => 'Validate that template/* are used to generate Dockerfiles ' ,
139203 'run ' => implode ("\n" , [
0 commit comments