mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-26 11:03:21 +00:00
Add reusable consumer CI for library commit previews
This commit is contained in:
1 parent
ce0524d49f
commit
f475ea1c63
4 files changed
+342
No files matched your search
@@ -0,0 +1,185 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { appendFileSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url));
|
||||
const workflowPath = join(repositoryRoot, '.github/workflows/library-preview.yml');
|
||||
const workflow = readFileSync(workflowPath, 'utf8');
|
||||
const fullSha = 'a'.repeat(40);
|
||||
|
||||
function readValidationProgram() {
|
||||
const lines = workflow.split(/\r?\n/);
|
||||
const stepIndex = lines.findIndex((line) => line.trim() === '- name: Validate preview SHAs and resolve the consumer commit');
|
||||
assert.notEqual(stepIndex, -1, 'workflow must contain the SHA validation step');
|
||||
|
||||
const runIndex = lines.findIndex((line, index) => index > stepIndex && line === ' run: |');
|
||||
assert.notEqual(runIndex, -1, 'SHA validation step must use a literal run block');
|
||||
|
||||
const scriptLines = [];
|
||||
for (let index = runIndex + 1; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (line.trim() === '') {
|
||||
scriptLines.push('');
|
||||
continue;
|
||||
}
|
||||
if (!line.startsWith(' ')) break;
|
||||
scriptLines.push(line.slice(10));
|
||||
}
|
||||
|
||||
const shellScript = scriptLines.join('\n').trimEnd();
|
||||
const heredoc = shellScript.match(/^node --input-type=module <<'NODE'\n([\s\S]*)\nNODE$/);
|
||||
assert.ok(heredoc, 'SHA validation must pass its literal Node program through a quoted heredoc');
|
||||
return heredoc[1];
|
||||
}
|
||||
|
||||
const validationProgram = readValidationProgram();
|
||||
|
||||
function runValidation(overrides = {}) {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'library-preview-inputs-'));
|
||||
const outputPath = join(directory, 'output');
|
||||
const summaryPath = join(directory, 'summary');
|
||||
const env = {
|
||||
...process.env,
|
||||
INPUT_COMMONLIB_SHA: '',
|
||||
INPUT_KIT_SHA: '',
|
||||
INPUT_CONSUMER_SHA: '',
|
||||
CALLER_REPOSITORY: 'vrtmrz/obsidian-livesync',
|
||||
CALLER_SHA: fullSha,
|
||||
GITHUB_OUTPUT: outputPath,
|
||||
GITHUB_STEP_SUMMARY: summaryPath,
|
||||
...overrides,
|
||||
};
|
||||
appendFileSync(outputPath, '');
|
||||
appendFileSync(summaryPath, '');
|
||||
|
||||
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', validationProgram], {
|
||||
cwd: repositoryRoot,
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const outputs = readFileSync(outputPath, 'utf8')
|
||||
.trim()
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => line.split(/=(.*)/s).slice(0, 2));
|
||||
const summary = readFileSync(summaryPath, 'utf8');
|
||||
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
return {
|
||||
...result,
|
||||
outputs: Object.fromEntries(outputs),
|
||||
summary,
|
||||
};
|
||||
}
|
||||
|
||||
test('blank library SHAs resolve the caller commit for the LiveSync repository', () => {
|
||||
const result = runValidation();
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.outputs, {
|
||||
commonlib_sha: '',
|
||||
kit_sha: '',
|
||||
consumer_sha: fullSha,
|
||||
});
|
||||
assert.match(result.summary, new RegExp(fullSha));
|
||||
});
|
||||
|
||||
test('a reusable workflow caller from another repository must pass consumer_sha', () => {
|
||||
const result = runValidation({ CALLER_REPOSITORY: 'vrtmrz/livesync-commonlib' });
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, /consumer_sha is required/);
|
||||
});
|
||||
|
||||
test('an explicit consumer SHA is accepted for an external caller', () => {
|
||||
const consumerSha = 'b'.repeat(40);
|
||||
const result = runValidation({
|
||||
CALLER_REPOSITORY: 'vrtmrz/livesync-commonlib',
|
||||
INPUT_COMMONLIB_SHA: fullSha,
|
||||
INPUT_KIT_SHA: 'c'.repeat(40),
|
||||
INPUT_CONSUMER_SHA: consumerSha,
|
||||
});
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.outputs, {
|
||||
commonlib_sha: fullSha,
|
||||
kit_sha: 'c'.repeat(40),
|
||||
consumer_sha: consumerSha,
|
||||
});
|
||||
});
|
||||
|
||||
test('a supplied consumer SHA takes precedence over the caller commit', () => {
|
||||
const consumerSha = 'b'.repeat(40);
|
||||
const result = runValidation({ INPUT_CONSUMER_SHA: consumerSha });
|
||||
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.outputs.consumer_sha, consumerSha);
|
||||
});
|
||||
|
||||
test('all supplied SHAs reject anything other than a full lowercase hexadecimal SHA', async (t) => {
|
||||
const invalidValues = ['main', 'ABCDEF0123456789abcdef0123456789abcdef01', 'a'.repeat(39), `${fullSha}\n`];
|
||||
|
||||
for (const [inputName, displayName] of [
|
||||
['INPUT_COMMONLIB_SHA', 'commonlib_sha'],
|
||||
['INPUT_KIT_SHA', 'kit_sha'],
|
||||
['INPUT_CONSUMER_SHA', 'consumer_sha'],
|
||||
]) {
|
||||
await t.test(displayName, async (inputTest) => {
|
||||
for (const invalidValue of invalidValues) {
|
||||
await inputTest.test(JSON.stringify(invalidValue), () => {
|
||||
const result = runValidation({ [inputName]: invalidValue });
|
||||
|
||||
assert.notEqual(result.status, 0);
|
||||
assert.match(result.stderr, new RegExp(`${inputName} must be blank or a full 40-character lowercase SHA`));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('workflow exposes the specified inputs, read-only permissions, and independently pinned actions', () => {
|
||||
assert.match(workflow, /^ workflow_call:\n inputs:/m);
|
||||
assert.match(workflow, /^ workflow_dispatch:\n inputs:/m);
|
||||
for (const input of ['commonlib_sha', 'kit_sha', 'consumer_sha']) {
|
||||
assert.match(workflow, new RegExp(`^ ${input}:`, 'm'));
|
||||
}
|
||||
assert.match(workflow, /^permissions:\n actions: read\n contents: read$/m);
|
||||
assert.doesNotMatch(workflow, /^\s+(actions|contents): write$/m);
|
||||
assert.doesNotMatch(workflow, /pull_request_target|secrets:\s*inherit/);
|
||||
|
||||
for (const actionRef of [
|
||||
'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0',
|
||||
'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e',
|
||||
'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed',
|
||||
]) {
|
||||
assert.ok(workflow.includes(actionRef), `workflow must pin ${actionRef}`);
|
||||
}
|
||||
assert.match(workflow, /^ uses: vrtmrz\/fancy-kit\/\.github\/actions\/install-library-preview@[0-9a-f]{40}(?:\s+#.*)?$/m);
|
||||
assert.match(workflow, /repository: vrtmrz\/obsidian-livesync\n\s+ref: \$\{\{ steps\.inputs\.outputs\.consumer_sha \}\}/);
|
||||
assert.match(workflow, /commonlib-sha: \$\{\{ steps\.inputs\.outputs\.commonlib_sha \}\}/);
|
||||
assert.match(workflow, /kit-sha: \$\{\{ steps\.inputs\.outputs\.kit_sha \}\}/);
|
||||
assert.match(workflow, /github-token: \$\{\{ github\.token \}\}/);
|
||||
});
|
||||
|
||||
test('consumer workflow runs all required checks and keeps input values out of shell source', () => {
|
||||
for (const command of [
|
||||
'npm ci',
|
||||
'npm run tsc-check',
|
||||
'npm run test:unit -- --maxWorkers=1',
|
||||
'npm run build',
|
||||
'npm run build --workspace self-hosted-livesync-cli',
|
||||
'npm run build --workspace livesync-webapp',
|
||||
'npm run build --workspace webpeer',
|
||||
'deno task --cwd src/apps/cli/testdeno test:setup-put-cat',
|
||||
]) {
|
||||
assert.ok(workflow.includes(command), `workflow must run ${command}`);
|
||||
}
|
||||
assert.match(workflow, /INPUT_COMMONLIB_SHA: \$\{\{ inputs\.commonlib_sha \}\}/);
|
||||
assert.match(workflow, /INPUT_KIT_SHA: \$\{\{ inputs\.kit_sha \}\}/);
|
||||
assert.match(workflow, /INPUT_CONSUMER_SHA: \$\{\{ inputs\.consumer_sha \}\}/);
|
||||
assert.doesNotMatch(validationProgram, /\$\{\{\s*inputs\./);
|
||||
});
|
||||
Reference in new issue
Block a user