-
-
Notifications
You must be signed in to change notification settings - Fork 863
feat(ci): add automated dev release workflow #1217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6cff6e3
aea0769
78990e5
ea344ae
ef4fb4e
fba875b
05430bb
fd27894
b109cc5
0f97e21
fd5bc44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,191 @@ | ||||||||||||||||||||||||||||||
| name: Create dev release | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Create prerelease tags on a schedule (every other Thursday) or manually. | ||||||||||||||||||||||||||||||
| # The existing build workflows already know how to package tag builds and create | ||||||||||||||||||||||||||||||
| # draft GitHub prereleases, so this workflow only needs to decide whether a new | ||||||||||||||||||||||||||||||
| # prerelease is warranted and push the next prerelease tag. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||
| schedule: | ||||||||||||||||||||||||||||||
| - cron: '0 12 * * 4' | ||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||
| release_line: | ||||||||||||||||||||||||||||||
| description: 'Release line to prerelease from' | ||||||||||||||||||||||||||||||
| required: true | ||||||||||||||||||||||||||||||
| default: patch | ||||||||||||||||||||||||||||||
| type: choice | ||||||||||||||||||||||||||||||
| options: | ||||||||||||||||||||||||||||||
| - patch | ||||||||||||||||||||||||||||||
| - minor | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||
| contents: write | ||||||||||||||||||||||||||||||
| actions: read # needed for /actions/runs/{run_id} (check_suite_id lookup) | ||||||||||||||||||||||||||||||
| checks: read # needed for /commits/{sha}/check-runs | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||
| group: dev-release | ||||||||||||||||||||||||||||||
| cancel-in-progress: false | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||
| preflight: | ||||||||||||||||||||||||||||||
| name: Pre-flight checks | ||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||
| should_release: ${{ steps.preflight.outputs.should_release }} | ||||||||||||||||||||||||||||||
| next_tag: ${{ steps.preflight.outputs.next_tag }} | ||||||||||||||||||||||||||||||
| since_ref: ${{ steps.preflight.outputs.since_ref }} | ||||||||||||||||||||||||||||||
| commits_since_ref: ${{ steps.preflight.outputs.commits_since_ref }} | ||||||||||||||||||||||||||||||
| head_sha: ${{ steps.preflight.outputs.head_sha }} | ||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||
| - uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||
| ref: master # explicit: prevent workflow_dispatch from a non-master branch tagging the wrong commit | ||||||||||||||||||||||||||||||
| fetch-depth: 0 | ||||||||||||||||||||||||||||||
ErikBjare marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Decide whether to create a dev release | ||||||||||||||||||||||||||||||
| id: preflight | ||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||
| GH_TOKEN: ${{ github.token }} | ||||||||||||||||||||||||||||||
| RELEASE_LINE: ${{ github.event.inputs.release_line || 'patch' }} | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then | ||||||||||||||||||||||||||||||
| # Use a fixed reference Thursday to compute biweekly parity, avoiding | ||||||||||||||||||||||||||||||
| # ISO week resets at year boundaries (which cause a 3-week gap in Dec/Jan). | ||||||||||||||||||||||||||||||
| ref_epoch=$(date -d "2024-01-04" +%s) # a known even-week Thursday | ||||||||||||||||||||||||||||||
| now_epoch=$(date -u +%s) | ||||||||||||||||||||||||||||||
| weeks_since=$(( (now_epoch - ref_epoch) / 604800 )) | ||||||||||||||||||||||||||||||
| if [ $((weeks_since % 2)) -eq 1 ]; then | ||||||||||||||||||||||||||||||
| echo "Skipping this week to keep the cadence biweekly." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The biweekly filter uses ISO week number parity: if [ $((10#$week % 2)) -eq 1 ]; then # skip odd weeksISO week numbers reset to 1 on the first Thursday-containing week of the year. At the year boundary the sequence of Thursday ISO weeks can be: Consider anchoring the biweekly logic to a fixed reference date instead: # Number of weeks since a known Thursday, then check parity
ref_epoch=$(date -d "2024-01-04" +%s) # a known even-week Thursday
now_epoch=$(date -u +%s)
weeks_since=$(( (now_epoch - ref_epoch) / 604800 ))
if [ $((weeks_since % 2)) -eq 1 ]; then
echo "Skipping — odd week in rolling biweekly window."
...
fiThis gives a stable 14-day cadence regardless of year boundaries. |
||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| bump_version() { | ||||||||||||||||||||||||||||||
| local version="$1" release_line="$2" | ||||||||||||||||||||||||||||||
| IFS='.' read -r major minor patch <<< "$version" | ||||||||||||||||||||||||||||||
| if [ "$release_line" = "minor" ]; then | ||||||||||||||||||||||||||||||
| minor=$((minor + 1)) | ||||||||||||||||||||||||||||||
| patch=0 | ||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||
| patch=$((patch + 1)) | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
| echo "${major}.${minor}.${patch}" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| latest_stable=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true) | ||||||||||||||||||||||||||||||
| if [ -z "$latest_stable" ]; then | ||||||||||||||||||||||||||||||
| echo "No stable tag found, refusing to create prerelease tags." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| next_base=$(bump_version "${latest_stable#v}" "$RELEASE_LINE") | ||||||||||||||||||||||||||||||
| prerelease_pattern="^v${next_base//./\\.}b[0-9]+$" | ||||||||||||||||||||||||||||||
| last_prerelease=$(git tag --sort=-version:refname | grep -E "$prerelease_pattern" | head -1 || true) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if [ -n "$last_prerelease" ]; then | ||||||||||||||||||||||||||||||
| since_ref="$last_prerelease" | ||||||||||||||||||||||||||||||
| last_prerelease_num=${last_prerelease##*b} | ||||||||||||||||||||||||||||||
| next_tag="v${next_base}b$((last_prerelease_num + 1))" | ||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||
| since_ref="$latest_stable" | ||||||||||||||||||||||||||||||
| next_tag="v${next_base}b1" | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| commits_since_ref=$(git rev-list "${since_ref}..HEAD" --count) | ||||||||||||||||||||||||||||||
| echo "latest_stable=$latest_stable" | ||||||||||||||||||||||||||||||
| echo "last_prerelease=${last_prerelease:-<none>}" | ||||||||||||||||||||||||||||||
| echo "since_ref=$since_ref" | ||||||||||||||||||||||||||||||
| echo "next_tag=$next_tag" | ||||||||||||||||||||||||||||||
| echo "commits_since_ref=$commits_since_ref" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if [ "$commits_since_ref" -eq 0 ]; then | ||||||||||||||||||||||||||||||
| echo "No new commits since $since_ref, skipping dev release." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| head_sha=$(git rev-parse HEAD) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Get the current workflow run's check suite ID so we can exclude | ||||||||||||||||||||||||||||||
| # our own check runs without relying on fragile job name strings | ||||||||||||||||||||||||||||||
| current_suite_id=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ | ||||||||||||||||||||||||||||||
| --jq '.check_suite_id' 2>/dev/null || echo "0") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| conclusions=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" \ | ||||||||||||||||||||||||||||||
| --paginate \ | ||||||||||||||||||||||||||||||
| --arg suite "$current_suite_id" \ | ||||||||||||||||||||||||||||||
| --jq '[.check_runs[] | select( | ||||||||||||||||||||||||||||||
| .app.slug == "github-actions" and | ||||||||||||||||||||||||||||||
| ((.check_suite.id | tostring) != $suite) | ||||||||||||||||||||||||||||||
| )] | map(.conclusion) | unique | .[]' 2>/dev/null || echo unknown) | ||||||||||||||||||||||||||||||
TimeToBuildBob marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+120
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The flag ordering here is incorrect. As written, the if [ -z "$conclusions" ] || [ "$conclusions" = "unknown" ]; then
echo "CI status unavailable on HEAD, skipping dev release."
...
exit 0
fimeans every scheduled and manual run will skip tag creation — the CI gate effectively never passes. The fix is to move
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| echo "CI conclusions: $conclusions" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if echo "$conclusions" | grep -qE 'failure|action_required|timed_out|cancelled|startup_failure'; then | ||||||||||||||||||||||||||||||
| echo "CI has failures on HEAD, skipping dev release." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if echo "$conclusions" | grep -qE 'null|pending'; then | ||||||||||||||||||||||||||||||
| echo "CI is still running on HEAD, skipping dev release." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if [ -z "$conclusions" ] || [ "$conclusions" = "unknown" ]; then | ||||||||||||||||||||||||||||||
| echo "CI status unavailable on HEAD, skipping dev release." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if ! echo "$conclusions" | grep -q 'success'; then | ||||||||||||||||||||||||||||||
| echo "No successful CI checks found on HEAD, skipping dev release." | ||||||||||||||||||||||||||||||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| exit 0 | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| echo "should_release=true" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| echo "next_tag=$next_tag" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| echo "since_ref=$since_ref" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| echo "commits_since_ref=$commits_since_ref" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
| echo "head_sha=$head_sha" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| create-tag: | ||||||||||||||||||||||||||||||
| name: Create dev release tag | ||||||||||||||||||||||||||||||
| needs: preflight | ||||||||||||||||||||||||||||||
| if: needs.preflight.outputs.should_release == 'true' | ||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||
| - uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||
| ref: ${{ needs.preflight.outputs.head_sha }} | ||||||||||||||||||||||||||||||
| fetch-depth: 1 | ||||||||||||||||||||||||||||||
| token: ${{ secrets.AWBOT_GH_TOKEN }} # PAT required — GITHUB_TOKEN cannot trigger downstream tag-based workflows | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Configure git | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| git config user.name "github-actions[bot]" | ||||||||||||||||||||||||||||||
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| - name: Create and push prerelease tag | ||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||
| tag="${{ needs.preflight.outputs.next_tag }}" | ||||||||||||||||||||||||||||||
| git tag -a "$tag" -m "Development prerelease $tag" | ||||||||||||||||||||||||||||||
| git push origin "$tag" | ||||||||||||||||||||||||||||||
|
Comment on lines
+166
to
+182
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Per GitHub's documentation on automatic token authentication, when you use the To fix this, the checkout token must be a Personal Access Token (PAT) or GitHub App token stored in secrets, not the built-in - uses: actions/checkout@v4
with:
ref: ${{ needs.preflight.outputs.head_sha }}
fetch-depth: 1
token: ${{ secrets.RELEASE_PAT }} # PAT with contents:write — must not be GITHUB_TOKEN |
||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||
| echo "## Dev release created" | ||||||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||||||
| echo "- Tag: \`$tag\`" | ||||||||||||||||||||||||||||||
| echo "- Changes since: \`${{ needs.preflight.outputs.since_ref }}\`" | ||||||||||||||||||||||||||||||
| echo "- Commits: \`${{ needs.preflight.outputs.commits_since_ref }}\`" | ||||||||||||||||||||||||||||||
| echo "" | ||||||||||||||||||||||||||||||
| echo "The existing tag-triggered build workflows will now build artifacts and create/update the draft prerelease." | ||||||||||||||||||||||||||||||
| } >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actions: readandchecks: readpermissionsThe workflow declares only
contents: write. When apermissionsblock is present, GitHub sets all other permission scopes tonone.Two API calls in
preflightneed scopes that are not granted:gh api repos/.../actions/runs/${GITHUB_RUN_ID}— requiresactions: readto getcheck_suite_id.gh api repos/.../commits/${head_sha}/check-runs— requireschecks: readon private forks.If
actions: readis absent,current_suite_idsilently falls back to"0". Because no real check suite has ID"0", the((.check_suite.id | tostring) != $suite)filter becomes a no-op and the current workflow's own running preflight job is included inconclusionswith conclusionnull. The guard:…would then fire on every single run, permanently blocking dev releases.