Skip to content
Closed
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
154 changes: 154 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: CI

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
shared:
name: Shared checks (${{ matrix.name }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- name: macOS
runner: macos-15
- name: Ubuntu
runner: ubuntu-24.04
- name: Windows
runner: windows-2025

steps:
- name: Check out source and test corpus
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
lfs: true
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"

- name: Install locked Python environment tool
run: python -m pip install uv==0.11.7

- name: Test release artifact contract
run: python -m unittest discover -s scripts/tests -v

- name: Check release tooling formatting
run: >-
uv run --project backend --frozen --only-group ci ruff format --check
scripts/release_artifact.py scripts/tests/test_release_artifact.py

- name: Lint release tooling
run: >-
uv run --project backend --frozen --only-group ci ruff check
scripts/release_artifact.py scripts/tests/test_release_artifact.py

- name: Check portable Python formatting
working-directory: backend
run: uv run --frozen --only-group ci ruff format --check .

- name: Lint portable Python
working-directory: backend
run: uv run --frozen --only-group ci ruff check .

- name: Test portable Python services
working-directory: backend
run: >-
uv run --frozen --only-group ci python -m pytest
tests/test_loras.py
tests/test_worker.py
tests/test_sidecar.py
tests/test_controller.py
tests/test_frozen.py

# Most DeckEngine behavior is model-independent. The excluded tests cross
# the Magenta/MLX import boundary and remain in the local full suite and
# the signed macOS release runtime check.
- name: Test portable Python deck behavior
working-directory: backend
run: >-
uv run --frozen --only-group ci python -m pytest tests/test_engine.py
-k "not constructor_uses_reference_sampling_defaults
and not embed_sample
and not sample_key_never_hits_the_text_embedder
and not sample_cache
and not failed_embed_does_not_evict"

# SA3 uses a copied Python interpreter and fake CLI; no runtime, weights,
# accelerator, shell, or network is involved. The selected model-manager
# nodes exercise only SA3 readiness. Its Magenta discovery/download nodes
# remain behind the backend-specific runtime gate.
- name: Test model-free Python runtime contracts
working-directory: backend
run: >-
uv run --frozen --only-group ci python -m pytest
tests/test_sa3.py
tests/test_models.py::test_readiness_classifies_a_checkout
tests/test_models.py::test_readiness_missing_when_no_checkout

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install frontend dependencies
working-directory: frontend
run: npm ci

- name: Lint frontend
working-directory: frontend
run: npm run lint

- name: Type-check frontend
working-directory: frontend
run: npx tsc -b

- name: Test frontend
working-directory: frontend
run: npm test

- name: Build frontend assets for native shell
working-directory: frontend
run: npm run build

- name: Install Ubuntu native build dependencies
if: runner.os == 'Linux'
run: >-
sudo apt-get update && sudo apt-get install --yes
build-essential
libasound2-dev
libayatana-appindicator3-dev
libgtk-3-dev
libssl-dev
libudev-dev
libwebkit2gtk-4.1-dev
libxdo-dev
librsvg2-dev

- name: Set up Rust
run: rustup toolchain install stable --profile minimal --no-self-update

- name: Test Rust workspace
run: cargo test --locked --workspace --manifest-path src-tauri/Cargo.toml

- name: Lint Rust workspace
run: >-
cargo clippy --locked --workspace --all-targets
--manifest-path src-tauri/Cargo.toml -- -D warnings
169 changes: 149 additions & 20 deletions .github/workflows/macos-release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: macOS release
name: Release

run-name: macOS release from ${{ github.ref_name }} by @${{ github.actor }}
run-name: Release ${{ github.ref_name }} by @${{ github.actor }}

# A protected release tag starts validation. Signing credentials remain behind
# the macos-release Environment's separate human approval gate.
Expand All @@ -24,6 +24,8 @@ jobs:
startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
revision: ${{ steps.release_identity.outputs.revision }}

steps:
- name: Check out the tagged commit
Expand All @@ -33,6 +35,7 @@ jobs:
persist-credentials: false

- name: Verify release tag and ancestry
id: release_identity
shell: bash
run: |
set -euo pipefail
Expand All @@ -51,8 +54,10 @@ jobs:
}

echo "Validated $GITHUB_REF_NAME at $TAG_COMMIT"
echo "revision=$TAG_COMMIT" >> "$GITHUB_OUTPUT"

release:
produce_macos:
name: Produce macOS arm64 artifact
needs: validate
if: >-
needs.validate.result == 'success' &&
Expand Down Expand Up @@ -87,6 +92,11 @@ jobs:
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"

- name: Check runner architecture
run: |
set -euo pipefail
Expand Down Expand Up @@ -198,11 +208,32 @@ jobs:
set -euo pipefail
just tauri-release

- name: Upload verified DMG
- name: Package verified release artifact
env:
LSDJ_RELEASE_REVISION: ${{ needs.validate.outputs.revision }}
run: |
set -euo pipefail
shopt -s nullglob
DMG_FILES=(src-tauri/target/release/bundle/dmg/*.dmg)
[[ "${#DMG_FILES[@]}" -eq 1 ]] || {
echo "Expected exactly one verified DMG, found ${#DMG_FILES[@]}" >&2
exit 1
}

python scripts/release_artifact.py create \
--producer macos-arm64 \
--release-tag "$GITHUB_REF_NAME" \
--revision "$LSDJ_RELEASE_REVISION" \
--asset "${DMG_FILES[0]}" \
--output-dir release-artifacts/macos-arm64

# The publisher receives installers only through immutable per-run
# Actions artifacts, together with the tag/revision metadata and digest.
- name: Upload verified producer bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: LSDJ-macOS-arm64
path: src-tauri/target/release/bundle/dmg/*.dmg
name: release-macos-arm64
path: release-artifacts/macos-arm64
if-no-files-found: error
retention-days: 14

Expand All @@ -218,9 +249,13 @@ jobs:
"${LSDJ_API_KEY_PATH:-}"

publish:
needs: release
name: Verify and publish complete release
needs:
- validate
- produce_macos
if: >-
needs.release.result == 'success' &&
needs.validate.result == 'success' &&
needs.produce_macos.result == 'success' &&
github.repository == 'protocol-works/lsdj' &&
startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
Expand All @@ -229,26 +264,120 @@ jobs:
contents: write

steps:
- name: Download verified DMG
- name: Check out the approved release tag
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"

- name: Download macOS producer bundle
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: LSDJ-macOS-arm64
path: dist
name: release-macos-arm64
path: release-input/macos-arm64

- name: Publish GitHub Release
- name: Verify complete required producer set
env:
LSDJ_RELEASE_REVISION: ${{ needs.validate.outputs.revision }}
shell: bash
run: |
set -euo pipefail
python scripts/release_artifact.py verify \
--input-root release-input \
--required-producer macos-arm64 \
--release-tag "$GITHUB_REF_NAME" \
--revision "$LSDJ_RELEASE_REVISION" \
--output-dir verified-release

- name: Publish verified GitHub Release
env:
GH_TOKEN: ${{ github.token }}
LSDJ_RELEASE_REVISION: ${{ needs.validate.outputs.revision }}
shell: bash
run: |
set -euo pipefail
mapfile -t DMG_FILES < <(find dist -maxdepth 1 -type f -name '*.dmg' -print)
[[ "${#DMG_FILES[@]}" -eq 1 ]] || {
echo "Expected exactly one verified DMG, found ${#DMG_FILES[@]}" >&2
if gh release view "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "A GitHub Release already exists for $GITHUB_REF_NAME" >&2
exit 1
fi

mapfile -d '' -t RELEASE_FILES < <(
find verified-release -maxdepth 1 -type f -print0 | sort -z
)
[[ "${#RELEASE_FILES[@]}" -gt 0 ]] || {
echo "The verified release contains no files" >&2
exit 1
}

DRAFT_RELEASE_ID=""
PUBLISHED=0
cleanup_draft() {
result=$?
if [[ -n "$DRAFT_RELEASE_ID" && "$PUBLISHED" -ne 1 ]]; then
CLEANUP_JSON="$RUNNER_TEMP/lsdj-cleanup-release.json"
if gh api \
"repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" \
> "$CLEANUP_JSON" 2>/dev/null && \
python scripts/release_artifact.py verify-draft-identity \
--release-json "$CLEANUP_JSON" \
--release-tag "$GITHUB_REF_NAME" \
--revision "$LSDJ_RELEASE_REVISION" \
--expected-release-id "$DRAFT_RELEASE_ID" >/dev/null; then
echo "Publication failed; removing this run's unpublished draft" >&2
gh api --method DELETE \
"repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" || \
echo "Could not remove this run's unpublished draft" >&2
else
echo "Could not prove this run still owns the failed draft; leaving it in place" >&2
fi
fi
exit "$result"
}
trap cleanup_draft EXIT

git fetch --no-tags origin \
"+$GITHUB_REF:refs/remotes/origin/lsdj-release-tag"
REMOTE_TAG_COMMIT="$(git rev-list -n 1 refs/remotes/origin/lsdj-release-tag)"
[[ "$REMOTE_TAG_COMMIT" == "$LSDJ_RELEASE_REVISION" ]] || {
echo "Release tag no longer resolves to the approved source revision" >&2
exit 1
}

gh release create "$GITHUB_REF_NAME" "${DMG_FILES[0]}" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--generate-notes \
--title "LSDJ $GITHUB_REF_NAME"
CREATE_RESPONSE="$RUNNER_TEMP/lsdj-created-release.json"
SOURCE_MARKER="Source revision: $LSDJ_RELEASE_REVISION"
gh api --method POST "repos/$GITHUB_REPOSITORY/releases" \
--field "tag_name=$GITHUB_REF_NAME" \
--field "target_commitish=$LSDJ_RELEASE_REVISION" \
--field "name=LSDJ $GITHUB_REF_NAME" \
--field "body=$SOURCE_MARKER" \
--field draft=true \
--field generate_release_notes=true \
> "$CREATE_RESPONSE"
DRAFT_RELEASE_ID="$(python scripts/release_artifact.py \
verify-draft-identity \
--release-json "$CREATE_RESPONSE" \
--release-tag "$GITHUB_REF_NAME" \
--revision "$LSDJ_RELEASE_REVISION")"

gh release upload "$GITHUB_REF_NAME" "${RELEASE_FILES[@]}" \
--repo "$GITHUB_REPOSITORY"

RELEASE_JSON="$RUNNER_TEMP/lsdj-draft-release.json"
gh api "repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" \
> "$RELEASE_JSON"
python scripts/release_artifact.py verify-github-release \
--release-json "$RELEASE_JSON" \
--verified-dir verified-release \
--release-tag "$GITHUB_REF_NAME" \
--revision "$LSDJ_RELEASE_REVISION" \
--expected-release-id "$DRAFT_RELEASE_ID"

gh api --method PATCH \
"repos/$GITHUB_REPOSITORY/releases/$DRAFT_RELEASE_ID" \
--field draft=false >/dev/null
PUBLISHED=1
Loading
Loading