Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ jobs:
- 'scripts/**'
- 'migrations/**'
- '.github/workflows/**'
# .github/actions/** (composite actions, e.g. setup-workspace) is workflow-adjacent
# infrastructure the same way .github/workflows/** above is -- without this, a PR editing
# ONLY a composite action file (touching nothing else backend matches) would skip both
# "Lint workflows" and "Lint composite actions" below entirely, with zero CI signal on a
# broken edit until the next push-triggered full run.
- '.github/actions/**'
- 'wrangler.jsonc'
- 'worker-configuration.d.ts'
- '.loopover.yml'
Expand Down Expand Up @@ -222,6 +228,17 @@ jobs:
- name: Lint workflows
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
run: npm run actionlint
# actionlint (above) does not cover .github/actions/**/action.yml at all -- confirmed a genuine,
# long-standing upstream limitation (rhysd/actionlint#46 and #401, open since 2021), not something
# fixable by a different invocation: even the raw actionlint binary, run directly with no wrapper,
# treats any file it's given as a workflow and errors on runs/inputs/outputs as unexpected
# top-level keys. This validates composite action files against GitHub's own official
# action-metadata JSON Schema instead -- real structural validation, not the full expression-
# context linting actionlint does for workflows, which genuinely doesn't exist anywhere for
# action.yml files.
- name: Lint composite actions
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
run: npm run lint:composite-actions
- name: Check migrations
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
run: npm run db:migrations:check
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mcp-release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
- name: Publish any package whose committed version isn't live on npm yet
env:
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mcp-release-watch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Check MCP release status
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mcp-ui-version-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

# No npm ci -- the script only imports Node built-ins.
- name: Sync the known-latest constant
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/orb-beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Check whether an ORB beta is due
id: report
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/orb-stable-release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Check whether a stable ORB release is due
id: report
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-engine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Resolve release version
id: version
Expand Down Expand Up @@ -160,7 +160,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org

- name: Create or verify release tag
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish-mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

# workflow_dispatch has no tag ref to derive a version from (unlike the old push:tags: trigger),
# so the dispatched commit's package.json is now the single source of truth for VERSION/TAG. Pure
Expand Down Expand Up @@ -177,7 +177,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org

- name: Create or verify release tag
Expand Down Expand Up @@ -244,7 +244,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
- name: Sync and open a PR if the known-latest copy is now stale
env:
# A real collaborator token, not github.token -- mirrors mcp-release-please.yml's own
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-miner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Resolve release version
id: version
Expand Down Expand Up @@ -158,7 +158,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org

- name: Create or verify release tag
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-ui-kit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Resolve release version
id: version
Expand Down Expand Up @@ -159,7 +159,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
registry-url: https://registry.npmjs.org

- name: Create or verify release tag
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-selfhost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ jobs:
# Release jobs receive publishing/Sentry credentials, so avoid shared dependency caches here.
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24.18.0"
node-version-file: .nvmrc

- name: Install deps
run: npm ci --ignore-scripts
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/selfhost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ jobs:

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24.18.0"
node-version-file: .nvmrc
cache: "npm"

- name: Install deps
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ui-preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
if: steps.cfg.outputs.ready == 'true'
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc

- name: Install trusted Wrangler
if: steps.cfg.outputs.ready == 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/upstream-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: 24.18.0
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"db:migrations:check": "tsx scripts/check-migrations.mjs",
"db:schema-drift:check": "tsx scripts/check-schema-drift.mjs",
"actionlint": "node scripts/actionlint.mjs",
"lint:composite-actions": "node scripts/lint-composite-actions.mjs",
"ui:dev": "npm run ui:preview",
"extension:build": "node scripts/build-extension.mjs",
"extension:lint": "npm --workspace @loopover/extension run lint",
Expand Down Expand Up @@ -99,7 +100,7 @@
"test:smoke:observability:metrics": "node scripts/smoke-observability-metrics.mjs",
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
"test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run test --workspace @loopover/engine && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run test:miner-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run docs:drift-check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run release-manifest:sync:check && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci",
"test:watch": "vitest",
Expand Down Expand Up @@ -139,6 +140,7 @@
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.5",
"@vitest/coverage-v8": "^4.1.9",
"ajv": "^8.20.0",
"drizzle-kit": "^0.31.10",
"gifenc": "^1.0.3",
"git-cliff": "^2.13.1",
Expand Down
86 changes: 86 additions & 0 deletions scripts/lint-composite-actions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env node
// Validates every .github/actions/*/action.yml against GitHub's official action-metadata JSON Schema,
// plus one composite-action-specific check the schema can't express: every `run:` step needs an
// explicit `shell:` (unlike a top-level workflow job, which defaults to bash on a Linux runner -- a
// missing one in a composite action is a silent hard failure at actual run time, not a parse-time
// error).
//
// actionlint (this repo's usual workflow linter, scripts/actionlint.mjs) does NOT support action.yml
// files at all -- confirmed this is a genuine, long-standing upstream limitation
// (github.com/rhysd/actionlint/issues/46 and /issues/401, open since 2021), not a configuration gap on
// this repo's side: even the raw actionlint binary, invoked directly with no wrapper, treats any file
// it's given as a workflow and errors on `runs`/`inputs`/`outputs` as unexpected top-level keys. This
// script is the closest available substitute -- real structural/schema validation, not the full
// expression-context linting actionlint does for workflows, which genuinely doesn't exist anywhere for
// action.yml files.
//
// Schema vendored locally (scripts/schemas/github-action.schema.json, from
// https://json.schemastore.org/github-action.json) rather than fetched live, so this check doesn't
// depend on network access in CI -- consistent with how the rest of this repo's drift/lint checks work
// offline against committed state.

import Ajv from "ajv";
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { parse } from "yaml";

const ACTIONS_DIR = ".github/actions";
const SCHEMA_PATH = new URL("./schemas/github-action.schema.json", import.meta.url);

const schema = JSON.parse(readFileSync(SCHEMA_PATH, "utf8"));
const ajv = new Ajv({ allErrors: true, strict: false });
const validateSchema = ajv.compile(schema);

function findActionFiles() {
const results = [];
for (const entry of readdirSync(ACTIONS_DIR, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
for (const name of ["action.yml", "action.yaml"]) {
const candidate = join(ACTIONS_DIR, entry.name, name);
try {
readFileSync(candidate);
results.push(candidate);
break; // a directory has one action file, not both
} catch {
// try the other extension
}
}
}
return results;
}

const actionFiles = findActionFiles();
if (actionFiles.length === 0) {
console.log(`No composite action files found under ${ACTIONS_DIR}/ -- nothing to validate.`);
process.exit(0);
}

let hasErrors = false;

for (const path of actionFiles) {
const doc = parse(readFileSync(path, "utf8"));

if (!validateSchema(doc)) {
hasErrors = true;
console.error(`${path}: schema violations:`);
for (const err of validateSchema.errors ?? []) {
console.error(` ${err.instancePath || "(root)"} ${err.message}`);
}
}

if (doc?.runs?.using === "composite") {
for (const [index, step] of (doc.runs.steps ?? []).entries()) {
if (step.run !== undefined && step.shell === undefined) {
hasErrors = true;
console.error(
`${path}: runs.steps[${index}] ("${step.name ?? "unnamed"}") has a run: but no shell: -- required for composite action steps, unlike a top-level workflow job which defaults to bash`,
);
}
}
}
}

if (hasErrors) {
process.exit(1);
}
console.log(`Validated ${actionFiles.length} composite action file(s) against the GitHub action-metadata schema: all clean.`);
Loading
Loading