chore: promote from staging 4860bd4 #52
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 Please | |
| on: | |
| push: | |
| branches: | |
| - next | |
| - main | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| concurrency: | |
| group: release-please-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| release-please: | |
| if: github.repository == 'team-telnyx/telnyx-cli' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.SDK_WRITE_TOKEN }} | |
| - name: Set up Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Install release-please | |
| run: npm install --no-save release-please@17 | |
| - name: Read Release-As version from next branch | |
| if: github.ref == 'refs/heads/next' | |
| id: version | |
| run: | | |
| VERSION="" | |
| for commit in $(git rev-list HEAD --max-count=50); do | |
| BODY=$(git log -1 --format=%b "$commit") | |
| RA=$(echo "$BODY" | grep -oP 'Release-As:\s*\K[\d.]+' | head -1 || true) | |
| if [ -n "$RA" ]; then | |
| VERSION="$RA" | |
| break | |
| fi | |
| done | |
| if [ -z "$VERSION" ]; then | |
| echo "::warning::No Release-As found; release-please will use semantic versioning" | |
| echo "has_version=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "has_version=true" >> "$GITHUB_OUTPUT" | |
| echo "Release-As version: $VERSION" | |
| fi | |
| - name: Run release-please (release-pr) | |
| if: github.ref == 'refs/heads/next' | |
| id: release-pr | |
| run: | | |
| ARGS=( | |
| release-pr | |
| --repo-url "${{ github.repositoryUrl }}" | |
| --token "${{ secrets.SDK_WRITE_TOKEN }}" | |
| --target-branch main | |
| --config-file release-please-config.json | |
| --manifest-file .release-please-manifest.json | |
| --local | |
| ) | |
| if [ "${{ steps.version.outputs.has_version }}" = "true" ]; then | |
| ARGS+=(--release-as "${{ steps.version.outputs.version }}") | |
| fi | |
| OUTPUT=$(npx release-please "${ARGS[@]}" 2>&1) || true | |
| echo "$OUTPUT" | |
| PR_URL=$(echo "$OUTPUT" | grep -oP 'https://(?:api\.)?github\.com/(?:repos/[^/]+/[^/]+/)?pulls?/\d+' | head -1) | |
| if [ -n "$PR_URL" ]; then | |
| PR_NUM=$(echo "$PR_URL" | grep -oP '\d+$') | |
| echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT" | |
| echo "prs_created=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "prs_created=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Ensure release PR carries autorelease label (self-heal) | |
| if: github.ref == 'refs/heads/next' | |
| env: | |
| GH_TOKEN: ${{ secrets.SDK_WRITE_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| # release-please applies `autorelease: pending` in a separate API call | |
| # AFTER opening the PR; a failure between the two is swallowed by the | |
| # `|| true` above and leaves the PR unlabeled. The label is how the | |
| # sync fallback, the auto-merge fallback, AND github-release (after | |
| # merge) find release PRs — an unlabeled one merges but never tags | |
| # (ghost release). Re-assert the label on any open release-please PR. | |
| for pr in $(gh pr list --repo "$REPO" --state open --json number,headRefName,labels \ | |
| --jq '.[] | select(.headRefName | startswith("release-please--")) | |
| | select(([.labels[].name] | index("autorelease: pending")) | not) | |
| | .number'); do | |
| gh pr edit "$pr" --repo "$REPO" --add-label "autorelease: pending" \ | |
| && echo "Re-added missing autorelease label to PR #$pr" \ | |
| || echo "::warning::Failed to add autorelease label to PR #$pr" | |
| done | |
| - name: Sync next code into release PR (tree replacement, not merge) | |
| if: github.ref == 'refs/heads/next' | |
| env: | |
| GH_TOKEN: ${{ secrets.SDK_WRITE_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PR_NUM: ${{ steps.release-pr.outputs.pr_number }} | |
| run: | | |
| # release-please creates a PR with only version bumps (CHANGELOG, | |
| # version.go, README, CHANGELOG.md, .release-please-manifest.json). | |
| # The actual SDK code changes are on `next` but NOT included in the | |
| # release PR. This step syncs `next` code into the release PR branch. | |
| # | |
| # CRITICAL: We use tree replacement (checkout + restore version files) | |
| # instead of git merge. A 3-way merge produces duplicate method | |
| # implementations when staging and prod have different method ordering | |
| # (git sees them as non-conflicting additions and keeps both copies). | |
| # Tree replacement avoids this entirely — the code tree comes from | |
| # `next` as-is, and only the version-bump files from release-please | |
| # are preserved on top. | |
| PR_BRANCH="" | |
| if [ -n "$PR_NUM" ]; then | |
| sleep 3 # brief propagation delay | |
| PR_BRANCH=$(gh pr view "$PR_NUM" --repo "$REPO" --json headRefName --jq '.headRefName' 2>/dev/null || true) | |
| fi | |
| # Fallback: query by label with retry | |
| if [ -z "$PR_BRANCH" ]; then | |
| for i in 1 2 3; do | |
| PR_BRANCH=$(gh pr list --repo "$REPO" --state open --label "autorelease: pending" --json headRefName --jq '.[0].headRefName' 2>/dev/null || true) | |
| if [ -n "$PR_BRANCH" ]; then break; fi | |
| echo "PR not found yet, retrying in 5s (attempt $i)..." | |
| sleep 5 | |
| done | |
| fi | |
| if [ -z "$PR_BRANCH" ]; then | |
| echo "::warning::No open release PR found after retries, skipping sync" | |
| exit 0 | |
| fi | |
| echo "Found release PR branch: $PR_BRANCH" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git fetch origin "$PR_BRANCH" next | |
| git checkout -B "$PR_BRANCH" "origin/$PR_BRANCH" | |
| # Save version-bump files from the release PR branch (release-please output) | |
| STASH_DIR=$(mktemp -d) | |
| for f in CHANGELOG.md pkg/cmd/version.go README.md .release-please-manifest.json; do | |
| if [ -f "$f" ]; then | |
| mkdir -p "$STASH_DIR/$(dirname "$f")" | |
| cp "$f" "$STASH_DIR/$f" | |
| fi | |
| done | |
| # Replace entire working tree from next (exact tree, no merge) | |
| # git rm handles deletions (files removed in next but still in this branch) | |
| # git clean removes untracked files (node_modules created by npx release-please) | |
| git reset --hard origin/next | |
| git clean -fdx | |
| # Restore version-bump files from the release PR branch | |
| for f in CHANGELOG.md pkg/cmd/version.go README.md .release-please-manifest.json; do | |
| if [ -f "$STASH_DIR/$f" ]; then | |
| mkdir -p "$(dirname "$f")" | |
| cp "$STASH_DIR/$f" "$f" | |
| git add "$f" | |
| fi | |
| done | |
| rm -rf "$STASH_DIR" | |
| # Commit if there are changes | |
| if git diff --cached --quiet; then | |
| echo "No changes after syncing next code" | |
| else | |
| git commit -m "chore: sync code from next into release PR" | |
| git push --force origin "$PR_BRANCH" | |
| echo "Successfully synced next code into release PR branch (tree replacement)" | |
| fi | |
| - name: Run release-please (github-release) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| npx release-please github-release \ | |
| --repo-url "${{ github.repositoryUrl }}" \ | |
| --token "${{ secrets.SDK_WRITE_TOKEN }}" \ | |
| --target-branch main \ | |
| --config-file release-please-config.json \ | |
| --manifest-file .release-please-manifest.json \ | |
| 2>&1 | |
| - name: Verify release commit produced a tag (ghost-release guard) | |
| if: github.ref == 'refs/heads/main' | |
| run: | | |
| # `github-release` exits 0 even when it finds nothing to release ([]). | |
| # If the commit that triggered this run is a merged `release: X.Y.Z` | |
| # commit, a missing vX.Y.Z tag here means it silently no-opped — | |
| # usually a merged release PR that lost its `autorelease: pending` | |
| # label. Fail loud instead of ghosting the release. | |
| MSG=$(git log -1 --format=%s "$GITHUB_SHA") | |
| VER=$(echo "$MSG" | grep -oP '^release:\s*\K[0-9][0-9.]*' || true) | |
| if [ -z "$VER" ]; then | |
| echo "Not a release commit; nothing to verify" | |
| exit 0 | |
| fi | |
| if git ls-remote --exit-code --tags origin "v$VER" >/dev/null 2>&1; then | |
| echo "Tag v$VER exists - release verified" | |
| else | |
| echo "::error::Merged release commit for $VER but no v$VER tag exists. github-release silently no-opped (merged release PR likely missing the autorelease pending label). Add the label to the merged PR and re-run this workflow." | |
| exit 1 | |
| fi | |
| - name: Enable auto-merge on release PRs | |
| if: github.ref == 'refs/heads/next' && steps.release-pr.outputs.prs_created == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.SDK_WRITE_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| PR_NUM: ${{ steps.release-pr.outputs.pr_number }} | |
| run: | | |
| if [ -n "$PR_NUM" ]; then | |
| gh pr merge "$PR_NUM" --auto --squash --repo "$REPO" 2>/dev/null || echo "Auto-merge already enabled or PR #$PR_NUM not mergeable" | |
| fi | |
| sleep 3 | |
| prs=$(gh pr list --repo "$REPO" --state open --label "autorelease: pending" --json number --jq '.[].number') | |
| for pr in $prs; do | |
| gh pr merge "$pr" --auto --squash --repo "$REPO" 2>/dev/null || true | |
| done |