name: Release Coolify RC run-name: ${{ inputs.tag }} on: workflow_dispatch: inputs: tag: description: Existing draft prerelease tag (for example, v4.4-rc.1) required: true type: string permissions: {} concurrency: group: coolify-rc-release cancel-in-progress: false env: GITHUB_REGISTRY: ghcr.io DOCKER_REGISTRY: docker.io IMAGE_NAME: coollabsio/coolify jobs: validate: runs-on: ubuntu-24.04 permissions: contents: write outputs: release_id: ${{ steps.draft.outputs.release_id }} version: ${{ steps.version.outputs.version }} steps: - name: Reject releases outside next if: ${{ github.ref != 'refs/heads/next' }} run: | echo "RC releases must run from the next branch, not ${{ github.ref }}." exit 1 - uses: actions/checkout@v5 with: fetch-depth: 0 persist-credentials: false - name: Validate version id: version env: TAG_NAME: ${{ inputs.tag }} run: | if [[ ! "${TAG_NAME}" =~ ^v[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then echo "Unsupported RC tag: ${TAG_NAME}" exit 1 fi VERSION="${TAG_NAME#v}" CONFIG_VERSION=$(jq -r '.coolify.nightly.version' versions.json) if [[ "${CONFIG_VERSION}" != "${VERSION}" ]]; then echo "RC tag ${VERSION} does not match nightly version ${CONFIG_VERSION}." exit 1 fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - name: Validate and pin draft prerelease id: draft uses: actions/github-script@v8 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 prerelease 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(`RC release ${process.env.TAG_NAME} must be marked as a prerelease.`); return; } if (!release.body?.trim()) { core.setFailed(`Draft prerelease ${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, tag_name: process.env.TAG_NAME, target_commitish: context.sha, prerelease: true, }); 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 - 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: Build and push RC image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: 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 }}:rc-release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }} ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:rc-release-${{ needs.validate.outputs.version }}-${{ github.sha }}-${{ matrix.arch }} revalidate: needs: [validate, build] runs-on: ubuntu-24.04 permissions: contents: write steps: - name: Revalidate draft prerelease uses: actions/github-script@v8 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 prerelease ${process.env.TAG_NAME} changed while the images were building.`); return; } if (!release.body?.trim()) { core.setFailed(`Draft prerelease ${process.env.TAG_NAME} no longer contains release notes.`); return; } if (release.target_commitish !== context.sha) { core.setFailed(`Draft prerelease ${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.`); } catch (error) { if (error.status !== 404) throw error; } publish: needs: [validate, build, revalidate] 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 RC and next on ${{ env.GITHUB_REGISTRY }} env: REGISTRY: ${{ env.GITHUB_REGISTRY }} VERSION: ${{ needs.validate.outputs.version }} run: | IMAGE="${REGISTRY}/${IMAGE_NAME}" SOURCE="rc-release-${VERSION}-${GITHUB_SHA}" docker buildx imagetools create \ "${IMAGE}:${SOURCE}-amd64" \ "${IMAGE}:${SOURCE}-aarch64" \ --tag "${IMAGE}:${VERSION}" \ --tag "${IMAGE}:next" - name: Publish RC and next on ${{ env.DOCKER_REGISTRY }} env: REGISTRY: ${{ env.DOCKER_REGISTRY }} VERSION: ${{ needs.validate.outputs.version }} run: | IMAGE="${REGISTRY}/${IMAGE_NAME}" SOURCE="rc-release-${VERSION}-${GITHUB_SHA}" docker buildx imagetools create \ "${IMAGE}:${SOURCE}-amd64" \ "${IMAGE}:${SOURCE}-aarch64" \ --tag "${IMAGE}:${VERSION}" \ --tag "${IMAGE}:next" - name: Publish reviewed draft prerelease uses: actions/github-script@v8 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 prerelease ${process.env.TAG_NAME} changed while the images were building.`); return; } if (!release.body?.trim()) { core.setFailed(`Draft prerelease ${process.env.TAG_NAME} no longer contains release notes.`); return; } if (release.target_commitish !== context.sha) { core.setFailed(`Draft prerelease ${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), tag_name: process.env.TAG_NAME, target_commitish: context.sha, prerelease: true, draft: false, });