Skip to content

fix(release): make the --local scan clone see the repointed release b… #68

fix(release): make the --local scan clone see the repointed release b…

fix(release): make the --local scan clone see the repointed release b… #68

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
# Widen the --local commit scan to next's promote commits: release-pr
# collects commits since the last release on the TARGET branch (main),
# so with only hidden ci:-typed commits on main it silently skips
# (releases previously depended on coincidental fix-typed hotfixes).
# next always contains main (the promote enforces ancestry), so
# pointing the local refs at HEAD only widens the scan; the PR is
# still created against main via the API.
git branch -f main HEAD 2>/dev/null || true
git update-ref refs/remotes/origin/main HEAD
# --local mode clones from --repo-url — a FRESH GitHub clone whose
# refs ignore the repoint above (it only rewrites this workspace), so
# the scan saw 0 commits on a quiet master and silently skipped the
# release PR (2026-07-09). Rewrite the URL to this workspace so the
# CLI clones the repointed refs; API PR-creation still parses
# owner/repo from the URL string.
git config --global url."file://${GITHUB_WORKSPACE}".insteadOf "${{ github.repositoryUrl }}"
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'
env:
GH_TOKEN: ${{ github.token }}
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
if ! gh api "repos/${{ github.repository }}/releases/tags/v$VER" >/dev/null 2>&1; then
echo "::error::Tag v$VER exists but there is no GitHub Release for it - the publish workflow (release: published) will never fire. Create it: gh release create v$VER --generate-notes"
exit 1
fi
echo "Tag v$VER and its GitHub Release exist - 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