/**
 * Offline GitHub Actions workflow parser for the readiness producers
 * (PRD #1739, #1896).
 *
 * The delivery/authority readiness questions — does the thing that ships equal
 * the thing that was validated, and does the shipping credential carry only the
 * authority it needs — are answered from what the repository's CI *declares*,
 * not from what a live API reports. Parsing is therefore deliberately offline by
 * construction: nothing in this module reaches the network, so `lisa doctor
 * --offline --readiness` needs no flag threading to stay honest.
 *
 * Reading is best-effort by design. A workflow file that cannot be parsed is
 * skipped rather than aborting the readiness run, because one malformed YAML
 * file must not turn the whole report into an error the operator cannot act on.
 * @module cli/doctor-readiness-workflows
 */
import { readdir, readFile } from "node:fs/promises";
import * as path from "node:path";
import { isJsonObject } from "../sync/json-path.js";
import { loadYaml } from "../utils/yaml.js";
/** The workflows directory every GitHub repository declares its CI in. */
const WORKFLOWS_DIR = path.join(".github", "workflows");
/**
 * Normalize a YAML scalar-or-list into a list of trimmed strings.
 * @param value - Candidate YAML value
 * @returns Normalized string list (empty when unreadable)
 */
function asStringList(value) {
    if (typeof value === "string") {
        return value.trim() === "" ? [] : [value.trim()];
    }
    if (Array.isArray(value)) {
        return value
            .filter((entry) => typeof entry === "string")
            .map(entry => entry.trim())
            .filter(entry => entry !== "");
    }
    return [];
}
/**
 * Normalize a `permissions`/`secrets` block, which GitHub allows as either a
 * map or a bare scalar (`write-all`, `read-all`, `inherit`).
 * @param value - Candidate YAML value
 * @returns The map, the scalar string, or null when absent
 */
function asBlock(value) {
    if (typeof value === "string") {
        return value.trim();
    }
    return isJsonObject(value) ? value : null;
}
/**
 * Normalize a job's `environment`, which may be a scalar name, a list, or a
 * `{ name, url }` map.
 * @param value - Candidate YAML value
 * @returns Declared environment names
 */
function asEnvironments(value) {
    if (isJsonObject(value)) {
        return typeof value.name === "string" ? [value.name.trim()] : [];
    }
    return asStringList(value);
}
/**
 * Normalize the `on:` block, which GitHub accepts as a scalar (`on: push`), a
 * list (`on: [push, workflow_dispatch]`), or a map with per-event filters.
 * @param value - Candidate YAML `on` value
 * @returns The normalized trigger (all fields empty when absent or unreadable)
 */
function parseTrigger(value) {
    if (!isJsonObject(value)) {
        return { events: asStringList(value), pushBranches: [], pushTags: [] };
    }
    const push = value.push;
    const filters = isJsonObject(push) ? push : undefined;
    return {
        events: Object.keys(value),
        pushBranches: asStringList(filters?.branches),
        pushTags: asStringList(filters?.tags),
    };
}
/**
 * Serialize a step's `env` and `with` blocks to searchable text so credential
 * evidence can quote the offending line without a second traversal.
 * @param step - The raw step object
 * @returns Flattened `key: value` text, one pair per line
 */
function serializeStepInputs(step) {
    return ["env", "with"].flatMap(key => serializeBlock(step[key])).join("\n");
}
/**
 * Flatten one YAML mapping to `key: value` lines so it can be searched as text.
 * @param value - Candidate YAML mapping
 * @returns One line per entry (empty when the value is not a mapping)
 */
function serializeBlock(value) {
    if (!isJsonObject(value)) {
        return [];
    }
    return Object.entries(value).map(([name, entry]) => `${name}: ${typeof entry === "string" ? entry : String(entry)}`);
}
/**
 * Normalize an `if:` condition, which GitHub accepts as an expression string or
 * a bare boolean. Any non-empty value means the thing is conditional; the text
 * itself is carried only so evidence can quote it.
 * @param value - Candidate YAML `if` value
 * @returns The condition text, or `""` when the block is absent
 */
