chore: bump version to 0.1.10 #2
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
| # 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 |