Some checks failed
Research feed publisher tests / test (push) Failing after 3s
Release / Pre-flight checks (push) Failing after 0s
Release / Research Edition — patch and packaging smoke tests (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been skipped
Release / Build Qt artifacts (false, 22, ubuntu-22.04, 3.9, false, false, false) (push) Has been skipped
Release / Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor) (22, 3.9) (push) Has been skipped
Release / Generate release notes (push) Has been skipped
Release / Create dev release tag (push) Has been skipped
CodeQL / Analyze (javascript) (push) Failing after 1s
CodeQL / Analyze (python) (push) Failing after 1s
Release / Build Qt artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, ubuntu-24.04-arm, 3.9, false, false, false) (push) Has been cancelled
Release / Build Tauri artifacts (false, 22, windows-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-15-intel, 3.9, false, false, false) (push) Has been cancelled
Release / Build Qt artifacts (false, 22, macos-latest, 3.9, false, false, false) (push) Has been cancelled
Release / Publish draft release (push) Has been cancelled
1472 lines
60 KiB
YAML
1472 lines
60 KiB
YAML
name: Release
|
|
|
|
# Unified release/build pipeline for ActivityWatch.
|
|
#
|
|
# Triggers:
|
|
# - schedule / workflow_dispatch: decide whether to create a new dev prerelease tag
|
|
# - push / pull_request: run the normal build matrices
|
|
# - tag push: run both build matrices, generate release notes, and publish one draft release
|
|
|
|
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
|
|
edition:
|
|
description: 'Build edition: research enables privacy filter in aw-watcher-window by default'
|
|
required: false
|
|
type: choice
|
|
options:
|
|
- standard
|
|
- research
|
|
default: standard
|
|
push:
|
|
branches: [master]
|
|
tags:
|
|
- v*
|
|
pull_request:
|
|
branches: [master]
|
|
|
|
permissions:
|
|
contents: write
|
|
actions: read
|
|
checks: read
|
|
|
|
jobs:
|
|
preflight:
|
|
name: Pre-flight checks
|
|
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: dev-release
|
|
cancel-in-progress: false
|
|
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@v7
|
|
with:
|
|
ref: master
|
|
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' }}
|
|
EDITION: ${{ github.event.inputs.edition || 'standard' }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
if [ "${GITHUB_EVENT_NAME}" = "schedule" ]; then
|
|
ref_epoch=$(date -d "2024-01-04" +%s)
|
|
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
|
|
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")
|
|
|
|
# If a prerelease for a version ahead of next_base already exists,
|
|
# continue that release line instead of starting a new one from the
|
|
# stable bump. Example: latest_stable=v0.13.2 + patch -> v0.13.3,
|
|
# but if v0.14.0b1 exists, produce v0.14.0b2 instead.
|
|
highest_prerelease=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+b[0-9]+$' | head -1 || true)
|
|
if [ -n "$highest_prerelease" ]; then
|
|
hp_base="${highest_prerelease%%b*}"
|
|
hp_base="${hp_base#v}"
|
|
if [ "$(printf '%s\n%s\n' "$hp_base" "$next_base" | sort -V | tail -1)" = "$hp_base" ] && \
|
|
[ "$hp_base" != "$next_base" ]; then
|
|
echo "Existing prerelease $highest_prerelease is ahead of computed v${next_base}; continuing that release line."
|
|
next_base="$hp_base"
|
|
fi
|
|
fi
|
|
|
|
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
|
|
|
|
if [ "$EDITION" = "research" ]; then
|
|
# A research release is the current standard prerelease lineage
|
|
# with an edition suffix. It must not consume the next standard bN.
|
|
if [ -n "$last_prerelease" ]; then
|
|
next_tag="${last_prerelease}-research"
|
|
else
|
|
next_tag="${next_tag}-research"
|
|
fi
|
|
if git rev-parse -q --verify "refs/tags/${next_tag}" >/dev/null; then
|
|
echo "Research tag $next_tag already exists; skipping duplicate release."
|
|
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
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)
|
|
|
|
current_suite_id=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
|
--jq '.check_suite_id' 2>/dev/null || echo "0")
|
|
|
|
# Auxiliary workflow jobs are not CI signals; exclude them so their
|
|
# skipped or failed conclusions do not block the release.
|
|
conclusions=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${head_sha}/check-runs" \
|
|
--paginate \
|
|
--slurp 2>/dev/null | jq -r --arg suite "$current_suite_id" '
|
|
[.[].check_runs[]?
|
|
| select(
|
|
.app.slug == "github-actions" and
|
|
((.check_suite.id | tostring) != $suite) and
|
|
(.name | test("^(Dependabot|Auto-merge|greeting|Pre-flight checks|Create dev release tag)$") | not)
|
|
)
|
|
| .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
|
|
concurrency:
|
|
group: dev-release
|
|
cancel-in-progress: false
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
ref: ${{ needs.preflight.outputs.head_sha }}
|
|
fetch-depth: 1
|
|
token: ${{ secrets.AWBOT_GH_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 }}"
|
|
msg="Development prerelease $tag"
|
|
if [[ "$tag" == *-research ]]; then
|
|
msg="Development prerelease $tag (research edition)"
|
|
fi
|
|
git tag -a "$tag" -m "$msg"
|
|
git push origin "$tag"
|
|
{
|
|
echo "## Dev release created"
|
|
echo ""
|
|
echo "- Tag: \`$tag\`"
|
|
if [[ "$tag" == *-research ]]; then
|
|
echo "- Edition: research"
|
|
fi
|
|
echo "- Changes since: \`${{ needs.preflight.outputs.since_ref }}\`"
|
|
echo "- Commits: \`${{ needs.preflight.outputs.commits_since_ref }}\`"
|
|
echo ""
|
|
echo "The tag-triggered build jobs in this workflow will now build artifacts and create/update the draft prerelease."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
research-edition-checks:
|
|
name: Research Edition — patch and packaging smoke tests
|
|
# Run on every PR and push so drifted submodule pins fail at review time,
|
|
# not when Erik pushes the annotated release tag. The patcher is stdlib-only
|
|
# so no poetry/build step is needed; only submodules (real source files) and
|
|
# pytest (for the unit-test fixture suite).
|
|
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-22.04
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
submodules: 'recursive'
|
|
fetch-depth: 1
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: '3.9'
|
|
|
|
- name: Install pytest
|
|
run: pip install pytest
|
|
|
|
# Verify every patch target string exists exactly once in the real
|
|
# submodule sources for both build flavours. Fails immediately if a
|
|
# target is missing or ambiguous — the same guard that would abort a tag
|
|
# build, now running at PR time.
|
|
- name: Verify Qt patch targets (--check)
|
|
run: python3 scripts/patch_research_edition_profile.py qt --check
|
|
|
|
- name: Verify Tauri patch targets (--check)
|
|
run: python3 scripts/patch_research_edition_profile.py tauri --check
|
|
|
|
# Fixture-based unit tests: patchers apply correctly on synthetic trees,
|
|
# fail closed on missing/ambiguous tokens, and leave ordinary defaults
|
|
# untouched on the unpatched tree. Also exercise the Windows installer
|
|
# collection step with fixture .exe files named by the real patched .iss
|
|
# sources; this catches naming drift without Inno Setup or signing.
|
|
- name: Run profile patcher and installer collection tests
|
|
run: python3 -m pytest scripts/tests/test_patch_research_edition_profile.py -q
|
|
|
|
- name: Test updater channel isolation and release versions
|
|
run: python3 -m pytest scripts/tests/test_generate_latest_json.py scripts/tests/test_configure_tauri_release.py -q
|
|
|
|
build-qt:
|
|
name: Build Qt artifacts
|
|
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
|
runs-on: ${{ matrix.os }}
|
|
continue-on-error: ${{ matrix.experimental }}
|
|
env:
|
|
AW_EXTRAS: true
|
|
MACOSX_DEPLOYMENT_TARGET: "12.0"
|
|
AW_RESEARCH_EDITION: ${{ matrix.research || (github.event_name == 'workflow_dispatch' && inputs.edition == 'research') || endsWith(github.ref_name, '-research') }}
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# Build Linux Qt/AppImage artifacts on the oldest supported runner to
|
|
# keep the bundled glibc baseline compatible with older distributions.
|
|
os: [ubuntu-22.04, windows-latest, macos-latest, macos-15-intel]
|
|
python_version: [3.9]
|
|
node_version: [22]
|
|
skip_rust: [false]
|
|
skip_webui: [false]
|
|
experimental: [false]
|
|
# Research Edition build leg: apply the research patch set and
|
|
# compile/test/package the *patched* tree on every PR and branch push,
|
|
# so a drifted patch target or a patched-source compile failure dies at
|
|
# PR time instead of at tag time. Linux only: cheapest full build and no
|
|
# signing secrets. On tag pushes and research dispatches `research`
|
|
# resolves to false, so this row merges into the existing ubuntu-22.04 row
|
|
# instead of adding a duplicate leg -- research tags already build the
|
|
# research edition in every row, and standard tags must not upload
|
|
# research artifacts to the standard release.
|
|
research: [false]
|
|
include:
|
|
- os: ubuntu-22.04
|
|
python_version: 3.9
|
|
node_version: 22
|
|
skip_rust: false
|
|
skip_webui: false
|
|
experimental: false
|
|
research: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
submodules: 'recursive'
|
|
fetch-depth: 0
|
|
|
|
- name: Set RELEASE
|
|
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: Determine and output version
|
|
run: |
|
|
VERSION_WITH_V=$(bash scripts/package/getversion.sh)
|
|
# Strip the research tag suffix for filenames: the edition gets its
|
|
# own token (activitywatch[-tauri][-research]-<version>-...), so the
|
|
# version part stays a plain version.
|
|
VERSION_WITH_V="${VERSION_WITH_V%-research}"
|
|
VERSION_NO_V="${VERSION_WITH_V#v}"
|
|
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> "$GITHUB_ENV"
|
|
echo "VERSION_NO_V=${VERSION_NO_V}" >> "$GITHUB_ENV"
|
|
echo "========================================"
|
|
echo "Build Version Information"
|
|
echo "========================================"
|
|
echo "GitHub ref: ${{ github.ref }}"
|
|
echo "GitHub ref_name: ${{ github.ref_name }}"
|
|
echo "Version (with v): ${VERSION_WITH_V}"
|
|
echo "Version (no v): ${VERSION_NO_V}"
|
|
echo "========================================"
|
|
|
|
- name: Verify aw-server-rust submodule version matches release tag
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
run: |
|
|
# Fail fast if the aw-server-rust submodule is pinned to an older
|
|
# release line than the one we're tagging. The Windows 0.14 Tauri
|
|
# bundle shipped with aw-server-rust v0.13.1 (activitywatch#1380);
|
|
# catch the same class of mismatch here before spending ~30 min on a
|
|
# full Tauri build.
|
|
#
|
|
# The bundled binary's version lives in
|
|
# aw-server-rust/aw-server/Cargo.toml (the workspace-level
|
|
# aw-server-rust/Cargo.toml has no [package].version field).
|
|
BUNDLED_VERSION=$(grep -m1 '^version = ' aw-server-rust/aw-server/Cargo.toml | sed -E 's/^version = "(.*)".*/\1/')
|
|
if [ -z "$BUNDLED_VERSION" ]; then
|
|
echo "ERROR: could not read aw-server version from aw-server-rust/aw-server/Cargo.toml" >&2
|
|
exit 1
|
|
fi
|
|
|
|
AW_VERSION="${VERSION_NO_V}" # e.g. "0.14.0b3"
|
|
if [ -z "$AW_VERSION" ]; then
|
|
echo "ERROR: VERSION_NO_V is empty — the 'Determine and output version' step must export it to GITHUB_ENV" >&2
|
|
exit 1
|
|
fi
|
|
AW_MAJOR_MINOR=$(echo "$AW_VERSION" | cut -d'.' -f1-2) # "0.14"
|
|
AWS_MAJOR_MINOR=$(echo "$BUNDLED_VERSION" | cut -d'.' -f1-2) # "0.14"
|
|
|
|
echo "AW release tag: ${AW_VERSION} (major.minor: ${AW_MAJOR_MINOR})"
|
|
echo "Bundled aw-server: ${BUNDLED_VERSION} (major.minor: ${AWS_MAJOR_MINOR})"
|
|
|
|
if [ "$AW_MAJOR_MINOR" != "$AWS_MAJOR_MINOR" ]; then
|
|
echo ""
|
|
echo "ERROR: aw-server-rust major.minor (${AWS_MAJOR_MINOR}) does not match"
|
|
echo " AW release major.minor (${AW_MAJOR_MINOR})."
|
|
echo " The aw-server-rust submodule is stale for this tag."
|
|
echo " Update the submodule (cd aw-server-rust && git pull) and re-tag."
|
|
exit 1
|
|
fi
|
|
echo "OK: aw-server version ${BUNDLED_VERSION} is consistent with AW release ${AW_VERSION}"
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: ${{ matrix.python_version }}
|
|
|
|
- name: Set up Node
|
|
if: ${{ !matrix.skip_webui }}
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: ${{ matrix.node_version }}
|
|
|
|
- name: Set up Rust
|
|
if: ${{ !matrix.skip_rust }}
|
|
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
|
|
id: toolchain
|
|
with:
|
|
toolchain: stable
|
|
|
|
- name: Cache node_modules
|
|
uses: actions/cache@v6
|
|
if: ${{ !matrix.skip_webui }}
|
|
with:
|
|
path: aw-server-rust/aw-webui/node_modules
|
|
key: ${{ matrix.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ matrix.os }}-node_modules-
|
|
|
|
- name: Cache cargo build
|
|
uses: actions/cache@v6
|
|
env:
|
|
cache-name: cargo-build-target
|
|
with:
|
|
path: aw-server-rust/target
|
|
key: ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-
|
|
|
|
- name: Install APT dependencies
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y \
|
|
appstream \
|
|
qt5-qmake \
|
|
qtbase5-dev \
|
|
qtwayland5 \
|
|
libqt5x11extras5 \
|
|
libfontconfig1 \
|
|
libxcb1 \
|
|
libfontconfig1-dev \
|
|
libfreetype6-dev \
|
|
libx11-dev \
|
|
libxcursor-dev \
|
|
libxext-dev \
|
|
libxfixes-dev \
|
|
libxft-dev \
|
|
libxi-dev \
|
|
libxrandr-dev \
|
|
libxrender-dev
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
if [ "$RUNNER_OS" == "Windows" ]; then
|
|
choco install innosetup
|
|
fi
|
|
pip3 install poetry==2.2.1
|
|
|
|
- name: Patch research edition defaults
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: |
|
|
python3 scripts/patch_research_edition_config.py aw-watcher-window/aw_watcher_window/config.py
|
|
# Participants' machines must resume tracking after reboots without
|
|
# setup steps: enable start-at-login on first launch (aw-qt#131).
|
|
python3 scripts/patch_research_edition_awqt.py aw-qt/aw_qt/config.py
|
|
|
|
# Watchers store buckets as `{client}_{hostname}`. That hostname is a
|
|
# participant identifier on named machines, and /api/0/export copies it
|
|
# into the map key, embedded id, and hostname field. Standard builds
|
|
# must not change; this patch is Research-only and fail-closed.
|
|
- name: Patch research edition export hostname sanitizer
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_export.py
|
|
|
|
# The watcher keeps approved app names and replaces browser titles with a
|
|
# study category. This preset maps both representations into the study
|
|
# taxonomy so Top Applications and Top Categories agree.
|
|
- name: Emit research edition category preset for the web UI
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
shell: bash
|
|
run: |
|
|
preset="$(python3 scripts/emit_research_category_preset.py)"
|
|
echo "AW_PRESET_CATEGORY_SETS=${preset}" >> "$GITHUB_ENV"
|
|
|
|
- name: Build
|
|
run: |
|
|
python3 -m venv venv
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
poetry install
|
|
make build SKIP_WEBUI=${{ matrix.skip_webui }} SKIP_SERVER_RUST=${{ matrix.skip_rust }}
|
|
pip freeze
|
|
|
|
- name: Run tests
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
make test SKIP_SERVER_RUST=${{ matrix.skip_rust }}
|
|
|
|
- name: Run integration tests
|
|
if: runner.os != 'Windows'
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
make test-integration
|
|
|
|
# The patcher's exact-match table is release-critical. Run it on every
|
|
# PR/release job (not only research tags) so a drifted submodule pin
|
|
# fails before packaging. Must stay on the unpatched tree: --check
|
|
# and the fail-closed tests assert the ordinary defaults.
|
|
- name: Test research edition profile patcher
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
python3 -m pytest scripts/tests/test_patch_research_edition_profile.py -q
|
|
|
|
# Research Edition profile identity is baked AFTER the module test
|
|
# suites: they assert the ordinary defaults (profile=default, port 5600)
|
|
# and would fail on the patched tree. Python modules are editable
|
|
# installs, so PyInstaller collects the patched source at package time;
|
|
# the Rust binaries are rebuilt below. Fail-closed: a stale submodule pin
|
|
# aborts the build rather than shipping a bundle that runs the default
|
|
# profile next to a participant's standard install.
|
|
- name: Patch research edition profile identity
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_profile.py qt
|
|
|
|
- name: Rebuild aw-server-rust with research defaults
|
|
if: env.AW_RESEARCH_EDITION == 'true' && matrix.skip_rust != true
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
make --directory=aw-server-rust aw-server SKIP_WEBUI=true
|
|
|
|
- name: Package
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
poetry install
|
|
make package SKIP_SERVER_RUST=${{ matrix.skip_rust }}
|
|
|
|
- name: Package dmg
|
|
if: runner.os == 'macOS'
|
|
run: |
|
|
SIGN_MACOS=false
|
|
if [[ "${GITHUB_REF:-}" == refs/tags/v* || "$AW_RESEARCH_EDITION" == "true" ]]; then
|
|
SIGN_MACOS=true
|
|
fi
|
|
|
|
if [ "$SIGN_MACOS" = true ]; then
|
|
for var in APPLE_EMAIL APPLE_PASSWORD APPLE_TEAMID CERTIFICATE_MACOS_P12_BASE64 CERTIFICATE_MACOS_P12_PASSWORD; do
|
|
if [ -z "${!var:-}" ]; then
|
|
echo "ERROR: $var is required to sign macOS release artifacts" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
./scripts/ci/import-macos-p12.sh
|
|
APPLE_PERSONALID=$(security find-identity -v -p codesigning "${KEY_CHAIN:-build.keychain-db}" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)
|
|
if [ -z "$APPLE_PERSONALID" ]; then
|
|
echo "ERROR: no codesigning identity found after importing the macOS certificate" >&2
|
|
exit 1
|
|
fi
|
|
export APPLE_PERSONALID
|
|
fi
|
|
|
|
BUNDLE="ActivityWatch"
|
|
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then
|
|
BUNDLE="ActivityWatch-Research"
|
|
fi
|
|
|
|
source venv/bin/activate
|
|
make "dist/${BUNDLE}.dmg" APP_BUNDLE="${BUNDLE}"
|
|
|
|
if [ "$SIGN_MACOS" = true ]; then
|
|
codesign --force --verbose --timestamp -s "${APPLE_PERSONALID}" "dist/${BUNDLE}.dmg"
|
|
|
|
brew install akeru-inc/tap/xcnotary
|
|
xcnotary precheck "dist/${BUNDLE}.app"
|
|
xcnotary precheck "dist/${BUNDLE}.dmg"
|
|
|
|
make dist/notarize
|
|
fi
|
|
EDITION=""
|
|
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then EDITION="-research"; fi
|
|
mv "dist/${BUNDLE}.dmg" dist/activitywatch${EDITION}-${VERSION_WITH_V}-macos-$(uname -m).dmg
|
|
env:
|
|
APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_PERSONALID: ""
|
|
APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}
|
|
CERTIFICATE_MACOS_P12_BASE64: ${{ secrets.CERTIFICATE_MACOS_P12_BASE64 }}
|
|
CERTIFICATE_MACOS_P12_PASSWORD: ${{ secrets.CERTIFICATE_MACOS_P12_PASSWORD }}
|
|
|
|
- name: Package AppImage
|
|
if: startsWith(runner.os, 'linux')
|
|
run: ./scripts/package/package-appimage.sh
|
|
|
|
- name: Package deb
|
|
if: startsWith(runner.os, 'linux')
|
|
run: ./scripts/package/package-deb.sh
|
|
|
|
- name: Upload packages
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: builds-${{ matrix.os }}-py${{ matrix.python_version }}${{ matrix.research && '-research' || '' }}
|
|
path: dist/activitywatch-*.*
|
|
|
|
build-qt-manylinux-2-28:
|
|
name: Build Qt artifacts (manylinux_2_28 — glibc 2.28 ABI floor)
|
|
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
|
runs-on: ubuntu-22.04
|
|
container:
|
|
image: quay.io/pypa/manylinux_2_28_x86_64
|
|
env:
|
|
AW_EXTRAS: true
|
|
AW_RESEARCH_EDITION: ${{ (github.event_name == 'workflow_dispatch' && inputs.edition == 'research') || endsWith(github.ref_name, '-research') }}
|
|
# AppImage tools (linuxdeploy/appimagetool) are themselves AppImages;
|
|
# FUSE is unavailable inside Docker containers, so use extract-and-run.
|
|
APPIMAGE_EXTRACT_AND_RUN: 1
|
|
# aw-sync enables openssl/vendored on Linux and tries to build OpenSSL
|
|
# from source; use the container's system openssl-devel instead.
|
|
OPENSSL_NO_VENDOR: 1
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
python_version: [3.9]
|
|
node_version: [22]
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
submodules: 'recursive'
|
|
fetch-depth: 0
|
|
|
|
# Configure git safe.directory immediately after checkout so that every
|
|
# subsequent step — including the version probe below — can read the full
|
|
# history. Deferring this step past the first `git describe` call causes
|
|
# the version probe to fall back to v0.0.0.dev-unknown.
|
|
- name: Configure git safe directory
|
|
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
|
|
- name: Set RELEASE
|
|
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: Determine and output version
|
|
run: |
|
|
VERSION_WITH_V=$(bash scripts/package/getversion.sh)
|
|
# Strip the research tag suffix for filenames: the edition gets its
|
|
# own token (activitywatch[-research]-<version>-...), so the version
|
|
# part stays a plain version string.
|
|
VERSION_WITH_V="${VERSION_WITH_V%-research}"
|
|
VERSION_NO_V="${VERSION_WITH_V#v}"
|
|
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> "$GITHUB_ENV"
|
|
echo "VERSION_NO_V=${VERSION_NO_V}" >> "$GITHUB_ENV"
|
|
echo "========================================"
|
|
echo "Build Version Information"
|
|
echo "========================================"
|
|
echo "GitHub ref: ${{ github.ref }}"
|
|
echo "GitHub ref_name: ${{ github.ref_name }}"
|
|
echo "Version (with v): ${VERSION_WITH_V}"
|
|
echo "Version (no v): ${VERSION_NO_V}"
|
|
echo "========================================"
|
|
|
|
- name: Verify aw-server-rust submodule version matches release tag
|
|
if: startsWith(github.ref, 'refs/tags/v')
|
|
run: |
|
|
# Fail fast if the aw-server-rust submodule is pinned to an older
|
|
# release line than the one we're tagging. Same check as in
|
|
# build-qt and build-qt-tauri — prevents shipping a manylinux
|
|
# bundle with a mismatched aw-server-rust binary.
|
|
#
|
|
# The bundled binary's version lives in
|
|
# aw-server-rust/aw-server/Cargo.toml (the workspace-level
|
|
# aw-server-rust/Cargo.toml has no [package].version field).
|
|
BUNDLED_VERSION=$(grep -m1 '^version = ' aw-server-rust/aw-server/Cargo.toml | sed -E 's/^version = "(.*)".*/\1/')
|
|
if [ -z "$BUNDLED_VERSION" ]; then
|
|
echo "ERROR: could not read aw-server version from aw-server-rust/aw-server/Cargo.toml" >&2
|
|
exit 1
|
|
fi
|
|
|
|
AW_VERSION="${VERSION_NO_V}" # e.g. "0.14.0b3"
|
|
if [ -z "$AW_VERSION" ]; then
|
|
echo "ERROR: VERSION_NO_V is empty — the 'Determine and output version' step must export it to GITHUB_ENV" >&2
|
|
exit 1
|
|
fi
|
|
AW_MAJOR_MINOR=$(echo "$AW_VERSION" | cut -d'.' -f1-2) # "0.14"
|
|
AWS_MAJOR_MINOR=$(echo "$BUNDLED_VERSION" | cut -d'.' -f1-2) # "0.14"
|
|
|
|
echo "AW release tag: ${AW_VERSION} (major.minor: ${AW_MAJOR_MINOR})"
|
|
echo "Bundled aw-server: ${BUNDLED_VERSION} (major.minor: ${AWS_MAJOR_MINOR})"
|
|
|
|
if [ "$AW_MAJOR_MINOR" != "$AWS_MAJOR_MINOR" ]; then
|
|
echo ""
|
|
echo "ERROR: aw-server-rust major.minor (${AWS_MAJOR_MINOR}) does not match"
|
|
echo " AW release major.minor (${AW_MAJOR_MINOR})."
|
|
echo " The aw-server-rust submodule is stale for this tag."
|
|
echo " Update the submodule (cd aw-server-rust && git pull) and re-tag."
|
|
exit 1
|
|
fi
|
|
echo "OK: aw-server version ${BUNDLED_VERSION} is consistent with AW release ${AW_VERSION}"
|
|
|
|
- name: Set up Node
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: ${{ matrix.node_version }}
|
|
|
|
- name: Set up Rust
|
|
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
|
|
id: toolchain
|
|
with:
|
|
toolchain: stable
|
|
|
|
- name: Cache node_modules
|
|
uses: actions/cache@v6
|
|
with:
|
|
path: aw-server-rust/aw-webui/node_modules
|
|
key: manylinux_2_28-node_modules-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
manylinux_2_28-node_modules-
|
|
|
|
- name: Cache cargo build
|
|
uses: actions/cache@v6
|
|
env:
|
|
cache-name: cargo-build-target
|
|
with:
|
|
path: aw-server-rust/target
|
|
key: manylinux_2_28-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
manylinux_2_28-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-
|
|
|
|
- name: Install Linux dependencies
|
|
run: |
|
|
dnf install -y epel-release
|
|
dnf install -y \
|
|
qt5-qtbase-devel \
|
|
qt5-qtx11extras \
|
|
qt5-qtwayland \
|
|
fontconfig-devel \
|
|
freetype-devel \
|
|
libX11-devel \
|
|
libXcursor-devel \
|
|
libXext-devel \
|
|
libXfixes-devel \
|
|
libXft-devel \
|
|
libXi-devel \
|
|
libXrandr-devel \
|
|
libXrender-devel \
|
|
libxcb-devel \
|
|
openssl-devel \
|
|
kernel-headers \
|
|
gcc \
|
|
gcc-c++ \
|
|
python39-devel \
|
|
python39-libs \
|
|
squashfs-tools \
|
|
zip \
|
|
wget
|
|
|
|
- name: Set up Python PATH
|
|
run: |
|
|
# Prepend the manylinux CPython 3.9 so `python3` resolves to the
|
|
# container's build Python, which ships headers and a shared
|
|
# libpython needed by PyInstaller.
|
|
echo "/opt/python/cp39-cp39/bin" >> "$GITHUB_PATH"
|
|
echo "LD_LIBRARY_PATH=/usr/lib64:${LD_LIBRARY_PATH:-}" >> "$GITHUB_ENV"
|
|
|
|
- name: Install Python tooling
|
|
run: python3 -m pip install poetry==2.2.1
|
|
|
|
- name: Patch research edition defaults
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: |
|
|
python3 scripts/patch_research_edition_config.py aw-watcher-window/aw_watcher_window/config.py
|
|
# Participants' machines must resume tracking after reboots without
|
|
# setup steps: enable start-at-login on first launch (aw-qt#131).
|
|
python3 scripts/patch_research_edition_awqt.py aw-qt/aw_qt/config.py
|
|
|
|
- name: Patch research edition export hostname sanitizer
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_export.py
|
|
|
|
- name: Emit research edition category preset for the web UI
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: |
|
|
preset="$(python3 scripts/emit_research_category_preset.py)"
|
|
echo "AW_PRESET_CATEGORY_SETS=${preset}" >> "$GITHUB_ENV"
|
|
|
|
- name: Build
|
|
run: |
|
|
python3 -m venv venv
|
|
source venv/bin/activate
|
|
poetry install
|
|
make build
|
|
pip freeze
|
|
|
|
- name: Run tests
|
|
run: |
|
|
source venv/bin/activate
|
|
make test
|
|
|
|
- name: Run integration tests
|
|
run: |
|
|
source venv/bin/activate
|
|
make test-integration
|
|
|
|
# The patcher's exact-match table is release-critical. Run it on every
|
|
# PR/release job (not only research tags) so a drifted submodule pin
|
|
# fails before packaging. Must stay on the unpatched tree: --check
|
|
# and the fail-closed tests assert the ordinary defaults.
|
|
- name: Test research edition profile patcher
|
|
run: |
|
|
source venv/bin/activate
|
|
python3 -m pytest scripts/tests/test_patch_research_edition_profile.py -q
|
|
|
|
# Research Edition profile identity is baked AFTER the module test
|
|
# suites: they assert the ordinary defaults (profile=default, port 5600)
|
|
# and would fail on the patched tree. Python modules are editable
|
|
# installs, so PyInstaller collects the patched source at package time;
|
|
# the Rust binaries are rebuilt below. Fail-closed: a stale submodule pin
|
|
# aborts the build rather than shipping a bundle that runs the default
|
|
# profile next to a participant's standard install.
|
|
- name: Patch research edition profile identity
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_profile.py qt
|
|
|
|
- name: Rebuild aw-server-rust with research defaults
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: |
|
|
source venv/bin/activate
|
|
make --directory=aw-server-rust aw-server SKIP_WEBUI=true
|
|
|
|
- name: Package
|
|
run: |
|
|
source venv/bin/activate
|
|
poetry install
|
|
make package
|
|
|
|
- name: Package AppImage
|
|
run: ./scripts/package/package-appimage.sh
|
|
|
|
- name: Rename artifacts with manylinux_2_28 suffix
|
|
run: |
|
|
EDITION=""
|
|
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then EDITION="-research"; fi
|
|
# ZIP produced by `make package`
|
|
ZIP_SRC="dist/activitywatch${EDITION}-${VERSION_WITH_V}-linux-x86_64.zip"
|
|
ZIP_DST="dist/activitywatch${EDITION}-${VERSION_WITH_V}-linux-x86_64-manylinux_2_28.zip"
|
|
[ -f "$ZIP_SRC" ] && mv -v "$ZIP_SRC" "$ZIP_DST"
|
|
# AppImage produced by package-appimage.sh (intentionally unversioned)
|
|
AI_SRC="dist/activitywatch${EDITION}-linux-x86_64.AppImage"
|
|
AI_DST="dist/activitywatch${EDITION}-linux-x86_64-manylinux_2_28.AppImage"
|
|
[ -f "$AI_SRC" ] && mv -v "$AI_SRC" "$AI_DST"
|
|
|
|
- name: ABI gate — verify glibc 2.28 floor
|
|
run: |
|
|
bash scripts/package/abi-gate.sh \
|
|
dist/activitywatch*-manylinux_2_28.zip \
|
|
dist/activitywatch*-manylinux_2_28.AppImage
|
|
|
|
- name: Upload packages
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: builds-manylinux_2_28-qt-py${{ matrix.python_version }}
|
|
path: dist/activitywatch-*.*
|
|
|
|
build-tauri:
|
|
name: Build Tauri artifacts
|
|
if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch'
|
|
runs-on: ${{ matrix.os }}
|
|
continue-on-error: ${{ matrix.experimental }}
|
|
env:
|
|
AW_EXTRAS: true
|
|
TAURI_BUILD: true
|
|
MACOSX_DEPLOYMENT_TARGET: "12.0"
|
|
AW_RESEARCH_EDITION: ${{ matrix.research || (github.event_name == 'workflow_dispatch' && inputs.edition == 'research') || endsWith(github.ref_name, '-research') }}
|
|
# All subprojects share one virtualenv. Poetry's parallel installer can
|
|
# race while replacing the same dependency from different lock files.
|
|
POETRY_INSTALLER_PARALLEL: "false"
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
strategy:
|
|
fail-fast: false
|
|
max-parallel: 5
|
|
matrix:
|
|
os:
|
|
[
|
|
ubuntu-24.04,
|
|
ubuntu-24.04-arm,
|
|
windows-latest,
|
|
macos-latest,
|
|
macos-15-intel,
|
|
]
|
|
python_version: [3.9]
|
|
node_version: [22]
|
|
skip_rust: [false]
|
|
skip_webui: [false]
|
|
experimental: [false]
|
|
# Research Edition build leg: apply the research patch set and
|
|
# compile/test/package the *patched* tree on every PR and branch push,
|
|
# so a drifted patch target or a patched-source compile failure dies at
|
|
# PR time instead of at tag time. Linux only: cheapest full build and no
|
|
# signing secrets. On tag pushes and research dispatches `research`
|
|
# resolves to false, so this row merges into the existing ubuntu-24.04 row
|
|
# instead of adding a duplicate leg -- research tags already build the
|
|
# research edition in every row, and standard tags must not upload
|
|
# research artifacts to the standard release.
|
|
research: [false]
|
|
include:
|
|
- os: ubuntu-24.04
|
|
python_version: 3.9
|
|
node_version: 22
|
|
skip_rust: false
|
|
skip_webui: false
|
|
experimental: false
|
|
research: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
submodules: "recursive"
|
|
fetch-depth: 0
|
|
|
|
- name: Set environment variables
|
|
run: |
|
|
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: Determine and output version
|
|
run: |
|
|
VERSION_WITH_V=$(bash scripts/package/getversion.sh)
|
|
# Strip the research tag suffix for filenames: the edition gets its
|
|
# own token (activitywatch[-tauri][-research]-<version>-...), so the
|
|
# version part stays a plain version.
|
|
VERSION_WITH_V="${VERSION_WITH_V%-research}"
|
|
VERSION_NO_V="${VERSION_WITH_V#v}"
|
|
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> "$GITHUB_ENV"
|
|
echo "VERSION_NO_V=${VERSION_NO_V}" >> "$GITHUB_ENV"
|
|
echo "========================================"
|
|
echo "Build Version Information (Tauri)"
|
|
echo "========================================"
|
|
echo "GitHub ref: ${{ github.ref }}"
|
|
echo "GitHub ref_name: ${{ github.ref_name }}"
|
|
echo "Version (with v): ${VERSION_WITH_V}"
|
|
echo "Version (no v): ${VERSION_NO_V}"
|
|
echo "========================================"
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: ${{ matrix.python_version }}
|
|
|
|
- name: Verify Tauri and Qt embed the same aw-server-rust revision
|
|
# Tauri embeds the Git revision in its Cargo.lock, independently of
|
|
# the top-level submodule used by Qt. Check the actual build inputs;
|
|
# checking only the submodule allowed v0.14.0b4 Tauri to ship 0.13.1.
|
|
run: python3 scripts/check_tauri_server.py "$VERSION_NO_V"
|
|
|
|
- name: Set up Node
|
|
if: ${{ !matrix.skip_webui }}
|
|
uses: actions/setup-node@v7
|
|
with:
|
|
node-version: ${{ matrix.node_version }}
|
|
|
|
- name: Set up Rust
|
|
if: ${{ !matrix.skip_rust }}
|
|
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master
|
|
id: toolchain
|
|
with:
|
|
toolchain: stable
|
|
|
|
- name: Cache node_modules
|
|
uses: actions/cache@v6
|
|
if: ${{ !matrix.skip_webui }}
|
|
with:
|
|
path: |
|
|
aw-server-rust/aw-webui/node_modules
|
|
aw-tauri/node_modules
|
|
key: ${{ matrix.os }}-node_modules-${{ hashFiles('**/package-lock.json') }}
|
|
restore-keys: |
|
|
${{ matrix.os }}-node_modules-
|
|
|
|
- name: Cache cargo build
|
|
uses: actions/cache@v6
|
|
env:
|
|
cache-name: cargo-build-target
|
|
with:
|
|
path: |
|
|
aw-server-rust/target
|
|
aw-tauri/src-tauri/target
|
|
key: ${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}
|
|
restore-keys: |
|
|
${{ matrix.os }}-${{ env.cache-name }}-${{ steps.toolchain.outputs.rustc_hash }}-
|
|
|
|
- name: Install APT dependencies
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y \
|
|
libgtk-3-dev \
|
|
libayatana-appindicator3-dev \
|
|
librsvg2-dev \
|
|
libsoup-3.0-dev \
|
|
xdg-utils
|
|
# Pin WebKit to avoid blank-webview regression on newer ubuntu-24.04 packages.
|
|
# See: ActivityWatch/aw-tauri#99
|
|
sudo apt-get install -y \
|
|
libwebkit2gtk-4.1-0=2.44.0-2 \
|
|
libwebkit2gtk-4.1-dev=2.44.0-2 \
|
|
libjavascriptcoregtk-4.1-0=2.44.0-2 \
|
|
libjavascriptcoregtk-4.1-dev=2.44.0-2 \
|
|
gir1.2-javascriptcoregtk-4.1=2.44.0-2 \
|
|
gir1.2-webkit2-4.1=2.44.0-2
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
if [ "$RUNNER_OS" == "Windows" ]; then
|
|
choco install innosetup
|
|
fi
|
|
pip3 install poetry==2.2.1
|
|
|
|
- name: Patch research edition defaults
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_config.py aw-watcher-window/aw_watcher_window/config.py
|
|
|
|
# Watchers store buckets as `{client}_{hostname}`. That hostname is a
|
|
# participant identifier on named machines, and /api/0/export copies it
|
|
# into the map key, embedded id, and hostname field. Standard builds
|
|
# must not change; this patch is Research-only and fail-closed.
|
|
- name: Patch research edition export hostname sanitizer
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_export.py
|
|
|
|
# The watcher keeps approved app names and replaces browser titles with a
|
|
# study category. This preset maps both representations into the study
|
|
# taxonomy so Top Applications and Top Categories agree.
|
|
- name: Emit research edition category preset for the web UI
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
shell: bash
|
|
run: |
|
|
preset="$(python3 scripts/emit_research_category_preset.py)"
|
|
echo "AW_PRESET_CATEGORY_SETS=${preset}" >> "$GITHUB_ENV"
|
|
|
|
# aw-tauri/Makefile enables bundle.createUpdaterArtifacts through a CLI
|
|
# --config override when TAURI_SIGNING_PRIVATE_KEY is set. The JSON file
|
|
# alone therefore does not describe whether signed bundles are produced.
|
|
- name: Configure Tauri release version and update channel
|
|
run: |
|
|
args=()
|
|
if [ "$AW_RESEARCH_EDITION" = true ]; then
|
|
args+=(--research)
|
|
if [[ "$GITHUB_REF" == refs/tags/v* || "$GITHUB_EVENT_NAME" == workflow_dispatch ]]; then
|
|
args+=(--require-signing-key)
|
|
fi
|
|
fi
|
|
python3 scripts/package/configure_tauri_release.py \
|
|
--config aw-tauri/src-tauri/tauri.conf.json \
|
|
--version "$VERSION_NO_V" "${args[@]}"
|
|
env:
|
|
TAURI_UPDATER_PUBLIC_KEY_RESEARCH: ${{ vars.TAURI_UPDATER_PUBLIC_KEY_RESEARCH }}
|
|
# Select the secret NAME, never `research_secret || standard_secret`:
|
|
# an absent research secret must not fall back to standard signing.
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets[env.AW_RESEARCH_EDITION == 'true' && 'TAURI_SIGNING_PRIVATE_KEY_RESEARCH' || 'TAURI_SIGNING_PRIVATE_KEY'] }}
|
|
|
|
- name: Build
|
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
|
|
with:
|
|
timeout_minutes: 60
|
|
max_attempts: 3
|
|
shell: bash
|
|
command: |
|
|
set -e
|
|
python3 -m venv venv
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
poetry install
|
|
make build SKIP_WEBUI=${{ matrix.skip_webui }} SKIP_SERVER_RUST=${{ matrix.skip_rust }}
|
|
pip freeze
|
|
env:
|
|
# aw-tauri/Makefile enables createUpdaterArtifacts when this key is set.
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets[env.AW_RESEARCH_EDITION == 'true' && 'TAURI_SIGNING_PRIVATE_KEY_RESEARCH' || 'TAURI_SIGNING_PRIVATE_KEY'] }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets[env.AW_RESEARCH_EDITION == 'true' && 'TAURI_SIGNING_PRIVATE_KEY_PASSWORD_RESEARCH' || 'TAURI_SIGNING_PRIVATE_KEY_PASSWORD'] }}
|
|
|
|
- name: Run tests
|
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
|
|
with:
|
|
timeout_minutes: 60
|
|
max_attempts: 3
|
|
shell: bash
|
|
command: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
make test SKIP_SERVER_RUST=${{ matrix.skip_rust }}
|
|
|
|
# The patcher's exact-match table is release-critical. Run it on every
|
|
# PR/release job (not only research tags) so a drifted submodule pin
|
|
# fails before packaging. Must stay on the unpatched tree: --check
|
|
# and the fail-closed tests assert the ordinary defaults.
|
|
- name: Test research edition profile patcher
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
python3 -m pytest scripts/tests/test_patch_research_edition_profile.py -q
|
|
|
|
# Research Edition profile identity is baked AFTER the module test
|
|
# suites: they assert the ordinary defaults (profile=default, port 5600)
|
|
# and would fail on the patched tree. Python modules are editable
|
|
# installs, so PyInstaller collects the patched source at package time;
|
|
# the Rust binaries are rebuilt below. Fail-closed: a stale submodule pin
|
|
# aborts the build rather than shipping a bundle that runs the default
|
|
# profile next to a participant's standard install.
|
|
- name: Patch research edition profile identity
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
run: python3 scripts/patch_research_edition_profile.py tauri
|
|
|
|
- name: Rebuild aw-tauri with research defaults
|
|
if: env.AW_RESEARCH_EDITION == 'true'
|
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
|
|
with:
|
|
timeout_minutes: 60
|
|
max_attempts: 3
|
|
shell: bash
|
|
command: |
|
|
set -e
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
make --directory=aw-tauri build
|
|
env:
|
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets[env.AW_RESEARCH_EDITION == 'true' && 'TAURI_SIGNING_PRIVATE_KEY_RESEARCH' || 'TAURI_SIGNING_PRIVATE_KEY'] }}
|
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets[env.AW_RESEARCH_EDITION == 'true' && 'TAURI_SIGNING_PRIVATE_KEY_PASSWORD_RESEARCH' || 'TAURI_SIGNING_PRIVATE_KEY_PASSWORD'] }}
|
|
|
|
- name: Import macOS signing certificate
|
|
if: runner.os == 'macOS' && (startsWith(github.ref, 'refs/tags/v') || env.AW_RESEARCH_EDITION == 'true')
|
|
run: |
|
|
for var in APPLE_EMAIL APPLE_PASSWORD APPLE_TEAMID CERTIFICATE_MACOS_P12_BASE64 CERTIFICATE_MACOS_P12_PASSWORD; do
|
|
if [ -z "${!var:-}" ]; then
|
|
echo "ERROR: $var is required to sign macOS release artifacts" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
./scripts/ci/import-macos-p12.sh
|
|
APPLE_PERSONALID=$(security find-identity -v -p codesigning "${KEY_CHAIN:-build.keychain-db}" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)
|
|
if [ -z "$APPLE_PERSONALID" ]; then
|
|
echo "ERROR: no codesigning identity found after importing the macOS certificate" >&2
|
|
exit 1
|
|
fi
|
|
echo "APPLE_PERSONALID=$APPLE_PERSONALID" >> "$GITHUB_ENV"
|
|
env:
|
|
APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}
|
|
CERTIFICATE_MACOS_P12_BASE64: ${{ secrets.CERTIFICATE_MACOS_P12_BASE64 }}
|
|
CERTIFICATE_MACOS_P12_PASSWORD: ${{ secrets.CERTIFICATE_MACOS_P12_PASSWORD }}
|
|
|
|
- name: Package
|
|
run: |
|
|
source venv/bin/activate || source venv/Scripts/activate
|
|
poetry install
|
|
make package SKIP_SERVER_RUST=${{ matrix.skip_rust }}
|
|
|
|
- name: Package Linux (Tauri bundles)
|
|
if: runner.os == 'Linux'
|
|
run: |
|
|
# aw-tauri's bundler produces .AppImage/.deb/.rpm under
|
|
# dist/activitywatch/aw-tauri/ via `make package`. Copy them out with
|
|
# versioned, arch-suffixed filenames so they match the
|
|
# `dist/activitywatch-*.*` upload pattern below.
|
|
ARCH=$(uname -m)
|
|
shopt -s nullglob
|
|
EDITION=""
|
|
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then EDITION="-research"; fi
|
|
for ext in AppImage deb rpm; do
|
|
files=( dist/activitywatch/aw-tauri/*.${ext} )
|
|
case ${#files[@]} in
|
|
0) continue ;;
|
|
1) cp -v "${files[0]}" "dist/activitywatch-tauri${EDITION}-${VERSION_WITH_V}-linux-${ARCH}.${ext}" ;;
|
|
*) echo "ERROR: expected at most 1 .${ext} bundle, found ${#files[@]}" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
- name: Package dmg
|
|
if: runner.os == 'macOS'
|
|
run: |
|
|
SIGN_MACOS=false
|
|
if [[ "${GITHUB_REF:-}" == refs/tags/v* || "$AW_RESEARCH_EDITION" == "true" ]]; then
|
|
SIGN_MACOS=true
|
|
fi
|
|
|
|
BUNDLE="ActivityWatch"
|
|
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then
|
|
BUNDLE="ActivityWatch-Research"
|
|
fi
|
|
|
|
source venv/bin/activate
|
|
make "dist/${BUNDLE}.dmg" APP_BUNDLE="${BUNDLE}"
|
|
|
|
if [ "$SIGN_MACOS" = true ]; then
|
|
codesign --force --verbose --timestamp -s "${APPLE_PERSONALID}" "dist/${BUNDLE}.dmg"
|
|
|
|
brew install akeru-inc/tap/xcnotary
|
|
xcnotary precheck "dist/${BUNDLE}.app"
|
|
xcnotary precheck "dist/${BUNDLE}.dmg"
|
|
|
|
make dist/notarize
|
|
fi
|
|
EDITION=""
|
|
if [[ "$AW_RESEARCH_EDITION" == "true" ]]; then EDITION="-research"; fi
|
|
mv "dist/${BUNDLE}.dmg" dist/activitywatch-tauri${EDITION}-${VERSION_WITH_V}-macos-$(uname -m).dmg
|
|
env:
|
|
APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
|
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
|
APPLE_TEAMID: ${{ secrets.APPLE_TEAMID }}
|
|
|
|
- name: Package Tauri updater artifacts
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist/updater
|
|
VERSION="${VERSION_WITH_V#v}"
|
|
|
|
case "$RUNNER_OS" in
|
|
macOS) OS_NAME="darwin" ;;
|
|
Linux) OS_NAME="linux" ;;
|
|
Windows) OS_NAME="windows" ;;
|
|
esac
|
|
case "$(uname -m)" in
|
|
arm64|aarch64) ARCH_NAME="aarch64" ;;
|
|
x86_64|amd64) ARCH_NAME="x86_64" ;;
|
|
*) ARCH_NAME="$(uname -m)" ;;
|
|
esac
|
|
PLATFORM_KEY="${OS_NAME}-${ARCH_NAME}"
|
|
EDITION=""
|
|
if [[ "${AW_RESEARCH_EDITION:-}" == "true" ]]; then EDITION="-research"; fi
|
|
|
|
# Only the bundle formats Tauri's updater consumes — avoids picking up
|
|
# .sig files for .deb/.rpm/.dmg if those ever get signed too.
|
|
found=0
|
|
while IFS= read -r -d '' sig; do
|
|
artifact="${sig%.sig}"
|
|
if [ -f "$artifact" ]; then
|
|
base="${artifact##*/}"
|
|
# Bundle names embed the crate version (e.g. aw-tauri_0.1.0_amd64.AppImage),
|
|
# so "everything after the first dot" would yield "1.0_amd64.AppImage".
|
|
case "$base" in
|
|
*.app.tar.gz) ext="app.tar.gz" ;;
|
|
*.AppImage.tar.gz) ext="AppImage.tar.gz" ;;
|
|
*.AppImage) ext="AppImage" ;;
|
|
*.nsis.zip) ext="nsis.zip" ;;
|
|
*.msi.zip) ext="msi.zip" ;;
|
|
*.msi) ext="msi" ;;
|
|
*.exe) ext="exe" ;;
|
|
*) echo "unknown updater bundle: $base" >&2; exit 1 ;;
|
|
esac
|
|
out="dist/updater/activitywatch-tauri${EDITION}-${VERSION}-${PLATFORM_KEY}.${ext}"
|
|
cp "$artifact" "$out"
|
|
cp "$sig" "$out.sig"
|
|
found=$((found + 1))
|
|
fi
|
|
done < <(find aw-tauri/src-tauri/target -type f \( \
|
|
-path "*/release/bundle/macos/*.sig" -o \
|
|
-path "*/release/bundle/appimage/*.sig" -o \
|
|
-path "*/release/bundle/nsis/*.sig" -o \
|
|
-path "*/release/bundle/msi/*.sig" \
|
|
\) -print0)
|
|
|
|
echo "Found $found Tauri updater artifact(s) for $PLATFORM_KEY"
|
|
ls -la dist/updater/ || true
|
|
|
|
- name: Upload packages
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: builds-tauri-${{ matrix.os }}-py${{ matrix.python_version }}${{ matrix.research && '-research' || '' }}
|
|
path: |
|
|
dist/activitywatch-*.*
|
|
dist/updater/*
|
|
|
|
release-notes:
|
|
name: Generate release notes
|
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
submodules: 'recursive'
|
|
fetch-depth: 0
|
|
|
|
- uses: ActivityWatch/check-version-format-action@5b223bd718c4c00466359532cf944474f069625a # v2.0.1
|
|
id: version
|
|
with:
|
|
prefix: 'v'
|
|
|
|
- name: Echo version
|
|
run: |
|
|
echo "${{ steps.version.outputs.full }} (stable: ${{ steps.version.outputs.is_stable }})"
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v7
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install deps
|
|
run: |
|
|
pip install requests
|
|
|
|
- name: Generate release notes
|
|
if: ${{ !endsWith(github.ref_name, '-research') }}
|
|
run: |
|
|
LAST_RELEASE=`STABLE_ONLY=${{ steps.version.outputs.is_stable }} ./scripts/get_latest_release.sh`
|
|
./scripts/build_changelog.py --range "$LAST_RELEASE...${{ steps.version.outputs.full }}"
|
|
|
|
- name: Generate release notes (research edition)
|
|
if: endsWith(github.ref_name, '-research')
|
|
run: |
|
|
# check-version-format-action cannot parse research-suffixed tags,
|
|
# so derive the range end from the tag name directly. Research
|
|
# releases are never stable.
|
|
LAST_RELEASE=$(STABLE_ONLY=false ./scripts/get_latest_release.sh)
|
|
# get_latest_release.sh only matches standard tags; use git describe
|
|
# to find the topologically nearest ancestral research release.
|
|
# Excluding the current tag also ignores newer or unrelated lineages.
|
|
prev_research=$(git describe --tags --abbrev=0 \
|
|
--match '*-research' --exclude "${GITHUB_REF_NAME}" \
|
|
"${GITHUB_REF_NAME}" 2>/dev/null || true)
|
|
if [ -n "$LAST_RELEASE" ] && [ -n "$prev_research" ] && \
|
|
git merge-base --is-ancestor "$LAST_RELEASE" "$prev_research"; then
|
|
LAST_RELEASE="$prev_research"
|
|
fi
|
|
./scripts/build_changelog.py --range "$LAST_RELEASE...${GITHUB_REF_NAME}"
|
|
|
|
- name: Rename
|
|
run: |
|
|
mv changelog.md release_notes.md
|
|
|
|
- name: Adjust release notes for research edition
|
|
if: endsWith(github.ref_name, '-research')
|
|
run: |
|
|
{
|
|
echo "> [!IMPORTANT]"
|
|
echo "> **ActivityWatch Research Edition** (\`${GITHUB_REF_NAME}\`) — prerelease build for research-study participants, with the privacy filter and study category map enabled by default (\`research_enabled = true\` in aw-watcher-window)."
|
|
echo "> Not intended for general use — regular users should install the latest [standard release](https://github.com/ActivityWatch/activitywatch/releases) instead."
|
|
echo ""
|
|
cat release_notes.md
|
|
} > release_notes_research.md
|
|
mv release_notes_research.md release_notes.md
|
|
# The generated Downloads section hardcodes standard-edition asset
|
|
# filenames, which do not exist on research releases (research
|
|
# artifacts carry a -research token). Point at the asset
|
|
# list instead of linking to 404s.
|
|
python3 - <<'PY'
|
|
import re
|
|
|
|
path = "release_notes.md"
|
|
notes = open(path).read()
|
|
notes = re.sub(
|
|
r"(?m)^ - \[\*\*(Windows|macOS|Linux)\*\*\]\([^)]*\)[^\n]*\n?",
|
|
"",
|
|
notes,
|
|
)
|
|
notes = notes.replace(
|
|
"# Downloads\n",
|
|
"# Downloads\n\nResearch Edition installers are attached to this"
|
|
" release as assets below (filenames contain `research`)."
|
|
" Standard-release download links do not apply to this build.\n",
|
|
)
|
|
open(path, "w").write(notes)
|
|
PY
|
|
|
|
- name: Upload release notes
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: release_notes
|
|
path: release_notes.md
|
|
|
|
release:
|
|
name: Publish draft release
|
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
needs: [build-qt, build-qt-manylinux-2-28, build-tauri, release-notes]
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
# Pin checkout in this contents-write job so a moved v7 tag cannot
|
|
# change the release automation. Build jobs keep actions/checkout@v7.
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Download build artifacts
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: dist
|
|
|
|
- name: Display structure of downloaded files
|
|
run: ls -R
|
|
working-directory: dist
|
|
|
|
- uses: ActivityWatch/check-version-format-action@5b223bd718c4c00466359532cf944474f069625a # v2.0.1
|
|
id: version
|
|
with:
|
|
prefix: 'v'
|
|
|
|
# Tag/repo values are passed via env (GITHUB_REF_NAME / GITHUB_REPOSITORY
|
|
# are set by Actions) rather than interpolated into the shell script, so
|
|
# a crafted v* tag cannot inject commands into this contents-write job.
|
|
#
|
|
# Keep versioned release assets separate. Research clients use the
|
|
# independent research-updates branch; these assets do not publish that feed.
|
|
- name: Generate updater manifest
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${GITHUB_REF_NAME}"
|
|
VERSION="${TAG#v}"
|
|
VERSION="${VERSION%-research}"
|
|
if [[ "$TAG" == *-research ]]; then
|
|
EDITION="research"
|
|
MANIFEST="latest-research.json"
|
|
NOTES="ActivityWatch ${TAG} (Research Edition)"
|
|
else
|
|
EDITION="standard"
|
|
MANIFEST="latest.json"
|
|
NOTES="ActivityWatch ${TAG}"
|
|
fi
|
|
python3 scripts/package/generate_latest_json.py \
|
|
--version "$VERSION" \
|
|
--edition "$EDITION" \
|
|
--notes "$NOTES" \
|
|
--repo "${GITHUB_REPOSITORY}" \
|
|
--tag "$TAG" \
|
|
--dist dist \
|
|
--output "dist/${MANIFEST}"
|
|
cat "dist/${MANIFEST}"
|
|
|
|
- name: Release
|
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
|
with:
|
|
draft: true
|
|
# Empty name falls back to the tag name (standard releases unchanged).
|
|
name: ${{ endsWith(github.ref_name, '-research') && format('{0} (Research Edition)', github.ref_name) || '' }}
|
|
files: |
|
|
dist/*/activitywatch-*.*
|
|
dist/*/updater/*
|
|
dist/latest.json
|
|
dist/latest-research.json
|
|
body_path: dist/release_notes/release_notes.md
|
|
# check-version-format-action leaves is_stable unset/false for
|
|
# research-suffixed tags, so this stays prerelease for them too.
|
|
prerelease: ${{ !(steps.version.outputs.is_stable == 'true') }}
|
|
|
|
# Retry CI after transient upload-artifact timeout.
|