-
Notifications
You must be signed in to change notification settings - Fork 1
243 lines (218 loc) · 10.5 KB
/
Copy pathrelease-please.yml
File metadata and controls
243 lines (218 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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