Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
afd45bb
chore: ignore local worktrees
unique01082 Aug 14, 2026
37b38ca
docs: add Rootline v2 implementation plan
unique01082 Aug 14, 2026
07f1b62
feat: establish Rootline workspace contracts
unique01082 Aug 14, 2026
b3fdb0e
feat: implement Rootline core and CLI v2
unique01082 Aug 14, 2026
28bdf06
fix: harden Rootline CLI plan safety
unique01082 Aug 14, 2026
052f83b
fix: make Rootline overlap checks locale independent
unique01082 Aug 14, 2026
5eb0e6f
fix: enforce CLI exits and canonical roots
unique01082 Aug 14, 2026
57a1279
fix: reject linked synchronization roots
unique01082 Aug 14, 2026
99715fb
feat: implement Rootline desktop offline workflow
unique01082 Aug 14, 2026
ed03914
fix: address Rootline desktop review findings
unique01082 Aug 14, 2026
754fb8c
fix: declare tree multiselect semantics
unique01082 Aug 14, 2026
f703f2f
feat: add Authentik hosted profile sync
unique01082 Aug 14, 2026
ebd12b0
fix: harden hosted sync lifecycle
unique01082 Aug 15, 2026
db0fe2b
fix: stop stale account sync retries
unique01082 Aug 15, 2026
a4775c7
feat: harden Rootline release pipeline
unique01082 Aug 15, 2026
5704843
fix: close Rootline release review gaps
unique01082 Aug 15, 2026
d5e9ff7
fix: resolve Rootline v2 final review findings
unique01082 Aug 15, 2026
038e58c
fix: close Rootline v2 follow-up review gaps
unique01082 Aug 15, 2026
8261f80
fix: fail closed on legacy profile provenance
unique01082 Aug 15, 2026
b7edaf9
fix: complete Rootline v2 acceptance gaps
unique01082 Aug 15, 2026
eedcfae
fix: close Rootline plan acceptance gaps
unique01082 Aug 15, 2026
845e31b
test: enforce Rootline plan acceptance
unique01082 Aug 15, 2026
0b828ea
feat: add signed desktop candidate workflow
unique01082 Aug 15, 2026
bcd83f2
fix: bootstrap workspace builds on clean runners
unique01082 Aug 15, 2026
2c8791c
fix: make clean CI runners self-contained
unique01082 Aug 15, 2026
e913f60
fix: make CLI smoke tests portable on Windows
unique01082 Aug 15, 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
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
.github
.superpowers
.worktrees
**/dist
**/node_modules
apps/desktop/src-tauri/target
docs
tests
148 changes: 148 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: CI

"on":
pull_request:
push:
branches: [main, master]

permissions:
contents: read

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

jobs:
typescript-quality:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm audit --prod --audit-level high
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test:unit
- run: pnpm build
- run: pnpm test:release

npm-tarball-smoke:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm --filter folder-structure-sync test:pack

api-image-smoke:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:api-image

postgres-integration:
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: rootline
POSTGRES_PASSWORD: rootline
POSTGRES_DB: rootline_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U rootline -d rootline_test"
--health-interval 2s
--health-timeout 5s
--health-retries 30
env:
DATABASE_URL: postgresql://rootline:rootline@127.0.0.1:5432/rootline_test?schema=public
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install Tauri system dependencies for the native PostgreSQL seam
run: sudo apt-get update && sudo apt-get install --yes libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: apps/desktop/src-tauri
- run: pnpm install --frozen-lockfile
- run: pnpm build:workspace-deps
- run: pnpm --filter @rootline/api prisma:generate
- run: pnpm --filter @rootline/api prisma:migrate:deploy
- run: pnpm --filter @rootline/api test:e2e

rust:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install Tauri system dependencies
run: sudo apt-get update && sudo apt-get install --yes libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: apps/desktop/src-tauri
- run: cargo fmt --check --manifest-path apps/desktop/src-tauri/Cargo.toml
- run: cargo clippy --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets --all-features -- -D warnings
- run: cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml

tauri-build:
name: Tauri ${{ matrix.label }}
strategy:
fail-fast: false
matrix:
include:
- label: macOS Universal
runner: macos-15
target: universal-apple-darwin
- label: Windows x64
runner: windows-2025
target: x86_64-pc-windows-msvc
- label: Windows ARM64
runner: windows-11-arm
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
targets: ${{ matrix.target == 'universal-apple-darwin' && 'aarch64-apple-darwin,x86_64-apple-darwin' || matrix.target }}
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: apps/desktop/src-tauri
- run: pnpm install --frozen-lockfile
- name: Run Windows filesystem adapter tests
if: runner.os == 'Windows' && matrix.target == 'x86_64-pc-windows-msvc'
shell: pwsh
run: |
pnpm --filter folder-structure-sync test
if ($LASTEXITCODE -ne 0) { throw "Node filesystem adapter tests failed on Windows." }
cargo test --manifest-path apps/desktop/src-tauri/Cargo.toml
if ($LASTEXITCODE -ne 0) { throw "Rust filesystem adapter tests failed on Windows." }
- run: pnpm --filter @rootline/desktop tauri build --target ${{ matrix.target }} --no-bundle
231 changes: 231 additions & 0 deletions .github/workflows/release-api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
name: Release Rootline API 2.0.0

