Files
openmaic/OpenMAIC/.github/workflows/ci.yml
2026-08-16 14:58:47 +08:00

276 lines
10 KiB
YAML

name: CI
on:
push:
# Integration branches are listed here as well as below: a pull_request run
# only ever proves one part merged into the branch, so without a push
# trigger the accumulated state of the branch that eventually reaches main
# is never built on its own.
branches: [main, integration/kv-asset-server-backend]
pull_request:
branches:
[
main,
feat/maic-editor-v0,
feat/maic-editor-v1,
runtime-server-backend,
integration/kv-asset-server-backend,
]
concurrency:
group: ci-${{ github.ref }}
# Never cancel a main run. Each main run validates only its own push range, so
# a superseded run takes the range that contained a package change with it and
# the replacement, comparing against the cancelled tip, sees nothing.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check:
name: Lint, Typecheck & Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Package version bumps
if: github.event_name == 'pull_request'
run: node scripts/check-package-version-bumps.mjs "$BASE_SHA"
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
# A branch creation or a force push reports an unusable `before`. Fail
# rather than substituting HEAD^: a force push can replace many commits at
# once, so the previous commit is not the range that needs checking, and
# publishing requires this run to be green.
- name: Package version bumps (main push)
if: github.event_name == 'push'
run: |
set -euo pipefail
base="$BEFORE_SHA"
if [ -z "$base" ] || ! git rev-parse --verify --quiet "$base^{commit}" >/dev/null; then
echo "::error::Push range base ${base:-<empty>} is unavailable, so this push cannot be checked."
echo "Re-run after the branch has an ordinary push range, or verify the versions by hand."
exit 1
fi
node scripts/check-package-version-bumps.mjs "$base"
env:
BEFORE_SHA: ${{ github.event.before }}
# The tarball smoke test proves this about the bytes that would actually
# be published, but it installs five package tarballs, so it runs only on
# the release path. This is the cheap source-level form, here so that a
# pull request restoring `workspace:*` fails at review time rather than at
# release time, after a version number has already been spent.
- name: Internal dependency ranges
run: node scripts/check-internal-dependency-ranges.mjs
- run: pnpm install --frozen-lockfile
# `postinstall` builds all five @openmaic packages, and some of them
# generate tracked, publishable files (renderer's fonts.css and its KaTeX
# font snapshot). The publish workflow refuses to release when the build
# rewrites one; catching it here means a stale generated file is a pull
# request failure instead of a release failure.
#
# Against GITHUB_SHA, not the index or HEAD: a bare `git diff` misses
# staged changes, while install code can create a commit and move HEAD.
# This check exists precisely because code ran before it, so its reference
# point must be something that code cannot move. Disabling replacement
# objects also prevents a local replace ref from redirecting the commit.
- name: Verify the install did not rewrite tracked package files
run: |
set -euo pipefail
actual_head="$(git --no-replace-objects rev-parse HEAD)"
if [ "$actual_head" != "$GITHUB_SHA" ]; then
echo "::error::HEAD moved from $GITHUB_SHA to $actual_head during installation."
exit 1
fi
if ! git --no-replace-objects diff --quiet "$GITHUB_SHA" -- packages/@openmaic; then
echo "::error::Installing rewrote tracked files under packages/@openmaic."
echo "Regenerate them and commit the result."
git --no-replace-objects --no-pager diff --stat "$GITHUB_SHA" -- packages/@openmaic
exit 1
fi
echo "Tracked package files match the commit under test."
- name: Prettier
run: pnpm check
- name: ESLint
run: pnpm lint
- name: TypeScript
run: npx tsc --noEmit
- name: i18n Key Alignment
run: pnpm check:i18n-keys
- name: Unit Tests
run: pnpm test
- name: Unit Tests (importer)
run: pnpm --filter @openmaic/importer test
- name: Unit Tests (dsl)
run: pnpm --filter @openmaic/dsl test
- name: TypeScript (generation)
run: pnpm --filter @openmaic/generation run typecheck
- name: Unit Tests (generation)
run: pnpm --filter @openmaic/generation test
- name: Node consumer smoke (generation scene)
shell: bash
run: |
set -euo pipefail
port=43127
node scripts/generation-node-smoke-server.mjs --port "$port" >"$RUNNER_TEMP/generation-smoke-server.log" 2>&1 &
server_pid=$!
cleanup() {
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
}
trap cleanup EXIT
for attempt in {1..50}; do
if node -e "fetch('http://127.0.0.1:$port/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"; then
break
fi
if [ "$attempt" -eq 50 ]; then
cat "$RUNNER_TEMP/generation-smoke-server.log"
exit 1
fi
sleep 0.1
done
output="$(node scripts/generation-node-smoke.mjs \
--requirement "Explain dependency injection" \
--endpoint "http://127.0.0.1:$port" \
--model smoke-model)"
printf '%s\n' "$output"
node -e 'const value = JSON.parse(process.argv[1]); if (!Array.isArray(value.outlines) || value.outlines.length === 0 || !value.scene || value.sceneValidation?.valid !== true) process.exit(1)' "$output"
# Covers the package's own test tree, which the root tsc reaches only by an
# incidental glob. The device-scope guard is written as `@ts-expect-error`
# probes in those tests, and a probe nothing type-checks proves nothing.
- name: TypeScript (storage, incl. tests)
run: pnpm --filter @openmaic/storage run typecheck
- name: Unit Tests (storage)
run: pnpm --filter @openmaic/storage test
render-service:
name: Render Service (typecheck + tests)
runs-on: ubuntu-latest
# The producer dep bundles puppeteer; skip its Chromium download — the unit
# tests exercise the service's boundaries (unzip, admission, body caps) and
# never launch a browser.
env:
PUPPETEER_SKIP_DOWNLOAD: "true"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
# render-service is its own npm package (own lockfile, Node-only deps),
# excluded from the root eslint/tsc/pnpm workspace — so it gets its own
# install + quality gate here rather than riding on the `check` job.
cache: npm
cache-dependency-path: render-service/package-lock.json
- name: Install
working-directory: render-service
run: npm ci
- name: TypeScript
working-directory: render-service
run: npm run typecheck
- name: Unit Tests
working-directory: render-service
run: npm test
- name: Build container image
run: docker build --tag openmaic-render-service:ci render-service
e2e:
name: E2E Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm exec playwright install chromium --with-deps
# Measures the emitted cover cards in Chromium. Lives with the unit tests
# (it needs the compiler), but only this job has a browser, so it runs
# here with the flag that turns a missing one into a failure.
- name: Cover-card layout guardrail
run: pnpm exec vitest run tests/video-export/cover-card-layout.browser.test.ts
env:
COVER_LAYOUT_BROWSER: '1'
- name: Interactive static HTML Chromium smoke
run: pnpm exec vitest run tests/video-export/interactive-static-html.browser.test.ts
env:
INTERACTIVE_STATIC_BROWSER: '1'
# runner.* is only valid in step contexts (not job-level env).
- name: Materialize Hyperframes lint samples
run: pnpm exec vitest run tests/video-export/e2e-materialize.test.ts
env:
HF_E2E_DIR: ${{ runner.temp }}/openmaic-hyperframes-samples
# Hyperframes 0.7.60 reports warning-only lint with exit status zero, so
# require its plain clean summary as well as a successful process.
- name: Lint Hyperframes samples
shell: bash
env:
HF_E2E_DIR: ${{ runner.temp }}/openmaic-hyperframes-samples
run: |
set -euo pipefail
for sample in quiz pbl-v2 pbl-legacy pbl-dense mixed arabic interactive-static; do
dir="$HF_E2E_DIR/$sample"
if output="$(pnpm exec hyperframes lint "$dir" 2>&1)"; then
status=0
else
status=$?
fi
printf '%s\n' "$output"
if [ "$status" -ne 0 ]; then
echo "::error::Hyperframes lint failed for $sample (exit $status)."
exit "$status"
fi
if ! grep -Fq -- '0 errors, 0 warnings' <<<"$output"; then
echo "::error::Hyperframes lint for $sample did not report zero errors and zero warnings."
exit 1
fi
done
- name: Run e2e tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7