Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d9987d88 | ||
|
|
e2b0ea3f55 | ||
|
|
fa492a1f39 | ||
|
|
ddeb528d10 | ||
|
|
ca37047f50 | ||
|
|
3327fb5d4f | ||
|
|
5e33271a60 | ||
|
|
95d9562e1b | ||
|
|
9d92dc3f11 | ||
|
|
a3c59023e5 | ||
|
|
3badc2007c | ||
|
|
a050647381 | ||
|
|
62cbd235cb | ||
|
|
d14a50c742 | ||
|
|
737593fbf5 | ||
|
|
31b17124d8 | ||
|
|
3d9023b2fd | ||
|
|
2cfa821094 | ||
|
|
691b05f882 | ||
|
|
f1559aff72 | ||
|
|
a25fd75aab | ||
|
|
b721de591a | ||
|
|
37031b65fa | ||
|
|
f36f0128b8 | ||
|
|
708a233f26 | ||
|
|
bdcba931a8 | ||
|
|
82c4662a99 | ||
|
|
05325ec5b4 | ||
|
|
0a1d6acf27 | ||
|
|
579066102a | ||
|
|
4aac744759 | ||
|
|
3d34125cf9 | ||
|
|
0e99cc8d40 | ||
|
|
e768643d3e | ||
|
|
e8856d7e20 | ||
|
|
d120cfcb4b | ||
|
|
cff7f48961 | ||
|
|
d705b482cc | ||
|
|
5e350aa7ef | ||
|
|
8d4e4ae178 | ||
|
|
6840499f99 | ||
|
|
af9449e5f2 | ||
|
|
0b781db043 | ||
|
|
dcf3e9ed8e | ||
|
|
e267b28dbb | ||
|
|
672775d9b5 | ||
|
|
8824a74fb6 | ||
|
|
622e2172d6 | ||
|
|
887b8655e9 | ||
|
|
1a71fddf7b | ||
|
|
1b0f4e1937 | ||
|
|
2c2a5c2327 | ||
|
|
5b52e615f7 | ||
|
|
5d734bac3f | ||
|
|
f3809b6cee | ||
|
|
c3a9c956fb | ||
|
|
d29561666e | ||
|
|
da2f27524f | ||
|
|
1e832286b6 | ||
|
|
2dbd546a85 | ||
|
|
8c2753607d | ||
|
|
4c6b9d0cd9 | ||
|
|
b89062b317 | ||
|
|
1bb53148d5 | ||
|
|
0946ec33ac | ||
|
|
9b39e85d11 | ||
|
|
bfd883b5fa | ||
|
|
44e0a67fd6 | ||
|
|
f0eca7c6f7 | ||
|
|
e83463b6e1 | ||
|
|
1d517005b0 | ||
|
|
7b84ccd21b | ||
|
|
67fbfc9844 | ||
|
|
c02216eb82 | ||
|
|
fcbad52833 | ||
|
|
ace07fed2f | ||
|
|
edb292afc2 | ||
|
|
fa48fd951c | ||
|
|
9daa3b7a89 | ||
|
|
90f1b6a16f | ||
|
|
b15c7a1e5a | ||
|
|
603a21aa4d |
@@ -1,45 +0,0 @@
|
||||
---
|
||||
name: git-safety
|
||||
description: Git safety rules. INVOKE WHEN: git push, force push, git reset, git clean, destructive git, push force, reset hard. NEVER force push or do destructive git operations.
|
||||
---
|
||||
|
||||
# git safety
|
||||
|
||||
rules for git operations in this repo.
|
||||
|
||||
## rules
|
||||
|
||||
- NEVER force push (`git push --force` or `git push -f`)
|
||||
- NEVER `git reset --hard` without explicit permission
|
||||
- NEVER `git clean -fd` without explicit permission
|
||||
- prefer `git stash` over `git reset`
|
||||
- always `git pull` before `git push`
|
||||
|
||||
## forbidden commands
|
||||
|
||||
these require EXPLICIT user permission:
|
||||
- `git push --force`
|
||||
- `git push -f`
|
||||
- `git push --force-with-lease`
|
||||
- `git reset --hard`
|
||||
- `git clean -fd`
|
||||
- `git checkout -- .` (discards all changes)
|
||||
|
||||
## safe alternatives
|
||||
|
||||
instead of `git reset --hard`:
|
||||
- `git stash` to save changes
|
||||
- `git stash pop` to restore
|
||||
|
||||
instead of force push:
|
||||
- `git pull --rebase` then normal push
|
||||
- ask user how to resolve conflicts
|
||||
|
||||
## workflow
|
||||
|
||||
1. `git pull` before making changes
|
||||
2. make changes
|
||||
3. `git add` relevant files
|
||||
4. `git commit`
|
||||
5. `git pull` again before push
|
||||
6. `git push` (no force flags)
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: release-safety
|
||||
description: Release safety rules. INVOKE WHEN: yarn release, npm publish, release canary, release packages, publishing, skip checks, skip tests. NEVER skip checks or tests without explicit permission.
|
||||
---
|
||||
|
||||
# release safety
|
||||
|
||||
rules for running releases in this monorepo.
|
||||
|
||||
## rules
|
||||
|
||||
- NEVER add `--skip-checks` unless user explicitly says "skip checks"
|
||||
- NEVER add `--skip-tests` unless user explicitly says "skip tests"
|
||||
- if checks or tests fail, FIX THE ISSUES instead of skipping
|
||||
- ask user before adding any skip flags
|
||||
- the point of checks is to catch problems before publishing
|
||||
|
||||
## when things fail
|
||||
|
||||
1. read the error output carefully
|
||||
2. fix the underlying issue
|
||||
3. re-run the release
|
||||
4. do NOT just skip the failing step
|
||||
|
||||
## common flags
|
||||
|
||||
safe flags:
|
||||
- `--canary` - publish canary version
|
||||
- `--ci` - run in ci mode
|
||||
- `--dirty` - allow dirty working directory
|
||||
- `--build-fast` - faster builds
|
||||
|
||||
dangerous flags (require explicit permission):
|
||||
- `--skip-checks` - skips type checking
|
||||
- `--skip-tests` - skips test suite
|
||||
- `--skip-build` - skips building packages
|
||||
@@ -7,7 +7,7 @@ node_modules
|
||||
**/*/node_modules
|
||||
**/*/dist
|
||||
**/*/.turbo
|
||||
**/*/.hanzogui
|
||||
**/*/.tamagui
|
||||
|
||||
# expo
|
||||
**/*/.expo
|
||||
|
||||
@@ -16,7 +16,7 @@ body:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Gui Version
|
||||
label: Hanzo GUI Version
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
@@ -32,9 +32,9 @@ body:
|
||||
render: markdown
|
||||
description: |
|
||||
- If you don't include reproduction, we will close the issue.
|
||||
- The easiest way to reproduce is using the starter free repo: https://github.com/hanzogui/starter-free
|
||||
- The easiest way to reproduce is using the starter free repo: https://github.com/hanzoai/starter-free
|
||||
- Clone that repo and push your reproduction as a new commit.
|
||||
- For simpler bugs you can fork [this CodeSandbox](https://codesandbox.io/p/github/hanzogui/starter-free) to reproduce the issue.
|
||||
- For simpler bugs you can fork [this CodeSandbox](https://codesandbox.io/p/github/gui/starter-free) to reproduce the issue.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -10,13 +10,13 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
@@ -28,7 +28,7 @@ runs:
|
||||
echo "path=$HOME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Bun
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
env:
|
||||
cache-name: bun_cache
|
||||
with:
|
||||
@@ -39,9 +39,9 @@ runs:
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
- name: Re-link workspace bins
|
||||
shell: bash
|
||||
# Re-run install to ensure bin symlinks are created for workspace packages after build
|
||||
run: bun install --frozen-lockfile
|
||||
run: bun install
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { basename, join } from 'node:path'
|
||||
|
||||
const repoRoot = process.cwd()
|
||||
const workflowPath = join(repoRoot, '.github/workflows/test-native.yml')
|
||||
const e2eDir = join(repoRoot, 'apps/kitchen-sink/e2e')
|
||||
|
||||
const explicitlyExcludedTests = new Map([
|
||||
// android-only: every test skips on iOS (device.getPlatform() === 'ios'), so running
|
||||
// it on the iOS sim produced zero coverage while burning ~6.6min. it still runs in the
|
||||
// android Detox job, which executes all e2e files unsharded.
|
||||
[
|
||||
'SelectAndroidOnPress.test.ts',
|
||||
'android-only (skips on iOS); runs in the android job',
|
||||
],
|
||||
])
|
||||
|
||||
const workflow = readFileSync(workflowPath, 'utf8')
|
||||
const shardMatches = [...workflow.matchAll(/test_files:\s*'([^']*)'/g)]
|
||||
|
||||
if (shardMatches.length === 0) {
|
||||
console.error(`No iOS Detox shard definitions found in ${workflowPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const assignedTestFiles = shardMatches.flatMap((match) =>
|
||||
match[1]
|
||||
.split(/\s+/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const assignedTestBasenames = assignedTestFiles
|
||||
.filter((entry) => entry.endsWith('.test.ts'))
|
||||
.map((entry) => basename(entry))
|
||||
|
||||
const allDetoxTests = readdirSync(e2eDir)
|
||||
.filter((entry) => entry.endsWith('.test.ts'))
|
||||
.sort()
|
||||
|
||||
const counts = new Map()
|
||||
for (const testName of assignedTestBasenames) {
|
||||
counts.set(testName, (counts.get(testName) ?? 0) + 1)
|
||||
}
|
||||
|
||||
const duplicateAssignments = [...counts.entries()]
|
||||
.filter(([, count]) => count > 1)
|
||||
.map(([testName, count]) => `${testName} (${count}x)`)
|
||||
|
||||
const missingAssignments = allDetoxTests.filter(
|
||||
(testName) => !counts.has(testName) && !explicitlyExcludedTests.has(testName)
|
||||
)
|
||||
|
||||
const staleExclusions = [...explicitlyExcludedTests.keys()].filter(
|
||||
(testName) => !allDetoxTests.includes(testName)
|
||||
)
|
||||
|
||||
if (
|
||||
duplicateAssignments.length === 0 &&
|
||||
missingAssignments.length === 0 &&
|
||||
staleExclusions.length === 0
|
||||
) {
|
||||
console.info('iOS Detox shard coverage is valid.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (duplicateAssignments.length > 0) {
|
||||
console.error('Duplicate iOS Detox shard assignments:')
|
||||
for (const entry of duplicateAssignments) {
|
||||
console.error(`- ${entry}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (missingAssignments.length > 0) {
|
||||
console.error('Missing iOS Detox tests from shard matrix:')
|
||||
for (const testName of missingAssignments) {
|
||||
console.error(`- ${testName}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (staleExclusions.length > 0) {
|
||||
console.error('Stale explicit shard exclusions:')
|
||||
for (const testName of staleExclusions) {
|
||||
console.error(`- ${testName}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (explicitlyExcludedTests.size > 0) {
|
||||
console.error('Explicitly excluded iOS Detox tests:')
|
||||
for (const [testName, reason] of explicitlyExcludedTests.entries()) {
|
||||
console.error(`- ${testName}: ${reason}`)
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
@@ -1,132 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# run each maestro flow as its own invocation, retrying ONLY the flow that
|
||||
# fails. the previous harness re-ran the WHOLE suite on any failure and only
|
||||
# passed on a single fully-clean attempt, so a transient flake landing on a
|
||||
# DIFFERENT flow each attempt could never go green even though every flow
|
||||
# passed at least once (observed on iOS Native: attempt 1 failed `Select`,
|
||||
# attempt 2 failed `OpenApp`, each passed on the other attempt -> suite red).
|
||||
#
|
||||
# per-flow retry isolates a flake to its own flow and keeps the other flows'
|
||||
# passes, so the suite goes green as long as every flow passes within its
|
||||
# own attempt budget.
|
||||
#
|
||||
# configuration (env vars, all optional):
|
||||
# FLOWS_DIR directory of flow .yaml files (default: ./flows)
|
||||
# BUNDLE_ID app to terminate between attempts (default: com.hanzogui.hanzoguikitchensink)
|
||||
# MAX_ATTEMPTS attempts per flow before giving up (default: 3)
|
||||
# SKIP_FLOWS space-separated basenames to skip; these are util
|
||||
# sub-flows / warm-ups, not standalone tests
|
||||
# (default: "OpenApp.yaml WarmUp.yaml")
|
||||
# METRO_PID_FILE if the pid in this file is dead, abort (default: /tmp/metro-pid)
|
||||
# FLOW_TIMEOUT hard timeout (seconds) per flow attempt; a wedged xcuitest
|
||||
# driver makes `maestro test` hang with no exit, so without it
|
||||
# the loop never advances and the job sits idle until the
|
||||
# runner's 45-min cap (default: 360)
|
||||
#
|
||||
# NOT using `set -e`: a failing/timed-out `maestro test` is expected and handled inline.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
FLOWS_DIR="${FLOWS_DIR:-./flows}"
|
||||
BUNDLE_ID="${BUNDLE_ID:-com.hanzogui.hanzoguikitchensink}"
|
||||
MAX_ATTEMPTS="${MAX_ATTEMPTS:-3}"
|
||||
SKIP_FLOWS="${SKIP_FLOWS:-OpenApp.yaml WarmUp.yaml}"
|
||||
METRO_PID_FILE="${METRO_PID_FILE:-/tmp/metro-pid}"
|
||||
FLOW_TIMEOUT="${FLOW_TIMEOUT:-360}"
|
||||
|
||||
metro_alive() {
|
||||
local pid
|
||||
pid="$(cat "$METRO_PID_FILE" 2>/dev/null || true)"
|
||||
[ -z "$pid" ] && return 0 # no tracked pid -> nothing to check
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
recover_sim() {
|
||||
echo "recovering simulator state (terminate app, keep Hermes bytecode cache)..."
|
||||
xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 2
|
||||
if ! metro_alive; then
|
||||
echo "::error::Metro died during test run — aborting (every retry would fail)"
|
||||
tail -50 /tmp/metro.log 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# run `maestro test <flow>` with a hard per-attempt timeout. when the iOS
|
||||
# xcuitest driver wedges (e.g. "Request for viewHierarchy failed, code: 500"),
|
||||
# maestro hangs with no exit — without a timeout the retry loop below never
|
||||
# advances and the whole job sits idle until the runner's 45-min cap (observed:
|
||||
# a 28-min hang on one flow that then got force-cancelled = red CI for a flake).
|
||||
# on timeout we SIGTERM then SIGKILL maestro's PROCESS GROUP — `set -m` gives the
|
||||
# backgrounded job its own pgid, so killing -$pid also reaps the child
|
||||
# xcodebuild/driver processes — then recover_sim resets state and the next
|
||||
# attempt re-establishes a fresh driver. returns maestro's exit code, or 124.
|
||||
run_flow_with_timeout() {
|
||||
local flow="$1"
|
||||
set -m
|
||||
maestro test "$flow" --no-ansi &
|
||||
local pid=$!
|
||||
set +m
|
||||
local waited=0
|
||||
while kill -0 "$pid" 2>/dev/null; do
|
||||
if [ "$waited" -ge "$FLOW_TIMEOUT" ]; then
|
||||
echo "::warning::flow exceeded ${FLOW_TIMEOUT}s — killing maestro (wedged xcuitest driver?)"
|
||||
kill -TERM -"$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null
|
||||
sleep 10
|
||||
kill -KILL -"$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null
|
||||
wait "$pid" 2>/dev/null
|
||||
return 124
|
||||
fi
|
||||
sleep 5
|
||||
waited=$((waited + 5))
|
||||
done
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
shopt -s nullglob
|
||||
flows=("$FLOWS_DIR"/*.yaml "$FLOWS_DIR"/*.yml)
|
||||
shopt -u nullglob
|
||||
|
||||
if [ "${#flows[@]}" -eq 0 ]; then
|
||||
echo "::error::no flow files found in $FLOWS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
failed=""
|
||||
for flow in "${flows[@]}"; do
|
||||
base="$(basename "$flow")"
|
||||
case " $SKIP_FLOWS " in
|
||||
*" $base "*)
|
||||
echo "skipping util sub-flow: $base"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
name="${base%.*}"
|
||||
passed=false
|
||||
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
|
||||
echo "::group::$name (attempt $attempt/$MAX_ATTEMPTS)"
|
||||
if run_flow_with_timeout "$flow"; then
|
||||
passed=true
|
||||
echo "::endgroup::"
|
||||
echo "✓ $name passed (attempt $attempt/$MAX_ATTEMPTS)"
|
||||
break
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
echo "::warning::$name failed (attempt $attempt/$MAX_ATTEMPTS)"
|
||||
if [ "$attempt" -lt "$MAX_ATTEMPTS" ]; then
|
||||
recover_sim
|
||||
fi
|
||||
done
|
||||
if [ "$passed" = false ]; then
|
||||
failed="$failed $name"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$failed" ]; then
|
||||
echo "::error::flows failed after $MAX_ATTEMPTS attempts:$failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "all flows passed"
|
||||
@@ -1,13 +1,13 @@
|
||||
name: Build iOS Test Container App
|
||||
env:
|
||||
# Relative path from the monorepo root to the app
|
||||
test_container_app_path: apps/kitchen-sink
|
||||
test_container_app_path: code/kitchen-sink
|
||||
# Package name in the monorepo
|
||||
test_app_package_name: '@hanzogui/kitchen-sink'
|
||||
# Should match the name of "ios/<app_name>.xcworkspace"
|
||||
ios_app_name: hanzoguikitchensink
|
||||
ios_app_name: guikitchensink
|
||||
# A unique ID to identify the app on GitHub Actions, this is used as part of cache keys and artifact names, must be unique among all workflows
|
||||
app_id: ios-hanzoguikitchensink
|
||||
app_id: ios-guikitchensink
|
||||
# The command used to prebuild the app
|
||||
prebuild_command: bunx expo prebuild --platform ios --no-install # --no-install is used to skip installing dependencies, specifically `pod install` as we want to do it after the Cache Pods step
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check If Build Already Exists
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
id: pre-check-has-build
|
||||
if: ${{ steps.get-fingerprint-from-cache.outputs.fingerprint != 'null' }}
|
||||
with:
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
|
||||
- name: Cache Pods
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
env:
|
||||
cache-name: ${{ env.app_id }}-pods
|
||||
with:
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
|
||||
- name: Upload Pod Install Log on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: pod-install-log
|
||||
path: ${{ env.test_container_app_path }}/pod-install.log
|
||||
@@ -228,7 +228,7 @@ jobs:
|
||||
|
||||
- name: Check If Build Already Exists
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }}
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
id: check-has-build
|
||||
with:
|
||||
key: ${{ env.app_id }}-${{ inputs.configuration }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}
|
||||
@@ -238,7 +238,7 @@ jobs:
|
||||
- name: Restore Build Cache
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
id: restore-build-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
env:
|
||||
cache-name: ${{ env.app_id }}-build
|
||||
with:
|
||||
@@ -263,13 +263,13 @@ jobs:
|
||||
|
||||
- name: Upload Built App to Cache
|
||||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
key: ${{ steps.check-has-build.outputs.cache-primary-key }}
|
||||
path: ${{ steps.get-built-app-path.outputs.built_app_path }}
|
||||
|
||||
- name: Upload Build Log
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v5.3.1
|
||||
if: ${{ always() && !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
with:
|
||||
name: xcodebuild-${{ env.app_id }}-${{ inputs.configuration }}.log
|
||||
@@ -277,7 +277,7 @@ jobs:
|
||||
${{ env.test_container_app_path }}/xcodebuild.log
|
||||
|
||||
- name: Save Build Cache
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
if: ${{ always() && !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }}
|
||||
with:
|
||||
key: ${{ steps.restore-build-cache.outputs.cache-primary-key }}
|
||||
|
||||
@@ -5,19 +5,14 @@ on:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
tagged-release:
|
||||
name: Changelog
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
# needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: marvinpinto/action-automatic-releases@d68defdd11f9dcc7f52f35c1b7c236ee7513bcc1 # latest (archived action - consider replacing with softprops/action-gh-release)
|
||||
- uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: '${{ github.token }}'
|
||||
prerelease: false
|
||||
|
||||
@@ -12,9 +12,6 @@ on:
|
||||
- v2
|
||||
- 'v2-*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# cancel in-progress runs on the same branch (avoids duplicate PR + push runs)
|
||||
concurrency:
|
||||
group: checks-${{ github.ref }}
|
||||
@@ -22,42 +19,30 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
# replaces dependabot. all critical advisories are pinned to patched
|
||||
# versions via root package.json "overrides", so this gates cleanly with
|
||||
# no ignore list. remaining high/moderate advisories require cross-major
|
||||
# bumps of example/tooling deps (vite, undici, tar, react-router, etc.) -
|
||||
# run `bun audit` locally to review them.
|
||||
- name: Security audit
|
||||
run: bun audit --audit-level critical
|
||||
|
||||
- name: Check
|
||||
run: bun run check
|
||||
|
||||
- name: Typecheck
|
||||
run: bun run typecheck
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
# tests depend on ^build; types are the checks job's business, not ours
|
||||
SKIP_TYPES: '1'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -69,7 +54,7 @@ jobs:
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
@@ -77,79 +62,35 @@ jobs:
|
||||
- name: Install playwright
|
||||
run: bunx playwright install
|
||||
|
||||
- name: Restore Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo/cache
|
||||
key: turbo-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun turbo run test:web --filter='!@hanzogui/kitchen-sink' --concurrency=1
|
||||
|
||||
- name: Run native unit tests
|
||||
run: bun turbo run test:native --filter=@hanzogui/static-tests --filter=@hanzogui/core-test --filter=@hanzogui/components-test --concurrency=1
|
||||
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
outputs:
|
||||
integration-relevant: ${{ steps.turbo-check.outputs.relevant }}
|
||||
integration-relevant: ${{ steps.filter.outputs.integration-relevant }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
# turbo reads workspace graph from package.json files — no bun install needed
|
||||
# TODO: turbo ls --affected is broken in <=2.8.20 (ignores git refs, false positives
|
||||
# on root config changes — vercel/turborepo#11144, #10869). fix landed in 2.8.21
|
||||
# canaries (PR #12424). once stable, simplify this to: turbo ls --affected
|
||||
- name: Install turbo
|
||||
run: npm install -g turbo@2
|
||||
|
||||
- name: Check if kitchen-sink dependency tree changed
|
||||
id: turbo-check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
BASE="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE="HEAD~1"
|
||||
fi
|
||||
|
||||
# get all package directories in kitchen-sink's transitive dependency tree
|
||||
# turbo ls resolves the real workspace dependency graph — no manual list needed
|
||||
DEP_DIRS=$(turbo ls --filter='@hanzogui/kitchen-sink...' 2>/dev/null | awk 'NR>1 && NF>0 {print $2}' | grep '/')
|
||||
|
||||
if [ -z "$DEP_DIRS" ]; then
|
||||
echo "relevant=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Could not resolve dependency tree, running tests as fallback"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# build a single regex from all dependency directories + root config files
|
||||
PATTERN=$(echo "$DEP_DIRS" | paste -sd'|' -)
|
||||
FULL_PATTERN="^($PATTERN)/|^(package\.json|bun\.lock|turbo\.json)$"
|
||||
|
||||
CHANGED=$(git diff --name-only "$BASE" HEAD)
|
||||
|
||||
if echo "$CHANGED" | grep -qE "$FULL_PATTERN"; then
|
||||
MATCHED=$(echo "$CHANGED" | grep -E "$FULL_PATTERN" | head -5)
|
||||
echo "relevant=true" >> $GITHUB_OUTPUT
|
||||
echo "::notice::Integration tests needed. Changed dependency files: $(echo "$MATCHED" | tr '\n' ', ')"
|
||||
else
|
||||
echo "relevant=false" >> $GITHUB_OUTPUT
|
||||
echo "::notice::Integration tests skipped — no changes in kitchen-sink dependency tree"
|
||||
fi
|
||||
filters: |
|
||||
integration-relevant:
|
||||
- 'code/core/**'
|
||||
- 'code/ui/**'
|
||||
- 'code/packages/**'
|
||||
- 'code/kitchen-sink/**'
|
||||
- 'code/compiler/babel-plugin/**'
|
||||
- 'code/compiler/loader/**'
|
||||
- 'code/compiler/metro-plugin/**'
|
||||
- 'code/demos/**'
|
||||
- 'package.json'
|
||||
- 'bun.lock'
|
||||
|
||||
integration-tests:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.integration-relevant == 'true'
|
||||
# gate is turbo's actual workspace dependency graph — no manual path list to maintain
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -158,7 +99,7 @@ jobs:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -170,7 +111,7 @@ jobs:
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
@@ -178,14 +119,6 @@ jobs:
|
||||
- name: Install playwright
|
||||
run: bunx playwright install --with-deps
|
||||
|
||||
- name: Restore Turbo cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .turbo/cache
|
||||
key: turbo-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
turbo-${{ runner.os }}-
|
||||
|
||||
- name: Run integration tests (shard ${{ matrix.shard }})
|
||||
run: cd apps/kitchen-sink && NODE_ENV=test npx playwright test --shard=${{ matrix.shard }}
|
||||
run: cd code/kitchen-sink && NODE_ENV=test npx playwright test --shard=${{ matrix.shard }}
|
||||
timeout-minutes: 20
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Publish on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Build + Publish to NPM
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Configure npm
|
||||
run: npm config set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish @hanzo/gui
|
||||
working-directory: code/ui/gui
|
||||
run: |
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Publishing @hanzo/gui@$VERSION"
|
||||
if npm view "@hanzo/gui@$VERSION" version 2>/dev/null; then
|
||||
echo "Already published — skipping"
|
||||
else
|
||||
npm publish --access public
|
||||
fi
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Release
|
||||
|
||||
# testing on a branch for now
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- test-release
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
# Full history needed for git tag and push
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Install playwright
|
||||
run: bunx playwright install
|
||||
|
||||
- name: Test
|
||||
run: bun run test
|
||||
|
||||
- name: Publish
|
||||
# just testing for now so --skip-publish
|
||||
run: bun run release:beta:dirty --ci --gui-git-user --skip-publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
tagged-release:
|
||||
name: Tagged Release
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
# needs: release
|
||||
|
||||
steps:
|
||||
- uses: marvinpinto/action-automatic-releases@latest
|
||||
with:
|
||||
repo_token: '${{ github.token }}'
|
||||
prerelease: false
|
||||
@@ -1,308 +0,0 @@
|
||||
name: Release
|
||||
|
||||
# github evaluates workflow_run from the default branch. keep this workflow on
|
||||
# main as well as v3-beta so successful v3-beta CI can publish automatically.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Checks]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release:
|
||||
description: Version to publish from main
|
||||
required: true
|
||||
default: canary
|
||||
type: choice
|
||||
options:
|
||||
- canary
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: npm-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
beta:
|
||||
name: Publish v3 beta
|
||||
if: >-
|
||||
github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'v3-beta'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout tested v3-beta source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Select latest v3-beta build
|
||||
id: candidate
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin v3-beta
|
||||
if [[ "$(git rev-parse HEAD)" != "$(git rev-parse origin/v3-beta)" ]]; then
|
||||
echo "A newer v3-beta commit exists; its Checks run will publish instead."
|
||||
echo "release=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "release=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup Bun
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Install npm with trusted publishing support
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
run: npm install --global npm@12.0.1
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Get Playwright version
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
id: beta-pw-version
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.beta-pw-version.outputs.version }}
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
run: bunx playwright install --with-deps chromium
|
||||
|
||||
- name: Publish tested source to the beta channel
|
||||
if: steps.candidate.outputs.release == 'true'
|
||||
env:
|
||||
BETA_VERSION: 3.0.0-beta.${{ github.run_number }}.${{ github.run_attempt }}
|
||||
run: >-
|
||||
bun scripts/release.ts
|
||||
--beta
|
||||
--version "$BETA_VERSION"
|
||||
--ci
|
||||
--dirty
|
||||
--skip-finish
|
||||
--skip-tests
|
||||
--skip-native-tests
|
||||
--skip-checks
|
||||
|
||||
ensure-ci:
|
||||
name: Approve main release source
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
steps:
|
||||
# a canary is a throwaway version on its own dist-tag that nothing installs
|
||||
# unless asked for by name, so it is cuttable from any branch and does not
|
||||
# wait on a full CI run. requiring current main meant merging a change just
|
||||
# to test it. a stable release still needs both.
|
||||
- name: Require current main and successful full CI
|
||||
if: inputs.release != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_SHA: ${{ github.sha }}
|
||||
WORKFLOW_FILE: checks.yaml
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$GITHUB_REF" == "refs/heads/main" ]]
|
||||
|
||||
main_sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)
|
||||
[[ "$TARGET_SHA" == "$main_sha" ]]
|
||||
|
||||
get_runs() {
|
||||
gh api --method GET "repos/$GITHUB_REPOSITORY/actions/workflows/$WORKFLOW_FILE/runs" \
|
||||
-f head_sha="$TARGET_SHA" -f per_page=20
|
||||
}
|
||||
|
||||
wait_for_run() {
|
||||
local run_id="$1"
|
||||
for ((attempt = 1; attempt <= 40; attempt++)); do
|
||||
read -r status conclusion < <(
|
||||
gh api "repos/$GITHUB_REPOSITORY/actions/runs/$run_id" \
|
||||
--jq '[.status, (.conclusion // "pending")] | @tsv'
|
||||
)
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
[[ "$conclusion" == "success" ]]
|
||||
return
|
||||
fi
|
||||
sleep 120
|
||||
done
|
||||
echo "Timed out waiting for CI run $run_id" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
runs=$(get_runs)
|
||||
run_id=$(jq -r '.workflow_runs | sort_by(.created_at) | last | .id // empty' <<< "$runs")
|
||||
if [[ -n "$run_id" ]]; then
|
||||
status=$(jq -r --argjson id "$run_id" '.workflow_runs[] | select(.id == $id) | .status' <<< "$runs")
|
||||
conclusion=$(jq -r --argjson id "$run_id" '.workflow_runs[] | select(.id == $id) | .conclusion // "pending"' <<< "$runs")
|
||||
if [[ "$status" == "completed" && "$conclusion" == "success" ]]; then
|
||||
echo "Full CI already passed for $TARGET_SHA."
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$status" != "completed" ]] && wait_for_run "$run_id"; then
|
||||
echo "Existing full CI passed for $TARGET_SHA."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
started=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
gh api --method POST \
|
||||
"repos/$GITHUB_REPOSITORY/actions/workflows/$WORKFLOW_FILE/dispatches" \
|
||||
-f ref=main
|
||||
|
||||
run_id=''
|
||||
for ((attempt = 1; attempt <= 12; attempt++)); do
|
||||
sleep 10
|
||||
runs=$(get_runs)
|
||||
run_id=$(jq -r --arg started "$started" \
|
||||
'[.workflow_runs[] | select(.event == "workflow_dispatch" and .created_at >= $started)] | sort_by(.created_at) | last | .id // empty' \
|
||||
<<< "$runs")
|
||||
[[ -n "$run_id" ]] && break
|
||||
done
|
||||
[[ -n "$run_id" ]]
|
||||
wait_for_run "$run_id"
|
||||
|
||||
publish:
|
||||
name: Publish approved main release
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
needs: ensure-ci
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout release source
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate release source and select baseline
|
||||
id: source
|
||||
env:
|
||||
RELEASE_KIND: ${{ inputs.release }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
# baseline selection below is needed for every release kind; only the
|
||||
# must-be-current-main assertion is stable-only (see ensure-ci).
|
||||
if [[ "$RELEASE_KIND" != 'canary' ]]; then
|
||||
[[ "$GITHUB_REF" == "refs/heads/main" ]]
|
||||
[[ "$(git rev-parse HEAD)" == "$(git rev-parse origin/main)" ]]
|
||||
fi
|
||||
|
||||
tag_ref=$(git describe --tags --match 'v*' --abbrev=0 2>/dev/null || true)
|
||||
canary_ref=$(git log --grep='^canary' --format='%H' -1)
|
||||
if [[ -z "$tag_ref" ]]; then
|
||||
base_ref="$canary_ref"
|
||||
elif [[ -z "$canary_ref" ]]; then
|
||||
base_ref="$tag_ref"
|
||||
elif (( $(git log -1 --format=%ct "$canary_ref") > $(git log -1 --format=%ct "$tag_ref") )); then
|
||||
base_ref="$canary_ref"
|
||||
else
|
||||
base_ref="$tag_ref"
|
||||
fi
|
||||
[[ -n "$base_ref" ]]
|
||||
echo "base-ref=$base_ref" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
with:
|
||||
node-version: '24'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Install npm with trusted publishing support
|
||||
run: npm install --global npm@12.0.1
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Prepare release commit
|
||||
env:
|
||||
RELEASE_KIND: ${{ inputs.release }}
|
||||
RELEASE_BASE_REF: ${{ steps.source.outputs.base-ref }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name 'Gui Release'
|
||||
git config user.email 'hanzogui@users.noreply.github.com'
|
||||
bun scripts/release.ts "--$RELEASE_KIND" --ci --dirty --skip-publish --skip-push --skip-tests --skip-native-tests --skip-checks
|
||||
|
||||
# a canary mutates no git at all, matching orez and takeout. pushing a
|
||||
# throwaway version commit to main is what wedged the one repo: main
|
||||
# requires a pull request and a merge queue there, so the push is rejected
|
||||
# with GH006 and the publish step below never runs. the bump only has to
|
||||
# exist in the tree for npm.
|
||||
- name: Push release commit
|
||||
if: inputs.release != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET_SHA: ${{ github.sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --no-tags origin main
|
||||
[[ "$(git rev-parse origin/main)" == "$TARGET_SHA" ]]
|
||||
auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
|
||||
trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT
|
||||
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $auth"
|
||||
git push origin HEAD:main
|
||||
|
||||
- name: Publish prepared version
|
||||
env:
|
||||
RELEASE_BASE_REF: ${{ steps.source.outputs.base-ref }}
|
||||
run: bun scripts/release.ts --republish --ci --dirty --skip-finish
|
||||
|
||||
- name: Push stable tag
|
||||
if: inputs.release != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(node -p "require('./pkgs/ui/hanzogui/package.json').version")
|
||||
[[ "$(git rev-list -n 1 "v$version")" == "$(git rev-parse HEAD)" ]]
|
||||
auth=$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')
|
||||
trap 'git config --local --unset-all http.https://github.com/.extraheader || true' EXIT
|
||||
git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $auth"
|
||||
git push origin "v$version"
|
||||
gh release create "v$version" --verify-tag --generate-notes --title "v$version"
|
||||
@@ -7,11 +7,8 @@ on:
|
||||
branches: [main, 'v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
test_container_app_path: apps/kitchen-sink-go
|
||||
test_container_app_path: code/kitchen-sink-go
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -29,7 +26,7 @@ jobs:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -81,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Start Metro Bundler
|
||||
run: |
|
||||
EXPO_NO_TELEMETRY=true GUI_PACKAGE_EXPORTS=${{ matrix.package-exports }} bunx expo start --offline &
|
||||
EXPO_NO_TELEMETRY=true HANZO_GUI_PACKAGE_EXPORTS=${{ matrix.package-exports }} bunx expo start --offline &
|
||||
echo "Waiting for Metro to start..."
|
||||
for i in {1..30}; do
|
||||
if curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios" > /dev/null 2>&1; then
|
||||
@@ -115,17 +112,25 @@ jobs:
|
||||
done
|
||||
xcrun simctl list devices booted
|
||||
|
||||
# per-flow retry (shared with test-ios-native): a flake in one flow is
|
||||
# retried in isolation instead of re-running the whole suite. runs Expo
|
||||
# Go (host.exp.Exponent), so terminate that between attempts.
|
||||
- name: Run Maestro Tests
|
||||
env:
|
||||
BUNDLE_ID: host.exp.Exponent
|
||||
run: bash "${{ github.workspace }}/.github/scripts/run-maestro-flows.sh"
|
||||
run: |
|
||||
for attempt in 1 2; do
|
||||
echo "Test attempt $attempt of 2..."
|
||||
if maestro test ./flows --exclude-tags=util --no-ansi; then
|
||||
echo "Tests passed!"
|
||||
exit 0
|
||||
fi
|
||||
if [ $attempt -lt 2 ]; then
|
||||
echo "Attempt $attempt failed, retrying..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "All attempts failed"
|
||||
exit 1
|
||||
|
||||
- name: Upload Maestro Artifacts on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: maestro-artifacts-exports-${{ matrix.package-exports }}
|
||||
path: |
|
||||
|
||||
@@ -7,21 +7,12 @@ on:
|
||||
branches: [main, 'v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# required: this workflow calls the reusable build-ios-kitchensink-app.yml,
|
||||
# whose build-ios job declares pull-requests: read (caller must grant >= callee)
|
||||
pull-requests: read
|
||||
|
||||
env:
|
||||
test_container_app_path: apps/kitchen-sink
|
||||
MAESTRO_DRIVER_STARTUP_TIMEOUT: 360000
|
||||
test_container_app_path: code/kitchen-sink
|
||||
MAESTRO_DRIVER_STARTUP_TIMEOUT: 120000
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# don't forward secrets (KV_STORE_REDIS_REST_*) to fork PR builds running on macOS runners.
|
||||
# fork PRs skip the native build entirely; same-repo refs still build with secrets.
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
uses: ./.github/workflows/build-ios-kitchensink-app.yml
|
||||
with:
|
||||
configuration: Debug
|
||||
@@ -30,7 +21,6 @@ jobs:
|
||||
test:
|
||||
name: Run Maestro Tests (exports=${{ matrix.package-exports }})
|
||||
needs: build
|
||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
@@ -42,7 +32,7 @@ jobs:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -51,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Restore Built App from Cache
|
||||
id: restore-app-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: ${{ needs.build.outputs.built-app-cache-key }}
|
||||
path: ${{ needs.build.outputs.built-app-path }}
|
||||
@@ -83,11 +73,9 @@ jobs:
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# start Metro in background, capture output for debugging
|
||||
# start Metro in background while simulator boots
|
||||
cd "${{ env.test_container_app_path }}"
|
||||
EXPO_NO_TELEMETRY=true GUI_PACKAGE_EXPORTS=${{ matrix.package-exports }} bunx expo start --dev-client --offline --reset-cache > /tmp/metro.log 2>&1 &
|
||||
METRO_PID=$!
|
||||
echo "$METRO_PID" > /tmp/metro-pid
|
||||
EXPO_NO_TELEMETRY=true HANZO_GUI_PACKAGE_EXPORTS=${{ matrix.package-exports }} bunx expo start --dev-client --offline --reset-cache &
|
||||
cd "${{ github.workspace }}"
|
||||
|
||||
# boot simulator
|
||||
@@ -100,49 +88,18 @@ jobs:
|
||||
xcrun simctl boot "$DEVICE_ID" || true
|
||||
xcrun simctl bootstatus "$DEVICE_ID" -b
|
||||
|
||||
# install app and verify FrontBoard registration
|
||||
APP_PATH="${{ needs.build.outputs.built-app-path }}"
|
||||
BUNDLE_ID="com.hanzogui.hanzoguikitchensink"
|
||||
for attempt in 1 2 3; do
|
||||
xcrun simctl install booted "$APP_PATH"
|
||||
sleep 3
|
||||
if xcrun simctl get_app_container booted "$BUNDLE_ID" 2>/dev/null; then
|
||||
echo "App installed and registered (attempt $attempt)"
|
||||
break
|
||||
fi
|
||||
echo "::warning::App not registered after install (attempt $attempt)"
|
||||
xcrun simctl uninstall booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 3
|
||||
if [ $attempt -eq 3 ]; then
|
||||
echo "::error::App failed to register after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# install app while waiting for Metro
|
||||
xcrun simctl install booted "${{ needs.build.outputs.built-app-path }}"
|
||||
|
||||
# wait for Metro to be ready, fail if it never responds
|
||||
# wait for Metro to be ready
|
||||
echo "Waiting for Metro..."
|
||||
METRO_READY=false
|
||||
for i in {1..90}; do
|
||||
# check Metro process is still alive
|
||||
if ! kill -0 "$METRO_PID" 2>/dev/null; then
|
||||
echo "::error::Metro process died unexpectedly"
|
||||
echo "=== Metro output ==="
|
||||
cat /tmp/metro.log
|
||||
exit 1
|
||||
fi
|
||||
for i in {1..60}; do
|
||||
if curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios" > /dev/null 2>&1; then
|
||||
echo "Metro is responding after ~$((i*2))s"
|
||||
METRO_READY=true
|
||||
echo "Metro is responding!"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$METRO_READY" != "true" ]; then
|
||||
echo "::error::Metro failed to respond within 180s"
|
||||
echo "=== Metro output ==="
|
||||
cat /tmp/metro.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Pre-warm Bundle
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
@@ -150,24 +107,15 @@ jobs:
|
||||
echo "Pre-warming JS bundle (Metro build + cache)..."
|
||||
MANIFEST=$(curl -s "http://127.0.0.1:8081/" -H "Expo-Platform: ios")
|
||||
BUNDLE_URL=$(echo "$MANIFEST" | jq -r '.launchAsset.url')
|
||||
if [ -z "$BUNDLE_URL" ] || [ "$BUNDLE_URL" = "null" ]; then
|
||||
echo "::error::Failed to get bundle URL from manifest"
|
||||
echo "Manifest response: $MANIFEST"
|
||||
exit 1
|
||||
fi
|
||||
echo "Fetching bundle from: $BUNDLE_URL"
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 900 "$BUNDLE_URL")
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "::error::Bundle fetch failed with HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
echo "Bundle pre-warm complete (HTTP $HTTP_CODE)"
|
||||
curl -s --max-time 900 "$BUNDLE_URL" > /dev/null 2>&1 || echo "Bundle fetch completed (or timed out)"
|
||||
echo "Bundle pre-warm complete"
|
||||
|
||||
- name: Start Simulator Log Collection
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: |
|
||||
# capture simulator device logs in background for debugging app crashes
|
||||
xcrun simctl spawn booted log stream --predicate 'process == "hanzoguikitchensink" OR subsystem == "com.hanzogui.hanzoguikitchensink"' --level debug > /tmp/simulator-app.log 2>&1 &
|
||||
xcrun simctl spawn booted log stream --predicate 'process == "guikitchensink" OR subsystem == "com.gui.guikitchensink"' --level debug > /tmp/simulator-app.log 2>&1 &
|
||||
echo $! > /tmp/simlog-pid
|
||||
# also capture broader crash/error logs
|
||||
xcrun simctl spawn booted log stream --predicate 'eventMessage CONTAINS "crash" OR eventMessage CONTAINS "fatal" OR eventMessage CONTAINS "error" OR process == "ReportCrash"' --level error > /tmp/simulator-errors.log 2>&1 &
|
||||
@@ -179,27 +127,27 @@ jobs:
|
||||
# WarmUp.yaml does clearState:true for a clean first launch.
|
||||
# Subsequent test flows use OpenApp.yaml (no clearState) so Hermes
|
||||
# bytecode cache persists across launches = much faster restarts.
|
||||
# if WarmUp fails, the Maestro xcuitest driver (port 7001) is usually
|
||||
# dead — every subsequent flow will fail with "Failed to connect to
|
||||
# /127.0.0.1:7001", so fail fast rather than burn 15+ min on doomed retries.
|
||||
if ! maestro test ./flows/WarmUp.yaml --no-ansi; then
|
||||
echo "::error::WarmUp failed — Maestro driver is likely dead, aborting"
|
||||
echo "=== Recent app logs ==="
|
||||
tail -50 /tmp/simulator-app.log 2>/dev/null || echo "No app logs"
|
||||
echo "=== Metro output ==="
|
||||
tail -50 /tmp/metro.log 2>/dev/null || echo "No metro logs"
|
||||
exit 1
|
||||
fi
|
||||
maestro test ./flows/WarmUp.yaml --no-ansi || true
|
||||
|
||||
# run each flow independently and retry ONLY the flow that flakes, so a
|
||||
# transient flake in one flow doesn't discard the passes of the others.
|
||||
# the OpenApp/WarmUp util sub-flows are skipped (WarmUp ran in the step
|
||||
# above; OpenApp is invoked via runFlow by every component flow).
|
||||
- name: Run Maestro Tests
|
||||
if: env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: bash "${{ github.workspace }}/.github/scripts/run-maestro-flows.sh"
|
||||
run: |
|
||||
# Run tests with one retry for flaky simulator detection
|
||||
for attempt in 1 2; do
|
||||
echo "Test attempt $attempt of 2..."
|
||||
if maestro test ./flows --exclude-tags=util --no-ansi; then
|
||||
echo "Tests passed!"
|
||||
exit 0
|
||||
fi
|
||||
if [ $attempt -lt 2 ]; then
|
||||
echo "Attempt $attempt failed, retrying..."
|
||||
sleep 10
|
||||
fi
|
||||
done
|
||||
echo "All attempts failed"
|
||||
exit 1
|
||||
|
||||
- name: Dump Logs on Failure
|
||||
- name: Dump App Logs on Failure
|
||||
if: failure() && env.SKIP_MAESTRO_TESTS != 'true'
|
||||
run: |
|
||||
# stop log collection
|
||||
@@ -211,11 +159,12 @@ jobs:
|
||||
echo "::group::Simulator Error Logs (last 100 lines)"
|
||||
tail -100 /tmp/simulator-errors.log 2>/dev/null || echo "No error logs captured"
|
||||
echo "::endgroup::"
|
||||
echo "::group::Metro Bundler Output (last 100 lines)"
|
||||
tail -100 /tmp/metro.log 2>/dev/null || echo "No metro logs captured"
|
||||
echo "::group::Metro Bundler Output"
|
||||
# show any metro errors from the background process
|
||||
ps aux | grep -i metro | grep -v grep || echo "Metro process not found"
|
||||
echo "::endgroup::"
|
||||
echo "::group::Crash Reports"
|
||||
find ~/Library/Logs/DiagnosticReports -name "*hanzoguikitchensink*" -newer /tmp/simulator-app.log 2>/dev/null | while read f; do
|
||||
find ~/Library/Logs/DiagnosticReports -name "*guikitchensink*" -newer /tmp/simulator-app.log 2>/dev/null | while read f; do
|
||||
echo "=== $f ==="
|
||||
head -50 "$f"
|
||||
done || echo "No crash reports found"
|
||||
@@ -228,11 +177,10 @@ jobs:
|
||||
|
||||
- name: Upload Maestro Artifacts on Failure
|
||||
if: failure() && env.SKIP_MAESTRO_TESTS != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: maestro-artifacts-exports-${{ matrix.package-exports }}
|
||||
path: |
|
||||
~/.maestro/tests/**/*
|
||||
/tmp/simulator-app.log
|
||||
/tmp/simulator-errors.log
|
||||
/tmp/metro.log
|
||||
|
||||
@@ -9,19 +9,13 @@ on:
|
||||
- rn82
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# required: this workflow calls the reusable build-ios-kitchensink-app.yml,
|
||||
# whose build-ios job declares pull-requests: read (caller must grant >= callee)
|
||||
pull-requests: read
|
||||
|
||||
# cancel in-progress runs on feature branches, but let main finish for full history
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
env:
|
||||
test_container_app_path: apps/kitchen-sink
|
||||
test_container_app_path: code/kitchen-sink
|
||||
# Increment this to force a fresh Android build (invalidates Redis KV cache)
|
||||
android_cache_version: v4
|
||||
|
||||
@@ -30,18 +24,6 @@ jobs:
|
||||
# iOS Build & Test
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
validate-ios-detox-shards:
|
||||
name: Validate iOS Detox Shards
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Validate shard coverage
|
||||
run: node .github/scripts/check-ios-detox-shards.mjs
|
||||
|
||||
build-ios:
|
||||
name: Build iOS App
|
||||
uses: ./.github/workflows/build-ios-kitchensink-app.yml
|
||||
@@ -51,40 +33,23 @@ jobs:
|
||||
|
||||
test-ios:
|
||||
name: iOS Detox Tests (${{ matrix.shard_name }})
|
||||
needs:
|
||||
- build-ios
|
||||
- validate-ios-detox-shards
|
||||
needs: build-ios
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# 4 shards balanced by test execution time. the old ~7min/shard ts-jest
|
||||
# type-check tax is gone (e2e now transpiles via e2e/tsconfig.json), so
|
||||
# startup is small and these fit well under the native-ci runner's
|
||||
# 45-minute process timeout. CompilerExtraction runs an in-test
|
||||
# `npx hanzogui build` (~12-19min, variable) so it gets its OWN shard
|
||||
# (with the iOS-skipped SafeArea/SheetKeyboardDrag, which cost ~0, plus
|
||||
# the small iOS-running SheetFitKeyboardSafeArea, ~1 launch); the
|
||||
# remaining files are bin-packed to ~14.5min test-exec each (~25min wall).
|
||||
# SelectAndroidOnPress is android-only (excluded here via
|
||||
# check-ios-detox-shards.mjs, still runs in the android job). Coverage
|
||||
# validated by that script (every e2e test assigned exactly once).
|
||||
- shard_name: '1/4'
|
||||
test_files: 'e2e/Accordion.test.ts e2e/CompilerExtraction.test.ts e2e/SafeArea.test.ts e2e/SheetKeyboardDrag.test.ts e2e/SheetFitKeyboardSafeArea.test.ts e2e/SheetPressResponderSheetRngh.test.ts'
|
||||
- shard_name: '2/4'
|
||||
test_files: 'e2e/PressStyleNative.noRngh.test.ts e2e/CompilerTernaryActive.test.ts e2e/TabsOnInteraction.test.ts e2e/PointerEvents.test.ts e2e/GroupPressNative.test.ts e2e/ShorthandVariables.test.ts e2e/check-rngh-status.test.ts'
|
||||
- shard_name: '3/4'
|
||||
test_files: 'e2e/SheetScrollableDrag.test.ts e2e/ThemeMutation.test.ts e2e/NativePortal.test.ts e2e/GroupPressTransitionMatrix.test.ts e2e/MenuRadioGroup.test.ts e2e/PressStyleScrollStuck.test.ts e2e/NativeMixedDriver.test.ts'
|
||||
- shard_name: '4/4'
|
||||
test_files: 'e2e/PressStyleNative.test.ts e2e/SheetPressRegression.test.ts e2e/SheetKeyboardFitContent.test.ts e2e/SelectRemount.test.ts e2e/SheetDragResist.test.ts e2e/ThemeChangeBasic.test.ts e2e/NestedPressExclusive.test.ts e2e/MediaQueryGtMd.test.ts'
|
||||
- shard_name: '1/2'
|
||||
test_files: 'e2e/check-rngh-status.test.ts e2e/GroupPressNative.test.ts e2e/MediaQueryGtMd.test.ts e2e/NativePortal.test.ts e2e/PointerEvents.test.ts e2e/PressStyleNative.noRngh.test.ts e2e/PressStyleNative.test.ts e2e/SafeArea.test.ts'
|
||||
- shard_name: '2/2'
|
||||
test_files: 'e2e/CompilerExtraction.test.ts e2e/SelectAndroidOnPress.test.ts e2e/SelectRemount.test.ts e2e/SheetDragResist.test.ts e2e/SheetKeyboardDrag.test.ts e2e/SheetScrollableDrag.test.ts e2e/ShorthandVariables.test.ts e2e/ThemeChangeBasic.test.ts e2e/ThemeMutation.test.ts'
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -93,7 +58,7 @@ jobs:
|
||||
|
||||
- name: Restore Built App from Cache
|
||||
id: restore-ios-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: ${{ needs.build-ios.outputs.built-app-cache-key }}
|
||||
path: ${{ needs.build-ios.outputs.built-app-path }}
|
||||
@@ -143,22 +108,8 @@ jobs:
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
run: |
|
||||
npm install -g detox-cli &
|
||||
detox_pid=$!
|
||||
|
||||
# install by fully-qualified name so homebrew auto-trusts only this
|
||||
# formula (homebrew 6 refuses untrusted third-party taps otherwise)
|
||||
brew install wix/brew/applesimutils &
|
||||
applesimutils_pid=$!
|
||||
|
||||
wait "$detox_pid"
|
||||
wait "$applesimutils_pid"
|
||||
|
||||
brew_bin="$(brew --prefix)/bin"
|
||||
echo "$brew_bin" >> "$GITHUB_PATH"
|
||||
export PATH="$brew_bin:$PATH"
|
||||
|
||||
command -v detox
|
||||
command -v applesimutils
|
||||
(brew tap wix/brew && brew install applesimutils) &
|
||||
wait
|
||||
|
||||
- name: Rebuild Detox Framework Cache
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
@@ -188,64 +139,24 @@ jobs:
|
||||
run: |
|
||||
xcrun simctl ui booted appearance light
|
||||
|
||||
# wait for SpringBoard to be fully ready (FrontBoard app registration depends on it)
|
||||
echo "Waiting for SpringBoard readiness..."
|
||||
for i in {1..30}; do
|
||||
if xcrun simctl spawn booted launchctl list 2>/dev/null | grep -q SpringBoard; then
|
||||
echo "SpringBoard ready after ~${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Install App on Simulator
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
APP_PATH="${{ needs.build-ios.outputs.built-app-path }}"
|
||||
BUNDLE_ID="com.hanzogui.hanzoguikitchensink"
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
echo "Install attempt $attempt/3..."
|
||||
xcrun simctl install booted "$APP_PATH"
|
||||
sleep 3
|
||||
|
||||
# verify FrontBoard registered the app
|
||||
if xcrun simctl get_app_container booted "$BUNDLE_ID" 2>/dev/null; then
|
||||
echo "App installed and registered on attempt $attempt"
|
||||
|
||||
# launch once to prime FrontBoard, then terminate
|
||||
xcrun simctl launch booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 2
|
||||
xcrun simctl terminate booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
break
|
||||
fi
|
||||
|
||||
echo "::warning::App not registered with FrontBoard after install (attempt $attempt)"
|
||||
if [ $attempt -lt 3 ]; then
|
||||
xcrun simctl uninstall booted "$BUNDLE_ID" 2>/dev/null || true
|
||||
sleep 5
|
||||
else
|
||||
echo "::error::App failed to register after 3 install attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
xcrun simctl install booted "${{ needs.build-ios.outputs.built-app-path }}"
|
||||
|
||||
- name: Setup Bun
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Run iOS Detox Tests
|
||||
if: env.SKIP_IOS_TESTS != 'true'
|
||||
env:
|
||||
DETOX_IOS_APP_PATH: build/Build/Products/Debug-iphonesimulator/hanzoguikitchensink.app
|
||||
DETOX_IOS_APP_PATH: build/Build/Products/Debug-iphonesimulator/guikitchensink.app
|
||||
run: |
|
||||
# --retries 0: no whole-file retry (that re-ran beforeAll + every test of a
|
||||
# flaky file, the 2x wall-time variance). individual flaky tests retry
|
||||
# in-place via jest.retryTimes (e2e/jest.setup.ts).
|
||||
bun run ../native-ci/src/run-detox-ios.ts --project-root "$PWD" --record-logs failing --retries 0 ${{ matrix.test_files }}
|
||||
bun run ../packages/native-ci/src/run-detox-ios.ts --project-root "$PWD" --record-logs failing --retries 1 ${{ matrix.test_files }}
|
||||
|
||||
- name: Report iOS Test Status
|
||||
if: always() && env.SKIP_IOS_TESTS == 'true'
|
||||
@@ -254,7 +165,7 @@ jobs:
|
||||
|
||||
- name: Upload Detox Artifacts on Failure
|
||||
if: failure() && env.SKIP_IOS_TESTS != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: detox-artifacts-ios-${{ strategy.job-index }}
|
||||
path: ${{ env.test_container_app_path }}/e2e/artifacts/
|
||||
@@ -268,11 +179,11 @@ jobs:
|
||||
name: Build Android App
|
||||
# Only run on main/v* branches (not PRs)
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/rn82'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
timeout-minutes: 45
|
||||
outputs:
|
||||
android-cache-key: ${{ steps.final-cache-key.outputs.cache_key }}
|
||||
android-path: apps/kitchen-sink/android
|
||||
android-path: code/kitchen-sink/android
|
||||
fingerprint: ${{ steps.calculate-fingerprint.outputs.fingerprint || steps.get-fingerprint-from-cache.outputs.fingerprint }}
|
||||
steps:
|
||||
- name: Free Disk Space
|
||||
@@ -286,7 +197,7 @@ jobs:
|
||||
df -h /
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -294,7 +205,7 @@ jobs:
|
||||
id: calculate-pre-fingerprint
|
||||
env:
|
||||
# Include cache version to allow manual cache busting
|
||||
PRE_FINGERPRINT_HASH: ${{ hashFiles('bun.lock', 'apps/kitchen-sink/app.json', 'packages/vxrn/expo-plugin.cjs') }}-${{ env.android_cache_version }}
|
||||
PRE_FINGERPRINT_HASH: ${{ hashFiles('bun.lock', 'code/kitchen-sink/app.json', 'packages/vxrn/expo-plugin.cjs') }}-${{ env.android_cache_version }}
|
||||
run: |
|
||||
echo "Pre-fingerprint hash: $PRE_FINGERPRINT_HASH"
|
||||
echo "pre_fingerprint_hash=$PRE_FINGERPRINT_HASH" >> $GITHUB_OUTPUT
|
||||
@@ -305,10 +216,10 @@ jobs:
|
||||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
||||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
||||
run: |
|
||||
FINGERPRINT_FROM_CACHE=$(curl -s "$KV_STORE_REDIS_REST_URL/get/android-hanzoguikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" | jq -r '.result')
|
||||
FINGERPRINT_FROM_CACHE=$(curl -s "$KV_STORE_REDIS_REST_URL/get/android-guikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" | jq -r '.result')
|
||||
|
||||
if [ "$FINGERPRINT_FROM_CACHE" != "null" ]; then
|
||||
curl -s -X POST "$KV_STORE_REDIS_REST_URL/EXPIRE/android-hanzoguikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
curl -s -X POST "$KV_STORE_REDIS_REST_URL/EXPIRE/android-guikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
echo "Fingerprint from cache: $FINGERPRINT_FROM_CACHE"
|
||||
echo "fingerprint=$FINGERPRINT_FROM_CACHE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -319,9 +230,9 @@ jobs:
|
||||
- name: Check if Android folder is already cached
|
||||
id: android-cache-check
|
||||
if: ${{ steps.get-fingerprint-from-cache.outputs.fingerprint != 'null' }}
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: apps/kitchen-sink/android
|
||||
path: code/kitchen-sink/android
|
||||
key: android-detox-${{ env.android_cache_version }}-${{ steps.get-fingerprint-from-cache.outputs.fingerprint }}
|
||||
lookup-only: true
|
||||
|
||||
@@ -340,17 +251,17 @@ jobs:
|
||||
|
||||
- name: Setup Android SDK
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Prebuild Android
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
working-directory: apps/kitchen-sink
|
||||
working-directory: code/kitchen-sink
|
||||
run: |
|
||||
bunx expo prebuild --platform android --clean
|
||||
|
||||
- name: Setup Detox Test Infrastructure
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
working-directory: apps/kitchen-sink
|
||||
working-directory: code/kitchen-sink
|
||||
run: |
|
||||
# Create Gradle init script for JNI library conflicts
|
||||
cat > android/init.gradle << 'INITGRADLE'
|
||||
@@ -404,11 +315,11 @@ jobs:
|
||||
"
|
||||
|
||||
# Create androidTest directory structure
|
||||
mkdir -p android/app/src/androidTest/java/com/hanzogui/hanzoguikitchensink
|
||||
mkdir -p android/app/src/androidTest/java/com/hanzoai/guikitchensink
|
||||
|
||||
# Create DetoxTest.java
|
||||
cat > android/app/src/androidTest/java/com/hanzogui/hanzoguikitchensink/DetoxTest.java << 'DETOXTEST'
|
||||
package com.hanzogui.hanzoguikitchensink;
|
||||
cat > android/app/src/androidTest/java/com/hanzoai/guikitchensink/DetoxTest.java << 'DETOXTEST'
|
||||
package com.gui.guikitchensink;
|
||||
|
||||
import com.wix.detox.Detox;
|
||||
import com.wix.detox.config.DetoxConfig;
|
||||
@@ -467,7 +378,7 @@ jobs:
|
||||
- name: Calculate Fingerprint
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
id: calculate-fingerprint
|
||||
working-directory: apps/kitchen-sink
|
||||
working-directory: code/kitchen-sink
|
||||
run: |
|
||||
FINGERPRINT=$(bunx @expo/fingerprint fingerprint:generate --platform android | jq -r '.hash')
|
||||
if [ -z "$FINGERPRINT" ]; then
|
||||
@@ -483,20 +394,20 @@ jobs:
|
||||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }}
|
||||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }}
|
||||
run: |
|
||||
curl -s -X POST "$KV_STORE_REDIS_REST_URL/SETEX/android-hanzoguikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000/${{ steps.calculate-fingerprint.outputs.fingerprint }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
curl -s -X POST "$KV_STORE_REDIS_REST_URL/SETEX/android-guikitchensink-fingerprint-from-pre-hash-${{ steps.calculate-pre-fingerprint.outputs.pre_fingerprint_hash }}/2592000/${{ steps.calculate-fingerprint.outputs.fingerprint }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN"
|
||||
|
||||
- name: Check If Build Already Exists (with new fingerprint)
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit }}
|
||||
id: android-cache-check-new
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: apps/kitchen-sink/android
|
||||
path: code/kitchen-sink/android
|
||||
key: android-detox-${{ env.android_cache_version }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}
|
||||
lookup-only: true
|
||||
|
||||
- name: Cache Gradle
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit && !steps.android-cache-check-new.outputs.cache-hit }}
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -507,7 +418,7 @@ jobs:
|
||||
|
||||
- name: Build Android App (Debug + Test APKs)
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit && !steps.android-cache-check-new.outputs.cache-hit }}
|
||||
working-directory: apps/kitchen-sink
|
||||
working-directory: code/kitchen-sink
|
||||
env:
|
||||
# Limit Gradle memory to avoid OOM on free runners
|
||||
GRADLE_OPTS: '-Dorg.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError'
|
||||
@@ -516,9 +427,9 @@ jobs:
|
||||
|
||||
- name: Cache Android Folder
|
||||
if: ${{ !steps.android-cache-check.outputs.cache-hit && !steps.android-cache-check-new.outputs.cache-hit }}
|
||||
uses: actions/cache/save@v4
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: apps/kitchen-sink/android
|
||||
path: code/kitchen-sink/android
|
||||
key: android-detox-${{ env.android_cache_version }}-${{ steps.calculate-fingerprint.outputs.fingerprint }}
|
||||
|
||||
- name: Set Final Cache Key
|
||||
@@ -535,16 +446,22 @@ jobs:
|
||||
needs: build-android
|
||||
# Only run on main/v* branches (not PRs)
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/rn82'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
timeout-minutes: 45
|
||||
# Android emulator in CI is flaky with window focus issues - don't block on failures
|
||||
continue-on-error: true
|
||||
# Retry matrix - run up to 2 attempts sequentially to handle flakiness
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
attempt: [1, 2]
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ${{ env.test_container_app_path }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -556,7 +473,7 @@ jobs:
|
||||
|
||||
- name: Restore Android Folder from Cache
|
||||
id: restore-android-cache
|
||||
uses: actions/cache/restore@v4
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: ${{ needs.build-android.outputs.android-cache-key }}
|
||||
path: ${{ needs.build-android.outputs.android-path }}
|
||||
@@ -567,15 +484,6 @@ jobs:
|
||||
echo "::warning::Android cache not restored (key: ${{ needs.build-android.outputs.android-cache-key }}). This may happen on first run after cache key changes."
|
||||
echo "SKIP_ANDROID_TESTS=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Free Disk Space
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android/sdk/ndk
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
df -h /
|
||||
|
||||
- name: Install Dependencies
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: ./.github/actions/install
|
||||
@@ -595,31 +503,28 @@ jobs:
|
||||
|
||||
- name: Setup Bun
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: 'package.json'
|
||||
|
||||
- name: Start Android Emulator and Run Tests
|
||||
if: env.SKIP_ANDROID_TESTS != 'true'
|
||||
id: android-tests
|
||||
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: 31
|
||||
target: google_apis
|
||||
arch: x86_64
|
||||
profile: pixel_4
|
||||
avd-name: test
|
||||
# 4G ram + 1G heap (was 2G/512M) - the kitchen-sink JS bundle eagerly imports
|
||||
# all 192 test cases, so first launch under software graphics needs more headroom
|
||||
# to let the main looper idle within AndroidX's 45s startActivitySync timeout.
|
||||
ram-size: 4096M
|
||||
heap-size: 1024M
|
||||
ram-size: 2048M
|
||||
heap-size: 512M
|
||||
disk-size: 4G
|
||||
force-avd-creation: false
|
||||
emulator-boot-timeout: 600
|
||||
disable-animations: true
|
||||
emulator-options: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none
|
||||
working-directory: apps/kitchen-sink
|
||||
emulator-options: -no-snapshot -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -camera-front none -memory 2048
|
||||
working-directory: code/kitchen-sink
|
||||
script: |
|
||||
# Free up memory before tests
|
||||
sudo swapoff -a || true
|
||||
@@ -646,8 +551,7 @@ jobs:
|
||||
|
||||
# Let Detox handle APK installation and ADB reverse setup via reversePorts config
|
||||
# Android emulator is flaky - capture exit code but don't fail the job
|
||||
# --retries 0: no whole-file retry; individual tests retry via jest.retryTimes (e2e/jest.setup.ts)
|
||||
bun run ../native-ci/src/run-detox-android.ts --headless --project-root "$PWD" --record-logs failing --retries 0 || echo "ANDROID_TESTS_FAILED=true" >> $GITHUB_ENV
|
||||
bun run ../packages/native-ci/src/run-detox-android.ts --headless --project-root "$PWD" --record-logs failing --retries 2 || echo "ANDROID_TESTS_FAILED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Report Android Test Status
|
||||
if: always()
|
||||
@@ -662,8 +566,8 @@ jobs:
|
||||
|
||||
- name: Upload Detox Artifacts on Failure
|
||||
if: env.ANDROID_TESTS_FAILED == 'true' && env.SKIP_ANDROID_TESTS != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: detox-artifacts-android
|
||||
name: detox-artifacts-android-attempt-${{ matrix.attempt }}
|
||||
path: ${{ env.test_container_app_path }}/e2e/artifacts/
|
||||
retention-days: 7
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
**/test-results/*
|
||||
**/.maestro-debug
|
||||
|
||||
# generated declarations are build artifacts: hanzogui-build emits every
|
||||
# package's into <pkg>/types/, the build fixtures emit to their package root.
|
||||
# they still ship on npm - every package lists "types" in its files[].
|
||||
# hand-authored .d.ts stay tracked (re-export shims like pkgs/core/config/v5.d.ts,
|
||||
# ambient stubs like apps/gui.hanzo.ai/types/bento.d.ts): none live under pkgs/*/types/.
|
||||
pkgs/**/types/**/*.d.ts
|
||||
pkgs/**/types/**/*.d.ts.map
|
||||
pkgs/build/__tests__/fixtures/**/*.d.ts
|
||||
pkgs/build/__tests__/fixtures/**/*.d.ts.map
|
||||
pkgs/**/vitest.config.d.ts
|
||||
pkgs/**/vitest.config.d.ts.map
|
||||
pkgs/**/*.test.d.ts
|
||||
pkgs/**/*.test.d.ts.map
|
||||
# keep types in repo to track them
|
||||
# **/types/**/*.d.ts
|
||||
# but map can be ignored and just published on npm
|
||||
**/types/**/*.d.ts.map
|
||||
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
@@ -40,31 +31,20 @@ node_modules
|
||||
|
||||
.vercel
|
||||
|
||||
apps/kitchen-sink/ios
|
||||
code/kitchen-sink/ios
|
||||
|
||||
.hanzogui
|
||||
.tamagui
|
||||
|
||||
.idea
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.test.localp
|
||||
.env.production.local
|
||||
|
||||
# key material / credentials - never commit
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.keystore
|
||||
*.jks
|
||||
*.p8
|
||||
*.mobileprovision
|
||||
**/serviceAccount*.json
|
||||
|
||||
apps/kitchen-sink/ios
|
||||
apps/kitchen-sink/android
|
||||
code/kitchen-sink/ios
|
||||
code/kitchen-sink/android
|
||||
|
||||
code/studio/types/**
|
||||
|
||||
@@ -76,7 +56,5 @@ code/studio/types/**
|
||||
.playwright-report/
|
||||
.playwright-results/
|
||||
test-results/
|
||||
|
||||
# Rust build artifacts — never commit; a debug .a is 272MB and GitHub caps at 100MB.
|
||||
apps/desktop/src-tauri/target/
|
||||
**/src-tauri/target/
|
||||
.gui/
|
||||
# Wed Mar 25 04:08:49 PDT 2026
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Build image
|
||||
|
||||
# Builds + pushes ghcr.io/hanzoai/gui (the gui.hanzo.ai site, served on
|
||||
# :3000 by the repo's existing Dockerfile) on a v* semver tag via the
|
||||
# canonical reusable workflow, natively on the self-hosted arcd fleet
|
||||
# (NO GitHub-hosted runners). Semver-only tags. Deployable on DOKS.
|
||||
#
|
||||
# This is the CONTAINER-image path. The repo's publish-gui*.yml workflows
|
||||
# publish the npm library packages and are unaffected.
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
uses: hanzoai/.github/.github/workflows/docker-build.yml@main
|
||||
with:
|
||||
image: ghcr.io/hanzoai/gui
|
||||
secrets: inherit
|
||||
@@ -1,66 +0,0 @@
|
||||
name: Checks
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
- '.vscode/**'
|
||||
branches:
|
||||
- main
|
||||
- v2
|
||||
- 'v2-*'
|
||||
|
||||
# cancel in-progress runs on the same branch (avoids duplicate PR + push runs)
|
||||
concurrency:
|
||||
group: checks-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Check
|
||||
run: bun run check
|
||||
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Get Playwright version
|
||||
id: pw-version
|
||||
run: echo "version=$(bunx playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }}
|
||||
|
||||
- name: Install playwright
|
||||
run: bunx playwright install
|
||||
|
||||
- name: Run unit tests
|
||||
run: bun turbo run test:web --filter='!@hanzogui/kitchen-sink' --concurrency=1
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Publish gui (all packages)
|
||||
|
||||
# Publishes the FULL gui surface — every @hanzogui/* primitive AND the
|
||||
# `hanzogui` + `@hanzo/gui` umbrellas — at the current on-disk version
|
||||
# (7.x), with freshly built `dist/`. This is the fix for the broken
|
||||
# 7.0.0 publish, where the primitives shipped without `dist/` and
|
||||
# `@hanzo/gui` therefore failed to resolve for standalone consumers.
|
||||
#
|
||||
# `publish-gui.yml` only ships the umbrella; this ships the primitives it
|
||||
# depends on so a clean `npm i @hanzo/gui` works end-to-end.
|
||||
#
|
||||
# Trigger: manual (workflow_dispatch, type "publish" to confirm) or by
|
||||
# pushing a `release/gui-all-v*` tag.
|
||||
#
|
||||
# NPM_TOKEN resolution mirrors publish-gui.yml:
|
||||
# 1. KMS via Universal Auth (KMS_CLIENT_ID/SECRET → short-lived token)
|
||||
# 2. KMS via long-lived HANZO_API_KEY (legacy bootstrap)
|
||||
# 3. Direct repo secret NPM_TOKEN (fallback)
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm:
|
||||
description: 'Type "publish" to publish ALL gui packages at the current version'
|
||||
required: true
|
||||
default: ''
|
||||
push:
|
||||
tags:
|
||||
- 'release/gui-all-v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish-all:
|
||||
name: Build + publish all gui packages
|
||||
# Retargeted arm64 self-hosted (spark, frequently offline) → the always-up
|
||||
# in-cluster amd64 ARC pool. @hanzogui/* are pure-JS; publishing is
|
||||
# arch-independent, so amd64 is safe and far more reliable.
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
if: github.event_name == 'push' || github.event.inputs.confirm == 'publish'
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Build all packages
|
||||
# Fresh checkout has no dist; --republish skips release.ts's own
|
||||
# build, so build here explicitly. `npm pack` (inside release.ts)
|
||||
# will fail loudly if any dist tree is missing.
|
||||
run: bun run build
|
||||
|
||||
- name: Resolve NPM_TOKEN (KMS → fallback to repo secret)
|
||||
id: token
|
||||
env:
|
||||
DIRECT_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
HANZO_API_KEY: ${{ secrets.HANZO_API_KEY }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_WORKSPACE_ID: ${{ vars.KMS_WORKSPACE_ID_GUI || 'e1359bf4-31b4-4dfa-bb90-323e2c298ad8' }}
|
||||
run: |
|
||||
set -eu
|
||||
npm_token=""
|
||||
if [ -n "${KMS_CLIENT_ID:-}" ] && [ -n "${KMS_CLIENT_SECRET:-}" ]; then
|
||||
HANZO_API_KEY=$(curl -sf "${KMS_ENDPOINT}/api/v1/auth/universal-auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken') || true
|
||||
fi
|
||||
if [ -n "${HANZO_API_KEY:-}" ]; then
|
||||
response=$(curl -sf \
|
||||
"${KMS_ENDPOINT}/api/v3/secrets/raw?workspaceId=${KMS_WORKSPACE_ID}&secretPath=/publish&environment=prod" \
|
||||
-H "Authorization: Bearer ${HANZO_API_KEY}" 2>/dev/null) || true
|
||||
if [ -n "${response:-}" ]; then
|
||||
npm_token=$(echo "$response" | jq -r '.secrets[] | select(.secretKey=="NPM_TOKEN") | .secretValue // empty')
|
||||
fi
|
||||
fi
|
||||
if [ -z "$npm_token" ]; then
|
||||
npm_token="${DIRECT_NPM_TOKEN:-}"
|
||||
if [ -n "$npm_token" ]; then
|
||||
echo "Using NPM_TOKEN from repo secret (KMS path unavailable)."
|
||||
fi
|
||||
fi
|
||||
if [ -z "$npm_token" ]; then
|
||||
echo "::error::No NPM_TOKEN available — need KMS_CLIENT_ID/SECRET, HANZO_API_KEY, or NPM_TOKEN repo secret."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${npm_token}"
|
||||
echo "npm_token=${npm_token}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure npm registry
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
run: |
|
||||
echo 'registry=https://registry.npmjs.org/' > ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc
|
||||
|
||||
- name: Publish gui surface (primitives + umbrella + @hanzo/gui alias)
|
||||
# Targeted publish of just the @hanzogui/* closure + hanzogui +
|
||||
# @hanzo/gui, at the current on-disk version. Avoids release.ts's
|
||||
# whole-workspace fan-out (which fails packing unrelated
|
||||
# unrelated workspace packages) and its fragile tarball-rename step.
|
||||
# Idempotent — skips any name@version already on npm.
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
NODE_AUTH_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
run: node ./scripts/publish-gui-surface.mjs
|
||||
@@ -1,249 +0,0 @@
|
||||
name: Publish gui
|
||||
|
||||
# Triggered only on `release/gui-v*` tags. Bumping a workspace
|
||||
# version alone does NOT publish — a tag push gates the run so an
|
||||
# accidental version bump on `main` cannot ship to npm. Tag scheme:
|
||||
# release/gui-v7.1.0 → gui@7.1.0
|
||||
#
|
||||
# Note: @hanzogui/admin moved to hanzoai/admin and publishes from there.
|
||||
#
|
||||
# NPM_TOKEN resolution order:
|
||||
# 1. KMS via Universal Auth (KMS_CLIENT_ID/SECRET → short-lived token)
|
||||
# 2. KMS via long-lived HANZO_API_KEY (legacy bootstrap)
|
||||
# 3. Direct repo secret NPM_TOKEN (fallback until KMS is provisioned)
|
||||
# Same shape as `~/work/hanzo/login/.github/workflows/deploy.yml`.
|
||||
#
|
||||
# Smoke test: after pack, install the .tgz tarball into a throwaway
|
||||
# /tmp consumer and require() it. If the static extractor or vite
|
||||
# build chokes on an import shape, this catches it before publish.
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'release/gui-v*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Build, smoke-test, publish
|
||||
# Retargeted arm64 self-hosted (spark, frequently offline) → the always-up
|
||||
# in-cluster amd64 ARC pool. Pure-JS publish is arch-independent.
|
||||
runs-on: hanzo-build-linux-amd64
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
|
||||
- name: Resolve NPM_TOKEN (KMS → fallback to repo secret)
|
||||
id: token
|
||||
env:
|
||||
DIRECT_NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
||||
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
||||
HANZO_API_KEY: ${{ secrets.HANZO_API_KEY }}
|
||||
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
|
||||
KMS_WORKSPACE_ID: ${{ vars.KMS_WORKSPACE_ID_GUI || 'e1359bf4-31b4-4dfa-bb90-323e2c298ad8' }}
|
||||
run: |
|
||||
set -eu
|
||||
# Preferred (when configured): pull NPM_TOKEN from KMS at runtime
|
||||
# via Universal Auth (KMS_CLIENT_ID/SECRET) or legacy HANZO_API_KEY.
|
||||
# Fallback: direct repo-secret NPM_TOKEN. The fallback keeps the
|
||||
# publish path unblocked while KMS provisioning catches up.
|
||||
npm_token=""
|
||||
if [ -n "${KMS_CLIENT_ID:-}" ] && [ -n "${KMS_CLIENT_SECRET:-}" ]; then
|
||||
HANZO_API_KEY=$(curl -sf "${KMS_ENDPOINT}/api/v1/auth/universal-auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
|
||||
| jq -r '.accessToken') || true
|
||||
fi
|
||||
if [ -n "${HANZO_API_KEY:-}" ]; then
|
||||
response=$(curl -sf \
|
||||
"${KMS_ENDPOINT}/api/v3/secrets/raw?workspaceId=${KMS_WORKSPACE_ID}&secretPath=/publish&environment=prod" \
|
||||
-H "Authorization: Bearer ${HANZO_API_KEY}" 2>/dev/null) || true
|
||||
if [ -n "${response:-}" ]; then
|
||||
npm_token=$(echo "$response" | jq -r '.secrets[] | select(.secretKey=="NPM_TOKEN") | .secretValue // empty')
|
||||
fi
|
||||
fi
|
||||
if [ -z "$npm_token" ]; then
|
||||
npm_token="${DIRECT_NPM_TOKEN:-}"
|
||||
if [ -n "$npm_token" ]; then
|
||||
echo "Using NPM_TOKEN from repo secret (KMS path unavailable)."
|
||||
fi
|
||||
fi
|
||||
if [ -z "$npm_token" ]; then
|
||||
echo "::error::No NPM_TOKEN available — need either KMS_CLIENT_ID/SECRET, HANZO_API_KEY, or NPM_TOKEN repo secret."
|
||||
exit 1
|
||||
fi
|
||||
echo "::add-mask::${npm_token}"
|
||||
echo "npm_token=${npm_token}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Configure npm registry
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
run: |
|
||||
echo 'registry=https://registry.npmjs.org/' > ~/.npmrc
|
||||
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc
|
||||
|
||||
- name: Build (postinstall already built js with SKIP_TYPES=1)
|
||||
# The install step ran `bun install` which fires postinstall
|
||||
# → `bun run build:js` (SKIP_TYPES=1 turbo run build --force).
|
||||
# That covers the publish target and every workspace consumer,
|
||||
# so a separate filtered build here would be a no-op rebuild.
|
||||
# Verifying the dist tree exists is enough — pack will fail
|
||||
# loudly if it doesn't.
|
||||
run: |
|
||||
test -d pkgs/ui/hanzogui/dist || (echo "::error::gui/dist missing"; exit 1)
|
||||
|
||||
- name: Build workspace version map
|
||||
# Map every workspace package name to its on-disk version so we can
|
||||
# resolve `workspace:*` deps to the actual version each subpackage is
|
||||
# at. The umbrella is on the 7.x track but the @hanzogui/* primitives
|
||||
# are still on the 102.x canary stream — they are NOT republished
|
||||
# here, only referenced.
|
||||
id: ws-versions
|
||||
run: |
|
||||
set -eu
|
||||
node -e "
|
||||
const fs=require('fs'); const path=require('path');
|
||||
const map={};
|
||||
const dirs=['pkgs/ui','pkgs/core','pkgs/compiler','pkgs'];
|
||||
const collect=(d)=>{
|
||||
if (!fs.existsSync(d)) return;
|
||||
const stat=fs.statSync(d);
|
||||
if (stat.isFile() && d.endsWith('package.json')) {
|
||||
try { const j=JSON.parse(fs.readFileSync(d)); if (j.name && j.version) map[j.name]=j.version; } catch{}
|
||||
return;
|
||||
}
|
||||
if (!stat.isDirectory()) return;
|
||||
const pkg=path.join(d,'package.json');
|
||||
if (fs.existsSync(pkg)) collect(pkg);
|
||||
for (const e of fs.readdirSync(d)) {
|
||||
if (e==='node_modules') continue;
|
||||
collect(path.join(d,e));
|
||||
}
|
||||
};
|
||||
for (const d of dirs) collect(d);
|
||||
fs.writeFileSync('/tmp/ws-versions.json', JSON.stringify(map));
|
||||
console.log('mapped', Object.keys(map).length, 'workspace packages');
|
||||
"
|
||||
|
||||
- name: Pack gui
|
||||
# `npm pack` does NOT resolve `workspace:*` specifiers; the resulting
|
||||
# tarball would ship literal `workspace:*` and break consumers. We
|
||||
# mirror what `scripts/release.ts` does — copy to a temp dir, rewrite
|
||||
# every `workspace:*` to the matching package's on-disk version (from
|
||||
# the workspace map built above), then pack.
|
||||
id: pack-gui
|
||||
run: |
|
||||
set -eu
|
||||
tmpdir=$(mktemp -d)
|
||||
cp -R pkgs/ui/hanzogui/. "$tmpdir/"
|
||||
rm -rf "$tmpdir/node_modules"
|
||||
node -e "
|
||||
const fs=require('fs');
|
||||
const map=JSON.parse(fs.readFileSync('/tmp/ws-versions.json'));
|
||||
const p='$tmpdir/package.json';
|
||||
const j=JSON.parse(fs.readFileSync(p));
|
||||
const ownVer=j.version;
|
||||
for (const f of ['dependencies','devDependencies','peerDependencies','optionalDependencies']) {
|
||||
if (!j[f]) continue;
|
||||
for (const k of Object.keys(j[f])) {
|
||||
if (j[f][k].startsWith('workspace:')) {
|
||||
const v=map[k]; if (!v) { console.error('no version for',k); process.exit(1); }
|
||||
j[f][k]=v;
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(p, JSON.stringify(j, null, 2)+'\n');
|
||||
"
|
||||
cd "$tmpdir"
|
||||
tgz=$(npm pack --silent | tail -1)
|
||||
echo "tgz=$tmpdir/${tgz}" >> "$GITHUB_OUTPUT"
|
||||
echo "tmpdir=$tmpdir" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Smoke-test packed tarball (metadata-only)
|
||||
env:
|
||||
GUI_TGZ: ${{ steps.pack-gui.outputs.tgz }}
|
||||
run: |
|
||||
set -eu
|
||||
# Metadata-only smoke: extract package.json + key dist files from
|
||||
# the tarball without npm-installing it. Full consumer install
|
||||
# would chicken-egg on first release (hanzogui's @hanzogui/*
|
||||
# workspace deps aren't on npm yet at the new version), so this
|
||||
# checks the tarball SHAPE — name, exports map resolvability,
|
||||
# required dist files present — without resolving the dep tree.
|
||||
inspect=$(mktemp -d)
|
||||
tar -xzf "${GUI_TGZ}" -C "$inspect"
|
||||
cd "$inspect/package"
|
||||
# Name must be exactly 'hanzogui' (the static extractor scans
|
||||
# for this literal token; never call this package 'Gui').
|
||||
test "$(node -p "require('./package.json').name")" = "hanzogui"
|
||||
# All workspace:* specs must be rewritten by the pack step
|
||||
# (literal 'workspace:' in the published tarball would break
|
||||
# consumer installs with EUNSUPPORTEDPROTOCOL).
|
||||
! grep -q '"workspace:' package.json
|
||||
# Every dist file referenced from exports must actually exist
|
||||
# in the tarball — catches broken exports maps + missing files.
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = require('./package.json');
|
||||
const walk = (v) => {
|
||||
if (typeof v === 'string') {
|
||||
if (v.startsWith('./')) {
|
||||
if (!fs.existsSync(v)) { console.error('missing:', v); process.exitCode = 1; }
|
||||
}
|
||||
} else if (v && typeof v === 'object') {
|
||||
for (const k of Object.keys(v)) walk(v[k]);
|
||||
}
|
||||
};
|
||||
walk(pkg.exports || {});
|
||||
walk(pkg.main); walk(pkg.module); walk(pkg.types);
|
||||
"
|
||||
echo "smoke OK: $(jq -r '.name + \"@\" + .version' package.json)"
|
||||
|
||||
- name: Publish gui
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
NODE_AUTH_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
TGZ: ${{ steps.pack-gui.outputs.tgz }}
|
||||
working-directory: pkgs/ui/hanzogui
|
||||
run: |
|
||||
version=$(node -p "require('./package.json').version")
|
||||
if npm view "hanzogui@${version}" version >/dev/null 2>&1; then
|
||||
echo "hanzogui@${version} already on npm — skipping."
|
||||
exit 0
|
||||
fi
|
||||
npm publish "${TGZ}" --access public --tag stable
|
||||
|
||||
|
||||
- name: Pack + Publish @hanzo/gui (alias of gui)
|
||||
# Republish the umbrella tarball under the @hanzo/gui scoped name so
|
||||
# downstream consumers can import either alias. Same dist tree as
|
||||
# gui — only `name` differs. The static extractor scans for the
|
||||
# literal 'gui' import token, so this alias is purely a consumer
|
||||
# ergonomics shim and never participates in extractor input. We reuse
|
||||
# the prepared tmpdir from the gui pack step (workspace specs
|
||||
# already rewritten) and only mutate `name`.
|
||||
env:
|
||||
NPM_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
NODE_AUTH_TOKEN: ${{ steps.token.outputs.npm_token }}
|
||||
TMPDIR_GUI: ${{ steps.pack-gui.outputs.tmpdir }}
|
||||
run: |
|
||||
set -eu
|
||||
aliasdir=$(mktemp -d)
|
||||
cp -R "$TMPDIR_GUI"/. "$aliasdir/"
|
||||
cd "$aliasdir"
|
||||
jq '.name = "@hanzo/gui"' package.json > package.json.new
|
||||
mv package.json.new package.json
|
||||
version=$(node -p "require('./package.json').version")
|
||||
if npm view "@hanzo/gui@${version}" version >/dev/null 2>&1; then
|
||||
echo "@hanzo/gui@${version} already on npm — skipping."
|
||||
exit 0
|
||||
fi
|
||||
tgz=$(npm pack --silent | tail -1)
|
||||
npm publish "${tgz}" --access public --tag stable
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"XcodeBuildMCP": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "xcodebuildmcp@latest"]
|
||||
},
|
||||
"native-devtools": {
|
||||
"command": "/Users/n8/.local/bin/native-devtools-mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
apps/tests/next-turbopack/src/TestComponent.tsx
|
||||
**/.react-router
|
||||
code/tests/next-turbopack/src/TestComponent.tsx
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"**/*.lock",
|
||||
"**/*.generated.*",
|
||||
"**/generated-*",
|
||||
"**/.hanzogui/**",
|
||||
"**/.tamagui/**",
|
||||
"**/node_modules/**",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/scripts/**",
|
||||
"**/.hanzogui/**",
|
||||
"**/.gui/**",
|
||||
"**/tmp-data/**",
|
||||
"**/.build/**",
|
||||
"**/.sst/**",
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
**/*.d.ts
|
||||
**/generated-*.ts
|
||||
apps/kitchen-sink/src/usecases/CompilerExtraction.native.tsx
|
||||
code/kitchen-sink/src/usecases/CompilerExtraction.native.tsx
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# Gui — agent operating contract
|
||||
|
||||
`CLAUDE.md` is a symlink to this file, so Claude, Codex, and every other agent
|
||||
read the same rules. Put durable, repo-wide agent guidance here.
|
||||
|
||||
## Finish what you start, then merge it back
|
||||
|
||||
The most common failure here is stopping early: a fix left on a branch, a
|
||||
migration written but never applied, a "critical, will do next" item dropped.
|
||||
Don't do that. If you were asked to do something, it is not done until:
|
||||
|
||||
- it is committed, pushed, validated, and **merged back to `main`** (or a PR is
|
||||
opened and driven to merge) — "I left it on a branch" / "I prepared the SQL"
|
||||
is not done;
|
||||
- it is **validated at the layer it changes**: typecheck/build for code, a real
|
||||
request or Playwright run for site behavior, an applied-and-verified query for
|
||||
a DB/RLS change. "Should work" is not validation;
|
||||
- any **migration is actually applied and the live state verified** — this repo's
|
||||
prod migration history is NOT auto-applied on deploy (see
|
||||
`apps/gui.hanzo.ai/supabase/README.md`); a committed migration that was never
|
||||
run is an open gap, not a fix;
|
||||
- for security/payments work especially, the loop is closed end to end — a
|
||||
documented finding with no shipped, deployed fix counts as no fix.
|
||||
|
||||
Keep going until the requested outcome is real. Continuing beats reporting. The
|
||||
only hard stops are the externally-irreversible ones — publishing/releasing to
|
||||
npm, force-pushing, rotating credentials, or changing prod infra (Cloudflare,
|
||||
DNS, Railway settings): pause and confirm for those, and hand them back with the
|
||||
exact steps when they block you. Everything else, finish it.
|
||||
|
||||
---
|
||||
|
||||
Please read ./CONTRIBUTING.md as well
|
||||
|
||||
Note you need to re-build packages (`bun run build` in the package directory) as you change them, unless you or someone is running a `bun run watch` at root.
|
||||
|
||||
FOR LONG RUNNNING DEBUGGING run `bun run watch` in the background its faster and rebuilds all packages.
|
||||
|
||||
keep commits to one line, add a trailing "Fixes #" if associated with a GH issue, and start with a convential commit style - UNLESS its a change that shouldn't go into the changelog, in those cases you can do things like `docs: ` or `site: `.
|
||||
|
||||
# Gui Testing Guide
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Kitchen Sink Tests
|
||||
|
||||
The kitchen-sink package contains the main integration tests for Gui components. To run these tests:
|
||||
|
||||
1. **Start the web server** (in the background):
|
||||
|
||||
```bash
|
||||
cd apps/kitchen-sink
|
||||
bun run start:web
|
||||
```
|
||||
|
||||
To open a specific test case in the browser:
|
||||
|
||||
```bash
|
||||
open "http://localhost:7979/?test=YourTestCaseName"
|
||||
```
|
||||
|
||||
Test case names match the file names in `apps/kitchen-sink/src/usecases/` (e.g., `SelectFocusScopeCase`).
|
||||
|
||||
To open a component demo:
|
||||
|
||||
```bash
|
||||
open "http://localhost:7979/?demo=Select"
|
||||
```
|
||||
|
||||
Demo names match files in `apps/demos/src/` without the `Demo` suffix (e.g., `Select` for `SelectDemo.tsx`).
|
||||
|
||||
2. **Run all web tests** with different animation drivers:
|
||||
|
||||
```bash
|
||||
bun run test:web
|
||||
```
|
||||
|
||||
This uses `run-tests-parallel.ts` which first runs `default` + `webkit` projects sequentially, then runs all four animated driver projects (`css`, `native`, `reanimated`, `motion`) in parallel against a single shared dev server.
|
||||
|
||||
3. **Run tests with a specific animation driver**:
|
||||
|
||||
```bash
|
||||
# Using env var + playwright --project flag
|
||||
cd apps/kitchen-sink
|
||||
NODE_ENV=test GUI_TEST_ANIMATION_DRIVER=css npx playwright test --project=animated-css
|
||||
|
||||
# Available projects: animated-css, animated-native, animated-reanimated, animated-motion
|
||||
```
|
||||
|
||||
4. **Run a specific test file**:
|
||||
|
||||
```bash
|
||||
# Using playwright directly
|
||||
cd apps/kitchen-sink
|
||||
npx playwright test tests/PopoverFocusScope.test.tsx
|
||||
|
||||
# Or with a specific driver
|
||||
NODE_ENV=test GUI_TEST_ANIMATION_DRIVER=css npx playwright test tests/YourTest.animated.test.tsx --project=animated-css
|
||||
```
|
||||
|
||||
5. **Debug tests**:
|
||||
```bash
|
||||
bun run test:web:debug
|
||||
# or
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
Tests are located in `apps/kitchen-sink/tests/` and follow these naming conventions:
|
||||
|
||||
- `ComponentName.test.tsx` - Standard tests that run ONCE with the default animation driver
|
||||
- `ComponentName.animated.test.tsx` - Animation-dependent tests that run with ALL animation drivers (css, native, reanimated, motion)
|
||||
|
||||
This separation significantly speeds up the test suite since most tests don't need to run 4x across all animation drivers. Only use `.animated.test.tsx` for tests that specifically verify animation behavior across different drivers.
|
||||
|
||||
### Writing Tests
|
||||
|
||||
When writing tests for focus behavior or component interactions:
|
||||
|
||||
1. Use appropriate wait times for animations and focus changes
|
||||
2. Be aware that `trapFocus` behavior depends on the component's open state
|
||||
3. Test both trapped and non-trapped focus scenarios
|
||||
4. Consider browser focus behavior when `trapFocus` is false
|
||||
|
||||
### Common Issues
|
||||
|
||||
- If tests fail due to timing, add appropriate `waitForTimeout` calls
|
||||
- For focus tests, ensure elements are visible before testing focus state
|
||||
- When testing popover/dialog components, wait for animations to complete
|
||||
|
||||
## Commit Message Conventions
|
||||
|
||||
- Use `site:` prefix (not `fix(site):`) for hanzogui.dev changes since they don't go in the changelog
|
||||
- Use `ci:` prefix (not `fix(ci):`) for CI/workflow changes since they don't go in the changelog
|
||||
- Keep commit messages to a single line
|
||||
|
||||
## iOS Development
|
||||
|
||||
See [docs/using-ios.md](./docs/using-ios.md) for iOS native development and Detox testing tips.
|
||||
|
||||
## hanzogui.dev API Authentication
|
||||
|
||||
When making authenticated API calls from the client side in hanzogui.dev, always use the `authFetch` helper:
|
||||
|
||||
```ts
|
||||
import { authFetch } from '~/features/api/authFetch'
|
||||
|
||||
const response = await authFetch('/api/some-endpoint', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ... }),
|
||||
})
|
||||
```
|
||||
|
||||
**Why this matters:** Cookies alone are not reliable for auth in production due to cross-origin/SameSite issues. The `authFetch` helper automatically includes the Authorization header with the user's access token. All payment/subscription endpoints require this.
|
||||
@@ -1,10 +1,10 @@
|
||||
# Contributing to `hanzogui`
|
||||
# Contributing to Hanzo GUI
|
||||
|
||||
To set up your development environment you'll need [bun](https://bun.sh) installed, then clone and install:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hanzoai/gui
|
||||
cd hanzogui
|
||||
cd gui
|
||||
bun install
|
||||
```
|
||||
|
||||
@@ -18,13 +18,13 @@ bun run watch
|
||||
|
||||
We compile out js to `dist` for "0-setup" installs by compiling a .native version of every file, and then in every web file we replace react-native with react-native-web. We also build our types out to ./types/\*.d.ts for a variety of reasons - it means we can't cause type issues for people with stricter configurations using our packages, it means we can track type regressions (the styled() type setup is impossible delicate to explosions of complexity), and it's generally faster for end users.
|
||||
|
||||
### Linking hanzogui into your existing project
|
||||
### Linking hanzo-gui into your existing project
|
||||
|
||||
Most package managers have a `link` command that lets you link in the local hanzogui to your project, we've built a package we found useful that works with `bun` or `yarn` depending on your configuration:
|
||||
Most package managers have a `link` command that lets you link in the local hanzo-gui to your project, we've built a package we found useful that works with `bun` or `yarn` depending on your configuration:
|
||||
|
||||
```bash
|
||||
npm i -g lllink
|
||||
lllink ~/path/to/hanzogui
|
||||
lllink ~/path/to/gui
|
||||
```
|
||||
|
||||
### Running native apps
|
||||
@@ -55,7 +55,7 @@ bun run kitchen-sink:build:ios
|
||||
|
||||
### Running web
|
||||
|
||||
You can run `bun run sandbox` or `bun run dev` (the hanzogui website).
|
||||
You can run `bun run sandbox` or `bun run dev` (the Hanzo GUI website).
|
||||
|
||||
### Local Testing Setup
|
||||
|
||||
@@ -64,7 +64,7 @@ You can run `bun run sandbox` or `bun run dev` (the hanzogui website).
|
||||
Install browser binaries before first run:
|
||||
|
||||
```bash
|
||||
cd apps/kitchen-sink
|
||||
cd code/kitchen-sink
|
||||
bun run test:web:setup # installs chromium
|
||||
bun run start:web # start dev server (background)
|
||||
bun run test:web # run all web tests
|
||||
@@ -86,7 +86,7 @@ The `run-detox.sh` script will automatically:
|
||||
- Build the app if the binary is missing or outdated
|
||||
|
||||
```bash
|
||||
cd apps/kitchen-sink
|
||||
cd code/kitchen-sink
|
||||
bun run detox:run:ios # run all iOS tests
|
||||
bun run detox:run:ios "Sheet" # filter by test name
|
||||
DETOX_DEVICE="iPhone 16 Pro" bun run detox:run:ios # override simulator device
|
||||
@@ -100,11 +100,11 @@ npx detox clean-framework-cache && npx detox build-framework-cache
|
||||
|
||||
### Fixing libraries
|
||||
|
||||
All compiler and CSS generation tests live in `pkgs/compiler/static-tests`.
|
||||
All compiler and CSS generation tests live in `code/compiler/static-tests`.
|
||||
|
||||
There are many native tests in `apps/kitchen-sink/tests`.
|
||||
There are many native tests in `code/kitchen-sink/tests`.
|
||||
|
||||
A variety of core tests live in `pkgs/core/core-test`.
|
||||
A variety of core tests live in `code/core/core-test`.
|
||||
|
||||
Before submitting a PR, please check everything works across every combination of environments.
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ ARG GITHUB_APP_CLIENT_ID
|
||||
ARG GITHUB_APP_SECRET
|
||||
ARG GITHUB_SPONSOR_WEBHOOK_SECRET
|
||||
ARG GITHUB_TOKEN
|
||||
ARG IS_GUI_DEV
|
||||
ARG IS_HANZO_GUI_DEV
|
||||
ARG NEXT_PUBLIC_GITHUB_APP_ID
|
||||
ARG NEXT_PUBLIC_GITHUB_AUTH_CLIENT_ID
|
||||
ARG NEXT_PUBLIC_IS_GUI_DEV
|
||||
ARG NEXT_PUBLIC_IS_HANZO_GUI_DEV
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
ARG NEXT_PUBLIC_SUPABASE_URL
|
||||
@@ -36,7 +36,7 @@ ARG TAKEOUT_RENEWAL_COUPON_ID
|
||||
ARG URL
|
||||
ARG ONE_SERVER_URL
|
||||
ARG APP_NAME
|
||||
ARG GUI_PRO_SECRET
|
||||
ARG HANZO_GUI_PRO_SECRET
|
||||
ARG DEEPSEEK_API_KEY
|
||||
ARG BENTO_GITHUB_TOKEN
|
||||
ARG BENTO_BRANCH
|
||||
@@ -44,7 +44,7 @@ ARG BENTO_BRANCH
|
||||
# install dependencies (sharp needs libvips for image processing)
|
||||
RUN apt-get update && apt-get install -y git bsdmainutils vim-common gh libvips-dev
|
||||
|
||||
WORKDIR /root/hanzogui
|
||||
WORKDIR /root/hanzo-gui
|
||||
COPY . .
|
||||
|
||||
# init git (allow empty commit if nothing to commit)
|
||||
@@ -58,7 +58,7 @@ RUN if [ -n "$BENTO_GITHUB_TOKEN" ]; then \
|
||||
echo "Cloning bento repository (branch: $BRANCH)..."; \
|
||||
unset GITHUB_TOKEN && \
|
||||
echo "$BENTO_GITHUB_TOKEN" | gh auth login --with-token && \
|
||||
gh repo clone hanzogui/bento -- --branch "$BRANCH" && \
|
||||
gh repo clone hanzoai/bento -- --branch "$BRANCH" && \
|
||||
gh auth logout --hostname github.com && \
|
||||
echo "✅ Bento repository cloned (branch: $BRANCH)" && \
|
||||
echo "REQUIRE_BENTO=true" > /tmp/bento_status; \
|
||||
@@ -67,7 +67,7 @@ RUN if [ -n "$BENTO_GITHUB_TOKEN" ]; then \
|
||||
echo "REQUIRE_BENTO=false" > /tmp/bento_status; \
|
||||
fi
|
||||
|
||||
WORKDIR /root/hanzogui
|
||||
WORKDIR /root/hanzo-gui
|
||||
|
||||
# Set REQUIRE_BENTO based on whether bento was cloned
|
||||
RUN export $(cat /tmp/bento_status)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# local build dockerfile - uses bento from ../bento
|
||||
# build from PARENT directory: docker build -f hanzogui5/Dockerfile.local -t hanzogui-site .
|
||||
# build from PARENT directory: docker build -f tamagui5/Dockerfile.local -t tamagui-site .
|
||||
FROM node:22
|
||||
|
||||
# install bun
|
||||
@@ -17,10 +17,10 @@ ARG GITHUB_APP_CLIENT_ID
|
||||
ARG GITHUB_APP_SECRET
|
||||
ARG GITHUB_SPONSOR_WEBHOOK_SECRET
|
||||
ARG GITHUB_TOKEN
|
||||
ARG IS_GUI_DEV
|
||||
ARG IS_TAMAGUI_DEV
|
||||
ARG NEXT_PUBLIC_GITHUB_APP_ID
|
||||
ARG NEXT_PUBLIC_GITHUB_AUTH_CLIENT_ID
|
||||
ARG NEXT_PUBLIC_IS_GUI_DEV
|
||||
ARG NEXT_PUBLIC_IS_TAMAGUI_DEV
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
ARG NEXT_PUBLIC_SUPABASE_URL
|
||||
@@ -36,7 +36,7 @@ ARG TAKEOUT_RENEWAL_COUPON_ID
|
||||
ARG URL
|
||||
ARG ONE_SERVER_URL
|
||||
ARG APP_NAME
|
||||
ARG GUI_PRO_SECRET
|
||||
ARG TAMAGUI_PRO_SECRET
|
||||
ARG DEEPSEEK_API_KEY
|
||||
|
||||
# install dependencies (sharp needs libvips for image processing)
|
||||
@@ -46,14 +46,14 @@ WORKDIR /root
|
||||
|
||||
# copy both repos
|
||||
COPY bento /root/bento
|
||||
COPY hanzogui5 /root/hanzogui
|
||||
COPY tamagui5 /root/tamagui
|
||||
|
||||
WORKDIR /root/hanzogui
|
||||
WORKDIR /root/tamagui
|
||||
|
||||
# init git (allow empty commit if nothing to commit)
|
||||
RUN git config --global user.email "you@example.com" && git config --global user.name "Docker Build" && git init . && git add -A && (git commit -m 'add' > /dev/null || true)
|
||||
|
||||
# install hanzogui deps first
|
||||
# install tamagui deps first
|
||||
RUN bun install
|
||||
|
||||
# setup bento - merge deps and symlink node_modules
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# gui — AI Assistant Context
|
||||
|
||||
# Hanzo GUI
|
||||
|
||||
<h3 align="center">
|
||||
Style library, design system, composable components, and more.
|
||||
</h3>
|
||||
@@ -1,57 +1,47 @@
|
||||
<div align="center">
|
||||
<picture width="572px">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/hanzoai/gui/raw/main/apps/gui.hanzo.ai/public/logo-black.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/hanzoai/gui/raw/main/apps/gui.hanzo.ai/public/logo-white.png">
|
||||
<img alt="Shows a black logo in light color mode and a white one in dark color mode." src="https://github.com/hanzoai/gui/raw/main/apps/gui.hanzo.ai/public/social.png">
|
||||
</picture>
|
||||
</div>
|
||||
# Hanzo GUI
|
||||
|
||||
<h3 align="center">
|
||||
Style library, design system, composable components, and more.
|
||||
</h3>
|
||||
|
||||
<h4 align="center">
|
||||
Gui is a bunch of libraries for building UIs that share code across React and React Native.
|
||||
Hanzo GUI is a bunch of libraries for building UIs that share code across React and React Native.
|
||||
</h4>
|
||||
|
||||
<br />
|
||||
|
||||
<div align="center">
|
||||
<img alt="NPM downloads" src="https://img.shields.io/npm/dw/@hanzogui/core?logo=npm&label=NPM%20downloads&cacheSeconds=3600"/>
|
||||
<img alt="Commits per month" src="https://img.shields.io/github/commit-activity/m/hanzogui/hanzogui?label=Commits&logo=git" />
|
||||
<img alt="Discord users online" src="https://img.shields.io/discord/909986013848412191?logo=discord&label=Discord&cacheSeconds=3600" />
|
||||
<a href="https://gurubase.io/g/hanzogui">
|
||||
<img alt="Gurubase" src="https://img.shields.io/badge/Gurubase-Ask%20Gui%20Guru-006BFF" />
|
||||
</a>
|
||||
<img alt="Commits per month" src="https://img.shields.io/github/commit-activity/m/hanzoai/gui?label=Commits&logo=git" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
- `@hanzogui/core` - Universal style library for React.
|
||||
- `@hanzogui/static` - Optimizing compiler that works with `core` and `hanzogui`.
|
||||
- `hanzogui` - UI kit that adapts to every platform.
|
||||
- `@hanzogui/web` - Universal style library for React.
|
||||
- `@hanzo/gui` - UI kit that adapts to every platform.
|
||||
- `@hanzogui/static` - Optimizing compiler that works with `gui-web` and `gui`.
|
||||
|
||||
<br />
|
||||
|
||||
**See [hanzogui.dev](https://hanzogui.dev) for documentation.**
|
||||
**See [gui.hanzo.ai](https://gui.hanzo.ai) for documentation.**
|
||||
|
||||
Gui lets you share more code between web and native apps without sacrificing the two things that typically suffer when you do: performance and code quality.
|
||||
Hanzo GUI lets you share more code between web and native apps without sacrificing the two things that typically suffer when you do: performance and code quality.
|
||||
|
||||
It does this with an optimizing compiler that outputs platform-specific optimizations - it turns styled components, even with complex logic or cross-module imports, into a simple `div` alongside atomic CSS on the web, or a View with its style objects hoisted on native.
|
||||
|
||||
The entirety of Gui works at compile time and runtime, and can be set up gradually, with initial usage as simple as importing it and using the base views and styled function.
|
||||
The entirety of Hanzo GUI works at compile time and runtime, and can be set up gradually, with initial usage as simple as importing it and using the base views and styled function.
|
||||
|
||||
We recommend checking out the starters with `npm create hanzogui@latest`, they range from a simple learning example to a production-ready monorepo.
|
||||
We recommend checking out the starters with `npm create hanzo-gui@latest`, they range from a simple learning example to a production-ready monorepo.
|
||||
|
||||
The compiler optimizes most and ultimately flattens a majority of styled components. In the [~500px² responsive browser section](https://hanzogui.dev) of the Gui website, 49 of the 55 or so [inline styled components](https://github.com/hanzoai/gui/blob/main/apps/gui.hanzo.ai/components/HeroResponsive.tsx) are flattened to a `div`. The homepage gains nearly 15% on Lighthouse with the compiler on.
|
||||
The compiler optimizes most and ultimately flattens a majority of styled components. In the [~500px² responsive browser section](https://gui.hanzo.ai) of the Hanzo GUI website, 49 of the 55 or so [inline styled components](https://github.com/hanzoai/gui/blob/main/code/gui.hanzo.ai/components/HeroResponsive.tsx) are flattened to a `div`. The homepage gains nearly 15% on Lighthouse with the compiler on.
|
||||
|
||||
[Learn more on the website](https://hanzogui.dev/docs/intro/introduction).
|
||||
[Learn more on the website](https://gui.hanzo.ai/docs/intro/introduction).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to Gui reference the [contributing guide](https://github.com/hanzoai/gui/blob/main/CONTRIBUTING.md).
|
||||
To contribute to Hanzo GUI reference the [contributing guide](https://github.com/hanzoai/gui/blob/main/CONTRIBUTING.md).
|
||||
|
||||
To contribute to documentation, see the docs source in `apps/gui.hanzo.ai/data/docs/`.
|
||||
To contribute to documentation, see the docs source in `code/gui.hanzo.ai/data/docs/`.
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
Thanks for helping keep Gui and its users safe.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Security fixes are made against the latest published major (`hanzogui` v2 and the
|
||||
current `@hanzogui/*` line). Please make sure you can reproduce on the latest
|
||||
release before reporting.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
**Please do not open a public issue for security problems.**
|
||||
|
||||
Report privately using **GitHub's private vulnerability reporting**:
|
||||
|
||||
1. Go to the repo's **Security** tab → **Report a vulnerability**
|
||||
(https://github.com/hanzoai/gui/security/advisories/new)
|
||||
2. Describe the issue, affected versions, and a proof-of-concept if you have one.
|
||||
|
||||
If you can't use GitHub advisories, email **security@hanzogui.dev** (or
|
||||
natewienert@gmail.com) with the details.
|
||||
|
||||
We aim to acknowledge reports within 3 business days and to ship a fix or
|
||||
mitigation for confirmed high/critical issues as quickly as we reasonably can.
|
||||
We're happy to credit you in the advisory unless you'd prefer to stay anonymous.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- The published `@hanzogui/*` / `hanzogui` npm packages.
|
||||
- The hanzogui.dev web application and its API endpoints (auth, payments,
|
||||
account/team management).
|
||||
- This repository's CI/CD and release/publish pipeline.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- Vulnerabilities in third-party dependencies that are already publicly known
|
||||
(please report those upstream; Dependabot tracks them here).
|
||||
- Findings that require a compromised developer machine, social engineering, or
|
||||
physical access.
|
||||
- Volumetric / denial-of-service testing against hanzogui.dev.
|
||||
|
||||
## Handling secrets
|
||||
|
||||
If you believe a credential has been committed to this repo (current tree or
|
||||
history), treat it as compromised: report it privately as above and **do not**
|
||||
post the value publicly. Maintainers: rotate the credential at its source first;
|
||||
history scrubbing is secondary.
|
||||
@@ -0,0 +1,137 @@
|
||||
# The Two-Namespace Problem: `@hanzo/gui` vs `@hanzogui/*`
|
||||
|
||||
## Summary
|
||||
|
||||
This repo publishes **127 npm packages** under two namespaces:
|
||||
|
||||
- **`@hanzo/gui`** (1 package, v4.0.0) — a barrel that re-exports everything
|
||||
- **`@hanzogui/*`** (126 packages, v3.0.x) — where all the actual code lives
|
||||
|
||||
`@hanzo/gui` contains zero implementation. Its `src/index.ts` is entirely:
|
||||
|
||||
```ts
|
||||
export * from '@hanzogui/core'
|
||||
export * from '@hanzogui/stacks'
|
||||
export * from '@hanzogui/button'
|
||||
// ... 50+ more re-exports
|
||||
```
|
||||
|
||||
This design causes serious, reproducible problems for downstream consumers. This document explains the issues, why they happen, and proposes a consolidation plan.
|
||||
|
||||
---
|
||||
|
||||
## Problems
|
||||
|
||||
### 1. pnpm duplicate copies
|
||||
|
||||
pnpm uses strict dependency isolation. When a consumer depends on both `@hanzo/gui` (which transitively pulls in `@hanzogui/core@3.0.6`) and `@hanzogui/button@3.0.2` (which also depends on `@hanzogui/core@3.0.6`), pnpm may resolve two physically separate copies of `@hanzogui/core`.
|
||||
|
||||
This breaks any library that relies on module identity — which includes the entire Tamagui/GUI styling system. `styled()` called with a component from copy A fails when the style engine is initialized in copy B.
|
||||
|
||||
**Symptoms:**
|
||||
- `TypeError: Cannot read properties of undefined (reading 'staticConfig')`
|
||||
- `TypeError: Cannot read properties of undefined (reading 'View')`
|
||||
|
||||
### 2. Rollup/Vite circular initialization failures
|
||||
|
||||
When Rollup bundles `@hanzo/gui`, it inlines all 50+ `export * from '@hanzogui/*'` re-exports into a single module. The `@hanzogui/*` packages have circular dependencies among themselves (e.g., `@hanzogui/button` → `@hanzogui/core` → `@hanzogui/web` → `@hanzogui/core`). Rollup resolves these into a single flat scope, but JavaScript execution order means some bindings are `undefined` at the time `styled()` is called.
|
||||
|
||||
**Symptoms:**
|
||||
- `styled(Stack, {...})` receives `undefined` instead of the `Stack` component
|
||||
- Only happens in production builds (dev mode serves modules individually)
|
||||
- Downstream consumers must add custom Vite plugins to work around this
|
||||
|
||||
### 3. Downstream consumers need resolution hacks
|
||||
|
||||
Every consumer using pnpm + Vite must add a custom plugin to force `@hanzogui/*` imports to resolve from `@hanzo/gui`'s own `node_modules`:
|
||||
|
||||
```ts
|
||||
// Required in every downstream vite.config.ts
|
||||
{
|
||||
name: 'resolve-hanzogui',
|
||||
enforce: 'pre',
|
||||
resolveId(source) {
|
||||
if (source.startsWith('@hanzogui/')) {
|
||||
return require_.resolve(source, { paths: [guiDir] })
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This is fragile, poorly documented, and needs to be copied into every project that uses `@hanzo/gui`.
|
||||
|
||||
### 4. Version drift between packages
|
||||
|
||||
Core packages are at 3.0.6, component packages are at 3.0.2–3.0.3. This means `@hanzogui/button@3.0.2` may depend on `@hanzogui/core@3.0.2` while the barrel pulls in `@hanzogui/core@3.0.6`. Even minor version mismatches can cause duplicate copies.
|
||||
|
||||
### 5. Publishing overhead
|
||||
|
||||
127 separate npm publishes per release. Version coordination across all packages. Any missed package creates a broken dependency graph.
|
||||
|
||||
---
|
||||
|
||||
## Root cause
|
||||
|
||||
The two-namespace design was intended for **tree-shaking on React Native**: importing `@hanzogui/button` avoids pulling in `@hanzogui/dialog`, `@hanzogui/sheet`, etc.
|
||||
|
||||
However:
|
||||
- Modern bundlers (Vite, Rollup, esbuild) tree-shake **named exports** from a single package just as effectively
|
||||
- The tree-shaking benefit is marginal compared to the cost of 126 separate packages
|
||||
- The barrel `@hanzo/gui` already defeats the purpose — most consumers use it, pulling in everything anyway
|
||||
|
||||
---
|
||||
|
||||
## Proposal: Consolidate into `@hanzo/gui`
|
||||
|
||||
### What changes
|
||||
|
||||
1. **Move all source** from `@hanzogui/*` into `@hanzo/gui` as internal directories
|
||||
2. **Single entry point**: `@hanzo/gui` exports everything (already does via barrel)
|
||||
3. **Subpath exports** for config/build-time packages not in the main barrel:
|
||||
- `@hanzo/gui/theme-builder` (was `@hanzogui/theme-builder`)
|
||||
- `@hanzo/gui/config-default` (was `@hanzogui/config-default`)
|
||||
- `@hanzo/gui/shorthands` (was `@hanzogui/shorthands`)
|
||||
4. **Single build output**: ESM + CJS + types
|
||||
5. **Deprecate `@hanzogui/*`** on npm, pointing to `@hanzo/gui`
|
||||
|
||||
### What stays the same
|
||||
|
||||
- Internal directory structure (`core/`, `button/`, `input/`, etc.) for code organization
|
||||
- Public API surface — `import { Button, Input, styled } from '@hanzo/gui'`
|
||||
- Support for React Native via `.native.tsx` file extensions
|
||||
|
||||
### Migration for consumers
|
||||
|
||||
| Before | After | Breaking? |
|
||||
|--------|-------|-----------|
|
||||
| `import { Button } from '@hanzo/gui'` | Same | No |
|
||||
| `import { Button } from '@hanzogui/button'` | `import { Button } from '@hanzo/gui'` | Yes (deprecate) |
|
||||
| `import { createThemes } from '@hanzogui/theme-builder'` | `import { createThemes } from '@hanzo/gui/theme-builder'` | Yes (subpath) |
|
||||
|
||||
### What this fixes
|
||||
|
||||
- **Zero duplicate copies** — one package, one resolution
|
||||
- **Zero circular init failures** — bundler sees one module graph, controls init order
|
||||
- **Zero downstream hacks** — no resolve plugins needed
|
||||
- **One npm publish** per release
|
||||
- **One version number** to track
|
||||
|
||||
### Implementation steps
|
||||
|
||||
1. Create `@hanzo/gui@5.0.0` with all source inlined
|
||||
2. Add `exports` field in `package.json` with subpath entries for config packages
|
||||
3. Verify tree-shaking works with Vite, Rollup, and Metro (React Native)
|
||||
4. Publish and deprecate all 126 `@hanzogui/*` packages on npm
|
||||
5. Provide codemod for `@hanzogui/* → @hanzo/gui` import rewrites
|
||||
|
||||
---
|
||||
|
||||
## Current workaround (for downstream consumers)
|
||||
|
||||
Until consolidation happens, consumers must:
|
||||
|
||||
1. **Never import from `@hanzogui/*` directly** — always use `@hanzo/gui`
|
||||
2. **Add a Vite resolve plugin** that forces `@hanzogui/*` to resolve from `@hanzo/gui`'s own `node_modules` (see example above)
|
||||
3. **Keep `@hanzo/gui` in Module Federation `shared` array** when using micro-frontends
|
||||
|
||||
These workarounds are documented here so they don't need to be rediscovered.
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"config": "./src/hanzogui.config.ts",
|
||||
"components": ["hanzogui"]
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
{
|
||||
"name": "@hanzogui/demos",
|
||||
"version": "8.0.3",
|
||||
"source": "demos/src/index.tsx",
|
||||
"files": [
|
||||
"src",
|
||||
"types",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
],
|
||||
"main": "dist/cjs",
|
||||
"module": "dist/esm",
|
||||
"types": "./src",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
"./photo/*": "./public/*.jpg",
|
||||
".": {
|
||||
"types": "./src/index.tsx",
|
||||
"react-native": "./dist/esm/index.native.js",
|
||||
"browser": "./dist/esm/index.mjs",
|
||||
"module": "./dist/esm/index.mjs",
|
||||
"import": "./dist/esm/index.mjs",
|
||||
"require": "./dist/cjs/index.cjs",
|
||||
"default": "./dist/esm/index.mjs"
|
||||
},
|
||||
"./demo/*": {
|
||||
"types": "./types/*.d.ts",
|
||||
"react-native": "./dist/esm/*.native.js",
|
||||
"browser": "./dist/esm/*.mjs",
|
||||
"module": "./dist/esm/*.mjs",
|
||||
"import": "./dist/esm/*.mjs",
|
||||
"require": "./dist/cjs/*.cjs",
|
||||
"default": "./dist/esm/*.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "hanzogui-build --skip-types",
|
||||
"watch": "hanzogui-build --skip-types --watch",
|
||||
"clean": "hanzogui-build clean",
|
||||
"clean:build": "hanzogui-build clean:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzogui/animate-presence": "workspace:*",
|
||||
"@hanzogui/avatar": "workspace:*",
|
||||
"@hanzogui/button": "workspace:*",
|
||||
"@hanzogui/checkbox": "workspace:*",
|
||||
"@hanzogui/checkbox-headless": "workspace:*",
|
||||
"@hanzogui/colors": "workspace:*",
|
||||
"@hanzogui/config": "workspace:*",
|
||||
"@hanzogui/context-menu": "workspace:*",
|
||||
"@hanzogui/core": "workspace:*",
|
||||
"@hanzogui/get-button-sized": "workspace:*",
|
||||
"@hanzogui/image": "workspace:*",
|
||||
"@hanzogui/input": "workspace:*",
|
||||
"@hanzogui/linear-gradient": "workspace:*",
|
||||
"@hanzogui/list-item": "workspace:*",
|
||||
"@hanzogui/logo": "workspace:*",
|
||||
"@hanzogui/lucide-icons-2": "workspace:*",
|
||||
"@hanzogui/menu": "workspace:*",
|
||||
"@hanzogui/popover": "workspace:*",
|
||||
"@hanzogui/progress": "workspace:*",
|
||||
"@hanzogui/radio-group": "workspace:*",
|
||||
"@hanzogui/radio-headless": "workspace:*",
|
||||
"@hanzogui/roving-focus": "workspace:*",
|
||||
"@hanzogui/select": "workspace:*",
|
||||
"@hanzogui/sheet": "workspace:*",
|
||||
"@hanzogui/slider": "workspace:*",
|
||||
"@hanzogui/stacks": "workspace:*",
|
||||
"@hanzogui/switch": "workspace:*",
|
||||
"@hanzogui/switch-headless": "workspace:*",
|
||||
"@hanzogui/tabs": "workspace:*",
|
||||
"@hanzogui/dev-config": "workspace:*",
|
||||
"@hanzogui/theme": "workspace:*",
|
||||
"@hanzogui/toast": "workspace:*",
|
||||
"@hanzo/gui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hanzogui/build": "workspace:*",
|
||||
"react": ">=19",
|
||||
"react-native": "0.83.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19"
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { Button, Card, H2, Paragraph, Text, Theme, XStack, YStack } from 'hanzogui'
|
||||
|
||||
// Proof surface for the token layer: true-black neutral ramp, one accent per
|
||||
// white-label brand (read straight from each brand's generated theme — the
|
||||
// $color9 accent slot), Geist type. Open with ?demo=BrandTokens.
|
||||
// Brand accent SoT: pkgs/core/themes/src/brands.ts.
|
||||
|
||||
const BRANDS = ['hanzo', 'lux', 'zoo', 'pars'] as const
|
||||
|
||||
const SURFACES = [
|
||||
['background', '$background', '#000 canvas'],
|
||||
['color2', '$color2', '#0a0a0a surface'],
|
||||
['color3', '$color3', '#111 raised'],
|
||||
['color4', '$color4', '#171717 elevated'],
|
||||
] as const
|
||||
|
||||
export function BrandTokensDemo() {
|
||||
return (
|
||||
<Theme name="dark">
|
||||
<YStack backgroundColor="$background" padding="$6" gap="$5" minHeight={640}>
|
||||
<YStack gap="$2">
|
||||
<H2 fontFamily="$heading" color="$color12">
|
||||
Hanzo GUI — design tokens
|
||||
</H2>
|
||||
<Paragraph color="$color11" fontFamily="$body">
|
||||
True-black canvas · one accent per brand · Geist type system
|
||||
</Paragraph>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Text color="$color11" fontFamily="$mono" fontSize="$2">
|
||||
neutral surface ramp
|
||||
</Text>
|
||||
<XStack gap="$3" flexWrap="wrap">
|
||||
{SURFACES.map(([name, tok, label]) => (
|
||||
<YStack
|
||||
key={name}
|
||||
backgroundColor={tok}
|
||||
width={160}
|
||||
height={96}
|
||||
borderRadius={12}
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
padding="$3"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Text color="$color12" fontFamily="$mono" fontSize="$3">
|
||||
{name}
|
||||
</Text>
|
||||
<Text color="$color10" fontSize="$2">
|
||||
{label}
|
||||
</Text>
|
||||
</YStack>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
<YStack gap="$2">
|
||||
<Text color="$color11" fontFamily="$mono" fontSize="$2">
|
||||
per-brand accent (applied sparingly over the shared canvas)
|
||||
</Text>
|
||||
<XStack gap="$4" flexWrap="wrap">
|
||||
{BRANDS.map((b) => (
|
||||
// brand themes come from the host app's config (@hanzogui/themes/v4),
|
||||
// not from the @hanzogui/dev-config this package is typed against
|
||||
// @ts-expect-error
|
||||
<Theme key={b} name={b}>
|
||||
<Card
|
||||
backgroundColor="$color2"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
borderRadius={12}
|
||||
padding="$4"
|
||||
width={210}
|
||||
gap="$3"
|
||||
>
|
||||
<XStack alignItems="center" gap="$2">
|
||||
<YStack
|
||||
width={14}
|
||||
height={14}
|
||||
borderRadius={999}
|
||||
backgroundColor="$color9"
|
||||
/>
|
||||
<Text color="$color12" fontFamily="$heading" fontSize="$5">
|
||||
{b}
|
||||
</Text>
|
||||
</XStack>
|
||||
<YStack
|
||||
height={44}
|
||||
borderRadius={8}
|
||||
backgroundColor="$color9"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<Text color="$color1" fontSize="$3" fontWeight="700">
|
||||
Primary CTA
|
||||
</Text>
|
||||
</YStack>
|
||||
<Button>Themed button</Button>
|
||||
</Card>
|
||||
</Theme>
|
||||
))}
|
||||
</XStack>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { ChevronRight, Cloud, Moon, Star, Sun } from '@hanzogui/lucide-icons-2'
|
||||
import { ListItem, Separator, Theme, XStack, YGroup } from '@hanzo/gui'
|
||||
|
||||
export function ListItemDemo() {
|
||||
return (
|
||||
<Theme name="surface1">
|
||||
<XStack $maxMd={{ flexDirection: 'column' }} px="$4" gap="$4">
|
||||
<ListItemDemo1 />
|
||||
<ListItemDemo2 />
|
||||
</XStack>
|
||||
</Theme>
|
||||
)
|
||||
}
|
||||
|
||||
function ListItemDemo1() {
|
||||
return (
|
||||
<YGroup
|
||||
self="center"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
rounded="$4"
|
||||
overflow="hidden"
|
||||
width={240}
|
||||
size="$4"
|
||||
>
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
gap="$3"
|
||||
icon={Star}
|
||||
title="Star"
|
||||
subTitle={<ListItem.Subtitle>Twinkles</ListItem.Subtitle>}
|
||||
iconAfter={ChevronRight}
|
||||
/>
|
||||
</YGroup.Item>
|
||||
<YGroup.Item>
|
||||
<ListItem gap="$3" icon={Moon}>
|
||||
Moon
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
<YGroup.Item>
|
||||
<ListItem gap="$3" icon={Sun}>
|
||||
Sun
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
<YGroup.Item>
|
||||
<ListItem gap="$3" icon={Cloud}>
|
||||
Cloud
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
</YGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function ListItemDemo2() {
|
||||
return (
|
||||
<YGroup
|
||||
self="center"
|
||||
borderWidth={1}
|
||||
borderColor="$borderColor"
|
||||
rounded="$4"
|
||||
overflow="hidden"
|
||||
width={240}
|
||||
size="$5"
|
||||
>
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
gap="$3"
|
||||
title="Star"
|
||||
subTitle="Subtitle"
|
||||
icon={Star}
|
||||
iconAfter={ChevronRight}
|
||||
/>
|
||||
</YGroup.Item>
|
||||
<Separator />
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
gap="$3"
|
||||
title="Moon"
|
||||
subTitle="Subtitle"
|
||||
icon={Moon}
|
||||
iconAfter={ChevronRight}
|
||||
/>
|
||||
</YGroup.Item>
|
||||
</YGroup>
|
||||
)
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import { isWeb } from '@hanzo/gui'
|
||||
import {
|
||||
Toast,
|
||||
toast,
|
||||
useToastItem,
|
||||
type ToastPosition,
|
||||
type ToastT,
|
||||
} from '@hanzogui/toast/v2'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowDownLeft,
|
||||
ArrowDownRight,
|
||||
ArrowUp,
|
||||
ArrowUpLeft,
|
||||
ArrowUpRight,
|
||||
} from '@hanzogui/lucide-icons-2'
|
||||
import { useRef, useState } from 'react'
|
||||
import { Button, Label, SizableText, Slider, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
export const ToastDemo = () => {
|
||||
const [position, setPosition] = useState<ToastPosition>('bottom-right')
|
||||
const [gap, setGap] = useState(14)
|
||||
const [visibleToasts, setVisibleToasts] = useState(4)
|
||||
const [duration, setDuration] = useState(4000)
|
||||
const count = useRef(0)
|
||||
|
||||
const showToast = (newPosition: ToastPosition) => {
|
||||
setPosition(newPosition)
|
||||
count.current++
|
||||
toast(`Toast #${count.current}`, {
|
||||
description: 'Swipe to dismiss or wait for auto-close.',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Toast
|
||||
position={position}
|
||||
gap={Math.round(gap)}
|
||||
visibleToasts={visibleToasts}
|
||||
duration={duration}
|
||||
>
|
||||
<Toast.Viewport offset={16}>
|
||||
<Toast.List
|
||||
renderItem={({ toast: t, index }) => (
|
||||
<Toast.Item key={t.id} toast={t} index={index} testID="toast-item">
|
||||
<ToastContent toast={t} />
|
||||
</Toast.Item>
|
||||
)}
|
||||
/>
|
||||
</Toast.Viewport>
|
||||
|
||||
<YStack gap="$3" self="center" width={280}>
|
||||
{/* Position buttons */}
|
||||
<YStack gap="$2" self="center">
|
||||
<XStack gap="$2">
|
||||
<PositionButton
|
||||
position="top-left"
|
||||
current={position}
|
||||
onPress={showToast}
|
||||
Icon={ArrowUpLeft}
|
||||
/>
|
||||
<PositionButton
|
||||
position="top-center"
|
||||
current={position}
|
||||
onPress={showToast}
|
||||
Icon={ArrowUp}
|
||||
/>
|
||||
<PositionButton
|
||||
position="top-right"
|
||||
current={position}
|
||||
onPress={showToast}
|
||||
Icon={ArrowUpRight}
|
||||
testID="toast-top-right-button"
|
||||
/>
|
||||
</XStack>
|
||||
<XStack gap="$2">
|
||||
<PositionButton
|
||||
position="bottom-left"
|
||||
current={position}
|
||||
onPress={showToast}
|
||||
Icon={ArrowDownLeft}
|
||||
/>
|
||||
<PositionButton
|
||||
position="bottom-center"
|
||||
current={position}
|
||||
onPress={showToast}
|
||||
Icon={ArrowDown}
|
||||
/>
|
||||
<PositionButton
|
||||
position="bottom-right"
|
||||
current={position}
|
||||
onPress={showToast}
|
||||
Icon={ArrowDownRight}
|
||||
testID="toast-show-button"
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
|
||||
{/* Controls */}
|
||||
<YStack gap="$1" paddingTop="$1">
|
||||
<DemoSlider
|
||||
label="Gap"
|
||||
value={gap}
|
||||
min={0}
|
||||
max={30}
|
||||
step={0.1}
|
||||
onChange={setGap}
|
||||
/>
|
||||
<DemoSlider
|
||||
label="Visible"
|
||||
value={visibleToasts}
|
||||
min={1}
|
||||
max={8}
|
||||
step={1}
|
||||
onChange={setVisibleToasts}
|
||||
/>
|
||||
<DemoSlider
|
||||
label="Duration"
|
||||
value={duration}
|
||||
min={1000}
|
||||
max={10000}
|
||||
step={100}
|
||||
onChange={setDuration}
|
||||
format={(v) => `${(v / 1000).toFixed(1)}s`}
|
||||
/>
|
||||
</YStack>
|
||||
</YStack>
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
|
||||
function ToastContent({ toast: t }: { toast: ToastT }) {
|
||||
const { handleClose } = useToastItem()
|
||||
|
||||
const title = typeof t.title === 'function' ? t.title() : t.title
|
||||
const description =
|
||||
typeof t.description === 'function' ? t.description() : t.description
|
||||
|
||||
return (
|
||||
<>
|
||||
<XStack gap="$3" alignItems="flex-start">
|
||||
<Toast.Icon />
|
||||
<YStack flex={1} gap="$0.5">
|
||||
{title && (
|
||||
<Toast.Title fontWeight="600" size="$3">
|
||||
{title}
|
||||
</Toast.Title>
|
||||
)}
|
||||
{description && (
|
||||
<Toast.Description color="$color9" size="$2">
|
||||
{description}
|
||||
</Toast.Description>
|
||||
)}
|
||||
</YStack>
|
||||
</XStack>
|
||||
|
||||
{isWeb && (
|
||||
<Toast.Close
|
||||
testID="toast-close-button"
|
||||
position="absolute"
|
||||
top={-6}
|
||||
left={-6}
|
||||
zIndex={1}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function DemoSlider({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
onChange,
|
||||
format,
|
||||
}: {
|
||||
label: string
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step?: number
|
||||
onChange: (v: number) => void
|
||||
format?: (v: number) => string
|
||||
}) {
|
||||
return (
|
||||
<XStack gap="$2" alignItems="center">
|
||||
<Label size="$2" width={55}>
|
||||
{label}
|
||||
</Label>
|
||||
<Slider
|
||||
flex={1}
|
||||
value={[value]}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
onValueChange={([v]) => onChange(v)}
|
||||
>
|
||||
<Slider.Track>
|
||||
<Slider.TrackActive />
|
||||
</Slider.Track>
|
||||
<Slider.Thumb theme="accent" size={16} borderRadius={100} index={0} />
|
||||
</Slider>
|
||||
<SizableText userSelect="none" size="$2" width={35} textAlign="right">
|
||||
{format ? format(value) : Math.round(value)}
|
||||
</SizableText>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
const PositionButton = ({
|
||||
position,
|
||||
current,
|
||||
onPress,
|
||||
Icon,
|
||||
testID,
|
||||
}: {
|
||||
position: ToastPosition
|
||||
current: ToastPosition
|
||||
onPress: (p: ToastPosition) => void
|
||||
Icon: any
|
||||
testID?: string
|
||||
}) => {
|
||||
const isActive = position === current
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={Icon}
|
||||
circular
|
||||
theme={isActive ? 'accent' : undefined}
|
||||
onPress={() => onPress(position)}
|
||||
testID={testID}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { CheckCircle2 } from '@hanzogui/lucide-icons-2'
|
||||
import { Toast, toast, useToasts } from '@hanzogui/toast/v2'
|
||||
import { Button, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: if you're copy-pasting this demo into your code, make sure to add the Toast wrapper as well.
|
||||
*/
|
||||
export const ToastDuplicateDemo = () => {
|
||||
return (
|
||||
<Toast>
|
||||
<Toast.Viewport>
|
||||
<ToastList />
|
||||
</Toast.Viewport>
|
||||
|
||||
<YStack items="center">
|
||||
<Button
|
||||
onPress={() => {
|
||||
toast('Successfully saved!', {
|
||||
description: "Don't worry... We've got your data.",
|
||||
})
|
||||
}}
|
||||
>
|
||||
Show toast
|
||||
</Button>
|
||||
</YStack>
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
|
||||
const ToastList = () => {
|
||||
const { toasts } = useToasts()
|
||||
|
||||
return (
|
||||
<>
|
||||
{toasts.map((t, index) => (
|
||||
<Toast.Item key={t.id} toast={t} index={index}>
|
||||
<XStack gap="$4" items="center">
|
||||
<YStack>
|
||||
<CheckCircle2 />
|
||||
</YStack>
|
||||
|
||||
<YStack>
|
||||
<Toast.Title>
|
||||
{typeof t.title === 'function' ? t.title() : t.title}
|
||||
</Toast.Title>
|
||||
{t.description && (
|
||||
<Toast.Description>
|
||||
{typeof t.description === 'function' ? t.description() : t.description}
|
||||
</Toast.Description>
|
||||
)}
|
||||
</YStack>
|
||||
</XStack>
|
||||
</Toast.Item>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import { Toast, useToastController, useToastState } from '@hanzogui/toast'
|
||||
import React from 'react'
|
||||
import { Button, Label, Switch, XStack, YStack } from '@hanzo/gui'
|
||||
|
||||
/**
|
||||
* IMPORTANT NOTE: if you're copy-pasting this demo into your code, make sure to add:
|
||||
* - <ToastProvider> at the root
|
||||
* - <ToastViewport /> where you want to show the toasts
|
||||
*/
|
||||
export const ToastV1Demo = () => {
|
||||
const [native, setNative] = React.useState(false)
|
||||
|
||||
return (
|
||||
<YStack gap="$4" items="center">
|
||||
<ToastControl native={native} />
|
||||
<CurrentToast />
|
||||
|
||||
<NativeOptions native={native} setNative={setNative} />
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
const CurrentToast = () => {
|
||||
const currentToast = useToastState()
|
||||
|
||||
if (!currentToast || currentToast.isHandledNatively) return null
|
||||
|
||||
return (
|
||||
<Toast
|
||||
key={currentToast.id}
|
||||
duration={currentToast.duration}
|
||||
viewportName={currentToast.viewportName}
|
||||
enterStyle={{ opacity: 0, scale: 0.95, y: -80 }}
|
||||
exitStyle={{ opacity: 0, scale: 0.95, y: -80 }}
|
||||
opacity={1}
|
||||
scale={1}
|
||||
y={-50}
|
||||
transition="quicker"
|
||||
bg="$color2"
|
||||
boxShadow="0px 2px 4px rgba(0,0,0,0.12), 0px 8px 24px rgba(0,0,0,0.08)"
|
||||
>
|
||||
<XStack gap="$5" items="center" justify="space-between">
|
||||
<YStack gap="$0.5" flex={1}>
|
||||
<Toast.Title>{currentToast.title}</Toast.Title>
|
||||
{!!currentToast.message && (
|
||||
<Toast.Description>{currentToast.message}</Toast.Description>
|
||||
)}
|
||||
</YStack>
|
||||
<Toast.Action asChild altText="Dismiss toast">
|
||||
<Button size="$2">Dismiss</Button>
|
||||
</Toast.Action>
|
||||
</XStack>
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
|
||||
const ToastControl = ({ native }: { native: boolean }) => {
|
||||
const toast = useToastController()
|
||||
|
||||
return (
|
||||
<XStack gap="$2" justify="center">
|
||||
<Button
|
||||
onPress={() => {
|
||||
toast.show('Successfully saved!', {
|
||||
message: "Don't worry, we've got your data.",
|
||||
native,
|
||||
demo: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Show
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
toast.hide()
|
||||
}}
|
||||
>
|
||||
Hide
|
||||
</Button>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
const NativeOptions = ({
|
||||
native,
|
||||
setNative,
|
||||
}: {
|
||||
native: boolean
|
||||
setNative: (native: boolean) => void
|
||||
}) => {
|
||||
return (
|
||||
<XStack gap="$3">
|
||||
<Label size="$1" onPress={() => setNative(false)}>
|
||||
Custom
|
||||
</Label>
|
||||
<Switch
|
||||
theme="surface2"
|
||||
id="native-toggle"
|
||||
size="$1"
|
||||
checked={!!native}
|
||||
onCheckedChange={(val) => setNative(val)}
|
||||
>
|
||||
<Switch.Thumb
|
||||
theme="accent"
|
||||
transition={[
|
||||
'quickest',
|
||||
{
|
||||
transform: {
|
||||
overshootClamping: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Switch>
|
||||
|
||||
<Label size="$1" onPress={() => setNative(true)}>
|
||||
Native
|
||||
</Label>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { config } from '@hanzogui/dev-config'
|
||||
import { createGui } from '@hanzo/gui'
|
||||
|
||||
const tamaConf = createGui(config)
|
||||
|
||||
export type Conf = typeof tamaConf
|
||||
|
||||
declare module 'hanzogui' {
|
||||
interface GuiCustomConfig extends Conf {}
|
||||
|
||||
interface TypeOverride {
|
||||
groupNames(): 'card' | 'takeoutBody' | 'content' | 'item'
|
||||
}
|
||||
}
|
||||
|
||||
export default tamaConf
|
||||
@@ -1,66 +0,0 @@
|
||||
// Include hanzogui config types for proper type checking
|
||||
import type {} from './hanzogui.config'
|
||||
|
||||
export * from './AccordionDemo'
|
||||
export * from './AddThemeDemo'
|
||||
export * from './AlertDialogDemo'
|
||||
export * from './AnimationsDelayDemo'
|
||||
export * from './AnimationsDemo'
|
||||
export * from './AnimationsEnterDemo'
|
||||
export * from './AnimationsHoverDemo'
|
||||
export * from './AnimationsPresenceDemo'
|
||||
export * from './AnimationsTimingDemo'
|
||||
export * from './AvatarDemo'
|
||||
export * from './BoxShadowTokenDemo'
|
||||
export * from './BuildAButtonDemo'
|
||||
export * from './ButtonDemo'
|
||||
export * from './ThemeBuilderDemo'
|
||||
export * from './CardDemo'
|
||||
export * from './CheckboxDemo'
|
||||
export * from './CheckboxHeadlessDemo'
|
||||
export * from './CheckboxUnstyledDemo'
|
||||
export * from './ColorsDemo'
|
||||
export * from './ContextMenuDemo'
|
||||
export * from './MenuDemo'
|
||||
export * from './DialogDemo'
|
||||
export * from './FormsDemo'
|
||||
export * from './GroupDemo'
|
||||
export * from './HeadingsDemo'
|
||||
export * from './ImageDemo'
|
||||
export * from './WebNativeImageDemo'
|
||||
export * from './InputsDemo'
|
||||
export * from './NewInputsDemo'
|
||||
export * from './LabelDemo'
|
||||
export * from './LinearGradientDemo'
|
||||
export * from './ListItemDemo'
|
||||
export * from './LucideIconsDemo'
|
||||
export * from './PopoverDemo'
|
||||
export * from './ProgressDemo'
|
||||
export * from './RadioGroupDemo'
|
||||
export * from './RadioGroupHeadlessDemo'
|
||||
export * from './RadioGroupUnstyledDemo'
|
||||
export * from './ReplaceThemeDemo'
|
||||
export * from './ScrollViewDemo'
|
||||
export * from './SelectDemo'
|
||||
export * from './SeparatorDemo'
|
||||
export * from './ShapesDemo'
|
||||
export * from './SheetDemo'
|
||||
export * from './SliderDemo'
|
||||
export * from './SpinnerDemo'
|
||||
export * from './StacksDemo'
|
||||
export * from './SwitchDemo'
|
||||
export * from './SwitchHeadlessDemo'
|
||||
export * from './SwitchUnstyledDemo'
|
||||
export * from './TabsAdvancedDemo'
|
||||
export * from './TabsDemo'
|
||||
export * from './TextDemo'
|
||||
export * from './ThemeInverseDemo'
|
||||
export * from './ToastDemo'
|
||||
export * from './ToastDuplicateDemo'
|
||||
export * from './ToastV1Demo'
|
||||
export * from './ToggleGroupDemo'
|
||||
export * from './TokensDemo'
|
||||
export * from './TooltipDemo'
|
||||
export * from './UpdateThemeDemo'
|
||||
export * from './useOnIntersecting'
|
||||
export * from './TabsHeadlessDemo'
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"module": "Preserve",
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../pkgs/core/config"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/core/core"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/logo"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/lucide-icons"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/dev-config"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/avatar"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/button"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/list-item"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/menu"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/popover"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/progress"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/radio-group"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/select"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/sheet"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/slider"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/stacks"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/hanzogui"
|
||||
},
|
||||
{
|
||||
"path": "../../pkgs/ui/toast"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"tests",
|
||||
"types",
|
||||
"dist",
|
||||
"**/__tests__"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
dist/
|
||||
node_modules/
|
||||
*.log
|
||||
src-tauri/target/
|
||||
src-tauri/gen/
|
||||
.vite/
|
||||
@@ -1,14 +0,0 @@
|
||||
import { defaultConfig } from '@hanzogui/config/v4'
|
||||
import { createGui } from '@hanzo/gui'
|
||||
|
||||
// v4 config — carries the true-black default dark theme + the per-brand accent
|
||||
// themes (dark_hanzo / dark_lux / dark_zoo / dark_pars) and the Geist type system.
|
||||
export const config = createGui(defaultConfig)
|
||||
|
||||
export default config
|
||||
|
||||
export type Conf = typeof config
|
||||
|
||||
declare module 'hanzogui' {
|
||||
interface GuiCustomConfig extends Conf {}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hanzo Desktop</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@hanzogui/desktop",
|
||||
"version": "8.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Tauri desktop proof shell — renders hanzogui on the true-black theme in a native webview.",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5175 --strictPort",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzogui/config": "workspace:*",
|
||||
"@hanzogui/react-native-web-lite": "workspace:*",
|
||||
"@hanzogui/tauri": "workspace:*",
|
||||
"@hanzogui/themes": "workspace:*",
|
||||
"@hanzo/gui": "workspace:*",
|
||||
"react": ">=19",
|
||||
"react-dom": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.6.0",
|
||||
"@types/react": "~19.1.10",
|
||||
"@types/react-dom": "~19.1.7",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "~5.9.2",
|
||||
"vite": "^8.0.3"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 39 KiB |
@@ -1,24 +0,0 @@
|
||||
# Standalone crate — not part of any parent Rust workspace in the monorepo.
|
||||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "hanzo-desktop"
|
||||
version = "0.1.0"
|
||||
description = "Hanzo desktop — Tauri weld proof shell"
|
||||
authors = ["Hanzo AI <dev@hanzo.ai>"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "hanzo_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -1,3 +0,0 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for the Hanzo desktop shell — window controls, shell open, fs, and global shortcuts consumed by @hanzogui/tauri.",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-is-maximized",
|
||||
"shell:allow-open",
|
||||
"fs:default",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-unregister"
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"default":{"identifier":"default","description":"Default capability for the Hanzo desktop shell — window controls, shell open, fs, and global shortcuts consumed by @hanzogui/tauri.","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-close","core:window:allow-hide","core:window:allow-start-dragging","core:window:allow-set-fullscreen","core:window:allow-is-maximized","shell:allow-open","fs:default","global-shortcut:allow-register","global-shortcut:allow-unregister"]}}
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 519 B |
|
Before Width: | Height: | Size: 807 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 805 B |
|
Before Width: | Height: | Size: 937 B |
|
Before Width: | Height: | Size: 655 B |
|
Before Width: | Height: | Size: 669 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 669 B |
|
Before Width: | Height: | Size: 667 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 667 B |
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 995 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 391 B |
|
Before Width: | Height: | Size: 590 B |
|
Before Width: | Height: | Size: 590 B |
|
Before Width: | Height: | Size: 705 B |
|
Before Width: | Height: | Size: 448 B |
|
Before Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 695 B |
|
Before Width: | Height: | Size: 905 B |
|
Before Width: | Height: | Size: 590 B |