diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 00000000..cc6d332b --- /dev/null +++ b/.github/README.md @@ -0,0 +1,71 @@ +# Edge Python CI/CD + +One workflow, [`main.yml`](workflows/main.yml), drives the whole monorepo, so the Actions tab shows a single "CI / CD" run per push/PR with every job as a node in one graph. Each package's logic lives in a **composite action** under [`actions/`](actions); `main.yml` only wires the dependency graph. The composite actions are not workflows and do not appear in the Actions tab. + +`compiler-check`, `runtime-lint` and `cli-lint` start at t=0. If any job fails the dependents never run (`needs:`), so a red build stops the deploys. `docs-build` runs on every event so a PR that breaks the docs is caught early; the deploy chain (`cdn → demo → docs-deploy`) is gated to `main` and pinned to the production branch. The `host` and `std` matrices use `fail-fast: false` so one capability / package failure still reports the others. `cli-lint` runs clippy + check once, then the heavy per-target `cli-release` build runs; `cli-test` waits on `host`, `std`, and the release artifacts. + +## Composite actions + +| Action | Inputs | Role | +|--------|--------|------| +| `compiler` | `mode: check\|build` | check: `cargo shear` + clippy (host and wasm targets). build: build + optimize `compiler_lib.wasm`, test, upload the artifact (and attach it to the GitHub Release on tags) | +| `runtime` | `mode: lint\|test` | lint: `deno lint runtime/`. test: Deno + Playwright suite (Chromium driving `createWorker` against the CDN wasm) | +| `host` | `capability` | Deno-lints and smoke-tests one capability (`dom`, `network`, `storage`, `time`) in headless Chromium. All JS, no release | +| `std` | `package` | Clippy + build + optimize + corpus test for one stdpkg (`json`, `re`, `math` as wasm; `test` is pure Edge Python, so it skips the wasm build and only runs the corpus). Stages `.wasm` / `.py`. No release | +| `cli` | `mode: lint\|release\|test`, `target` | lint: `cargo clippy -D warnings` + `cargo check` (once). release: `cargo build --release` per target → tarball artifact. test: `cargo test` (drives a real Chromium) | +| `demo` | CF token + account | Hashes deps into `version.json` (cache-busting), builds Tailwind, deploys `demo/` to `edge-python-demo` | +| `docs` | `mode: build\|deploy`, CF token + account | build: `npm ci` + `next build` static export (`docs/out`, sitemap via `postbuild`), upload artifact. deploy: pull artifact + push to `edge-python-docs` | +| `cdn-deploy` | CF token + account | Pulls every artifact, stages `./compiler ./runtime ./std ./host ./cli`, one `wrangler pages deploy` to `edge-python-cdn` | + +## Cloudflare Pages + +Three **Direct Upload** projects. Actions push prebuilt directories via `wrangler pages deploy`; Cloudflare doesn't clone or build. + +| Project | Source | Production URL | +|---------|--------|----------------| +| `edge-python-cdn` | `_site/{compiler,runtime,std,host,cli}` (consolidates the old per-package `-runtime` / `-host` / `-std` projects) | `https://edge-python-cdn.pages.dev` | +| `edge-python-demo` | `demo/` (wasm hashed for `version.json`, not bundled) | `https://edge-python-demo.pages.dev` | +| `edge-python-docs` | `docs/out` (Nextra static export) | `https://edgepython.com` (custom domain; also `https://edge-python-docs.pages.dev`) | + +All deploys run **only on pushes to `main`** and are pinned to the production `main` branch. PRs and tags never deploy; the next `main` push refreshes the projects. + +### Cloudflare and GitHub setup + +```bash +# Wrangler CLI (Node 22+) +npx wrangler login +npx wrangler pages project create edge-python-cdn --production-branch=main +npx wrangler pages project create edge-python-demo --production-branch=main +npx wrangler pages project create edge-python-docs --production-branch=main +``` + +`edge-python-docs` serves `edgepython.com` (replacing the old Mintlify docs): after +the first deploy, add `edgepython.com` as a custom domain on the project +(Pages -> Custom domains) and remove it from Mintlify. + +Repo secrets (*Settings -> Secrets and variables -> Actions*): + +- `CLOUDFLARE_API_TOKEN`, `Account -> Cloudflare Pages -> Edit`. Create via dashboard: . +- `CLOUDFLARE_ACCOUNT_ID`, from `npx wrangler whoami` or any dashboard sidebar. + +Rotate: create new token -> update secret -> revoke old token. + +## Releases + +Pushing a `v*` tag runs the pipeline; the `compiler` build job uploads `compiler_lib.wasm` to the matching Release. Tag must match workspace version. + +1. Bump `version` under `[workspace.package]` in root `Cargo.toml` (every crate inherits via `version.workspace = true`). Run `cargo check` to refresh `Cargo.lock`, commit. +2. Tag and push: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +On tag push: `compiler-check` lints, then the `compiler` build job optimizes the artifact and attaches it to a fresh Release with auto-generated notes. The CDN, demo and docs deploys do not run on tags; they already deployed from the preceding `main` push. + +Nothing is published to crates.io, distribution is the `.wasm` on the Release. `starter-module` carries its own version and isn't bumped with the workspace. + +Consumer crates pick up the release automatically: `compiler/Cargo.toml` declares `links = "compiler_lib"` and `compiler/build.rs` downloads `/releases/download/v/compiler_lib.wasm` into `OUT_DIR`. Downstreams read `DEP_COMPILER_LIB_WASM` in their own `build.rs`, see [root README](../../README.md#consume-the-release-from-a-rust-host). Tag bumps flow via `cargo update`. + +Gated behind the default-on `prebuilt` feature. Producer-side compiler steps pass `--no-default-features` to avoid fetching the asset that this same pipeline uploads later. diff --git a/.github/actions/cdn-deploy/action.yml b/.github/actions/cdn-deploy/action.yml new file mode 100644 index 00000000..61cc0d84 --- /dev/null +++ b/.github/actions/cdn-deploy/action.yml @@ -0,0 +1,65 @@ +name: CDN deploy +description: Stage every package output under one tree and deploy it to edge-python-cdn. + +inputs: + cloudflare-api-token: + description: Cloudflare API token. + required: true + cloudflare-account-id: + description: Cloudflare account id. + required: true + +runs: + using: composite + steps: + - name: Download compiler wasm + uses: actions/download-artifact@v8 + with: + name: compiler_lib_wasm + path: /tmp/compiler/ + + - name: Download std artifacts + uses: actions/download-artifact@v8 + with: + pattern: dist-* + path: /tmp/std/ + merge-multiple: true + + - name: Download cli artifacts + uses: actions/download-artifact@v8 + with: + pattern: edge-* + path: /tmp/cli/ + merge-multiple: true + + - name: Stage site + shell: bash + run: | + mkdir -p _site/compiler _site/runtime _site/std _site/host _site/cli + + # compiler: the wasm module. + cp /tmp/compiler/compiler_lib.wasm _site/compiler/ + + # runtime: JS sources plus the wasm it bundles. + cp -r runtime/. _site/runtime/ + cp /tmp/compiler/compiler_lib.wasm _site/runtime/ + + # std: .wasm / .py. + cp -r /tmp/std/. _site/std/ + + # host: flatten each capability's src/ into _site/host//. + for dir in host/*/src; do + cap="$(basename "$(dirname "$dir")")" + mkdir -p "_site/host/$cap" + cp -r "$dir"/. "_site/host/$cap/" + done + + # cli: release tarballs. + cp -r /tmp/cli/. _site/cli/ + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ inputs.cloudflare-api-token }} + accountId: ${{ inputs.cloudflare-account-id }} + command: pages deploy _site --project-name=edge-python-cdn --branch=main diff --git a/.github/actions/cli/action.yml b/.github/actions/cli/action.yml new file mode 100644 index 00000000..65b8b892 --- /dev/null +++ b/.github/actions/cli/action.yml @@ -0,0 +1,89 @@ +name: CLI +description: cli/ is its own workspace. lint = clippy + check; release = build per target; test = cargo test. + +inputs: + mode: + description: "lint | release | test" + required: true + target: + description: Rust target triple (release mode). + required: false + default: "" + +runs: + using: composite + steps: + - name: Toolchain (lint) + if: inputs.mode == 'lint' + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache Rust (lint) + if: inputs.mode == 'lint' + uses: Swatinem/rust-cache@v2 + with: + workspaces: cli -> cli/target + + - name: Clippy and check + if: inputs.mode == 'lint' + shell: bash + working-directory: cli + run: | + cargo clippy --all-targets -- -D warnings + cargo check + + - name: Toolchain (release) + if: inputs.mode == 'release' + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ inputs.target }} + + - name: Install musl tools + if: inputs.mode == 'release' && contains(inputs.target, 'linux-musl') + shell: bash + run: sudo apt-get update && sudo apt-get install -y musl-tools + + - name: Cache Rust (release) + if: inputs.mode == 'release' + uses: Swatinem/rust-cache@v2 + with: + workspaces: cli -> cli/target + key: ${{ inputs.target }} + + - name: Build release + if: inputs.mode == 'release' + shell: bash + working-directory: cli + run: cargo build --release --target "${{ inputs.target }}" + + - name: Tar + if: inputs.mode == 'release' + shell: bash + working-directory: cli + run: tar -C "target/${{ inputs.target }}/release" -czf "edge-${{ inputs.target }}.tar.gz" edge + + - name: Upload release artifact + if: inputs.mode == 'release' + uses: actions/upload-artifact@v6 + with: + name: edge-${{ inputs.target }} + path: cli/edge-${{ inputs.target }}.tar.gz + retention-days: 1 + + - name: Toolchain (test) + if: inputs.mode == 'test' + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust (test) + if: inputs.mode == 'test' + uses: Swatinem/rust-cache@v2 + with: + workspaces: cli -> cli/target + + # Drives a real Chromium; ubuntu-latest ships google-chrome-stable. + - name: Test + if: inputs.mode == 'test' + shell: bash + working-directory: cli + run: cargo test diff --git a/.github/actions/compiler/action.yml b/.github/actions/compiler/action.yml new file mode 100644 index 00000000..e5d76e72 --- /dev/null +++ b/.github/actions/compiler/action.yml @@ -0,0 +1,139 @@ +name: Compiler +description: Rust compiler crate. check = shear + clippy; build = wasm build/opt + test + upload. + +inputs: + mode: + description: "check | build" + required: true + github-token: + description: Token for the tag Release upload (build mode, tags only). + required: false + default: "" + +runs: + using: composite + steps: + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + + - name: Toolchain (clippy) + if: inputs.mode == 'check' + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Install cargo-shear + if: inputs.mode == 'check' + shell: bash + run: cargo install cargo-shear + + # Detects declared but unused dependencies across the workspace. + - name: Shear + if: inputs.mode == 'check' + shell: bash + run: cargo shear + + # --no-default-features skips the prebuilt wasm download in build.rs. + - name: Clippy (host) + if: inputs.mode == 'check' + shell: bash + run: cargo clippy --all-targets --no-default-features -- -D warnings + + - name: Install wasm target (check) + if: inputs.mode == 'check' + shell: bash + run: rustup target add wasm32-unknown-unknown + + - name: Clippy (wasm) + if: inputs.mode == 'check' + shell: bash + run: cargo clippy --lib --target wasm32-unknown-unknown -p edge-python -p slugify-mod -- -D warnings + + # build-std needs nightly plus rust-src. + - name: Toolchain (nightly) + if: inputs.mode == 'build' + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Install wasm target (build) + if: inputs.mode == 'build' + shell: bash + run: rustup target add wasm32-unknown-unknown + + # apt ships an old binaryen, so fetch the upstream release. + - name: Install wasm-opt + if: inputs.mode == 'build' + shell: bash + run: | + curl -sSL https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz \ + | tar -xz --strip-components=2 -C /usr/local/bin binaryen-version_121/bin/wasm-opt + wasm-opt --version + + - name: Build + if: inputs.mode == 'build' + shell: bash + run: | + RUSTFLAGS="-Z location-detail=none -Z fmt-debug=none -Z unstable-options -C panic=immediate-abort" \ + cargo +nightly build \ + --target wasm32-unknown-unknown \ + --lib \ + --release \ + -p edge-python \ + -Z build-std=std,panic_abort + + - name: Size (unoptimized) + if: inputs.mode == 'build' + shell: bash + run: ls -lh target/wasm32-unknown-unknown/release/compiler_lib.wasm + + # Two passes: -Oz with traps-never-happen, then reflatten for a fresh CFG. + - name: Optimize + if: inputs.mode == 'build' + shell: bash + run: | + INPUT=target/wasm32-unknown-unknown/release/compiler_lib.wasm + + wasm-opt -Oz --converge \ + --generate-global-effects \ + --strip-debug --strip-producers \ + --enable-bulk-memory-opt \ + --enable-nontrapping-float-to-int \ + --enable-sign-ext \ + -tnh \ + -o /tmp/wasm_stage1.wasm "$INPUT" + + wasm-opt --flatten --rereloop -Oz -Oz \ + --enable-bulk-memory-opt \ + --enable-nontrapping-float-to-int \ + --enable-sign-ext \ + -o "$INPUT" /tmp/wasm_stage1.wasm + + rm /tmp/wasm_stage1.wasm + + - name: Size (optimized) + if: inputs.mode == 'build' + shell: bash + run: ls -lh target/wasm32-unknown-unknown/release/compiler_lib.wasm + + - name: Test + if: inputs.mode == 'build' + shell: bash + run: cargo test -p edge-python --no-default-features + + - name: Upload wasm artifact + if: inputs.mode == 'build' + uses: actions/upload-artifact@v6 + with: + name: compiler_lib_wasm + path: target/wasm32-unknown-unknown/release/compiler_lib.wasm + retention-days: 1 + + - name: Upload WASM Release + if: inputs.mode == 'build' && startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + token: ${{ inputs.github-token }} + files: target/wasm32-unknown-unknown/release/compiler_lib.wasm + fail_on_unmatched_files: true + generate_release_notes: true diff --git a/.github/actions/demo/action.yml b/.github/actions/demo/action.yml new file mode 100644 index 00000000..da5bd87c --- /dev/null +++ b/.github/actions/demo/action.yml @@ -0,0 +1,48 @@ +name: Demo deploy +description: Hash deps into version.json, build Tailwind, deploy demo/ to edge-python-demo. + +inputs: + cloudflare-api-token: + description: Cloudflare API token. + required: true + cloudflare-account-id: + description: Cloudflare account id. + required: true + +runs: + using: composite + steps: + - uses: actions/setup-node@v6 + with: + node-version: "22" + + # Downloaded for hashing only; the demo loads the wasm from the CDN at runtime. + - name: Download wasm artifact + uses: actions/download-artifact@v8 + with: + name: compiler_lib_wasm + path: /tmp/wasm/ + + # Hash compiler wasm + runtime JS + demo Python entries for cache-busting. + - name: Write version manifest + shell: bash + run: | + HASH=$( { sha256sum /tmp/wasm/compiler_lib.wasm; \ + find runtime -type f | LC_ALL=C sort | xargs sha256sum; \ + find demo/runtime -type f | LC_ALL=C sort | xargs sha256sum; \ + } | sha256sum | cut -c1-12) + printf '{"v":"%s"}\n' "$HASH" > demo/version.json + + - name: Build Tailwind CSS + shell: bash + working-directory: demo + run: | + echo "@tailwind base;@tailwind components;@tailwind utilities;" \ + | npx tailwindcss@3 --input - --output tailwind.css --minify + + - name: Deploy to Cloudflare Pages + uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ inputs.cloudflare-api-token }} + accountId: ${{ inputs.cloudflare-account-id }} + command: pages deploy demo --project-name=edge-python-demo --branch=main diff --git a/.github/actions/docs/action.yml b/.github/actions/docs/action.yml new file mode 100644 index 00000000..baa690db --- /dev/null +++ b/.github/actions/docs/action.yml @@ -0,0 +1,62 @@ +name: Docs +description: Nextra static export. build = next build + upload docs/out; deploy = pull artifact + push to edge-python-docs. + +inputs: + mode: + description: "build | deploy" + required: true + cloudflare-api-token: + description: Cloudflare API token (deploy only). + required: false + default: "" + cloudflare-account-id: + description: Cloudflare account id (deploy only). + required: false + default: "" + +runs: + using: composite + steps: + - name: Setup Node + if: inputs.mode == 'build' + uses: actions/setup-node@v6 + with: + node-version: "22" + cache: npm + cache-dependency-path: docs/package-lock.json + + # Reproducible install from the committed lockfile. + - name: Install + if: inputs.mode == 'build' + shell: bash + working-directory: docs + run: npm ci + + - name: Build + if: inputs.mode == 'build' + shell: bash + working-directory: docs + run: npm run build + + - name: Upload static export + if: inputs.mode == 'build' + uses: actions/upload-artifact@v6 + with: + name: docs-out + path: docs/out + retention-days: 1 + + - name: Download static export + if: inputs.mode == 'deploy' + uses: actions/download-artifact@v8 + with: + name: docs-out + path: docs/out + + - name: Deploy to Cloudflare Pages + if: inputs.mode == 'deploy' + uses: cloudflare/wrangler-action@v4 + with: + apiToken: ${{ inputs.cloudflare-api-token }} + accountId: ${{ inputs.cloudflare-account-id }} + command: pages deploy docs/out --project-name=edge-python-docs --branch=main diff --git a/.github/actions/host/action.yml b/.github/actions/host/action.yml new file mode 100644 index 00000000..625cb490 --- /dev/null +++ b/.github/actions/host/action.yml @@ -0,0 +1,45 @@ +name: Host capability +description: Deno-lint and smoke-test one host capability (all JS) in Chromium. No release. + +inputs: + capability: + description: "dom | network | storage | time" + required: true + +runs: + using: composite + steps: + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Cache Deno modules + uses: actions/cache@v5 + with: + path: ~/.cache/deno + key: deno-${{ runner.os }}-${{ hashFiles('host/**/deno.json', 'host/**/deno.lock') }} + restore-keys: deno-${{ runner.os }}- + + - name: Lint src/ + shell: bash + working-directory: host/${{ inputs.capability }} + run: deno lint src/ + + # ~150MB Chromium; cached so only the first run pays the download. + - name: Cache Playwright browsers + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-chromium + + - name: Install Chromium + shell: bash + run: deno run -A npm:playwright install --with-deps chromium + + # HOSTCAP narrows test discovery to this capability's corpus. + - name: Test tests/ + shell: bash + working-directory: host + env: + HOSTCAP: ${{ inputs.capability }} + run: deno test --allow-all tests/ diff --git a/.github/actions/runtime/action.yml b/.github/actions/runtime/action.yml new file mode 100644 index 00000000..beb31bbe --- /dev/null +++ b/.github/actions/runtime/action.yml @@ -0,0 +1,45 @@ +name: Runtime +description: Deno/JS runtime. lint = deno lint; test = Deno + Playwright suite. + +inputs: + mode: + description: "lint | test" + required: true + +runs: + using: composite + steps: + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Cache Deno modules + uses: actions/cache@v5 + with: + path: ~/.cache/deno + key: deno-${{ runner.os }}-${{ hashFiles('**/deno.json', '**/deno.lock') }} + restore-keys: deno-${{ runner.os }}- + + - name: Lint + if: inputs.mode == 'lint' + shell: bash + run: deno lint runtime/ + + # ~150MB Chromium; cached so only the first run pays the download. + - name: Cache Playwright browsers + if: inputs.mode == 'test' + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-chromium + + # --with-deps installs the OS libs Chromium needs on Linux. + - name: Install Chromium + if: inputs.mode == 'test' + shell: bash + run: deno run -A npm:playwright install --with-deps chromium + + - name: Test + if: inputs.mode == 'test' + shell: bash + run: deno test --allow-all runtime/tests/runtime.test.js diff --git a/.github/actions/std/action.yml b/.github/actions/std/action.yml new file mode 100644 index 00000000..6f93a040 --- /dev/null +++ b/.github/actions/std/action.yml @@ -0,0 +1,142 @@ +name: Std package +description: Lint, build/optimize (wasm), test one stdpkg, then stage its artifact. No release. + +inputs: + package: + description: "json | re | math | test" + required: true + +runs: + using: composite + steps: + # Stable for clippy, nightly for the build-std wasm build. + - name: Toolchain (stable) + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + components: clippy + + - name: Toolchain (nightly) + uses: dtolnay/rust-toolchain@nightly + with: + targets: wasm32-unknown-unknown + components: rust-src + + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Cache Cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + std/${{ inputs.package }}/target + key: cargo-std-${{ runner.os }}-${{ inputs.package }}-${{ hashFiles(format('std/{0}/Cargo.toml', inputs.package)) }} + restore-keys: cargo-std-${{ runner.os }}-${{ inputs.package }}- + + - name: Cache Deno modules + uses: actions/cache@v5 + with: + path: ~/.cache/deno + key: deno-${{ runner.os }}-${{ hashFiles('std/**/deno.json', 'std/**/deno.lock') }} + restore-keys: deno-${{ runner.os }}- + + - name: Cache Playwright browsers + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-chromium + + - name: Install Chromium + shell: bash + run: deno run -A npm:playwright install --with-deps chromium + + # apt ships an old binaryen, so fetch the upstream release. + - name: Install wasm-opt + if: inputs.package != 'test' + shell: bash + run: | + curl -sSL "https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz" \ + | tar -xz --strip-components=2 -C /usr/local/bin "binaryen-version_121/bin/wasm-opt" + wasm-opt --version + + # Lint only the cdylib; --all-targets clashes with its panic handler. + - name: Clippy + if: inputs.package != 'test' + shell: bash + working-directory: std/${{ inputs.package }} + run: cargo +stable clippy --release --target wasm32-unknown-unknown -- -D warnings + + # `test` is pure Edge Python (entry.py): no crate to build. + - name: Build + if: inputs.package != 'test' + shell: bash + working-directory: std/${{ inputs.package }} + run: | + RUSTFLAGS="-Z location-detail=none -Z fmt-debug=none -Z unstable-options -C panic=immediate-abort" \ + cargo +nightly build \ + --target wasm32-unknown-unknown \ + --lib --release \ + -Z build-std=std,panic_abort + + - name: Size (unoptimized) + if: inputs.package != 'test' + shell: bash + run: ls -lh "std/${{ inputs.package }}/target/wasm32-unknown-unknown/release/${{ inputs.package }}.wasm" + + # Two passes: -Oz with traps-never-happen, then reflatten for a fresh CFG. + - name: Optimize + if: inputs.package != 'test' + shell: bash + env: + WASM: std/${{ inputs.package }}/target/wasm32-unknown-unknown/release/${{ inputs.package }}.wasm + run: | + wasm-opt -Oz --converge \ + --generate-global-effects \ + --strip-debug --strip-producers \ + --enable-bulk-memory-opt \ + --enable-nontrapping-float-to-int \ + --enable-sign-ext \ + -tnh \ + -o /tmp/wasm_stage1.wasm "$WASM" + + wasm-opt --flatten --rereloop -Oz -Oz \ + --enable-bulk-memory-opt \ + --enable-nontrapping-float-to-int \ + --enable-sign-ext \ + -o "$WASM" /tmp/wasm_stage1.wasm + + rm /tmp/wasm_stage1.wasm + + - name: Size (optimized) + if: inputs.package != 'test' + shell: bash + run: ls -lh "std/${{ inputs.package }}/target/wasm32-unknown-unknown/release/${{ inputs.package }}.wasm" + + # STDPKG narrows Deno's test discovery to this package's corpus. + - name: Test + shell: bash + working-directory: std + env: + STDPKG: ${{ inputs.package }} + run: deno test --allow-all tests/ + + # Stage native -> .wasm, pure-Python -> .py. + - name: Stage artifact + shell: bash + run: | + mkdir -p _dist + if [ -f "std/${{ inputs.package }}/src/entry.py" ]; then + cp "std/${{ inputs.package }}/src/entry.py" "_dist/${{ inputs.package }}.py" + else + cp "std/${{ inputs.package }}/target/wasm32-unknown-unknown/release/${{ inputs.package }}.wasm" "_dist/${{ inputs.package }}.wasm" + fi + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: dist-${{ inputs.package }} + path: _dist/ + retention-days: 1 diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index 25589758..00000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Edge Python CI/CD - -``` -deno lint -> deno test ┐ -check -> wasm -> runtime -> demo -``` - -| Workflow | Role | -|----------|------| -| `_check.yml` | `cargo shear` + `clippy` (host and wasm targets) | -| `_wasm.yml` | Builds and optimizes `compiler_lib.wasm`. On tags, attaches the `.wasm` to the GitHub Release | -| `_runtime_check.yml` | JS-side gate: `deno lint runtime/` + `deno test runtime/tests/` (Playwright + Chromium driving `createWorker` against the CDN-deployed wasm). Independent branch, runs in parallel with the Rust pipeline; only the CDN upload below blocks on it | -| `_runtime.yml` | Bundles `runtime/` + `compiler_lib.wasm` and deploys them to Cloudflare Pages | -| `_demo.yml` | Hashes `compiler_lib.wasm` into `version.json` (cache-busting) and deploys `demo/` to Cloudflare Pages | -| `cli.yml` | Standalone (not part of the pipeline above): builds and tests `cli/`; on `main` pushes also publishes the release binary + `cli/setup/` scripts (`install.sh`, `uninstall.sh`) to GitHub Pages | -| `host.yml` | Standalone: deno-lints and tests each host capability (`dom`, `network`, `storage`, `time`) in headless Chromium; on `main` pushes also deploys their ESM sources to Cloudflare Pages (`edge-python-host`) | -| `std.yml` | Standalone: clippy + build + optimize + test each stdpkg (`json`, `re`, `math` as wasm; `test` is pure Edge Python, so its steps skip the wasm build and only run the corpus); on `main` pushes also deploys the per-package `.wasm` to Cloudflare Pages (`edge-python-std`) | -| `docs.yml` | Standalone (triggered only by `docs/**` changes): `npm ci` + `next build` static export of the Nextra docs (`docs/out`, sitemap via `postbuild`); PRs build only, `main` pushes also deploy to Cloudflare Pages (`edge-python-docs`) | - -## Cloudflare Pages - -Five **Direct Upload** projects, Actions pushes prebuilt directories via `wrangler pages deploy`; Cloudflare doesn't clone or build. - -| Project | Source | Production URL | -|---------|--------|----------------| -| `edge-python-demo` | `demo/` (wasm hashed for `version.json`, not bundled) | `https://edge-python-demo.pages.dev` | -| `edge-python-runtime` | `runtime/` + bundled `compiler_lib.wasm` | `https://edge-python-runtime.pages.dev` | -| `edge-python-host` | `host//src/` for each capability, flattened to `/` | `https://edge-python-host.pages.dev` | -| `edge-python-std` | per-package optimized `.wasm` from `std//` | `https://edge-python-std.pages.dev` | -| `edge-python-docs` | `docs/out` (Nextra static export) | `https://edgepython.com` (custom domain; also `https://edge-python-docs.pages.dev`) | - -All five deploys run **only on pushes to `main`** and are pinned to the production `main` branch in the matching workflow (`_runtime.yml` / `_demo.yml` / `host.yml` / `std.yml` / `docs.yml`). PRs and tags never deploy; the next `main` push refreshes the projects. - -### Cloudflare and GitHub setup - -```bash -# Wrangler CLI (Node 22+) -npx wrangler login -npx wrangler pages project create edge-python-demo --production-branch=main -npx wrangler pages project create edge-python-runtime --production-branch=main -npx wrangler pages project create edge-python-docs --production-branch=main -``` - -`edge-python-docs` serves `edgepython.com` (replacing the old Mintlify docs): after the first deploy, add `edgepython.com` as a custom domain on the project (Pages -> Custom domains) and remove it from Mintlify. - -Repo secrets (*Settings -> Secrets and variables -> Actions*): - -- `CLOUDFLARE_API_TOKEN`, `Account -> Cloudflare Pages -> Edit`. Create via dashboard: . -- `CLOUDFLARE_ACCOUNT_ID`, from `npx wrangler whoami` or any dashboard sidebar. - -Rotate: create new token -> update secret -> revoke old token. - -## Releases - -Pushing a `v*` tag triggers the pipeline; `_wasm.yml` uploads `compiler_lib.wasm` to the matching Release. Tag must match workspace version. - -1. Bump `version` under `[workspace.package]` in root `Cargo.toml` (every crate inherits via `version.workspace = true`). Run `cargo check` to refresh `Cargo.lock`, commit. -2. Tag and push: - -```bash -git tag v0.1.0 -git push origin v0.1.0 -``` - -On tag push: `_check` lints, `_wasm` builds and optimizes the artifact and attaches it to a fresh Release with auto-generated notes. The CDN deploys (`_runtime` + `_demo`) do not run on tags; they already deployed from the preceding `main` push. - -Nothing is published to crates.io, distribution is the `.wasm` on the Release. `starter-module` carries its own version and isn't bumped with the workspace. - -Consumer crates pick up the release automatically: `compiler/Cargo.toml` declares `links = "compiler_lib"` and `compiler/build.rs` downloads `/releases/download/v/compiler_lib.wasm` into `OUT_DIR`. Downstreams read `DEP_COMPILER_LIB_WASM` in their own `build.rs`, see [root README](../../README.md#consume-the-release-from-a-rust-host). Tag bumps flow via `cargo update`. - -Gated behind the default-on `prebuilt` feature. Producer-side steps (`_check`, `_wasm`) pass `--no-default-features` to avoid fetching the asset that this same pipeline uploads later. diff --git a/.github/workflows/_check.yml b/.github/workflows/_check.yml deleted file mode 100644 index 0e210c8e..00000000 --- a/.github/workflows/_check.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: _Check - -on: - workflow_call: - -jobs: - lint: - name: Clippy & Shear - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: Cache Rust - uses: Swatinem/rust-cache@v2 - - - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - - name: Install cargo-shear - run: cargo install cargo-shear - - # Detects declared but unused dependencies across the whole workspace. - - name: Shear (unused deps) - run: cargo shear - - # Host clippy. Wasm crates excluded via default-members. `--no-default-features` disables the producer's `prebuilt` feature so `compiler/build.rs` doesn't try to download the release wasm asset (which doesn't exist yet on the first publish of a new tag). - - name: Clippy (host / tests) - run: cargo clippy --all-targets --no-default-features -- -D warnings - - # Lint for wasm target. - - name: Install wasm target - run: rustup target add wasm32-unknown-unknown - - - name: Clippy (wasm) - run: cargo clippy --lib --target wasm32-unknown-unknown -p edge-python -p slugify-mod -- -D warnings diff --git a/.github/workflows/_demo.yml b/.github/workflows/_demo.yml deleted file mode 100644 index ff9fbe2a..00000000 --- a/.github/workflows/_demo.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: _Demo Deploy - -on: - workflow_call: - -jobs: - deploy: - name: Cloudflare Pages - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: "22" - - # Download the .wasm for hashing only; the demo loads it from the CDN at runtime (https://runtime.edgepython.com/js/compiler_lib.wasm), so it does not ship inside demo/. - - name: Download wasm artifact - uses: actions/download-artifact@v8 - with: - name: compiler_lib_wasm - path: /tmp/wasm/ - - # Hash all worker dependencies: the compiler_lib.wasm binary, runtime JS sources from edge-python-runtime, and Python entry files from edge-python-demo - - name: Write version manifest - run: | - HASH=$( { sha256sum /tmp/wasm/compiler_lib.wasm; \ - find runtime -type f | LC_ALL=C sort | xargs sha256sum; \ - find demo/runtime -type f | LC_ALL=C sort | xargs sha256sum; \ - } | sha256sum | cut -c1-12) - printf '{"v":"%s"}\n' "$HASH" > demo/version.json - - - name: Build Tailwind CSS - working-directory: demo - run: | - echo "@tailwind base;@tailwind components;@tailwind utilities;" \ - | npx tailwindcss@3 --input - --output tailwind.css --minify - - - name: Deploy to Cloudflare Pages - uses: cloudflare/wrangler-action@v4 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - # Pin to the Pages production branch. See the matching comment in _runtime.yml. - command: pages deploy demo --project-name=edge-python-demo --branch=main diff --git a/.github/workflows/_runtime.yml b/.github/workflows/_runtime.yml deleted file mode 100644 index 2af985c3..00000000 --- a/.github/workflows/_runtime.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: _Runtime CDN Upload - -on: - workflow_call: - -jobs: - deploy: - name: Cloudflare Pages - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: "22" - - # Download the .wasm already compiled and optimized by _wasm.yml. - - name: Download wasm artifact - uses: actions/download-artifact@v8 - with: - name: compiler_lib_wasm - path: runtime/ - - # Stage runtime/ under /js/ so Pages serves it at runtime.edgepython.com/js/*. Reserved siblings: /wasi/ for the future WASI runtime. - - name: Stage publish dir - run: | - mkdir -p _site - cp -r runtime _site/js - - - name: Deploy to Cloudflare Pages - uses: cloudflare/wrangler-action@v4 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - # Pin to the Pages production branch. The job only runs on main pushes (gated in pipeline.yml). - command: pages deploy _site --project-name=edge-python-runtime --branch=main diff --git a/.github/workflows/_runtime_check.yml b/.github/workflows/_runtime_check.yml deleted file mode 100644 index 4824f8c9..00000000 --- a/.github/workflows/_runtime_check.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: _Runtime Check - -on: - workflow_call: - -jobs: - lint: - name: Deno Lint - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Cache Deno modules - uses: actions/cache@v5 - with: - path: ~/.cache/deno - key: deno-${{ runner.os }}-${{ hashFiles('**/deno.json', '**/deno.lock') }} - restore-keys: deno-${{ runner.os }}- - - - name: Lint runtime/ - run: deno lint runtime/ - - test: - name: Deno Test (Playwright + CDN wasm) - needs: lint - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Cache Deno modules - uses: actions/cache@v5 - with: - path: ~/.cache/deno - key: deno-${{ runner.os }}-${{ hashFiles('**/deno.json', '**/deno.lock') }} - restore-keys: deno-${{ runner.os }}- - - # ~150MB Chromium binary; cached across runs so only the first run pays the download. - - name: Cache Playwright browsers - uses: actions/cache@v5 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-chromium - - # --with-deps installs OS-level libs (libnss, libatk, ...) required by Chromium on Linux runners. - - name: Install Chromium - run: deno run -A npm:playwright install --with-deps chromium - - - name: Test runtime/tests/ - run: deno test --allow-all runtime/tests/runtime.test.js diff --git a/.github/workflows/_wasm.yml b/.github/workflows/_wasm.yml deleted file mode 100644 index 10cf5f1b..00000000 --- a/.github/workflows/_wasm.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: _WebAssembly Build - -on: - workflow_call: - -jobs: - build: - name: WebAssembly - runs-on: ubuntu-latest - permissions: - contents: write - - steps: - - uses: actions/checkout@v6 - - - name: Cache Rust - uses: Swatinem/rust-cache@v2 - - - uses: dtolnay/rust-toolchain@nightly - with: - components: rust-src - - - name: Install wasm target - run: rustup target add wasm32-unknown-unknown - - - name: Install wasm-opt - run: | - curl -sSL https://github.com/WebAssembly/binaryen/releases/download/version_121/binaryen-version_121-x86_64-linux.tar.gz \ - | tar -xz --strip-components=2 -C /usr/local/bin binaryen-version_121/bin/wasm-opt - wasm-opt --version - - - name: Build - run: | - RUSTFLAGS="-Z location-detail=none -Z fmt-debug=none -Z unstable-options -C panic=immediate-abort" \ - cargo +nightly build \ - --target wasm32-unknown-unknown \ - --lib \ - --release \ - -p edge-python \ - -Z build-std=std,panic_abort - - - name: Size (unoptimized) - run: ls -lh target/wasm32-unknown-unknown/release/compiler_lib.wasm - - - name: Optimize - run: | - INPUT=target/wasm32-unknown-unknown/release/compiler_lib.wasm - - wasm-opt -Oz --converge \ - --generate-global-effects \ - --strip-debug --strip-producers \ - --enable-bulk-memory-opt \ - --enable-nontrapping-float-to-int \ - --enable-sign-ext \ - -tnh \ - -o /tmp/wasm_stage1.wasm "$INPUT" - - wasm-opt --flatten --rereloop -Oz -Oz \ - --enable-bulk-memory-opt \ - --enable-nontrapping-float-to-int \ - --enable-sign-ext \ - -o "$INPUT" /tmp/wasm_stage1.wasm - - rm /tmp/wasm_stage1.wasm - - - name: Size (optimized) - run: ls -lh target/wasm32-unknown-unknown/release/compiler_lib.wasm - - # `--no-default-features` disables the producer's `prebuilt` feature so `compiler/build.rs` doesn't try to fetch the release wasm asset (which is uploaded later in this job on a first-tag publish). - - name: Test - run: cargo test -p edge-python --no-default-features - - - name: Upload wasm artifact - uses: actions/upload-artifact@v6 - with: - name: compiler_lib_wasm - path: target/wasm32-unknown-unknown/release/compiler_lib.wasm - retention-days: 1 - - - name: Upload WASM Release - uses: softprops/action-gh-release@v2 - if: startsWith(github.ref, 'refs/tags/') - with: - files: target/wasm32-unknown-unknown/release/compiler_lib.wasm - fail_on_unmatched_files: true - generate_release_notes: true diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml deleted file mode 100644 index b5a94fa1..00000000 --- a/.github/workflows/cli.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: CLI - -on: - push: - branches: [main] - pull_request: - branches: [main] - -permissions: - contents: read - -jobs: - build_test: - name: Build & Test - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@stable - - # `cli/` is a separate workspace; key the cache off its Cargo.lock and target dir. - - name: Cache Rust - uses: Swatinem/rust-cache@v2 - with: - workspaces: cli -> cli/target - - # Engine tests drive a real Chromium; ubuntu-latest ships google-chrome-stable preinstalled. - - name: Build - working-directory: cli - run: cargo build - - - name: Test - working-directory: cli - run: cargo test - - # One native build per target; GitHub's free ARM/macOS runners avoid the cross-compile pain. - build_release: - name: Build release (${{ matrix.target }}) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: build_test - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-musl - runner: ubuntu-latest - - target: aarch64-unknown-linux-musl - runner: ubuntu-24.04-arm - - target: x86_64-apple-darwin - runner: macos-latest - - target: aarch64-apple-darwin - runner: macos-latest - runs-on: ${{ matrix.runner }} - - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install musl tools - if: contains(matrix.target, 'linux-musl') - run: sudo apt-get update && sudo apt-get install -y musl-tools - - - name: Cache Rust - uses: Swatinem/rust-cache@v2 - with: - workspaces: cli -> cli/target - key: ${{ matrix.target }} - - - name: Build release - working-directory: cli - run: cargo build --release --target ${{ matrix.target }} - - - name: Tar - working-directory: cli - run: tar -C target/${{ matrix.target }}/release -czf edge-${{ matrix.target }}.tar.gz edge - - - uses: actions/upload-artifact@v4 - with: - name: edge-${{ matrix.target }} - path: cli/edge-${{ matrix.target }}.tar.gz - - pages: - name: Publish to GitHub Pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: build_release - runs-on: ubuntu-latest - permissions: - contents: read - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deploy.outputs.page_url }} - - steps: - - uses: actions/checkout@v6 - - - name: Collect tarballs and stage Pages site - run: | - mkdir -p _site - cp cli/setup/*.sh _site/ - - - uses: actions/download-artifact@v4 - with: - path: _site - pattern: edge-* - merge-multiple: true - - - uses: actions/upload-pages-artifact@v3 - with: - path: _site - - - id: deploy - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 217598f9..00000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Docs - -on: - push: - branches: [main] - paths: ["docs/**", ".github/workflows/docs.yml"] - pull_request: - branches: [main] - paths: ["docs/**", ".github/workflows/docs.yml"] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - # Build the docs static export and deploy it to Cloudflare Pages. - deploy: - name: Cloudflare Pages - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: "22" - cache: npm - cache-dependency-path: docs/package-lock.json - - # Reproducible install from the committed lockfile. - - name: Install - working-directory: docs - run: npm ci - - # `output: 'export'` makes `next build` emit a fully static site to docs/out. - - name: Build - working-directory: docs - run: npm run build - - # Deploy only on main pushes; PRs stop after a successful build. - - name: Deploy to Cloudflare Pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: cloudflare/wrangler-action@v4 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - # Pin to the Pages production branch (job is already gated to main). - command: pages deploy docs/out --project-name=edge-python-docs --branch=main diff --git a/.github/workflows/host.yml b/.github/workflows/host.yml deleted file mode 100644 index b049c554..00000000 --- a/.github/workflows/host.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Host - -on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - # Deno lint per capability. Anchors the matrix so downstream jobs reuse it. - lint: - name: Lint (${{ matrix.capability }}) - runs-on: ubuntu-latest - strategy: &capability-matrix - fail-fast: false - matrix: - capability: [dom, network, storage, time] - steps: - - uses: actions/checkout@v6 - - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - # Stays consistent with the test job's cache; protects host packages that lint remote-imported modules. - - name: Cache Deno modules - uses: actions/cache@v5 - with: - path: ~/.cache/deno - key: deno-${{ runner.os }}-${{ hashFiles('host/**/deno.json', 'host/**/deno.lock') }} - restore-keys: deno-${{ runner.os }}- - - - name: Lint src/ - working-directory: host/${{ matrix.capability }} - run: deno lint src/ - - # Smoke test each capability against the real demo in headless Chromium. Gated on lint to avoid wasting Chromium time. - test: - name: Test (${{ matrix.capability }}) - needs: lint - runs-on: ubuntu-latest - strategy: *capability-matrix - steps: - - uses: actions/checkout@v6 - - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Cache Deno modules - uses: actions/cache@v5 - with: - path: ~/.cache/deno - key: deno-${{ runner.os }}-${{ hashFiles('host/**/deno.json', 'host/**/deno.lock') }} - restore-keys: deno-${{ runner.os }}- - - # ~150MB Chromium; cached across runs so only the first run pays the download. - - name: Cache Playwright browsers - uses: actions/cache@v5 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-chromium - - # --with-deps installs OS-level libs Chromium needs. Idempotent: cache-hit makes it a no-op. - - name: Install Chromium - run: deno run -A npm:playwright install --with-deps chromium - - # `HOSTCAP` narrows test discovery to this capability's corpus. - - name: Test tests/ - working-directory: host - env: - HOSTCAP: ${{ matrix.capability }} - run: deno test --allow-all tests/ - - # Publish every capability's ESM to Cloudflare Pages on pushes to main only. - deploy: - name: Deploy - needs: test - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - # Host serves its ESM sources, so checkout the tree. - - uses: actions/checkout@v6 - - # Flatten each capability's src/ into _site/ so the public path is /index.js; siblings move together, so relative imports stay valid. - - name: Assemble site - run: | - mkdir -p _site - for dir in host/*/src; do - cap="$(basename "$(dirname "$dir")")" - mkdir -p "_site/$cap" - cp -r "$dir"/. "_site/$cap/" - done - - # Pages serves the modules at host.edgepython.com//*. - - name: Deploy - uses: cloudflare/wrangler-action@v4 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy _site --project-name=edge-python-host --branch=main diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..a861fdb0 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,180 @@ +name: CI / CD + +# Single pipeline for the whole monorepo. Each job's logic lives in a composite action under .github/actions/; this file only wires the dependency graph. + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + compiler-check: + name: Compiler / Lint and Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/compiler + with: + mode: check + + runtime-lint: + name: Runtime / Deno Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/runtime + with: + mode: lint + + compiler: + name: Compiler / Test and Release + needs: [compiler-check, runtime-lint] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/compiler + with: + mode: build + github-token: ${{ github.token }} + + runtime: + name: Runtime / Deno Test + needs: [compiler] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/runtime + with: + mode: test + + host: + name: Host / ${{ matrix.capability }} + needs: [runtime] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + capability: [dom, network, storage, time] + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/host + with: + capability: ${{ matrix.capability }} + + std: + name: Std / ${{ matrix.package }} + needs: [runtime] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + package: [json, re, math, test] + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/std + with: + package: ${{ matrix.package }} + + # Clippy + check once (no matrix); gates the release build. + cli-lint: + name: CLI / Lint and Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/cli + with: + mode: lint + + # Heavy release build per target; starts as soon as cli-lint is green. + cli-release: + name: CLI / Release (${{ matrix.target }}) + needs: [cli-lint] + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-musl + runner: ubuntu-latest + - target: aarch64-unknown-linux-musl + runner: ubuntu-24.04-arm + - target: x86_64-apple-darwin + runner: macos-latest + - target: aarch64-apple-darwin + runner: macos-latest + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/cli + with: + mode: release + target: ${{ matrix.target }} + + cli-test: + name: CLI / Test + needs: [host, std, cli-release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/cli + with: + mode: test + + cdn: + name: Cloudflare Upload (CDN) + needs: [cli-test] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/cdn-deploy + with: + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + demo: + name: Demo + needs: [cdn] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/demo + with: + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + + # Builds on every run (PR gate) and uploads the static export for docs-deploy. + docs-build: + name: Docs / Build + needs: [cli-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/docs + with: + mode: build + + # Tail of the cdn -> demo -> docs deploy chain; main pushes only. + docs-deploy: + name: Docs / Deploy + needs: [demo, docs-build] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/docs + with: + mode: deploy + cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }} + cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml deleted file mode 100644 index 53b21927..00000000 --- a/.github/workflows/pipeline.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: CI / CD - -on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] - -jobs: - # Lint and check the codebase, including running clippy and checking for unused code. - check: - name: Lint and Check - uses: ./.github/workflows/_check.yml - secrets: inherit - - # Build the release WebAssembly artifact. - wasm: - name: WebAssembly - needs: check - permissions: - contents: write - uses: ./.github/workflows/_wasm.yml - secrets: inherit - - # JS-side gate: lint runtime/ and run the Deno + Playwright suite against the CDN-deployed wasm. Independent of the Rust pipeline above, runs in parallel with `wasm`, converges at `runtime` below. - runtime_check: - name: Runtime JS Check - uses: ./.github/workflows/_runtime_check.yml - secrets: inherit - - # Upload runtime (JS + WASM) to the CDN. Convergence point: blocked on both `wasm` (artifact) and `runtime_check` (JS gate). - runtime: - name: Upload Runtime to CDN - needs: [wasm, runtime_check] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - permissions: - contents: read - uses: ./.github/workflows/_runtime.yml - secrets: inherit - - # Build the demo after the runtime upload, since the demo consumes the CDN-hosted runtime. - demo: - name: Build Demo - needs: runtime - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - permissions: - contents: read - uses: ./.github/workflows/_demo.yml - secrets: inherit diff --git a/.github/workflows/std.yml b/.github/workflows/std.yml deleted file mode 100644 index aab1bf96..00000000 --- a/.github/workflows/std.yml +++ /dev/null @@ -1,196 +0,0 @@ -name: Std - -on: - push: - branches: [main] - tags: ["v*"] - pull_request: - branches: [main] - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - BINARYEN_VERSION: "121" - -jobs: - # Clippy each stdpkg on wasm32. Anchors the matrix so downstream jobs reuse it. - lint: - name: Lint (${{ matrix.package }}) - runs-on: ubuntu-latest - strategy: &package-matrix - fail-fast: false - matrix: - package: [json, re, math, test] - steps: - - uses: actions/checkout@v6 - - - uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown - components: clippy - - # Stable prefix avoids the nightly job's cache. Keyed per package so each shard owns its slot. - - name: Cache Cargo - uses: actions/cache@v5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - std/${{ matrix.package }}/target - key: cargo-stable-${{ runner.os }}-${{ matrix.package }}-${{ hashFiles(format('std/{0}/Cargo.toml', matrix.package)) }} - restore-keys: cargo-stable-${{ runner.os }}-${{ matrix.package }}- - - # Lint only the cdylib; --all-targets clashes with its panic handler. - - name: Clippy src/ - if: matrix.package != 'test' - working-directory: std/${{ matrix.package }} - run: cargo clippy --release --target wasm32-unknown-unknown -- -D warnings - - # Build, optimize, and test each package; uploads the wasm artifact for the deploy step. - wasm: - name: WASM (${{ matrix.package }}) - needs: lint - runs-on: ubuntu-latest - strategy: *package-matrix - env: - WASM: std/${{ matrix.package }}/target/wasm32-unknown-unknown/release/${{ matrix.package }}.wasm - steps: - - uses: actions/checkout@v6 - - # build-std needs nightly plus rust-src. - - uses: dtolnay/rust-toolchain@nightly - with: - targets: wasm32-unknown-unknown - components: rust-src - - - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - # Nightly prefix avoids the lint job's cache. - - name: Cache Cargo - uses: actions/cache@v5 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - std/${{ matrix.package }}/target - key: cargo-nightly-${{ runner.os }}-${{ matrix.package }}-${{ hashFiles(format('std/{0}/Cargo.toml', matrix.package)) }} - restore-keys: cargo-nightly-${{ runner.os }}-${{ matrix.package }}- - - - name: Cache Deno modules - uses: actions/cache@v5 - with: - path: ~/.cache/deno - key: deno-${{ runner.os }}-${{ hashFiles('std/**/deno.json', 'std/**/deno.lock') }} - restore-keys: deno-${{ runner.os }}- - - - name: Cache Playwright browsers - uses: actions/cache@v5 - with: - path: ~/.cache/ms-playwright - key: playwright-${{ runner.os }}-chromium - - - name: Install Chromium - run: deno run -A npm:playwright install --with-deps chromium - - # apt ships an old binaryen, so fetch the upstream release. - - name: Install wasm-opt - if: matrix.package != 'test' - run: | - curl -sSL "https://github.com/WebAssembly/binaryen/releases/download/version_${BINARYEN_VERSION}/binaryen-version_${BINARYEN_VERSION}-x86_64-linux.tar.gz" \ - | tar -xz --strip-components=2 -C /usr/local/bin "binaryen-version_${BINARYEN_VERSION}/bin/wasm-opt" - wasm-opt --version - - # `test` is pure Edge Python (src/entry.py): no crate to compile, so skip the wasm build/optimize/upload and let the corpus run below. - - name: Build - if: matrix.package != 'test' - working-directory: std/${{ matrix.package }} - run: | - RUSTFLAGS="-Z location-detail=none -Z fmt-debug=none -Z unstable-options -C panic=immediate-abort" \ - cargo +nightly build \ - --target wasm32-unknown-unknown \ - --lib --release \ - -Z build-std=std,panic_abort - - - name: Size (unoptimized) - if: matrix.package != 'test' - run: ls -lh "$WASM" - - # Two passes: -Oz with traps-never-happen, then reflatten for a fresh CFG. - - name: Optimize - if: matrix.package != 'test' - run: | - wasm-opt -Oz --converge \ - --generate-global-effects \ - --strip-debug --strip-producers \ - --enable-bulk-memory-opt \ - --enable-nontrapping-float-to-int \ - --enable-sign-ext \ - -tnh \ - -o /tmp/wasm_stage1.wasm "$WASM" - - wasm-opt --flatten --rereloop -Oz -Oz \ - --enable-bulk-memory-opt \ - --enable-nontrapping-float-to-int \ - --enable-sign-ext \ - -o "$WASM" /tmp/wasm_stage1.wasm - - rm /tmp/wasm_stage1.wasm - - - name: Size (optimized) - if: matrix.package != 'test' - run: ls -lh "$WASM" - - # STDPKG narrows Deno's test discovery to this package's corpus. The driver routes .py packages to src/entry.py, native ones to the built wasm. - - name: Test - working-directory: std - env: - STDPKG: ${{ matrix.package }} - run: deno test --allow-all tests/ - - # Stage the deployable under its canonical CDN name: native -> .wasm, pure-Python (src/entry.py) -> .py. - - name: Stage artifact - run: | - mkdir -p _dist - if [ -f "std/${{ matrix.package }}/src/entry.py" ]; then - cp "std/${{ matrix.package }}/src/entry.py" "_dist/${{ matrix.package }}.py" - else - cp "$WASM" "_dist/${{ matrix.package }}.wasm" - fi - - - uses: actions/upload-artifact@v6 - with: - name: dist-${{ matrix.package }} - path: _dist/ - retention-days: 1 - - # Publish every per-package artifact (.wasm + .py) to Cloudflare Pages on pushes to main only. - deploy: - name: Deploy - needs: wasm - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - # Pull every per-package artifact uploaded by the matrix above. - - name: Download artifacts - uses: actions/download-artifact@v8 - with: - pattern: dist-* - path: _site/ - merge-multiple: true - - # Pages serves each package at std.edgepython.com/* (e.g. json.wasm, test.py). - - name: Deploy - uses: cloudflare/wrangler-action@v4 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy _site --project-name=edge-python-std --branch=main diff --git a/cli/src/uninstall.rs b/cli/src/uninstall.rs index e18de951..26e3f329 100644 --- a/cli/src/uninstall.rs +++ b/cli/src/uninstall.rs @@ -27,11 +27,10 @@ pub fn run() -> Result<()> { // Tell the script which prompt path the user already answered. cmd.env("EDGE_UNINSTALL_REMOVE_BROWSER", if remove_browser { "1" } else { "0" }); // Point at the install dir derived from where this binary lives, so non-default installs still clean up. - if let Ok(exe) = std::env::current_exe() { - if let Some(dir) = exe.parent() { + if let Ok(exe) = std::env::current_exe() + && let Some(dir) = exe.parent() { cmd.env("EDGE_INSTALL_DIR", dir); } - } let status = cmd.status().map_err(|e| anyhow!("running bash: {e}"))?; let _ = std::fs::remove_file(&temp);