Files
obsidian-livesync/test/workflows/libraryPreviewInputs.test.mjs
T

186 lines
7.1 KiB
JavaScript

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\./);
});