feat(ci): add automated dev release workflow#1217
feat(ci): add automated dev release workflow#1217TimeToBuildBob wants to merge 1 commit intoActivityWatch:masterfrom
Conversation
Greptile SummaryThis PR introduces an automated dev-release workflow ( Key issues found:
Confidence Score: 2/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Cron as Schedule / workflow_dispatch
participant PF as preflight job
participant GH_API as GitHub API (check-runs)
participant Git as git (master)
participant CT as create-tag job
participant Build as build.yml / build-tauri.yml
Cron->>PF: trigger (every other Thursday or manual)
PF->>PF: check ISO week parity (schedule only)
PF->>Git: git tag --sort=-version:refname
PF->>PF: compute next_base & next_tag (bump_version)
PF->>Git: git rev-list since_ref..HEAD --count
alt no new commits
PF-->>Cron: should_release=false (exit)
end
PF->>GH_API: GET /commits/{head_sha}/check-runs
GH_API-->>PF: check-run conclusions
alt failure / timed_out / pending / unknown / no success
PF-->>Cron: should_release=false (exit)
end
PF-->>CT: should_release=true, next_tag, since_ref, commits_since_ref
CT->>Git: checkout master
CT->>Git: git tag -a next_tag
CT->>Git: git push origin next_tag
Git-->>Build: tag push triggers v* workflows
Build->>Build: build + package artifacts
Build->>Build: softprops/action-gh-release (draft=true, prerelease=true)
Last reviewed commit: "feat(ci): add automa..." |
| files: dist/*/activitywatch-*.* | ||
| body_path: dist/release_notes/release_notes.md | ||
| prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }} # must compare to true, since boolean outputs are actually just strings, and "false" is truthy since it's not empty: https://github.com/actions/runner/issues/1483#issuecomment-994986996 | ||
| prerelease: true |
There was a problem hiding this comment.
Stable releases will be incorrectly flagged as prereleases
Hardcoding prerelease: true means every tag-triggered release — including stable tags like v0.13.0 — will be published on GitHub as a prerelease. The old conditional correctly distinguished between stable and prerelease tags:
prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}The check-version-format-action at the top of the release job already detects this via steps.version.outputs.is_stable, so the information is available. By throwing that away and hardcoding true, any future stable release tag that goes through this pipeline will be incorrectly marked as a prerelease on GitHub — meaning it won't show as the "Latest release" and users won't automatically receive stable update notifications.
The same issue exists in .github/workflows/build-tauri.yml at line 250.
| prerelease: true | |
| prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }} |
| files: dist/*/activitywatch-*.* | ||
| body_path: dist/release_notes_tauri/release_notes.md | ||
| prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }} | ||
| prerelease: true |
There was a problem hiding this comment.
Stable releases will be incorrectly flagged as prereleases
Same issue as in build.yml — hardcoding prerelease: true will cause all stable tags (e.g., v0.13.0) to be published as prereleases on GitHub. The steps.version.outputs.is_stable output is already computed earlier in this job and should be used here.
| prerelease: true | |
| prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }} |
| if echo "$conclusions" | grep -qE 'failure|action_required|timed_out'; 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 |
There was a problem hiding this comment.
cancelled CI conclusion is not guarded against
The CI-gate checks for failure, action_required, timed_out, null, and pending, but it does not handle the cancelled conclusion. If any CI job on HEAD was cancelled (e.g., a previous run was superseded by a new push), cancelled will appear in $conclusions without matching any of the blocking patterns. The workflow will fall through to the should_release=true path and create a prerelease tag even though CI did not actually pass on that commit.
Consider adding cancelled to the failure guard:
| if echo "$conclusions" | grep -qE 'failure|action_required|timed_out'; 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 echo "$conclusions" | grep -qE 'failure|action_required|timed_out|cancelled'; then |
| conclusions=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" \ | ||
| --paginate \ | ||
| --jq '[.check_runs[] | select( | ||
| .app.slug == "github-actions" and | ||
| (.name | test("^Pre-flight checks$|^Create dev release$") | not) | ||
| )] | map(.conclusion) | unique | .[]' 2>/dev/null || echo unknown) |
There was a problem hiding this comment.
Workflow filters itself out by name, but name may change
The jq filter excludes check runs named "Pre-flight checks" or "Create dev release" to avoid the workflow waiting on itself:
(.name | test("^Pre-flight checks$|^Create dev release$") | not)These are hardcoded strings that must exactly match the name: fields at the top of the preflight and the workflow itself (name: Create dev release). If either name is ever changed, the self-exclusion silently breaks and the preflight job would see itself as a pending/in-progress check, causing every scheduled run to emit "CI is still running on HEAD" and never release.
A more robust approach would be to filter by the workflow's own run ID using $GITHUB_RUN_ID, or alternatively to match the app.slug check together with the exact job name from GITHUB_JOB:
conclusions=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" \
--paginate \
--jq "[.check_runs[] | select(
.app.slug == \"github-actions\" and
.external_id != \"${GITHUB_RUN_ID}\"
)] | map(.conclusion) | unique | .[]" 2>/dev/null || echo unknown)| - uses: actions/checkout@v4 | ||
| with: | ||
| ref: master | ||
| fetch-depth: 0 | ||
| submodules: recursive | ||
| token: ${{ secrets.GITHUB_TOKEN }} |
There was a problem hiding this comment.
create-tag job re-checks out the repo unnecessarily with fetch-depth: 0 and submodules: recursive
The create-tag job only needs to create and push an annotated tag on the master HEAD commit. It does not run any build steps, inspect submodule content, or traverse full git history. Fetching fetch-depth: 0 (all history) and initialising all submodules substantially slows down this job for no benefit.
A shallow single-depth checkout is sufficient:
- uses: actions/checkout@v4
with:
ref: master
fetch-depth: 1
token: ${{ secrets.GITHUB_TOKEN }}
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?
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