name: Release on: push: branches: - main concurrency: group: release-${{ github.ref }} cancel-in-progress: false permissions: contents: write pull-requests: read env: DOCKER_IMAGE: jhd3197/serverkit jobs: promotion-gate: name: Verify dev promotion runs-on: ubuntu-latest steps: - name: Require a merged dev to main pull request uses: actions/github-script@v9 with: script: | const { owner, repo } = context.repo; const { data: pulls } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner, repo, commit_sha: context.sha, }); const promotion = pulls.find((pull) => ( pull.merged_at && pull.base.ref === 'main' && pull.head.ref === 'dev' && pull.head.repo?.full_name === `${owner}/${repo}` )); if (!promotion) { core.setFailed( 'Releases from main require a merged pull request from this repository\'s dev branch.' ); return; } core.info(`Verified dev → main promotion via PR #${promotion.number}.`); # A release must not ship with a red frontend or drifted builtin # extensions, so those suites gate here alongside backend-ci. # scripts-ci (installer/updater suites incl. the ~45-min update-e2e leg) # supports workflow_call too but is deliberately NOT gated to keep # releases fast — add `uses: ./.github/workflows/scripts-ci.yml` as a # fourth gate job if that trade-off changes. ci-gate: name: CI Gate needs: promotion-gate uses: ./.github/workflows/backend-ci.yml frontend-gate: name: Frontend Gate needs: promotion-gate uses: ./.github/workflows/frontend-ci.yml extensions-gate: name: Extensions Gate needs: promotion-gate uses: ./.github/workflows/extensions-ci.yml version: name: Resolve Version runs-on: ubuntu-latest needs: [ci-gate, frontend-gate, extensions-gate] outputs: version: ${{ steps.version.outputs.version }} tag: ${{ steps.version.outputs.tag }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get version id: version run: | VERSION=$(tr -d '[:space:]' < VERSION) TAG="v$VERSION" echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "Releasing ServerKit version: $VERSION" - name: Require an unused release version env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ steps.version.outputs.tag }} run: | if git show-ref --verify --quiet "refs/tags/$TAG"; then echo "::error::Refusing to overwrite existing tag $TAG. Bump VERSION before promoting dev to main." exit 1 fi if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "::error::Refusing to overwrite existing release $TAG. Bump VERSION before promoting dev to main." exit 1 fi - name: Create and push tag run: | TAG="${{ steps.version.outputs.tag }}" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "$TAG" -m "Release $TAG" git push origin "refs/tags/$TAG" build-release: name: Build Release Tarball runs-on: ${{ matrix.runner }} needs: version # Past runs finish in ~13 min; without this cap a hung apt mirror once # held the job for GitHub's 6-hour default. timeout-minutes: 30 strategy: fail-fast: true matrix: include: - runner: ubuntu-latest arch: amd64 - runner: ubuntu-24.04-arm arch: arm64 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '22' - name: Install build dependencies run: | sudo apt-get update sudo apt-get install -y python3-venv python3-pip rsync - name: Build release tarball env: BUILD_ARCH: ${{ matrix.arch }} run: bash scripts/build-release.sh - name: Upload release artifacts uses: actions/upload-artifact@v4 with: name: serverkit-release-${{ matrix.arch }} path: | serverkit-*.tar.gz checksums.txt if-no-files-found: error build-docker: name: Build Docker Image runs-on: ubuntu-latest needs: version steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} continue-on-error: true # Build amd64 first and boot it before anything is published. Issue #101 # shipped because this job went straight from `build` to `push`: the image # was missing scripts/, so /api/v1/servers/install.sh 404'd in every # container while the whole test suite stayed green (pytest runs against # the source tree, where the file always exists). Only booting the artifact # catches that. The layers are cached, so the multi-arch push below reuses # this build rather than repeating it. - name: Build amd64 image for smoke test uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64 push: false load: true tags: serverkit:smoke cache-from: type=gha cache-to: type=gha,mode=max build-args: | VERSION=${{ needs.version.outputs.version }} BUILD_TIME=${{ github.event.repository.updated_at }} GIT_COMMIT=${{ github.sha }} - name: Smoke test the built image run: SKIP_BUILD=1 bash scripts/test/smoke-docker-image.sh serverkit:smoke - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ secrets.DOCKERHUB_USERNAME != '' }} cache-from: type=gha tags: | ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:${{ needs.version.outputs.version }} build-args: | VERSION=${{ needs.version.outputs.version }} BUILD_TIME=${{ github.event.repository.updated_at }} GIT_COMMIT=${{ github.sha }} release: name: Create Release runs-on: ubuntu-latest needs: [version, build-docker, build-release] permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Download release artifacts uses: actions/download-artifact@v4 with: path: release-artifacts pattern: serverkit-release-* merge-multiple: true - name: Assemble release files run: | mkdir -p release cp VERSION release/VERSION cp docker-compose.yml release/docker-compose.yml cp Dockerfile release/Dockerfile mv release-artifacts/serverkit-*.tar.gz release/ 2>/dev/null || true mv release-artifacts/checksums.txt release/checksums.txt 2>/dev/null || true cd release sha256sum serverkit-*.tar.gz >> checksums.txt sort -u checksums.txt -o checksums.txt cat checksums.txt - name: Create GitHub Release uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.version.outputs.tag }} name: ServerKit ${{ needs.version.outputs.tag }} draft: false prerelease: false overwrite_files: false files: | release/* body: | ## ServerKit ${{ needs.version.outputs.tag }} ServerKit is a server control panel for managing web applications, databases, Docker containers, and security on VPS/dedicated servers. ### Docker ```bash docker pull ${{ env.DOCKER_IMAGE }}:${{ needs.version.outputs.version }} docker pull ${{ env.DOCKER_IMAGE }}:latest ``` ### One-line installer ```bash curl -fsSL https://serverkit.ai/install.sh | bash ``` ### Quick Start ```bash git clone https://github.com/${{ github.repository }}.git cd ServerKit docker compose up -d ``` Default credentials: `admin` / `admin` ### What's New See [CHANGELOG](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details. env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} summary: name: Release Summary runs-on: ubuntu-latest needs: [version, release] steps: - name: Create summary run: | echo "## ServerKit Release Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Version:** ${{ needs.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Docker" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Tag | Image |" >> $GITHUB_STEP_SUMMARY echo "|-----|-------|" >> $GITHUB_STEP_SUMMARY echo "| latest | ${{ env.DOCKER_IMAGE }}:latest |" >> $GITHUB_STEP_SUMMARY echo "| ${{ needs.version.outputs.version }} | ${{ env.DOCKER_IMAGE }}:${{ needs.version.outputs.version }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Release" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Tarballs: serverkit-${{ needs.version.outputs.tag }}-linux-amd64.tar.gz, serverkit-${{ needs.version.outputs.tag }}-linux-arm64.tar.gz" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Release created at: https://github.com/${{ github.repository }}/releases/tag/${{ needs.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY