feat(ci): add automated dev release workflow#1217
feat(ci): add automated dev release workflow#1217ErikBjare merged 11 commits intoActivityWatch:masterfrom
Conversation
Greptile SummaryThis PR introduces an automated biweekly dev-release workflow ( Key changes and observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant S as Scheduler or dispatch
participant P as preflight job
participant G as GitHub API
participant C as create-tag job
participant B as build workflows
S->>P: trigger biweekly cron or manual dispatch
P->>P: biweekly parity check via fixed ref epoch
P->>P: resolve latest stable tag and compute next tag
P->>P: count commits since last ref
P->>G: fetch check_suite_id for current run
P->>G: fetch check-runs for HEAD sha paginated
G-->>P: conclusions list filtered by suite
P->>P: block on failure or cancelled or null and require success
P-->>C: outputs should_release=true and next_tag and head_sha
C->>C: checkout pinned head_sha via PAT
C->>C: create annotated tag locally
C->>G: push tag via PAT to trigger downstream workflows
G-->>B: tag push event triggers build.yml and build-tauri.yml
B->>B: set VERSION_TAG from GITHUB_REF_NAME on tag builds
B->>B: build package and rename dmg with VERSION_TAG
B->>B: create draft release with prerelease flag derived from is_stable
Last reviewed commit: "fix(ci): add missing..." |
BelKed
left a comment
There was a problem hiding this comment.
It might be beneficial to generate a release on each commit to the repository, as this could help improve the feedback loop and make iteration faster :)
If this isn’t included in this PR, I think updating the submodules is still important to ensure the app stays up to date, since we don’t have a monorepo structure and the application is distributed across multiple repositories.
That also raises a question: how should updates be handled in the other dependent repositories, especially since some of them also include additional nested repositories?
|
Fixed the critical regression flagged by Greptile (aea0769): Bug: I had hardcoded Fix: Reverted both lines back to the original conditional
The other two Greptile findings (cancelled CI conclusions, fragile self-exclusion filter) are valid secondary issues I can address separately if Erik wants this in before fixing everything. The Windows CI failure is a pre-existing flaky Poetry dependency issue ( |
Address two Greptile review findings:
1. Add `cancelled` and `startup_failure` to the CI failure pattern.
Previously, a cancelled CI run would not block a dev release.
2. Replace fragile hardcoded job name strings with check_suite.id
filtering. The old filter relied on exact job name matches
("Pre-flight checks", "Create dev release") which would break
if jobs were renamed. Now uses GITHUB_RUN_ID to get the current
run's check suite ID and exclude all check runs from it.
Co-authored-by: Bob <bob@superuserlabs.org>
|
Addressed the two secondary Greptile findings (78990e5):
|
|
@TimeToBuildBob Please address @BelKed's comments in #1217 (review) Updating the submodules could be automated, should probably do that. We used to let dependabot e.g. #926 but they have been disabled since we weren't merging them, could re-enable and let them auto-merge on passing CI. ...or we could make it into a monorepo (the path we went in gptme, which has its gptme-contrib repo counterpart for the non-core/app stuff), might actually make sense to do from a purely pragmatic perspective - but it's a fair bit of work and there is a certain logic in keeping the modules of ActivityWatch as separate repos (which would have to get archived if we monorepoify). Monorepo could also risk making a mess of things in the long run. Not doing right now, just thinking aloud... |
|
Thanks for the feedback, @BelKed! Per-commit releases: Biweekly is a deliberate starting point to keep the signal-to-noise ratio manageable — prerelease tags show up in the releases feed and we didn't want to flood it on every commit. That said, the Submodule updates: Agreed this is a gap — a new activitywatch dev release tag doesn't automatically propagate into the dependent repos that pin submodules. A few options Erik and I discussed:
Option 1 is the path of least resistance. I can add a Dependent repos with nested repos: For the deeper nesting (repos that themselves have submodule deps), it gets recursive. That's a reasonable argument for eventually moving toward a monorepo, but as Erik noted, that's a separate and bigger decision. For now, the biweekly dev tag gives a stable anchor that dependent repos can pin to, rather than chasing individual commits. Let me know if you'd like me to add dependabot submodule tracking to this PR, or if you'd prefer to do that as a follow-up. |
|
Addressed Erik's request about submodule propagation in ea344ae. What changed
That gives us the low-maintenance path Erik suggested: new upstream module releases can flow into the meta-repo without adding more custom dispatch logic to I left the release cadence itself biweekly for now. If we want faster feedback later, we can change that independently of the submodule-update mechanism. |
|
Addressed the remaining review thread in ef4fb4e. What changed
That job only tags I also resolved the remaining Greptile threads that were already fixed in 78990e5 (cancelled CI handling + robust self-exclusion). |
|
Quick status update:
So this should be ready to merge from my side. |
|
@greptileai review |
|
@TimeToBuildBob See remaining Greptile comments. Also, why didn't you do the unified release.yml design in https://github.com/gptme/gptme/blob/master/.github/workflows/release.yml that we ended up landing on during gptme/gptme#1643 ? I think putting it in a single file like release.yml would make sense, but I notice both build + release is in build.yml right now. |
|
Fixed the remaining Greptile finding in fba875b. What changed
That closes the real bug here: we now tag the exact commit whose CI status was checked, instead of re-reading On the So my take is:
If you want, I can do that follow-up refactor next instead of stretching this PR further. |
|
Quick follow-up on Erik's I spun the architectural cleanup out into #1219 so this PR can stay focused on landing the dev prerelease pipeline without turning into a bigger CI refactor. That follow-up tracks unifying the current split setup into a single Current PR status from my side:
So I still think #1217 is ready to merge, with #1219 as the explicit cleanup follow-up. |
|
@greptileai review |
The <<'PY' heredoc terminator was indented inside the YAML run: | block. YAML literal blocks strip leading whitespace but heredoc terminators must appear at column 0 in bash (only <<- strips leading tabs, not spaces). Replace with equivalent pure bash arithmetic — no Python dependency needed for simple semver bumping.
|
Fixed in fd27894. The Replaced with equivalent pure bash — |
|
@greptileai review |
| conclusions=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" \ | ||
| --paginate \ | ||
| --jq --arg suite "$current_suite_id" \ | ||
| '[.check_runs[] | select( | ||
| .app.slug == "github-actions" and | ||
| ((.check_suite.id | tostring) != $suite) | ||
| )] | map(.conclusion) | unique | .[]' 2>/dev/null || echo unknown) |
There was a problem hiding this comment.
--jq flag will consume --arg as its expression value
The flag ordering here is incorrect. gh api's --jq flag takes the immediately-following token as its JQ expression argument. Since --arg is the next token, cobra/pflag will set the jq expression to the literal string --arg, then treat suite, "$current_suite_id", and the real filter '[.check_runs[] | ...]' as trailing positional arguments — which gh api doesn't accept.
As written, the gh api call will fail with a jq parse error on stderr. Because of 2>/dev/null || echo unknown, the failure is silently swallowed and conclusions is always set to "unknown". The subsequent guard:
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 --jq after the --arg clause so the real filter string is its argument:
| conclusions=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" \ | |
| --paginate \ | |
| --jq --arg suite "$current_suite_id" \ | |
| '[.check_runs[] | select( | |
| .app.slug == "github-actions" and | |
| ((.check_suite.id | tostring) != $suite) | |
| )] | map(.conclusion) | unique | .[]' 2>/dev/null || echo unknown) | |
| 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) |
| if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then | ||
| week=$(date -u +%V) | ||
| if [ $((10#$week % 2)) -eq 1 ]; then | ||
| echo "Skipping this week to keep the cadence biweekly." | ||
| echo "should_release=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
ISO week parity can create a 3-week gap at year boundaries
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: ...50 (even→release), 52 (even→release), 1 (odd→skip), 2 (even→release).... The jump from week 52 to week 1 produces a 3-week gap between releases, doubling the expected cadence for one cycle.
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.
.github/workflows/dev-release.yml
Outdated
| 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" No newline at end of file |
There was a problem hiding this comment.
Missing newline at end of file
The file ends without a trailing newline. Most editors and linters expect files to end with a newline (\n). This can also cause issues with tools that concatenate files or process them line-by-line.
| } >> "$GITHUB_STEP_SUMMARY" | |
| } >> "$GITHUB_STEP_SUMMARY" |
(Add a trailing newline after this line.)
- Move --arg before --jq so gh api receives the correct jq expression instead of consuming '--arg' as the expression (causing silent failure and conclusions always being 'unknown') - Replace ISO week parity with reference-date calculation to avoid 3-week gaps at year boundaries (ISO week resets in Dec/Jan) - Add trailing newline at end of file
|
Fixed all three Greptile findings in b109cc5: P1 — P2 — ISO week parity at year boundaries: The P2 — Missing trailing newline: Added. |
|
@greptileai review |
| permissions: | ||
| contents: write |
There was a problem hiding this comment.
Missing
actions: read and checks: read permissions
The workflow declares only contents: write. When a permissions block is present, GitHub sets all other permission scopes to none.
Two API calls in preflight need 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: read is absent, current_suite_id silently 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 in conclusions with conclusion null. The guard:
if echo "$conclusions" | grep -qE 'null|pending'; then
echo "CI is still running on HEAD, skipping dev release."…would then fire on every single run, permanently blocking dev releases.
permissions:
contents: write
actions: read # needed for /actions/runs/{run_id} (check_suite_id lookup)
checks: read # needed for /commits/{sha}/check-runs| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ needs.preflight.outputs.head_sha }} | ||
| fetch-depth: 1 | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - 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" |
There was a problem hiding this comment.
GITHUB_TOKEN push will not trigger downstream build workflows
The create-tag job checks out with token: ${{ secrets.GITHUB_TOKEN }} and then runs git push origin "$tag". GitHub explicitly prevents the GITHUB_TOKEN from triggering new workflow runs — including workflows that listen on on: push: tags: v*.
Per GitHub's documentation on automatic token authentication, when you use the GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run. This means when dev-release.yml pushes the new vX.Y.ZbN tag, build.yml and build-tauri.yml will never be triggered, even though those workflows have on: push: tags: v*. The entire automation chain — "reuse the existing tag-triggered build workflows" — is broken at this fundamental step.
To fix this, the checkout token must be a Personal Access Token (PAT) or GitHub App token stored in secrets, not the built-in GITHUB_TOKEN. A PAT-triggered push counts as a regular user push and correctly fires on: push: tags: handlers in other workflows:
- 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- Add actions: read and checks: read to permissions block so the
preflight CI gate can call /actions/runs/{id} and /commits/{sha}/check-runs
- Switch create-tag checkout from GITHUB_TOKEN to AWBOT_GH_TOKEN so the
pushed dev release tag actually triggers build.yml and build-tauri.yml
(GITHUB_TOKEN-originated pushes are blocked from spawning new workflow runs)
|
Fixed the two remaining Greptile findings in 0f97e21: P1 — Missing P0 — |
|
@greptileai review |
|
Merged! |
Summary
Why
ActivityWatch wants gptme-style automated dev/nightly releases so users can test upcoming changes before a stable release. The repo already had decent tag-based packaging; it was just missing the automation layer that decides when to cut a prerelease tag.
This keeps the design simple:
dev-release.ymldecides if a prerelease should happen and pushes the nextvX.Y.ZbNtagbuild.yml/build-tauri.ymlcontinue doing the actual packaging + draft release creationNotes
masteris not greenCloses #1216