/** The GitHub Actions promotion step Lisa recognizes for CI-built artifacts. */
export const PROMOTION_ACTION = "actions/download-artifact";
const DOCKER_BUILD_PUSH_ACTION = "docker/build-push-action";
const GITHUB_RELEASE_ACTION = "softprops/action-gh-release";
const PYPI_PUBLISH_ACTION = "pypa/gh-action-pypi-publish";
const UPLOAD_ARTIFACT_ACTION = "actions/upload-artifact";
const ARTIFACT_INTEGRITY_COMMANDS = [
    /\bgh\s+attestation\s+verify\b/i,
    /\bcosign\s+verify\b/i,
    /\bslsa-verifier\b/i,
    /\bsha256sum\s+(?:--check|-c)\b/i,
    /\bshasum\s+-a\s+256\s+(?:--check|-c)\b/i,
];
/**
 * Normalize an action reference to its owner/repo component.
 * @param uses - Raw `uses:` value
 * @returns Lowercase action id without an `@ref`
 */
function actionId(uses) {
    return (uses.split("@")[0] ?? "").trim().toLowerCase();
}
/**
 * Whether a serialized `with:` block sets a boolean option to true.
 * @param inputs - Flattened step inputs
 * @param name - Input name to read
 * @returns True when the option is explicitly true
 */
function hasTrueInput(inputs, name) {
    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    return new RegExp(`(^|\\n)${escaped}:\\s*['"]?true['"]?(\\s|$)`, "i").test(inputs);
}
/**
 * Whether an action step publishes something externally.
 * @param step - The step to classify
 * @returns True when the action is a known publishing action
 */
export function isPublishAction(step) {
    const id = actionId(step.uses);
    if (id === DOCKER_BUILD_PUSH_ACTION) {
        return hasTrueInput(step.inputs, "push");
    }
    return id === PYPI_PUBLISH_ACTION || id === GITHUB_RELEASE_ACTION;
}
/**
 * Whether an action reference is the trusted artifact-promotion action.
 * @param uses - Raw action reference
 * @returns True when the action id is exactly actions/download-artifact
 */
export function isPromotionAction(uses) {
    return actionId(uses) === PROMOTION_ACTION;
}
/**
 * Whether an action reference uploads an artifact produced by CI.
 * @param uses - Raw action reference
 * @returns True when the action id is exactly actions/upload-artifact
 */
function isUploadAction(uses) {
    return actionId(uses) === UPLOAD_ARTIFACT_ACTION;
}
/**
 * Read a scalar step input from the parser's flattened `with:` text.
 * @param inputs - Flattened step inputs
 * @param name - Input name to read
 * @returns The input value, or null when absent
 */
function stepInput(inputs, name) {
    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const match = new RegExp(`(^|\\n)${escaped}:\\s*(.+)(\\n|$)`).exec(inputs);
    const value = match?.[2]?.trim();
    if (value === undefined || value === "") {
        return null;
    }
    return value.replace(/^['"]|['"]$/g, "");
}
/**
 * List artifact names uploaded by jobs whose outputs have already been validated.
 * @param validatingAncestorJobs - Validating jobs in the publishing job's `needs:` closure
 * @returns Uploaded artifact names
 */
function uploadedArtifactNames(validatingAncestorJobs) {
    return new Set(validatingAncestorJobs.flatMap(job => job.steps
        .filter(step => isUploadAction(step.uses))
        .map(step => stepInput(step.inputs, "name"))
        .filter((name) => name !== null)));
}
/**
 * Whether the job promotes an artifact produced by another job.
 * @param job - The publishing job
 * @param publishStep - The step that ships
 * @param ancestorJobs - Jobs in the publishing job's `needs:` closure
 * @returns True when a download-artifact step is present before publication
 */
export function promotesValidatedArtifact(job, publishStep, ancestorJobs = []) {
    const uploads = uploadedArtifactNames(ancestorJobs);
    return job.steps.slice(0, job.steps.indexOf(publishStep)).some(step => {
        if (!isPromotionAction(step.uses)) {
            return false;
        }
        const downloadName = stepInput(step.inputs, "name");
        return (downloadName === null || uploads.size === 0 || uploads.has(downloadName));
    });
}
/**
 * Whether a named artifact download disagrees with named ancestor uploads.
 * @param job - The publishing job
 * @param publishStep - The step that ships
 * @param ancestorJobs - Jobs in the publishing job's `needs:` closure
 * @returns True when the downloaded artifact name is not uploaded by any ancestor
 */
export function hasArtifactNameMismatch(job, publishStep, ancestorJobs) {
    const uploads = uploadedArtifactNames(ancestorJobs);
    return job.steps.slice(0, job.steps.indexOf(publishStep)).some(step => {
        if (!isPromotionAction(step.uses)) {
            return false;
        }
        const downloadName = stepInput(step.inputs, "name");
        return downloadName !== null && !uploads.has(downloadName);
    });
}
/**
 * Whether a named promotion verifies the downloaded artifact's identity before
 * publication. A matching artifact name proves a useful link, but a digest or
 * attestation check is the offline evidence that the bytes were not swapped.
 * @param job - The publishing job
 * @param publishStep - The step that ships
 * @param ancestorJobs - Jobs in the publishing job's `needs:` closure
 * @returns True when a named promotion lacks digest/attestation verification
 */
export function hasUnprovenArtifactIntegrity(job, publishStep, ancestorJobs) {
    const uploads = uploadedArtifactNames(ancestorJobs);
    if (uploads.size === 0) {
        return false;
    }
    const priorSteps = job.steps.slice(0, job.steps.indexOf(publishStep));
    const matchingNamedDownloadIndex = priorSteps.reduce((index, step, currentIndex) => {
        if (!isPromotionAction(step.uses)) {
            return index;
        }
        const downloadName = stepInput(step.inputs, "name");
        return downloadName !== null && uploads.has(downloadName)
            ? currentIndex
            : index;
    }, -1);
    return (matchingNamedDownloadIndex >= 0 &&
        !priorSteps
            .slice(matchingNamedDownloadIndex + 1)
            .some(step => ARTIFACT_INTEGRITY_COMMANDS.some(command => command.test(step.run))));
}
/**
 * The release job downloaded a named artifact different from anything a
 * validating ancestor uploaded, so the shipped bytes are not the tested bytes.
 * @param where - Evidence location label
 * @returns The violation outcome
 */
export function artifactNameMismatch(where) {
    return {
        kind: "violation",
        evidence: `${where} downloads a named artifact that no validating ancestor uploads, ` +
            "so the release path cannot prove the shipped artifact is the one CI tested",
    };
}
/**
 * A named artifact promotion exists, but no digest/attestation check proves the
 * downloaded bytes are the same bytes the validating job uploaded.
 * @param where - Evidence location label
 * @returns The unresolved outcome
 */
export function unresolvedArtifactIntegrity(where) {
    return {
        kind: "unresolved",
        reason: `${where} promotes a named artifact from a validating job, but no ` +
            "digest or attestation verification step before publication proves the " +
            "downloaded bytes are the same bytes CI validated",
    };
}
/**
 * A downloaded artifact without linked validation is unestablished, not clean.
 * @param where - Evidence location label
 * @returns The unresolved outcome
 */
export function unresolvedPromotedArtifact(where) {
    return {
        kind: "unresolved",
        reason: `${where} promotes an artifact via \`${PROMOTION_ACTION}\`, but no ` +
            "validating job precedes it in this workflow or resolved caller chain, so " +
            "the downloaded artifact cannot be tied to anything that was validated",
    };
}
//# sourceMappingURL=doctor-readiness-promoted-artifact.js.map