mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
254 lines
8.1 KiB
JavaScript
254 lines
8.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { parse } from 'yaml';
|
|
|
|
const PUBLISH_ACTION_ALLOWLIST = Object.freeze([
|
|
'actions/checkout@v4',
|
|
'samuelmeuli/action-snapcraft@v3',
|
|
]);
|
|
const BUILD_ACTION_ALLOWLIST = Object.freeze([
|
|
'actions/cache/restore@v4',
|
|
'actions/cache/save@v4',
|
|
'actions/cache@v4',
|
|
'actions/checkout@v4',
|
|
'actions/download-artifact@v4',
|
|
'actions/setup-node@v4',
|
|
'actions/upload-artifact@v4',
|
|
'pnpm/action-setup@v4',
|
|
'softprops/action-gh-release@v2',
|
|
]);
|
|
const PUBLISH_JOB_ID = 'publish-snap';
|
|
const PUBLISH_JOB_CONDITION =
|
|
"${{ startsWith(github.event.release.tag_name, 'v') && github.event.release.draft == false }}";
|
|
const PUBLISH_STEP_NAME = 'Publish all public-release snaps to edge';
|
|
const PUBLISH_STEP_CONTRACT = Object.freeze({
|
|
name: PUBLISH_STEP_NAME,
|
|
shell: 'bash',
|
|
run: [
|
|
'set -euo pipefail',
|
|
'',
|
|
'node -e \\',
|
|
` "const fs=require('node:fs'); const selected=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); for (const asset of selected.snapAssets) console.log(asset.name);" \\`,
|
|
' "${RUNNER_TEMP}/selected-snap-release-assets.json" |',
|
|
' while IFS= read -r SNAP_NAME; do',
|
|
' SNAP_FILE="${RUNNER_TEMP}/snap-release-downloads/${SNAP_NAME}"',
|
|
' echo "Publishing public release asset: ${SNAP_NAME}"',
|
|
' # Candidate/stable promotion is manual after installed-Snap frame-copy and missing-runtime fallback smoke.',
|
|
' # GitHub Actions never promotes automatically.',
|
|
' snapcraft upload --release=edge "${SNAP_FILE}"',
|
|
' done',
|
|
'',
|
|
].join('\n'),
|
|
});
|
|
|
|
function stripShellComment(line) {
|
|
let quote = null;
|
|
for (let index = 0; index < line.length; index += 1) {
|
|
const character = line[index];
|
|
if (quote === "'") {
|
|
if (character === quote) {
|
|
quote = null;
|
|
}
|
|
continue;
|
|
}
|
|
if (character === '\\') {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if (quote === '"') {
|
|
if (character === quote) {
|
|
quote = null;
|
|
}
|
|
continue;
|
|
}
|
|
if (character === "'" || character === '"') {
|
|
quote = character;
|
|
continue;
|
|
}
|
|
if (
|
|
character === '#' &&
|
|
(index === 0 || /[\s;|&()]/.test(line[index - 1]))
|
|
) {
|
|
return line.slice(0, index);
|
|
}
|
|
}
|
|
return line;
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
}
|
|
|
|
function normalizeRunSource(runSource) {
|
|
return runSource
|
|
.split('\n')
|
|
.map(stripShellComment)
|
|
.join('\n')
|
|
.replace(/\\\r?\n[ \t]*/g, '');
|
|
}
|
|
|
|
function collectDefaultShell(container, containerName, explicitShells) {
|
|
if (!Object.hasOwn(container, 'defaults')) {
|
|
return;
|
|
}
|
|
assert.ok(
|
|
isRecord(container.defaults),
|
|
`${containerName} defaults must be a mapping`
|
|
);
|
|
if (!Object.hasOwn(container.defaults, 'run')) {
|
|
return;
|
|
}
|
|
assert.ok(
|
|
isRecord(container.defaults.run),
|
|
`${containerName} run defaults must be a mapping`
|
|
);
|
|
if (!Object.hasOwn(container.defaults.run, 'shell')) {
|
|
return;
|
|
}
|
|
assert.equal(
|
|
typeof container.defaults.run.shell,
|
|
'string',
|
|
`${containerName} default shell must resolve to a string`
|
|
);
|
|
explicitShells.push(container.defaults.run.shell);
|
|
}
|
|
|
|
function collectWorkflowPolicyInputs(workflowText) {
|
|
const workflow = parse(workflowText);
|
|
assert.ok(isRecord(workflow), 'workflow must be a YAML mapping');
|
|
assert.ok(isRecord(workflow.jobs), 'workflow jobs must be a YAML mapping');
|
|
|
|
const actions = [];
|
|
const explicitShells = [];
|
|
const jobActions = [];
|
|
const jobsWithoutSteps = [];
|
|
const runSources = [];
|
|
collectDefaultShell(workflow, 'workflow', explicitShells);
|
|
for (const [jobName, job] of Object.entries(workflow.jobs)) {
|
|
assert.ok(isRecord(job), `workflow job "${jobName}" must be a mapping`);
|
|
collectDefaultShell(job, `workflow job "${jobName}"`, explicitShells);
|
|
if (Object.hasOwn(job, 'uses')) {
|
|
assert.equal(
|
|
typeof job.uses,
|
|
'string',
|
|
'reusable workflow identifiers must resolve to strings'
|
|
);
|
|
jobActions.push(job.uses);
|
|
}
|
|
if (!Array.isArray(job.steps)) {
|
|
jobsWithoutSteps.push(jobName);
|
|
continue;
|
|
}
|
|
for (const step of job.steps) {
|
|
assert.ok(isRecord(step), 'workflow steps must be mappings');
|
|
if (Object.hasOwn(step, 'uses')) {
|
|
assert.equal(
|
|
typeof step.uses,
|
|
'string',
|
|
'workflow action identifiers must resolve to strings'
|
|
);
|
|
actions.push(step.uses);
|
|
}
|
|
if (Object.hasOwn(step, 'run')) {
|
|
assert.equal(
|
|
typeof step.run,
|
|
'string',
|
|
'workflow run commands must resolve to strings'
|
|
);
|
|
runSources.push(normalizeRunSource(step.run));
|
|
}
|
|
if (Object.hasOwn(step, 'shell')) {
|
|
assert.equal(
|
|
typeof step.shell,
|
|
'string',
|
|
'workflow step shells must resolve to strings'
|
|
);
|
|
explicitShells.push(step.shell);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
actions,
|
|
commandSource: runSources.join('\n'),
|
|
explicitShells,
|
|
jobActions,
|
|
jobsWithoutSteps,
|
|
workflow,
|
|
};
|
|
}
|
|
|
|
function assertWorkflowExecutionShape({
|
|
explicitShells,
|
|
jobActions,
|
|
jobsWithoutSteps,
|
|
}) {
|
|
assert.deepEqual(
|
|
jobActions,
|
|
[],
|
|
'job-level reusable workflow delegation is not allowed'
|
|
);
|
|
assert.deepEqual(
|
|
jobsWithoutSteps,
|
|
[],
|
|
'every workflow job must define a concrete steps sequence'
|
|
);
|
|
assert.deepEqual(
|
|
explicitShells.filter((shell) => shell !== 'bash'),
|
|
[],
|
|
'every explicit workflow shell must be exactly "bash"'
|
|
);
|
|
}
|
|
|
|
function literalSnapcraftTokens(commandSource) {
|
|
return commandSource.match(/\bsnapcraft\b/g) ?? [];
|
|
}
|
|
|
|
export function assertPublishSnapWorkflowPolicy(workflowText) {
|
|
const policyInputs = collectWorkflowPolicyInputs(workflowText);
|
|
const { actions, commandSource, workflow } = policyInputs;
|
|
assertWorkflowExecutionShape(policyInputs);
|
|
assert.deepEqual(
|
|
workflow.on,
|
|
{ release: { types: ['published'] } },
|
|
'the publish workflow must retain its exact release trigger'
|
|
);
|
|
assert.deepEqual(
|
|
[...actions].sort(),
|
|
[...PUBLISH_ACTION_ALLOWLIST].sort(),
|
|
'the publish workflow must use exactly the allowlisted actions'
|
|
);
|
|
const publishJob = workflow.jobs[PUBLISH_JOB_ID];
|
|
assert.ok(isRecord(publishJob), 'the canonical publish job must exist');
|
|
assert.equal(
|
|
publishJob.if,
|
|
PUBLISH_JOB_CONDITION,
|
|
'the publish job must retain its exact release condition'
|
|
);
|
|
assert.deepEqual(
|
|
publishJob.steps.filter((step) => step.name === PUBLISH_STEP_NAME),
|
|
[PUBLISH_STEP_CONTRACT],
|
|
'the publish workflow must retain the exact reviewed publication step'
|
|
);
|
|
assert.equal(
|
|
literalSnapcraftTokens(commandSource).length,
|
|
1,
|
|
'the publish workflow must contain exactly one literal Snapcraft command'
|
|
);
|
|
}
|
|
|
|
export function assertBuildSnapWorkflowPolicy(workflowText) {
|
|
const policyInputs = collectWorkflowPolicyInputs(workflowText);
|
|
const { actions, commandSource } = policyInputs;
|
|
assertWorkflowExecutionShape(policyInputs);
|
|
assert.deepEqual(
|
|
actions.filter((action) => !BUILD_ACTION_ALLOWLIST.includes(action)),
|
|
[],
|
|
'the build workflow must use only allowlisted actions'
|
|
);
|
|
assert.equal(
|
|
literalSnapcraftTokens(commandSource).length,
|
|
0,
|
|
'the build workflow must not contain a literal Snapcraft token'
|
|
);
|
|
}
|