Skip to content

Commit 5043044

Browse files
authored
Merge pull request #24 from redis-developer/feat/auto-release
feat: adopt Auto-based release strategy from redis-vl-python
2 parents cb88173 + ebf58e5 commit 5043044

4 files changed

Lines changed: 326 additions & 83 deletions

File tree

.autorc

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{
2+
"plugins": [
3+
"git-tag",
4+
"conventional-commits",
5+
"released"
6+
],
7+
"owner": "redis-developer",
8+
"repo": "sql-redis",
9+
"onlyPublishWithReleaseLabel": true,
10+
"labels": [
11+
{
12+
"name": "auto:major",
13+
"changelogTitle": "💥 Breaking Change",
14+
"description": "Increment the major version when merged",
15+
"releaseType": "major",
16+
"color": "#C5000B",
17+
"overwrite": true
18+
},
19+
{
20+
"name": "auto:minor",
21+
"changelogTitle": "🚀 Enhancement",
22+
"description": "Increment the minor version when merged",
23+
"releaseType": "minor",
24+
"color": "#F1A60E",
25+
"overwrite": true
26+
},
27+
{
28+
"name": "auto:patch",
29+
"changelogTitle": "🐛 Bug Fix",
30+
"description": "Increment the patch version when merged",
31+
"releaseType": "patch",
32+
"color": "#870048",
33+
"overwrite": true
34+
},
35+
{
36+
"name": "auto:skip-release",
37+
"description": "Preserve the current version when merged",
38+
"releaseType": "skip",
39+
"color": "#bf5416",
40+
"overwrite": true
41+
},
42+
{
43+
"name": "auto:release",
44+
"description": "Create a release when this PR is merged",
45+
"releaseType": "release",
46+
"color": "#007f70",
47+
"overwrite": true
48+
},
49+
{
50+
"name": "auto:internal",
51+
"changelogTitle": "🏠 Internal",
52+
"description": "Changes only affect the internal API",
53+
"releaseType": "none",
54+
"color": "#696969",
55+
"overwrite": true
56+
},
57+
{
58+
"name": "auto:documentation",
59+
"changelogTitle": "📝 Documentation",
60+
"description": "Changes only affect the documentation",
61+
"releaseType": "none",
62+
"color": "#cfd3d7",
63+
"overwrite": true
64+
},
65+
{
66+
"name": "auto:tests",
67+
"changelogTitle": "🧪 Tests",
68+
"description": "Add or improve existing tests",
69+
"releaseType": "none",
70+
"color": "#ffd3cc",
71+
"overwrite": true
72+
},
73+
{
74+
"name": "auto:dependencies",
75+
"changelogTitle": "🔩 Dependency Updates",
76+
"description": "Update one or more dependencies version",
77+
"releaseType": "none",
78+
"color": "#8732bc",
79+
"overwrite": true
80+
},
81+
{
82+
"name": "auto:ci",
83+
"changelogTitle": "CI/CD Changes",
84+
"description": "Updates to CI/CD workflows and processes",
85+
"releaseType": "none",
86+
"color": "#E5A3DD",
87+
"overwrite": true
88+
}
89+
]
90+
}

