mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ui): script to build bundled (#11801)
We share the bundled UI built from p2 for Radius. This PR adds a script to bundle it, and this gets uploaded to R2 on every green commit to main.
This commit is contained in:
parent
5f6198c397
commit
c5d98a78ed
3 changed files with 139 additions and 6 deletions
73
.github/workflows/p2-cloud-bundle.yml
vendored
Normal file
73
.github/workflows/p2-cloud-bundle.yml
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Builds p2's web UI for Radius Cloud Brain after CI passes on main (apps/p2/scripts/cloud-bundle.ts) and uploads it
|
||||
# to the R2 bucket pi-ui under the commit's full SHA, which Radius serves under /pi/ui/. Builds are immutable: a SHA
|
||||
# that has one is skipped. Never runs for PRs: the job holds R2 write credentials.
|
||||
|
||||
name: p2 cloud bundle
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sha:
|
||||
description: Full commit SHA to build and upload
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'main' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository)
|
||||
runs-on: ubuntu-latest
|
||||
environment: pi-model-upload
|
||||
env:
|
||||
SHA: ${{ github.event.workflow_run.head_sha || inputs.sha }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.PI_ARTIFACTS_R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.PI_ARTIFACTS_R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
AWS_EC2_METADATA_DISABLED: 'true'
|
||||
R2: --endpoint-url https://67c0d357268b0fca6e0b465bb9d01b84.r2.cloudflarestorage.com
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ env.SHA }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
|
||||
# The web UI imports pi-ai, whose generated model data is not committed.
|
||||
- name: Hydrate model data
|
||||
run: npm run hydrate:model-data
|
||||
|
||||
- name: Build
|
||||
working-directory: apps/p2
|
||||
run: node --import ./src/source.ts scripts/cloud-bundle.ts "$SHA" "$RUNNER_TEMP/cloud-bundle"
|
||||
|
||||
# The manifest goes last, so a build whose manifest exists is complete.
|
||||
- name: Upload
|
||||
run: |
|
||||
if aws s3api head-object $R2 --bucket pi-ui --key "$SHA/manifest.json" >/dev/null 2>&1; then
|
||||
echo "pi-ui/$SHA exists: kept"
|
||||
exit 0
|
||||
fi
|
||||
cache="public, max-age=31536000, immutable"
|
||||
aws s3 cp $R2 --recursive --exclude manifest.json --cache-control "$cache" \
|
||||
"$RUNNER_TEMP/cloud-bundle/$SHA" "s3://pi-ui/$SHA"
|
||||
aws s3 cp $R2 --cache-control "$cache" \
|
||||
"$RUNNER_TEMP/cloud-bundle/$SHA/manifest.json" "s3://pi-ui/$SHA/manifest.json"
|
||||
|
|
@ -47,12 +47,11 @@ P2_FAKE=1 npm start # http://127.0.0.1:8788, data in ~/.pi/p2
|
|||
immediately. At most two previous generations remain available for up to 60 seconds. This polling is temporary
|
||||
until loader model D provides scoped, per-facet `ClientBundle` pushes (`docs/plugin-loading.md` 5.3 and 12.2 step
|
||||
4). The TUI launcher separately polls `/__p2_tui`. There is no Vite, source-module server or `index.html`.
|
||||
- **Bundles for Radius Cloud Brain:** the public repo `earendil-works/bundled-ui` holds prebuilt copies of this web
|
||||
bundle, built from a pi-private checkout by its `scripts/build.mts` (imports `buildWebBundle`). The script patches
|
||||
three places in the built script: the Chord socket URL (`` new WebSocket(`${...}://${location.host}/chord`) ``),
|
||||
the version poll (`fetch("/__p2_web",{cache:"no-store",credentials:"same-origin"})`) and the `/__p2_web/` asset
|
||||
prefix. It fails when a target is missing, so changing those in `src/hosts/web/main.tsx` or `WEB_BUNDLE_PATH` means
|
||||
updating that script.
|
||||
- **Bundles for Radius Cloud Brain:** after CI passes on main, `.github/workflows/p2-cloud-bundle.yml` builds this
|
||||
web bundle with `scripts/cloud-bundle.ts` and uploads it to the R2 bucket `pi-ui` under the commit's full SHA, which
|
||||
Radius serves under `/pi/ui/`. The script patches the built script's Chord socket URL, version poll and `/__p2_web/`
|
||||
asset prefix, and fails when one is missing: changing those in `src/hosts/web/main.tsx` or `WEB_BUNDLE_PATH` means
|
||||
updating it.
|
||||
- `npm run dev` also watches plugins and supervises runtime restarts; see **Dev mode** below. Under `npm start`,
|
||||
code changes (web included) take effect only after a restart.
|
||||
- **Data report** (`scripts/data-report.ts`, read-only, beside a live server): per session its title, location,
|
||||
|
|
|
|||
61
apps/p2/scripts/cloud-bundle.ts
Normal file
61
apps/p2/scripts/cloud-bundle.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Builds p2's web UI for Radius Cloud Brain into `<out>/<sha>/`: the plugins' web facets, their stylesheet and a
|
||||
// manifest.json naming both. Radius serves the `pi-ui` bucket under `/pi/ui/` and puts the Chord URL in the page's
|
||||
// `#root` `data-pi-bootstrap`; the built script assumes p2 serves it, so its Chord socket and its version poll are
|
||||
// patched. A missing patch target, or anything else still loaded from p2's `/__p2_web/`, fails the build:
|
||||
// `src/hosts/web/main.tsx` or `WEB_BUNDLE_PATH` changed, and this script must follow.
|
||||
//
|
||||
// node --import ./src/source.ts scripts/cloud-bundle.ts <full commit sha> <out dir>
|
||||
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { buildWebBundle } from "../src/hosts/bundles.ts";
|
||||
|
||||
/** The plugins Cloud Brain serves. `web-shell` registers the "app" slot; without it the page stays on "Loading…". */
|
||||
const PLUGINS = ["web-shell", "artifacts", "chat", "drafts", "models", "sessions", "status", "subagents", "workspace"];
|
||||
|
||||
const [sha, out] = process.argv.slice(2);
|
||||
if (!sha || !/^[0-9a-f]{40}$/.test(sha) || !out) throw new Error("usage: cloud-bundle.ts <full commit sha> <out dir>");
|
||||
|
||||
const base = `/pi/ui/${sha}`;
|
||||
const dir = join(out, sha);
|
||||
const bundle = await buildWebBundle(PLUGINS);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
// Source maps embed the sources and the bundle is served publicly: they are left out, and so are the comments naming
|
||||
// them (the last line of the script and of the stylesheet).
|
||||
for (const [file, { body }] of bundle.files) {
|
||||
if (file.endsWith(".map")) continue;
|
||||
let text = new TextDecoder().decode(body);
|
||||
if (file === bundle.script) text = patchScript(text.replace(/\n\/\/# sourceMappingURL=\S+\n?$/, "\n"));
|
||||
else if (file === bundle.style) text = text.replace(/\n\/\*# sourceMappingURL=\S+ \*\/\n?$/, "\n");
|
||||
await writeFile(join(dir, file), text);
|
||||
}
|
||||
const manifest = {
|
||||
version: `pi-${sha}`,
|
||||
script: bundle.script,
|
||||
stylesheets: bundle.style ? [bundle.style] : [],
|
||||
commit: sha,
|
||||
};
|
||||
await writeFile(join(dir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
console.log(`built ${dir}`);
|
||||
|
||||
function patchScript(script: string): string {
|
||||
script = patch(
|
||||
script,
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: the built script's exact text, not a template.
|
||||
'new WebSocket(`${location.protocol==="https:"?"wss":"ws"}://${location.host}/chord`)',
|
||||
'new WebSocket(JSON.parse(document.getElementById("root").dataset.piBootstrap).chordUrl)',
|
||||
);
|
||||
script = patch(
|
||||
script,
|
||||
'fetch("/__p2_web",{cache:"no-store",credentials:"same-origin"})',
|
||||
`fetch("${base}/manifest.json",{cache:"no-store"})`,
|
||||
);
|
||||
if (script.includes("__p2_web")) throw new Error("the built script still names /__p2_web");
|
||||
return script;
|
||||
}
|
||||
|
||||
function patch(source: string, from: string, to: string): string {
|
||||
if (!source.includes(from)) throw new Error(`patch target not found: ${from}`);
|
||||
return source.replaceAll(from, to);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue