diff --git a/.github/workflows/build-linux-bundle.yml b/.github/workflows/build-linux-bundle.yml
deleted file mode 100644
index e61300e3..00000000
--- a/.github/workflows/build-linux-bundle.yml
+++ /dev/null
@@ -1,78 +0,0 @@
-name: "Build Linux Bundle"
-
-on:
- workflow_dispatch:
- inputs:
- push_to_cache:
- description: "Push to Nix Cache"
- required: false
- type: boolean
- default: false
- push:
- branches:
- - master
-
-jobs:
- build:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: ❄ Install Nix
- uses: DeterminateSystems/nix-installer-action@main
-
- - name: 🗑 Free disk space
- run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
-
- - name: 📦 Build Nix package
- run: nix build --no-link --print-out-paths
-
- - name: 📤 Push to Nix cache
- if: github.ref == 'refs/heads/master' || inputs.push_to_cache == true
- env:
- NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN }}
- run: |
- nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
- nix build --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite
-
- - name: 🏗 Build Linux bundle
- run: nix build .#graphite-bundle.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz
-
- - name: 📦 Upload Linux bundle
- uses: actions/upload-artifact@v4
- with:
- name: graphite-linux-bundle
- path: graphite-linux-bundle.tar.xz
- compression-level: 0
-
- - name: 🔧 Install Flatpak tooling
- run: |
- sudo apt-get update
- sudo apt-get install -y flatpak flatpak-builder
- flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-
- - name: 🏗 Build Flatpak
- run: |
- nix build .#graphite-flatpak-manifest
-
- rm -rf .flatpak
- mkdir -p .flatpak
-
- cp ./result .flatpak/manifest.json
-
- cd .flatpak
- mkdir -p repo
-
- flatpak-builder --user --force-clean --install-deps-from=flathub --repo=repo build ./manifest.json
-
- flatpak build-bundle repo Graphite.flatpak art.graphite.Graphite --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
-
- - name: 📦 Upload Flatpak package
- uses: actions/upload-artifact@v4
- with:
- name: graphite-flatpak
- path: .flatpak/Graphite.flatpak
- compression-level: 0
diff --git a/.github/workflows/build-mac-bundle.yml b/.github/workflows/build-mac-bundle.yml
deleted file mode 100644
index 681542e0..00000000
--- a/.github/workflows/build-mac-bundle.yml
+++ /dev/null
@@ -1,158 +0,0 @@
-name: "Build Mac Bundle"
-
-on:
- workflow_dispatch: {}
- push:
- branches:
- - master
-
-jobs:
- build:
- runs-on: macos-latest
-
- env:
- WASM_BINDGEN_CLI_VERSION: "0.2.100"
-
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: 🦀 Install Rust
- uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- toolchain: stable
- override: true
- rustflags: ""
- target: wasm32-unknown-unknown
-
- - name: 💾 Set up Cargo cache
- uses: actions/cache@v4
- with:
- path: |
- ~/.cargo/registry
- ~/.cargo/git
- target
- key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
-
- - name: 🟢 Install Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: .nvmrc
- cache: npm
- cache-dependency-path: |
- package-lock.json
- frontend/package-lock.json
-
- - name: 🚧 Install native dependencies
- env:
- GITHUB_TOKEN: ${{ github.token }}
- BINSTALL_DISABLE_TELEMETRY: "true"
- run: |
- brew update
- brew install \
- pkg-config \
- openssl@3 \
- binaryen \
- llvm \
- cargo-binstall
-
- echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
- echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
- echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH
-
- cargo binstall --no-confirm --force wasm-pack
- cargo binstall --no-confirm --force cargo-about
- cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}"
-
- - name: 🏗 Build Mac bundle
- env:
- CARGO_TERM_COLOR: always
- run: cargo run build desktop
-
- - name: 📁 Stage artifacts
- shell: bash
- run: |
- rm -rf target/artifacts
- mkdir -p target/artifacts
- cp -R target/release/Graphite.app target/artifacts/Graphite.app
-
- - name: 📦 Upload Mac bundle
- if: github.ref != 'refs/heads/master'
- uses: actions/upload-artifact@v4
- with:
- name: graphite-mac-bundle
- path: target/artifacts
-
- - name: 🔏 Sign and notarize (preparation)
- if: github.ref == 'refs/heads/master'
- env:
- APPLE_CERT_BASE64: ${{ secrets.APPLE_CERT_BASE64 }}
- APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
- run: |
- mkdir -p .sign
- echo "$APPLE_CERT_BASE64" | base64 --decode > .sign/certificate.p12
-
- security create-keychain -p "" .sign/main.keychain
- security default-keychain -s .sign/main.keychain
- security unlock-keychain -p "" .sign/main.keychain
- security set-keychain-settings -t 3600 -u .sign/main.keychain
-
- security import .sign/certificate.p12 -k .sign/main.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign
- security set-key-partition-list -S apple-tool:,apple: -s -k "" .sign/main.keychain
-
- cat > .sign/entitlements.plist <<'EOF'
-
-
-
-
- com.apple.security.cs.allow-jit
-
- com.apple.security.cs.allow-unsigned-executable-memory
-
- com.apple.security.cs.disable-executable-page-protection
-
- com.apple.security.cs.disable-library-validation
-
-
-
- EOF
-
- - name: 🔏 Sign and notarize
- if: github.ref == 'refs/heads/master'
- env:
- APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
- APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
- APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }}
- run: |
- CERTIFICATE="$APPLE_CERT_NAME"
- ENTITLEMENTS=".sign/entitlements.plist"
- APP_PATH="target/artifacts/Graphite.app"
- ZIP_PATH=".sign/Graphite.zip"
-
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib"
- codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep
-
- codesign --verify --deep --strict --verbose=4 "$APP_PATH"
-
- ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
- xcrun notarytool submit "$ZIP_PATH" --wait --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD"
- rm "$ZIP_PATH"
-
- xcrun stapler staple -v "$APP_PATH"
-
- spctl -a -vv "$APP_PATH"
-
- - name: 📦 Upload signed Mac bundle
- if: github.ref == 'refs/heads/master'
- uses: actions/upload-artifact@v4
- with:
- name: graphite-mac-bundle-signed
- path: target/artifacts
diff --git a/.github/workflows/build-nix-package.yml b/.github/workflows/build-nix-package.yml
deleted file mode 100644
index 2f2295a9..00000000
--- a/.github/workflows/build-nix-package.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: "Build Nix Package"
-
-on:
- workflow_dispatch: {}
-
-jobs:
- build:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: ❄ Install Nix
- uses: DeterminateSystems/nix-installer-action@main
-
- - name: 💾 Set up Nix cache
- uses: DeterminateSystems/magic-nix-cache-action@main
-
- - name: 📦 Build Nix package dev
- run: nix build .#graphite-dev --print-build-logs
diff --git a/.github/workflows/build-web-bundle.yml b/.github/workflows/build-web-bundle.yml
deleted file mode 100644
index bde9beff..00000000
--- a/.github/workflows/build-web-bundle.yml
+++ /dev/null
@@ -1,155 +0,0 @@
-name: "Build Web Bundle"
-
-on:
- workflow_dispatch: {}
- push:
- branches:
- - master
-
-env:
- CARGO_TERM_COLOR: always
- INDEX_HTML_HEAD_REPLACEMENT:
-
-jobs:
- build:
- runs-on: [self-hosted, target/wasm]
- permissions:
- contents: write
- deployments: write
- actions: write
- env:
- RUSTC_WRAPPER: /usr/bin/sccache
- CARGO_INCREMENTAL: 0
- SCCACHE_DIR: /var/lib/github-actions/.cache
-
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: 🗑 Clear wasm-bindgen cache
- run: rm -r ~/.cache/.wasm-pack || true
-
- - name: 🟢 Install Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: .nvmrc
-
- - name: 🚧 Install build dependencies
- run: |
- cd frontend
- npm run setup
-
- - name: 🦀 Install Rust
- uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- toolchain: stable
- override: true
- rustflags: ""
- target: wasm32-unknown-unknown
-
- - name: ✂ Replace template in
of index.html
- run: |
- sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
-
- - name: 🌐 Build Graphite web code
- env:
- NODE_ENV: production
- run: mold -run cargo run build web
-
- - name: 📤 Publish to Cloudflare Pages
- id: cloudflare
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- run: |
- MAX_ATTEMPTS=5
- DELAY=30
- for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
- echo "Attempt $ATTEMPT of $MAX_ATTEMPTS..."
- if OUTPUT=$(npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true 2>&1); then
- URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.pages\.dev' | tail -1)
- echo "url=$URL" >> "$GITHUB_OUTPUT"
- echo "Published successfully: $URL"
- exit 0
- fi
- echo "Attempt $ATTEMPT failed:"
- echo "$OUTPUT"
- if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
- echo "Retrying in ${DELAY}s..."
- sleep $DELAY
- DELAY=$((DELAY * 3))
- fi
- done
- echo "All $MAX_ATTEMPTS Cloudflare Pages publish attempts failed."
- exit 1
-
- - name: 💬 Comment build link URL to commit hash page on GitHub
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- CF_URL: ${{ steps.cloudflare.outputs.url }}
- run: |
- if [ -z "$CF_URL" ]; then
- echo "No Cloudflare URL available, skipping comment."
- exit 1
- fi
- gh api \
- -X POST \
- -H "Accept: application/vnd.github+json" \
- /repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \
- -f body="| 📦 **Build Complete for** $(git rev-parse HEAD) |
- |-|
- | $CF_URL |"
-
- - name: ✂ Strip analytics script from built output for clean artifact
- run: |
- sed -i "s|$INDEX_HTML_HEAD_REPLACEMENT||" frontend/dist/index.html
-
- - name: 📦 Upload web bundle artifact
- uses: actions/upload-artifact@v4
- with:
- name: graphite-web-bundle
- path: frontend/dist
-
- - name: 📃 Generate code documentation info for website
- run: |
- cd tools/editor-message-tree
- cargo run
- cd ../..
- mkdir -p artifacts-generated
- mv website/generated/hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt
-
- - name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
- id: cache-website-code-docs
- uses: actions/cache/restore@v4
- with:
- path: artifacts
- key: website-code-docs
-
- - name: 🔍 Check if auto-generated code docs artifacts changed
- id: website-code-docs-changed
- run: |
- if ! diff --brief --recursive artifacts-generated artifacts; then
- echo "Auto-generated code docs artifacts have changed."
- rm -rf artifacts
- mv artifacts-generated artifacts
- echo "changed=true" >> $GITHUB_OUTPUT
- else
- echo "Auto-generated code docs artifacts have not changed."
- rm -rf artifacts
- rm -rf artifacts-generated
- fi
-
- - name: 💾 Save cache of auto-generated code docs artifacts
- if: steps.website-code-docs-changed.outputs.changed == 'true'
- uses: actions/cache/save@v4
- with:
- path: artifacts
- key: ${{ steps.cache-website-code-docs.outputs.cache-primary-key }}
-
- - name: ♻️ Trigger website rebuild if the auto-generated code docs artifacts have changed
- if: steps.website-code-docs-changed.outputs.changed == 'true'
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- rm -rf artifacts
- gh workflow run website.yml --ref master
diff --git a/.github/workflows/build-windows-bundle.yml b/.github/workflows/build-windows-bundle.yml
deleted file mode 100644
index 8402b094..00000000
--- a/.github/workflows/build-windows-bundle.yml
+++ /dev/null
@@ -1,173 +0,0 @@
-name: "Build Windows Bundle"
-
-on:
- workflow_dispatch: {}
- push:
- branches:
- - master
-
-jobs:
- build:
- runs-on: windows-latest
- permissions:
- contents: read
- id-token: write
-
- env:
- WASM_BINDGEN_CLI_VERSION: "0.2.100"
-
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: 🦀 Install Rust
- uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- toolchain: stable
- override: true
- rustflags: ""
- target: wasm32-unknown-unknown
-
- - name: 💾 Set up Cargo cache
- uses: actions/cache@v4
- with:
- path: |
- ${{ env.USERPROFILE }}\.cargo\registry
- ${{ env.USERPROFILE }}\.cargo\git
- target
- key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
-
- - name: 🟢 Install Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: .nvmrc
- cache: npm
- cache-dependency-path: |
- package-lock.json
- frontend/package-lock.json
-
- - name: 📦 Install Cargo-binstall
- uses: cargo-bins/cargo-binstall@main
-
- - name: 🚧 Install native dependencies
- shell: pwsh
- env:
- GITHUB_TOKEN: ${{ github.token }}
- BINSTALL_DISABLE_TELEMETRY: "true"
- run: |
- winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements
- winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements
- winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements
- winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements
- winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements
-
- "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append
- "PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append
-
- cargo binstall --no-confirm --force wasm-pack
- cargo binstall --no-confirm --force cargo-about
- cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION"
-
- - name: 🏗 Build Windows bundle
- shell: bash # `cargo-about` refuses to run in powershell
- env:
- CARGO_TERM_COLOR: always
- run: cargo run build desktop
-
- - name: 📁 Stage artifacts
- shell: bash
- run: |
- rm -rf target/artifacts
- mkdir -p target/artifacts
- cp -R target/release/Graphite target/artifacts/Graphite
-
- - name: 📦 Upload Windows bundle
- if: github.ref != 'refs/heads/master'
- uses: actions/upload-artifact@v4
- with:
- name: graphite-windows-bundle
- path: target/artifacts
-
- - name: 🔑 Azure login
- if: github.ref == 'refs/heads/master'
- uses: azure/login@v1
- with:
- client-id: ${{ secrets.AZURE_CLIENT_ID }}
- tenant-id: ${{ secrets.AZURE_TENANT_ID }}
- subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- enable-AzPSSession: true
-
- - name: 🔏 Sign
- if: github.ref == 'refs/heads/master'
- uses: azure/artifact-signing-action@v1
- with:
- endpoint: https://eus.codesigning.azure.net/
- signing-account-name: Graphite
- certificate-profile-name: Graphite
- files: |
- ${{ github.workspace }}\target\artifacts\Graphite\Graphite.exe
- ${{ github.workspace }}\target\artifacts\Graphite\libcef.dll
- ${{ github.workspace }}\target\artifacts\Graphite\chrome_elf.dll
- ${{ github.workspace }}\target\artifacts\Graphite\vulkan-1.dll
- ${{ github.workspace }}\target\artifacts\Graphite\dxcompiler.dll
- ${{ github.workspace }}\target\artifacts\Graphite\libEGL.dll
- ${{ github.workspace }}\target\artifacts\Graphite\libGLESv2.dll
- ${{ github.workspace }}\target\artifacts\Graphite\vk_swiftshader.dll
- file-digest: SHA256
- timestamp-rfc3161: http://timestamp.acs.microsoft.com
- timestamp-digest: SHA256
- correlation-id: ${{ github.sha }}
-
- - name: ✅ Verify signatures
- if: github.ref == 'refs/heads/master'
- shell: pwsh
- run: |
- $ErrorActionPreference = "Stop"
-
- $TargetDir = "target\artifacts\Graphite"
-
- if (-not (Test-Path $TargetDir)) {
- throw "TargetDir not found: $TargetDir"
- }
-
- $UnsignedOrBad = @()
-
- Get-ChildItem -Path $TargetDir -Recurse -File -Include *.exe,*.dll | ForEach-Object {
- $sig = Get-AuthenticodeSignature -FilePath $_.FullName
-
- if ($sig.Status -ne 'Valid') {
- $UnsignedOrBad += "$($_.FullName) (Status=$($sig.Status))"
- }
- }
-
- if ($UnsignedOrBad.Count -gt 0) {
- Write-Host "Unsigned or invalid binaries detected:"
- $UnsignedOrBad | ForEach-Object {
- Write-Host "::error::$_"
- }
-
- if ($env:GITHUB_STEP_SUMMARY) {
- "### ❌ Unsigned or invalid binaries detected" |
- Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
- "" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
- $UnsignedOrBad | ForEach-Object {
- "* `$_" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
- }
- }
-
- exit 1
- }
-
- Write-Host "All binaries are signed and valid."
-
- if ($env:GITHUB_STEP_SUMMARY) {
- "### ✅ All binaries are signed and valid" |
- Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
- }
-
- - name: 📦 Upload signed Windows bundle
- if: github.ref == 'refs/heads/master'
- uses: actions/upload-artifact@v4
- with:
- name: graphite-windows-bundle-signed
- path: target/artifacts
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 00000000..11af2efd
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,675 @@
+name: "Build"
+
+on:
+ push:
+ branches:
+ - master
+ workflow_dispatch:
+ inputs:
+ web:
+ description: "Web"
+ type: boolean
+ windows:
+ description: "Windows"
+ type: boolean
+ mac:
+ description: "Mac"
+ type: boolean
+ linux:
+ description: "Linux"
+ type: boolean
+ push_to_nix_cache:
+ description: "Linux: push to Nix cache"
+ type: boolean
+ debug:
+ description: "Debug build"
+ type: boolean
+ workflow_call:
+ inputs:
+ web:
+ type: boolean
+ windows:
+ type: boolean
+ mac:
+ type: boolean
+ linux:
+ type: boolean
+ push_to_nix_cache:
+ type: boolean
+ debug:
+ type: boolean
+ checkout_repo:
+ type: string
+ checkout_ref:
+ type: string
+ pr_number:
+ type: string
+
+jobs:
+ web:
+ if: github.event_name == 'push' || inputs.web
+ runs-on: [self-hosted, target/wasm]
+ permissions:
+ contents: write
+ deployments: write
+ pull-requests: write
+ actions: write
+ env:
+ CARGO_TERM_COLOR: always
+ RUSTC_WRAPPER: /usr/bin/sccache
+ CARGO_INCREMENTAL: 0
+ SCCACHE_DIR: /var/lib/github-actions/.cache
+ INDEX_HTML_HEAD_REPLACEMENT:
+
+ steps:
+ - name: 📥 Clone repository
+ uses: actions/checkout@v6
+ with:
+ repository: ${{ inputs.checkout_repo || github.repository }}
+ ref: ${{ inputs.checkout_ref || '' }}
+
+ - name: 🗑 Clear wasm-bindgen cache
+ run: rm -r ~/.cache/.wasm-pack || true
+
+ - name: 🟢 Install Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: .nvmrc
+
+ - name: 🚧 Install build dependencies
+ run: |
+ cd frontend
+ npm run setup
+
+ - name: 🦀 Install Rust
+ uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ cache: false
+ rustflags: ""
+ target: wasm32-unknown-unknown
+
+ - name: ✂ Replace template in of index.html
+ if: github.event_name == 'push'
+ run: |
+ sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
+
+ - name: 🌐 Build Graphite web code
+ env:
+ NODE_ENV: production
+ run: mold -run cargo run build web${{ inputs.debug && ' debug' || '' }}
+
+ - name: 📤 Publish to Cloudflare Pages
+ id: cloudflare
+ env:
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ run: |
+ MAX_ATTEMPTS=5
+ DELAY=30
+ for ATTEMPT in $(seq 1 $MAX_ATTEMPTS); do
+ echo "Attempt $ATTEMPT of $MAX_ATTEMPTS..."
+ if OUTPUT=$(npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true 2>&1); then
+ URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.pages\.dev' | head -1)
+ echo "url=$URL" >> "$GITHUB_OUTPUT"
+ echo "Published successfully: $URL"
+ exit 0
+ fi
+ echo "Attempt $ATTEMPT failed:"
+ echo "$OUTPUT"
+ if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
+ echo "Retrying in ${DELAY}s..."
+ sleep $DELAY
+ DELAY=$((DELAY * 3))
+ fi
+ done
+ echo "All $MAX_ATTEMPTS Cloudflare Pages publish attempts failed."
+ exit 1
+
+ - name: 💬 Comment with the build link
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CF_URL: ${{ steps.cloudflare.outputs.url }}
+ run: |
+ if [ -z "$CF_URL" ]; then
+ echo "No Cloudflare URL available, skipping comment."
+ exit 0
+ fi
+
+ COMMENT_BODY="| 📦 **Web Build Complete for** $(git rev-parse HEAD) |
+ |-|
+ | $CF_URL |"
+
+ if [ "${{ github.event_name }}" = "push" ]; then
+ # Comment on the commit hash page
+ gh api \
+ -X POST \
+ -H "Accept: application/vnd.github+json" \
+ /repos/${{ github.repository }}/commits/$(git rev-parse HEAD)/comments \
+ -f body="$COMMENT_BODY"
+ else
+ # Comment on the PR (use provided PR number from !build, or look it up by branch name)
+ PR_NUMBER="${{ inputs.pr_number }}"
+ if [ -z "$PR_NUMBER" ]; then
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
+ fi
+
+ if [ -n "$PR_NUMBER" ]; then
+ gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "$COMMENT_BODY"
+ else
+ echo "No open PR found, skipping comment."
+ fi
+ fi
+
+ - name: ✂ Strip analytics script from built output for clean artifact
+ if: github.event_name == 'push'
+ run: |
+ sed -i "s|$INDEX_HTML_HEAD_REPLACEMENT||" frontend/dist/index.html
+
+ - name: 📦 Upload web bundle artifact
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-web-bundle
+ path: frontend/dist
+
+ - name: 📃 Generate code documentation info for website
+ if: github.event_name == 'push'
+ run: |
+ cd tools/editor-message-tree
+ cargo run
+ cd ../..
+ mkdir -p artifacts-generated
+ mv website/generated/hierarchical_message_system_tree.txt artifacts-generated/hierarchical_message_system_tree.txt
+
+ - name: 💿 Obtain cache of auto-generated code docs artifacts, to check if they've changed
+ if: github.event_name == 'push'
+ id: cache-website-code-docs
+ uses: actions/cache/restore@v5
+ with:
+ path: artifacts
+ key: website-code-docs
+
+ - name: 🔍 Check if auto-generated code docs artifacts changed
+ if: github.event_name == 'push'
+ id: website-code-docs-changed
+ run: |
+ if ! diff --brief --recursive artifacts-generated artifacts; then
+ echo "Auto-generated code docs artifacts have changed."
+ rm -rf artifacts
+ mv artifacts-generated artifacts
+ echo "changed=true" >> $GITHUB_OUTPUT
+ else
+ echo "Auto-generated code docs artifacts have not changed."
+ rm -rf artifacts
+ rm -rf artifacts-generated
+ fi
+
+ - name: 💾 Save cache of auto-generated code docs artifacts
+ if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
+ uses: actions/cache/save@v5
+ with:
+ path: artifacts
+ key: ${{ steps.cache-website-code-docs.outputs.cache-primary-key }}
+
+ - name: ♻️ Trigger website rebuild if the auto-generated code docs artifacts have changed
+ if: github.event_name == 'push' && steps.website-code-docs-changed.outputs.changed == 'true'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ rm -rf artifacts
+ gh workflow run website.yml --ref master
+
+ windows:
+ if: github.event_name == 'push' || inputs.windows
+ runs-on: windows-latest
+ permissions:
+ contents: read
+ id-token: write
+ pull-requests: write
+
+ env:
+ WASM_BINDGEN_CLI_VERSION: "0.2.100"
+
+ steps:
+ - name: 📥 Clone repository
+ uses: actions/checkout@v6
+ with:
+ repository: ${{ inputs.checkout_repo || github.repository }}
+ ref: ${{ inputs.checkout_ref || '' }}
+
+ - name: 🦀 Install Rust
+ uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ cache: false
+ rustflags: ""
+ target: wasm32-unknown-unknown
+
+ - name: 💾 Set up Cargo cache
+ uses: actions/cache@v5
+ with:
+ path: |
+ ${{ env.USERPROFILE }}\.cargo\registry
+ ${{ env.USERPROFILE }}\.cargo\git
+ target
+ key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: 🟢 Install Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+ cache-dependency-path: |
+ package-lock.json
+ frontend/package-lock.json
+
+ - name: 📦 Install Cargo-binstall
+ uses: cargo-bins/cargo-binstall@main
+
+ - name: 🚧 Install native dependencies
+ shell: pwsh
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ BINSTALL_DISABLE_TELEMETRY: "true"
+ run: |
+ winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements
+ winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements
+ winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements
+ winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements
+ winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements
+
+ "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append
+ "PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append
+
+ cargo binstall --no-confirm --force wasm-pack
+ cargo binstall --no-confirm --force cargo-about
+ cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION"
+
+ - name: 🏗 Build Windows bundle
+ shell: bash # `cargo-about` refuses to run in powershell
+ env:
+ CARGO_TERM_COLOR: always
+ run: cargo run build desktop${{ inputs.debug && ' debug' || '' }}
+
+ - name: 📁 Stage artifacts
+ shell: bash
+ run: |
+ PROFILE=${{ inputs.debug && 'debug' || 'release' }}
+ rm -rf target/artifacts
+ mkdir -p target/artifacts
+ cp -R target/$PROFILE/Graphite target/artifacts/Graphite
+
+ - name: 📦 Upload Windows bundle
+ if: github.event_name != 'push'
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-windows-bundle
+ path: target/artifacts
+
+ - name: 💬 Comment artifact link on PR
+ if: github.event_name != 'push'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-windows-bundle") | .archive_download_url')
+ PR_NUMBER="${{ inputs.pr_number }}"
+ if [ -z "$PR_NUMBER" ]; then
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
+ fi
+ if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then
+ gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| 📦 **Windows Build Complete for** $(git rev-parse HEAD) |
+ |-|
+ | [Download artifact]($ARTIFACT_URL) |"
+ fi
+
+ - name: 🔑 Azure login
+ if: github.event_name == 'push'
+ uses: azure/login@v1
+ with:
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ enable-AzPSSession: true
+
+ - name: 🔏 Sign
+ if: github.event_name == 'push'
+ uses: azure/artifact-signing-action@v1
+ with:
+ endpoint: https://eus.codesigning.azure.net/
+ signing-account-name: Graphite
+ certificate-profile-name: Graphite
+ files: |
+ ${{ github.workspace }}\target\artifacts\Graphite\Graphite.exe
+ ${{ github.workspace }}\target\artifacts\Graphite\libcef.dll
+ ${{ github.workspace }}\target\artifacts\Graphite\chrome_elf.dll
+ ${{ github.workspace }}\target\artifacts\Graphite\vulkan-1.dll
+ ${{ github.workspace }}\target\artifacts\Graphite\dxcompiler.dll
+ ${{ github.workspace }}\target\artifacts\Graphite\libEGL.dll
+ ${{ github.workspace }}\target\artifacts\Graphite\libGLESv2.dll
+ ${{ github.workspace }}\target\artifacts\Graphite\vk_swiftshader.dll
+ file-digest: SHA256
+ timestamp-rfc3161: http://timestamp.acs.microsoft.com
+ timestamp-digest: SHA256
+ correlation-id: ${{ github.sha }}
+
+ - name: ✅ Verify signatures
+ if: github.event_name == 'push'
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = "Stop"
+
+ $TargetDir = "target\artifacts\Graphite"
+
+ if (-not (Test-Path $TargetDir)) {
+ throw "TargetDir not found: $TargetDir"
+ }
+
+ $UnsignedOrBad = @()
+
+ Get-ChildItem -Path $TargetDir -Recurse -File -Include *.exe,*.dll | ForEach-Object {
+ $sig = Get-AuthenticodeSignature -FilePath $_.FullName
+
+ if ($sig.Status -ne 'Valid') {
+ $UnsignedOrBad += "$($_.FullName) (Status=$($sig.Status))"
+ }
+ }
+
+ if ($UnsignedOrBad.Count -gt 0) {
+ Write-Host "Unsigned or invalid binaries detected:"
+ $UnsignedOrBad | ForEach-Object {
+ Write-Host "::error::$_"
+ }
+
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "### ❌ Unsigned or invalid binaries detected" |
+ Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+ "" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+ $UnsignedOrBad | ForEach-Object {
+ "* `$_" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+ }
+ }
+
+ exit 1
+ }
+
+ Write-Host "All binaries are signed and valid."
+
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "### ✅ All binaries are signed and valid" |
+ Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+ }
+
+ - name: 📦 Upload signed Windows bundle
+ if: github.event_name == 'push'
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-windows-bundle-signed
+ path: target/artifacts
+
+ mac:
+ if: github.event_name == 'push' || inputs.mac
+ runs-on: macos-latest
+ permissions:
+ contents: read
+ pull-requests: write
+
+ env:
+ WASM_BINDGEN_CLI_VERSION: "0.2.100"
+
+ steps:
+ - name: 📥 Clone repository
+ uses: actions/checkout@v6
+ with:
+ repository: ${{ inputs.checkout_repo || github.repository }}
+ ref: ${{ inputs.checkout_ref || '' }}
+
+ - name: 🦀 Install Rust
+ uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ cache: false
+ rustflags: ""
+ target: wasm32-unknown-unknown
+
+ - name: 💾 Set up Cargo cache
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: 🟢 Install Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: .nvmrc
+ cache: npm
+ cache-dependency-path: |
+ package-lock.json
+ frontend/package-lock.json
+
+ - name: 🚧 Install native dependencies
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ BINSTALL_DISABLE_TELEMETRY: "true"
+ run: |
+ brew update
+ brew install \
+ pkg-config \
+ openssl@3 \
+ binaryen \
+ llvm \
+ cargo-binstall
+
+ echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
+ echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV
+ echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH
+
+ cargo binstall --no-confirm --force wasm-pack
+ cargo binstall --no-confirm --force cargo-about
+ cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}"
+
+ - name: 🏗 Build Mac bundle
+ env:
+ CARGO_TERM_COLOR: always
+ run: cargo run build desktop${{ inputs.debug && ' debug' || '' }}
+
+ - name: 📁 Stage artifacts
+ shell: bash
+ run: |
+ PROFILE=${{ inputs.debug && 'debug' || 'release' }}
+ rm -rf target/artifacts
+ mkdir -p target/artifacts
+ cp -R target/$PROFILE/Graphite.app target/artifacts/Graphite.app
+
+ - name: 📦 Upload Mac bundle
+ if: github.event_name != 'push'
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-mac-bundle
+ path: target/artifacts
+
+ - name: 💬 Comment artifact link on PR
+ if: github.event_name != 'push'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-mac-bundle") | .archive_download_url')
+ PR_NUMBER="${{ inputs.pr_number }}"
+ if [ -z "$PR_NUMBER" ]; then
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
+ fi
+ if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then
+ gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| 📦 **Mac Build Complete for** $(git rev-parse HEAD) |
+ |-|
+ | [Download artifact]($ARTIFACT_URL) |"
+ fi
+
+ - name: 🔏 Sign and notarize (preparation)
+ if: github.event_name == 'push'
+ env:
+ APPLE_CERT_BASE64: ${{ secrets.APPLE_CERT_BASE64 }}
+ APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
+ run: |
+ mkdir -p .sign
+ echo "$APPLE_CERT_BASE64" | base64 --decode > .sign/certificate.p12
+
+ security create-keychain -p "" .sign/main.keychain
+ security default-keychain -s .sign/main.keychain
+ security unlock-keychain -p "" .sign/main.keychain
+ security set-keychain-settings -t 3600 -u .sign/main.keychain
+
+ security import .sign/certificate.p12 -k .sign/main.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign
+ security set-key-partition-list -S apple-tool:,apple: -s -k "" .sign/main.keychain
+
+ cat > .sign/entitlements.plist <<'EOF'
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-executable-page-protection
+
+ com.apple.security.cs.disable-library-validation
+
+
+
+ EOF
+
+ - name: 🔏 Sign and notarize
+ if: github.event_name == 'push'
+ env:
+ APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
+ APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }}
+ run: |
+ CERTIFICATE="$APPLE_CERT_NAME"
+ ENTITLEMENTS=".sign/entitlements.plist"
+ APP_PATH="target/artifacts/Graphite.app"
+ ZIP_PATH=".sign/Graphite.zip"
+
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib"
+ codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep
+
+ codesign --verify --deep --strict --verbose=4 "$APP_PATH"
+
+ ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
+ xcrun notarytool submit "$ZIP_PATH" --wait --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD"
+ rm "$ZIP_PATH"
+
+ xcrun stapler staple -v "$APP_PATH"
+
+ spctl -a -vv "$APP_PATH"
+
+ - name: 📦 Upload signed Mac bundle
+ if: github.event_name == 'push'
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-mac-bundle-signed
+ path: target/artifacts
+
+ linux:
+ if: github.event_name == 'push' || inputs.linux
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
+
+ steps:
+ - name: 📥 Clone repository
+ uses: actions/checkout@v6
+ with:
+ repository: ${{ inputs.checkout_repo || github.repository }}
+ ref: ${{ inputs.checkout_ref || '' }}
+
+ - name: ❄ Install Nix
+ uses: DeterminateSystems/nix-installer-action@main
+
+ - name: 🗑 Free disk space
+ run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache
+
+ - name: 📦 Build Nix package
+ run: nix build .#graphite${{ inputs.debug && '-dev' || '' }} --no-link --print-out-paths
+
+ - name: 📤 Push to Nix cache
+ if: (github.event_name == 'push' || inputs.push_to_nix_cache) && !inputs.debug
+ env:
+ NIX_CACHE_AUTH_TOKEN: ${{ secrets.NIX_CACHE_AUTH_TOKEN }}
+ run: |
+ nix run nixpkgs#cachix -- authtoken $NIX_CACHE_AUTH_TOKEN
+ nix build --no-link --print-out-paths | nix run nixpkgs#cachix -- push graphite
+
+ - name: 🏗 Build Linux bundle
+ run: nix build .#graphite${{ inputs.debug && '-dev' || '' }}-bundle.tar.xz && cp ./result ./graphite-linux-bundle.tar.xz
+
+ - name: 📦 Upload Linux bundle
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-linux-bundle
+ path: graphite-linux-bundle.tar.xz
+ compression-level: 0
+
+ - name: 💬 Comment artifact link on PR
+ if: github.event_name != 'push'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ ARTIFACT_URL=$(gh api /repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --jq '.artifacts[] | select(.name == "graphite-linux-bundle") | .archive_download_url')
+ PR_NUMBER="${{ inputs.pr_number }}"
+ if [ -z "$PR_NUMBER" ]; then
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
+ PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --head "$BRANCH" --json number --jq '.[0].number' 2>/dev/null || true)
+ fi
+ if [ -n "$PR_NUMBER" ] && [ -n "$ARTIFACT_URL" ]; then
+ gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "| 📦 **Linux Build Complete for** $(git rev-parse HEAD) |
+ |-|
+ | [Download artifact]($ARTIFACT_URL) |"
+ fi
+
+ - name: 🔧 Install Flatpak tooling
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y flatpak flatpak-builder
+ flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+
+ - name: 🏗 Build Flatpak
+ run: |
+ nix build .#graphite-flatpak-manifest
+
+ rm -rf .flatpak
+ mkdir -p .flatpak
+
+ cp ./result .flatpak/manifest.json
+
+ cd .flatpak
+ mkdir -p repo
+
+ flatpak-builder --user --force-clean --install-deps-from=flathub --repo=repo build ./manifest.json
+
+ flatpak build-bundle repo Graphite.flatpak art.graphite.Graphite --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
+
+ - name: 📦 Upload Flatpak package
+ uses: actions/upload-artifact@v6
+ with:
+ name: graphite-flatpak
+ path: .flatpak/Graphite.flatpak
+ compression-level: 0
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index e49d3ffb..7d696fba 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -12,7 +12,7 @@ jobs:
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: 🔒 Check crate security advisories for root workspace
uses: EmbarkStudios/cargo-deny-action@v2
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8410b6b2..f825072e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,48 +8,6 @@ env:
CARGO_TERM_COLOR: always
jobs:
- # Rust format check on GitHub runner
- rust-fmt:
- runs-on: ubuntu-latest
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: 🚦 Check if CI can be skipped
- id: skip-check
- uses: cariad-tech/merge-queue-ci-skipper@main
-
- - name: 🦀 Install Rust
- if: steps.skip-check.outputs.skip-check != 'true'
- uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- toolchain: stable
- override: true
- rustflags: ""
- components: rustfmt
-
- - name: 🔬 Check Rust formatting
- if: steps.skip-check.outputs.skip-check != 'true'
- run: cargo fmt --all -- --check
-
- # License compatibility check on GitHub runner
- cargo-deny:
- runs-on: ubuntu-latest
- steps:
- - name: 📥 Clone repository
- uses: actions/checkout@v4
-
- - name: 📜 Check crate license compatibility for root workspace
- uses: EmbarkStudios/cargo-deny-action@v2
- with:
- command: check bans licenses sources
-
- - name: 📜 Check crate license compatibility for /libraries/rawkit
- uses: EmbarkStudios/cargo-deny-action@v2
- with:
- command: check bans licenses sources
- manifest-path: libraries/rawkit/Cargo.toml
-
# Build the web app on the self-hosted wasm runner
build:
runs-on: [self-hosted, target/wasm]
@@ -65,7 +23,7 @@ jobs:
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: 🚦 Check if CI can be skipped
id: skip-check
@@ -77,7 +35,7 @@ jobs:
- name: 🟢 Install Node.js
if: steps.skip-check.outputs.skip-check != 'true'
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
@@ -93,6 +51,7 @@ jobs:
with:
toolchain: stable
override: true
+ cache: false
rustflags: ""
target: wasm32-unknown-unknown
@@ -134,7 +93,7 @@ jobs:
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: 🚦 Check if CI can be skipped
id: skip-check
@@ -146,6 +105,7 @@ jobs:
with:
toolchain: stable
override: true
+ cache: false
rustflags: ""
- name: 🦀 Fetch Rust dependencies
@@ -159,3 +119,46 @@ jobs:
RUSTFLAGS: -Dwarnings
run: |
mold -run cargo test --all-features
+
+ # Rust format check on GitHub runner
+ rust-fmt:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 📥 Clone repository
+ uses: actions/checkout@v6
+
+ - name: 🚦 Check if CI can be skipped
+ id: skip-check
+ uses: cariad-tech/merge-queue-ci-skipper@main
+
+ - name: 🦀 Install Rust
+ if: steps.skip-check.outputs.skip-check != 'true'
+ uses: actions-rust-lang/setup-rust-toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ cache: false
+ rustflags: ""
+ components: rustfmt
+
+ - name: 🔬 Check Rust formatting
+ if: steps.skip-check.outputs.skip-check != 'true'
+ run: cargo fmt --all -- --check
+
+ # License compatibility check on GitHub runner
+ check-licenses:
+ runs-on: ubuntu-latest
+ steps:
+ - name: 📥 Clone repository
+ uses: actions/checkout@v6
+
+ - name: 📜 Check crate license compatibility for root workspace
+ uses: EmbarkStudios/cargo-deny-action@v2
+ with:
+ command: check bans licenses sources
+
+ - name: 📜 Check crate license compatibility for /libraries/rawkit
+ uses: EmbarkStudios/cargo-deny-action@v2
+ with:
+ command: check bans licenses sources
+ manifest-path: libraries/rawkit/Cargo.toml
diff --git a/.github/workflows/comment-!build-commands.yml b/.github/workflows/comment-!build-commands.yml
index 9b7a5a18..a1799e55 100644
--- a/.github/workflows/comment-!build-commands.yml
+++ b/.github/workflows/comment-!build-commands.yml
@@ -1,158 +1,137 @@
# USAGE:
-# After reviewing the code, core team members may comment on a PR with the exact text:
-# - `!build-debug` to build with debug symbols and optimizations disabled
-# - `!build` to build without debug symbols and optimizations enabled
-# The comment may not contain any other text, not even whitespace or newlines.
+# After reviewing the code, core team members may comment on a PR with `!build` followed by optional `` and `` arguments.
+# This matches the syntax of the `cargo run build` CLI command, but allows platforms to be specified.
+#
+# ``: `web` (default), `desktop` (all platforms), or `desktop:` (subset of `windows+mac+linux`)
+# ``: `release` (default) or `debug`
+#
+# Examples:
+# - !build
+# - !build debug
+# - !build desktop
+# - !build desktop:windows+mac
+# - !build desktop:linux debug
name: "!build PR Command"
on:
issue_comment:
types:
- created
-env:
- CARGO_TERM_COLOR: always
jobs:
- build:
+ setup:
# Command should be limited to core team members (those in the organization) for security.
# From the GitHub Actions docs:
# author_association = 'MEMBER': Author is a member of the organization that owns the repository.
if: >
github.event.issue.pull_request &&
github.event.comment.author_association == 'MEMBER' &&
- (github.event.comment.body == '!build-debug' || github.event.comment.body == '!build')
- runs-on: self-hosted
+ startsWith(github.event.comment.body, '!build')
+ runs-on: ubuntu-latest
permissions:
- contents: read
- deployments: write
pull-requests: write
- env:
- RUSTC_WRAPPER: /usr/bin/sccache
- CARGO_INCREMENTAL: 0
- SCCACHE_DIR: /var/lib/github-actions/.cache
+ outputs:
+ repo: ${{ steps.pr_info.outputs.repo }}
+ ref: ${{ steps.pr_info.outputs.ref }}
+ web: ${{ steps.pr_info.outputs.web }}
+ windows: ${{ steps.pr_info.outputs.windows }}
+ mac: ${{ steps.pr_info.outputs.mac }}
+ linux: ${{ steps.pr_info.outputs.linux }}
+ debug: ${{ steps.pr_info.outputs.debug }}
steps:
- - name: 🔎 Find branch for this PR
- id: commit_info
+ - name: 🔎 Parse command, find branch, and set build flags
+ id: pr_info
run: |
- RESPONSE=$(curl -L -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }})
- REPO=$(echo $RESPONSE | jq -r '.head.repo.full_name')
- REF=$(echo $RESPONSE | jq -r '.head.ref')
- SHA=$(echo $RESPONSE | jq -r '.head.sha')
- echo "repo=$REPO" >> $GITHUB_OUTPUT
- echo "ref=$REF" >> $GITHUB_OUTPUT
- echo "sha=$SHA" >> $GITHUB_OUTPUT
+ COMMENT="${{ github.event.comment.body }}"
- - name: 📥 Clone repository
- uses: actions/checkout@v4
- with:
- repository: ${{ steps.commit_info.outputs.repo }}
- ref: ${{ steps.commit_info.outputs.ref }}
+ # Split into space-separated words
+ read -ra WORDS <<< "$COMMENT"
- - name: 🗑 Clear wasm-bindgen cache
- continue-on-error: true
- run: rm -r ~/.cache/.wasm-pack
-
- - name: 🟢 Install Node.js
- uses: actions/setup-node@v4
- with:
- node-version-file: .nvmrc
-
- - name: 🚧 Install build dependencies
- run: |
- cd frontend
- npm run setup
-
- - name: 🦀 Install Rust
- uses: actions-rust-lang/setup-rust-toolchain@v1
- with:
- toolchain: stable
- override: true
- rustflags: ""
- target: wasm32-unknown-unknown
-
- - name: ✂ Replace template in of index.html
- env:
- INDEX_HTML_HEAD_REPLACEMENT: ""
- run: |
- # Remove the INDEX_HTML_HEAD_REPLACEMENT environment variable for build links (not master deploys)
- sed -i "s||$INDEX_HTML_HEAD_REPLACEMENT|" frontend/index.html
-
- - name: ⌨ Set build command based on comment
- id: build_command
- run: |
- if [[ "${{ github.event.comment.body }}" == "!build-debug" ]]; then
- echo "command=build web debug" >> $GITHUB_OUTPUT
- elif [[ "${{ github.event.comment.body }}" == "!build" ]]; then
- echo "command=build web" >> $GITHUB_OUTPUT
- else
- echo "Failed to detect if the build command written in the comment should have been '!build-debug', or '!build'" >> $GITHUB_OUTPUT
+ # First word must be "!build"
+ if [[ "${WORDS[0]}" != "!build" ]]; then
+ echo "::error::Expected comment to start with !build"
+ exit 1
fi
- - name: 💬 Comment Actions run link
- id: comment_actions_run_link
- uses: actions/github-script@v6
+ # Initialize build flags (web defaults to true, matching the CLI default target)
+ WEB="true"
+ WINDOWS="false"
+ MAC="false"
+ LINUX="false"
+ DEBUG="false"
+
+ # Parse target (optional, defaults to `web` if omitted)
+ IDX=1
+ case "${WORDS[$IDX]:-}" in
+ # Target: `web` enables just the web build (already the default, but accepted explicitly)
+ "web")
+ ((IDX++)) ;;
+ # Target: `desktop` enables all three desktop platforms
+ "desktop")
+ WEB="false"; WINDOWS="true"; MAC="true"; LINUX="true"; ((IDX++)) ;;
+ # Target: `desktop:` enables a subset of desktop platforms, split by `+`
+ desktop:*)
+ WEB="false"
+ PLATFORMS="${WORDS[$IDX]#desktop:}"
+ IFS='+' read -ra PARTS <<< "$PLATFORMS"
+ for PART in "${PARTS[@]}"; do
+ case "$PART" in
+ "windows") WINDOWS="true" ;;
+ "mac") MAC="true" ;;
+ "linux") LINUX="true" ;;
+ *) echo "::error::Unrecognized platform: $PART"; exit 1 ;;
+ esac
+ done
+ ((IDX++))
+ ;;
+ esac
+
+ # Parse profile (optional, defaults to `release` if omitted)
+ case "${WORDS[$IDX]:-}" in
+ "debug") DEBUG="true"; ((IDX++)) ;;
+ "release") ((IDX++)) ;;
+ esac
+
+ # Reject any unexpected trailing words
+ if [[ $IDX -lt ${#WORDS[@]} ]]; then
+ echo "::error::Unexpected argument: ${WORDS[$IDX]}"
+ exit 1
+ fi
+
+ # Write parsed build flags to job outputs
+ echo "web=$WEB" >> $GITHUB_OUTPUT
+ echo "windows=$WINDOWS" >> $GITHUB_OUTPUT
+ echo "mac=$MAC" >> $GITHUB_OUTPUT
+ echo "linux=$LINUX" >> $GITHUB_OUTPUT
+ echo "debug=$DEBUG" >> $GITHUB_OUTPUT
+
+ # Fetch the PR's head branch and repo (needed for forked PRs where the code lives in another repo)
+ RESPONSE=$(curl -L -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28' https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }})
+ echo "repo=$(echo $RESPONSE | jq -r '.head.repo.full_name')" >> $GITHUB_OUTPUT
+ echo "ref=$(echo $RESPONSE | jq -r '.head.ref')" >> $GITHUB_OUTPUT
+
+ - name: 💬 Edit comment with workflow run link
+ uses: actions/github-script@v8
with:
script: |
github.rest.issues.updateComment({
comment_id: ${{ github.event.comment.id }},
owner: context.repo.owner,
repo: context.repo.repo,
- body: '!build ([Run ID ' + context.runId + '](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + '))'
+ body: '${{ github.event.comment.body }} ([Run ID ' + context.runId + '](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + '))'
});
- - name: 🌐 Build Graphite web code
- env:
- NODE_ENV: production
- if: ${{ success() || failure()}}
- run: mold -run cargo run ${{ steps.build_command.outputs.command }}
-
- - name: ❗ Warn on build failure
- if: ${{ failure() }}
- uses: actions/github-script@v6
- with:
- script: |
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: 'The build process has failed. Please check the [build logs](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + ') for details.'
- });
-
- - name: 📤 Publish to Cloudflare Pages
- id: cloudflare
- env:
- CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- run: |
- if OUTPUT=$(npx wrangler@3 pages deploy "frontend/dist" --project-name="graphite-dev" --commit-dirty=true 2>&1); then
- URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.pages\.dev' | tail -1)
- echo "url=$URL" >> "$GITHUB_OUTPUT"
- echo "Published successfully: $URL"
- else
- echo "$OUTPUT"
- exit 1
- fi
-
- - name: ❗ Warn on publish failure
- if: ${{ failure() }}
- uses: actions/github-script@v6
- with:
- script: |
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: 'The deployment to Cloudflare Pages has failed. Please check the [build logs](https://github.com/GraphiteEditor/Graphite/actions/runs/' + context.runId + ') for details.'
- });
-
- - name: 💬 Comment build link
- uses: actions/github-script@v6
- with:
- script: |
- github.rest.issues.createComment({
- issue_number: context.issue.number,
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: '| 📦 **Build Complete for** ${{ steps.commit_info.outputs.sha }} |\n|-|\n| ${{ steps.cloudflare.outputs.url }} |'
- })
+ invoke-build:
+ needs: setup
+ uses: ./.github/workflows/build.yml
+ secrets: inherit
+ with:
+ web: ${{ needs.setup.outputs.web == 'true' }}
+ windows: ${{ needs.setup.outputs.windows == 'true' }}
+ mac: ${{ needs.setup.outputs.mac == 'true' }}
+ linux: ${{ needs.setup.outputs.linux == 'true' }}
+ debug: ${{ needs.setup.outputs.debug == 'true' }}
+ checkout_repo: ${{ needs.setup.outputs.repo }}
+ checkout_ref: ${{ needs.setup.outputs.ref }}
+ pr_number: ${{ github.event.issue.number }}
diff --git a/.github/workflows/comment-clippy-warnings.yaml b/.github/workflows/comment-clippy-warnings.yaml
index d9e2ff68..9e352bf6 100644
--- a/.github/workflows/comment-clippy-warnings.yaml
+++ b/.github/workflows/comment-clippy-warnings.yaml
@@ -15,7 +15,7 @@ jobs:
contents: read
pull-requests: write
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- name: Install Rust
uses: actions-rs/toolchain@v1
diff --git a/.github/workflows/comment-profiling-changes.yaml b/.github/workflows/comment-profiling-changes.yaml
index 3369f4e6..275f6589 100644
--- a/.github/workflows/comment-profiling-changes.yaml
+++ b/.github/workflows/comment-profiling-changes.yaml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -35,7 +35,7 @@ jobs:
- name: Cache iai-callgrind binary
id: cache-iai
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.cargo/bin/iai-callgrind-runner
key: ${{ runner.os }}-iai-callgrind-runner-0.16.1
@@ -65,7 +65,7 @@ jobs:
- name: Cache benchmark baselines
id: cache-benchmark-baselines
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: target/iai
key: ${{ runner.os }}-${{ runner.arch }}-${{ steps.cpu-info.outputs.cpu-hash }}-benchmark-baselines-master-${{ steps.master-sha.outputs.sha }}
diff --git a/.github/workflows/library-rawkit.yml b/.github/workflows/library-rawkit.yml
index 5d5b8bf6..910daac0 100644
--- a/.github/workflows/library-rawkit.yml
+++ b/.github/workflows/library-rawkit.yml
@@ -26,13 +26,14 @@ jobs:
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: 🦀 Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true
+ cache: false
rustflags: ""
- name: 📦 Run sccache-cache
diff --git a/.github/workflows/provide-shaders.yml b/.github/workflows/provide-shaders.yml
index 84f75311..647878ce 100644
--- a/.github/workflows/provide-shaders.yml
+++ b/.github/workflows/provide-shaders.yml
@@ -13,7 +13,7 @@ jobs:
contents: read
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: ❄ Install Nix
uses: DeterminateSystems/nix-installer-action@main
diff --git a/.github/workflows/build-production.yml b/.github/workflows/release.yml
similarity index 95%
rename from .github/workflows/build-production.yml
rename to .github/workflows/release.yml
index d9a186e2..602f4016 100644
--- a/.github/workflows/build-production.yml
+++ b/.github/workflows/release.yml
@@ -1,4 +1,4 @@
-name: "Editor: Production (Latest Stable)"
+name: "Release"
on:
push:
@@ -22,13 +22,13 @@ jobs:
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: 🗑 Clear wasm-bindgen cache
run: rm -r ~/.cache/.wasm-pack
- name: 🟢 Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
@@ -42,6 +42,7 @@ jobs:
with:
toolchain: stable
override: true
+ cache: false
rustflags: ""
target: wasm32-unknown-unknown
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
index fae935b4..99964261 100644
--- a/.github/workflows/website.yml
+++ b/.github/workflows/website.yml
@@ -24,10 +24,10 @@ jobs:
steps:
- name: 📥 Clone repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: 🟢 Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
@@ -51,7 +51,7 @@ jobs:
- name: 💿 Obtain cache of auto-generated code docs artifacts
id: cache-website-code-docs
- uses: actions/cache/restore@v4
+ uses: actions/cache/restore@v5
with:
path: artifacts
key: website-code-docs
diff --git a/.nix/default.nix b/.nix/default.nix
index e77cc4a6..20e26a96 100644
--- a/.nix/default.nix
+++ b/.nix/default.nix
@@ -59,7 +59,8 @@ in
graphite-dev = (lib.call ./pkgs/graphite.nix) { dev = true; };
graphite-raster-nodes-shaders = lib.call ./pkgs/graphite-raster-nodes-shaders.nix;
graphite-branding = lib.call ./pkgs/graphite-branding.nix;
- graphite-bundle = lib.call ./pkgs/graphite-bundle.nix;
+ graphite-bundle = (lib.call ./pkgs/graphite-bundle.nix) { };
+ graphite-dev-bundle = (lib.call ./pkgs/graphite-bundle.nix) { graphite = graphite-dev; };
graphite-flatpak-manifest = lib.call ./pkgs/graphite-flatpak-manifest.nix;
# TODO: graphene-cli = lib.call ./pkgs/graphene-cli.nix;
diff --git a/.nix/pkgs/graphite-bundle.nix b/.nix/pkgs/graphite-bundle.nix
index d50b00fc..cf2d54de 100644
--- a/.nix/pkgs/graphite-bundle.nix
+++ b/.nix/pkgs/graphite-bundle.nix
@@ -4,6 +4,9 @@
system,
...
}:
+{
+ graphite ? self.packages.${system}.graphite,
+}:
let
bundle =
{
@@ -13,7 +16,6 @@ let
}:
(
let
- graphite = self.packages.${system}.graphite;
tar = if compression == null then archive else true;
nameArchiveSuffix = if tar then ".tar" else "";
nameCompressionSuffix = if compression == null then "" else "." + compression;