/
/
/
1name: Auto Release
2
3# Automatically creates releases with proper version increments
4# - Nightly: runs at 02:00 UTC daily if there are 2+ commits (format: 1.2.3.dev20251025HH)
5# - Beta: manual trigger (format: 1.2.0b1, 1.2.0b2, etc.)
6# - Stable: manual trigger (format: 1.2.3, 1.2.4, etc.)
7
8on:
9 schedule:
10 # Run at 03:00 UTC every day for nightly releases
11 - cron: "0 3 * * *"
12 workflow_dispatch:
13 inputs:
14 channel:
15 description: "Release channel"
16 required: true
17 type: choice
18 options:
19 - nightly
20 - beta
21 - stable
22 default: nightly
23 important_notes:
24 description: "Important notes (breaking changes, critical info, etc.)"
25 required: false
26
27permissions:
28 actions: write
29 contents: write
30
31jobs:
32 check-and-release:
33 runs-on: ubuntu-latest
34 outputs:
35 version: ${{ steps.next_version.outputs.version }}
36 should_release: ${{ steps.check_commits.outputs.has_commits }}
37 channel: ${{ steps.set_channel.outputs.channel }}
38 steps:
39 - name: Set release channel
40 id: set_channel
41 run: |
42 # Use input channel for manual runs, default to nightly for scheduled runs
43 if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
44 CHANNEL="${{ inputs.channel }}"
45 else
46 CHANNEL="nightly"
47 fi
48 echo "channel=$CHANNEL" >> $GITHUB_OUTPUT
49 echo "Release channel: $CHANNEL"
50
51 - name: Checkout repository
52 uses: actions/checkout@v6
53 with:
54 fetch-depth: 0 # Fetch all history for proper comparison
55
56 - name: Check for new commits
57 id: check_commits
58 run: |
59 CHANNEL="${{ steps.set_channel.outputs.channel }}"
60
61 # Define search patterns for each channel
62 case "$CHANNEL" in
63 nightly)
64 SEARCH_PATTERN="dev"
65 ;;
66 beta)
67 SEARCH_PATTERN="b"
68 ;;
69 stable)
70 # For stable, we want versions that don't contain dev or beta suffixes
71 SEARCH_PATTERN="stable"
72 ;;
73 esac
74
75 # Get the latest release for the channel
76 if [ "$CHANNEL" = "stable" ]; then
77 # For stable, get releases that don't contain dev or beta suffixes
78 LATEST_RELEASE=$(gh release list --exclude-drafts --limit 100 --json createdAt,tagName,isPrerelease --jq '.[] | select(.tagName | contains("dev") | not) | select(.tagName | test("b[0-9]") | not)' 2>/dev/null | jq -s '.[0]' || echo "")
79 else
80 # For nightly and beta, filter by pattern
81 LATEST_RELEASE=$(gh release list --exclude-drafts --limit 100 --json createdAt,tagName,isPrerelease --jq ".[] | select(.tagName | contains(\"$SEARCH_PATTERN\"))" 2>/dev/null | jq -s '.[0]' || echo "")
82 fi
83
84 if [ -z "$LATEST_RELEASE" ] || [ "$LATEST_RELEASE" == "null" ]; then
85 echo "No previous $CHANNEL releases found"
86 echo "has_commits=true" >> $GITHUB_OUTPUT
87 echo "last_tag=" >> $GITHUB_OUTPUT
88 else
89 RELEASE_DATE=$(echo "$LATEST_RELEASE" | jq -r '.createdAt')
90 LAST_TAG=$(echo "$LATEST_RELEASE" | jq -r '.tagName')
91 echo "Latest $CHANNEL release: $LAST_TAG at $RELEASE_DATE"
92 echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT
93
94 # Check if there are commits since the latest release
95 COMMITS_SINCE=$(git log --since="$RELEASE_DATE" --oneline | wc -l)
96 echo "Commits since last $CHANNEL release: $COMMITS_SINCE"
97
98 # Require at least 2 commits for auto-release (nightly only)
99 # For manual beta/stable releases, always proceed
100 if [ "$CHANNEL" = "nightly" ]; then
101 if [ "$COMMITS_SINCE" -ge 2 ]; then
102 echo "has_commits=true" >> $GITHUB_OUTPUT
103 else
104 echo "has_commits=false" >> $GITHUB_OUTPUT
105 echo "Only $COMMITS_SINCE commit(s) found. Need at least 2 commits for auto-release."
106 fi
107 else
108 # Manual releases (beta/stable) always proceed
109 echo "has_commits=true" >> $GITHUB_OUTPUT
110 fi
111 fi
112 env:
113 GH_TOKEN: ${{ github.token }}
114
115 - name: Get last stable release (for beta and nightly versioning)
116 id: last_stable
117 if: ${{ steps.set_channel.outputs.channel == 'beta' || steps.set_channel.outputs.channel == 'nightly' }}
118 run: |
119 # Get the latest stable release (no dev or beta suffixes)
120 LATEST_STABLE=$(gh release list --exclude-drafts --limit 100 --json createdAt,tagName,isPrerelease --jq '.[] | select(.tagName | contains("dev") | not) | select(.tagName | test("b[0-9]") | not)' 2>/dev/null | jq -s '.[0]' || echo "")
121
122 if [ -z "$LATEST_STABLE" ] || [ "$LATEST_STABLE" == "null" ]; then
123 echo "No previous stable releases found"
124 echo "stable_tag=" >> $GITHUB_OUTPUT
125 else
126 STABLE_TAG=$(echo "$LATEST_STABLE" | jq -r '.tagName')
127 echo "Latest stable release: $STABLE_TAG"
128 echo "stable_tag=$STABLE_TAG" >> $GITHUB_OUTPUT
129 fi
130 env:
131 GH_TOKEN: ${{ github.token }}
132
133 - name: Calculate next version
134 id: next_version
135 if: steps.check_commits.outputs.has_commits == 'true'
136 run: |
137 LAST_TAG="${{ steps.check_commits.outputs.last_tag }}"
138 CHANNEL="${{ steps.set_channel.outputs.channel }}"
139
140 case "$CHANNEL" in
141 nightly)
142 # Nightly: format 1.2.3.devYYYYMMDDHH
143 # Always one minor version ahead of the last stable release
144 TODAY=$(date -u +%Y%m%d)
145 HOUR=$(date -u +%H)
146 LAST_STABLE_TAG="${{ steps.last_stable.outputs.stable_tag }}"
147
148 # Determine the base version (should be one minor version ahead of stable)
149 if [ -n "$LAST_STABLE_TAG" ]; then
150 STABLE_VERSION=$(echo "$LAST_STABLE_TAG" | sed 's/^v//')
151
152 if [[ "$STABLE_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
153 MAJOR="${BASH_REMATCH[1]}"
154 MINOR="${BASH_REMATCH[2]}"
155 NEXT_MINOR=$((MINOR + 1))
156 BASE_VERSION="${MAJOR}.${NEXT_MINOR}.0"
157 else
158 BASE_VERSION="0.1.0"
159 fi
160 else
161 # No stable release found, start with default
162 BASE_VERSION="0.1.0"
163 fi
164
165 NEW_VERSION="${BASE_VERSION}.dev${TODAY}${HOUR}"
166 echo "Nightly version based on stable ${LAST_STABLE_TAG}: ${NEW_VERSION}"
167 ;;
168
169 beta)
170 # Beta: format 1.2.0b1, 1.2.0b2, etc.
171 # Always base the version on the last STABLE release, not dev versions
172 LAST_BETA_TAG="${{ steps.check_commits.outputs.last_tag }}"
173 LAST_STABLE_TAG="${{ steps.last_stable.outputs.stable_tag }}"
174
175 # Check if there's an existing beta version
176 if [ -n "$LAST_BETA_TAG" ]; then
177 BETA_VERSION=$(echo "$LAST_BETA_TAG" | sed 's/^v//')
178
179 # Check if it's already a beta version (e.g., 2.7.0b1)
180 if [[ "$BETA_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)b([0-9]+)$ ]]; then
181 BASE_VERSION="${BASH_REMATCH[1]}"
182 BETA_NUM="${BASH_REMATCH[2]}"
183 NEXT_BETA=$((BETA_NUM + 1))
184 NEW_VERSION="${BASE_VERSION}b${NEXT_BETA}"
185 echo "Incrementing existing beta: ${LAST_BETA_TAG} -> ${NEW_VERSION}"
186 else
187 # Should not happen, but fallback
188 NEW_VERSION="0.1.0b1"
189 fi
190 elif [ -n "$LAST_STABLE_TAG" ]; then
191 # No beta exists, increment minor from last stable and start at b1
192 STABLE_VERSION=$(echo "$LAST_STABLE_TAG" | sed 's/^v//')
193
194 if [[ "$STABLE_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
195 MAJOR="${BASH_REMATCH[1]}"
196 MINOR="${BASH_REMATCH[2]}"
197 NEXT_MINOR=$((MINOR + 1))
198 NEW_VERSION="${MAJOR}.${NEXT_MINOR}.0b1"
199 echo "Creating first beta based on stable ${LAST_STABLE_TAG}: ${NEW_VERSION}"
200 else
201 NEW_VERSION="0.1.0b1"
202 fi
203 else
204 # No stable or beta found, start fresh
205 NEW_VERSION="0.1.0b1"
206 fi
207 ;;
208
209 stable)
210 # Stable: format 1.2.3, increment patch version
211 if [ -z "$LAST_TAG" ]; then
212 NEW_VERSION="0.1.0"
213 else
214 VERSION=$(echo "$LAST_TAG" | sed 's/^v//')
215
216 # Extract major.minor.patch and increment patch
217 if [[ "$VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
218 MAJOR="${BASH_REMATCH[1]}"
219 MINOR="${BASH_REMATCH[2]}"
220 PATCH="${BASH_REMATCH[3]}"
221 NEXT_PATCH=$((PATCH + 1))
222 NEW_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}"
223 else
224 NEW_VERSION="0.1.0"
225 fi
226 fi
227 ;;
228 esac
229
230 echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
231 echo "New $CHANNEL version: $NEW_VERSION"
232
233 - name: Log release decision
234 run: |
235 CHANNEL="${{ steps.set_channel.outputs.channel }}"
236 if [ "${{ steps.check_commits.outputs.has_commits }}" == "true" ]; then
237 echo "â
Will create $CHANNEL release ${{ steps.next_version.outputs.version }}"
238 else
239 echo "âï¸ Skipping release - not enough commits"
240 fi
241
242 trigger-release:
243 name: Trigger Release Workflow
244 needs: check-and-release
245 if: needs.check-and-release.outputs.should_release == 'true'
246 permissions:
247 actions: write
248 contents: write
249 pull-requests: read
250 packages: write
251 id-token: write # Required for PyPI publishing
252 uses: ./.github/workflows/release.yml
253 with:
254 version: ${{ needs.check-and-release.outputs.version }}
255 channel: ${{ needs.check-and-release.outputs.channel }}
256 important_notes: ${{ inputs.important_notes }}
257 secrets:
258 PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
259 PRIVILEGED_GITHUB_TOKEN: ${{ secrets.PRIVILEGED_GITHUB_TOKEN }}
260