"on":
workflow_dispatch:
inputs:
confirmation:
description: Type release-api-v2.0.0 to deploy the stable API
required: true
type: string

permissions:
contents: read

concurrency:
group: release-api-2.0.0
cancel-in-progress: false

jobs:
preflight:
runs-on: ubuntu-24.04
environment: api-production
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm audit --prod --audit-level high
- name: Require exact version and every production deployment secret
env:
CONFIRMATION: ${{ inputs.confirmation }}
ROOTLINE_API_DATABASE_URL: ${{ secrets.ROOTLINE_API_DATABASE_URL }}
ROOTLINE_API_DEPLOY_WEBHOOK_URL: ${{ secrets.ROOTLINE_API_DEPLOY_WEBHOOK_URL }}
ROOTLINE_API_DEPLOY_TOKEN: ${{ secrets.ROOTLINE_API_DEPLOY_TOKEN }}
ROOTLINE_API_BASE_URL: ${{ secrets.ROOTLINE_API_BASE_URL }}
ROOTLINE_JWT_ISSUER: ${{ secrets.ROOTLINE_JWT_ISSUER }}
ROOTLINE_JWT_AUDIENCE: ${{ secrets.ROOTLINE_JWT_AUDIENCE }}
ROOTLINE_JWT_JWKS_B64: ${{ secrets.ROOTLINE_JWT_JWKS_B64 }}
run: |
set -eu
if [ "${GITHUB_REF}" != "refs/tags/v2.0.0" ]; then
echo "::error::Stable API release is restricted to the existing tag refs/tags/v2.0.0."
exit 1
fi
if [ "${CONFIRMATION}" != "release-api-v2.0.0" ]; then
echo "::error::Stable API release blocked. Re-run with confirmation=release-api-v2.0.0."
exit 1
fi
missing=""
for name in ROOTLINE_API_DATABASE_URL ROOTLINE_API_DEPLOY_WEBHOOK_URL ROOTLINE_API_DEPLOY_TOKEN ROOTLINE_API_BASE_URL ROOTLINE_JWT_ISSUER ROOTLINE_JWT_AUDIENCE ROOTLINE_JWT_JWKS_B64; do
eval "value=\${$name:-}"
[ -n "$value" ] || missing="$missing $name"
done
if [ -n "$missing" ]; then
echo "::error::Stable API release blocked. Configure repository environment secrets:$missing"
exit 1
fi
case "${ROOTLINE_API_BASE_URL}" in https://*) ;; *) echo "::error::ROOTLINE_API_BASE_URL must use HTTPS."; exit 1 ;; esac
case "${ROOTLINE_API_DEPLOY_WEBHOOK_URL}" in https://*) ;; *) echo "::error::ROOTLINE_API_DEPLOY_WEBHOOK_URL must use HTTPS."; exit 1 ;; esac
node <<'NODE'
const database = new URL(process.env.ROOTLINE_API_DATABASE_URL);
if (!["postgres:", "postgresql:"].includes(database.protocol)) throw new Error("ROOTLINE_API_DATABASE_URL must be PostgreSQL.");
if (database.searchParams.get("sslmode") !== "require" || database.searchParams.get("sslaccept") !== "strict") {
throw new Error("ROOTLINE_API_DATABASE_URL must verify PostgreSQL TLS with sslmode=require&sslaccept=strict.");
}
if (new URL(process.env.ROOTLINE_JWT_ISSUER).protocol !== "https:") throw new Error("ROOTLINE_JWT_ISSUER must use HTTPS.");
const jwks = JSON.parse(Buffer.from(process.env.ROOTLINE_JWT_JWKS_B64, "base64").toString("utf8"));
if (!Array.isArray(jwks.keys) || !jwks.keys.some((key) => key?.kty === "RSA")) {
throw new Error("ROOTLINE_JWT_JWKS_B64 must decode to a JWKS with an RSA public key.");
}
NODE
node -e "const p=require('./apps/api/package.json'); if(p.version !== '2.0.0') throw new Error('apps/api/package.json must be version 2.0.0')"

