fix(cli): use PowerShell Expand-Archive for zip extraction on Windows… #43
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 | |
| # 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 | |
| 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" | |
| ) | |
| 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: ${{ !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}' '${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: | | |
| 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. | |
| # No fallback to releases_created — that includes gateway-only releases. | |
| NPM_PUBLISH="${{ steps.release.outputs['sdk--release_created'] }}" | |
| { | |
| 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 }}, 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 }} | |
| 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 |