Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ updates:
schedule:
interval: "monthly"

# Maintain submodule versions
# NOTE: too noisy, easier to update by hand
#- package-ecosystem: "gitsubmodule"
# directory: "/"
# schedule:
# interval: "monthly"
# Maintain submodule versions so module releases propagate into the meta-repo
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "monthly"

# Maintain dependencies for pip/poetry
# NOTE: too noisy, easier to update by hand
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/build-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ jobs:
echo "RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}" >> $GITHUB_ENV
echo "TAURI_BUILD=true" >> $GITHUB_ENV

- name: Set tag metadata
if: startsWith(github.ref, 'refs/tags/v')
run: |
echo "VERSION_TAG=${GITHUB_REF_NAME}" >> $GITHUB_ENV

- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -160,7 +165,7 @@ jobs:

make dist/notarize
fi
mv dist/ActivityWatch.dmg dist/activitywatch-$(scripts/package/getversion.sh)-macos-x86_64.dmg
mv dist/ActivityWatch.dmg dist/activitywatch-${VERSION_TAG:-$(scripts/package/getversion.sh)}-macos-x86_64.dmg
env:
APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
Expand Down Expand Up @@ -242,4 +247,4 @@ jobs:
draft: true
files: dist/*/activitywatch-*.*
body_path: dist/release_notes_tauri/release_notes.md
prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}
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
7 changes: 6 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ jobs:
run: |
echo "RELEASE=${{ startsWith(github.ref_name, 'v') || github.ref_name == 'master' }}" >> $GITHUB_ENV

- name: Set tag metadata
if: startsWith(github.ref, 'refs/tags/v')
run: |
echo "VERSION_TAG=${GITHUB_REF_NAME}" >> $GITHUB_ENV

- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -173,7 +178,7 @@ jobs:
# Notarize
make dist/notarize
fi
mv dist/ActivityWatch.dmg dist/activitywatch-$(scripts/package/getversion.sh)-macos-x86_64.dmg
mv dist/ActivityWatch.dmg dist/activitywatch-${VERSION_TAG:-$(scripts/package/getversion.sh)}-macos-x86_64.dmg
env:
APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
Expand Down
191 changes: 191 additions & 0 deletions .github/workflows/dev-release.yml
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
Comment on lines +22 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

  1. gh api repos/.../actions/runs/${GITHUB_RUN_ID} — requires actions: read to get check_suite_id.
  2. gh api repos/.../commits/${head_sha}/check-runs — requires checks: read on 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

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

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

Choose a reason for hiding this comment

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

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

ISO 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."
  ...
fi

This 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)
Comment on lines +120 to +126
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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

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


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

Choose a reason for hiding this comment

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

P0 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

{
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"
Loading