Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f89efa9
ci(release): regenerate versioned clients on release branches
turinglambdaai Oct 22, 2026
278d999
ci: allow dispatching full CI for generated release branches
turinglambdaai Oct 22, 2026
03d5bad
ci(release): tag and publish only after verified version merge
turinglambdaai Oct 22, 2026
aef5486
ci(release): limit client regeneration to release branch pushes
turinglambdaai Oct 22, 2026
1d4e100
ci(release): verify signing key matches embedded public keys
turinglambdaai Oct 22, 2026
2f1bbc4
revert: keep release automation in review until PR merge
turinglambdaai Oct 22, 2026
35a4a4e
ci(release): verify signing key matches embedded public keys
turinglambdaai Oct 22, 2026
3c4b8e4
ci(release): allow safe retries without moving annotated tags
turinglambdaai Oct 22, 2026
d0ca378
ci(release): generate clients only after release PR is opened
turinglambdaai Oct 22, 2026
aecdaab
ci(release): pass release branch to generated client commit
turinglambdaai Oct 22, 2026
3468f6e
ci(release): match the v-prefixed release branch name
turinglambdaai Oct 22, 2026
595b846
fix(release): derive manifest key ID from updater contract
turinglambdaai Oct 22, 2026
89519c4
ci(release): validate pinned update key IDs
turinglambdaai Oct 22, 2026
59b7feb
test(release): enforce update key ID alignment in preflight
turinglambdaai Oct 22, 2026
f9a2045
fix(release): reject signing keys not trusted by clients
turinglambdaai Oct 22, 2026
b534f86
ci(release): centralize key ID preflight
turinglambdaai Oct 22, 2026
7a56735
test(release): lint update signing scripts in CI
turinglambdaai Oct 22, 2026
ecc3446
ci: cancel obsolete runs for the same ref
turinglambdaai Oct 22, 2026
2898201
AGENTS.md: add family portfolio placement section
turinglambdaai Oct 22, 2026
b981a57
AGENTS.md: sync portfolio placement with PORTFOLIO.md revision
turinglambdaai Oct 22, 2026
e7c6049
AGENTS.md: sync portfolio placement with PORTFOLIO.md (paid-track exp…
turinglambdaai Oct 22, 2026
c7ae32d
fix(linux): prevent undo-banner crash in native acceptance (#54)
turinglambdaai Oct 22, 2026
5e0e60a
Merge main into release automation and harden shell syntax checks
turinglambdaai Oct 22, 2026
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
37 changes: 34 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: CI

on:
workflow_dispatch:
push:
branches: [main]
pull_request:
Expand All @@ -9,6 +10,10 @@ on:
permissions:
contents: read

concurrency:
group: ci-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

# Single source of truth for the rivet pin; every install step below
# references this so a bump is one edit, not a grep-replace across jobs.
env:
Expand All @@ -23,6 +28,12 @@ jobs:
run: scripts/sync-i18n.sh --check
- name: Verify release version alignment
run: bash scripts/check-release-version.sh
- name: Check release script shell syntax
run: |
set -euo pipefail
for script in scripts/check-release-version.sh scripts/make-update-manifest.sh scripts/update-keys.sh; do
bash -n "$script"
done
- name: Verify dependency pins stay aligned
run: bash scripts/check-dependency-pins.sh
- name: Verify Windows installer bitmap and checkbox surface
Expand Down Expand Up @@ -136,14 +147,34 @@ jobs:
- name: Build host
run: raco rivet build
- name: Boot smoke
shell: bash
run: |
set -euo pipefail
smoke_home="$RUNNER_TEMP/taskly-smoke-home"
smoke_log="$RUNNER_TEMP/taskly-smoke.log"
mkdir -p "$smoke_home"
HOME="$smoke_home" ./.rivet/stage/RivetHost &
HOME="$smoke_home" ./.rivet/stage/RivetHost >"$smoke_log" 2>&1 &
APP_PID=$!
cleanup() {
if kill -0 "$APP_PID" 2>/dev/null; then
kill -TERM "$APP_PID" 2>/dev/null || true
for _ in 1 2 3 4 5; do
kill -0 "$APP_PID" 2>/dev/null || break
sleep 1
done
kill -KILL "$APP_PID" 2>/dev/null || true
fi
wait "$APP_PID" 2>/dev/null || true
}
trap cleanup EXIT
sleep 8
kill -0 $APP_PID
kill $APP_PID
if ! kill -0 "$APP_PID" 2>/dev/null; then
cat "$smoke_log" || true
echo "::error::RivetHost exited before the 8-second smoke window"
exit 1
fi
cleanup
trap - EXIT

windows-host:
name: Windows host (WinRT build)
Expand Down
253 changes: 253 additions & 0 deletions .github/workflows/release-version-client-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
name: Regenerate versioned clients

on:
pull_request:
types: [opened, synchronize, reopened, closed]
branches: [main]

permissions:
contents: write
actions: write

concurrency:
group: release-version-client-sync-${{ github.ref }}
cancel-in-progress: false

env:
RIVET_PIN: 1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a
KEEL_PIN: 0740fdfbdab7bf5dab32232c82f07dd98aa6a196

jobs:
regenerate:
if: >-
github.event_name == 'pull_request' &&
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/version-')
name: Regenerate and validate versioned clients
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0

- name: Set up Racket CS 9.3
uses: Bogdanp/setup-racket@v1.15
with:
architecture: x64
distribution: full
variant: CS
version: '9.3'

- name: Install native build dependencies
run: |
sudo apt-get update
sudo apt-get install --yes cmake ninja-build pkg-config \
libgtk-4-dev libadwaita-1-dev libjson-glib-dev \
libsecret-1-dev libglib2.0-dev

- name: Set up embeddable Racket CS
uses: turinglambdaai/rivet/.github/actions/setup-embed-racket@1ebb3ac09ed2e23eeb0a91cbec6e689aac01ab1a

- name: Install pinned Racket dependencies
run: |
raco pkg install --auto --no-docs "https://github.com/turinglambdaai/keel.git#${KEEL_PIN}"
raco pkg install --auto --no-docs "https://github.com/turinglambdaai/rivet.git#${RIVET_PIN}"
raco pkg install --auto --no-docs db crypto

- name: Regenerate clients from the canonical RPC contract
run: |
set -euo pipefail
raco rivet build
bash scripts/check-release-version.sh

- name: Commit generated client version identities
env:
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add \
macos-host/Sources/RivetHost/GeneratedBackend.swift \
windows/GeneratedBackend.hpp \
linux/GeneratedBackend.hpp
if git diff --cached --quiet; then
echo "Generated client files are already aligned."
else
git commit -m "chore(release): regenerate versioned clients"
git push origin "HEAD:${RELEASE_BRANCH}"
fi

- name: Dispatch full CI and product acceptance on generated commit
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
set -euo pipefail
gh workflow run ci.yml --ref "${RELEASE_BRANCH}"
gh workflow run acceptance.yml --ref "${RELEASE_BRANCH}"

publish-verified-release:
name: Tag and publish after merged version PR
if: >-
github.event_name == 'pull_request' &&
github.event.action == 'closed' &&
github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.event.pull_request.head.ref, 'release/version-')
runs-on: ubuntu-latest
timeout-minutes: 180
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 0

- name: Verify version, release notes, and signing secret
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
UPDATE_ED25519_PRIVATE_KEY: ${{ secrets.UPDATE_ED25519_PRIVATE_KEY }}
run: |
set -euo pipefail
VERSION="$(tr -d '[:space:]' < VERSION)"
TAG="v${VERSION}"
[[ "$RELEASE_BRANCH" == "release/version-v$VERSION" ]] || {
echo "::error::Release branch $RELEASE_BRANCH does not match VERSION $VERSION"
exit 1
}
bash scripts/check-release-version.sh
NOTES="$(awk -v header="## [${VERSION}]" '
index($0, header) == 1 {on=1; next}
on && /^## / {exit}
on {print}
' CHANGELOG.md)"
[[ -n "$(printf '%s' "$NOTES" | tr -d '[:space:]')" ]] || {
echo "::error::Missing release notes for $VERSION"
exit 1
}
[[ -n "${UPDATE_ED25519_PRIVATE_KEY:-}" ]] || {
echo "::error::UPDATE_ED25519_PRIVATE_KEY is not configured; no tag will be created"
exit 1
}
# Confirm the configured private key matches both embedded public
# keys before creating an immutable tag. Never print the private key.
KEY_PEM="$(mktemp)"
trap 'rm -f "$KEY_PEM"' EXIT
printf '%s\n' "$UPDATE_ED25519_PRIVATE_KEY" > "$KEY_PEM"
DER_PUBLIC="$(openssl pkey -in "$KEY_PEM" -pubout -outform DER 2>/dev/null | base64 -w0)"
RAW_PUBLIC="$(openssl pkey -in "$KEY_PEM" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 -w0)"
EXPECTED_DER="$(sed -n '/(define update-public-key-b64/{n;s/^[[:space:]]*"\([^"]*\)".*/\1/p;}' racket/taskly/updater.rkt)"
EXPECTED_RAW="$(sed -n 's/.*public static let publicKeyBase64 = "\([^"]*\)".*/\1/p' macos-host/Sources/RivetHost/UpdateService.swift)"
[[ -n "$EXPECTED_DER" && "$DER_PUBLIC" == "$EXPECTED_DER" ]] || {
echo "::error::Signing secret does not match the backend Ed25519 public key"
exit 1
}
[[ -n "$EXPECTED_RAW" && "$RAW_PUBLIC" == "$EXPECTED_RAW" ]] || {
echo "::error::Signing secret does not match the macOS Ed25519 public key"
exit 1
}
if [[ -n "$(git ls-remote --tags origin "refs/tags/$TAG")" ]]; then
# A failed publish can be retried without moving the immutable tag,
# but only if it is annotated and points at this exact merge commit.
git fetch --force origin "refs/tags/$TAG:refs/tags/$TAG"
[[ "$(git cat-file -t "$TAG")" == "tag" ]] || {
echo "::error::Existing tag $TAG is lightweight; refusing to use it"
exit 1
}
[[ "$(git rev-parse "$TAG^{commit}")" == "$(git rev-parse HEAD)" ]] || {
echo "::error::Existing tag $TAG points at a different commit"
exit 1
}
echo "Reusing existing annotated tag $TAG for a safe release retry"
fi
echo "Verified release candidate $TAG at $(git rev-parse HEAD)"

- name: Wait for CI and product acceptance on the merge commit
env:
GH_TOKEN: ${{ github.token }}
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
set -euo pipefail
ci_ok=false
acceptance_ok=false
for attempt in $(seq 1 180); do
runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${MERGE_SHA}&per_page=100")"
ci="$(jq -c --arg sha "$MERGE_SHA" '[.workflow_runs[] | select(.name == "CI" and .event == "push" and .head_sha == $sha)] | sort_by(.created_at) | last // empty' <<< "$runs")"
acceptance="$(jq -c --arg sha "$MERGE_SHA" '[.workflow_runs[] | select(.name == "Product Acceptance" and .event == "push" and .head_sha == $sha)] | sort_by(.created_at) | last // empty' <<< "$runs")"
if [[ -n "$ci" ]]; then
status="$(jq -r '.status' <<< "$ci")"
conclusion="$(jq -r '.conclusion // ""' <<< "$ci")"
if [[ "$status" == "completed" && "$conclusion" != "success" ]]; then
echo "::error::CI did not pass for merge commit $MERGE_SHA: $conclusion"
exit 1
fi
[[ "$status" == "completed" && "$conclusion" == "success" ]] && ci_ok=true
fi
if [[ -n "$acceptance" ]]; then
status="$(jq -r '.status' <<< "$acceptance")"
conclusion="$(jq -r '.conclusion // ""' <<< "$acceptance")"
if [[ "$status" == "completed" && "$conclusion" != "success" ]]; then
echo "::error::Product Acceptance did not pass for merge commit $MERGE_SHA: $conclusion"
exit 1
fi
[[ "$status" == "completed" && "$conclusion" == "success" ]] && acceptance_ok=true
fi
if [[ "$ci_ok" == true && "$acceptance_ok" == true ]]; then
echo "CI and Product Acceptance passed for $MERGE_SHA"
break
fi
if [[ "$attempt" -eq 180 ]]; then
echo "::error::Timed out waiting for CI and Product Acceptance on $MERGE_SHA"
exit 1
fi
sleep 15
done

- name: Create immutable annotated tag and dispatch release build
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
VERSION="$(tr -d '[:space:]' < VERSION)"
TAG="v${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if git show-ref --verify --quiet "refs/tags/$TAG"; then
echo "Reusing immutable tag $TAG"
else
git tag -a "$TAG" -m "Taskly $TAG"
git push origin "$TAG"
fi
gh workflow run release.yml --ref "$TAG"

- name: Wait for the release workflow
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
set -euo pipefail
for attempt in $(seq 1 360); do
runs="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs?head_sha=${RELEASE_SHA}&per_page=100")"
release="$(jq -c --arg sha "$RELEASE_SHA" '[.workflow_runs[] | select(.name == "Release" and .event == "workflow_dispatch" and .head_sha == $sha)] | sort_by(.created_at) | last // empty' <<< "$runs")"
if [[ -n "$release" ]]; then
status="$(jq -r '.status' <<< "$release")"
conclusion="$(jq -r '.conclusion // ""' <<< "$release")"
url="$(jq -r '.html_url' <<< "$release")"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" != "success" ]]; then
echo "::error::Release workflow failed: $url"
exit 1
fi
echo "Release completed successfully: $url"
exit 0
fi
fi
if [[ "$attempt" -eq 360 ]]; then
echo "::error::Timed out waiting for release workflow"
exit 1
fi
sleep 15
done
14 changes: 14 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ jobs:
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
bash scripts/check-release-version.sh "$GITHUB_REF_NAME"
mkdir -p dist
# cp -R merges: rivet releases leave a dist/taskly-linux-<arch>/
# staging bundle in the artifacts, and a plain mv dies on the
Expand All @@ -251,6 +252,19 @@ jobs:
KEY_PEM="$RUNNER_TEMP/update-signing-key.pem"
printf '%s\n' "$UPDATE_ED25519_PRIVATE_KEY" > "$KEY_PEM"
chmod 600 "$KEY_PEM"
# Refuse to publish a feed signed by a key the shipped clients do not trust.
DER_PUBLIC="$(openssl pkey -in "$KEY_PEM" -pubout -outform DER 2>/dev/null | base64 -w0)"
RAW_PUBLIC="$(openssl pkey -in "$KEY_PEM" -pubout -outform DER 2>/dev/null | tail -c 32 | base64 -w0)"
EXPECTED_DER="$(sed -n '/(define update-public-key-b64/{n;s/^[[:space:]]*"\([^"]*\)".*/\1/p;}' racket/taskly/updater.rkt)"
EXPECTED_RAW="$(sed -n 's/.*public static let publicKeyBase64 = "\([^"]*\)".*/\1/p' macos-host/Sources/RivetHost/UpdateService.swift)"
[[ -n "$EXPECTED_DER" && "$DER_PUBLIC" == "$EXPECTED_DER" ]] || {
echo "::error::Signing secret does not match the backend Ed25519 public key"
exit 1
}
[[ -n "$EXPECTED_RAW" && "$RAW_PUBLIC" == "$EXPECTED_RAW" ]] || {
echo "::error::Signing secret does not match the macOS Ed25519 public key"
exit 1
}
scripts/make-update-manifest.sh "$GITHUB_REF_NAME" dist "$KEY_PEM"
test -s dist/update-manifest.json || {
echo "::error::Signed update manifest was not generated"
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,9 @@ taskly/
- **macOS 宿主 UI**:`macos-host/Sources/RivetHost/`(AppModel @Observable + SwiftUI 视图),交互基准见 PRODUCT-SPEC §5b
- **改文案**:改 `shared/i18n/`,跑 `scripts/sync-i18n.sh`;改清单图标:改 `shared/emoji.json` 同样同步
- **发版**:见「快速命令」发布链;rivet pin(ci.yml 与 release.yml 的 `RIVET_PIN` 保持一致)只能有意 bump 并重新跑全套 CI

## 组合定位(Portfolio placement)

- 轨道与授权:轨道 2 消费买断(消费主打);授权形态:AGPL-3.0;商业归属:收费产品——本地 SQLite 单文件 + 三平台原生 + CLI 是真实差异,按 Things 式买断 + 更新窗收费;v0.2 还年轻,先补成熟度并按未来 Pro 边界(候选:自然语言输入、提醒/日历集成、主题)做每个功能决策。
- 组合归属唯一权威:[PORTFOLIO.md](https://github.com/turinglambdaai/turinglambdaai.github.io/blob/master/PORTFOLIO.md)(turinglambdaai.github.io 仓库)。改归属/授权先改权威档,再同步本节。
- 纪律:价格只进本仓 `PRICING.md`,且收费开始时才发布;README 与产品站不做价格预告。
Loading
Loading