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
26 changes: 17 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,20 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git diff --name-only --diff-filter=ACMRT "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/changed-files"
# Include deletions and both rename paths; preserve unusual filenames.
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/changed-files"
docs_only=true
full=false
while IFS= read -r file; do
while IFS= read -r -d '' file; do
case "$file" in
.agents/docs/*.md|.agents/docs/*.markdown) continue ;;
esac
case "$file" in
*.md|*.markdown) ;;
*) docs_only=false ;;
esac
case "$file" in
AGENTS.md|.agents/*|package.json|package-lock.json|cordisx-package.json|runtime-manifest.json|tsconfig.json|vite.config.mjs|.github/*|scripts/*|entities/*)
AGENTS.md|.agents/*|package.json|package-lock.json|cordisx-package.json|cordisx-agent-tools.json|runtime-manifest.json|tsconfig*.json|vite.config.*|vitest.config.*|eslint.config.*|stylelint.config.*|dprint.json|.lintstagedrc.*|.github/*|scripts/*|entities/*)
full=true
;;
esac
Expand All @@ -55,9 +59,11 @@ jobs:
echo "- docs-only: \`$docs_only\`"
echo "- full gate: \`$full\`"
} >> "$GITHUB_STEP_SUMMARY"
- name: Check scope regression cases
run: node --test scripts/ci-scope.test.mjs

check:
if: always() && (github.event_name == 'push' || needs.scope.outputs.full == 'true')
if: ${{ !cancelled() && (github.event_name == 'push' || needs.scope.result != 'success' || needs.scope.outputs.full == 'true') }}
needs: scope
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down Expand Up @@ -94,7 +100,7 @@ jobs:
cache=$([[ "$CACHE_HIT" == 'true' ]] && echo hit || echo miss)
evidence="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
jq -n --arg schema 'cordisx/ci-job-result/v1' --arg repo "$GITHUB_REPOSITORY" \
--arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg gateVersion 'chatroom-check-v2' \
--arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg gateVersion 'chatroom-check-v3' \
--arg classification "$CLASSIFICATION" --arg job "$GITHUB_JOB" --arg startedAt "$STARTED_AT" \
--arg completedAt "$completed_at" --arg cache "$cache" --arg result "$RESULT" \
--arg environment 'github-hosted/ubuntu-latest/node22' \
Expand Down Expand Up @@ -128,7 +134,8 @@ jobs:
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts --allow-git=all
# The Git Host dependency needs prepare to produce cordisx/contracts and JSX exports.
- run: npm ci --allow-git=all
- name: Typecheck the plugin
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
Expand All @@ -151,7 +158,7 @@ jobs:
cache=$([[ "$CACHE_HIT" == 'true' ]] && echo hit || echo miss)
evidence="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
jq -n --arg schema 'cordisx/ci-job-result/v1' --arg repo "$GITHUB_REPOSITORY" \
--arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg gateVersion 'chatroom-check-v2' \
--arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg gateVersion 'chatroom-check-v3' \
--arg classification 'changed-typecheck' --arg job "$GITHUB_JOB" --arg startedAt "$STARTED_AT" \
--arg completedAt "$completed_at" --arg cache "$cache" --arg result "$RESULT" \
--arg environment 'github-hosted/ubuntu-latest/node22' \
Expand Down Expand Up @@ -185,7 +192,8 @@ jobs:
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts --allow-git=all
# The Git Host dependency needs prepare to produce cordisx/contracts and JSX exports.
- run: npm ci --allow-git=all
- name: Build, then run tests against the build
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
Expand All @@ -209,7 +217,7 @@ jobs:
cache=$([[ "$CACHE_HIT" == 'true' ]] && echo hit || echo miss)
evidence="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
jq -n --arg schema 'cordisx/ci-job-result/v1' --arg repo "$GITHUB_REPOSITORY" \
--arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg gateVersion 'chatroom-check-v2' \
--arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg gateVersion 'chatroom-check-v3' \
--arg classification 'build-test' --arg job "$GITHUB_JOB" --arg startedAt "$STARTED_AT" \
--arg completedAt "$completed_at" --arg cache "$cache" --arg result "$RESULT" \
--arg environment 'github-hosted/ubuntu-latest/node22' \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/shared-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts --allow-git=all
- name: Install the exact shared audit provider
run: |
Expand Down
94 changes: 94 additions & 0 deletions scripts/ci-scope.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { test } from 'node:test';

const workflow = readFileSync(new URL('../.github/workflows/ci.yml', import.meta.url), 'utf8');
const block = workflow.split(' run: |\n')[1].split(' - name: Check scope regression cases')[0];
const script = block.split('\n').map(line => line.replace(/^ /, '')).join('\n');
const sensitive = 'entities/example.ts';

function classify(mode, filename = 'README.md') {
const cwd = mkdtempSync(path.join(tmpdir(), 'ci-scope-'));
const git = (...args) => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
function write(file, content) {
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true });
writeFileSync(path.join(cwd, file), content);
}
try {
git('init', '--quiet');
git('config', 'user.name', 'CI scope test');
git('config', 'user.email', 'ci@example.invalid');
write(sensitive, 'export {}\n');
write('README.md', '# Base\n');
git('add', '.');
git('commit', '--quiet', '-m', 'base');
const base = git('rev-parse', 'HEAD');
if (mode === 'delete') git('rm', '--quiet', '--', sensitive);
if (mode === 'rename') git('mv', '--', sensitive, 'moved.md');
if (mode !== 'empty') {
write(filename, '# Changed\n');
git('add', '.');
git('commit', '--quiet', '-m', 'change');
}
const output = path.join(cwd, 'output');
execFileSync('bash', ['-e', '-o', 'pipefail', '-c', script], {
cwd,
env: {
...process.env,
BASE_SHA: base,
HEAD_SHA: git('rev-parse', 'HEAD'),
RUNNER_TEMP: cwd,
GITHUB_OUTPUT: output,
GITHUB_STEP_SUMMARY: path.join(cwd, 'summary'),
},
});
return Object.fromEntries(readFileSync(output, 'utf8').trim().split('\n').map(line => line.split('=')));
} finally {
rmSync(cwd, { recursive: true, force: true });
}
}

for (const mode of ['delete', 'rename', 'empty']) {
test(`${mode} cannot downgrade a sensitive diff to documentation`, () => {
assert.equal(classify(mode).full, 'true');
});
}

test('ordinary README remains lightweight', () => {
assert.equal(classify('edit').full, 'false');
});

test('guide classification respects owner semantics and unusual filenames', () => {
assert.equal(classify('edit', '.agents/docs/two words\nexample.md').full, 'false');
});

test('maintenance rules retain complete validation', () => {
assert.equal(classify('edit', '.agents/rules/README.md').full, 'true');
});

test('scope failure cannot skip the complete gate', () => {
assert.ok(workflow.includes("needs.scope.result != 'success'"));
});

for (const file of ['cordisx-agent-tools.json', 'stylelint.config.mjs', 'tsconfig.extra.json', 'vite.config.ts']) {
test(`shared build/quality and package configuration retains full checks: ${file}`, () => {
assert.equal(classify('edit', file).full, 'true');
});
}

test('affected compilation jobs prepare the Git Host dependency', () => {
for (const name of ['typecheck', 'build-test']) {
const job = workflow.split(`\n ${name}:\n`)[1].split(/\n [a-z-]+:\n/)[0];
assert.ok(job.includes('run: npm ci --allow-git=all'));
assert.ok(!job.includes('npm ci --ignore-scripts'));
}
});

test('complete jobs honor cancellation so superseded PR runs release their slot', () => {
const source = readFileSync(new URL('../.github/workflows/ci.yml', import.meta.url), 'utf8');
assert.ok(source.includes(' if: ${{ !cancelled() && '));
assert.ok(!source.includes(' if: always() && '));
});
Loading