Refactor CI with build-push-action and Docker caching (#3545)

* Switch CI to build-push-action and add GHCR caching

* Set YAML `indent_size` to 2 in .editorconfig

* CI: Reuse Docker steps

* Update docker/login-action to v4

* Upload preview Docker image artifact in CI

* Add reusable docker-build workflow
This commit is contained in:
Adam Daley authored and GitHub committed 2026-05-14 22:29:17 +01:00
1 parent ac508b96b4
commit 5f5a6e5a6a
6 files changed
+293 -173

No files matched your search

+1 -2
View File
@@ -39,8 +39,7 @@ indent_size = 4
[*.{yaml,yml}] [*.{yaml,yml}]
indent_style = space indent_style = space
indent_size = 4 indent_size = 2
trim_trailing_whitespace = false
[.php_cs{,.dist}] [.php_cs{,.dist}]
indent_style = space indent_style = space
+91 -55
View File
@@ -70,6 +70,7 @@ jobs:
if: &normal_ci ${{ (github.event_name == 'pull_request' && github.event.action != 'labeled' && github.event.action != 'unlabeled' && github.event.pull_request.base.repo.id != github.event.pull_request.head.repo.id) || github.event_name == 'push' }} if: &normal_ci ${{ (github.event_name == 'pull_request' && github.event.action != 'labeled' && github.event.action != 'unlabeled' && github.event.pull_request.base.repo.id != github.event.pull_request.head.repo.id) || github.event_name == 'push' }}
steps: steps:
- &checkout_step - &checkout_step
name: 'Checkout Repository'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: 'Perform Spellcheck' - name: 'Perform Spellcheck'
@@ -80,8 +81,8 @@ jobs:
php-build-test: php-build-test:
name: 'PHP Build and Test (${{ matrix.php }})' name: 'PHP Build and Test (${{ matrix.php }})'
permissions: permissions:
actions: write
contents: read contents: read
packages: write
needs: [ spellcheck ] needs: [ spellcheck ]
if: *normal_ci if: *normal_ci
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -93,20 +94,36 @@ jobs:
steps: steps:
- *checkout_step - *checkout_step
- name: 'Set up Docker Buildx' - &lowercase_repo
name: 'Lowercase github.repository'
run: |
echo "IMAGE_NAME=${GITHUB_REPOSITORY@L}" >> ${GITHUB_ENV}
- &login_ghcr
name: 'Login to GitHub Container Registry'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- &setup_buildx
name: 'Set up Docker Buildx'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: 'Build Docker Test Image' - name: 'Build Docker Test Image'
run: | uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
docker buildx build \ with:
--target test \ build-args: |
--build-arg PHP_VERSION=${{ matrix.php }} \ PHP_VERSION=${{ matrix.php }}
--build-arg INSTALL_TRANSLATIONS=false \ INSTALL_TRANSLATIONS=false
--cache-from type=gha,scope=fossbilling-test-${{ matrix.php }} \ cache-from: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:cache-ci-${{ matrix.php }}
--cache-to type=gha,scope=fossbilling-test-${{ matrix.php }},mode=max \ cache-to: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:cache-ci-${{ matrix.php }},mode=max
--load \ context: .
--tag fossbilling:test-${{ matrix.php }} \ load: true
. tags: |
fossbilling:test-${{ matrix.php }}
target: test
- name: 'PHPStan' - name: 'PHPStan'
run: | run: |
@@ -127,22 +144,74 @@ jobs:
run: bash ./.github/scripts/run-docker-live-tests.sh fossbilling:test-${{ matrix.php }} run: bash ./.github/scripts/run-docker-live-tests.sh fossbilling:test-${{ matrix.php }}
preview-build: preview-build:
name: 'Create Preview Release' name: 'Create Preview Build'
permissions: permissions:
actions: write
contents: read contents: read
packages: write
needs: [ php-build-test ] needs: [ php-build-test ]
if: *normal_ci if: ${{ (github.event_name == 'pull_request' && github.event.action != 'labeled' && github.event.action != 'unlabeled' && github.event.pull_request.base.repo.id != github.event.pull_request.head.repo.id) || (github.event_name == 'push' && github.ref != 'refs/heads/main') }}
uses: ./.github/workflows/release-build.yml runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- *checkout_step
- *lowercase_repo
- *login_ghcr
- *setup_buildx
- name: 'Build Release Artifact'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
build-args: |
FOSSBILLING_VERSION=${{ github.sha }}
FOSSBILLING_VERSION_TRUNCATE=7
SENTRY_DSN=${{ vars.SENTRY_DSN }}
cache-from: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:cache-preview
cache-to: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:cache-preview,mode=max
context: .
target: release-artifact
outputs: type=local,dest=./release-tree
- name: 'Create Preview Build Archive'
run: tar -cf /tmp/build.tar -C ./release-tree ./
- name: 'Upload Preview Build Archive'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
if-no-files-found: 'error'
name: preview-build
path: /tmp/build.tar
retention-days: 1
preview-docker:
name: 'Build and Push Preview Docker Image'
permissions:
contents: read
packages: write
needs: [ php-build-test ]
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
uses: ./.github/workflows/docker-build.yml
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
with: with:
artifact-name: 'preview-build' target: runtime
preview: true cache-scope: cache-preview
cache-scope: fossbilling-preview extract-release-tree: true
release-tree-artifact-name: preview-build
meta-images: fossbilling/fossbilling
meta-tags: |
type=raw,value=preview
meta-flavor: |
latest=false
build-args: |
FOSSBILLING_VERSION=${{ github.sha }}
FOSSBILLING_VERSION_TRUNCATE=7
SENTRY_DSN=${{ vars.SENTRY_DSN }}
upload-preview: upload-preview:
name: 'Upload Preview Release' name: 'Upload Preview Release'
needs: [ preview-build ] needs: [ preview-build, preview-docker ]
if: *normal_ci if: ${{ !cancelled() && (needs.preview-build.result == 'success' || needs.preview-docker.result == 'success') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
steps: steps:
@@ -178,7 +247,7 @@ jobs:
AWS_S3_ENDPOINT: ${{ secrets.AWS_S3_ENDPOINT }} AWS_S3_ENDPOINT: ${{ secrets.AWS_S3_ENDPOINT }}
AWS_REGION: 'auto' AWS_REGION: 'auto'
SOURCE_DIR: './dist' SOURCE_DIR: './dist'
- name: 'Upload Downloadable Preview to GitHub' - name: 'Upload Downloadable Preview to GitHub'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with: with:
@@ -186,36 +255,3 @@ jobs:
name: 'FOSSBilling Preview' name: 'FOSSBilling Preview'
path: './src/*' path: './src/*'
retention-days: 14 retention-days: 14
deploy-preview-docker:
name: 'Build and Push Preview to Docker Hub'
permissions:
contents: read
id-token: write
needs: [ preview-build ]
if: ${{ github.ref == 'refs/heads/main' }}
uses: docker/github-builder/.github/workflows/build.yml@c2782c55efa56a01b9c30021db8f5ec3993228a3 # v1.8.0
secrets:
registry-auths: |
- registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
with:
output: image
push: true
platforms: linux/amd64,linux/arm64
cache: true
cache-mode: max
cache-scope: fossbilling-preview
sbom: true
set-meta-labels: true
set-meta-annotations: true
meta-images: fossbilling/fossbilling
meta-flavor: |
latest=false
meta-tags: |
type=raw,value=preview
build-args: |
FOSSBILLING_VERSION=${{ github.sha }}
FOSSBILLING_VERSION_TRUNCATE=7
SENTRY_DSN=${{ vars.SENTRY_DSN }}
+187
View File
@@ -0,0 +1,187 @@
name: Multi-platform Docker Build
on:
workflow_call:
inputs:
target:
required: true
type: string
build-args:
required: false
type: string
default: ''
cache-scope:
required: true
type: string
meta-images:
required: true
type: string
meta-tags:
required: true
type: string
meta-flavor:
required: false
type: string
default: ''
extract-release-tree:
required: false
type: boolean
default: false
release-tree-artifact-name:
required: false
type: string
default: 'release-tree'
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
outputs:
release-tree-artifact:
value: ${{ jobs.build.outputs.release-tree-artifact }}
permissions:
contents: read
packages: write
jobs:
build:
name: 'Build (${{ matrix.platform }})'
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
- platform: linux/arm64
runner: ubuntu-24.04-arm
fail-fast: false
outputs:
release-tree-artifact: ${{ steps.release-tree-output.outputs.artifact-name || '' }}
steps:
- name: 'Checkout Repository'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: 'Lowercase github.repository'
run: echo "IMAGE_NAME=${GITHUB_REPOSITORY@L}" >> "${GITHUB_ENV}"
- name: 'Set Release Tree Artifact Output'
id: release-tree-output
if: inputs.extract-release-tree
run: echo "artifact-name=${{ inputs.release-tree-artifact-name }}" >> "${GITHUB_OUTPUT}"
- name: 'Login to GitHub Container Registry'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 'Login to Docker Hub'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Set up Docker Buildx'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: 'Docker Metadata'
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with:
images: ${{ inputs.meta-images }}
tags: ${{ inputs.meta-tags }}
flavor: ${{ inputs.meta-flavor }}
- name: 'Build and Push by Digest'
id: build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
build-args: ${{ inputs.build-args }}
cache-from: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:${{ inputs.cache-scope }}
cache-to: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:${{ inputs.cache-scope }},mode=max
context: .
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,push-by-digest=true,name=${{ inputs.meta-images }}
target: ${{ inputs.target }}
- name: 'Extract Release Tree'
if: inputs.extract-release-tree && matrix.platform == 'linux/amd64'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
with:
build-args: ${{ inputs.build-args }}
cache-from: type=registry,ref=ghcr.io/${{ env.IMAGE_NAME }}:${{ inputs.cache-scope }}
context: .
outputs: type=local,dest=./release-tree
target: release-artifact
- name: 'Create Release Tree Archive'
if: inputs.extract-release-tree && matrix.platform == 'linux/amd64'
run: tar -cf /tmp/build.tar -C ./release-tree ./
- name: 'Upload Release Tree'
if: inputs.extract-release-tree && matrix.platform == 'linux/amd64'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
if-no-files-found: 'error'
name: ${{ inputs.release-tree-artifact-name }}
path: /tmp/build.tar
retention-days: 1
- name: 'Export Digest'
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: 'Upload Digest'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
if-no-files-found: 'error'
name: digest-${{ matrix.platform }}
path: /tmp/digests/*
retention-days: 1
merge:
name: 'Merge Multi-Platform Manifest'
needs: [ build ]
runs-on: ubuntu-latest
steps:
- name: 'Login to Docker Hub'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Set up Docker Buildx'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: 'Docker Metadata'
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
with:
images: ${{ inputs.meta-images }}
tags: ${{ inputs.meta-tags }}
flavor: ${{ inputs.meta-flavor }}
- name: 'Download Digests'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: /tmp/digests
pattern: digest-*
merge-multiple: true
- name: 'Create Manifest List and Push'
working-directory: /tmp/digests
run: |
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ inputs.meta-images }}@sha256:%s ' *)
- name: 'Inspect Image'
run: |
docker buildx imagetools inspect \
'${{ inputs.meta-images }}:${{ steps.meta.outputs.version }}'
-61
View File
@@ -1,61 +0,0 @@
name: 'Build and Prepare Release Archive'
on:
workflow_call:
inputs:
artifact-name:
description: 'The artifact name to use when uploading the preview archive.'
default: 'preview-build'
required: false
type: string
preview:
description: 'Whether the release is a preview release or not.'
default: true
required: false
type: boolean
cache-scope:
description: 'The GHA cache scope to read cached layers from (e.g. from a parallel Docker image build).'
default: ''
required: false
type: string
jobs:
release-build:
name: 'Build and Prepare Release'
permissions:
actions: write
contents: read
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: 'Set up Docker Buildx'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: 'Build Release Artifact'
run: |
cache_args=()
if [ -n "${{ inputs.cache-scope }}" ]; then
cache_args+=(--cache-from "type=gha,scope=${{ inputs.cache-scope }}")
fi
docker buildx build \
--target release-artifact \
--build-arg FOSSBILLING_VERSION="${{ inputs.preview && github.sha || github.ref_name }}" \
--build-arg FOSSBILLING_VERSION_TRUNCATE="${{ inputs.preview && '7' || '0' }}" \
--build-arg SENTRY_DSN="${{ vars.SENTRY_DSN }}" \
"${cache_args[@]}" \
--output type=local,dest=./release-tree \
.
- name: 'Create Release Build Archive'
run: tar -cf /tmp/build.tar -C ./release-tree ./
- name: 'Upload Release Build Archive'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
if-no-files-found: 'error'
name: ${{ inputs.artifact-name }}
path: /tmp/build.tar
retention-days: 1
+13 -55
View File
@@ -2,7 +2,7 @@ name: Upload Release to Docker Hub
on: on:
release: release:
types: types:
- published - published
concurrency: concurrency:
@@ -12,7 +12,7 @@ concurrency:
jobs: jobs:
check-latest: check-latest:
name: 'Check if Release is Latest' name: 'Check if Release is Latest'
permissions: permissions:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
@@ -38,67 +38,25 @@ jobs:
deploy-release-docker: deploy-release-docker:
name: 'Build and Push Release to Docker Hub' name: 'Build and Push Release to Docker Hub'
permissions: permissions:
contents: read contents: read
id-token: write packages: write
needs: [ check-latest ] needs: [ check-latest ]
uses: docker/github-builder/.github/workflows/build.yml@c2782c55efa56a01b9c30021db8f5ec3993228a3 # v1.8.0 uses: ./.github/workflows/docker-build.yml
secrets: secrets:
registry-auths: | DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
- registry: docker.io DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
with: with:
output: image target: runtime
push: true cache-scope: cache-release
platforms: linux/amd64,linux/arm64 extract-release-tree: true
cache: true release-tree-artifact-name: release-build
cache-mode: max
cache-scope: fossbilling-release
sbom: true
set-meta-labels: true
set-meta-annotations: true
meta-images: fossbilling/fossbilling meta-images: fossbilling/fossbilling
meta-flavor: |
latest=false
meta-tags: | meta-tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=raw,value=latest,enable=${{ needs.check-latest.outputs.is_latest == 'true' }} type=raw,value=latest,enable=${{ needs.check-latest.outputs.is_latest == 'true' }}
meta-flavor: |
latest=false
build-args: | build-args: |
FOSSBILLING_VERSION=${{ github.ref_name }} FOSSBILLING_VERSION=${{ github.ref_name }}
SENTRY_DSN=${{ vars.SENTRY_DSN }} SENTRY_DSN=${{ vars.SENTRY_DSN }}
build-release-archive:
name: 'Build Release Archive'
permissions:
actions: write
contents: read
needs: [ check-latest ]
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: 'Set up Docker Buildx'
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: 'Build Release Artifact'
run: |
docker buildx build \
--target release-artifact \
--build-arg FOSSBILLING_VERSION="${{ github.ref_name }}" \
--build-arg SENTRY_DSN="${{ vars.SENTRY_DSN }}" \
--cache-from type=gha,scope=fossbilling-release \
--output type=local,dest=./release-tree \
.
- name: 'Create Release Build Archive'
run: tar -cf /tmp/build.tar -C ./release-tree ./
- name: 'Upload Release Build Archive'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
if-no-files-found: 'error'
name: release-build
path: /tmp/build.tar
retention-days: 1
+1
View File
@@ -1,6 +1,7 @@
.vscode .vscode
._* ._*
.DS_Store .DS_Store
.opencode
/cache /cache
/src/config.php /src/config.php
/src/data/log/* /src/data/log/*