image:
needs: preflight
runs-on: ubuntu-24.04
environment: api-production
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.immutable.outputs.image }}
build_id: ${{ steps.identity.outputs.build_id }}
stable_image: ${{ steps.image.outputs.stable_image }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:api-image
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: identity
run: echo "build_id=rootline-2.0.0-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
- id: image
run: |
repository="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/rootline-api"
echo "image=${repository}:candidate-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
echo "stable_image=${repository}:2.0.0" >> "$GITHUB_OUTPUT"
- name: Refuse to overwrite stable image tag
env:
IMAGE: ${{ steps.image.outputs.stable_image }}
run: |
set +e
inspection=$(docker buildx imagetools inspect "$IMAGE" 2>&1)
inspection_status=$?
set -e
if [ "$inspection_status" -eq 0 ]; then
echo "::error::Stable API image ${IMAGE} already exists. Published 2.0.0 bytes are immutable; release a new version."
exit 1
fi
case "$inspection" in
*"not found"*|*"manifest unknown"*) ;;
*) echo "::error::Could not prove stable API tag ${IMAGE} is unused; refusing to push."; printf '%s\n' "$inspection"; exit 1 ;;
esac
- id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
file: apps/api/Dockerfile
push: true
tags: ${{ steps.image.outputs.image }}
build-args: ROOTLINE_BUILD_ID=${{ steps.identity.outputs.build_id }}
- id: immutable
name: Resolve immutable image reference
env:
IMAGE: ${{ steps.image.outputs.image }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
repository=${IMAGE%:*}
echo "image=${repository}@${DIGEST}" >> "$GITHUB_OUTPUT"

migrate:
needs: [preflight, image]
runs-on: ubuntu-24.04
environment: api-production
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Apply checked-in production migrations
env:
DATABASE_URL: ${{ secrets.ROOTLINE_API_DATABASE_URL }}
run: pnpm --filter @rootline/api exec prisma migrate deploy

deploy:
needs: [preflight, image, migrate]
runs-on: ubuntu-24.04
environment: api-production
steps:
- name: Request deployment of the immutable image
env:
IMAGE: ${{ needs.image.outputs.image }}
ROOTLINE_BUILD_ID: ${{ needs.image.outputs.build_id }}
DEPLOY_URL: ${{ secrets.ROOTLINE_API_DEPLOY_WEBHOOK_URL }}
DEPLOY_TOKEN: ${{ secrets.ROOTLINE_API_DEPLOY_TOKEN }}
run: |
curl --fail-with-body --silent --show-error --retry 3 -X POST \
-H "Authorization: Bearer ${DEPLOY_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"image\":\"${IMAGE}\",\"buildId\":\"${ROOTLINE_BUILD_ID}\"}" \
"${DEPLOY_URL}"

health:
needs: [preflight, image, deploy]
runs-on: ubuntu-24.04
environment: api-production
steps:
- name: Require deployed API health
env:
ROOTLINE_API_BASE_URL: ${{ secrets.ROOTLINE_API_BASE_URL }}
EXPECTED_BUILD_ID: ${{ needs.image.outputs.build_id }}
run: |
set -eu
for attempt in $(seq 1 30); do
echo "Health check attempt ${attempt}/30"
body=$(curl --fail --silent --show-error --max-time 10 "${ROOTLINE_API_BASE_URL%/}/healthz" || true)
printf '%s' "$body" | jq -e '.status == "ok" and .buildId == env.EXPECTED_BUILD_ID' >/dev/null && exit 0
sleep 10
done
echo "::error::Stable API release blocked: /healthz did not report the requested build identity ${EXPECTED_BUILD_ID}."
exit 1

promote:
needs: [image, health]
runs-on: ubuntu-24.04
environment: api-production
permissions:
contents: read
packages: write
steps:
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Recheck stable tag before promotion
env:
STABLE_IMAGE: ${{ needs.image.outputs.stable_image }}
run: |
set +e
inspection=$(docker buildx imagetools inspect "$STABLE_IMAGE" 2>&1)
inspection_status=$?
set -e
if [ "$inspection_status" -eq 0 ]; then
echo "::error::Stable API image ${STABLE_IMAGE} already exists. Refusing to overwrite it."
exit 1
fi
case "$inspection" in
*"not found"*|*"manifest unknown"*) ;;
*) echo "::error::Could not prove stable API tag ${STABLE_IMAGE} is unused; refusing promotion."; printf '%s\n' "$inspection"; exit 1 ;;
esac
- name: Promote the verified digest to the stable tag
env:
SOURCE_IMAGE: ${{ needs.image.outputs.image }}
STABLE_IMAGE: ${{ needs.image.outputs.stable_image }}
run: docker buildx imagetools create --tag "$STABLE_IMAGE" "$SOURCE_IMAGE"
Loading
Loading