Release #36
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 | |
| if: github.event_name == 'push' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Create missing manifest tags and relabel stale PRs | |
| 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" | |
| ) | |
| TAGGED=0 | |
| for pkg_path in "sdk" "cli" "codemod" "adapters/react" "adapters/vue" "adapters/svelte"; 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 | |
| # 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: always() # run even if recover-stale-releases was skipped (workflow_dispatch) | |
| 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}' '${version}'; 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: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| GATEWAY_TAG="${{ inputs.gateway_tag }}" | |
| GATEWAY_RELEASE_CREATED=$([[ -n "$GATEWAY_TAG" ]] && echo "true" || echo "") | |
| NPM_PUBLISH=$([[ "${{ inputs.republish_npm }}" == "true" ]] && echo "true" || echo "") | |
| RELEASES_CREATED=$([[ -n "$GATEWAY_RELEASE_CREATED" || -n "$NPM_PUBLISH" ]] && echo "true" || echo "") | |
| echo "releases_created=${RELEASES_CREATED}" >> "$GITHUB_OUTPUT" | |
| echo "paths_released=" >> "$GITHUB_OUTPUT" | |
| echo "gateway_release_created=${GATEWAY_RELEASE_CREATED}" >> "$GITHUB_OUTPUT" | |
| echo "gateway_tag_name=${GATEWAY_TAG}" >> "$GITHUB_OUTPUT" | |
| echo "npm_publish=${NPM_PUBLISH}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "releases_created=${{ steps.release.outputs.releases_created }}" >> "$GITHUB_OUTPUT" | |
| echo "paths_released=${{ steps.release.outputs.paths_released }}" >> "$GITHUB_OUTPUT" | |
| echo "gateway_release_created=${{ steps.release.outputs['gateway--release_created'] }}" >> "$GITHUB_OUTPUT" | |
| echo "gateway_tag_name=${{ steps.release.outputs['gateway--tag_name'] }}" >> "$GITHUB_OUTPUT" | |
| echo "npm_publish=${{ steps.release.outputs.releases_created }}" >> "$GITHUB_OUTPUT" | |
| 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 npm publish --provenance --access public; 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 npm publish --provenance --access public; 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 npm publish --provenance --access public; 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 npm publish --provenance --access public; 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 npm publish --provenance --access public; 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 npm publish --provenance --access public; 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 |