function asCondition(value) {
    if (value === undefined || value === null) {
        return "";
    }
    return typeof value === "string" ? value.trim() : String(value);
}
/**
 * Parse one job's `steps` list.
 * @param value - Candidate YAML `steps` value
 * @returns Normalized steps
 */
function parseSteps(value) {
    if (!Array.isArray(value)) {
        return [];
    }
    return value.filter(isJsonObject).map(step => ({
        name: typeof step.name === "string" ? step.name : "",
        run: typeof step.run === "string" ? step.run.trim() : "",
        uses: typeof step.uses === "string" ? step.uses.trim() : "",
        env: serializeBlock(step.env).join("\n"),
        inputs: serializeStepInputs(step),
        ifCondition: asCondition(step.if),
    }));
}
/**
 * Parse the `jobs` map of one workflow document.
 * @param document - Parsed workflow root
 * @param file - Repo-relative workflow path (stamped on each job)
 * @returns Normalized jobs, in declaration order
 */
function parseJobs(document, file) {
    const jobs = document.jobs;
    if (!isJsonObject(jobs)) {
        return [];
    }
    return Object.entries(jobs).flatMap(([id, raw]) => {
        if (!isJsonObject(raw)) {
            return [];
        }
        const job = raw;
        return [
            {
                id,
                workflow: file,
                needs: asStringList(job.needs),
                steps: parseSteps(job.steps),
                uses: typeof job.uses === "string" ? job.uses.trim() : "",
                permissions: asBlock(job.permissions),
                secrets: asBlock(job.secrets),
                environment: asEnvironments(job.environment),
                ifCondition: asCondition(job.if),
                env: serializeBlock(job.env).join("\n"),
                inputs: serializeBlock(job.with).join("\n"),
                services: isJsonObject(job.services) ? Object.keys(job.services) : [],
            },
        ];
    });
}
/**
 * List the workflow files a repository declares, in stable name order.
 * @param root - Repository root
 * @returns Workflow file names, or an empty list when the directory is absent
 */
async function listWorkflowFiles(root) {
    try {
        const entries = await readdir(path.join(root, WORKFLOWS_DIR));
        return entries
            .filter(entry => entry.endsWith(".yml") || entry.endsWith(".yaml"))
            .sort((a, b) => a.localeCompare(b));
    }
    catch {
        return [];
    }
}
/**
 * Parse one workflow file, returning null when it cannot be read or parsed.
 * @param root - Repository root
 * @param fileName - Workflow file name
 * @returns The parsed workflow, or null
 */
async function parseOneWorkflow(root, fileName) {
    const file = `${WORKFLOWS_DIR.split(path.sep).join("/")}/${fileName}`;
    try {
        const source = await readFile(path.join(root, WORKFLOWS_DIR, fileName), "utf8");
        const document = loadYaml(source);
        if (!isJsonObject(document)) {
            return null;
        }
        const record = document;
        return {
            file,
            name: typeof record.name === "string" ? record.name : fileName,
            permissions: asBlock(record.permissions),
            on: parseTrigger(record.on),
            jobs: parseJobs(record, file),
        };
    }
    catch {
        // A malformed workflow is skipped, never fatal: one bad file must not turn
        // the readiness report into an error the operator cannot act on.
        return null;
    }
}
/**
 * Read and parse every `.github/workflows/*.yml` file in a repository, offline.
 * @param root - Repository root
 * @returns Parsed workflows in stable file-name order (empty when there are none)
 */
export async function parseRepositoryWorkflows(root) {
    const fileNames = await listWorkflowFiles(root);
    const parsed = await Promise.all(fileNames.map(async (fileName) => await parseOneWorkflow(root, fileName)));
    return parsed.filter((workflow) => workflow !== null);
}
//# sourceMappingURL=doctor-readiness-workflows.js.map