import type { CompiledJob, CompiledStep, Segment } from "./compile"; export const MARKER_PREFIX = "::stig::"; const SHELL_COMMANDS: Record = { bash: "bash --noprofile --norc -eo pipefail", sh: "sh -e", python: "python3", python3: "python3", node: "node", bun: "bun", }; const KILL_GRACE_SECONDS = 15; function doubleQuoted(segments: Segment[]): string { const body = segments .map((segment) => segment.kind === "secret" ? `\${STIG_SECRET_${segment.name}}` : segment.value.replace(/(["\\$`])/g, "\\$1"), ) .join(""); return `"${body}"`; } function singleQuoted(value: string): string { return `'${value.replace(/'/g, "'\\''")}'`; } function exports(env: Array<[string, Segment[]]>): string[] { return env.map(([name, segments]) => `export ${name}=${doubleQuoted(segments)}`); } function heredoc(path: string, content: string): string[] { let delimiter = `STIG_EOF_${crypto.randomUUID().replace(/-/g, "")}`; while (content.includes(delimiter)) delimiter += "_"; return [`cat > ${path} <<'${delimiter}'`, content, delimiter]; } function stepBody(step: CompiledStep): string[] { const lines: string[] = []; const directory = step.workingDirectory ? `cd ${singleQuoted(step.workingDirectory)} || exit 1` : ":"; if (step.kind === "noop") { lines.push(`echo ${singleQuoted(`${step.name}: provided by the runner`)}`, "__stig_code=0"); return lines; } if (step.kind === "upload-artifact" && step.artifact) { const archive = `"$__stig_tmp/artifacts/${step.artifact.name}.tgz"`; lines.push( `( ${directory}; tar czf ${archive} -- ${step.artifact.path} )`, "__stig_code=$?", `if [ $__stig_code -eq 0 ]; then __stig_mark artifact ${step.artifact.name} ${archive}; fi`, ); return lines; } const command = SHELL_COMMANDS[step.shell] ?? SHELL_COMMANDS.bash; const timeout = step.timeoutMinutes ? `timeout --kill-after=${KILL_GRACE_SECONDS} ${Math.ceil(step.timeoutMinutes * 60)} ` : ""; lines.push("(", ` ${directory}`, ...exports(step.env).map((line) => ` ${line}`)); lines.push(` exec ${timeout}${command} "$__stig_tmp/steps/${step.index}"`, ")", "__stig_code=$?"); return lines; } /** Generates the self-contained bash program that runs a job and reports progress through log markers. */ export function buildJobScript(job: CompiledJob): string { const lines = [ "#!/usr/bin/env bash", `__stig_mark() { printf '\\n${MARKER_PREFIX}%s\\n' "$*"; }`, "export CI=true STIG_ACTIONS=true GITHUB_ACTIONS=true", ...exports(job.env), "__stig_failed=0", '__stig_dir="${STIG_WORKSPACE:-/workspace}"', '__stig_tmp="$__stig_dir/.stig"', 'rm -rf "$__stig_tmp" "$__stig_dir/src" && mkdir -p "$__stig_tmp/steps" "$__stig_tmp/artifacts"', ]; for (const step of job.steps) { if (step.kind === "run") lines.push(...heredoc(`"$__stig_tmp/steps/${step.index}"`, step.script)); } lines.push( '__stig_should_run() { if [ $__stig_failed -eq 0 ]; then [ "$1" = 1 ]; else [ "$2" = 1 ]; fi; }', "__stig_mark setup-start", 'echo "Checking out $STIG_REPOSITORY@$STIG_SHA"', 'git clone --quiet --no-checkout "$STIG_CLONE_URL" "$__stig_dir/src" && git -C "$__stig_dir/src" checkout --quiet --detach "$STIG_SHA"', "__stig_code=$?", "__stig_mark setup-end $__stig_code", "if [ $__stig_code -ne 0 ]; then __stig_mark job-end 1; exit 1; fi", 'cd "$__stig_dir/src" || exit 1', 'export GITHUB_WORKSPACE="$PWD" STIG_WORKSPACE_SRC="$PWD"', ); for (const step of job.steps) { lines.push( `if __stig_should_run ${step.runIfOk ? 1 : 0} ${step.runIfFailed ? 1 : 0}; then`, ` __stig_mark step-start ${step.index}`, ...stepBody(step).map((line) => ` ${line}`), ` __stig_mark step-end ${step.index} $__stig_code`, ` if [ $__stig_code -ne 0 ] && [ ${step.continueOnError ? 1 : 0} = 0 ]; then __stig_failed=1; fi`, "else", ` __stig_mark step-skip ${step.index}`, "fi", ); } lines.push("__stig_mark job-end $__stig_failed", "exit $__stig_failed", ""); return lines.join("\n"); } export function jobTimeoutCommand(job: CompiledJob, scriptPath: string): string[] { return ["timeout", `--kill-after=${KILL_GRACE_SECONDS}`, String(job.timeoutMinutes * 60), "bash", scriptPath]; }