-
Notifications
You must be signed in to change notification settings - Fork 9
131 lines (117 loc) · 5.52 KB
/
auto-release.yml
File metadata and controls
131 lines (117 loc) · 5.52 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
name: Auto Release on Merge to Main
on:
push:
branches: [main]
permissions:
contents: write
jobs:
tag-and-release:
# Recursion guards:
# 1. Subject-prefix match: the bot's own bump commits always begin with
# "chore(release): bump version to". Using startsWith here (not
# contains) means prose in PR bodies that mentions the phrase cannot
# trip the guard — only the actual commit subject can.
# 2. Fallback: match by the bot's author email.
if: |
!startsWith(github.event.head_commit.message, 'chore(release): bump version to') &&
github.event.head_commit.author.email != 'github-actions[bot]@users.noreply.github.com'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# PAT (fine-grained w/ Contents: read+write or classic w/ `repo` scope)
# lets the bump commit bypass branch protection AND ensures the pushed
# tag triggers release.yml (GITHUB_TOKEN-pushed tags do not).
token: ${{ secrets.RELEASE_PAT }}
- uses: dtolnay/rust-toolchain@stable
- name: Determine bump type from conventional commits
id: bump
run: |
set -e
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
RANGE="HEAD"
else
RANGE="${LAST_TAG}..HEAD"
fi
# SUBJECTS = one subject line per non-merge commit (first line only).
# BODIES = full bodies, used strictly for the `BREAKING CHANGE:`
# trailer match (must be line-start + colon).
SUBJECTS=$(git log "$RANGE" --format='%s' --no-merges)
BODIES=$(git log "$RANGE" --format='%b' --no-merges)
if [ -z "$SUBJECTS" ]; then
echo "No commits since $LAST_TAG."
echo "type=none" >> "$GITHUB_OUTPUT"
exit 0
fi
# Type detection. `BREAKING CHANGE:` trailer is checked at line
# start with a required colon — avoids matching prose like
# "- BREAKING CHANGE or !: -> major" inside a body.
# Header prefixes are matched against SUBJECTS only, so a body
# containing "feat: xyz" as documentation does not trigger a bump.
if echo "$SUBJECTS" | grep -qE '^[a-zA-Z]+(\([^)]*\))?!:' \
|| echo "$BODIES" | grep -qE '^BREAKING CHANGE:'; then
TYPE=major
elif echo "$SUBJECTS" | grep -qE '^feat(\([^)]*\))?:'; then
TYPE=minor
elif echo "$SUBJECTS" | grep -qE '^(fix|perf)(\([^)]*\))?:'; then
TYPE=patch
else
echo "Only chore/docs/ci/test/refactor commits — no release."
TYPE=none
fi
echo "type=$TYPE" >> "$GITHUB_OUTPUT"
echo "Detected bump: $TYPE (since ${LAST_TAG:-<initial>})"
- name: Compute next version
if: steps.bump.outputs.type != 'none'
id: version
run: |
set -e
CUR=$(grep -E '^version\s*=' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
IFS='.' read -r MA MI PA <<< "$CUR"
case "${{ steps.bump.outputs.type }}" in
major) MA=$((MA+1)); MI=0; PA=0 ;;
minor) MI=$((MI+1)); PA=0 ;;
patch) PA=$((PA+1)) ;;
esac
NEW="${MA}.${MI}.${PA}"
echo "current=$CUR" >> "$GITHUB_OUTPUT"
echo "next=$NEW" >> "$GITHUB_OUTPUT"
echo "Bumping $CUR -> $NEW"
- name: Fail fast if tag already exists
if: steps.bump.outputs.type != 'none'
run: |
# Check both the local and remote tag namespaces. A tag may exist
# locally but not remotely (e.g. a failed push from a prior run),
# so we also consult origin explicitly.
if git rev-parse "v${{ steps.version.outputs.next }}" >/dev/null 2>&1 \
|| git ls-remote --tags origin "v${{ steps.version.outputs.next }}" | grep -q "v${{ steps.version.outputs.next }}"; then
echo "Tag v${{ steps.version.outputs.next }} already exists — manual intervention needed."
exit 1
fi
- name: Bump Cargo.toml
if: steps.bump.outputs.type != 'none'
run: |
sed -i.bak -E "s/^version = \".*\"/version = \"${{ steps.version.outputs.next }}\"/" Cargo.toml
rm Cargo.toml.bak
grep -E '^version\s*=' Cargo.toml | head -1
- name: Refresh Cargo.lock
if: steps.bump.outputs.type != 'none'
run: cargo check --quiet
- name: Commit, tag, push
if: steps.bump.outputs.type != 'none'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock
# Using the namespaced [squeez-release-bot] marker so the recursion
# guard cannot be tripped by prose in user commit messages.
git commit -m "chore(release): bump version to ${{ steps.version.outputs.next }} [squeez-release-bot]"
# Annotated tag so `git push --tags` propagates it (lightweight tags
# are not pushed by `--follow-tags`, which silently broke the prior
# release pipeline when v1.0.0 was cut but never published).
git tag -a "v${{ steps.version.outputs.next }}" -m "Release v${{ steps.version.outputs.next }}"
git push origin main
git push origin "v${{ steps.version.outputs.next }}"
echo "Released v${{ steps.version.outputs.next }} — release.yml will take it from here."