cicd(release): mail-worker v2026.7.10 #12
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Mail-worker | |
| on: | |
| push: | |
| tags: | |
| - "mail-worker-v*" | |
| workflow_dispatch: | |
| concurrency: | |
| group: deploy-mail-worker | |
| cancel-in-progress: false | |
| # Least privilege: deploy authenticates to Unikraft Cloud via secrets, not the | |
| # GitHub token. It only needs to read the repo. | |
| permissions: | |
| contents: read | |
| env: | |
| KRAFTCLOUD_METRO: fra | |
| # Pinned new Unikraft CLI (`unikraft`, from unikraft-cloud/cli) version. | |
| # Bump deliberately after verifying a new release against a dry-run deploy. | |
| UNIKRAFT_CLI_VERSION: "0.4.1" | |
| # Image namespace on Unikraft Cloud is the organization slug; published | |
| # images and running instances live under `<org>/<name>`. | |
| KRAFTCLOUD_ORG: gaaaa | |
| jobs: | |
| deploy: | |
| name: Deploy to Unikraft Cloud | |
| runs-on: ubuntu-latest | |
| # The image now builds once in its own step (below); an uncached build is | |
| # the long pole, so keep a generous ceiling until build caching lands. | |
| timeout-minutes: 30 | |
| environment: production | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - name: Prepare files for Unikraft Cloud | |
| run: | | |
| cp apps/mail-worker/Kraftfile Kraftfile | |
| cp apps/mail-worker/Dockerfile Dockerfile | |
| # Installs the `unikraft` CLI and logs it in headlessly. The new CLI keeps | |
| # its own auth store (a legacy `kraft`/UKC_TOKEN login does not carry | |
| # over), so every deploy authenticates here via the org token. | |
| - name: Install and log in to Unikraft Cloud | |
| uses: unikraft/setup-action@29c9cce5bc7e894f8929c845d4fba518ef8f1015 # v1 | |
| with: | |
| version: ${{ env.UNIKRAFT_CLI_VERSION }} | |
| token: ${{ secrets.KRAFTCLOUD_TOKEN }} | |
| organization: ${{ env.KRAFTCLOUD_ORG }} | |
| - name: Extract version from tag | |
| id: meta | |
| run: | | |
| if [[ "$GITHUB_REF_NAME" == mail-worker-v* ]]; then | |
| echo "version=${GITHUB_REF_NAME#mail-worker-v}" >> $GITHUB_OUTPUT | |
| else | |
| echo "version=$(node -p "require('./apps/mail-worker/package.json').version")" >> $GITHUB_OUTPUT | |
| fi | |
| echo "short_sha=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT | |
| # Build and publish the image ONCE. The new CLI separates packaging from | |
| # running, so the deploy step below only re-runs the fast `unikraft run` | |
| # on retry — never a fresh ~9-min image build (the old kraftkit path | |
| # rebuilt on every retry and ran out of each attempt's window). The build | |
| # (~9-min compile + ~300 MiB push) has no tight deadline of its own; the | |
| # job's timeout-minutes is its single ceiling, so a slow uncached build | |
| # isn't killed by a second, shorter clock. | |
| - name: Build and publish image | |
| run: | | |
| unikraft build . \ | |
| --output ${{ env.KRAFTCLOUD_ORG }}/batuda-mail-worker:${{ steps.meta.outputs.version }} | |
| - name: Pre-flight cmdline size guard | |
| run: | | |
| # Unikraft Cloud bakes every -e var into the unikernel kernel command | |
| # line, which Unikraft caps at 4096 bytes / 60 args: past the byte | |
| # cap the boot aborts with -E2BIG, past the arg cap argv is silently | |
| # truncated and vars vanish at runtime. Assemble the exact -e payload | |
| # this deploy will send and fail well under both ceilings, so an | |
| # oversized release is caught here instead of at boot. Non-secret | |
| # config ships in the baked file and is not counted. Secret values | |
| # stay inside the array and are never printed. | |
| E_ARGS=( | |
| -e NODE_ENV=production | |
| -e SERVICE_VERSION="${{ steps.meta.outputs.version }}" | |
| -e GIT_SHA="${{ github.sha }}" | |
| -e REGION="${{ env.KRAFTCLOUD_METRO }}" | |
| -e OTEL_EXPORTER_OTLP_HEADERS="${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }},x-honeycomb-dataset=batuda-mail-worker" | |
| -e DATABASE_URL="${{ secrets.DATABASE_URL }}" | |
| -e EMAIL_CREDENTIAL_KEY="${{ secrets.EMAIL_CREDENTIAL_KEY }}" | |
| -e STORAGE_ACCESS_KEY_ID="${{ secrets.STORAGE_ACCESS_KEY_ID }}" | |
| -e STORAGE_SECRET_ACCESS_KEY="${{ secrets.STORAGE_SECRET_ACCESS_KEY }}" | |
| ) | |
| BYTES=$(printf '%s' "${E_ARGS[*]}" | wc -c | tr -d ' ') | |
| FLAGS=0 | |
| for tok in "${E_ARGS[@]}"; do | |
| if [ "$tok" = "-e" ]; then | |
| FLAGS=$((FLAGS + 1)) | |
| fi | |
| done | |
| echo "Assembled -e payload: ${BYTES} bytes across ${FLAGS} -e flags (limits: 3500 bytes / 50 flags)" | |
| if [ "$BYTES" -gt 3500 ] || [ "$FLAGS" -gt 50 ]; then | |
| echo "::error::cmdline -e payload is ${BYTES} bytes / ${FLAGS} flags, over the pre-flight limit of 3500 bytes / 50 flags; move non-secret config into apps/mail-worker/config.production.json" | |
| exit 1 | |
| fi | |
| - name: Deploy to Unikraft Cloud | |
| uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 | |
| with: | |
| max_attempts: 3 | |
| retry_wait_seconds: 30 | |
| timeout_minutes: 10 | |
| # Background worker — no public HTTP endpoint, so no service group. | |
| # Use a stable instance name (`batuda-mail-worker`, mirroring the | |
| # batuda-server / batuda-web naming) so ops commands target a | |
| # predictable identity. The IMAP IDLE connections drop on any | |
| # restart and the worker's claim-and-reconnect loop re-establishes | |
| # them, so a ~5s gap between delete + recreate is acceptable. | |
| # Each retry first deletes any leftover instance so the fixed name | |
| # is free before `run` recreates it (a half-created instance from | |
| # a failed attempt would otherwise collide on the name). | |
| # -m 2048M mirrors server's footprint; the deploy tree (aws-sdk + | |
| # imapflow + mailparser + pg + effect) is comparable. 1024 ran | |
| # out of initrd RAM extracting the CPIO. | |
| # --restart on-failure: if the worker crashes (non-zero exit), the | |
| # platform starts a fresh one instead of leaving it dead but still | |
| # listed as "running" and processing no mail. A clean shutdown | |
| # (exit 0) is left alone. Pairs with the crash-guards in main.ts. | |
| # --timeout 5m gives the deploy up to 5 minutes to wait for the | |
| # freshly-pushed ~300 MiB image to become available before the | |
| # instance is created — the Unikraft team's guidance for the | |
| # propagation delay. The new CLI's --timeout is a whole-command | |
| # deadline that takes a real duration; the old kraftkit API | |
| # capped per-RPC timeouts at 10s (HTTP 400 above 10000ms). | |
| # Scale-to-zero stays off via the Kraftfile label (baked into the | |
| # image), keeping the IDLE-holding worker always-warm. | |
| command: | | |
| # instances delete/get/logs take a bare name and reject --metro | |
| # (only run/services create accept it); the login profile spans metros. | |
| unikraft instances delete batuda-mail-worker 2>&1 | grep -v "not found" || true | |
| unikraft run \ | |
| --timeout 5m \ | |
| --metro ${{ env.KRAFTCLOUD_METRO }} \ | |
| --name batuda-mail-worker \ | |
| --image ${{ env.KRAFTCLOUD_ORG }}/batuda-mail-worker:${{ steps.meta.outputs.version }} \ | |
| -m 2048M \ | |
| --restart on-failure \ | |
| -e NODE_ENV=production \ | |
| -e SERVICE_VERSION="${{ steps.meta.outputs.version }}" \ | |
| -e GIT_SHA="${{ github.sha }}" \ | |
| -e REGION="${{ env.KRAFTCLOUD_METRO }}" \ | |
| -e OTEL_EXPORTER_OTLP_HEADERS="${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }},x-honeycomb-dataset=batuda-mail-worker" \ | |
| -e DATABASE_URL="${{ secrets.DATABASE_URL }}" \ | |
| -e EMAIL_CREDENTIAL_KEY="${{ secrets.EMAIL_CREDENTIAL_KEY }}" \ | |
| -e STORAGE_ACCESS_KEY_ID="${{ secrets.STORAGE_ACCESS_KEY_ID }}" \ | |
| -e STORAGE_SECRET_ACCESS_KEY="${{ secrets.STORAGE_SECRET_ACCESS_KEY }}" | |
| - name: Verify deployment | |
| run: | | |
| sleep 5 | |
| unikraft instances get batuda-mail-worker | |
| # claimAvailableInboxes runs every 5 s, so an IDLE-established | |
| # log line should appear within ~10 s if any inbox is provisioned. | |
| unikraft instances logs batuda-mail-worker --tail 100 || echo "::warning::No logs yet" |