internal updates (#11187)

This commit is contained in:
Andras Bacsai
2026-08-11 16:14:08 +02:00
committed by GitHub
8 changed files with 319 additions and 123 deletions
+222 -73
View File
@@ -1,12 +1,18 @@
name: Release Coolify
name: Release Coolify Fix
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: Existing draft release tag (for example, v4.3.1)
required: true
type: string
permissions:
contents: read
packages: write
permissions: {}
concurrency:
group: coolify-fix-release
cancel-in-progress: false
env:
GITHUB_REGISTRY: ghcr.io
@@ -14,14 +20,116 @@ env:
IMAGE_NAME: coollabsio/coolify
jobs:
promote-image:
validate:
runs-on: ubuntu-24.04
environment: production-release
permissions:
contents: write
outputs:
release_id: ${{ steps.draft.outputs.release_id }}
version: ${{ steps.version.outputs.version }}
steps:
- name: Reject releases outside v4.x
if: ${{ github.ref_name != 'v4.x' }}
run: |
echo "Fix releases must run from v4.x, not ${{ github.ref_name }}."
exit 1
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.event.release.tag_name }}
- name: Validate version
id: version
env:
TAG_NAME: ${{ inputs.tag }}
run: |
if [[ ! "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Unsupported fix release tag: ${TAG_NAME}"
exit 1
fi
VERSION="${TAG_NAME#v}"
CONFIG_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)
if [[ "${CONFIG_VERSION}" != "${VERSION}" ]]; then
echo "Release tag ${VERSION} does not match config version ${CONFIG_VERSION}."
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Validate and pin draft release
id: draft
uses: actions/github-script@v7
env:
TAG_NAME: ${{ inputs.tag }}
with:
script: |
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const release = releases.find((candidate) => candidate.tag_name === process.env.TAG_NAME);
if (!release) {
core.setFailed(`Create a draft release for ${process.env.TAG_NAME} before running this workflow.`);
return;
}
if (!release.draft) {
core.setFailed(`Release ${process.env.TAG_NAME} must still be a draft.`);
return;
}
if (release.prerelease) {
core.setFailed(`Fix release ${process.env.TAG_NAME} cannot be marked as a prerelease.`);
return;
}
if (!release.body?.trim()) {
core.setFailed(`Draft release ${process.env.TAG_NAME} must contain reviewed release notes.`);
return;
}
try {
await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${process.env.TAG_NAME}`,
});
core.setFailed(`Git tag ${process.env.TAG_NAME} already exists.`);
return;
} catch (error) {
if (error.status !== 404) throw error;
}
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.id,
target_commitish: context.sha,
});
core.setOutput('release_id', release.id);
build:
needs: validate
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
- arch: aarch64
platform: linux/aarch64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: docker/setup-buildx-action@v3
@@ -39,69 +147,110 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Resolve release image
id: release
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: |
if [[ ! "${TAG_NAME}" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then
echo "Unsupported release tag: ${TAG_NAME}"
exit 1
fi
VERSION="${TAG_NAME#v}"
RELEASE_SHA=$(git rev-list -n 1 "${TAG_NAME}")
CONFIG_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)
if [[ "${CONFIG_VERSION}" != "${VERSION}" ]]; then
echo "Release tag ${VERSION} does not match config version ${CONFIG_VERSION}."
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "sha=${RELEASE_SHA}" >> "$GITHUB_OUTPUT"
- name: Promote version on ${{ env.GITHUB_REGISTRY }}
env:
REGISTRY: ${{ env.GITHUB_REGISTRY }}
VERSION: ${{ steps.release.outputs.version }}
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SOURCE_TAG="sha-${RELEASE_SHA}"
docker buildx imagetools create "${IMAGE}:${SOURCE_TAG}" --tag "${IMAGE}:${VERSION}"
- name: Promote version on ${{ env.DOCKER_REGISTRY }}
env:
REGISTRY: ${{ env.DOCKER_REGISTRY }}
VERSION: ${{ steps.release.outputs.version }}
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SOURCE_TAG="sha-${RELEASE_SHA}"
docker buildx imagetools create "${IMAGE}:${SOURCE_TAG}" --tag "${IMAGE}:${VERSION}"
- name: Promote latest on ${{ env.GITHUB_REGISTRY }}
if: ${{ ! github.event.release.prerelease }}
env:
REGISTRY: ${{ env.GITHUB_REGISTRY }}
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SOURCE_TAG="sha-${RELEASE_SHA}"
docker buildx imagetools create "${IMAGE}:${SOURCE_TAG}" --tag "${IMAGE}:latest"
- name: Promote latest on ${{ env.DOCKER_REGISTRY }}
if: ${{ ! github.event.release.prerelease }}
env:
REGISTRY: ${{ env.DOCKER_REGISTRY }}
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SOURCE_TAG="sha-${RELEASE_SHA}"
docker buildx imagetools create "${IMAGE}:${SOURCE_TAG}" --tag "${IMAGE}:latest"
- uses: sarisia/actions-status-discord@v1
if: always()
- name: Build and push release image (${{ matrix.arch }})
uses: docker/build-push-action@v6
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}
context: .
file: docker/production/Dockerfile
platforms: ${{ matrix.platform }}
push: true
build-args: |
COOLIFY_VERSION=${{ needs.validate.outputs.version }}
tags: |
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}
publish:
needs: [validate, build]
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
steps:
- uses: docker/setup-buildx-action@v3
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.GITHUB_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to ${{ env.DOCKER_REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Publish version and latest on ${{ env.GITHUB_REGISTRY }}
env:
REGISTRY: ${{ env.GITHUB_REGISTRY }}
VERSION: ${{ needs.validate.outputs.version }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SOURCE="release-${VERSION}-${GITHUB_SHA}"
docker buildx imagetools create \
"${IMAGE}:${SOURCE}-amd64" \
"${IMAGE}:${SOURCE}-aarch64" \
--tag "${IMAGE}:${VERSION}" \
--tag "${IMAGE}:latest"
- name: Publish version and latest on ${{ env.DOCKER_REGISTRY }}
env:
REGISTRY: ${{ env.DOCKER_REGISTRY }}
VERSION: ${{ needs.validate.outputs.version }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
SOURCE="release-${VERSION}-${GITHUB_SHA}"
docker buildx imagetools create \
"${IMAGE}:${SOURCE}-amd64" \
"${IMAGE}:${SOURCE}-aarch64" \
--tag "${IMAGE}:${VERSION}" \
--tag "${IMAGE}:latest"
- name: Publish reviewed draft release
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ needs.validate.outputs.release_id }}
TAG_NAME: ${{ inputs.tag }}
with:
script: |
const releaseId = Number(process.env.RELEASE_ID);
const { data: release } = await github.rest.repos.getRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: releaseId,
});
if (release.tag_name !== process.env.TAG_NAME || !release.draft || release.prerelease) {
core.setFailed(`Draft release ${process.env.TAG_NAME} changed while the images were building.`);
return;
}
if (!release.body?.trim()) {
core.setFailed(`Draft release ${process.env.TAG_NAME} no longer contains release notes.`);
return;
}
if (release.target_commitish !== context.sha) {
core.setFailed(`Draft release ${process.env.TAG_NAME} no longer targets ${context.sha}.`);
return;
}
try {
await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${process.env.TAG_NAME}`,
});
core.setFailed(`Git tag ${process.env.TAG_NAME} was created while the images were building.`);
return;
} catch (error) {
if (error.status !== 404) throw error;
}
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: Number(process.env.RELEASE_ID),
draft: false,
});
+10 -14
View File
@@ -30,6 +30,12 @@ jobs:
with:
persist-credentials: false
- name: Resolve internal version
id: version
run: |
BASE_VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)
echo "version=${BASE_VERSION}-dev.${GITHUB_SHA::9}" >> "$GITHUB_OUTPUT"
- name: Login to ${{ env.GITHUB_REGISTRY }}
uses: docker/login-action@v3
with:
@@ -51,6 +57,8 @@ jobs:
file: docker/production/Dockerfile
platforms: ${{ matrix.platform }}
push: true
build-args: |
COOLIFY_VERSION=${{ steps.version.outputs.version }}
tags: |
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-${{ matrix.arch }}
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-${{ matrix.arch }}
@@ -78,33 +86,21 @@ jobs:
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
env:
REGISTRY: ${{ env.GITHUB_REGISTRY }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
TAG_ARGS=(--tag "${IMAGE}:sha-${SHA}")
# Moving tag for the latest production-line SHA image (v4.x only).
if [ "${BRANCH}" = "v4.x" ]; then
TAG_ARGS+=(--tag "${IMAGE}:edge")
fi
docker buildx imagetools create \
"${IMAGE}:sha-${SHA}-amd64" \
"${IMAGE}:sha-${SHA}-aarch64" \
"${TAG_ARGS[@]}"
--tag "${IMAGE}:sha-${SHA}"
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
env:
REGISTRY: ${{ env.DOCKER_REGISTRY }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
TAG_ARGS=(--tag "${IMAGE}:sha-${SHA}")
# Moving tag for the latest production-line SHA image (v4.x only).
if [ "${BRANCH}" = "v4.x" ]; then
TAG_ARGS+=(--tag "${IMAGE}:edge")
fi
docker buildx imagetools create \
"${IMAGE}:sha-${SHA}-amd64" \
"${IMAGE}:sha-${SHA}-aarch64" \
"${TAG_ARGS[@]}"
--tag "${IMAGE}:sha-${SHA}"
+15 -14
View File
@@ -20,7 +20,7 @@ Coolify uses two long-lived branches so production fixes can ship without waitin
| Branch | Role | Docker image tags | How it ships |
| --- | --- | --- | --- |
| **`v4.x`** | Production / releasable line | `sha-<commit>` and moving `edge` via **Build Coolify (SHA)** | GitHub release promotes the SHA image to a semantic version (and `latest` for stable releases) |
| **`v4.x`** | Production / releasable line | Immutable `sha-<commit>` images via **Build Coolify (SHA)** | The manual fix-release workflow rebuilds the selected commit with the stable version and updates `latest` |
| **`next`** | Development line for features and larger changes | Branch tag (for example `next`) via **Staging Build** | Becomes production only after merge into `v4.x` |
### Where to merge
@@ -51,7 +51,7 @@ Only commits on **`v4.x`** produce production SHA images and can be tagged for a
1. **Prepare the Release**
- Land the work on **`v4.x`**: merge a fix PR into `v4.x`, or merge ready work from `next` into `v4.x` for a feature release.
- Set the release version in `config/constants.php` and `versions.json` on the commit you will tag. Both values must match the planned Git tag without the `v` prefix (for example, `4.2.0` for tag `v4.2.0`).
- Set the upcoming release version in `config/constants.php`. It must match the planned Git tag without the `v` prefix (for example, `4.3.1` for tag `v4.3.1`). Keep `coolify.v4.version` in `versions.json` on the currently published stable version until the CDN update.
- Verify the changelog and required tests before merging.
- After the release (or after the fix merges), merge `v4.x` back into `next` if those branches have diverged.
@@ -59,23 +59,24 @@ Only commits on **`v4.x`** produce production SHA images and can be tagged for a
- Merge the release commit into `v4.x` through a pull request.
- The `Build Coolify (SHA)` workflow builds AMD64 and ARM64 images and publishes them to Docker Hub and GHCR using immutable architecture tags.
- After both builds complete, the workflow creates the multi-architecture `sha-<commit-sha>` manifest in both registries.
- For pushes to **`v4.x`**, the same multi-architecture manifest is also tagged as `edge`, so `coollabsio/coolify:edge` always points at the latest production-line SHA image. Builds from `main` publish only the immutable `sha-<commit-sha>` tags.
- The image reports a traceable development version such as `4.3.1-dev.d64cbda3e`.
- This workflow does not update a semantic version tag or `latest`.
3. **Wait for the SHA Image**
3. **Prepare the Draft Release**
- Confirm the complete `Build Coolify (SHA)` workflow, including its `merge-manifest` job, succeeded.
- Do not publish the release before the multi-architecture SHA image exists in both registries.
- Create a reviewed draft GitHub release with the planned tag, such as `v4.3.1`, targeting `v4.x`.
- Add the final release notes, leave the release as a draft, and do not mark a fix release as a prerelease.
4. **Create and Publish the GitHub Release**
- Create a GitHub release with a semantic version tag such as `v4.2.0`, targeting the exact commit that produced the SHA image.
- Mark beta or other test releases as prereleases. Publish production versions as stable releases.
- Publishing the release starts the `Release Coolify` workflow. It verifies that the Git tag matches `config/constants.php`, then promotes the existing SHA image without rebuilding it.
- The workflow assigns the semantic version tag in Docker Hub and GHCR. Stable releases also update `latest`; prereleases do not.
4. **Run the Fix-Release Workflow**
- Run `Release Coolify Fix` manually from `v4.x` and enter the existing draft tag.
- The workflow validates the draft, its release notes, the version in `config/constants.php`, and the absence of an existing Git tag.
- It pins the draft to the selected commit and rebuilds AMD64 and ARM64 images with the exact stable version.
- After both builds succeed, it publishes the semantic version and `latest` manifests to Docker Hub and GHCR, then publishes the existing draft release.
5. **Verify the Promotion**
- Confirm the `Release Coolify` workflow succeeded.
- Verify the semantic version image has the same manifest digest as `sha-<commit-sha>` in Docker Hub and GHCR.
- For stable releases, also verify `latest` points to the promoted release manifest.
5. **Verify the Release**
- Confirm the `Release Coolify Fix` workflow succeeded and the reviewed draft is now published.
- Verify the semantic version image and `latest` contain AMD64 and ARM64 manifests in Docker Hub and GHCR.
- Verify Coolify reports the exact stable version without a development SHA suffix.
6. **Update the CDN**
- To make a new version available to self-hosted instances, update the version information on the CDN manually.
+1 -1
View File
@@ -2,7 +2,7 @@
return [
'coolify' => [
'version' => '4.3.0',
'version' => env('COOLIFY_VERSION') ?: '4.3.0',
'helper_version' => '1.0.14',
'realtime_version' => '1.0.17',
'railpack_version' => '0.23.0',
+3
View File
@@ -76,8 +76,11 @@ ARG TARGETPLATFORM
ARG POSTGRES_VERSION
ARG CLOUDFLARED_VERSION
ARG NGINX_VERSION
ARG COOLIFY_VERSION
ARG CI=true
ENV COOLIFY_VERSION=${COOLIFY_VERSION}
WORKDIR /var/www/html
USER root
+10 -4
View File
@@ -1,4 +1,10 @@
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-90 hover:opacity-100 dark:hover:text-white hover:text-black']) }}
href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('constants.coolify.version') }}" target="_blank">
v{{ config('constants.coolify.version') }}
</a>
@php($version = config('constants.coolify.version'))
@if (str_contains($version, '-dev.'))
<span {{ $attributes->merge(['class' => 'text-xs opacity-90']) }}>v{{ $version }}</span>
@else
<a {{ $attributes->merge(['class' => 'text-xs cursor-pointer opacity-90 hover:opacity-100 dark:hover:text-white hover:text-black']) }}
href="https://github.com/coollabsio/coolify/releases/tag/v{{ config('constants.coolify.version') }}" target="_blank">
v{{ $version }}
</a>
@endif
@@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Facades\Blade;
/**
* Header version badge should open the matching GitHub release page.
*/
@@ -15,3 +17,14 @@ test('desktop header version links to the coolify github release for the install
->toContain("https://github.com/coollabsio/coolify/releases/tag/v{{ config('constants.coolify.version') }}")
->toContain('target="_blank"');
});
test('development versions are not linked to nonexistent github releases', function () {
config(['constants.coolify.version' => '4.3.1-dev.d64cbda3e']);
$version = Blade::render('<x-version />');
expect($version)
->toContain('v4.3.1-dev.d64cbda3e')
->not->toContain('href=')
->not->toContain('target="_blank"');
});
+45 -17
View File
@@ -1,37 +1,62 @@
<?php
it('publishes v4 branch builds only under the commit sha', function () {
it('publishes v4 branch builds under the commit sha with a traceable internal version', function () {
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/coolify-sha-build.yml');
$dockerfile = file_get_contents(dirname(__DIR__, 2).'/docker/production/Dockerfile');
$constants = file_get_contents(dirname(__DIR__, 2).'/config/constants.php');
expect($workflow)
->toContain('name: Build Coolify (SHA)')
->toContain('sha-${{ github.sha }}-${{ matrix.arch }}')
->toContain('sha-${{ github.sha }}')
->not->toContain('bootstrap/getVersion.php')
->not->toContain('steps.version.outputs.VERSION')
->not->toContain('IMAGE_NAME }}:latest');
->toContain('php bootstrap/getVersion.php')
->toContain('version=${BASE_VERSION}-dev.${GITHUB_SHA::9}')
->toContain('COOLIFY_VERSION=${{ steps.version.outputs.version }}')
->not->toContain('IMAGE_NAME }}:latest')
->and($dockerfile)
->toContain('ARG COOLIFY_VERSION')
->toContain('ENV COOLIFY_VERSION=${COOLIFY_VERSION}')
->and($constants)
->toContain("'version' => env('COOLIFY_VERSION') ?: '4.3.0'");
});
it('promotes the released commit image without rebuilding it', function () {
it('orders a maintenance development build before its stable release', function () {
expect(version_compare('4.3.0-dev.d64cbda3e', '4.3.0', '<'))->toBeTrue()
->and(version_compare('4.3.0', '4.3.0-dev.d64cbda3e', '>'))->toBeTrue();
});
it('requires a reviewed draft release before building a stable version', function () {
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/coolify-release.yml');
expect($workflow)
->toContain('release:')
->toContain('types: [published]')
->toContain('TAG_NAME: ${{ github.event.release.tag_name }}')
->toContain('git rev-list -n 1 "${TAG_NAME}"')
->toContain('SOURCE_TAG="sha-${RELEASE_SHA}"')
->toContain('workflow_dispatch:')
->toContain('tag:')
->toContain('github.ref_name != \'v4.x\'')
->toContain('github.paginate(github.rest.repos.listReleases')
->toContain('release.draft')
->toContain('release.prerelease')
->toContain('release.body?.trim()')
->toContain('bootstrap/getVersion.php')
->toContain('--tag "${IMAGE}:${VERSION}"')
->not->toContain('docker/build-push-action');
->toContain('target_commitish: context.sha')
->not->toContain('generate-notes');
});
it('only promotes stable releases to latest', function () {
it('rebuilds stable images and publishes the reviewed draft after both architectures succeed', function () {
$workflow = file_get_contents(dirname(__DIR__, 2).'/.github/workflows/coolify-release.yml');
expect($workflow)
->toContain('if: ${{ ! github.event.release.prerelease }}')
->toContain('--tag "${IMAGE}:latest"');
->toContain('docker/build-push-action@v6')
->toContain('COOLIFY_VERSION=${{ needs.validate.outputs.version }}')
->toContain('release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }}')
->toContain('--tag "${IMAGE}:${VERSION}"')
->toContain('--tag "${IMAGE}:latest"')
->toContain('github.rest.repos.getRelease')
->toContain('release.target_commitish !== context.sha')
->toContain('release_id: Number(process.env.RELEASE_ID)')
->toContain('draft: false')
->toContain('permissions: {}')
->not->toContain('sarisia/actions-status-discord@v1')
->not->toContain('SOURCE_TAG="sha-${RELEASE_SHA}"');
});
it('documents the sha image release process', function () {
@@ -46,10 +71,13 @@ it('documents the sha image release process', function () {
->toContain('Merge the release commit into `v4.x`')
->toContain('`Build Coolify (SHA)`')
->toContain('`sha-<commit-sha>`')
->toContain('targeting the exact commit that produced the SHA image')
->toContain('promotes the existing SHA image without rebuilding it')
->toContain('Create a reviewed draft GitHub release')
->toContain('rebuilds AMD64 and ARM64 images with the exact stable version')
->toContain('publishes the existing draft release')
->toContain('Update the CDN')
->toContain('Only commits on **`v4.x`** produce production SHA images')
->not->toContain('`edge`')
->not->toContain('promotes the existing SHA image')
->not->toContain('Merging to `main`')
->not->toContain('Production Build (v4)');
});