Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions .github/workflows/preview-image-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
# api/.github/workflows/preview-image-build.yml — Phase 1b companion.
#
# Builds + pushes a `:pr-<N>-<sha>` GHCR tag of the api image on every PR
# open / sync / reopen. The infra repo's preview-create workflow (also
# Phase 1b) pulls this exact tag when it provisions the per-PR namespace.
#
# Why a sibling workflow rather than a tag-add on deploy.yml: deploy.yml
# only fires on push to master. PR builds need a parallel pipeline.
#
# Scope:
# - Mirrors deploy.yml's image-build step (sibling repo checkout for
# common/ + proto/, same Dockerfile, same buildx invocation) but
# SKIPS the test gate (CI's ci.yml already runs the test suite on
# PRs; the preview-image build is purely "package the binary so a
# preview env can pull it"). Faster turnaround, no duplicated load
# on the test runner.
# - Tags ONLY `:pr-<N>-<sha>`. Never touches `:latest` or `:master-*`
# (production tag namespace is reserved for deploy.yml). Per
# IMAGE-RETENTION-POLICY.md the pin-prod-images workflow only
# protects `master-*` + semver tags; `pr-*` tags are free to be
# reaped by GHCR retention.
# - Soft-fails if GHCR_PUSH_TOKEN is unset (matches deploy.yml's
# auth posture — GHCR_PUSH_TOKEN is a classic PAT with write:packages
# for the InstaNode-dev org; per-job GITHUB_TOKEN cannot push to
# the org-owned package, see deploy.yml line 230 comment).
# - Does NOT deploy. The infra repo's preview-create workflow handles
# the `kubectl set image` equivalent (applies a fresh Deployment).
#
# Concurrency: a rapid push that fires this workflow twice for the same
# PR will cancel the older run — the latest SHA is what the preview env
# wants.

name: preview-image-build

on:
pull_request:
types: [opened, synchronize, reopened]
# Same docs-only path skip as deploy.yml: preview images for a
# markdown-only PR are useless and burn CI minutes.
paths-ignore:
- '**.md'
- 'docs/**'
- 'CLAUDE.md'
- '.gitignore'
- 'LICENSE'
- 'BUGBASH-*/**'

permissions:
contents: read
packages: write

concurrency:
group: preview-image-build-${{ github.event.pull_request.number }}
cancel-in-progress: true

env:
IMAGE_REPO: ghcr.io/instanode-dev/instant-api

jobs:
build:
name: Build + push :pr-<N>-<sha> image
runs-on: ubuntu-latest
steps:
- name: Soft-check GHCR_PUSH_TOKEN
id: prereq
env:
GHCR_PUSH_TOKEN: ${{ secrets.GHCR_PUSH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${GHCR_PUSH_TOKEN:-}" ]; then
echo "::warning::GHCR_PUSH_TOKEN not set on api repo — skipping preview image build."
echo "::warning::infra preview-create will soft-fail with 'image not yet pushed' on rollout."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Checkout api (this repo) into ./api
if: steps.prereq.outputs.skip == 'false'
uses: actions/checkout@v6
with:
path: api
ref: ${{ github.event.pull_request.head.sha }}

- name: Checkout common sibling into ./common
if: steps.prereq.outputs.skip == 'false'
uses: actions/checkout@v6
with:
repository: ${{ vars.COMMON_REPO || format('{0}/common', github.repository_owner) }}
token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
path: common

- name: Checkout proto sibling into ./proto
if: steps.prereq.outputs.skip == 'false'
uses: actions/checkout@v6
with:
repository: ${{ vars.PROTO_REPO || format('{0}/proto', github.repository_owner) }}
token: ${{ secrets.REPO_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
path: proto

- name: Compute build metadata
if: steps.prereq.outputs.skip == 'false'
id: meta
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
# Defensive: github.event.pull_request.number is always numeric +
# github-controlled, but match deploy.yml's shape discipline.
case "${PR_NUMBER}" in
[1-9]|[1-9][0-9]|[1-9][0-9][0-9]|[1-9][0-9][0-9][0-9]|[1-9][0-9][0-9][0-9][0-9]) ;;
*) echo "::error::unexpected PR_NUMBER: ${PR_NUMBER}"; exit 1 ;;
esac
case "${PR_HEAD_SHA}" in
[0-9a-f]*) ;;
*) echo "::error::unexpected SHA shape: ${PR_HEAD_SHA}"; exit 1 ;;
esac
SHORT_SHA="${PR_HEAD_SHA:0:7}"
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Tag matches the pattern preview-create expects.
VERSION="pr-${PR_NUMBER}-${SHORT_SHA}"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "build_time=${BUILD_TIME}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Building ${VERSION} (${BUILD_TIME})"

- name: Set up Docker Buildx
if: steps.prereq.outputs.skip == 'false'
uses: docker/setup-buildx-action@v4

- name: Log in to GHCR
if: steps.prereq.outputs.skip == 'false'
# Same PAT posture as deploy.yml: GHCR_PUSH_TOKEN is a classic PAT
# with write:packages for the InstaNode-dev org. The per-job
# GITHUB_TOKEN, even with `packages: write`, is scoped to THIS
# repo and cannot push to the org-owned package — see deploy.yml
# for the full rationale.
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}

- name: Build and push :pr-<N>-<sha> image
if: steps.prereq.outputs.skip == 'false'
run: |
set -euo pipefail
docker buildx build \
--platform linux/amd64 \
-f api/Dockerfile \
--build-arg GIT_SHA="${{ steps.meta.outputs.short_sha }}" \
--build-arg BUILD_TIME="${{ steps.meta.outputs.build_time }}" \
--build-arg VERSION="${{ steps.meta.outputs.version }}" \
-t "${IMAGE_REPO}:${{ steps.meta.outputs.version }}" \
--push \
.
echo "pushed ${IMAGE_REPO}:${{ steps.meta.outputs.version }}"
Loading