Skip to content

fix(ENG-933): robustness fixes across trainer, studio, credentials, scaffold, CI #2020

fix(ENG-933): robustness fixes across trainer, studio, credentials, scaffold, CI

fix(ENG-933): robustness fixes across trainer, studio, credentials, scaffold, CI #2020

Workflow file for this run

name: CI
on:
push:
pull_request:
jobs:
# Repo-wide em-dash guard. The script scans every tracked file (yaml,
# md, json, html, root config, AND the linted TS / TSX / JS / JSX /
# MTS / MJS source). The overlap with the ESLint rule
# `local/no-em-dash` on source files is the point: this job is one
# single check that succeeds iff the repo is clean end-to-end, so a
# future weakening of the rule's include globs cannot silently let an
# em dash slip through. The check is a single `git grep`, so the job
# finishes in a few seconds and does not consume Node / pnpm cache
# budget.
no_em_dash:
name: no em dashes
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
# The job runs `git grep` on the checkout but never pushes or
# talks to the remote, so the `GITHUB_TOKEN` Actions stashes
# in `.git/config` by default is unused. Disabling persistence
# keeps it from being shipped in any artifact / log capture
# downstream (`zizmor`'s `artipacked` lint). The matching
# `excessive-permissions` lint zizmor also emits is *not*
# addressed by an explicit `permissions: { contents: read }`
# block on this job: this is a public repo on GitHub's
# default-restrictive workflow-permissions setting, so the
# token already arrives as `contents: read` only, and the
# other jobs in this file follow the same pattern (only
# `coverage` carries a `permissions:` block, and only
# because Codecov OIDC + GitHub native coverage upload
# need scopes the default does not include). Adding the
# block here would break that pattern without granting any
# additional security.
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
- name: Check no em dashes
run: node scripts/check-no-em-dash.mts
build:
name: typecheck · lint · test · build · ${{ matrix.os }} · node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
# Force bash on every OS so `run:` steps that use POSIX redirection (e.g.
# `>> "$GITHUB_STEP_SUMMARY"`) work uniformly. windows-latest defaults to
# pwsh otherwise. Git Bash ships on the GitHub-hosted Windows image.
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
# Cross every Node entry below with all three GitHub-hosted OS images.
# The Node matrix exists to surface strip-types / transform-types /
# amaro behaviour shifts; running it on Linux + Windows + macOS also
# catches platform-specific regressions in path handling, child
# process spawning, and pnpm hardlink/symlink behaviour.
os: [ubuntu-latest, windows-latest, macos-latest]
# The 22.x line is pinned to the DoS-patched range (22.22.0+, see
# https://nodejs.org/en/blog/vulnerability/january-2026-dos-mitigation-async-hooks);
# earlier 22.x are out of support. The 24.x entries still cover
# strip-types / transform-types / amaro behaviour shifts.
node:
# Node 22 LTS DoS-patched range
- '22.22.0' # exact minimum supported version (DoS-patched)
- '>=22.22.0 <23' # latest 22.x range
- '>=24.0.0 <24.1.0' # Node 24 initial state; strip-types default-on / Stability: RC
- '>=24.1.0 <24.3.0' # amaro updates
# Major: experimental warning removed in Node 24
- '>=24.3.0 <24.5.0' # warning removed / amaro 1.1.0 / CJS TS loader refactor
- '>=24.5.0 <24.8.0' # amaro updates
- '>=24.8.0 <24.10.0' # amaro updates
- '>=24.10.0 <24.12.0' # amaro updates
# Major: type stripping marked Stable in Node 24 LTS
- '24.12.0' # exact boundary version where type stripping became stable
- '>=24.12.0 <25' # stable Node 24 range after 24.12.0
# Node 26 (Current). Follow the same floor + latest pattern as
# Node 24: 26.0.0 catches initial-release behaviour, the open
# range pulls in patch updates without manual bumps.
- '26.0.0' # exact 26.x floor (initial release)
- '>=26.0.0 <27' # latest 26.x
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# action-setup@v6 bootstrap-installs pnpm v11 (per its committed
# lockfile) and only honours `packageManager` via pnpm's own
# `manage-package-manager-versions`. Force @pnpm/exe so bootstrap
# pnpm runs on its bundled Node regardless of which matrix Node
# we switch to next.
#
# Pinning `version: 11.13.1` instead doesn't work: action-setup
# compares it against the full `[email protected]+sha512.xxx`
# packageManager string and refuses on mismatch
# (ERR_PNPM_BAD_PM_VERSION-style error).
#
# Do NOT pin pnpm 11.12.0 or 11.13.0: those two releases publish
# their per-platform npm packages (@pnpm/linux-x64, @pnpm/macos-
# arm64, @pnpm/win-x64, ...) WITHOUT the native binary (~1.9 kB
# metadata-only tarballs), so `standalone: true` here resolves
# @pnpm/exe, finds no binary, and leaves the placeholder that dies
# with `pnpm: 1: This: not found` on every OS. Fixed upstream in
# 11.13.1 (platform packages ship the binary again); 11.11.0 was
# the last good release before the gap. See pnpm/pnpm#12955
# (release-flow regression from pnpm/pnpm#12949, fixed in 11.13.1).
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
standalone: true
# Turbo's local cache is at .turbo/cache. Keying on the commit SHA
# always writes a fresh entry; restore-keys falls back to the most
# recent prior cache for the same OS/node so unaffected task hashes
# hit immediately. node version is part of the key because turbo's
# hash currently does not factor it in, and we want to keep the
# matrix entries from clobbering each other if that ever changes.
- name: Cache Turborepo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-node${{ matrix.node }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-node${{ matrix.node }}-
- name: Setup matrix Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Print resolved Node version
run: |
node --version
echo "**Resolved Node:** \`$(node --version)\` (matrix: \`${{ matrix.node }}\`)" >> "$GITHUB_STEP_SUMMARY"
- name: Install
# Belt-and-suspenders: pnpm-workspace.yaml's `allowBuilds`
# already gates which packages may run postinstall, and
# `playwright` is not in that list, so `pnpm install` won't
# invoke Playwright's browser-download script today. Setting
# `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` here keeps that
# contract explicit so a future `allowBuilds` change can't
# silently re-enable a 150 MB postinstall download that the
# following cache + `playwright install` steps are meant to
# satisfy.
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
run: pnpm install
- name: Typecheck
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Format check
run: pnpm format:check
# Playwright bundles its own browser binaries (~150 MB). Cache by
# OS and the lockfile hash so a `@playwright/test` upgrade busts
# the cache automatically. Cache locations are OS-specific:
# Linux uses ~/.cache/ms-playwright, macOS uses
# ~/Library/Caches/ms-playwright, Windows uses
# %USERPROFILE%/AppData/Local/ms-playwright. `actions/cache`
# expands `~` through Node's `os.homedir()` and only treats `/`
# as a path separator on Windows runners; `~\AppData\…` would
# not resolve, so all three patterns use POSIX separators.
# `actions/cache` silently skips paths that don't exist on the
# current runner, so listing all three is safe.
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cache/ms-playwright
~/Library/Caches/ms-playwright
~/AppData/Local/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
playwright-${{ runner.os }}-
# `--with-deps` installs apt packages that Chromium needs at
# runtime (libnss3, libxkbcommon0, etc.). It's Linux-only; the
# action no-ops it on macOS/Windows but warns; gating on runner.os
# keeps the log clean.
- name: Install Playwright (Linux)
if: runner.os == 'Linux'
run: pnpm --filter @arkor/e2e-studio exec playwright install --with-deps chromium
- name: Install Playwright (macOS/Windows)
if: runner.os != 'Linux'
run: pnpm --filter @arkor/e2e-studio exec playwright install chromium
- name: Test
env:
ARKOR_INTERNAL_SCAFFOLD_ARKOR_SPEC: file:${{ github.workspace }}/packages/arkor
run: pnpm test
# Upload Playwright's HTML report + traces / videos / screenshots
# for every non-cancelled outcome so failures retain their
# artefacts regardless of which step failed. `if: failure()`
# would only fire when a *prior* step had already failed at the
# time this step runs; because Upload sits before Build (line
# below), a Test green + Build red sequence used to lose the
# report entirely. `always() && !cancelled()` also uploads on
# fully-green runs; the report dir is small then, so the extra
# artefact cost is negligible relative to the debugging value of
# having traces for every run. The `!cancelled()` half ensures
# a manual cancel doesn't get stalled waiting for an artefact
# upload that the maintainer is no longer interested in.
# `if-no-files-found: ignore` covers the case where Turbo
# cancelled `@arkor/e2e-studio#test` before Playwright could
# write anything (see comment on the option below).
#
# `matrix.node` would disambiguate across the matrix but it
# contains `<`, `>`, `=`, which `actions/upload-artifact` rejects
# (NTFS-incompatible); `strategy.job-index` is the documented
# alphanumeric escape hatch.
- name: Upload Playwright report
if: ${{ always() && !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-${{ runner.os }}-${{ strategy.job-index }}
path: |
e2e/studio/playwright-report
e2e/studio/test-results
# Turbo may cancel `@arkor/e2e-studio#test` when an earlier
# task fails (e.g. a unit test failure in `arkor:test`),
# leaving Playwright with no chance to write its report.
# `actions/upload-artifact@v7` defaults to failing the step
# when no files match, which would convert "test step
# failed for an unrelated reason" into "upload step also
# failed" and noise up the CI summary. `ignore` makes the
# upload silently no-op when there's nothing to publish.
if-no-files-found: ignore
- name: Build
run: pnpm build
# Cross-pm install matrix: exercises `arkor init --use-<pm>` and
# `create-arkor --use-<pm>` against every package manager the SDK accepts,
# crossed with OS and a curated set of Node versions known to shift
# spawn / pipe / strip-types behaviour (e.g. libuv 1.51 in Node 22.17
# changed macOS pipe-flush timing, the kind of thing that ate hours of
# debugging in eng-606). The build job above already runs `pnpm test`
# end-to-end, but its install assertions only cover the pms that a
# typical contributor has on PATH (npm + pnpm). The yarn / yarn-berry /
# bun lanes pull their runtimes in CI-only because they aren't a
# development prerequisite and we don't want `pnpm test` on a fresh
# checkout to require them.
#
# KNOWN MATRIX GAP (Windows × bun × `arkor init`): this combination is
# NOT covered by the install matrix. `arkor init --use-bun` on Windows
# currently fails to produce `bun.lock` due to a CLI-binary-axis
# divergence between `arkor init` and `create-arkor` (the two share
# `cli-internal/install.ts` but the spawn shape differs in a way that
# bun on Windows is sensitive to). The gate is implemented at the
# test-runner level in `e2e/cli/src/arkor-init.test.ts` via `it.skip`
# so vitest reports the case as skipped per CI run; the
# `create-arkor.test.ts` lane DOES run bun on Windows and covers bun
# itself + the create-arkor spawn shape. The `arkor init --use-bun`
# CLI flag stays exposed because the divergence is treated as a bug
# to fix, not a permanent constraint; until then, the
# docs/cli/init.mdx page and `docs/ja/cli/init.mdx` mirror carry an
# explicit Windows caveat on the `--use-bun` flag so users hitting it
# have signal that this is known. Re-enable the case (drop the
# `isBunOnWindows` arm in arkor-init.test.ts, drop the caveat in
# both docs/cli/init.mdx files, AND this comment block) when the
# spawn-shape divergence is fixed.
#
# Each runner sets `ARKOR_E2E_PM=<label>`; the e2e suites
# (e2e/cli/src/{arkor-init,create-arkor}.test.ts) gate their install
# cases on that env var so exactly one pm's install assertions run per
# matrix entry. The rest of the e2e suite runs unfiltered: setup
# (workspace install + CLI build + corepack/bun bootstrap) dominates
# each job's wall time, so the marginal cost of running every e2e test
# over the matrix pm/Node/OS triple is small and occasionally surfaces
# cross-axis interactions outside the install path (e.g. yarn-berry's
# PnP fs layout influencing scaffold or spawn assertions).
install-matrix:
name: install · ${{ matrix.id }} · ${{ matrix.os }} · node ${{ matrix.node }}
runs-on: ${{ matrix.os }}
# Gate the 147-job Cartesian fanout on the fast em-dash guard so a
# style regression in any tracked file (yaml, md, ts, ...) fails
# before the install matrix burns runner minutes provisioning
# corepack / yarn-berry / bun across every OS, Node, and pm cell.
needs: no_em_dash
defaults:
run:
shell: bash
strategy:
fail-fast: false
# Full Cartesian product: 3 OS × 7 Node × 7 pm = 147 jobs.
# This is intentional. `arkor init` and `create-arkor` are
# bootstrap-time entry points; every (OS, Node, pm) cell
# exercises a different combination of `child_process.spawn`
# shell semantics (`.cmd` shims on Windows, corepack
# provisioning, yarn-berry's PnP-vs-`node-modules` linker,
# pnpm 9 / 10 / 11 lockfile and `allowBuilds` schema
# divergence, bun's lockfile + spawn-shape quirks). The
# observed divergences this matrix has caught (bun on
# Windows missing `bun.lock` under `arkor init`'s spawn
# shape; pnpm 9's `packages:` requirement; yarn 4 PnP
# default on `CI=1`) were not reproducible from a smaller
# axis-folded matrix; a single missing combination would
# have masked the bug at review time. The runtime / queue-
# time cost is accepted for that reason.
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
# Curated Node versions: the supported Node lines, i.e.
# the active LTS lines (22, 24) plus Current (26, not
# yet LTS; promotes in Oct 2026). main's engines bump
# (`packages/arkor/package.json` → `>=22.22.0`) made the
# prior pre-22.22 entries (22.13/17/21) point at
# unsupported configurations, so they've been dropped
# along with the yarn × pre-22.22 excludes that masked
# the engines mismatch.
#
# Each Node line gets two entries: exact engines floor
# (so the lowest officially supported version is actually
# exercised, not just whatever 22.22+ patch setup-node
# happens to resolve) plus the live `<minor>` range. The
# build matrix above uses the same pattern.
node:
- '22.22.0' # exact engines floor (DoS-patched 22 LTS)
- '>=22.22.0 <23' # latest 22.x
- '24.0.0' # exact 24.x floor (initial release; type-stripping default-on / Stability: RC)
- '24.12.0' # exact boundary where type stripping became Stable
- '>=24.12.0 <25' # latest 24.x
- '26.0.0' # exact 26.x floor (initial release)
- '>=26.0.0 <27' # latest 26.x
id: [pnpm-9, pnpm-10, pnpm-11, npm, yarn, yarn-berry, bun]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# The prior `rolldownIncompat` bootstrap path (separate Node 24
# checkout to install + build CLIs before exercising them under
# a matrix Node where rolldown's native binding wouldn't load)
# was for the pre-22.22 Node entries that the engines bump
# removed from this matrix. Every supported Node now satisfies
# rolldown's `^20.19 || >=22.12` requirement, so the bootstrap
# path is no longer needed and the matrix uses a single Node
# setup throughout.
- name: Setup matrix Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ matrix.node }}
# Intentionally no `cache: pnpm`; at this point pnpm isn't on
# PATH yet (the install-matrix job uses Node's bundled corepack
# to provision pnpm in the *next* step instead of running
# `pnpm/action-setup` up-front), so setup-node's pnpm-aware
# cache lookup fails with `Unable to locate executable file:
# pnpm`. Each job re-fetches deps fresh; ~30 s/job extra is
# noise next to the 2–3 min setup cost, and skipping the cache
# avoids a class of cross-OS pitfalls (e.g. corepack writing
# shims into a system-owned bin dir on Windows).
# We deliberately use Node's bundled corepack instead of
# pnpm/action-setup. action-setup adds its pnpm to PATH in a way
# that shadows corepack's shim, which would defeat the matrix
# --activate below: every entry would end up running action-setup's
# pinned pnpm regardless of which version we asked corepack to
# activate. Workspace `pnpm install` still resolves to pnpm 11.13.1
# because corepack reads `packageManager` from the root
# package.json; only the matrix pm test runs in tmpdirs (no
# packageManager field there), so corepack falls back to the
# activated default.
- name: Enable corepack
run: |
# Some Node 22.x minors ship a corepack predating the
# npm-registry signing-key rotation, so a plain `corepack
# enable` followed by `pnpm install` blows up in
# `verifySignature`. Upgrade first to be safe across the
# matrix Nodes: `[email protected]` carries the
# post-rotation keys and its engines `^20.10.0 ||
# ^22.11.0 || >=24.0.0` cover every supported entry.
# `--force` is required on Windows to clobber the runner
# image's pre-seeded `C:\npm\prefix\yarn.cmd`. Pinned
# (not `@latest`) so an upstream corepack release can't
# change resolution behaviour or raise its supported-Node
# floor and start failing this matrix without any
# repository change, mirroring the same pinning policy
# applied to pnpm/yarn/bun below.
npm install -g --force [email protected]
corepack enable
echo "COREPACK_ENABLE_DOWNLOAD_PROMPT=0" >> "$GITHUB_ENV"
- name: Install workspace + build CLIs
run: |
pnpm install
pnpm --filter create-arkor build
pnpm --filter arkor build
# NOTE: an earlier "Pack arkor into a scaffold tarball" step
# used to run `pnpm pack` and expose the result as a workflow
# output, fed into the now-removed `ARKOR_INTERNAL_SCAFFOLD_ARKOR_SPEC`
# job env. With the per-case `file:../<tgz>` override (the
# only viable shape on Windows + pnpm-10 / bun) the real-
# install tests now pack their own tarball in `beforeAll`,
# so the workflow-level pack was paying its `pnpm pack` cost
# on every leg of the (very large) matrix without anything
# consuming the output. PR #99 round-39 Copilot review.
- name: Setup pm under test (${{ matrix.id }})
if: ${{ matrix.id != 'bun' }}
run: |
case "${{ matrix.id }}" in
pnpm-9) corepack prepare "[email protected]" --activate ;;
pnpm-10) corepack prepare "[email protected]" --activate ;;
pnpm-11) corepack prepare "[email protected]" --activate ;;
npm) echo "Using bundled npm $(npm --version)" ;;
yarn) corepack prepare "[email protected]" --activate ;;
# yarn classic and berry both use the `yarn` npm package;
# only the version line differs (1.x vs 4.x).
yarn-berry) corepack prepare "[email protected]" --activate ;;
esac
# bun isn't on corepack's package list and its official `curl |
# bash` installer is POSIX-only, so on Windows it'd silently no-op.
# `oven-sh/setup-bun` handles all three OSes uniformly and adds
# bun to PATH via $GITHUB_PATH for us.
#
# Pinning the version (rather than `latest`) keeps the matrix
# reproducible in lockstep with the corepack-prepared
# pnpm-*/yarn versions above. Copilot review on PR #99 flagged
# `bun-version: latest` as a way for an upstream bun release to
# silently flip CI green/red without a repo change. Bump in a
# dedicated commit when intentionally rolling forward.
- name: Setup bun
if: ${{ matrix.id == 'bun' }}
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: '1.3.13'
- name: Confirm pm version
run: |
# Run from $RUNNER_TEMP, not the workspace cwd. corepack
# enforces the workspace's `packageManager: [email protected]` and
# rejects `yarn --version` / `bun --version` with "This project
# is configured to use pnpm". Stepping out into a directory
# without a packageManager field lets corepack fall back to the
# version we just `--activate`d for the matrix pm, which is
# also more useful diagnostically (it prints the matrix
# version, not the workspace's pinned 11.13.1 for pnpm-N
# entries).
cd "$RUNNER_TEMP"
case "${{ matrix.id }}" in
pnpm-*) pnpm --version ;;
npm) npm --version ;;
yarn|yarn-berry) yarn --version ;;
bun) bun --version ;;
esac
# `ARKOR_INTERNAL_SCAFFOLD_ARKOR_SPEC` is deliberately NOT
# set at the job level. The real-install matrix tests
# (`runs real <pm> install + git commit` in
# `e2e/cli/src/{arkor-init,create-arkor}.test.ts`) pack
# their own `arkor-*.tgz` in `beforeAll` and pass a per-case
# `file:../<tgz>` override into `runCli` / `runCreateArkor`,
# which is required because pnpm 10 rejects absolute
# Windows-drive `file:` URIs (`file:D:\…`) and bun on Windows
# skips `bun.lock` generation for multi-segment relative
# `file:` paths. Setting a job-wide absolute fallback here
# would mask that requirement. A future test that performs
# a real install without overriding would silently pass on
# Linux/macOS and break only on Windows × pnpm-10. (PR #99
# round-39 Copilot review: that earlier fallback predated
# the per-case override and is now vestigial.)
#
# `YARN_NETWORK_CONCURRENCY=1` (yarn 1 optionalDeps
# extraction race) lives in e2e/cli/src/spawn-cli.ts, set
# per spawn so local `ARKOR_E2E_PM=yarn-berry pnpm --filter
# @arkor/e2e-cli test` runs work without extra env-var
# threading. The round-13 yarn-4 immutable-install fix lives
# in `packages/cli-internal/src/install.ts` itself (the SDK
# forwards `YARN_ENABLE_IMMUTABLE_INSTALLS=false` to the
# spawned `yarn install`), so real users running `arkor init
# --use-yarn` in their CI no longer fail on the missing
# initial lockfile.
- name: Run e2e suite under matrix pm
run: |
# Derive the e2e install-test gate label from the matrix id.
# yarn-berry and the pnpm-N entries share a common label
# across versions because the test only cares about which
# `--use-<flag>` to pass; the binary version is selected by
# corepack / setup-bun above.
#
# The label vocabulary lives in
# `e2e/cli/src/install-matrix.ts` (`InstallCaseLabel` /
# `INSTALL_CASES`). When adding or removing a matrix `id`
# here, update that file in lockstep; the e2e suite's gate
# function reads `ARKOR_E2E_PM` against the same label set.
case "${{ matrix.id }}" in
pnpm-*) PM_LABEL=pnpm ;;
yarn-berry) PM_LABEL=yarn-berry ;;
*) PM_LABEL="${{ matrix.id }}" ;;
esac
# We deliberately don't `-t`-filter to "real <pm> install":
# setup (workspace install + CLI build + corepack/bun) is the
# bulk of each job's wall time, so dropping the filter to run
# the full e2e suite is ~30 s/job extra and surfaces any
# cross-axis interactions we didn't anticipate (e.g. yarn-berry
# PnP affecting unrelated spawn paths). The install cases
# themselves are still pm-gated via `ARKOR_E2E_PM` so only one
# pm's install assertions run per job.
ARKOR_E2E_PM="$PM_LABEL" pnpm --filter @arkor/e2e-cli exec vitest run
coverage:
name: coverage · upload to Codecov
runs-on: ubuntu-latest
# The matrix above already exercises tests across Node versions; this
# job runs once on the newest LTS to keep coverage + Test Analytics
# uploads stable. Codecov uploads use OIDC (`use_oidc: true`); no
# CODECOV_TOKEN secret is stored in this repo. `id-token: write` is
# required for the runner to mint the OIDC JWT that codecov-action
# exchanges for an upload token. See
# https://docs.codecov.com/docs/github-tokenless-uploads.
#
# `code-quality: write` is required by GitHub's native code-coverage
# public preview (announced 2026-05-26): `actions/upload-code-coverage`
# POSTs the Cobertura XML through the GitHub API using the workflow's
# default GITHUB_TOKEN, and the token needs that scope to write to
# the code-quality API. Coverage uploads to GitHub run in parallel
# with the Codecov uploads below; Codecov stays authoritative while
# the GitHub feature is in preview, so both signals are available
# side-by-side. See
# https://github.blog/changelog/2026-05-26-code-coverage-in-pull-requests-is-now-in-public-preview/.
permissions:
id-token: write
code-quality: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Same reason as the build job; see comment there.
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
standalone: true
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
cache: pnpm
- name: Cache Turborepo
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-coverage-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-coverage-
- name: Install
# Belt-and-suspenders: pnpm-workspace.yaml's `allowBuilds`
# already gates which packages may run postinstall, and
# `playwright` is not in that list, so `pnpm install` won't
# invoke Playwright's browser-download script today. Setting
# `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` here keeps that
# contract explicit so a future `allowBuilds` change can't
# silently re-enable a 150 MB postinstall download that the
# following cache + `playwright install` steps are meant to
# satisfy.
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
run: pnpm install
# Mirror the matrix job's Playwright cache so the coverage run
# also reuses the previously-downloaded browsers.
- name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
playwright-${{ runner.os }}-
- name: Install Playwright
run: pnpm --filter @arkor/e2e-studio exec playwright install --with-deps chromium
# Tests must keep going on failure so every package still produces
# its lcov.info / junit.xml; turbo's default `--continue=never` would
# cancel sibling tasks after the first failure and starve the upload
# steps below. Subsequent uploads gate on `!cancelled()` so they run
# even when tests fail; the job's final outcome still reflects the
# test result.
- name: Test with coverage
env:
ARKOR_INTERNAL_SCAFFOLD_ARKOR_SPEC: file:${{ github.workspace }}/packages/arkor
# `create-arkor`'s tsdown only emits sourcemaps when this is
# set; e2e/cli's c8 needs them to remap V8 coverage from the
# spawned scaffolder back to packages/create-arkor/src/**.
# Replaces the inline `CREATE_ARKOR_BUILD_SOURCEMAP=1 ...`
# that lived in e2e/cli's pretest:coverage hook before
# workspace deps + turbo's `^build` made that pretest
# redundant.
CREATE_ARKOR_BUILD_SOURCEMAP: "1"
run: pnpm exec turbo run test:coverage --continue
- name: Upload arkor coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
files: ./packages/arkor/coverage/lcov.info
flags: arkor
fail_ci_if_error: false
- name: Upload create-arkor coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
files: ./packages/create-arkor/coverage/lcov.info
flags: create-arkor
fail_ci_if_error: false
- name: Upload cli-internal coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
files: ./packages/cli-internal/coverage/lcov.info
flags: cli-internal
fail_ci_if_error: false
- name: Upload studio-app coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
files: ./packages/studio-app/coverage/lcov.info
flags: studio-app
fail_ci_if_error: false
# E2E coverage is collected via c8 wrapping vitest in e2e/cli; the
# spawned CLI children inherit NODE_V8_COVERAGE and their hits are
# remapped through tsdown's sourcemaps back into
# packages/{arkor,create-arkor}/src/**.
- name: Upload e2e-cli coverage
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
files: ./e2e/cli/coverage/lcov.info
flags: e2e-cli
fail_ci_if_error: false
# GitHub native code coverage (public preview, announced 2026-05-26).
# Uploads the Cobertura XML produced alongside lcov.info by each
# package's vitest v8 coverage reporter so GitHub can display
# per-PR coverage % directly on pull requests. e2e/cli is excluded:
# see the skip note below for the c8 / cobertura empty-output
# rationale.
# Codecov stays authoritative while the feature is in preview; once
# GitHub Coverage reaches GA we can decide whether to drop one path.
# `fail-on-error: false` mirrors the Codecov uploads' philosophy:
# an upload glitch must not turn a green test run red. The action
# itself already no-ops on `merge_group` events and fork PRs, so no
# extra `if:` gating is needed for those cases.
- name: Upload arkor coverage to GitHub
if: ${{ !cancelled() }}
uses: actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3 # v1.3.0
with:
file: ./packages/arkor/coverage/cobertura-coverage.xml
language: TypeScript
label: code-coverage/arkor
fail-on-error: false
- name: Upload create-arkor coverage to GitHub
if: ${{ !cancelled() }}
uses: actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3 # v1.3.0
with:
file: ./packages/create-arkor/coverage/cobertura-coverage.xml
language: TypeScript
label: code-coverage/create-arkor
fail-on-error: false
- name: Upload cli-internal coverage to GitHub
if: ${{ !cancelled() }}
uses: actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3 # v1.3.0
with:
file: ./packages/cli-internal/coverage/cobertura-coverage.xml
language: TypeScript
label: code-coverage/cli-internal
fail-on-error: false
- name: Upload studio-app coverage to GitHub
if: ${{ !cancelled() }}
uses: actions/upload-code-coverage@abb5995db9e0199b0e2bb9dbd136fce4cb1ec4d3 # v1.3.0
with:
file: ./packages/studio-app/coverage/cobertura-coverage.xml
language: TypeScript
label: code-coverage/studio-app
fail-on-error: false
# e2e/cli intentionally does NOT upload a Cobertura report to GitHub.
# c8's cobertura reporter currently emits an empty 402-byte shell
# here: the istanbul cobertura writer doesn't pick up c8's
# sourcemap-remapped coverage of the spawned CLI children the way
# the lcov reporter does. The Codecov upload above stays on the
# lcov path, which has produced usable signal historically; once
# the c8 ↔ cobertura gap is closed (or we switch e2e/cli off c8),
# re-add this upload alongside the others.
# Codecov Test Analytics: uploads JUnit XML produced by the vitest
# `junit` reporter. Tracks pass/fail history per test, surfaces flaky
# tests, and shows failure trends on the Codecov dashboard. See
# https://docs.codecov.com/docs/test-analytics.
- name: Upload arkor test results
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
report_type: test_results
files: ./packages/arkor/coverage/junit.xml
flags: arkor
fail_ci_if_error: false
- name: Upload create-arkor test results
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
report_type: test_results
files: ./packages/create-arkor/coverage/junit.xml
flags: create-arkor
fail_ci_if_error: false
- name: Upload cli-internal test results
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
report_type: test_results
files: ./packages/cli-internal/coverage/junit.xml
flags: cli-internal
fail_ci_if_error: false
- name: Upload studio-app test results
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
report_type: test_results
files: ./packages/studio-app/coverage/junit.xml
flags: studio-app
fail_ci_if_error: false
- name: Upload e2e-cli test results
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
report_type: test_results
files: ./e2e/cli/coverage/junit.xml
flags: e2e-cli
fail_ci_if_error: false
# Studio E2E reports JUnit XML produced by the Playwright junit
# reporter (see e2e/studio/playwright.config.ts). No lcov: browser
# tests don't collect server-side V8 coverage, so we only feed
# Test Analytics for pass/fail trend tracking.
- name: Upload e2e-studio test results
if: ${{ !cancelled() }}
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
use_oidc: true
disable_search: true
report_type: test_results
files: ./e2e/studio/coverage/junit.xml
flags: e2e-studio
fail_ci_if_error: false