refactor: replace VERSION file with version.txt for version managemen… #76
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
| name: Release | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| gateway_tag: | |
| description: 'Re-run GoReleaser for this tag (e.g. gateway/v0.1.2). Leave empty to skip.' | |
| required: false | |
| default: '' | |
| republish_npm: | |
| description: 'Re-publish all npm packages' | |
| required: false | |
| type: boolean | |
| default: false | |
| # Prevent concurrent release runs racing each other. | |
| # cancel-in-progress: false ensures an in-flight release always completes. | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write # npm provenance OIDC signing | |
| packages: write # push to GHCR | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # recover-stale-releases | |
| # | |
| # Runs before release-please on every push. Idempotently creates any git tags | |
| # that the manifest says should exist but don't (e.g. when the tagging step | |
| # failed on a previous run), and relabels merged release PRs that are still | |
| # carrying the 'autorelease: pending' label. This permanently prevents the | |
| # "untagged merged release PRs outstanding - aborting" failure that occurs | |
| # whenever the tagging step doesn't complete. | |
| # --------------------------------------------------------------------------- | |
| recover-stale-releases: | |
| name: Recover Stale Releases | |
| runs-on: ubuntu-latest | |
| outputs: | |
| npm_tags_created: ${{ steps.recover.outputs.npm_tags_created }} | |
| # Runs on every trigger but steps are conditioned on push events. | |
| # This ensures the job always succeeds (never "skipped"), so downstream | |
| # jobs can depend on it without if: always() — which breaks output propagation. | |
| steps: | |
| - uses: actions/checkout@v4 | |
| if: github.event_name == 'push' | |
| with: | |
| fetch-depth: 0 | |
| - name: Create missing manifest tags and relabel stale PRs | |
| id: recover | |
| if: github.event_name == 'push' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| git fetch --tags --quiet | |
| # Map: package path → tag prefix (must match include-component-in-tag convention) | |
| declare -A TAG_PREFIX=( | |
| ["sdk"]="sdk-v" | |
| ["cli"]="cli-v" | |
| ["codemod"]="codemod-v" | |
| ["adapters/react"]="react-v" | |
| ["adapters/vue"]="vue-v" | |
| ["adapters/svelte"]="svelte-v" | |
| ["plugins/vite"]="vite-v" | |
| ["plugins/next"]="next-v" | |
| ) | |
| TAGGED=0 | |
| for pkg_path in "sdk" "cli" "codemod" "adapters/react" "adapters/vue" "adapters/svelte" "plugins/vite" "plugins/next"; do | |
| manifest_ver=$(jq -r --arg p "$pkg_path" '.[$p]' .release-please-manifest.json) | |
| pkg_ver=$(jq -r '.version' "${pkg_path}/package.json") | |
| prefix="${TAG_PREFIX[$pkg_path]}" | |
| tag="${prefix}${manifest_ver}" | |
| # Only create the tag when both sources agree on the version (meaning | |
| # the release PR was merged) and the tag doesn't already exist. | |
| if [[ "$manifest_ver" == "$pkg_ver" ]] && ! git rev-parse "$tag" >/dev/null 2>&1; then | |
| commit=$(git log --format="%H" -1 -- "${pkg_path}/package.json") | |
| echo "::notice::Creating missing tag $tag at $commit" | |
| git tag "$tag" "$commit" | |
| git push origin "$tag" | |
| TAGGED=1 | |
| fi | |
| done | |
| # Signal that new npm package tags were created (release PR was merged | |
| # but release-please failed to tag — common with grouped PRs). | |
| echo "npm_tags_created=${TAGGED}" >> "$GITHUB_OUTPUT" | |
| # If we just created tags, find any merged release PRs that are still | |
| # labeled 'autorelease: pending' and flip them to 'autorelease: tagged' | |
| # so that release-please no longer considers them untagged. | |
| if [[ "$TAGGED" -eq 1 ]]; then | |
| gh pr list \ | |
| --label "autorelease: pending" \ | |
| --state merged \ | |
| --json number \ | |
| --jq '.[].number' \ | |
| | while read -r pr_number; do | |
| echo "::notice::Relabeling PR #${pr_number}: autorelease: pending → autorelease: tagged" | |
| gh pr edit "$pr_number" \ | |
| --remove-label "autorelease: pending" \ | |
| --add-label "autorelease: tagged" | |
| done | |
| fi | |
| release-please: | |
| name: Release Please | |
| runs-on: ubuntu-latest | |
| needs: recover-stale-releases | |
| if: ${{ !cancelled() }} | |
| outputs: | |
| releases_created: ${{ steps.resolve.outputs.releases_created }} | |
| paths_released: ${{ steps.resolve.outputs.paths_released }} | |
| gateway_release_created: ${{ steps.resolve.outputs.gateway_release_created }} | |
| gateway_tag_name: ${{ steps.resolve.outputs.gateway_tag_name }} | |
| npm_publish: ${{ steps.resolve.outputs.npm_publish }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Validate release-please title patterns | |
| if: github.event_name == 'push' | |
| run: | | |
| set -euo pipefail | |
| pattern=$(jq -r '."pull-request-title-pattern" // empty' release-please-config.json) | |
| group_pattern=$(jq -r '."group-pull-request-title-pattern" // empty' release-please-config.json) | |
| for token in '${scope}' '${component}' '${version}'; do | |
| if [[ "$pattern" != *"$token"* ]]; then | |
| echo "::error::pull-request-title-pattern must include $token" | |
| exit 1 | |
| fi | |
| done | |
| for token in '${scope}'; do | |
| if [[ "$group_pattern" != *"$token"* ]]; then | |
| echo "::error::group-pull-request-title-pattern must include $token" | |
| exit 1 | |
| fi | |
| done | |
| - uses: googleapis/release-please-action@v4 | |
| id: release | |
| if: github.event_name == 'push' | |
| with: | |
| config-file: release-please-config.json | |
| manifest-file: .release-please-manifest.json | |
| - name: Resolve outputs | |
| id: resolve | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| GATEWAY_TAG="${{ inputs.gateway_tag }}" | |
| if [[ -n "$GATEWAY_TAG" ]]; then GATEWAY_RELEASE_CREATED="true"; else GATEWAY_RELEASE_CREATED=""; fi | |
| if [[ "${{ inputs.republish_npm }}" == "true" ]]; then NPM_PUBLISH="true"; else NPM_PUBLISH=""; fi | |
| if [[ -n "$GATEWAY_RELEASE_CREATED" || -n "$NPM_PUBLISH" ]]; then RELEASES_CREATED="true"; else RELEASES_CREATED=""; fi | |
| { | |
| echo "releases_created=${RELEASES_CREATED}" | |
| echo "paths_released=" | |
| echo "gateway_release_created=${GATEWAY_RELEASE_CREATED}" | |
| echo "gateway_tag_name=${GATEWAY_TAG}" | |
| echo "npm_publish=${NPM_PUBLISH}" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "::notice::workflow_dispatch — npm_publish=${NPM_PUBLISH}, gateway_release_created=${GATEWAY_RELEASE_CREATED}, gateway_tag=${GATEWAY_TAG}" | |
| else | |
| # All npm packages use linked-versions, so sdk is representative. | |
| RP_NPM="${{ steps.release.outputs['sdk--release_created'] }}" | |
| # Fallback: release-please cannot create GitHub Releases from grouped | |
| # PRs (the merged PR title lacks ${component}). When that happens the | |
| # recover-stale-releases job still creates the git tags. Use its | |
| # npm_tags_created output as a reliable signal that a release PR was | |
| # merged and npm packages should be published. | |
| RECOVER_NPM="${{ needs.recover-stale-releases.outputs.npm_tags_created }}" | |
| if [[ "$RP_NPM" == "true" || "$RECOVER_NPM" == "1" ]]; then | |
| NPM_PUBLISH="true" | |
| else | |
| NPM_PUBLISH="" | |
| fi | |
| { | |
| echo "releases_created=${{ steps.release.outputs.releases_created }}" | |
| echo "paths_released=${{ steps.release.outputs.paths_released }}" | |
| echo "gateway_release_created=${{ steps.release.outputs['gateway--release_created'] }}" | |
| echo "gateway_tag_name=${{ steps.release.outputs['gateway--tag_name'] }}" | |
| echo "npm_publish=${NPM_PUBLISH}" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "::notice::push — releases_created=${{ steps.release.outputs.releases_created }}, rp_npm=${RP_NPM}, recover_npm=${RECOVER_NPM}, npm_publish=${NPM_PUBLISH}, gateway=${{ steps.release.outputs['gateway--release_created'] }}" | |
| fi | |
| publish-npm: | |
| name: Publish to npm | |
| runs-on: ubuntu-latest | |
| needs: release-please | |
| if: needs.release-please.outputs.npm_publish == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'pnpm' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm run build | |
| - name: Test all packages | |
| run: pnpm run test | |
| - name: Publish sdk | |
| working-directory: sdk | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/sdk@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "sdk@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish cli | |
| working-directory: cli | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/cli@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "cli@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish codemod | |
| working-directory: codemod | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/codemod@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "codemod@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish react adapter | |
| working-directory: adapters/react | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/react@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "react@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish vue adapter | |
| working-directory: adapters/vue | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/vue@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "vue@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish svelte adapter | |
| working-directory: adapters/svelte | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/svelte@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "svelte@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish vite plugin | |
| working-directory: plugins/vite | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/vite@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "vite@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Publish next plugin | |
| working-directory: plugins/next | |
| run: | | |
| V=$(node -p "require('./package.json').version") | |
| PUBLISHED=$(npm view @rep-protocol/next@${V} version 2>/dev/null || true) | |
| if [ "$PUBLISHED" = "$V" ]; then echo "next@${V} already published, skipping."; else pnpm publish --provenance --access public --no-git-checks; fi | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| release-gateway: | |
| name: GoReleaser | |
| runs-on: ubuntu-latest | |
| needs: release-please | |
| if: needs.release-please.outputs.gateway_release_created == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version: '1.24.5' | |
| cache-dependency-path: gateway/go.sum | |
| - name: Extract version from tag | |
| id: version | |
| # Tag format: gateway/v0.1.0 → v0.1.0 | |
| run: | | |
| TAG="${{ needs.release-please.outputs.gateway_tag_name }}" | |
| echo "tag=${TAG#gateway/}" >> "$GITHUB_OUTPUT" | |
| echo "version=${TAG#gateway/v}" >> "$GITHUB_OUTPUT" | |
| - name: Create local tag for GoReleaser validation | |
| # GoReleaser requires GORELEASER_CURRENT_TAG to exist as a real git tag. | |
| # release-please creates gateway/vX.Y.Z; we alias it locally as vX.Y.Z. | |
| # This tag is never pushed — it exists only in this CI workspace. | |
| run: | | |
| git tag "${{ steps.version.outputs.tag }}" | |
| - name: Run GoReleaser | |
| uses: goreleaser/goreleaser-action@v6 | |
| with: | |
| version: '~> v2' | |
| args: release --clean | |
| workdir: gateway | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.tag }} | |
| docker-gateway: | |
| name: Docker multi-arch | |
| runs-on: ubuntu-latest | |
| needs: [release-please, release-gateway] | |
| if: needs.release-please.outputs.gateway_release_created == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract version from tag | |
| id: version | |
| run: | | |
| TAG="${{ needs.release-please.outputs.gateway_tag_name }}" | |
| echo "version=${TAG#gateway/}" >> "$GITHUB_OUTPUT" | |
| - name: Build and push multi-arch image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: gateway | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: | | |
| ghcr.io/ruachtech/rep/gateway:${{ steps.version.outputs.version }} | |
| ghcr.io/ruachtech/rep/gateway:latest |