diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index 5ccb43a8e..a5fe461f5 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -3,17 +3,6 @@ name: Production Build (v4) on: push: branches: ["v4.x"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - .github/workflows/coolify-helper-next.yml - - .github/workflows/coolify-realtime.yml - - .github/workflows/coolify-realtime-next.yml - - .github/workflows/pr-quality.yaml - - docker/coolify-helper/Dockerfile - - docker/coolify-realtime/Dockerfile - - docker/testing-host/Dockerfile - - templates/** - - CHANGELOG.md permissions: contents: read @@ -55,11 +44,6 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: @@ -68,17 +52,13 @@ jobs: platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-${{ matrix.arch }} merge-manifest: runs-on: ubuntu-24.04 needs: build-push steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - uses: docker/setup-buildx-action@v3 - name: Login to ${{ env.GITHUB_REGISTRY }} @@ -95,28 +75,16 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ - --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ - --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ - --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ - --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} diff --git a/.github/workflows/coolify-release.yml b/.github/workflows/coolify-release.yml new file mode 100644 index 000000000..9cd3b90e5 --- /dev/null +++ b/.github/workflows/coolify-release.yml @@ -0,0 +1,107 @@ +name: Release Coolify + +on: + release: + types: [published] + +permissions: + contents: read + packages: write + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: coollabsio/coolify + +jobs: + promote-image: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ github.event.release.tag_name }} + + - 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: 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() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/RELEASE.md b/RELEASE.md index bc159b040..1df585fc5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -15,20 +15,35 @@ This guide outlines the release process for Coolify, intended for developers and ## Release Process -1. **Development on `next` or Feature Branches** - - Improvements, fixes, and new features are developed on the `next` branch or separate feature branches. +1. **Prepare the Release** + - Develop changes on `next` or feature branches. + - Set the release version in `config/constants.php` and `versions.json`. Both values must match the planned Git tag without the `v` prefix (for example, `4.2.0` for tag `v4.2.0`). + - Verify the changelog and required tests before merging. -2. **Merging to `main`** - - Once ready, changes are merged from the `next` branch into the `main` branch (via a pull request). +2. **Build the Release Commit** + - Merge the release commit into `v4.x` through a pull request. + - The `Production Build (v4)` 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-` manifest in both registries. + - This workflow does not update a semantic version tag or `latest`. -3. **Building the Release** - - After merging to `main`, GitHub Actions automatically builds release images for all architectures and pushes them to the GitHub Container Registry and Docker Hub with the specific version tag and the `latest` tag. +3. **Wait for the SHA Image** + - Confirm the complete `Production Build (v4)` workflow, including its `merge-manifest` job, succeeded. + - Do not publish the release before the multi-architecture SHA image exists in both registries. -4. **Creating a GitHub Release** - - A new GitHub release is manually created with details of the changes made in the version. +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. -5. **Updating the CDN** - - To make a new version publicly available, the version information on the CDN needs to be updated manually. After that the new version number will be available at [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json). +5. **Verify the Promotion** + - Confirm the `Release Coolify` workflow succeeded. + - Verify the semantic version image has the same manifest digest as `sha-` in Docker Hub and GHCR. + - For stable releases, also verify `latest` points to the promoted release manifest. + +6. **Update the CDN** + - To make a new version available to self-hosted instances, update the version information on the CDN manually. + - Confirm the new version is available at [https://cdn.coollabs.io/coolify/versions.json](https://cdn.coollabs.io/coolify/versions.json). > [!NOTE] > The CDN update may not occur immediately after the GitHub release. It can take hours or even days due to additional testing, stability checks, or potential hotfixes. **The update becomes available only after the CDN is updated. After the CDN is updated, a discord announcement will be made in the Production Release channel.** @@ -36,7 +51,7 @@ This guide outlines the release process for Coolify, intended for developers and ## Version Types
- Stable (coming soon) + Stable - **Stable** - The production version suitable for stable, production environments (recommended). diff --git a/tests/Unit/ProductionImageWorkflowTest.php b/tests/Unit/ProductionImageWorkflowTest.php new file mode 100644 index 000000000..557e86b64 --- /dev/null +++ b/tests/Unit/ProductionImageWorkflowTest.php @@ -0,0 +1,46 @@ +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'); +}); + +it('promotes the released commit image without rebuilding it', 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('bootstrap/getVersion.php') + ->toContain('--tag "${IMAGE}:${VERSION}"') + ->not->toContain('docker/build-push-action'); +}); + +it('only promotes stable releases to latest', 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"'); +}); + +it('documents the sha image release process', function () { + $releaseGuide = file_get_contents(dirname(__DIR__, 2).'/RELEASE.md'); + + expect($releaseGuide) + ->toContain('Merge the release commit into `v4.x`') + ->toContain('`sha-`') + ->toContain('targeting the exact commit that produced the SHA image') + ->toContain('promotes the existing SHA image without rebuilding it') + ->toContain('Update the CDN') + ->not->toContain('Merging to `main`'); +});