diff --git a/.github/workflows/pr-title-sync.yml b/.github/workflows/pr-title-sync.yml new file mode 100644 index 00000000..023f5f66 --- /dev/null +++ b/.github/workflows/pr-title-sync.yml @@ -0,0 +1,64 @@ +name: PR Title Sync + +on: + pull_request: + types: [opened, synchronize, edited] + paths: + - 'VERSION' + +concurrency: + group: pr-title-sync-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + sync: + name: Sync PR title to VERSION + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + if: github.actor != 'github-actions[bot]' + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + with: + fetch-depth: 1 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Read VERSION + current title + id: inspect + run: | + set -euo pipefail + VERSION=$(cat VERSION | tr -d '[:space:]') + TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + # Only rewrite titles that ALREADY follow the v prefix pattern. + # Custom titles (no prefix) are left alone — user kept them intentionally. + if printf '%s' "$TITLE" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ '; then + PREFIX=$(printf '%s' "$TITLE" | awk '{print $1}') + REST=$(printf '%s' "$TITLE" | sed 's/^v[0-9][0-9.]* //') + { + echo "prefix=$PREFIX" + echo "rest=$REST" + echo "eligible=true" + } >> "$GITHUB_OUTPUT" + else + echo "eligible=false" >> "$GITHUB_OUTPUT" + fi + + - name: Rewrite title if version changed + if: steps.inspect.outputs.eligible == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUM: ${{ github.event.pull_request.number }} + NEW_V: ${{ steps.inspect.outputs.version }} + OLD_PREFIX: ${{ steps.inspect.outputs.prefix }} + REST: ${{ steps.inspect.outputs.rest }} + run: | + if [ "v$NEW_V" = "$OLD_PREFIX" ]; then + echo "Title already matches v$NEW_V; no change." + exit 0 + fi + NEW_TITLE="v$NEW_V $REST" + echo "Rewriting: $OLD_PREFIX ... → v$NEW_V ..." + gh pr edit "$PR_NUM" --title "$NEW_TITLE" diff --git a/.github/workflows/version-gate.yml b/.github/workflows/version-gate.yml new file mode 100644 index 00000000..58f2432c --- /dev/null +++ b/.github/workflows/version-gate.yml @@ -0,0 +1,73 @@ +name: Version Gate + +on: + pull_request: + paths: + - 'VERSION' + - 'CHANGELOG.md' + - 'package.json' + +concurrency: + group: version-gate-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + check: + name: Check VERSION is not stale vs queue + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + steps: + - name: Checkout PR head + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + + - name: Read versions + id: versions + run: | + set -euo pipefail + PR_VERSION=$(cat VERSION | tr -d '[:space:]') + BASE_REF="${{ github.event.pull_request.base.ref }}" + git fetch origin "$BASE_REF" --depth=1 --quiet || true + BASE_VERSION=$(git show "origin/$BASE_REF:VERSION" 2>/dev/null | tr -d '[:space:]' || echo "0.0.0.0") + { + echo "pr_version=$PR_VERSION" + echo "base_version=$BASE_VERSION" + echo "base_ref=$BASE_REF" + } >> "$GITHUB_OUTPUT" + + - name: Detect bump level + id: bump + run: | + LEVEL=$(bun run scripts/detect-bump.ts "${{ steps.versions.outputs.base_version }}" "${{ steps.versions.outputs.pr_version }}") + echo "level=$LEVEL" >> "$GITHUB_OUTPUT" + + - name: Query queue (util) — fail-open on error + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set +e + bun run bin/gstack-next-version \ + --base "${{ steps.versions.outputs.base_ref }}" \ + --bump "${{ steps.bump.outputs.level }}" \ + --current-version "${{ steps.versions.outputs.base_version }}" \ + --workspace-root null \ + > next.json 2> next.err + RC=$? + if [ "$RC" != "0" ] || [ ! -s next.json ]; then + echo '{"offline":true}' > next.json + echo "::warning::util exit=$RC — failing open. stderr:" + cat next.err || true + fi + + - name: Compare PR VERSION to next free slot + env: + PR_VERSION: ${{ steps.versions.outputs.pr_version }} + run: | + bun run scripts/compare-pr-version.ts next.json "${{ github.event.pull_request.number }}" diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..c755dee7 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,71 @@ +# GitLab CI parity for workspace-aware ship. +# Mirrors .github/workflows/version-gate.yml and pr-title-sync.yml. +# Projects that mirror to GitLab get the same protection as GitHub. + +stages: + - check + +variables: + BUN_VERSION: "1.3.10" + +.setup-bun: &setup-bun + - apt-get update -qq && apt-get install -qq -y curl jq git + - curl -fsSL https://bun.sh/install | bash -s "bun-v$BUN_VERSION" + - export PATH="$HOME/.bun/bin:$PATH" + +version-gate: + stage: check + image: debian:stable-slim + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + changes: + - VERSION + - CHANGELOG.md + - package.json + script: + - *setup-bun + - PR_VERSION=$(cat VERSION | tr -d '[:space:]') + - BASE_VERSION=$(git show "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME:VERSION" 2>/dev/null | tr -d '[:space:]' || echo "0.0.0.0") + - LEVEL=$(bun run scripts/detect-bump.ts "$BASE_VERSION" "$PR_VERSION") + # Util fail-open: on non-zero exit, emit offline marker + - | + set +e + bun run bin/gstack-next-version \ + --base "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" \ + --bump "$LEVEL" \ + --current-version "$BASE_VERSION" \ + --workspace-root null \ + > next.json + RC=$? + if [ "$RC" != "0" ] || [ ! -s next.json ]; then + echo '{"offline":true}' > next.json + echo "WARNING: util exit=$RC — failing open" + fi + set -e + - PR_VERSION="$PR_VERSION" bun run scripts/compare-pr-version.ts next.json "$CI_MERGE_REQUEST_IID" + +pr-title-sync: + stage: check + image: debian:stable-slim + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + changes: + - VERSION + script: + - apt-get update -qq && apt-get install -qq -y curl jq git + - curl -fsSL https://gitlab.com/gitlab-org/cli/-/releases/permalink/latest/downloads/glab_linux_amd64.deb -o glab.deb && dpkg -i glab.deb + - VERSION=$(cat VERSION | tr -d '[:space:]') + - TITLE="$CI_MERGE_REQUEST_TITLE" + - | + if printf '%s' "$TITLE" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ '; then + PREFIX=$(printf '%s' "$TITLE" | awk '{print $1}') + REST=$(printf '%s' "$TITLE" | sed 's/^v[0-9][0-9.]* //') + if [ "v$VERSION" != "$PREFIX" ]; then + echo "Rewriting: $PREFIX ... → v$VERSION ..." + glab mr update "$CI_MERGE_REQUEST_IID" -t "v$VERSION $REST" + else + echo "Title already matches v$VERSION; no change." + fi + else + echo "Title does not use v prefix — leaving alone." + fi