emit(ruby): root a constant its own namespace shadows, without losing… #1456
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| # Only one Pages deployment at a time. | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: false | |
| env: | |
| CARGO_TERM_COLOR: always | |
| # The Rails apps the /ide/ + /playground/ demos ship (in-browser). Pinned | |
| # so the page — and any blog post's claims about it — stay stable against | |
| # upstream churn; bump deliberately. Each bundle embeds the app's LICENSE | |
| # and its commit (bundle-src.mjs). Lobsters is the ruby-bench benchmark | |
| # copy (MIT); campfire is Basecamp's ONCE Campfire (MIT) — the write+push | |
| # app (Action Cable, turbo_stream broadcasts) and the current climb; | |
| # Mastodon (AGPL) is the scale case. | |
| MASTODON_SHA: 163f96cee4dea23365bff9b433871e68d20d9ee7 | |
| RUBY_BENCH_SHA: d771f81f1ce9db51376e03ca7b8e6a83160556d4 | |
| CAMPFIRE_SHA: 2aa4141077141335e79cb0eac5a0eec1a57b2772 | |
| jobs: | |
| # ------------------------------------------------------------------- | |
| # Generate the real-blog fixture once, upload as artifact. Every | |
| # downstream test job downloads + extracts it — pay the Ruby/Rails | |
| # install cost once per CI run instead of in each of seven jobs. | |
| # ------------------------------------------------------------------- | |
| generate-fixture: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Set up Ruby | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: "3.4" | |
| - name: Install Rails | |
| run: gem install rails | |
| - name: Generate fixtures/real-blog | |
| run: bin/rh fixture | |
| - name: Pack fixture | |
| run: | | |
| tar --exclude='./fixtures/real-blog/tmp' \ | |
| --exclude='./fixtures/real-blog/log' \ | |
| -czf real-blog.tar.gz fixtures/real-blog | |
| - name: Upload fixture | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: real-blog-fixture | |
| path: real-blog.tar.gz | |
| retention-days: 1 | |
| # ------------------------------------------------------------------- | |
| # Build the in-browser compiler wasm (roundhouse_wasm.wasm) from source | |
| # and upload it as an artifact for build-site. Runs in PARALLEL with | |
| # generate-fixture — they're independent (the wasm embeds the compiler; | |
| # the fixture is runtime input fed in the browser), so this multi-minute | |
| # LTO build hides behind work build-site already waits on, adding ~0 to | |
| # the critical path to deploy. Rebuilds via the in-repo vendored | |
| # ruby-rbs-sys (wasm/vendor/ — wasm32 build support upstream-pending as | |
| # ruby/rbs#2992), so the 3.8 MB binary is NOT committed and the published | |
| # demos (/playground/, /studio/) always track main. The only external | |
| # need is the WASI SDK (a manual tarball, cached here). | |
| # ------------------------------------------------------------------- | |
| build-wasm: | |
| runs-on: ubuntu-latest | |
| env: | |
| WASI_SDK_VERSION: "33.0" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-wasip1 | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: wasm | |
| - name: Install libclang (host bindgen) | |
| run: sudo apt-get update && sudo apt-get install -y libclang-dev | |
| - name: Cache WASI SDK | |
| id: wasi-sdk | |
| uses: actions/cache@v4 | |
| with: | |
| path: /opt/wasi-sdk | |
| key: wasi-sdk-${{ env.WASI_SDK_VERSION }}-x86_64-linux | |
| - name: Install WASI SDK | |
| if: steps.wasi-sdk.outputs.cache-hit != 'true' | |
| run: | | |
| ver="${WASI_SDK_VERSION}"; major="${ver%%.*}" | |
| url="https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${major}/wasi-sdk-${ver}-x86_64-linux.tar.gz" | |
| curl -fsSL "$url" -o /tmp/wasi-sdk.tar.gz | |
| sudo mkdir -p /opt/wasi-sdk | |
| sudo tar -xzf /tmp/wasi-sdk.tar.gz -C /opt/wasi-sdk --strip-components=1 | |
| - name: Build roundhouse_wasm.wasm | |
| env: | |
| WASI_SDK_PATH: /opt/wasi-sdk | |
| run: cargo build --release --target wasm32-wasip1 --manifest-path wasm/Cargo.toml | |
| - name: Upload wasm artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: roundhouse-wasm | |
| path: wasm/target/wasm32-wasip1/release/roundhouse_wasm.wasm | |
| retention-days: 1 | |
| # ------------------------------------------------------------------- | |
| # Build the spinel AOT compiler from matz/spinel@master and publish | |
| # the toolchain as an artifact. Downstream toolchain-spinel / | |
| # compare-spinel jobs download + chmod +x and either prepend to PATH | |
| # or pass SPINEL=.../spinel to the scaffold Makefile (runtime/spinel/ | |
| # scaffold/Makefile honors SPINEL ?= spinel). | |
| # | |
| # The single `spinel` binary resolves its `spinel_rbs_extract` sidecar | |
| # and `lib/` relative to its own directory, so the tree ships as a unit. | |
| # Tracking master rather than pinning a SHA — bump to a pin if upstream | |
| # churn turns flaky. | |
| # ------------------------------------------------------------------- | |
| build-spinel: | |
| # Re-enabled 2026-06-17 (framework-tests-spinel 5/5 + compare-spinel 7/7 | |
| # green again on matz/spinel master). Tracks matz/spinel master UNPINNED, | |
| # so it can go red independently of any roundhouse change; continue-on-error | |
| # keeps that advisory rather than failing the whole run (the downstream | |
| # framework-tests / compare-spinel jobs are likewise advisory). If a build | |
| # failure here cascades to skipped/failed downstream jobs, that's fine — the | |
| # signal is still visible without blocking unrelated work. (toolchain-spinel | |
| # stays gated below pending matz/spinel#1447.) | |
| continue-on-error: true | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| repository: matz/spinel | |
| ref: master | |
| path: spinel-src | |
| - name: make deps (fetch vendored libprism) | |
| run: make -C spinel-src deps | |
| - name: make all | |
| run: make -C spinel-src all | |
| - name: Stage artifact tree | |
| # `make all` now builds a single `spinel` binary (a top-level symlink | |
| # to bin/spinel) plus the `spinel_rbs_extract` sidecar under build/; | |
| # the legacy spinel_parse/spinel_analyze/spinel_codegen split moved | |
| # under legacy/ and is no longer produced. (The build also emits | |
| # bin/spinel-{doctor,reduce,flatten}, which roundhouse doesn't ship.) | |
| # The single binary locates both `spinel_rbs_extract` and `lib/` | |
| # relative to the directory it is invoked from, so they must ship | |
| # alongside it. The extractor is REQUIRED for the --rbs path | |
| # (toolchain-spinel / compare-spinel); without it, --rbs is silently | |
| # a no-op and the seeded path goes untested. `cp -L` dereferences the | |
| # symlink to copy the real binary. | |
| # `spin` (spinel's project tool, a native binary spinel compiles | |
| # from tools/spin.rb) ships too: the spinel archive is a spin | |
| # package and smoke-spinel's README blocks drive `spin build` / | |
| # `spin test`. spin locates the compiler as its PATH sibling. | |
| # `packages/` ships as well: the compiler resolves bundled spin | |
| # packages (json, set, …) at `<lib-parent>/packages` — a plain | |
| # `require "json"` in runtime code (runtime/spinel/json.rb since | |
| # 72029f0a) silently resolves NOTHING without it, and every | |
| # spinel job goes red on `undefined method 'generate'`. | |
| run: | | |
| mkdir spinel-dist | |
| cp -L spinel-src/spinel spinel-dist/spinel | |
| cp spinel-src/bin/spin spinel-dist/spin | |
| cp spinel-src/build/spinel_rbs_extract spinel-dist/ | |
| cp -R spinel-src/lib spinel-dist/lib | |
| cp -R spinel-src/packages spinel-dist/packages | |
| - name: Upload spinel toolchain | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: spinel-dist | |
| path: spinel-dist | |
| retention-days: 1 | |
| # ------------------------------------------------------------------- | |
| # Library correctness. Runs the default (non-ignored) test suite: | |
| # ingest / analyze / IR round-trip / per-target emit snapshot tests. | |
| # ------------------------------------------------------------------- | |
| unit: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: cargo test | |
| run: cargo test --all-targets | |
| # ------------------------------------------------------------------- | |
| # One job per target language. Each generates a project from the | |
| # fixtures in-process and runs the target's real toolchain over it. | |
| # | |
| # SHRUNK by the smoke consolidation (see the smoke-<target> matrix): | |
| # the smoke jobs run each archive's full build + model/controller | |
| # test + Playwright legs from the published tgz, so toolchain jobs | |
| # whose only coverage was real-blog build/test (go, rust, kotlin, | |
| # swift) are retired — their harnesses remain as dev-loop tools | |
| # (cargo test --test <t>_toolchain -- --ignored). The survivors stay | |
| # for coverage smoke can't reach: tiny-blog gates (crystal, elixir, | |
| # python — the absent-feature shape real-blog can't test) and | |
| # typescript's dual-profile tsc + node:test (async coloring). | |
| # ------------------------------------------------------------------- | |
| toolchain-crystal: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Crystal | |
| uses: crystal-lang/install-crystal@v1 | |
| with: | |
| crystal: latest | |
| - name: cargo test --test crystal_toolchain -- --ignored | |
| run: cargo test --test crystal_toolchain -- --ignored --nocapture | |
| framework-tests-swift: | |
| needs: generate-fixture | |
| # Per-target framework-test conformance gate for the Swift target: | |
| # transpiles runtime/ruby/test/**/*_test.rb to XCTest classes and runs | |
| # them under `swift test`. Complements smoke-swift (the archive's | |
| # real-blog suite): this one runs the framework runtime's own | |
| # assertions, catching adapter drift the app tests miss. Sibling of | |
| # framework-tests-kotlin. NOTE: needs a full toolchain with XCTest — | |
| # Linux toolchains bundle it; macOS would need Xcode (not just CLT). | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: swift-actions/setup-swift@v2 | |
| with: | |
| swift-version: '6.1' | |
| - name: Install libsqlite3-dev (CSQLite systemLibrary) | |
| run: sudo apt-get update -q && sudo apt-get install -y -q libsqlite3-dev | |
| # Scoped to the green subset (inflector + router + view_helpers — | |
| # the same three kotlin runs). The remaining two canonical gates | |
| # (errors / ac_base) are wired in the test file but fail on the | |
| # SAME emit-gap families kotlin defers (class-reflection | |
| # `< StandardError`; controller-body typing). Drop names as those | |
| # gaps close. Tracked in #34. | |
| - name: cargo test --test framework_tests_swift (green subset) | |
| run: cargo test --test framework_tests_swift -- --ignored --nocapture inflector router view_helpers | |
| # toolchain-typescript: tsc parity + node:test on real-blog, | |
| # under both the default (`node-sync`, better-sqlite3) profile | |
| # AND the `node-async` libsql profile that exercises async | |
| # coloring. The filter `real_blog` excludes the still- | |
| # aspirational tiny_blog gate (broken on main, pre-existing) and | |
| # the diagnostic `dump_transpiled_blog_tsc_errors` non-asserting | |
| # helper. Drop the filter once tiny_blog reaches parity. | |
| toolchain-typescript: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: cargo test --test typescript_toolchain real_blog -- --ignored | |
| run: cargo test --test typescript_toolchain real_blog -- --ignored --nocapture | |
| framework-tests-typescript: | |
| # Per-target framework-test conformance gate. Transpiles | |
| # runtime/ruby/test/**/*_test.rb to TS and runs under tsx. | |
| # Independent of the real-blog fixture (each test builds a | |
| # minimal in-process App), so this parallelizes freely | |
| # against generate-fixture / toolchain-* jobs. Catches | |
| # per-target adapter contract drift (e.g., is_exists vs | |
| # exists rename, regex anchor translation) before it | |
| # cascades into real-blog runtime failures. Sibling of the | |
| # disabled toolchain-typescript above; runs on the same | |
| # Node setup but a much smaller and faster scope (~12s vs | |
| # minutes). See project_framework_test_transpile.md. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: cargo test --test framework_tests_typescript -- --ignored | |
| run: cargo test --test framework_tests_typescript -- --ignored --nocapture | |
| # Browser smoke for the TypeScript *SharedWorker* deployment profile | |
| # (juntos) — distinct from the tsx-server smoke-typescript job below. | |
| # Harness + README live in tests/browser_smoke/. The pipeline (driven | |
| # by `npm test` → scripts/prebuild.mjs → playwright.config.ts): | |
| # 1. emit real-blog under `--profile worker` (cargo run emit_preview), | |
| # 2. npm install + vite-build the emitted SPA into .emitted/dist, | |
| # 3. `npm run preview` serves it; Playwright drives headless Chromium. | |
| # The spec opens its OWN SharedWorker port and dispatches synthetic | |
| # GET/POST/DELETE/422 requests + a multi-tab BroadcastChannel probe, | |
| # asserting no 5xx. This is the only job that exercises the emitted | |
| # framework runtime *inside a SharedWorker*, so it catches portability | |
| # gaps the server target can't see: Node-only globals (Buffer/process/ | |
| # require) in transpiled output, missing browser polyfills, broken | |
| # MessagePort / BroadcastChannel wiring, installDb ordering. Builds | |
| # from source (needs generate-fixture, like toolchain-typescript), | |
| # NOT from the published archive. Playwright config gates on $CI | |
| # (GitHub sets it) → fresh server + forbidOnly + github reporter. | |
| browser-smoke-typescript: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Install harness deps | |
| run: npm ci | |
| working-directory: tests/browser_smoke | |
| - name: Install Playwright Chromium (+ system deps) | |
| run: npx playwright install --with-deps chromium | |
| working-directory: tests/browser_smoke | |
| - name: Run SharedWorker browser smoke (emit → vite build → drive) | |
| run: npm test | |
| working-directory: tests/browser_smoke | |
| - name: Upload Playwright report on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: browser-smoke-playwright-report | |
| path: | | |
| tests/browser_smoke/playwright-report | |
| tests/browser_smoke/test-results | |
| retention-days: 7 | |
| if-no-files-found: ignore | |
| # The /ide/ demo, driven in a real chromium: analyze the pinned | |
| # Mastodon bundle in the wasm worker, then assert the demo beats — | |
| # typed hover (@account → Account, incl. inside a HAML template), | |
| # typed completion (@status. → account: Account?), related files via | |
| # the inferred render graph, and the coverage ledger being on. This | |
| # regression-guards the published demo (and the blog post's claims): | |
| # a change that mutes any beat fails CI before it reaches Pages. | |
| # Deterministic inputs (pinned SHA + fresh wasm), so blocking. | |
| browser-smoke-ide: | |
| needs: [build-wasm, generate-fixture] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: roundhouse-wasm | |
| path: wasm/lib | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Cache Mastodon source bundle | |
| id: mastodon-src | |
| uses: actions/cache@v4 | |
| with: | |
| path: wasm/ide/app-src.json | |
| key: mastodon-app-src-${{ env.MASTODON_SHA }}-v2 | |
| - name: Fetch pinned Mastodon + bundle sources | |
| if: steps.mastodon-src.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL "https://codeload.github.com/mastodon/mastodon/tar.gz/${MASTODON_SHA}" -o /tmp/mastodon.tar.gz | |
| mkdir -p /tmp/mastodon && tar -xzf /tmp/mastodon.tar.gz -C /tmp/mastodon --strip-components=1 | |
| node wasm/ide/bundle-src.mjs /tmp/mastodon wasm/ide/app-src.json \ | |
| --name mastodon --commit "${MASTODON_SHA}" \ | |
| --open app/controllers/statuses_controller.rb | |
| # The other two picker apps, so verify-ide.mjs can exercise the app | |
| # switch (blog + lobsters + mastodon). Mirrors the build-site assembly. | |
| - name: Cache lobsters source bundle | |
| id: lobsters-src | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/app-lobsters.json | |
| key: lobsters-app-src-${{ env.RUBY_BENCH_SHA }}-v1 | |
| - name: Fetch pinned ruby-bench + bundle lobsters (cache miss) | |
| if: steps.lobsters-src.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL "https://codeload.github.com/ruby/ruby-bench/tar.gz/${RUBY_BENCH_SHA}" -o /tmp/ruby-bench.tar.gz | |
| mkdir -p /tmp/lobsters | |
| tar -xzf /tmp/ruby-bench.tar.gz -C /tmp/lobsters --strip-components=3 "ruby-bench-${RUBY_BENCH_SHA}/benchmarks/lobsters" | |
| node wasm/ide/bundle-src.mjs /tmp/lobsters /tmp/app-lobsters.json \ | |
| --name lobsters --commit "${RUBY_BENCH_SHA}" \ | |
| --open app/controllers/stories_controller.rb | |
| - name: Cache campfire source bundle | |
| id: campfire-src | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/app-campfire.json | |
| key: campfire-app-src-${{ env.CAMPFIRE_SHA }}-v1 | |
| - name: Fetch pinned once-campfire + bundle sources (cache miss) | |
| if: steps.campfire-src.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL "https://codeload.github.com/basecamp/once-campfire/tar.gz/${CAMPFIRE_SHA}" -o /tmp/campfire.tar.gz | |
| mkdir -p /tmp/campfire && tar -xzf /tmp/campfire.tar.gz -C /tmp/campfire --strip-components=1 | |
| node wasm/ide/bundle-src.mjs /tmp/campfire /tmp/app-campfire.json \ | |
| --name campfire --commit "${CAMPFIRE_SHA}" \ | |
| --open app/models/message.rb | |
| - name: Assemble picker bundles + manifest | |
| run: | | |
| set -euo pipefail | |
| node wasm/ide/bundle-src.mjs fixtures/real-blog wasm/ide/app-blog.json \ | |
| --name blog --open app/controllers/articles_controller.rb | |
| node wasm/ide/bundle-src.mjs fixtures/roda-blog wasm/lib/app-roda.json \ | |
| --name roda-blog --open app.rb | |
| cp /tmp/app-lobsters.json wasm/ide/app-lobsters.json | |
| cp /tmp/app-campfire.json wasm/ide/app-campfire.json | |
| printf '%s\n' '{"default":"mastodon","apps":[{"name":"blog","label":"Rails blog","src":"app-blog.json"},{"name":"lobsters","label":"Lobsters","src":"app-lobsters.json"},{"name":"campfire","label":"Campfire","src":"app-campfire.json"},{"name":"mastodon","label":"Mastodon","src":"app-src.json"}]}' > wasm/ide/apps.json | |
| # Playground picker (lib/): lobsters + mastodon alongside the committed | |
| # blog fixture, so verify-playground exercises the app switch + the | |
| # off-thread Mastodon transpile. | |
| cp /tmp/app-lobsters.json wasm/lib/app-lobsters.json | |
| cp /tmp/app-campfire.json wasm/lib/app-campfire.json | |
| cp wasm/ide/app-src.json wasm/lib/app-mastodon.json | |
| printf '%s\n' '{"default":"blog","apps":[{"name":"blog","label":"Rails blog","src":"fixture.json","open":"app/models/article.rb"},{"name":"roda","label":"Roda + Sequel blog","src":"app-roda.json","open":"app.rb"},{"name":"lobsters","label":"Lobsters","src":"app-lobsters.json","open":"app/models/story.rb"},{"name":"campfire","label":"Campfire","src":"app-campfire.json","open":"app/models/message.rb"},{"name":"mastodon","label":"Mastodon","src":"app-mastodon.json","open":"app/models/status.rb"}]}' > wasm/lib/apps.json | |
| - name: Install harness deps (Playwright) | |
| run: npm ci | |
| working-directory: tests/browser_smoke | |
| - name: Install Playwright Chromium (+ system deps) | |
| run: npx playwright install --with-deps chromium | |
| working-directory: tests/browser_smoke | |
| - name: Serve wasm/ and verify the /ide/ + /playground/ pages | |
| run: | | |
| set -euo pipefail | |
| (cd wasm && python3 -m http.server 8099 >/tmp/ide-server.log 2>&1 &) | |
| sleep 1 | |
| (cd wasm/ide && node verify-ide.mjs) | |
| # Same shared worker/client, exercised through the emit surface incl. | |
| # the off-thread Mastodon transpile. | |
| (cd wasm/playground && node verify-playground.mjs) | |
| framework-tests-crystal: | |
| # Per-target framework-test conformance gate for the Crystal | |
| # target. Transpiles runtime/ruby/test/**/*_test.rb through | |
| # crystal::emit and runs the result under `crystal spec`. | |
| # Independent of the real-blog fixture (each test builds a | |
| # minimal in-process App with a single TestModule), so this | |
| # parallelizes freely against generate-fixture / toolchain-* | |
| # jobs. Catches per-target adapter contract drift and Crystal | |
| # strict-typing regressions (NamedTuple/Hash bridging, | |
| # nullable narrowing, etc.) before they cascade into real-blog | |
| # runtime failures. Sibling of framework-tests-typescript / | |
| # framework-tests-ruby. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: Install Crystal | |
| uses: crystal-lang/install-crystal@v1 | |
| with: | |
| crystal: latest | |
| - name: cargo test --test framework_tests_crystal -- --ignored | |
| run: cargo test --test framework_tests_crystal -- --ignored --nocapture | |
| framework-tests-kotlin: | |
| # Per-target framework-test conformance gate for the Kotlin target. | |
| # Transpiles runtime/ruby/test/**/*_test.rb through kotlin::emit and | |
| # runs the emitted JUnit-5 spec under `gradle test`. Independent of the | |
| # real-blog fixture (each test builds a minimal in-process App with a | |
| # single TestModule), so this parallelizes freely against | |
| # generate-fixture / toolchain-* jobs. Complements smoke-kotlin (the | |
| # archive's real-blog suite): this one runs the framework runtime's | |
| # own assertions, catching adapter drift the app tests miss. Sibling | |
| # of framework-tests-typescript / framework-tests-crystal. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - uses: gradle/actions/setup-gradle@v4 | |
| with: | |
| gradle-version: '9.5.1' | |
| # Scoped to the green subset (inflector + router + view_helpers). The | |
| # remaining two canonical gates (errors / ac_base) are wired in the | |
| # test file but still fail on deeper kotlin emit gaps (class-reflection | |
| # `< StandardError`; controller-body typing) — see the comment there. | |
| # Same filter convention as toolchain-typescript's tiny_blog exclusion; | |
| # drop names as those gaps close. Tracked in #34. | |
| - name: cargo test --test framework_tests_kotlin (green subset) | |
| run: cargo test --test framework_tests_kotlin -- --ignored --nocapture inflector router view_helpers | |
| framework-tests-ruby: | |
| # Per-target framework-test conformance gate for the Ruby target | |
| # (CRuby/MRI). Transpiles runtime/ruby/test/**/*_test.rb through | |
| # ruby::emit_spinel (named for historical reasons; emits the | |
| # Ruby-shape output that runs under stock CRuby). Catches | |
| # per-target lowering regressions (test-class-shape rewrites, | |
| # fixture refs, parent-class swap) that the source-side | |
| # `framework_ruby_tests_pass` gate can't see because it runs the | |
| # Ruby verbatim. Sibling of framework-tests-typescript above and | |
| # framework-tests-spinel below. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./runtime/spinel/scaffold | |
| - name: cargo test --test framework_tests_ruby -- --ignored | |
| run: cargo test --test framework_tests_ruby -- --ignored --nocapture | |
| # Per-target framework-test conformance gate for the spinel AOT | |
| # target. Same emit pipeline as framework-tests-ruby, but each | |
| # emitted test is compiled to a native binary via `spinel --rbs sig | |
| # <file>.rb -o <bin>` and executed; the autorun shim returns | |
| # non-zero on any assertion failure. | |
| # | |
| # Status (2026-06-02, spinel a782696): all 5 expected green locally. | |
| # matz/spinel #915 (the old `return NULL` blocker named in earlier | |
| # revisions of this comment) has landed, so errors/ac_base/router/ | |
| # inflector pass. The `view_helpers` stand-in previously hit two | |
| # spinel mis-compiles of the inline `Article < ActiveRecord::Base`; | |
| # both are now worked around roundhouse-side: | |
| # (a) the `Article` ctor's `id` param was widened to sp_RbVal because | |
| # the inherited `ActiveRecord::Base.create(attrs={})` factory | |
| # calls `new(attrs)` with a Hash — spinel unifies that poly call | |
| # site with `Article.new(7)` (Integer) into one C param type. The | |
| # body `self.id = id` then inlined the inherited attr_accessor | |
| # setter as a direct write to the narrow `mrb_int` id field, so | |
| # `mrb_int = sp_RbVal` failed to compile. Filed as matz/spinel#1275 | |
| # with a 19-line standalone repro (no RBS; the optional default | |
| # and the `?Integer` RBS are both red herrings — a required param | |
| # reproduces identically). WORKED AROUND: the stand-in now assigns | |
| # `@id = id` (direct ivar write) instead of `self.id = id`; the | |
| # direct-write path widens the field to match, so all four targets | |
| # (ruby/crystal/ts/spinel) agree. Real generated models are | |
| # unaffected — they use the Hash-`attrs` ctor shape, which doesn't | |
| # trigger the unification. The spinel-side fix still tracked in #1275. | |
| # (b) the `[]` field-lookup returns Integer|String across branches | |
| # and spinel commits to one C type instead of boxing — filed as | |
| # matz/spinel#1255 (clean minimal repro). ADDRESSED roundhouse- | |
| # side: inner test stand-ins now emit an `.rbs` sidecar | |
| # (`sig/test/<stem>_inner.rbs`) with `def []: ... -> (Integer | | |
| # String)`, the annotation the issue's own repro confirms makes | |
| # spinel box. So (b) no longer needs the spinel fix and no longer | |
| # appears in the log. | |
| # #1275 and #1255 have since closed upstream (2026-07 audit); (a)'s | |
| # direct ivar write is idiomatic-ctor Ruby and (b)'s sidecar is typed | |
| # surface we want anyway, so neither is reverted. continue-on-error | |
| # stays ON for the same structural reason as toolchain-spinel below: | |
| # every spinel job tracks matz/spinel master UNPINNED, so it surfaces | |
| # regressions without letting upstream churn gate unrelated work. | |
| # `ar_base` is disabled in-test (b453fcf) pending real-sqlite | |
| # rewiring of the test_helper. | |
| framework-tests-spinel: | |
| # Re-enabled 2026-06-17 (5/5 green on matz/spinel master). Advisory. | |
| needs: build-spinel | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: spinel-dist | |
| path: spinel-dist | |
| - name: Stage spinel binaries | |
| run: | | |
| chmod +x spinel-dist/spinel spinel-dist/spin spinel-dist/spinel_rbs_extract | |
| echo "$PWD/spinel-dist" >> $GITHUB_PATH | |
| - name: cargo test --test framework_tests_spinel -- --ignored | |
| run: cargo test --test framework_tests_spinel -- --ignored --nocapture | |
| toolchain-elixir: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Elixir | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| elixir-version: '1.18' | |
| otp-version: '27' | |
| - name: cargo test --test elixir_toolchain -- --ignored | |
| run: cargo test --test elixir_toolchain -- --ignored --nocapture | |
| # ------------------------------------------------------------------- | |
| # (toolchain-ruby retired by the smoke consolidation: smoke-ruby's | |
| # README § Test runs the same five driver suites from the published | |
| # archive. The harness remains a dev-loop tool: | |
| # cargo test --test ruby_toolchain -- --ignored.) | |
| # ------------------------------------------------------------------- | |
| # Spinel-AOT toolchain. Same emit pipeline as toolchain-ruby (lowered | |
| # real-blog + target-specific tests), but the runner is the spinel | |
| # AOT compiler from build-spinel. Each emitted test file compiles to | |
| # a native binary via `$SPINEL <file>.rb -o <bin>`; each binary | |
| # runs and asserts exit 0 (autorun shim emits explicit per-test | |
| # dispatch — no minitest at_exit). | |
| # | |
| # Requires libsqlite3-dev at link time (the binaries `-lsqlite3`). | |
| # ubuntu-latest ships libsqlite3-0 (runtime) but not the dev linker | |
| # symlink, so we install it explicitly. | |
| # Briefly went green after matz/spinel#631 closed (bffabac); then | |
| # matz/spinel c66d00a landed the shape-A fix for #634 and surfaced | |
| # a follow-on over-widening: @location ivar (nil-default + typed | |
| # writers) now reads as sp_RbVal, breaking sp_StrStrHash_set's | |
| # const char* slot in the Location-header path. Comment on #634 | |
| # (issuecomment-4503927146) tracks the consumer-side narrow. | |
| # Drop continue-on-error when matz lands the body-driven post-pass. | |
| # Re-enabled 2026-06-02 (was `if: false`). The real-blog app now | |
| # compiles under spinel and passes 9/9 model tests after the from_stmt | |
| # void*/mrb_int fix (5aae416). One spinel-specific runtime assertion | |
| # remains in articles_controller_test (the same emitted source passes | |
| # under CRuby in toolchain-ruby — an AOT runtime divergence, not an | |
| # emit bug), so the job stays advisory via continue-on-error until that | |
| # closes. Running it (vs `if: false`) keeps the model-suite progress | |
| # visible and catches regressions in the compile path. | |
| toolchain-spinel: | |
| # Un-gated 2026-06-18: matz/spinel#1447 (the `||=`-into-poly-setter → | |
| # null-Flash segfault in the controller integration test) closed | |
| # upstream, clearing the last blocker on the real-blog AOT suite (the | |
| # void-tail compile error was already fixed in #1444). Stays advisory | |
| # via continue-on-error — like every spinel job it tracks matz/spinel | |
| # master UNPINNED, so it surfaces regressions without gating unrelated | |
| # work; if master churns red, the signal is visible but non-blocking. | |
| needs: [generate-fixture, build-spinel] | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: spinel-dist | |
| path: spinel-dist | |
| - name: Stage spinel binaries | |
| run: | | |
| chmod +x spinel-dist/spinel spinel-dist/spin spinel-dist/spinel_rbs_extract | |
| echo "$PWD/spinel-dist" >> $GITHUB_PATH | |
| - name: Install libsqlite3-dev | |
| run: sudo apt-get update -qq && sudo apt-get install -y libsqlite3-dev | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./runtime/spinel/scaffold | |
| - name: cargo test --test spinel_toolchain -- --ignored | |
| run: cargo test --test spinel_toolchain -- --ignored --nocapture | |
| # On failure, capture spinel's actual generated C for the failing | |
| # controller test. The failure is at RUNTIME (the C compiles under | |
| # spinel's `-Wno-all`), so spinel reaps its transient out.c — we | |
| # regenerate it emit-only (`spinel -c`) from the leftover emit | |
| # scratch, then surface the type mismatch with a plain gcc compile | |
| # (no `-Wno-all`). This is the environment-independent repro for the | |
| # spinel issue: the input `.rb`/`.rbs` plus the wrong C spinel emitted | |
| # here (e.g. `dispatch_request(sp_SymPolyHash*)` fed an `sp_StrIntHash*`), | |
| # which other environments don't reproduce. | |
| - name: Capture spinel codegen on failure | |
| if: failure() | |
| run: | | |
| set +e | |
| SC=$(find "${TMPDIR:-/tmp}" /tmp "$RUNNER_TEMP" -maxdepth 3 -type d \ | |
| -name 'roundhouse-spinel-real-blog' 2>/dev/null | head -1) | |
| echo "emit scratch: $SC" | |
| mkdir -p ci-spinel-repro | |
| cc --version > ci-spinel-repro/cc-version.txt 2>&1 | |
| uname -a >> ci-spinel-repro/cc-version.txt | |
| spinel-dist/spinel --version >> ci-spinel-repro/cc-version.txt 2>&1 || true | |
| if [ -z "$SC" ]; then echo "scratch not found"; exit 0; fi | |
| T=test/controllers/articles_controller_test.rb | |
| ( cd "$SC" && spinel -c --rbs sig "$T" -o /tmp/out.c ) \ | |
| > ci-spinel-repro/spinel-emit.log 2>&1 | |
| cp /tmp/out.c ci-spinel-repro/articles_controller_test.c 2>/dev/null | |
| # Compiler-agnostic evidence of the mismatch (plain gcc, no -Wno-all): | |
| gcc -fsyntax-only -I"$PWD/spinel-dist/lib" /tmp/out.c \ | |
| > ci-spinel-repro/gcc-warnings.txt 2>&1 | |
| # The emitted sources that reproduce it (small, self-contained). | |
| for d in test sig app config runtime; do | |
| [ -d "$SC/$d" ] && cp -r "$SC/$d" "ci-spinel-repro/$d" | |
| done | |
| echo "captured:"; ls -R ci-spinel-repro | head -40 | |
| - name: Upload spinel codegen repro | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: spinel-toolchain-repro | |
| path: ci-spinel-repro | |
| retention-days: 7 | |
| toolchain-python: | |
| # Re-enabled 2026-06-06: the transpiled framework set is degrade-free, | |
| # the emitted real-blog unittest is green (21/21), and `compare python` | |
| # matches Rails 5/5. Python version tracks the local dev environment | |
| # (system `python3`, which the `python_toolchain` test shells out to); | |
| # `from __future__ import annotations` keeps the emit version-agnostic | |
| # back to 3.11 (the emitted `requires-python`). | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.14' | |
| - name: cargo test --test python_toolchain -- --ignored | |
| run: cargo test --test python_toolchain -- --ignored --nocapture | |
| toolchain-csharp: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| # `dotnet build` of the emitted ASP.NET Core project — the C# analog of | |
| # crystal's `--no-codegen` / go's `vet`: compiles the model layer, the | |
| # transpiled framework runtime, and the primitives together. | |
| - name: cargo test --test csharp_toolchain -- --ignored | |
| run: cargo test --test csharp_toolchain -- --ignored --nocapture | |
| # ------------------------------------------------------------------- | |
| # DOM equivalence: boot Rails + the target server side-by-side and | |
| # diff response bodies via tools/compare. The toolchain jobs above | |
| # only verify that the emitted code compiles and passes its native | |
| # tests; these jobs verify it actually behaves the same as Rails on | |
| # live HTTP. One job per target — same matrix as toolchain. | |
| # ------------------------------------------------------------------- | |
| # compare: one job per compile-and-boot target, DOM-diffing the live | |
| # transpiled server against Rails over HTTP. Matrixed from nine | |
| # near-identical jobs; every per-target difference (toolchain install + | |
| # versions) is a guarded step below. ruby/setup-ruby runs LAST on every | |
| # target so its MRI 3.4 wins PATH when Rails boots — required by the JVM | |
| # targets (setup-java/gradle otherwise leave system Ruby 3.2 resolving | |
| # bin/rails; see the old compare-kotlin note) and harmless everywhere | |
| # else. fail-fast off so one target's failure doesn't cancel the rest | |
| # (matches the old independent jobs). ruby/jruby/spinel stay separate | |
| # below: two setup-ruby steps / an extra build-spinel dependency don't | |
| # fit this shape. | |
| compare: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [rust, crystal, kotlin, swift, csharp, typescript, go, elixir, python] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - name: Install Crystal | |
| if: matrix.target == 'crystal' | |
| uses: crystal-lang/install-crystal@v1 | |
| with: | |
| crystal: latest | |
| - name: Install JDK (Kotlin) | |
| if: matrix.target == 'kotlin' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - name: Install Gradle (Kotlin) | |
| if: matrix.target == 'kotlin' | |
| uses: gradle/actions/setup-gradle@v4 | |
| with: | |
| gradle-version: '9.5.1' | |
| - name: Install Swift | |
| if: matrix.target == 'swift' | |
| uses: swift-actions/setup-swift@v2 | |
| with: | |
| swift-version: '6.1' | |
| - name: Install libsqlite3-dev (Swift CSQLite systemLibrary) | |
| if: matrix.target == 'swift' | |
| run: sudo apt-get update -q && sudo apt-get install -y -q libsqlite3-dev | |
| - name: Install .NET (C#) | |
| if: matrix.target == 'csharp' | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Install Node (TypeScript) | |
| if: matrix.target == 'typescript' | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Install Go | |
| if: matrix.target == 'go' | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.24' | |
| cache: false | |
| - name: Install Elixir | |
| if: matrix.target == 'elixir' | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| elixir-version: '1.18' | |
| otp-version: '27' | |
| - name: Install Python | |
| if: matrix.target == 'python' | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.14' | |
| - name: Install uv (Python) | |
| if: matrix.target == 'python' | |
| uses: astral-sh/setup-uv@v7 | |
| # setup-ruby LAST — see the job comment (MRI 3.4 must win PATH). | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./fixtures/real-blog | |
| - name: scripts/compare ${{ matrix.target }} | |
| run: scripts/compare ${{ matrix.target }} | |
| # compare-ruby: same shape as every other compare-* — Puma+Rack | |
| # server bound to :3000 via ruby_overlay's config.ru, hit over | |
| # HTTP. Two ruby setups: the fixture's Gemfile (Rails reference) | |
| # and the scaffold's Gemfile (Puma + Rack target). Node is needed | |
| # for tailwindcss asset compilation; no target compile step | |
| # (Ruby is interpreted). | |
| compare-ruby: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./fixtures/real-blog | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./runtime/spinel/scaffold | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: scripts/compare ruby | |
| run: scripts/compare ruby | |
| # compare-jruby: same emitted tree as compare-ruby but served by Puma | |
| # on the JVM via JRuby with the JDBC db backend (runtime/db.rb = | |
| # db_jruby.rb). The Rails reference runs under CRuby 3.4 (the default | |
| # `ruby`); the target is launched with an explicit `jruby`, so the two | |
| # setup-ruby steps are ordered jruby-first / CRuby-last to leave 3.4 as | |
| # the default. JRuby must be >= 10 — the runtime uses Fiber[:k] storage | |
| # (Ruby 3.2+), which JRuby 9.4 (Ruby 3.1) lacks. No node/tailwind: | |
| # assets aren't built (the DOM diff compares asset tags, not files — | |
| # see scripts/compare's jruby branch). | |
| compare-jruby: | |
| needs: generate-fixture | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| # JRuby 10 requires Java 21+ (it refuses to boot on older JDKs). | |
| # setup-ruby provisions JRuby but not the JDK, so pin Java first. | |
| - uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| # JRuby provisioning only — no bundler-cache. The committed scaffold | |
| # Gemfile is MRI-only (`gem "sqlite3"`), so it can't be bundled under | |
| # JRuby; `scripts/compare jruby` resolves the JRuby bundle | |
| # (jdbc-sqlite3) against the emitted tree's own Gemfile instead. | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: 'jruby-10.0' | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./fixtures/real-blog | |
| - name: scripts/compare jruby | |
| run: scripts/compare jruby | |
| # compare-spinel: DOM-diff against a live AOT-compiled binary served | |
| # over Tep::Server (FFI HTTP/1.1). The spinel-transpile Makefile | |
| # target vendors the Tep transport (precompiles sphttp.o + sed- | |
| # substitutes @TEP_SPHTTP_O@ in net.rb) before AOT-compile; the | |
| # scaffold's `make build` then drives `spinel main.rb -o build/blog`. | |
| # 7/7 paths match against Rails as of matz/spinel#1017 (PR #1018) | |
| # + #1020 (commit 23d9a18). `continue-on-error` is still on because | |
| # spinel master can churn faster than this job rebuilds against it; | |
| # drop it once the upstream cadence settles. | |
| compare-spinel: | |
| # Re-enabled 2026-06-17 (7/7 paths match Rails on matz/spinel master). Advisory. | |
| needs: [generate-fixture, build-spinel] | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: spinel-dist | |
| path: spinel-dist | |
| - name: Stage spinel binaries | |
| run: | | |
| chmod +x spinel-dist/spinel spinel-dist/spin spinel-dist/spinel_rbs_extract | |
| echo "$PWD/spinel-dist" >> $GITHUB_PATH | |
| - name: Install libsqlite3-dev | |
| run: sudo apt-get update -qq && sudo apt-get install -y libsqlite3-dev | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./fixtures/real-blog | |
| - uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./runtime/spinel/scaffold | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: scripts/compare spinel | |
| run: scripts/compare spinel | |
| # ------------------------------------------------------------------- | |
| # smoke-<target>: the archive runs its own README. Each published | |
| # <target>.tgz is the complete test artifact — the transpiled app, | |
| # its model/controller test suite, and the Playwright e2e suite | |
| # (e2e/, injected by project::ensure_e2e with a webServer config | |
| # that boots the archive's own binary and seeds its own db/seed.sql). | |
| # scripts/smoke extracts the tgz and executes the README's ```sh | |
| # blocks verbatim — docs-as-contract: if the README is wrong, the | |
| # job is red. (The go pilot caught `go build .` colliding with the | |
| # app/ source dir, a missing-storage/ boot panic, and four more | |
| # never-executed README instructions across targets.) | |
| # | |
| # Each smoke job subsumes the target's old toolchain-<t> build+test | |
| # legs (README § Build/Test) and e2e-<t> (README § End-to-end). The | |
| # toolchain harnesses remain as dev-loop tools: | |
| # cargo test --test <t>_toolchain -- --ignored. | |
| # | |
| # Per-job content = toolchain setup only; no per-target build/seed/ | |
| # boot knowledge lives here (that's the point — it's all in the | |
| # archive). The repo-side playwright install pre-warms the browser | |
| # cache + system deps so the README's own `npx playwright install | |
| # chromium` (same ^1.49 range) is a cache hit. | |
| # | |
| # ruby/jruby ride smoke too: their scaffold document ships as | |
| # SPECIMEN.md and the generated quick-start takes README.md. Only | |
| # spinel keeps the scaffold README top-level (specimen doc + matz's | |
| # extraction surface; not in the smoke matrix). | |
| # ------------------------------------------------------------------- | |
| smoke: | |
| needs: build-site | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| target: [rust, crystal, kotlin, swift, csharp, typescript, go, elixir, python, ruby, jruby] | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: browse-archives | |
| path: browse | |
| - name: Install Rust | |
| if: matrix.target == 'rust' | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Install Crystal | |
| if: matrix.target == 'crystal' | |
| uses: crystal-lang/install-crystal@v1 | |
| with: | |
| crystal: latest | |
| - name: Install JDK (Kotlin, JRuby) | |
| if: matrix.target == 'kotlin' || matrix.target == 'jruby' | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| - name: Install Gradle (Kotlin) | |
| if: matrix.target == 'kotlin' | |
| uses: gradle/actions/setup-gradle@v4 | |
| with: | |
| gradle-version: '9.5.1' | |
| - name: Install Swift | |
| if: matrix.target == 'swift' | |
| uses: swift-actions/setup-swift@v2 | |
| with: | |
| swift-version: '6.1' | |
| - name: Install libsqlite3-dev (Swift CSQLite systemLibrary) | |
| if: matrix.target == 'swift' | |
| run: sudo apt-get update -q && sudo apt-get install -y -q libsqlite3-dev | |
| - name: Install .NET (C#) | |
| if: matrix.target == 'csharp' | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Install Go | |
| if: matrix.target == 'go' | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.24' | |
| cache: false | |
| - name: Install Elixir | |
| if: matrix.target == 'elixir' | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| elixir-version: '1.18' | |
| otp-version: '27' | |
| - name: Install Python | |
| if: matrix.target == 'python' | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.14' | |
| - name: Install uv (Python) | |
| if: matrix.target == 'python' | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Install Ruby (MRI 3.4) | |
| if: matrix.target == 'ruby' | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| - name: Install JRuby 10 | |
| if: matrix.target == 'jruby' | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: 'jruby-10.0' | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Pre-warm Playwright (chromium + system deps) | |
| run: | | |
| cd e2e | |
| npm install --no-audit --no-fund | |
| npx playwright install --with-deps chromium | |
| - name: scripts/smoke ${{ matrix.target }} | |
| run: scripts/smoke --tgz browse/${{ matrix.target }}.tgz ${{ matrix.target }} | |
| # spinel: the archive's README § Build AOT-compiles main.rb → build/blog | |
| # with the spinel toolchain (build-spinel artifact); § Test runs the | |
| # scaffold Makefile's `spinel-test` (each emitted test → a native binary); | |
| # § End-to-end boots build/blog under Playwright. Unlike ruby it needs no | |
| # MRI/bundler — static assets ship prebuilt in the archive (ensure_static_ | |
| # assets, like jruby) so Build is the bare native compile. Toolchain = | |
| # spinel on PATH + a C compiler + libsqlite3-dev (the binary links | |
| # -lsqlite3) + Node for Playwright. Advisory (continue-on-error) like every | |
| # spinel job — it tracks matz/spinel master UNPINNED. needs build-spinel | |
| # for the compiler and build-site for the published spinel.tgz. | |
| smoke-spinel: | |
| needs: [build-site, build-spinel] | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: browse-archives | |
| path: browse | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: spinel-dist | |
| path: spinel-dist | |
| - name: Stage spinel binaries | |
| run: | | |
| chmod +x spinel-dist/spinel spinel-dist/spin spinel-dist/spinel_rbs_extract | |
| echo "$PWD/spinel-dist" >> $GITHUB_PATH | |
| - name: Install libsqlite3-dev | |
| run: sudo apt-get update -qq && sudo apt-get install -y libsqlite3-dev | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Pre-warm Playwright (chromium + system deps) | |
| run: | | |
| cd e2e | |
| npm install --no-audit --no-fund | |
| npx playwright install --with-deps chromium | |
| - name: scripts/smoke spinel | |
| run: scripts/smoke --tgz browse/spinel.tgz spinel | |
| # ------------------------------------------------------------------- | |
| # Build the Pages site: per-target manifests (real-blog fixture) + | |
| # static HTML/CSS/JS from site/. Per matz/spinel#314, the archives at | |
| # rubys.github.io/roundhouse/browse/<target>.tgz are matz's primary | |
| # extraction surface for spinel-side bug repros — they need to | |
| # publish *current emit* even when per-target toolchain/compare jobs | |
| # fail (broken emit is exactly what's interesting to investigate). | |
| # | |
| # So build-site only gates on the fixture being available. It does | |
| # not gate on unit / toolchain-* / compare-* — a flaky elixir CI run | |
| # or a known-broken target shouldn't stall the archive workflow, and | |
| # the archive itself is available via the workflow-run artifact even | |
| # when unit fails. Pages publication (the `deploy` job below) does | |
| # gate on unit, so a broken library can't push to the live site. | |
| # | |
| # build-site runs on PRs too (no `if: main`). Building the archives is | |
| # harmless off-main and catches archive-generation breakage on the PR; | |
| # PUBLISHING is the boundary, and that lives entirely in `deploy` | |
| # (gated `if: main`). It also uploads the `browse-archives` artifact so | |
| # the e2e-<target> jobs can boot the exact built bytes without | |
| # rebuilding the site. | |
| # ------------------------------------------------------------------- | |
| build-site: | |
| needs: [generate-fixture, build-wasm] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: real-blog-fixture | |
| - name: Extract fixture | |
| run: tar -xzf real-blog.tar.gz | |
| # The compiler wasm, built fresh from source by the parallel build-wasm | |
| # job (no committed binary). Lands in _site/lib/ for /playground/+/studio/. | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: roundhouse-wasm | |
| path: wasm-artifact | |
| # Compile the static asset graph once and inject it into every emit | |
| # archive's static/assets/ (via ROUNDHOUSE_ASSETS_DIR) so the published | |
| # archives are self-contained — no build step required by a downloader. | |
| # The asset set is identical across targets (same Rails-derived HTML + | |
| # same importmap pins), so one build from the fixture serves all. | |
| # | |
| # Two halves, both landing in build/assets/: | |
| # - tailwind.css — compiled by the Tailwind v4 CLI (Node). | |
| # - the JS importmap graph — turbo.min.js / stimulus.min.js / | |
| # stimulus-loading.js (copied out of the turbo-rails / stimulus-rails | |
| # gem dirs) plus the fixture's own app/javascript (application.js + | |
| # controllers/*). These are the modules the emitted layout's | |
| # `javascript_importmap_tags` references at /assets/*; without them | |
| # the ES-module graph 404s and Turbo never boots (so turbo_comment + | |
| # action_cable can't pass). Mirrors the scaffold Makefile's `assets` | |
| # rule, which ruby/spinel/jruby run in-archive. | |
| # collect_asset_files (src/project.rs) walks build/assets/ recursively | |
| # and preserves subdirs, so controllers/*.js land at | |
| # static/assets/controllers/*.js — matching the `controllers/*` pins. | |
| - name: Install Node | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Install Ruby (for the turbo-rails / stimulus-rails JS bundles) | |
| uses: ruby/setup-ruby@v1 | |
| with: | |
| ruby-version: '3.4' | |
| bundler-cache: true | |
| working-directory: ./runtime/spinel/scaffold | |
| - name: Build static asset graph for archives | |
| run: | | |
| mkdir -p build/assets/controllers | |
| # Tailwind CSS. | |
| (cd e2e && npm install --no-audit --no-fund) | |
| printf '@import "tailwindcss";\n@source "%s/fixtures/real-blog/app";\n' "$PWD" \ | |
| > e2e/.tailwind-input.css | |
| (cd e2e && npx @tailwindcss/cli --input .tailwind-input.css \ | |
| --output "$GITHUB_WORKSPACE/build/assets/tailwind.css" --minify) | |
| rm -f e2e/.tailwind-input.css | |
| # Gem-bundled JS (turbo-rails + stimulus-rails). Gem dirs are | |
| # queried at build time so a version bump needs no edit here. | |
| turbo_dir=$(cd runtime/spinel/scaffold && bundle exec ruby -e \ | |
| 'puts Gem::Specification.find_by_name("turbo-rails").gem_dir') | |
| stimulus_dir=$(cd runtime/spinel/scaffold && bundle exec ruby -e \ | |
| 'puts Gem::Specification.find_by_name("stimulus-rails").gem_dir') | |
| cp "$turbo_dir/app/assets/javascripts/turbo.min.js" build/assets/ | |
| cp "$stimulus_dir/app/assets/javascripts/stimulus.min.js" build/assets/ | |
| cp "$stimulus_dir/app/assets/javascripts/stimulus-loading.js" build/assets/ | |
| # App JS from the fixture (the importmap entry + Stimulus controllers). | |
| cp fixtures/real-blog/app/javascript/application.js build/assets/ | |
| cp fixtures/real-blog/app/javascript/controllers/*.js build/assets/controllers/ | |
| echo "baked asset graph:"; (cd build/assets && find . -type f | sort) | |
| - name: cargo run --bin build-site | |
| env: | |
| ROUNDHOUSE_ASSETS_DIR: build/assets | |
| run: cargo run --release --bin build-site | |
| # Live SharedWorker blog demo → _site/blog/. Emits real-blog under | |
| # the worker profile, vite-builds it with ROUNDHOUSE_BASE set to the | |
| # Pages mount path, and drops dist/ alongside the static site. The | |
| # app runs entirely in the browser (SharedWorker + sqlite-wasm via | |
| # the opfs-sahpool VFS) — no server, and crucially no COOP/COEP | |
| # cross-origin-isolation headers, so it serves straight off Pages. | |
| # Deep-link reloads are handled by site/404.html (already copied | |
| # into _site by build-site) bouncing back to the shell's decoder. | |
| # ROUNDHOUSE_BASE here must match the base hard-coded in | |
| # site/404.html. | |
| - name: Build live SharedWorker blog demo (→ _site/blog) | |
| env: | |
| ROUNDHOUSE_BASE: /roundhouse/blog/ | |
| run: | | |
| set -euo pipefail | |
| DEMO="$(mktemp -d)" | |
| cargo run --release --quiet --bin emit_preview -- \ | |
| --target typescript --profile worker --out "$DEMO" fixtures/real-blog | |
| (cd "$DEMO" && npm install --no-audit --no-fund --silent && npm run build) | |
| mkdir -p _site/blog | |
| cp -R "$DEMO"/dist/. _site/blog/ | |
| echo "blog demo files:"; find _site/blog -maxdepth 2 -type f | sort | |
| # In-browser playground + studio demos → _site/{lib,playground,studio}/. | |
| # Where the blog ships the *emitted app*, these ship the *compiler itself* | |
| # as wasm. The two surfaces share ../lib/ (driver + editor + tree widget + | |
| # bundler + the compiler wasm + the seed fixture), so the served tree must | |
| # keep lib/ a sibling of the surface dirs (the pages import ../lib/). | |
| # /playground/ = edit Ruby → emitted code, any target; /studio/ = edit | |
| # Ruby → run the emitted TS blog live (rung D). fixture.json is regenerated | |
| # from the just-built real-blog so it tracks create-blog. The compiler wasm | |
| # is built fresh from source by the parallel build-wasm job and pulled in | |
| # as an artifact (NOT committed) — see wasm/vendor/ruby-rbs-sys/README.md | |
| # and the "Publishing to Pages" section of docs/browser-demo-plan.md. Pure | |
| # static files (no npm/vite, no COOP/COEP), so they serve straight off Pages. | |
| - name: Bundle the in-browser playground + studio demos (→ _site/lib, _site/playground, _site/studio) | |
| run: | | |
| set -euo pipefail | |
| mkdir -p _site/lib _site/playground _site/studio | |
| # Shared lib: code modules + the freshly-built compiler wasm (from the | |
| # build-wasm artifact — not committed; see that job + wasm/vendor/). | |
| cp wasm/lib/transpile.mjs \ | |
| wasm/lib/analyzer.mjs \ | |
| wasm/lib/engine.mjs \ | |
| wasm/lib/worker.mjs \ | |
| wasm/lib/wasm-client.mjs \ | |
| wasm/lib/wasi-shim.mjs \ | |
| wasm/lib/editor.js \ | |
| wasm/lib/tree.js \ | |
| wasm/lib/bundle.mjs \ | |
| wasm/lib/test-runtime.mjs \ | |
| wasm/lib/sourcemap.mjs \ | |
| wasm/lib/app-host.mjs \ | |
| _site/lib/ | |
| cp wasm-artifact/roundhouse_wasm.wasm _site/lib/ | |
| # Fresh seed app from the just-generated real-blog fixture (→ lib). | |
| (cd wasm/browser-spike && node gen-fixture.mjs) | |
| cp wasm/browser-spike/fixture.json _site/lib/fixture.json | |
| # Each surface: its own HTML + entry module(s). Studio also ships its | |
| # app-host service worker (sw.js), which serves the running app. | |
| cp wasm/playground/index.html wasm/playground/playground.js _site/playground/ | |
| cp wasm/studio/index.html wasm/studio/studio.js wasm/studio/sw.js _site/studio/ | |
| echo "lib files:"; find _site/lib -type f | sort | |
| echo "playground files:"; find _site/playground -type f | sort | |
| echo "studio files:"; find _site/studio -type f | sort | |
| # The /ide/ demo: Monaco over the analyzer wasm in a Web Worker, | |
| # preloaded with Mastodon at $MASTODON_SHA (source-only bundle, | |
| # LICENSE + commit embedded — AGPL source redistribution). Same | |
| # bundle + cache key the browser-smoke-ide job verified. | |
| - name: Cache Mastodon source bundle | |
| id: mastodon-src-site | |
| uses: actions/cache@v4 | |
| with: | |
| path: wasm/ide/app-src.json | |
| key: mastodon-app-src-${{ env.MASTODON_SHA }}-v2 | |
| - name: Fetch pinned Mastodon + bundle sources (cache miss) | |
| if: steps.mastodon-src-site.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL "https://codeload.github.com/mastodon/mastodon/tar.gz/${MASTODON_SHA}" -o /tmp/mastodon.tar.gz | |
| mkdir -p /tmp/mastodon && tar -xzf /tmp/mastodon.tar.gz -C /tmp/mastodon --strip-components=1 | |
| node wasm/ide/bundle-src.mjs /tmp/mastodon wasm/ide/app-src.json \ | |
| --name mastodon --commit "${MASTODON_SHA}" \ | |
| --open app/controllers/statuses_controller.rb | |
| - name: Bundle the in-browser IDE demo (→ _site/ide) | |
| run: | | |
| set -euo pipefail | |
| mkdir -p _site/ide | |
| cp wasm/ide/index.html wasm/ide/ide.js _site/ide/ | |
| cp wasm/ide/app-src.json _site/ide/ | |
| echo "ide files:"; find _site/ide -type f | sort | |
| # App-picker bundles + manifests. Both surfaces gained an app selector; | |
| # each fetches an apps.json listing the shipped source bundles and swaps | |
| # them in-place (analyze/transpile are stateless per call). Mastodon is | |
| # /ide/-only — its strict-mode ingest gaps (alias, class<<self) hard-fail | |
| # the /playground/'s full-transpile path, whereas /ide/'s analysis is | |
| # gap-tolerant. Lobsters (ruby-bench, MIT) ships to both; the blog ships | |
| # to /ide/ as a source bundle (the playground reuses lib/fixture.json). | |
| - name: Cache lobsters source bundle | |
| id: lobsters-src-site | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/app-lobsters.json | |
| key: lobsters-app-src-${{ env.RUBY_BENCH_SHA }}-v1 | |
| - name: Fetch pinned ruby-bench + bundle lobsters (cache miss) | |
| if: steps.lobsters-src-site.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL "https://codeload.github.com/ruby/ruby-bench/tar.gz/${RUBY_BENCH_SHA}" -o /tmp/ruby-bench.tar.gz | |
| mkdir -p /tmp/lobsters | |
| tar -xzf /tmp/ruby-bench.tar.gz -C /tmp/lobsters --strip-components=3 "ruby-bench-${RUBY_BENCH_SHA}/benchmarks/lobsters" | |
| node wasm/ide/bundle-src.mjs /tmp/lobsters /tmp/app-lobsters.json \ | |
| --name lobsters --commit "${RUBY_BENCH_SHA}" \ | |
| --open app/controllers/stories_controller.rb | |
| - name: Cache campfire source bundle | |
| id: campfire-src-site | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/app-campfire.json | |
| key: campfire-app-src-${{ env.CAMPFIRE_SHA }}-v1 | |
| - name: Fetch pinned once-campfire + bundle sources (cache miss) | |
| if: steps.campfire-src-site.outputs.cache-hit != 'true' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL "https://codeload.github.com/basecamp/once-campfire/tar.gz/${CAMPFIRE_SHA}" -o /tmp/campfire.tar.gz | |
| mkdir -p /tmp/campfire && tar -xzf /tmp/campfire.tar.gz -C /tmp/campfire --strip-components=1 | |
| node wasm/ide/bundle-src.mjs /tmp/campfire /tmp/app-campfire.json \ | |
| --name campfire --commit "${CAMPFIRE_SHA}" \ | |
| --open app/models/message.rb | |
| - name: Assemble app-picker bundles + manifests (→ _site/lib, _site/ide) | |
| run: | | |
| set -euo pipefail | |
| # Blog source bundle for /ide/ (the playground reuses lib/fixture.json). | |
| node wasm/ide/bundle-src.mjs fixtures/real-blog /tmp/app-blog.json \ | |
| --name blog --open app/controllers/articles_controller.rb | |
| cp /tmp/app-lobsters.json _site/lib/app-lobsters.json | |
| cp /tmp/app-lobsters.json _site/ide/app-lobsters.json | |
| cp /tmp/app-blog.json _site/ide/app-blog.json | |
| # campfire: the write+push app (Action Cable channels, model-side | |
| # broadcast_*_to, turbo_stream views) and the current climb — the | |
| # picker's fourth app, transpiling partially with the gap ledger. | |
| cp /tmp/app-campfire.json _site/lib/app-campfire.json | |
| cp /tmp/app-campfire.json _site/ide/app-campfire.json | |
| # Mastodon ships to BOTH now: /ide/ analyzes it, and /playground/ | |
| # transpiles it off the main thread (partial output + gap diagnostics — | |
| # the emit runs in the shared worker, so the multi-second pass never | |
| # freezes the tab; see wasm/lib/wasm-client.mjs). | |
| cp wasm/ide/app-src.json _site/lib/app-mastodon.json | |
| # roda-blog: the Roda + Sequel exemplar (issue #67) — the picker's | |
| # second CLEAN app, transpiled through the roda front-end. | |
| node wasm/ide/bundle-src.mjs fixtures/roda-blog _site/lib/app-roda.json \ | |
| --name roda-blog --open app.rb | |
| # Playground manifest: blog + roda (clean) + lobsters + campfire + mastodon (partial → honest ledger). | |
| printf '%s\n' '{"default":"blog","apps":[{"name":"blog","label":"Rails blog","src":"fixture.json","open":"app/models/article.rb"},{"name":"roda","label":"Roda + Sequel blog","src":"app-roda.json","open":"app.rb"},{"name":"lobsters","label":"Lobsters","src":"app-lobsters.json","open":"app/models/story.rb"},{"name":"campfire","label":"Campfire","src":"app-campfire.json","open":"app/models/message.rb"},{"name":"mastodon","label":"Mastodon","src":"app-mastodon.json","open":"app/models/status.rb"}]}' > _site/lib/apps.json | |
| # IDE manifest: all four (analysis is gap-tolerant; Mastodon default). | |
| printf '%s\n' '{"default":"mastodon","apps":[{"name":"blog","label":"Rails blog","src":"app-blog.json"},{"name":"lobsters","label":"Lobsters","src":"app-lobsters.json"},{"name":"campfire","label":"Campfire","src":"app-campfire.json"},{"name":"mastodon","label":"Mastodon","src":"app-src.json"}]}' > _site/ide/apps.json | |
| echo "lib app bundles:"; ls -1 _site/lib/*.json | sort | |
| echo "ide app bundles:"; ls -1 _site/ide/*.json | sort | |
| # Bench results page — fetched from the Hetzner publishing endpoint | |
| # (cron-driven, populates bench/results/latest on each run). The | |
| # render step degrades gracefully: if Hetzner is unreachable, the | |
| # `_site/bench/` directory simply isn't created and the landing | |
| # page's link 404s until the next CI cycle picks up fresh data. | |
| - name: Fetch latest bench data | |
| id: fetch-bench | |
| env: | |
| BENCH_URL: https://roundbench.showcase.party | |
| run: | | |
| mkdir -p _site/bench | |
| if curl -fsSL --max-time 30 "$BENCH_URL/summary.json" -o _site/bench/summary.json; then | |
| curl -fsSL --max-time 30 "$BENCH_URL/env.json" -o _site/bench/env.json || true | |
| curl -fsSL --max-time 30 "$BENCH_URL/per-run.json" -o _site/bench/per-run.json || true | |
| curl -fsSL --max-time 30 "$BENCH_URL/summary.md" -o _site/bench/summary.md || true | |
| echo "bench_data=present" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::bench data unavailable at $BENCH_URL; skipping bench page render" | |
| rmdir _site/bench | |
| echo "bench_data=absent" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Render bench page | |
| if: steps.fetch-bench.outputs.bench_data == 'present' | |
| run: scripts/bench-report _site/bench/summary.json _site/bench/index.html | |
| # Lobsters bench results page — same Hetzner endpoint, /lobsters/ path | |
| # (roundbench.showcase.party/lobsters/ → _site/bench/lobsters/). Fetched | |
| # and rendered independently of the blog bench above: it renders | |
| # whenever lobsters data is reachable even if the blog data isn't, and | |
| # degrades the same way (no dir, link 404s until the next cycle). | |
| # The nightly serves the LANE layout (matrix.json at the run root, one | |
| # subdir per lane — what `bench-lobsters LANES…` writes); the flat | |
| # summary.json fetch remains as the legacy fallback so a rollback on | |
| # the bench box doesn't blank the page. | |
| - name: Fetch latest lobsters bench data | |
| id: fetch-lobsters-bench | |
| env: | |
| BENCH_URL: https://roundbench.showcase.party/lobsters | |
| run: | | |
| mkdir -p _site/bench/lobsters | |
| fetch() { curl -fsSL --max-time 30 "$1" -o "$2" || { rm -f "$2"; return 1; }; } | |
| if fetch "$BENCH_URL/matrix.json" _site/bench/lobsters/matrix.json; then | |
| fetch "$BENCH_URL/env.json" _site/bench/lobsters/env.json || true | |
| # Emitted-tree tarballs (~390 KB + ~820 KB). Mirrored so Pages is | |
| # the download source: the bench box is timing runs, and serving a | |
| # multi-megabyte download mid-measurement perturbs the very numbers | |
| # the archive is evidence for. The report links them relatively, so | |
| # this copy links to this copy. `|| true` — runs predating the | |
| # tarballs still render, they just have nothing to link. | |
| for f in spinel-tree.tgz ruby-tree.tgz; do | |
| fetch "$BENCH_URL/$f" "_site/bench/lobsters/$f" || true | |
| done | |
| # spinel included: the AOT lane is rendered on the bench box's own | |
| # page, and without its summary.json here the mirrored page drops | |
| # the AOT bars, the spinel parity badge and the AOT toolchain | |
| # provenance — a quieter page that reads as a complete one. | |
| for lane in rails rails-int rails-jruby ruby ruby-int jruby spinel; do | |
| mkdir -p "_site/bench/lobsters/$lane" | |
| for f in summary.json rails.json env.json summary.md; do | |
| fetch "$BENCH_URL/$lane/$f" "_site/bench/lobsters/$lane/$f" || true | |
| done | |
| rmdir "_site/bench/lobsters/$lane" 2>/dev/null || true | |
| done | |
| # A matrix with only the Rails lanes (a partial nightly where the | |
| # roundhouse emit lane failed to produce a summary) is unrenderable | |
| # — lobsters-report aborts without the canonical ruby/ruby-int lane. | |
| # Treat that like absent data and skip the render, rather than | |
| # failing the whole build-site job on a bench-box hiccup. | |
| if [ -f _site/bench/lobsters/ruby/summary.json ] || [ -f _site/bench/lobsters/ruby-int/summary.json ]; then | |
| echo "lobsters_bench_data=present" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::lobsters matrix present but no ruby emit lane summary at $BENCH_URL; skipping lobsters bench page render" | |
| rm -rf _site/bench/lobsters | |
| rmdir _site/bench 2>/dev/null || true | |
| echo "lobsters_bench_data=absent" >> "$GITHUB_OUTPUT" | |
| fi | |
| elif fetch "$BENCH_URL/summary.json" _site/bench/lobsters/summary.json; then | |
| fetch "$BENCH_URL/rails.json" _site/bench/lobsters/rails.json || true | |
| fetch "$BENCH_URL/env.json" _site/bench/lobsters/env.json || true | |
| fetch "$BENCH_URL/sequence.json" _site/bench/lobsters/sequence.json || true | |
| fetch "$BENCH_URL/summary.md" _site/bench/lobsters/summary.md || true | |
| echo "lobsters_bench_data=present" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::lobsters bench data unavailable at $BENCH_URL; skipping lobsters bench page render" | |
| rmdir _site/bench/lobsters | |
| rmdir _site/bench 2>/dev/null || true | |
| echo "lobsters_bench_data=absent" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Render lobsters bench page | |
| if: steps.fetch-lobsters-bench.outputs.lobsters_bench_data == 'present' | |
| # The report detects the layout from the directory contents. | |
| run: scripts/lobsters-report _site/bench/lobsters _site/bench/lobsters/index.html | |
| # Lobsters conformance — upstream HEAD's own RSpec suite run against | |
| # an emit of the same checkout (scripts/lobsters-specs on the bench | |
| # box, published to roundbench.showcase.party/lobsters-specs/). The | |
| # JSON is fetched and rendered HERE rather than fetching the page the | |
| # box already rendered: the renderer lives in this repo and evolves | |
| # with it, so a fix to the report reaches the site on the next CI run | |
| # instead of waiting for the box's next cron cycle. | |
| # | |
| # Deliberately ungated on content. If the box publishes a run whose | |
| # canary failed or whose checkout was dirty, the page renders WITH the | |
| # alarm banner that says so — suppressing it would defeat the point of | |
| # having one. | |
| - name: Fetch latest lobsters conformance data | |
| id: fetch-lobsters-specs | |
| env: | |
| BENCH_URL: https://roundbench.showcase.party/lobsters-specs | |
| run: | | |
| mkdir -p _site/bench/lobsters-specs | |
| if curl -fsSL --max-time 30 "$BENCH_URL/summary.json" -o _site/bench/lobsters-specs/summary.json; then | |
| echo "specs_data=present" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::warning::lobsters conformance data unavailable at $BENCH_URL; skipping conformance page render" | |
| rm -rf _site/bench/lobsters-specs | |
| rmdir _site/bench 2>/dev/null || true | |
| echo "specs_data=absent" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Render lobsters conformance page | |
| if: steps.fetch-lobsters-specs.outputs.specs_data == 'present' | |
| run: scripts/lobsters-spec-report _site/bench/lobsters-specs _site/bench/lobsters-specs/index.html | |
| # The per-target archives, for the e2e-<target> jobs to download and | |
| # boot — the exact bytes published to rubys.github.io/roundhouse. | |
| - name: Upload browse archives | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: browse-archives | |
| path: _site/browse/*.tgz | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@v5 | |
| with: | |
| path: _site | |
| deploy: | |
| needs: | |
| - build-site | |
| - unit | |
| if: github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v5 |