Skip to content

refactor: replace VERSION file with version.txt for version managemen… #76

refactor: replace VERSION file with version.txt for version managemen…

refactor: replace VERSION file with version.txt for version managemen… #76

Workflow file for this run

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