docs(spec): image support (view.image + <ImageUpload> + metaobjects-u… #222
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
| # Local CI — runs on the maintainer's self-hosted runner at ZERO Actions cost. | |
| # | |
| # SECURITY (public repo + self-hosted runner): this workflow must NEVER gain a | |
| # `pull_request` trigger. Push events to main can only come from users with | |
| # write access, so fork-submitted code cannot reach the runner. Keep it that way. | |
| # | |
| # Tiers: | |
| # push to main -> affected ports only (scripts/ci-affected-ports.sh) | |
| # nightly + dispatch -> full matrix | |
| # | |
| # The ts and java ports are each split into two parallel jobs — a FAST lane (the | |
| # quick pure-code correctness signal: ts-fast = build+typecheck/conformance/ | |
| # completeness, java-fast = java+kotlin conformance) and a SLOW lane (docker | |
| # integration + heavy build: ts-slow, java-slow = reactor + integration). The | |
| # split surfaces the verdict fast and means a cancel/OOM/flake mid-integration no | |
| # longer kills the fast signal. `detect` emits the umbrella token (`ts` / `java`); | |
| # both of that port's jobs gate on it. | |
| # | |
| # Integration-running jobs (ts-slow, python, csharp, java-slow) share a Postgres | |
| # SIDECAR instead of each test booting its own Testcontainers Postgres per | |
| # scenario (the java+kotlin lanes alone booted ~30-40 serial containers). Each | |
| # port's PG helper honors METAOBJECTS_TEST_PG_URL and CREATEs a uniquely-named | |
| # database per scenario against the sidecar. The sidecar publishes 5432 to a | |
| # DYNAMIC host port (`ports: - 5432`, read back via job.services.*.ports) so | |
| # concurrent jobs on the shared self-hosted host never collide on a fixed port. | |
| # | |
| # Measured 2026-07-03 (pre-split, full tier, all ports in parallel, warm caches): | |
| # detect 0.2m · gates 0.1m · csharp 2m · python 2.6m · ts 5.3m · java 9.8m | |
| name: local-ci | |
| on: | |
| push: | |
| branches: [main] | |
| schedule: | |
| - cron: '17 8 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| tier: | |
| description: 'full = all ports; affected = ports touched by HEAD~1..HEAD' | |
| type: choice | |
| options: [full, affected] | |
| default: full | |
| concurrency: | |
| group: local-ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| detect: | |
| runs-on: [self-hosted, linux] | |
| outputs: | |
| ports: ${{ steps.sel.outputs.ports }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - id: sel | |
| env: | |
| BASE: ${{ github.event.before }} | |
| run: | | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| ports="$(scripts/ci-affected-ports.sh "$BASE" "$GITHUB_SHA")" | |
| elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.tier }}" = "affected" ]; then | |
| ports="$(scripts/ci-affected-ports.sh "$GITHUB_SHA~1" "$GITHUB_SHA")" | |
| else | |
| ports="ts java python csharp" | |
| fi | |
| echo "selected ports: '$ports'" | |
| echo "ports=$ports" >> "$GITHUB_OUTPUT" | |
| gates: | |
| needs: detect | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # leak-scan diffs against origin/main | |
| persist-credentials: false | |
| - run: scripts/ci-local.sh --only gates --strict-toolchains | |
| # TS FAST lane — build+typecheck, conformance, completeness (the pure-code | |
| # correctness signal). No docker/integration, so no sidecar. | |
| ts-fast: | |
| needs: detect | |
| if: contains(needs.detect.outputs.ports, 'ts') | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - run: scripts/ci-local.sh --only ts-fast --strict-toolchains | |
| # TS SLOW lane — build + docker/sidecar integration tests. Uses the shared | |
| # Postgres sidecar (dynamic host port; see the note above the integration jobs). | |
| ts-slow: | |
| needs: detect | |
| if: contains(needs.detect.outputs.ports, 'ts') | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 45 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: metaobjects | |
| POSTGRES_PASSWORD: metaobjects | |
| POSTGRES_DB: metaobjects_test | |
| ports: | |
| - 5432 # dynamic host port — no fixed-port collision | |
| options: >- | |
| --health-cmd "pg_isready -U metaobjects" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - env: | |
| METAOBJECTS_TEST_PG_URL: postgres://metaobjects:metaobjects@localhost:${{ job.services.postgres.ports['5432'] }}/metaobjects_test | |
| run: scripts/ci-local.sh --only ts-slow --strict-toolchains | |
| # Java FAST lane — java + kotlin conformance only (the quick correctness signal). | |
| # No docker/integration, so no sidecar. | |
| java-fast: | |
| needs: detect | |
| if: contains(needs.detect.outputs.ports, 'java') | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - run: scripts/ci-local.sh --only java-fast --strict-toolchains | |
| # Java SLOW lane — full reactor + docker integration (java + kotlin). Uses the | |
| # shared Postgres sidecar so integration stops booting per-scenario containers. | |
| java-slow: | |
| needs: detect | |
| if: contains(needs.detect.outputs.ports, 'java') | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 45 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: metaobjects | |
| POSTGRES_PASSWORD: metaobjects | |
| POSTGRES_DB: metaobjects_test | |
| ports: | |
| - 5432 | |
| options: >- | |
| --health-cmd "pg_isready -U metaobjects" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - env: | |
| METAOBJECTS_TEST_PG_URL: postgres://metaobjects:metaobjects@localhost:${{ job.services.postgres.ports['5432'] }}/metaobjects_test | |
| run: scripts/ci-local.sh --only java-slow --strict-toolchains | |
| python: | |
| needs: detect | |
| if: contains(needs.detect.outputs.ports, 'python') | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 45 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: metaobjects | |
| POSTGRES_PASSWORD: metaobjects | |
| POSTGRES_DB: metaobjects_test | |
| ports: | |
| - 5432 | |
| options: >- | |
| --health-cmd "pg_isready -U metaobjects" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - env: | |
| METAOBJECTS_TEST_PG_URL: postgres://metaobjects:metaobjects@localhost:${{ job.services.postgres.ports['5432'] }}/metaobjects_test | |
| run: scripts/ci-local.sh --only python --strict-toolchains | |
| csharp: | |
| needs: detect | |
| if: contains(needs.detect.outputs.ports, 'csharp') | |
| runs-on: [self-hosted, linux] | |
| timeout-minutes: 45 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: metaobjects | |
| POSTGRES_PASSWORD: metaobjects | |
| POSTGRES_DB: metaobjects_test | |
| ports: | |
| - 5432 | |
| options: >- | |
| --health-cmd "pg_isready -U metaobjects" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - env: | |
| METAOBJECTS_TEST_PG_URL: postgres://metaobjects:metaobjects@localhost:${{ job.services.postgres.ports['5432'] }}/metaobjects_test | |
| run: scripts/ci-local.sh --only csharp --strict-toolchains | |
| notify: | |
| needs: [detect, gates, ts-fast, ts-slow, java-fast, java-slow, python, csharp] | |
| if: always() && contains(needs.*.result, 'failure') | |
| runs-on: [self-hosted, linux] | |
| steps: | |
| - env: | |
| # env-indirection: head_commit.message is untrusted text — never | |
| # interpolate ${{ }} directly into the script body. | |
| HEAD_MSG: ${{ github.event.head_commit.message }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| export DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$(id -u)/bus" | |
| notify-send -u critical "local-ci FAILED: metaobjects" \ | |
| "$(printf '%s' "$HEAD_MSG" | head -n1) | |
| $RUN_URL" || echo "notify-send unavailable; GitHub email still fires" |