.github/workflows/auto-release.yml

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
name: Auto Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
inputs:
8+
first_release:
9+
description: "Bootstrap: publish the version currently in pyproject.toml. Bypasses the auto:release label gate and skips version bumping. Use only for the first release or manual recovery."
10+
type: boolean
11+
default: false
12+
13+
permissions:
14+
contents: write
15+
issues: write
16+
pull-requests: write
17+
18+
concurrency:
19+
group: auto-release-${{ github.ref }}
20+
cancel-in-progress: false
21+
22+
env:
23+
PYTHON_VERSION: "3.11"
24+
UV_VERSION: "0.7.13"
25+
AUTO_VERSION: "11.3.6"
26+
RELEASE_BOT_NAME: "github-actions[bot]"
27+
RELEASE_BOT_EMAIL: "41898282+github-actions[bot]@users.noreply.github.com"
28+
29+
jobs:
30+
gate:
31+
name: Gate on merged PR label
32+
runs-on: ubuntu-latest
33+
# Prevent infinite loops from the bot's "chore(release)" commit.
34+
if: github.actor != 'github-actions[bot]'
35+
outputs:
36+
should_release: ${{ steps.find_pr.outputs.should_release }}
37+
pr_number: ${{ steps.find_pr.outputs.pr_number }}
38+
steps:
39+
- name: Find merged PR and check labels
40+
id: find_pr
41+
uses: actions/github-script@v7
42+
env:
43+
FIRST_RELEASE: ${{ github.event.inputs.first_release }}
44+
with:
45+
script: |
46+
// Manual bootstrap: bypass the PR label check entirely.
47+
if (context.eventName === 'workflow_dispatch' && process.env.FIRST_RELEASE === 'true') {
48+
core.notice('Manual first_release=true; bypassing PR label check.');
49+
core.setOutput('should_release', 'true');
50+
core.setOutput('pr_number', '');
51+
return;
52+
}
53+
const { owner, repo } = context.repo;
54+
const sha = context.sha;
55+
const maxAttempts = 6;
56+
let pulls;
57+
// GitHub can briefly lag in associating a merge commit with its PR, so retry.
58+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
59+
pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({
60+
owner, repo, commit_sha: sha,
61+
});
62+
if (pulls.data.length) break;
63+
if (attempt < maxAttempts) {
64+
await new Promise((r) => setTimeout(r, 10000));
65+
}
66+
}
67+
if (!pulls.data || !pulls.data.length) {
68+
core.notice(`No PR associated with ${sha}. Not releasing.`);
69+
core.setOutput('should_release', 'false');
70+
core.setOutput('pr_number', '');
71+
return;
72+
}
73+
const pr = pulls.data.find((p) => p.merged_at && p.base?.ref === 'main') ?? pulls.data[0];
74+
const labels = (pr.labels || []).map((l) => l.name);
75+
const should = labels.includes('auto:release');
76+
core.setOutput('pr_number', String(pr.number));
77+
core.setOutput('should_release', should ? 'true' : 'false');
78+
core.notice(`PR #${pr.number} labels: ${labels.join(', ')}`);
79+
core.notice(`should_release=${should}`);
80+
81+
release:
82+
name: Tag, release, and publish
83+
runs-on: ubuntu-latest
84+
needs: gate
85+
if: needs.gate.outputs.should_release == 'true'
86+
env:
87+
# 'true' for manual bootstrap runs; 'false' or empty otherwise.
88+
FIRST_RELEASE: ${{ github.event.inputs.first_release || 'false' }}
89+
steps:
90+
- name: Checkout
91+
uses: actions/checkout@v6
92+
with:
93+
ref: main
94+
fetch-depth: 0
95+
fetch-tags: true
96+
# RELEASE_PAT (a PAT with repo + workflow scopes) lets the bot push to
97+
# protected branches and trigger other workflows. Falls back to
98+
# GITHUB_TOKEN for repos without branch protection.
99+
token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
100+
101+
- name: Install Python
102+
uses: actions/setup-python@v6
103+
with:
104+
python-version: ${{ env.PYTHON_VERSION }}
105+
106+
- name: Install uv
107+
uses: astral-sh/setup-uv@v6
108+
with:
109+
version: ${{ env.UV_VERSION }}
110+
enable-cache: true
111+
python-version: ${{ env.PYTHON_VERSION }}
112+
cache-dependency-glob: |
113+
pyproject.toml
114+
uv.lock
115+
116+
- name: Install auto
117+
run: |
118+
set -euo pipefail
119+
curl -L "https://github.com/intuit/auto/releases/download/v${AUTO_VERSION}/auto-linux.gz" -o auto-linux.gz
120+
gunzip auto-linux.gz
121+
chmod +x auto-linux
122+
sudo mv auto-linux /usr/local/bin/auto
123+
auto --version
124+
125+
- name: Sanity build (pre-tag)
126+
run: uv build
127+
128+
- name: Capture previous tag
129+
id: previous_tag
130+
run: |
131+
set -euo pipefail
132+
echo "tag=$(git describe --tags --abbrev=0 2>/dev/null || echo '')" >> "$GITHUB_OUTPUT"
133+
134+
- name: Resolve next version (auto)
135+
id: resolve_version
136+
if: env.FIRST_RELEASE != 'true'
137+
env:
138+
GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
139+
run: |
140+
set -euo pipefail
141+
RAW_VERSION="$(auto shipit --name "${RELEASE_BOT_NAME}" --email "${RELEASE_BOT_EMAIL}" --dry-run --quiet | tail -n1 | tr -d '\r')"
142+
VERSION="${RAW_VERSION#v}"
143+
if [ -z "$VERSION" ]; then
144+
echo "Could not resolve release version from auto."
145+
exit 1
146+
fi
147+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
148+
echo "Resolved version: $VERSION"
149+
150+
- name: Apply version to pyproject.toml
151+
if: env.FIRST_RELEASE != 'true'
152+
run: |
153+
set -euo pipefail
154+
VERSION="${{ steps.resolve_version.outputs.version }}"
155+
sed -i "s/^version = \".*\"$/version = \"${VERSION}\"/" pyproject.toml
156+
grep '^version = ' pyproject.toml
157+
158+
- name: Commit version bump
159+
if: env.FIRST_RELEASE != 'true'
160+
env:
161+
GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
162+
run: |
163+
set -euo pipefail
164+
if git diff --quiet -- pyproject.toml; then
165+
echo "No pyproject version change to commit."
166+
else
167+
git config user.name "${RELEASE_BOT_NAME}"
168+
git config user.email "${RELEASE_BOT_EMAIL}"
169+
git add pyproject.toml
170+
# [skip ci] avoids re-running this workflow on the bot's own commit.
171+
git commit -m "chore(release): v${{ steps.resolve_version.outputs.version }} [skip ci]"
172+
git push origin HEAD:main
173+
fi
174+
175+
- name: Read final version from pyproject.toml
176+
id: final_version
177+
run: |
178+
set -euo pipefail
179+
VERSION="$(grep -E '^version = ' pyproject.toml | head -1 | sed -E 's/version = "([^"]+)".*/\1/')"
180+
if [ -z "$VERSION" ]; then
181+
echo "Could not read version from pyproject.toml"
182+
exit 1
183+
fi
184+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
185+
echo "Final version: $VERSION"
186+
187+
- name: Capture release commit SHA
188+
id: release_commit
189+
run: |
190+
set -euo pipefail
191+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
192+
193+
- name: Create labels (idempotent)
194+
env:
195+
GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
196+
run: auto create-labels
197+
198+
- name: Create and push tag
199+
run: |
200+
set -euo pipefail
201+
TAG="v${{ steps.final_version.outputs.version }}"
202+
TARGET_SHA="${{ steps.release_commit.outputs.sha }}"
203+
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
204+
EXISTING_SHA="$(git rev-list -n1 "$TAG")"
205+
if [ "$EXISTING_SHA" = "$TARGET_SHA" ]; then
206+
echo "Tag $TAG already exists at $TARGET_SHA. Skipping."
207+
exit 0
208+
fi
209+
echo "Tag $TAG exists at $EXISTING_SHA but expected $TARGET_SHA."
210+
exit 1
211+
fi
212+
git tag "$TAG" "$TARGET_SHA"
213+
git push origin "$TAG"
214+
215+
- name: Create GitHub release with auto notes
216+
env:
217+
GH_TOKEN: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }}
218+
run: |
219+
set -euo pipefail
220+
args=(--to "${{ steps.release_commit.outputs.sha }}" --use-version "v${{ steps.final_version.outputs.version }}")
221+
if [ -n "${{ steps.previous_tag.outputs.tag }}" ]; then
222+
args=(--from "${{ steps.previous_tag.outputs.tag }}" "${args[@]}")
223+
fi
224+
auto release "${args[@]}"
225+
226+
- name: Build final artifacts
227+
run: |
228+
set -euo pipefail
229+
rm -rf dist
230+
uv build
231+
232+
- name: Publish to PyPI
233+
env:
234+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI }}
235+
run: uv publish

0 commit comments

Comments
 (0)