Build and push images #63
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Build and push images | |
on: | |
workflow_dispatch: | |
inputs: | |
dockerhubImageName: | |
description: 'dockerhub image name' | |
required: true | |
default: 'index.docker.io/dtyq/php-dockerfile' | |
ghcrImageName: | |
description: 'ghcr.io image name' | |
required: true | |
default: 'ghcr.io/dtyq/php-dockerfile' | |
awsEcrImageName: | |
description: 'AWS ECR image name' | |
required: true | |
default: 'public.ecr.aws/dtyq/php-dockerfile' | |
schedule: | |
# every Wednesday at 3:42 AM(UTC, Beijing time: 11:42 AM) | |
- cron: '42 3 * * 3' | |
env: | |
TRY_MIRRORS: "" | |
PUBLIC_MIRROR: "https://dl-cdn.alpinelinux.org" # no mirror needed at github actions | |
IMAGE_NAME: php-dockerfile # only used for default local image name | |
jobs: | |
prepare: | |
name: Prepare matrix | |
runs-on: ubuntu-latest | |
outputs: | |
tasks: ${{ steps.gentasks.outputs.tasks }} | |
images: ${{ steps.gentasks.outputs.images }} | |
outputs: ${{ steps.gentasks.outputs.outputs }} | |
steps: | |
- name: Generate tasks | |
shell: python3 {0} | |
id: gentasks | |
run: | | |
import os, json | |
from distutils.version import LooseVersion | |
phpAlpineVersions = { | |
"8.4": "3.22", | |
"8.3": "3.22", | |
"8.2": "3.22", | |
"8.1": "3.18", | |
"8.0": "3.16", | |
} | |
extVersions = [ | |
# swoole master branch | |
("swoole-master", "8.1", None), | |
# swoole 6.0 branch | |
("swoole-6.0.2", "8.1", None), | |
# swoole 5.1 branch | |
("swoole-5.1.8", "8.0", "8.3"), | |
# swoole 4.8 branch | |
("swoole-4.8.13", "8.0", "8.2"), | |
# swow ci branch | |
("swow-ci", "8.0", None), | |
# swow | |
("swow-1.6.1", "8.0", "8.4"), | |
] | |
extraExts = [ | |
"jsonpath", | |
"parle", | |
"xlswriter", | |
] | |
extraExtTag = "-".join(sorted(extraExts)) | |
githubOutput = open(os.environ["GITHUB_OUTPUT"], "w") | |
tags = [] | |
index = 0 | |
for phpVer, alpineVer in phpAlpineVersions.items(): | |
for extVersion in extVersions: | |
ext, minPhpVer, maxPhpVer = extVersion | |
if minPhpVer and LooseVersion(phpVer) < LooseVersion(minPhpVer): | |
continue | |
if maxPhpVer and LooseVersion(phpVer) > LooseVersion(maxPhpVer): | |
continue | |
tag = f"{phpVer}-alpine-{alpineVer}-{ext}-{extraExtTag}" | |
tags.append({ | |
"tag": tag, | |
"outputVar": f"output{index}", | |
}) | |
print(f"Add tag {tag}") | |
index += 1 | |
serialized = json.dumps({"tag": tags}) | |
githubOutput.write(f"tasks={serialized}\n") | |
images = [] | |
images.append("${{ inputs.ghcrImageName }}" or "ghcr.io/dtyq/php-dockerfile") | |
if "${{ secrets.DOCKERHUB_CRED }}": | |
images.append("${{ inputs.dockerhubImageName }}" or "index.docker.io/dtyq/php-dockerfile") | |
if "${{ secrets.AWS_ECR_CRED }}": | |
images.append("${{ inputs.awsEcrImageName }}" or "public.ecr.aws/dtyq/php-dockerfile") | |
serialized = json.dumps(images) | |
githubOutput.write(f"images={serialized}\n") | |
githubOutput.close() | |
build: | |
name: ${{ matrix.tag.tag }} build and push | |
runs-on: ubuntu-latest | |
needs: prepare | |
strategy: | |
matrix: ${{ fromJson(needs.prepare.outputs.tasks) }} | |
max-parallel: 5 | |
fail-fast: false | |
permissions: | |
contents: write | |
packages: write | |
# NOTE: needs more if more output var added | |
# this is dumb, but we cannot generate dynamic outputs via needs.prepare.outputs.outputs | |
outputs: | |
output1: ${{ steps.push.outputs.output1 }} | |
output2: ${{ steps.push.outputs.output2 }} | |
output3: ${{ steps.push.outputs.output3 }} | |
output4: ${{ steps.push.outputs.output4 }} | |
output5: ${{ steps.push.outputs.output5 }} | |
output6: ${{ steps.push.outputs.output6 }} | |
output7: ${{ steps.push.outputs.output7 }} | |
output8: ${{ steps.push.outputs.output8 }} | |
output9: ${{ steps.push.outputs.output9 }} | |
output10: ${{ steps.push.outputs.output10 }} | |
output11: ${{ steps.push.outputs.output11 }} | |
output12: ${{ steps.push.outputs.output12 }} | |
output13: ${{ steps.push.outputs.output13 }} | |
output14: ${{ steps.push.outputs.output14 }} | |
output15: ${{ steps.push.outputs.output15 }} | |
output16: ${{ steps.push.outputs.output16 }} | |
output17: ${{ steps.push.outputs.output17 }} | |
output18: ${{ steps.push.outputs.output18 }} | |
output19: ${{ steps.push.outputs.output19 }} | |
output20: ${{ steps.push.outputs.output20 }} | |
output21: ${{ steps.push.outputs.output21 }} | |
output22: ${{ steps.push.outputs.output22 }} | |
output23: ${{ steps.push.outputs.output23 }} | |
output24: ${{ steps.push.outputs.output24 }} | |
output25: ${{ steps.push.outputs.output25 }} | |
output26: ${{ steps.push.outputs.output26 }} | |
output27: ${{ steps.push.outputs.output27 }} | |
output28: ${{ steps.push.outputs.output28 }} | |
output29: ${{ steps.push.outputs.output29 }} | |
output30: ${{ steps.push.outputs.output30 }} | |
steps: | |
# checkout source code | |
- name: Checkout | |
uses: actions/checkout@v4 | |
- name: Set up secrets | |
shell: python3 {0} | |
run: | | |
import os, json, base64 | |
fw = open(os.path.expanduser("~/.docker/config.json"), "w") | |
config = { | |
"auths": {}, | |
} | |
# GHCR | |
cred = base64.b64encode(b'whatever:${{ github.token }}').decode() | |
config["auths"]["ghcr.io"] = { | |
"auth": cred, | |
} | |
# AWS ECR | |
cred = "${{ secrets.AWS_ECR_CRED }}" | |
if cred: | |
config["auths"]["public.ecr.aws"] = { | |
"auth": cred, | |
} | |
# DockerHub | |
cred = "${{ secrets.DOCKERHUB_CRED }}" | |
if cred: | |
config["auths"]["https://index.docker.io/v1/"] = { | |
"auth": cred, | |
} | |
fw.write(json.dumps(config)) | |
fw.close() | |
- name: Build image | |
shell: bash | |
id: build | |
run: | | |
./build.py "${{ matrix.tag.tag }}" | |
- name: Push image | |
shell: python {0} | |
id: push | |
run: | | |
import sys, os, json, subprocess | |
imageName = os.environ["IMAGE_NAME"] | |
images = json.loads('${{ needs.prepare.outputs.images }}') | |
digests = {} | |
for image in images: | |
for tag in ("${{ matrix.tag.tag }}", "${{ matrix.tag.tag }}-debuggable"): | |
print(f"tagging {imageName}:{tag} to {image}:{tag}") | |
proc = subprocess.run( | |
["docker", "tag", f"{imageName}:{tag}", f"{image}:{tag}"] | |
) | |
if proc.returncode != 0: | |
print(f"failed to tag {imageName}:{tag} to {image}:{tag}") | |
sys.exit(1) | |
print(f"pushing {image}:{tag}") | |
proc = subprocess.run(["docker", "push", f"{image}:{tag}"]) | |
if proc.returncode != 0: | |
print(f"failed to push {image}:{tag}") | |
sys.exit(1) | |
print(f"get {image}:{tag} digest") | |
proc = subprocess.run( | |
[ | |
"docker", | |
"inspect", | |
f"{image}:{tag}", | |
"--format", | |
"{{index .RepoDigests 0}}", | |
], | |
capture_output=True, | |
) | |
if proc.returncode != 0: | |
print(f"failed to inspect {image}:{tag}") | |
sys.exit(1) | |
digest = proc.stdout.decode().strip().split("@")[1] | |
if digest: | |
print(f"got {image}:{tag} digest: {digest}") | |
else: | |
print(f"failed to get {image}:{tag} digest") | |
sys.exit(1) | |
digests[f"{image}:{tag}"] = digest | |
githubOutput = open(os.environ["GITHUB_OUTPUT"], "w") | |
githubOutput.write(f"${{ matrix.tag.outputVar }}={json.dumps(digests)}\n") | |
githubOutput.close() | |
gen-attests: | |
name: Generate attestation jobs | |
runs-on: ubuntu-latest | |
needs: build | |
if: ${{ always() }} | |
outputs: | |
tasks: ${{ steps.gen-attests.outputs.tasks }} | |
steps: | |
- name: Duplicate json | |
shell: cp {0} ./attests.json | |
run: | | |
${{ toJson(needs.build.outputs) }} | |
- name: Generate attestation jobs | |
shell: python3 {0} | |
id: gen-attests | |
run: | | |
import json, os | |
hashes = json.load(open("./attests.json")) | |
imageDigests = {} | |
for _, content in hashes.items(): | |
if not content: | |
continue | |
for image, digest in json.loads(content).items(): | |
imageName = image.split(":")[0] | |
imageDigests[imageName] = imageDigests.get(imageName, []) | |
imageDigests[imageName].append(digest) | |
attests = [] | |
for imageName, digests in imageDigests.items(): | |
for digest in digests: | |
attests.append({ | |
"image": imageName, | |
"digest": digest, | |
}) | |
serialized = json.dumps({"attests": attests}) | |
githubOutput = open(os.environ["GITHUB_OUTPUT"], "w") | |
githubOutput.write(f"tasks={serialized}\n") | |
githubOutput.close() | |
attest: | |
name: ${{ matrix.attests.image }} artifact attestation | |
runs-on: ubuntu-latest | |
needs: gen-attests | |
strategy: | |
matrix: ${{ fromJson(needs.gen-attests.outputs.tasks) }} | |
max-parallel: 10 | |
permissions: | |
contents: write | |
packages: write | |
id-token: write | |
attestations: write | |
steps: | |
- name: Set up secrets | |
shell: python3 {0} | |
run: | | |
import os, json, base64 | |
fw = open(os.path.expanduser("~/.docker/config.json"), "w") | |
config = { | |
"auths": {}, | |
} | |
# GHCR | |
cred = base64.b64encode(b'whatever:${{ github.token }}').decode() | |
config["auths"]["ghcr.io"] = { | |
"auth": cred, | |
} | |
# AWS ECR | |
cred = "${{ secrets.AWS_ECR_CRED }}" | |
if cred: | |
config["auths"]["public.ecr.aws"] = { | |
"auth": cred, | |
} | |
# DockerHub | |
cred = "${{ secrets.DOCKERHUB_CRED }}" | |
if cred: | |
config["auths"]["https://index.docker.io/v1/"] = { | |
"auth": cred, | |
} | |
fw.write(json.dumps(config)) | |
fw.close() | |
- name: Generate artifact attestation | |
uses: actions/attest-build-provenance@v2 | |
with: | |
subject-name: ${{ matrix.attests.image }} | |
subject-digest: ${{ matrix.attests.digest }} | |
push-to-registry: true |