Skip to content

chore: bump version to 0.1.10 #2

chore: bump version to 0.1.10

chore: bump version to 0.1.10 #2

# Build and push staging images from the `staging` branch.
# Tags: X.Y.Z-staging.N and :staging.
name: Publish (staging)
on:
push:
branches:
- staging
workflow_dispatch:
inputs:
version_override:
description: "Optional version override (atomic Git tag 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
- name: Build 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: ruby scripts/env-classification/metaboost-env.rb merge-env --profile dev
--group web --output apps/web/.env.local
- name: Build 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"
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}"
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."
} >> "$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@v7
with:
script: |
const version = "${{ needs.reserve-version.outputs.version }}";
const path = "docs/operations/CHANGELOG-UPCOMING.md";
const fs = require("fs");
let body;
if (fs.existsSync(path)) {
body = fs.readFileSync(path, "utf8");
} else {
body = "Prerelease; no " + path + " at this commit.";
}
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;
}
await github.rest.repos.createRelease({
owner, repo, tag_name: tag, name: tag, body, prerelease: true,
});
changelog-pr-to-develop:
needs: [github-prerelease-create, reserve-version, verify-published-tags]
if: success()
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
ref: develop
persist-credentials: false
- name: Write archive and reset UPCOMING markers
uses: actions/github-script@v7
env:
PUBLISH_SHA: ${{ github.sha }}
PUBLISH_VERSION: ${{ needs.reserve-version.outputs.version }}
RUN_ID: ${{ github.run_id }}
REF_NAME: ${{ github.ref_name }}
with:
script: |
const fs = require("fs");
const owner = context.repo.owner;
const repo = context.repo.repo;
const ref = process.env.PUBLISH_SHA;
const version = process.env.PUBLISH_VERSION;
const runId = process.env.RUN_ID;
const refName = process.env.REF_NAME || "unknown";
const label = refName === "main" ? "production" : refName;
const upcomingPath = "docs/operations/CHANGELOG-UPCOMING.md";
let upcoming = "";
try {
const { data } = await github.rest.repos.getContent({ owner, repo, path: upcomingPath, ref });
if (data && data.type === "file" && "content" in data && "encoding" in data && data.encoding === "base64") {
upcoming = Buffer.from(String(data.content).replace(/\n/g, ""), "base64").toString("utf8");
}
} catch (e) {
if (e.status === 404) { upcoming = ""; } else { throw e; }
}
const safe = version.replace(/\./g, "-");
const dir = "docs/operations/CHANGELOG-ARCHIVE";
fs.mkdirSync(dir, { recursive: true });
const head =
"# Release " + version + " (" + label + ")\n\n" +
"From [`" + ref.substring(0, 7) + "`](https://github.com/" + owner + "/" + repo + "/commit/" + ref + "]) — " +
"[workflow run](https://github.com/" + owner + "/" + repo + "/actions/runs/" + runId + ").\n\n";
fs.writeFileSync(dir + "/RELEASE-" + safe + ".md", head + (upcoming || "_(no CHANGELOG-UPCOMING at that commit.)_\n"));
if (fs.existsSync(upcomingPath)) {
let s = fs.readFileSync(upcomingPath, "utf8");
if (s.includes("UPCOMING-AUTO-START") && s.includes("UPCOMING-AUTO-END")) {
s = s.replace(
/<!--\s*UPCOMING-AUTO-START\s*-->[\s\S]*?<!--\s*UPCOMING-AUTO-END\s*-->/m,
"<!-- UPCOMING-AUTO-START -->\n" +
"<!-- Add concise bullets (most important first) for the next significant changes. -->\n" +
"<!-- UPCOMING-AUTO-END -->\n"
);
fs.writeFileSync(upcomingPath, s, "utf8");
}
}
- uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: archive changelog for ${{ github.ref_name }} ${{ needs.reserve-version.outputs.version }}"
title: "chore: archive changelog for ${{ needs.reserve-version.outputs.version }} (${{ github.ref_name }})"
body: |
Automated follow-up to a successful [publish](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) on `${{ github.ref_name }}`.
base: develop
branch: automation/changelog-${{ github.ref_name }}-${{ needs.reserve-version.outputs.version }}-${{ github.run_id }}
delete-branch: true
signoff: false