Skip to content

feat(ci): add automated dev release workflow#1217

Open
TimeToBuildBob wants to merge 1 commit intoActivityWatch:masterfrom
TimeToBuildBob:bob/aw-dev-releases
Open

feat(ci): add automated dev release workflow#1217
TimeToBuildBob wants to merge 1 commit intoActivityWatch:masterfrom
TimeToBuildBob:bob/aw-dev-releases

Conversation

@TimeToBuildBob
Copy link
Contributor

Summary

  • add a scheduled/manual workflow that creates new beta prerelease tags when master has new green commits
  • reuse the existing tag-triggered build workflows instead of introducing a second packaging pipeline
  • ensure tag builds always publish draft prereleases and preserve the exact tag in macOS artifact filenames

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.yml decides if a prerelease should happen and pushes the next vX.Y.ZbN tag
  • existing build.yml / build-tauri.yml continue doing the actual packaging + draft release creation

Notes

  • schedule is every Thursday at 12:00 UTC, but only even ISO weeks are allowed, so the effective cadence is biweekly
  • manual dispatch supports either patch-line or minor-line prereleases
  • prerelease tagging is skipped when there are no commits since the previous stable/prerelease tag, or when CI on master is not green

Closes #1216

@greptile-apps
Copy link

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR introduces an automated dev-release workflow (dev-release.yml) that pushes vX.Y.ZbN-style prerelease tags on a biweekly schedule or on manual dispatch, then relies on the existing build.yml and build-tauri.yml tag-triggered pipelines to build and publish draft releases. The design is clean and well-scoped.

Key issues found:

  • Stable releases will be permanently marked as prereleases — both build.yml and build-tauri.yml changed prerelease from the correct conditional (${{ !(steps.version.outputs.is_stable == 'true') }}) to the hardcoded prerelease: true. Every future stable tag push (e.g., v0.14.0) will now produce a GitHub release incorrectly flagged as a prerelease, preventing it from appearing as the "Latest release" on the repo's Releases page.
  • cancelled CI conclusions are not blocked — the CI gate in preflight checks for failure, action_required, timed_out, null, and pending, but silently passes through cancelled. A run where some jobs were cancelled (e.g., superseded by a later push) would still produce a dev release tag.
  • Self-exclusion filter is fragile — the jq filter that prevents the preflight job from blocking on itself uses hardcoded job-name strings ("Pre-flight checks" and "Create dev release"). If either name: field is ever renamed, every scheduled run will see its own pending check and permanently stop releasing without any visible error.

Confidence Score: 2/5

  • Not safe to merge as-is — hardcoding prerelease: true will silently break the next stable release publication.
  • The prerelease: true regression in both build workflows is a high-impact bug that will affect the very next stable release tag, causing it to show up as a prerelease rather than the latest stable release on GitHub. The cancelled CI gap and fragile self-exclusion filter in the new workflow are secondary concerns but also warrant fixes before this lands on master.
  • build.yml (line 278) and build-tauri.yml (line 250) — the hardcoded prerelease: true must be reverted to the original conditional before merging.

Important Files Changed

Filename Overview
.github/workflows/dev-release.yml New workflow that orchestrates biweekly prerelease tagging. Core logic is sound, but the CI-gate does not handle cancelled check conclusions and the self-exclusion filter is fragile (relies on hardcoded job name strings).
.github/workflows/build.yml Added VERSION_TAG env var for macOS artifact naming (correct). However, prerelease was hardcoded to true, which will incorrectly mark all future stable releases as prereleases on GitHub.
.github/workflows/build-tauri.yml Same changes as build.yml: correct VERSION_TAG fallback for macOS DMG naming, but the same prerelease: true regression affects stable release tagging for the Tauri build path.

Sequence Diagram

sequenceDiagram
    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)
Loading

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
prerelease: true
prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}

Comment on lines +122 to +132
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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:

Suggested change
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

Comment on lines +113 to +118
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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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)

Comment on lines +157 to +162
- uses: actions/checkout@v4
with:
ref: master
fetch-depth: 0
submodules: recursive
token: ${{ secrets.GITHUB_TOKEN }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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 }}

Copy link
Contributor

@BelKed BelKed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Automated nightly/dev releases

2 participants