Skip to content

chore: bump version to 0.1.12 #4

chore: bump version to 0.1.12

chore: bump version to 0.1.12 #4

# Build and push staging images from the `staging` branch.
# Tags: X.Y.Z-staging.N and floating :staging. Version Git ref reserved via the Git API.
name: Publish (staging)
on:
push:
branches:
- staging
workflow_dispatch:
inputs:
version_override:
description: "Optional version override (default: atomic Git tag reservation for
staging line)"
required: false
type: string
permissions:
contents: read
packages: read
jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Install dependencies
run: npm ci --include=optional
- name: Security audit
run: npm audit --omit=dev --audit-level=moderate
- name: Build all packages
run: npm run build:packages
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Setup web env for build
run: test -f apps/web/.env.local || cp apps/web/.env.example apps/web/.env.local
- name: Build all apps
run: npm run build:apps
reserve-version:
needs: validate
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
version: ${{ steps.reserve.outputs.version }}
float_tag: ${{ steps.reserve.outputs.float_tag }}
is_prod: ${{ steps.reserve.outputs.is_prod }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Reserve next version
id: reserve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ github.sha }}
OVERRIDE: ${{ inputs.version_override }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
BASE=$(node -p "require('./package.json').version" | sed 's/-.*//')
SUFFIX="staging"
FLOAT="staging"
IS_PROD="false"
# This workflow is only for the `staging` branch; ref name is not used in logic below.
create_tag() {
local tag="$1"
local sha="$2"
local body
body=$(mktemp)
LAST_CREATE_CODE=$(curl -sS -o "$body" -w "%{http_code}" \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-X POST "https://api.github.com/repos/${REPO}/git/refs" \
-d "{\"ref\":\"refs/tags/${tag}\",\"sha\":\"${sha}\"}")
LAST_CREATE_BODY=$(cat "$body")
rm -f "$body"
echo "create-ref attempt: tag=${tag} status=${LAST_CREATE_CODE}"
if [ "$LAST_CREATE_CODE" != "201" ] && [ "$LAST_CREATE_CODE" != "422" ]; then
echo "GitHub API error ${LAST_CREATE_CODE} while creating tag ${tag}:" >&2
printf '%s\n' "$LAST_CREATE_BODY" >&2
return 1
fi
}
if [ -n "$OVERRIDE" ]; then
VERSION="$OVERRIDE"
create_tag "$VERSION" "$SHA"
if [ "$LAST_CREATE_CODE" != "201" ] && [ "$LAST_CREATE_CODE" != "422" ]; then
exit 1
fi
if [ "$LAST_CREATE_CODE" = "422" ]; then
echo "::error::Override tag $VERSION already exists" >&2
exit 1
fi
else
START=0
if TAG_LINES=$(git ls-remote --tags origin "refs/tags/${BASE}-${SUFFIX}.*" 2>/dev/null); then
MAX=$(printf '%s\n' "$TAG_LINES" | awk -v prefix="refs/tags/${BASE}-${SUFFIX}." '
{
ref=$2
sub(/\^\{\}$/, "", ref)
if (index(ref, prefix) == 1) {
n=substr(ref, length(prefix) + 1)
if (n ~ /^[0-9]+$/) {
if (max == "" || (n + 0) > (max + 0)) { max=n + 0 }
}
}
}
END { if (max != "") { print max } }')
if [ -n "$MAX" ]; then
START=$((MAX + 1))
fi
fi
N=$START
while :; do
VERSION="${BASE}-${SUFFIX}.${N}"
create_tag "$VERSION" "$SHA"
if [ "$LAST_CREATE_CODE" = "201" ]; then
break
fi
if [ "$LAST_CREATE_CODE" = "422" ]; then
N=$((N + 1))
continue
fi
exit 1
done
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "float_tag=$FLOAT" >> "$GITHUB_OUTPUT"
echo "is_prod=$IS_PROD" >> "$GITHUB_OUTPUT"
echo "Reserved version: $VERSION"
publish-docker:
needs: [validate, reserve-version]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- app: api
dockerfile: infra/docker/local/api/Dockerfile
- app: management-api
dockerfile: infra/docker/local/management-api/Dockerfile
- app: web
dockerfile: infra/docker/local/web/Dockerfile
- app: web-sidecar
dockerfile: infra/docker/local/web-sidecar/Dockerfile
- app: management-web
dockerfile: infra/docker/local/management-web/Dockerfile
- app: management-web-sidecar
dockerfile: infra/docker/local/management-web-sidecar/Dockerfile
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: |
ghcr.io/${{ github.repository }}/${{ matrix.app }}:${{ needs.reserve-version.outputs.version }}
ghcr.io/${{ github.repository }}/${{ matrix.app }}:${{ needs.reserve-version.outputs.float_tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
verify-published-tags:
needs: [reserve-version, publish-docker]
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
steps:
- name: Verify expected tags in GHCR
env:
VERSION: ${{ needs.reserve-version.outputs.version }}
FLOAT_TAG: ${{ needs.reserve-version.outputs.float_tag }}
GHCR_TOKEN_PRIMARY: ${{ secrets.GHCR_REGISTRY_TOKEN }}
GHCR_TOKEN_FALLBACK: ${{ secrets.GITHUB_TOKEN }}
run: |
GHCR_TOKEN="$GHCR_TOKEN_PRIMARY"
if [ -z "$GHCR_TOKEN" ]; then
GHCR_TOKEN="$GHCR_TOKEN_FALLBACK"
echo "GHCR_REGISTRY_TOKEN not set; falling back to GITHUB_TOKEN for verification."
fi
APPS="api management-api web web-sidecar management-web management-web-sidecar"
for APP in $APPS; do
IMAGE_PATH="${{ github.repository }}/${APP}"
echo "Checking tags for ${IMAGE_PATH}"
TAGS_RESPONSE=$(curl -s -w "\n%{http_code}" \
-H "Authorization: Bearer $GHCR_TOKEN" \
"https://ghcr.io/v2/${IMAGE_PATH}/tags/list")
TAGS_JSON=$(echo "$TAGS_RESPONSE" | sed '$d')
TAGS_STATUS=$(echo "$TAGS_RESPONSE" | tail -n1)
if [ "$TAGS_STATUS" != "200" ]; then
echo "Tag verification failed for ${IMAGE_PATH} (HTTP ${TAGS_STATUS})."
exit 1
fi
if ! echo "$TAGS_JSON" | jq -e --arg VERSION "$VERSION" '.tags | index($VERSION)' >/dev/null; then
echo "Missing version tag ${VERSION} for ${IMAGE_PATH}."
exit 1
fi
if ! echo "$TAGS_JSON" | jq -e --arg T "$FLOAT_TAG" '.tags | index($T)' >/dev/null; then
echo "Missing floating tag ${FLOAT_TAG} for ${IMAGE_PATH}."
exit 1
fi
done
workflow-summary:
needs: [reserve-version, verify-published-tags]
runs-on: ubuntu-latest
steps:
- name: Published image version (workflow summary)
env:
VERSION: ${{ needs.reserve-version.outputs.version }}
REPO: ${{ github.repository }}
FT: ${{ needs.reserve-version.outputs.float_tag }}
run: |
{
echo "## Published Docker images (staging branch)"
echo ""
echo "**Image version (semver tag):** \`$VERSION\`"
echo ""
echo "Each image was also tagged **\`$FT\`** (floating)."
echo ""
echo "| Image | Tags |"
echo "|-------|------|"
for app in api management-api web web-sidecar management-web management-web-sidecar; do
echo "| \`ghcr.io/${REPO}/${app}\` | \`${VERSION}\`, \`${FT}\` |"
done
echo ""
echo "Pin GitOps to the version tag or the floating tag. Git tag matches the semver when created."
echo ""
echo "## All published image references"
for app in api management-api web web-sidecar management-web management-web-sidecar; do
echo "- \`ghcr.io/${REPO}/${app}:${VERSION}\` (version tag)"
echo "- \`ghcr.io/${REPO}/${app}:${FT}\` (floating tag)"
done
} >> "$GITHUB_STEP_SUMMARY"
github-prerelease-create:
needs: [reserve-version, verify-published-tags]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
- name: Create GitHub prerelease
uses: actions/github-script@v9
with:
script: |
const version = "${{ needs.reserve-version.outputs.version }}";
const baseVersion = version.replace(/-(staging|alpha|beta)\.\d+$/, "");
const path = "docs/development/CHANGELOGS/" + baseVersion + ".md";
const fs = require("fs");
let body;
if (fs.existsSync(path)) {
body = fs.readFileSync(path, "utf8");
} else {
body = "Prerelease build; no " + path + " at this commit. Bump version on develop at the start of work and write notes continuously in that file.";
}
const tag = version;
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: list } = await github.rest.repos.listReleases({ owner, repo, per_page: 100 });
if (list.some((r) => r.tag_name === tag)) {
core.info("Release for tag " + tag + " already exists; skip.");
return;
}
const rel = await github.rest.repos.createRelease({
owner, repo, tag_name: tag, name: tag, body, prerelease: true,
});
core.info("Created release: " + rel.data.html_url);