diff --git a/.core-services/README.md b/.core-services/README.md new file mode 100644 index 0000000..7dc5960 --- /dev/null +++ b/.core-services/README.md @@ -0,0 +1,299 @@ +# Core Services Repository Validator + +This directory is an intentionally committed, offline repository-standards kit. Its canonical +maintainable source and regression suite live in +[`OpenSesame/core-services-ai-tooling`](https://github.com/OpenSesame/core-services-ai-tooling), +under `library/repository-validator/`. Consumer repositories receive a single generated runtime and +the readable configuration files needed to inspect and run it without GitHub or npm access. + +## What Validation Covers + +The validator compares the tracked GitHub Actions workflows and composite actions with the bundled +Core Services standard: + +- `on_push_ci.yml` triggers, the `commit-identifier` dispatch input, and the required jobs and + check names for the detected surfaces (npm, Terraform). App CI is required only when a + `package.json` defines a `build` or `test` script, or one under those prefixes such as + `test:unit`. Without them, App CI would only run `npm ci --ignore-scripts`, which installs + dependencies and that Repo Checks already does. It doesn't run any other script, so a build or + test script with another name, such as `verify`, still needs its own CI job. +- `CI Result` wiring through `check-ci-result` +- core-github-actions calls pinned to a release SHA with a `# /` comment at or + above the minimum release + (core-github-actions' own CI calls its components as `./.github/workflows/.yml` or + `./.github/actions/`, so each change runs the versions it changes; the validator accepts + those only when the `origin` remote is `OpenSesame/core-github-actions`) +- `permissions: {}` at the top of every workflow and a reason comment on every granted permission +- the `ubuntu-26.04` runner and no `ubuntu-latest` +- `--ignore-scripts` on every `npm ci` and `npm install` +- Node and npm floors: a major-only `.nvmrc`, `engines.node` with a major ceiling that matches + it, an `engines.npm` floor at or above 11.10.0 that the npm shipped with the `engines.node` floor + meets, `engine-strict=true` and `min-release-age` of at + least 7 days in `.npmrc`, no `packageManager` npm pin, and no global npm installs in workflows +- the `repo-checks` job that calls core-github-actions `run_repo_checks`, and the mise tasks, npm + scripts, lock files and actionlint runner label it needs for each detected technology +- kebab-case workflow inputs and the `on_call_` / `on_dispatch_` file prefixes +- the repository baseline: required files, no committed secrets or Terraform state, the Core + `.editorconfig`, VS Code extensions and settings, and tool configs (see Repository Baseline) + +It does not repeat what actionlint and zizmor already report. + +Validation does not execute repository tasks, run workflows, contact live +systems, or replace normal human code review, tests, security review, and deployment controls. The +human author owns all AI-assisted output and remains responsible for the repository change. + +## Standard And Contract + +- `.core-services/repository-standard.json` is the centrally managed snapshot of current Core + Services requirements. +- `.core-services/repository-contract.json` is owned by this repository. It records justified + exceptions. +- `.core-services/repository-contract.schema.json` describes the contract document shape. The + validator also applies standard-dependent checks that JSON Schema alone cannot express. + +Start with `templates/repository-contract.minimal.json`. Copy the object in +`templates/contract-exception.example.json` into the contract's `exceptions` array only when there +is a specific requirement, rationale, accountable owner, and optionally paths and a review date. The +standard lists the requirements no exception can waive, such as the required CI jobs, CI Result, +core-github-actions pins, permissions, runner, and npm install rules. + +## File Ownership + +The repository owns only `.core-services/repository-contract.json` and its repository-specific +exceptions, workflows, and documentation. + +The following files are generated or centrally managed and must not be edited directly: + +- `.core-services/README.md` +- `.core-services/generated/validate-repo.mjs` +- `.core-services/repository-contract.schema.json` +- `.core-services/repository-standard.json` +- `.core-services/templates/*.json` +- `.core-services/validator-manifest.json` + +The bundle is committed intentionally as the deployable offline artifact; it is not accidental +build output. `validator-manifest.json` records its validator and standard versions, every managed +file checksum, and the exact clean ai-tooling source commit used to install or update it. + +## Run And Interpret Validation + +With the repository's Node version: + +```sh +node .core-services/generated/validate-repo.mjs +node .core-services/generated/validate-repo.mjs --json +``` + +- Passes confirm a requirement the validator could inspect. +- Warnings identify something the validator cannot confirm statically, such as a computed + `runs-on`, or a warning-level exception. +- Policy failures exit `1` and require a repository change or a justified exception where the + standard permits one. +- Operational or invocation failures exit `2` and identify malformed input, missing required files, + unsupported arguments, or a validator execution problem. + +The validator inspects visible local configuration; a passing result does not prove behavior that +is hidden behind task execution, external services, generated infrastructure, or deployment. + +## Run In CI + +Installing the kit does not run validation in CI. CI runs it through the `repo-checks` job, which +calls core-github-actions `run_repo_checks`. Its `Standard` job runs the installed bundle and +reports `Repo Checks / Standard / Validate`. Add the job to `on_push_ci.yml` and to `ci-result`'s +`needs` so the `CI Result` required check fails with it. Replace `` with the full commit SHA +of the release named in the comment: `git rev-list -n1 workflows/run_repo_checks/1.0.2` in +core-github-actions. + +```yaml + repo-checks: + name: Repo Checks + needs: ci-context + if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} + uses: OpenSesame/core-github-actions/.github/workflows/run_repo_checks.yml@ # workflows/run_repo_checks/1.0.2 + permissions: + contents: read # Required to check out the code to lint, format-check, scan and validate. + with: + commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} + secrets: + ORG_READ_ONLY_SSH_KEY: ${{ secrets.ORG_READ_ONLY_SSH_KEY }} +``` + +Pass `ORG_GITHUB_PACKAGES_READ_ONLY_TOKEN: ${{ secrets.ORG_GITHUB_PACKAGES_READ_ONLY_TOKEN }}` as +well when the repository installs private `@opensesame` packages. + +The validator enforces this wiring itself. It requires the job in every repository, with exactly +`contents: read`, the `ORG_READ_ONLY_SSH_KEY` secret passed by name (not `secrets: inherit`), no +`continue-on-error`, the `ci-context` gate and commit, and a pin at or above +`workflows/run_repo_checks/1.0.2`. While `.core-services/validator-manifest.json` exists, it also +fails when a managed file no longer matches the manifest checksums, or when the manifest and the +installed bundle disagree on the validator version. A repo-owned job that runs the bundle is no +longer needed; remove it once Repo Checks runs. + +With the key, the Standard job reads the newest `repository-validator/` tag from +core-services-ai-tooling and passes it as `--latest-version`. An older installed kit is a warning, +not a failure; run update to clear it. Without the key, or if the lookup fails, the job validates +without that check. The validator itself stays offline. + +### Repo Checks Prerequisites + +`run_repo_checks` detects technologies from tracked files outside `.core-services/`, +`node_modules/`, `.terraform/`, `dist/`, `build/`, `coverage/` and `vendor/`, then runs mise tasks +and npm scripts by name. The validator uses the same detection and requires what each check runs: + +- GitHub Actions (`.github/workflows/*.yml` and `.github/actions/**/action.yml`): mise tasks + `lint:gha` and `scan:gha`, which must run `zizmor --offline`, and the `ubuntu-26.04` label under + `self-hosted-runner.labels` in `.github/actionlint.yaml` +- Terraform (`*.tf`): mise tasks `init:terraform`, which must use `-lockfile=readonly`, + `validate:terraform`, `lint:terraform`, `format:terraform` and `scan:terraform`, and a committed + `.terraform.lock.hcl` in each root +- Shell (`*.sh`, `*.bash`, `*.zsh`, or a first line with an sh, bash or zsh shebang): mise tasks + `lint:shell` and `format:shell` +- Markdown (`*.md`): npm script `lint:markdown` +- JS (`*.js`, `*.mjs`, `*.cjs`, `*.jsx`, `*.ts`, `*.mts`, `*.cts`, `*.tsx`): npm scripts `lint:js` + and `format:prettier` + +Every repository also needs: + +- `mise.toml` and `mise.lock`, with every tool in `[tools]` locked for `linux-x64` and + `macos-arm64` (`mise lock --platform linux-x64,macos-arm64`). Tools from backends that mise + locks without platform artifacts, such as `pipx:`, `go:`, `cargo:` and `gem:`, only need a lock + entry. An `npm:` tool fails: move it to `package.json` `devDependencies`, since mise holds only + native tools. +- mise tasks as `[tasks.""]` tables or as keys of one `[tasks]` table. The required flags, + such as `--offline` and `-lockfile=readonly`, must be in the task's `run` command, not in its + description or a comment. +- `.nvmrc`, and `package-lock.json` when Markdown or JS is present +- a `package.json` script for each mise task that runs it, such as + `"lint:gha": "mise run lint:gha"` +- `lint`, `format`, `scan` and `check` scripts that are each one + `npm-run-all --continue-on-error` command, where `lint`, `format` and `scan` run every required + script with their prefix and `check` runs every required script, so `npm run check` runs what + Repo Checks runs. An aggregate with no required script under its prefix is optional. + +A Terraform root is a directory with `*.tf` files that is not under a `modules/` directory and is +not called as a local module (`source = "./…"` or `"../…"` in a `module` block) from another +directory. These findings +report under `repo-checks-prerequisites`, which a path-scoped contract exception can waive, for +example for a root that is being deleted. + +## Repository Baseline + +Every repository commits the same baseline files. The validator checks them so editors and CI +agree on formatting before a change reaches review. Each rule reports under its own requirement, +which a path-scoped contract exception can waive. + +- `required-files`: a non-empty `README.md`, `AGENTS.md`, `.gitignore`, `.editorconfig`, + `.vscode/extensions.json`, `.vscode/settings.json`, and `CODEOWNERS` in `.github/`, the root, + or `docs/`. +- `committed-secrets`: no tracked `.terraform/` directory, Terraform state, saved plans, + `.tfvars` values or CLI credentials, `.env` files (`.env.example`, `.env.sample`, + `.env.template` and `.env.tpl` are fine), private keys or certificates, or cloud credential + files. +- `editorconfig`: `root = true`, and `[*]` sets `charset = utf-8`, `end_of_line = lf`, + `indent_size = 2`, `indent_style = space`, `insert_final_newline = true` and + `trim_trailing_whitespace = true`. `[*.md]` sets `trim_trailing_whitespace = false`, and + `[Makefile]` sets `indent_style = tab` when there is a Makefile. The section headers must be + exactly `[*]`, `[*.md]` and `[Makefile]`; a brace list such as `[*.{md,markdown}]` doesn't + count. Other sections and properties are the repository's own. +- `vscode-extensions`: `.vscode/extensions.json` recommends `editorconfig.editorconfig` and + `streetsidesoftware.code-spell-checker`, plus, for each technology Repo Checks detects: + - GitHub Actions: `github.vscode-github-actions`, `redhat.vscode-yaml` + - Terraform: `hashicorp.terraform` + - Markdown: `davidanson.vscode-markdownlint` + - Shell: `timonwong.shellcheck`, `foxundermoon.shell-format` + - JS: `dbaeumer.vscode-eslint`, `esbenp.prettier-vscode` +- `vscode-settings`: `.vscode/settings.json` formats on save with the formatter each check uses, + so saving never produces a diff Repo Checks rejects. Other settings are the repository's own. + + ```jsonc + { + "editor.formatOnSave": true, + "editor.rulers": [100], + // Markdown + "[markdown]": { "editor.defaultFormatter": null }, + // Terraform + "[terraform]": { "editor.defaultFormatter": "hashicorp.terraform" }, + "[terraform-vars]": { "editor.defaultFormatter": "hashicorp.terraform" }, + // Shell + "[shellscript]": { "editor.defaultFormatter": "foxundermoon.shell-format" }, + // JS: Prettier formats JavaScript, TypeScript, JSON and YAML; ESLint fixes on save + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.requireConfig": true, + "yaml.format.enable": false, + "eslint.format.enable": false, + "editor.codeActionsOnSave": { "source.fixAll.eslint": "always" }, + "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[jsonc]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + } + ``` + + Include only the groups for the technologies the repository has. +- `tool-configs`: the tools Repo Checks runs have Core configuration instead of their defaults: + `.tflint.hcl` and `.checkov.yml` for Terraform, a markdownlint-cli2 config such as + `.markdownlint-cli2.yaml` for Markdown, and an ESLint config such as `eslint.config.mjs` and a + Prettier config (`.prettierrc`, `prettier.config.*`, or a `prettier` key in `package.json`, + which may name a shared config) for JS. A `.cjs` or `.mjs` config file is itself JavaScript, so + it also makes Repo Checks and the validator see JS. The actionlint runner label is checked with + the Repo Checks prerequisites. + +## Install, Update, And Status + +Run lifecycle commands from a clean `core-services-ai-tooling` checkout so provenance can be tied to +its exact `HEAD`: + +```sh +node scripts/repository-validation/repository-validator.mjs install /path/to/consumer +node scripts/repository-validation/repository-validator.mjs install /path/to/consumer --dry-run +node scripts/repository-validation/repository-validator.mjs update /path/to/consumer +node scripts/repository-validation/repository-validator.mjs update /path/to/consumer --dry-run +node scripts/repository-validation/repository-validator.mjs status /path/to/consumer +``` + +Install creates a minimal contract only when one is absent. Update preserves the contract's +exceptions, moves its standard reference to the new version, and refuses to replace a generated +file whose checksum differs from the installed manifest. Both commands add the generated-file marker +to `.gitattributes` and the disposable debug-source path to `.gitignore`. They also keep every +centrally managed file out of formatting, linting, and scans while +`.core-services/repository-contract.json` stays in scope: + +- `.prettierignore` and `.semgrepignore` get `.core-services/*` followed by + `!.core-services/repository-contract.json`. Earlier kit entries such as + `.core-services/generated/` or `.core-services/**` are replaced in place. A new `.semgrepignore` + repeats Semgrep's default ignore list, because the file replaces those defaults. +- `sonar.exclusions` in `sonar-project.properties`, when that file exists, gets the managed paths + `.core-services/*/**`, `.core-services/README.md`, `.core-services/repository-contract.schema.json`, + `.core-services/repository-standard.json`, and `.core-services/validator-manifest.json`. Sonar has + no negation, so an earlier `.core-services/**` entry is removed. +- When the repository has an ESLint config, they print a reminder to add `.core-services/**` to its + ignores. ESLint lints no JSON there, so it can skip the whole directory. They do not edit ESLint + config. + +Other content stays untouched. They do not stage, commit, push, or access the network. + +Status is read-only and reports installed provenance, current, missing, modified, and stale managed +files separately from contract problems. When checksums drift, do not edit the manifest or generated +file to make status pass. Inspect the diff, preserve any useful discovery in the canonical source, +restore the installed generated file from version control if appropriate, and run update again. +Malformed or unrecognized manifests require manual inspection before any update. + +## Debug Unbundled Source + +Materialize readable source from a clean ai-tooling checkout without changing the normal validation +task. The lifecycle commands run from the ai-tooling checkout. The validator checks its current +directory, so run the debug copy from the consumer repository; run elsewhere, it validates that +directory instead. + +```sh +node scripts/repository-validation/repository-validator.mjs materialize-source /path/to/consumer +(cd /path/to/consumer && node .core-services/debug-source/src/validate-repo.mjs) +node scripts/repository-validation/repository-validator.mjs remove-debug-source /path/to/consumer +``` + +`.core-services/debug-source/` is local, disposable, gitignored debugging material. It is not +consumer-owned production code. Fixes discovered there belong in `core-services-ai-tooling`; rebuild +the central bundle, run its regression suite, and update the consumer kit from the corrected source. diff --git a/.core-services/generated/validate-repo.mjs b/.core-services/generated/validate-repo.mjs new file mode 100644 index 0000000..3715dde --- /dev/null +++ b/.core-services/generated/validate-repo.mjs @@ -0,0 +1,10183 @@ +#!/usr/bin/env node +// Generated by scripts/repository-validation/build-repository-validator.mjs. Do not edit. Validator version 0.14.1. + +// library/repository-validator/src/validate-repo.mjs +import { resolve as resolve5 } from "node:path"; +import { pathToFileURL } from "node:url"; + +// library/repository-validator/src/validator/constants.mjs +var VALIDATOR_VERSION = "0.14.1"; +var CONTRACT_PATH = ".core-services/repository-contract.json"; +var STANDARD_PATH = ".core-services/repository-standard.json"; +var MANIFEST_PATH = ".core-services/validator-manifest.json"; +var BUNDLE_PATH = ".core-services/generated/validate-repo.mjs"; +var REPO_CHECKS_JOB = "repo-checks"; + +// library/repository-validator/src/validator/errors.mjs +var ValidatorError = class extends Error { +}; + +// library/repository-validator/src/validator/findings.mjs +function createFinding(severity, code, path, message, remediation = null) { + return { severity, code, path, message, remediation }; +} +function createRuleFinding(severity, code, requirement, path, message, remediation = null) { + return { ...createFinding(severity, code, path, message, remediation), requirement }; +} +function summarizeFindings(findings) { + return { + errors: findings.filter((finding2) => finding2.severity === "error").length, + warnings: findings.filter((finding2) => finding2.severity === "warning").length, + passes: findings.filter((finding2) => finding2.severity === "pass").length + }; +} +function determineExitCode(findings) { + return findings.some((finding2) => finding2.severity === "error") ? 1 : 0; +} +function createProvenance(standard) { + return { + standardId: standard.standard?.id ?? null, + standardVersion: standard.standard?.version ?? null, + sourceRepository: standard.standard?.sourceRepository ?? null, + sourceCommit: standard.standard?.sourceCommit ?? null + }; +} +function deduplicateFindings(findings) { + const uniqueFindings = []; + const seen = /* @__PURE__ */ new Set(); + for (const finding2 of findings) { + const key = `${finding2.severity}\0${finding2.code}\0${finding2.path}\0${finding2.message}`; + if (!seen.has(key)) uniqueFindings.push(finding2); + seen.add(key); + } + return uniqueFindings; +} + +// library/repository-validator/src/validator/values.mjs +function isNonemptyString(value) { + return typeof value === "string" && value.trim().length > 0; +} +function isObject(value) { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} +function determineExceptionSeverity(exception) { + return exception.isWarning === false ? "pass" : "warning"; +} + +// library/repository-validator/src/validator/cli.mjs +import { existsSync, statSync as statSync2 } from "node:fs"; +import { resolve as resolve4 } from "node:path"; + +// library/repository-validator/src/validator/validation.mjs +import { readFileSync as readFileSync2 } from "node:fs"; +import { resolve as resolve3 } from "node:path"; + +// library/repository-validator/standard/repository-standard.json +var repository_standard_default = { + standard: { + id: "core-services-ai-ready-repository", + version: "0.14.1", + sourceRepository: "OpenSesame/core-services-ai-tooling", + sourceCommit: "repo-local-prototype" + }, + surfaceDetection: { + npm: { + include: ["(^|/)package\\.json$"] + }, + terraform: { + include: ["\\.tf$"], + exclude: ["(^|/)\\.terraform/"] + } + }, + repository: { + requiredFiles: [ + { + paths: ["README.md"] + }, + { + paths: ["AGENTS.md"] + }, + { + paths: [".gitignore"] + }, + { + paths: [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"] + }, + { + paths: [".editorconfig"] + }, + { + paths: [".vscode/extensions.json"] + }, + { + paths: [".vscode/settings.json"] + } + ], + committedFiles: [ + { + id: "terraform-working-directory", + description: "a Terraform working directory", + include: ["(^|/)\\.terraform/"] + }, + { + id: "terraform-state", + description: "Terraform state", + include: ["\\.tfstate(?:\\..+)?$"] + }, + { + id: "terraform-plan", + description: "a saved Terraform plan", + include: ["\\.tfplan(?:\\.json)?$", "(^|/)(?:tfplan|plan\\.out)$"] + }, + { + id: "terraform-variable-values", + description: "Terraform variable values", + include: ["\\.tfvars(?:\\.json)?$"] + }, + { + id: "terraform-local-credentials", + description: "Terraform CLI credentials", + include: ["(^|/)(?:credentials\\.tfrc\\.json|\\.terraformrc|terraform\\.rc)$"] + }, + { + id: "local-environment", + description: "a local .env file", + include: ["(^|/)\\.env(?:\\.[^/]+)?$"], + exclude: ["\\.env\\.(?:example|sample|template|tpl)$"] + }, + { + id: "private-key-or-certificate", + description: "a private key or certificate", + include: [ + "\\.(?:cer|crt|der|key|p12|pem|pfx|ppk)$", + "(^|/)(?:id_rsa|id_dsa|id_ecdsa|id_ed25519)$" + ] + }, + { + id: "cloud-credentials", + description: "cloud credentials", + include: ["(^|/)\\.aws/credentials$", "(^|/)application_default_credentials\\.json$"] + } + ], + editorConfig: { + path: ".editorconfig", + preamble: { + root: "true" + }, + sections: { + "*": { + charset: "utf-8", + end_of_line: "lf", + indent_size: "2", + indent_style: "space", + insert_final_newline: "true", + trim_trailing_whitespace: "true" + }, + "*.md": { + trim_trailing_whitespace: "false" + } + }, + conditionalSections: [ + { + include: ["(^|/)Makefile$"], + section: "Makefile", + properties: { + indent_style: "tab" + } + } + ] + }, + vscode: { + extensionsPath: ".vscode/extensions.json", + settingsPath: ".vscode/settings.json", + extensions: { + all: ["editorconfig.editorconfig", "streetsidesoftware.code-spell-checker"], + gha: ["github.vscode-github-actions", "redhat.vscode-yaml"], + terraform: ["hashicorp.terraform"], + markdown: ["davidanson.vscode-markdownlint"], + shell: ["timonwong.shellcheck", "foxundermoon.shell-format"], + js: ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] + }, + settings: { + all: [ + { + "editor.formatOnSave": true, + "editor.rulers": [100] + } + ], + terraform: [ + { + "[terraform]": { + "editor.defaultFormatter": "hashicorp.terraform" + }, + "[terraform-vars]": { + "editor.defaultFormatter": "hashicorp.terraform" + } + } + ], + markdown: [ + { + "[markdown]": { + "editor.defaultFormatter": null + } + } + ], + shell: [ + { + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + } + } + ], + js: [ + { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.requireConfig": true, + "yaml.format.enable": false, + "eslint.format.enable": false, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } + } + ] + } + }, + toolConfigs: { + terraform: [ + { + tool: "tflint", + paths: [".tflint.hcl"] + }, + { + tool: "Checkov", + paths: [".checkov.yml", ".checkov.yaml"] + } + ], + markdown: [ + { + tool: "markdownlint-cli2", + paths: [ + ".markdownlint-cli2.yaml", + ".markdownlint-cli2.jsonc", + ".markdownlint-cli2.cjs", + ".markdownlint-cli2.mjs", + ".markdownlint.yaml", + ".markdownlint.yml", + ".markdownlint.jsonc", + ".markdownlint.json", + ".markdownlint.cjs", + ".markdownlint.mjs" + ] + } + ], + js: [ + { + tool: "ESLint", + paths: [ + "eslint.config.mjs", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.ts", + "eslint.config.mts", + "eslint.config.cts", + ".eslintrc.json", + ".eslintrc.js", + ".eslintrc.cjs", + ".eslintrc.yaml", + ".eslintrc.yml" + ] + }, + { + tool: "Prettier", + paths: [ + ".prettierrc", + ".prettierrc.json", + ".prettierrc.json5", + ".prettierrc.yaml", + ".prettierrc.yml", + ".prettierrc.toml", + ".prettierrc.js", + ".prettierrc.cjs", + ".prettierrc.mjs", + "prettier.config.js", + "prettier.config.cjs", + "prettier.config.mjs", + ".prettierrc.ts", + ".prettierrc.mts", + ".prettierrc.cts", + "prettier.config.ts", + "prettier.config.mts", + "prettier.config.cts", + "package.json#prettier" + ] + } + ] + } + }, + githubActions: { + workflowPattern: "^\\.github/workflows/[^/]+\\.ya?ml$", + actionPattern: "^\\.github/actions/(?:[^/]+/)+action\\.ya?ml$", + ciWorkflow: { + path: ".github/workflows/on_push_ci.yml", + requiredTriggers: ["push", "pull_request", "workflow_dispatch"], + dispatchRevisionInput: "commit-identifier", + contextJob: "ci-context", + contextRevision: "${{ inputs.commit-identifier }}", + jobRevision: "${{ needs.ci-context.outputs.commit-identifier }}", + jobCondition: "needs.ci-context.outputs.should-run-ci == 'true'", + jobs: [ + { + id: "ci-context", + name: "CI Context", + checks: ["CI Context / Resolve"], + uses: ["workflows/resolve_ci_context"] + }, + { + id: "app-ci", + name: "App CI", + namePattern: "^App CI(?: \\([^()]+\\))?$", + checks: ["App CI ()"], + surfaces: ["npm"], + requiredWithScripts: ["build", "test"] + }, + { + id: "dependency-audit", + name: "Dependency Audit", + checks: ["Dependency Audit / Audit"], + uses: ["workflows/run_npm_audit"], + surfaces: ["npm"], + waivable: true + }, + { + id: "repo-checks", + name: "Repo Checks", + checks: [ + "Repo Checks / Detect", + "Repo Checks / Standard / Validate", + "Repo Checks / Summary" + ], + uses: ["workflows/run_repo_checks"], + permissions: { + contents: "read" + }, + secrets: ["ORG_READ_ONLY_SSH_KEY"] + }, + { + id: "terraform", + name: "Terraform", + checks: ["Terraform / / Validate Plan"], + uses: [ + "workflows/tf_validate_plan_env_roots", + "workflows/tf_validate_plan_single_root" + ], + surfaces: ["terraform"] + }, + { + id: "semgrep", + name: "Semgrep", + checks: ["Semgrep / Scan"], + uses: ["workflows/run_semgrep_scan"] + }, + { + id: "sonar", + name: "Sonar", + checks: ["Sonar / Scan"], + uses: ["workflows/run_sonar_scan"], + waivable: true + } + ], + resultJob: { + id: "ci-result", + name: "CI Result", + if: "${{ !cancelled() && needs.ci-context.outputs.should-run-ci != 'false' }}", + action: "actions/check-ci-result", + stepName: "Check CI job results", + with: { + "needs-json": "${{ toJSON(needs) }}" + } + } + }, + coreGithubActions: { + repository: "OpenSesame/core-github-actions", + minimumReleases: { + "actions/pr-open-check": "2.1.0", + "workflows/resolve_ci_context": "1.0.0", + "workflows/run_npm_audit": "1.0.0", + "workflows/run_sonar_scan": "2.0.0", + "actions/run-semgrep": "2.0.0", + "workflows/run_semgrep_scan": "2.0.0", + "workflows/tf_validate_plan_env_roots": "1.0.0", + "workflows/tf_validate_plan_single_root": "1.0.0", + "workflows/run_infra_ci": "1.0.1", + "workflows/run_repo_checks": "1.0.2", + "actions/resolve-feature-workspace": "1.0.0", + "actions/configure-aws-oidc": "1.0.2", + "actions/check-ci-result": "1.0.0", + "workflows/trigger_workflow_and_wait": "1.0.0", + "workflows/tf_apply": "0.2.1", + "workflows/deploy_environment": "0.3.1", + "workflows/deploy_thru_prod": "0.3.1" + } + }, + runner: { + label: "ubuntu-26.04", + forbiddenLabels: ["ubuntu-latest"] + }, + npmInstall: { + requiredFlag: "--ignore-scripts", + actionsRequiringInstallFalse: ["cypress-io/github-action"] + }, + repoChecks: { + excludePattern: "(^|/)(\\.core-services|node_modules|\\.terraform|dist|build|coverage|vendor)/", + technologies: [ + { + id: "gha", + name: "GitHub Actions", + include: "^\\.github/(workflows/[^/]+|actions/.+/action)\\.ya?ml$", + miseTasks: ["lint:gha", "scan:gha"] + }, + { + id: "terraform", + name: "Terraform", + include: "\\.tf$", + miseTasks: [ + "init:terraform", + "validate:terraform", + "lint:terraform", + "format:terraform", + "scan:terraform" + ] + }, + { + id: "markdown", + name: "Markdown", + include: "\\.md$", + npmScripts: ["lint:markdown"] + }, + { + id: "shell", + name: "Shell", + include: "\\.(sh|bash|zsh)$", + shebang: "^#!\\s*(/usr/bin/env\\s+)?(/usr)?(/local)?(/bin/)?(sh|bash|zsh)(\\s|$)", + miseTasks: ["lint:shell", "format:shell"] + }, + { + id: "js", + name: "JS", + include: "\\.(js|mjs|cjs|jsx|ts|mts|cts|tsx)$", + npmScripts: ["lint:js", "format:prettier"] + } + ], + miseTaskCommands: { + "scan:gha": "\\bzizmor\\b[^\\n]*--offline", + "init:terraform": "-lockfile=readonly" + }, + miseConfig: "mise.toml", + miseLock: "mise.lock", + lockPlatforms: ["linux-x64", "macos-arm64"], + npmBackends: ["npm"], + unplatformedBackends: ["pipx", "go", "cargo", "gem", "dotnet", "spm"], + packageJson: "package.json", + nodeVersionFile: ".nvmrc", + npmLockFile: "package-lock.json", + aggregateScripts: ["lint", "format", "scan", "check"], + checkScript: "check", + terraformLockFile: ".terraform.lock.hcl", + terraformModulePattern: "(^|/)modules/", + actionlintConfigPaths: [".github/actionlint.yaml", ".github/actionlint.yml"], + runnerLabels: ["ubuntu-26.04"] + }, + nodeVersions: { + nodeVersionFile: ".nvmrc", + packageJson: "package.json", + npmrc: ".npmrc", + minimumNpm: "11.10.0", + minimumReleaseAgeDays: 7, + bundledNpm: { + "22": [ + ["22.0.0", "10.5.1"], + ["22.1.0", "10.7.0"], + ["22.3.0", "10.8.1"], + ["22.5.0", "10.8.2"], + ["22.9.0", "10.8.3"], + ["22.10.0", "10.9.0"], + ["22.13.0", "10.9.2"], + ["22.18.0", "10.9.3"], + ["22.21.0", "10.9.4"], + ["22.22.2", "10.9.7"], + ["22.22.3", "10.9.8"], + ["22.23.3", "10.9.9"] + ], + "24": [ + ["24.0.0", "11.3.0"], + ["24.3.0", "11.4.2"], + ["24.5.0", "11.5.1"], + ["24.8.0", "11.6.0"], + ["24.10.0", "11.6.1"], + ["24.11.1", "11.6.2"], + ["24.13.1", "11.8.0"], + ["24.14.0", "11.9.0"], + ["24.14.1", "11.11.0"], + ["24.15.0", "11.12.1"], + ["24.16.0", "11.13.0"], + ["24.18.0", "11.16.0"], + ["24.19.0", "11.17.0"], + ["24.20.0", "11.19.0"], + ["24.22.0", "11.19.1"] + ], + "26": [ + ["26.0.0", "11.12.1"], + ["26.1.0", "11.13.0"], + ["26.3.0", "11.16.0"], + ["26.4.0", "11.17.0"], + ["26.6.0", "11.18.0"], + ["26.7.0", "11.19.0"], + ["26.8.2", "11.19.1"], + ["26.11.0", "11.20.0"] + ] + } + }, + inputNames: { + pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" + }, + filePrefixes: { + callOnly: "on_call_", + dispatchAndCall: "on_dispatch_" + } + }, + exceptions: { + nonOverridableRequirements: [ + "ci-workflow", + "ci-jobs", + "ci-result", + "core-gha-pins", + "workflow-permissions", + "runner", + "npm-ignore-scripts", + "repo-validation", + "kit-integrity" + ] + } +}; + +// node_modules/yaml/browser/dist/nodes/identity.js +var ALIAS = /* @__PURE__ */ Symbol.for("yaml.alias"); +var DOC = /* @__PURE__ */ Symbol.for("yaml.document"); +var MAP = /* @__PURE__ */ Symbol.for("yaml.map"); +var PAIR = /* @__PURE__ */ Symbol.for("yaml.pair"); +var SCALAR = /* @__PURE__ */ Symbol.for("yaml.scalar"); +var SEQ = /* @__PURE__ */ Symbol.for("yaml.seq"); +var NODE_TYPE = /* @__PURE__ */ Symbol.for("yaml.node.type"); +var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS; +var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC; +var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP; +var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR; +var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR; +var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ; +function isCollection(node) { + if (node && typeof node === "object") + switch (node[NODE_TYPE]) { + case MAP: + case SEQ: + return true; + } + return false; +} +function isNode(node) { + if (node && typeof node === "object") + switch (node[NODE_TYPE]) { + case ALIAS: + case MAP: + case SCALAR: + case SEQ: + return true; + } + return false; +} +var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor; + +// node_modules/yaml/browser/dist/visit.js +var BREAK = /* @__PURE__ */ Symbol("break visit"); +var SKIP = /* @__PURE__ */ Symbol("skip children"); +var REMOVE = /* @__PURE__ */ Symbol("remove node"); +function visit(node, visitor) { + const visitor_ = initVisitor(visitor); + if (isDocument(node)) { + const cd = visit_(null, node.contents, visitor_, Object.freeze([node])); + if (cd === REMOVE) + node.contents = null; + } else + visit_(null, node, visitor_, Object.freeze([])); +} +visit.BREAK = BREAK; +visit.SKIP = SKIP; +visit.REMOVE = REMOVE; +function visit_(key, node, visitor, path) { + const ctrl = callVisitor(key, node, visitor, path); + if (isNode(ctrl) || isPair(ctrl)) { + replaceNode(key, path, ctrl); + return visit_(key, ctrl, visitor, path); + } + if (typeof ctrl !== "symbol") { + if (isCollection(node)) { + path = Object.freeze(path.concat(node)); + for (let i = 0; i < node.items.length; ++i) { + const ci = visit_(i, node.items[i], visitor, path); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + node.items.splice(i, 1); + i -= 1; + } + } + } else if (isPair(node)) { + path = Object.freeze(path.concat(node)); + const ck = visit_("key", node.key, visitor, path); + if (ck === BREAK) + return BREAK; + else if (ck === REMOVE) + node.key = null; + const cv = visit_("value", node.value, visitor, path); + if (cv === BREAK) + return BREAK; + else if (cv === REMOVE) + node.value = null; + } + } + return ctrl; +} +async function visitAsync(node, visitor) { + const visitor_ = initVisitor(visitor); + if (isDocument(node)) { + const cd = await visitAsync_(null, node.contents, visitor_, Object.freeze([node])); + if (cd === REMOVE) + node.contents = null; + } else + await visitAsync_(null, node, visitor_, Object.freeze([])); +} +visitAsync.BREAK = BREAK; +visitAsync.SKIP = SKIP; +visitAsync.REMOVE = REMOVE; +async function visitAsync_(key, node, visitor, path) { + const ctrl = await callVisitor(key, node, visitor, path); + if (isNode(ctrl) || isPair(ctrl)) { + replaceNode(key, path, ctrl); + return visitAsync_(key, ctrl, visitor, path); + } + if (typeof ctrl !== "symbol") { + if (isCollection(node)) { + path = Object.freeze(path.concat(node)); + for (let i = 0; i < node.items.length; ++i) { + const ci = await visitAsync_(i, node.items[i], visitor, path); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK) + return BREAK; + else if (ci === REMOVE) { + node.items.splice(i, 1); + i -= 1; + } + } + } else if (isPair(node)) { + path = Object.freeze(path.concat(node)); + const ck = await visitAsync_("key", node.key, visitor, path); + if (ck === BREAK) + return BREAK; + else if (ck === REMOVE) + node.key = null; + const cv = await visitAsync_("value", node.value, visitor, path); + if (cv === BREAK) + return BREAK; + else if (cv === REMOVE) + node.value = null; + } + } + return ctrl; +} +function initVisitor(visitor) { + if (typeof visitor === "object" && (visitor.Collection || visitor.Node || visitor.Value)) { + return Object.assign({ + Alias: visitor.Node, + Map: visitor.Node, + Scalar: visitor.Node, + Seq: visitor.Node + }, visitor.Value && { + Map: visitor.Value, + Scalar: visitor.Value, + Seq: visitor.Value + }, visitor.Collection && { + Map: visitor.Collection, + Seq: visitor.Collection + }, visitor); + } + return visitor; +} +function callVisitor(key, node, visitor, path) { + if (typeof visitor === "function") + return visitor(key, node, path); + if (isMap(node)) + return visitor.Map?.(key, node, path); + if (isSeq(node)) + return visitor.Seq?.(key, node, path); + if (isPair(node)) + return visitor.Pair?.(key, node, path); + if (isScalar(node)) + return visitor.Scalar?.(key, node, path); + if (isAlias(node)) + return visitor.Alias?.(key, node, path); + return void 0; +} +function replaceNode(key, path, node) { + const parent = path[path.length - 1]; + if (isCollection(parent)) { + parent.items[key] = node; + } else if (isPair(parent)) { + if (key === "key") + parent.key = node; + else + parent.value = node; + } else if (isDocument(parent)) { + parent.contents = node; + } else { + const pt = isAlias(parent) ? "alias" : "scalar"; + throw new Error(`Cannot replace node with ${pt} parent`); + } +} + +// node_modules/yaml/browser/dist/doc/directives.js +var escapeChars = { + "!": "%21", + ",": "%2C", + "[": "%5B", + "]": "%5D", + "{": "%7B", + "}": "%7D" +}; +var escapeTagName = (tn) => tn.replace(/[!,[\]{}]/g, (ch) => escapeChars[ch]); +var Directives = class _Directives { + constructor(yaml, tags) { + this.docStart = null; + this.docEnd = false; + this.yaml = Object.assign({}, _Directives.defaultYaml, yaml); + this.tags = Object.assign({}, _Directives.defaultTags, tags); + } + clone() { + const copy = new _Directives(this.yaml, this.tags); + copy.docStart = this.docStart; + return copy; + } + /** + * During parsing, get a Directives instance for the current document and + * update the stream state according to the current version's spec. + */ + atDocument() { + const res = new _Directives(this.yaml, this.tags); + switch (this.yaml.version) { + case "1.1": + this.atNextDocument = true; + break; + case "1.2": + this.atNextDocument = false; + this.yaml = { + explicit: _Directives.defaultYaml.explicit, + version: "1.2" + }; + this.tags = Object.assign({}, _Directives.defaultTags); + break; + } + return res; + } + /** + * @param onError - May be called even if the action was successful + * @returns `true` on success + */ + add(line, onError) { + if (this.atNextDocument) { + this.yaml = { explicit: _Directives.defaultYaml.explicit, version: "1.1" }; + this.tags = Object.assign({}, _Directives.defaultTags); + this.atNextDocument = false; + } + const parts = line.trim().split(/[ \t]+/); + const name = parts.shift(); + switch (name) { + case "%TAG": { + if (parts.length !== 2) { + onError(0, "%TAG directive should contain exactly two parts"); + if (parts.length < 2) + return false; + } + const [handle, prefix] = parts; + this.tags[handle] = prefix; + return true; + } + case "%YAML": { + this.yaml.explicit = true; + if (parts.length !== 1) { + onError(0, "%YAML directive should contain exactly one part"); + return false; + } + const [version] = parts; + if (version === "1.1" || version === "1.2") { + this.yaml.version = version; + return true; + } else { + const isValid = /^\d+\.\d+$/.test(version); + onError(6, `Unsupported YAML version ${version}`, isValid); + return false; + } + } + default: + onError(0, `Unknown directive ${name}`, true); + return false; + } + } + /** + * Resolves a tag, matching handles to those defined in %TAG directives. + * + * @returns Resolved tag, which may also be the non-specific tag `'!'` or a + * `'!local'` tag, or `null` if unresolvable. + */ + tagName(source, onError) { + if (source === "!") + return "!"; + if (source[0] !== "!") { + onError(`Not a valid tag: ${source}`); + return null; + } + if (source[1] === "<") { + const verbatim = source.slice(2, -1); + if (verbatim === "!" || verbatim === "!!") { + onError(`Verbatim tags aren't resolved, so ${source} is invalid.`); + return null; + } + if (source[source.length - 1] !== ">") + onError("Verbatim tags must end with a >"); + return verbatim; + } + const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s); + if (!suffix) + onError(`The ${source} tag has no suffix`); + const prefix = this.tags[handle]; + if (prefix) { + try { + return prefix + decodeURIComponent(suffix); + } catch (error) { + onError(String(error)); + return null; + } + } + if (handle === "!") + return source; + onError(`Could not resolve tag: ${source}`); + return null; + } + /** + * Given a fully resolved tag, returns its printable string form, + * taking into account current tag prefixes and defaults. + */ + tagString(tag) { + for (const [handle, prefix] of Object.entries(this.tags)) { + if (tag.startsWith(prefix)) + return handle + escapeTagName(tag.substring(prefix.length)); + } + return tag[0] === "!" ? tag : `!<${tag}>`; + } + toString(doc) { + const lines = this.yaml.explicit ? [`%YAML ${this.yaml.version || "1.2"}`] : []; + const tagEntries = Object.entries(this.tags); + let tagNames; + if (doc && tagEntries.length > 0 && isNode(doc.contents)) { + const tags = {}; + visit(doc.contents, (_key, node) => { + if (isNode(node) && node.tag) + tags[node.tag] = true; + }); + tagNames = Object.keys(tags); + } else + tagNames = []; + for (const [handle, prefix] of tagEntries) { + if (handle === "!!" && prefix === "tag:yaml.org,2002:") + continue; + if (!doc || tagNames.some((tn) => tn.startsWith(prefix))) + lines.push(`%TAG ${handle} ${prefix}`); + } + return lines.join("\n"); + } +}; +Directives.defaultYaml = { explicit: false, version: "1.2" }; +Directives.defaultTags = { "!!": "tag:yaml.org,2002:" }; + +// node_modules/yaml/browser/dist/doc/anchors.js +function anchorIsValid(anchor) { + if (/[\x00-\x19\s,[\]{}]/.test(anchor)) { + const sa = JSON.stringify(anchor); + const msg = `Anchor must not contain whitespace or control characters: ${sa}`; + throw new Error(msg); + } + return true; +} +function anchorNames(root) { + const anchors = /* @__PURE__ */ new Set(); + visit(root, { + Value(_key, node) { + if (node.anchor) + anchors.add(node.anchor); + } + }); + return anchors; +} +function findNewAnchor(prefix, exclude) { + for (let i = 1; true; ++i) { + const name = `${prefix}${i}`; + if (!exclude.has(name)) + return name; + } +} +function createNodeAnchors(doc, prefix) { + const aliasObjects = []; + const sourceObjects = /* @__PURE__ */ new Map(); + let prevAnchors = null; + return { + onAnchor: (source) => { + aliasObjects.push(source); + prevAnchors ?? (prevAnchors = anchorNames(doc)); + const anchor = findNewAnchor(prefix, prevAnchors); + prevAnchors.add(anchor); + return anchor; + }, + /** + * With circular references, the source node is only resolved after all + * of its child nodes are. This is why anchors are set only after all of + * the nodes have been created. + */ + setAnchors: () => { + for (const source of aliasObjects) { + const ref = sourceObjects.get(source); + if (typeof ref === "object" && ref.anchor && (isScalar(ref.node) || isCollection(ref.node))) { + ref.node.anchor = ref.anchor; + } else { + const error = new Error("Failed to resolve repeated object (this should not happen)"); + error.source = source; + throw error; + } + } + }, + sourceObjects + }; +} + +// node_modules/yaml/browser/dist/doc/applyReviver.js +function applyReviver(reviver, obj, key, val) { + if (val && typeof val === "object") { + if (Array.isArray(val)) { + for (let i = 0, len = val.length; i < len; ++i) { + const v0 = val[i]; + const v1 = applyReviver(reviver, val, String(i), v0); + if (v1 === void 0) + delete val[i]; + else if (v1 !== v0) + val[i] = v1; + } + } else if (val instanceof Map) { + for (const k of Array.from(val.keys())) { + const v0 = val.get(k); + const v1 = applyReviver(reviver, val, k, v0); + if (v1 === void 0) + val.delete(k); + else if (v1 !== v0) + val.set(k, v1); + } + } else if (val instanceof Set) { + for (const v0 of Array.from(val)) { + const v1 = applyReviver(reviver, val, v0, v0); + if (v1 === void 0) + val.delete(v0); + else if (v1 !== v0) { + val.delete(v0); + val.add(v1); + } + } + } else { + for (const [k, v0] of Object.entries(val)) { + const v1 = applyReviver(reviver, val, k, v0); + if (v1 === void 0) + delete val[k]; + else if (v1 !== v0) + val[k] = v1; + } + } + } + return reviver.call(obj, key, val); +} + +// node_modules/yaml/browser/dist/nodes/toJS.js +function toJS(value, arg, ctx) { + if (Array.isArray(value)) + return value.map((v, i) => toJS(v, String(i), ctx)); + if (value && typeof value.toJSON === "function") { + if (!ctx || !hasAnchor(value)) + return value.toJSON(arg, ctx); + const data = { aliasCount: 0, count: 1, res: void 0 }; + ctx.anchors.set(value, data); + ctx.onCreate = (res2) => { + data.res = res2; + delete ctx.onCreate; + }; + const res = value.toJSON(arg, ctx); + if (ctx.onCreate) + ctx.onCreate(res); + return res; + } + if (typeof value === "bigint" && !ctx?.keep) + return Number(value); + return value; +} + +// node_modules/yaml/browser/dist/nodes/Node.js +var NodeBase = class { + constructor(type) { + Object.defineProperty(this, NODE_TYPE, { value: type }); + } + /** Create a copy of this node. */ + clone() { + const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** A plain JavaScript representation of this node. */ + toJS(doc, { mapAsMap, maxAliasCount, onAnchor, reviver } = {}) { + if (!isDocument(doc)) + throw new TypeError("A document argument is required"); + const ctx = { + anchors: /* @__PURE__ */ new Map(), + doc, + keep: true, + mapAsMap: mapAsMap === true, + mapKeyWarned: false, + maxAliasCount: typeof maxAliasCount === "number" ? maxAliasCount : 100 + }; + const res = toJS(this, "", ctx); + if (typeof onAnchor === "function") + for (const { count, res: res2 } of ctx.anchors.values()) + onAnchor(res2, count); + return typeof reviver === "function" ? applyReviver(reviver, { "": res }, "", res) : res; + } +}; + +// node_modules/yaml/browser/dist/nodes/Alias.js +var Alias = class extends NodeBase { + constructor(source) { + super(ALIAS); + this.source = source; + Object.defineProperty(this, "tag", { + set() { + throw new Error("Alias nodes cannot have tags"); + } + }); + } + /** + * Resolve the value of this alias within `doc`, finding the last + * instance of the `source` anchor before this node. + */ + resolve(doc, ctx) { + if (ctx?.maxAliasCount === 0) + throw new ReferenceError("Alias resolution is disabled"); + let nodes; + if (ctx?.aliasResolveCache) { + nodes = ctx.aliasResolveCache; + } else { + nodes = []; + visit(doc, { + Node: (_key, node) => { + if (isAlias(node) || hasAnchor(node)) + nodes.push(node); + } + }); + if (ctx) + ctx.aliasResolveCache = nodes; + } + let found = void 0; + for (const node of nodes) { + if (node === this) + break; + if (node.anchor === this.source) + found = node; + } + if (found && ctx) { + const { anchors, doc: doc2, maxAliasCount } = ctx; + let data = anchors.get(found); + if (!data) { + toJS(found, null, ctx); + data = anchors.get(found); + } + if (data?.res === void 0) { + const msg = "This should not happen: Alias anchor was not resolved?"; + throw new ReferenceError(msg); + } + if (maxAliasCount >= 0) { + data.count += 1; + if (data.aliasCount === 0) + data.aliasCount = getAliasCount(doc2, found, anchors); + if (data.count * data.aliasCount > maxAliasCount) { + const msg = "Excessive alias count indicates a resource exhaustion attack"; + throw new ReferenceError(msg); + } + } + } + return found; + } + toJSON(_arg, ctx) { + if (!ctx) + return { source: this.source }; + const source = this.resolve(ctx.doc, ctx); + if (!source) { + const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`; + throw new ReferenceError(msg); + } + return ctx.anchors.get(source).res; + } + toString(ctx, _onComment, _onChompKeep) { + const src = `*${this.source}`; + if (ctx) { + anchorIsValid(this.source); + if (ctx.options.verifyAliasOrder && !ctx.anchors.has(this.source)) { + const msg = `Unresolved alias (the anchor must be set before the alias): ${this.source}`; + throw new Error(msg); + } + if (ctx.implicitKey) + return `${src} `; + } + return src; + } +}; +function getAliasCount(doc, node, anchors) { + if (isAlias(node)) { + const source = node.resolve(doc); + const anchor = anchors && source && anchors.get(source); + return anchor ? anchor.count * anchor.aliasCount : 0; + } else if (isCollection(node)) { + let count = 0; + for (const item of node.items) { + const c = getAliasCount(doc, item, anchors); + if (c > count) + count = c; + } + return count; + } else if (isPair(node)) { + const kc = getAliasCount(doc, node.key, anchors); + const vc = getAliasCount(doc, node.value, anchors); + return Math.max(kc, vc); + } + return 1; +} + +// node_modules/yaml/browser/dist/nodes/Scalar.js +var isScalarValue = (value) => !value || typeof value !== "function" && typeof value !== "object"; +var Scalar = class extends NodeBase { + constructor(value) { + super(SCALAR); + this.value = value; + } + toJSON(arg, ctx) { + return ctx?.keep ? this.value : toJS(this.value, arg, ctx); + } + toString() { + return String(this.value); + } +}; +Scalar.BLOCK_FOLDED = "BLOCK_FOLDED"; +Scalar.BLOCK_LITERAL = "BLOCK_LITERAL"; +Scalar.PLAIN = "PLAIN"; +Scalar.QUOTE_DOUBLE = "QUOTE_DOUBLE"; +Scalar.QUOTE_SINGLE = "QUOTE_SINGLE"; + +// node_modules/yaml/browser/dist/doc/createNode.js +var defaultTagPrefix = "tag:yaml.org,2002:"; +function findTagObject(value, tagName, tags) { + if (tagName) { + const match = tags.filter((t) => t.tag === tagName); + const tagObj = match.find((t) => !t.format) ?? match[0]; + if (!tagObj) + throw new Error(`Tag ${tagName} not found`); + return tagObj; + } + return tags.find((t) => t.identify?.(value) && !t.format); +} +function createNode(value, tagName, ctx) { + if (isDocument(value)) + value = value.contents; + if (isNode(value)) + return value; + if (isPair(value)) { + const map2 = ctx.schema[MAP].createNode?.(ctx.schema, null, ctx); + map2.items.push(value); + return map2; + } + if (value instanceof String || value instanceof Number || value instanceof Boolean || typeof BigInt !== "undefined" && value instanceof BigInt) { + value = value.valueOf(); + } + const { aliasDuplicateObjects, onAnchor, onTagObj, schema: schema4, sourceObjects } = ctx; + let ref = void 0; + if (aliasDuplicateObjects && value && typeof value === "object") { + ref = sourceObjects.get(value); + if (ref) { + ref.anchor ?? (ref.anchor = onAnchor(value)); + return new Alias(ref.anchor); + } else { + ref = { anchor: null, node: null }; + sourceObjects.set(value, ref); + } + } + if (tagName?.startsWith("!!")) + tagName = defaultTagPrefix + tagName.slice(2); + let tagObj = findTagObject(value, tagName, schema4.tags); + if (!tagObj) { + if (value && typeof value.toJSON === "function") { + value = value.toJSON(); + } + if (!value || typeof value !== "object") { + const node2 = new Scalar(value); + if (ref) + ref.node = node2; + return node2; + } + tagObj = value instanceof Map ? schema4[MAP] : Symbol.iterator in Object(value) ? schema4[SEQ] : schema4[MAP]; + } + if (onTagObj) { + onTagObj(tagObj); + delete ctx.onTagObj; + } + const node = tagObj?.createNode ? tagObj.createNode(ctx.schema, value, ctx) : typeof tagObj?.nodeClass?.from === "function" ? tagObj.nodeClass.from(ctx.schema, value, ctx) : new Scalar(value); + if (tagName) + node.tag = tagName; + else if (!tagObj.default) + node.tag = tagObj.tag; + if (ref) + ref.node = node; + return node; +} + +// node_modules/yaml/browser/dist/nodes/Collection.js +function collectionFromPath(schema4, path, value) { + let v = value; + for (let i = path.length - 1; i >= 0; --i) { + const k = path[i]; + if (typeof k === "number" && Number.isInteger(k) && k >= 0) { + const a = []; + a[k] = v; + v = a; + } else { + v = /* @__PURE__ */ new Map([[k, v]]); + } + } + return createNode(v, void 0, { + aliasDuplicateObjects: false, + keepUndefined: false, + onAnchor: () => { + throw new Error("This should not happen, please report a bug."); + }, + schema: schema4, + sourceObjects: /* @__PURE__ */ new Map() + }); +} +var isEmptyPath = (path) => path == null || typeof path === "object" && !!path[Symbol.iterator]().next().done; +var Collection = class extends NodeBase { + constructor(type, schema4) { + super(type); + Object.defineProperty(this, "schema", { + value: schema4, + configurable: true, + enumerable: false, + writable: true + }); + } + /** + * Create a copy of this collection. + * + * @param schema - If defined, overwrites the original's schema + */ + clone(schema4) { + const copy = Object.create(Object.getPrototypeOf(this), Object.getOwnPropertyDescriptors(this)); + if (schema4) + copy.schema = schema4; + copy.items = copy.items.map((it) => isNode(it) || isPair(it) ? it.clone(schema4) : it); + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** + * Adds a value to the collection. For `!!map` and `!!omap` the value must + * be a Pair instance or a `{ key, value }` object, which may not have a key + * that already exists in the map. + */ + addIn(path, value) { + if (isEmptyPath(path)) + this.add(value); + else { + const [key, ...rest] = path; + const node = this.get(key, true); + if (isCollection(node)) + node.addIn(rest, value); + else if (node === void 0 && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + } + /** + * Removes a value from the collection. + * @returns `true` if the item was found and removed. + */ + deleteIn(path) { + const [key, ...rest] = path; + if (rest.length === 0) + return this.delete(key); + const node = this.get(key, true); + if (isCollection(node)) + return node.deleteIn(rest); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path, keepScalar) { + const [key, ...rest] = path; + const node = this.get(key, true); + if (rest.length === 0) + return !keepScalar && isScalar(node) ? node.value : node; + else + return isCollection(node) ? node.getIn(rest, keepScalar) : void 0; + } + hasAllNullValues(allowScalar) { + return this.items.every((node) => { + if (!isPair(node)) + return false; + const n = node.value; + return n == null || allowScalar && isScalar(n) && n.value == null && !n.commentBefore && !n.comment && !n.tag; + }); + } + /** + * Checks if the collection includes a value with the key `key`. + */ + hasIn(path) { + const [key, ...rest] = path; + if (rest.length === 0) + return this.has(key); + const node = this.get(key, true); + return isCollection(node) ? node.hasIn(rest) : false; + } + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path, value) { + const [key, ...rest] = path; + if (rest.length === 0) { + this.set(key, value); + } else { + const node = this.get(key, true); + if (isCollection(node)) + node.setIn(rest, value); + else if (node === void 0 && this.schema) + this.set(key, collectionFromPath(this.schema, rest, value)); + else + throw new Error(`Expected YAML collection at ${key}. Remaining path: ${rest}`); + } + } +}; + +// node_modules/yaml/browser/dist/stringify/stringifyComment.js +var stringifyComment = (str) => str.replace(/^(?!$)(?: $)?/gm, "#"); +function indentComment(comment, indent) { + if (/^\n+$/.test(comment)) + return comment.substring(1); + return indent ? comment.replace(/^(?! *$)/gm, indent) : comment; +} +var lineComment = (str, indent, comment) => str.endsWith("\n") ? indentComment(comment, indent) : comment.includes("\n") ? "\n" + indentComment(comment, indent) : (str.endsWith(" ") ? "" : " ") + comment; + +// node_modules/yaml/browser/dist/stringify/foldFlowLines.js +var FOLD_FLOW = "flow"; +var FOLD_BLOCK = "block"; +var FOLD_QUOTED = "quoted"; +function foldFlowLines(text, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) { + if (!lineWidth || lineWidth < 0) + return text; + if (lineWidth < minContentWidth) + minContentWidth = 0; + const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length); + if (text.length <= endStep) + return text; + const folds = []; + const escapedFolds = {}; + let end = lineWidth - indent.length; + if (typeof indentAtStart === "number") { + if (indentAtStart > lineWidth - Math.max(2, minContentWidth)) + folds.push(0); + else + end = lineWidth - indentAtStart; + } + let split = void 0; + let prev = void 0; + let overflow = false; + let i = -1; + let escStart = -1; + let escEnd = -1; + if (mode === FOLD_BLOCK) { + i = consumeMoreIndentedLines(text, i, indent.length); + if (i !== -1) + end = i + endStep; + } + for (let ch; ch = text[i += 1]; ) { + if (mode === FOLD_QUOTED && ch === "\\") { + escStart = i; + switch (text[i + 1]) { + case "x": + i += 3; + break; + case "u": + i += 5; + break; + case "U": + i += 9; + break; + default: + i += 1; + } + escEnd = i; + } + if (ch === "\n") { + if (mode === FOLD_BLOCK) + i = consumeMoreIndentedLines(text, i, indent.length); + end = i + indent.length + endStep; + split = void 0; + } else { + if (ch === " " && prev && prev !== " " && prev !== "\n" && prev !== " ") { + const next = text[i + 1]; + if (next && next !== " " && next !== "\n" && next !== " ") + split = i; + } + if (i >= end) { + if (split) { + folds.push(split); + end = split + endStep; + split = void 0; + } else if (mode === FOLD_QUOTED) { + while (prev === " " || prev === " ") { + prev = ch; + ch = text[i += 1]; + overflow = true; + } + const j = i > escEnd + 1 ? i - 2 : escStart - 1; + if (escapedFolds[j]) + return text; + folds.push(j); + escapedFolds[j] = true; + end = j + endStep; + split = void 0; + } else { + overflow = true; + } + } + } + prev = ch; + } + if (overflow && onOverflow) + onOverflow(); + if (folds.length === 0) + return text; + if (onFold) + onFold(); + let res = text.slice(0, folds[0]); + for (let i2 = 0; i2 < folds.length; ++i2) { + const fold = folds[i2]; + const end2 = folds[i2 + 1] || text.length; + if (fold === 0) + res = ` +${indent}${text.slice(0, end2)}`; + else { + if (mode === FOLD_QUOTED && escapedFolds[fold]) + res += `${text[fold]}\\`; + res += ` +${indent}${text.slice(fold + 1, end2)}`; + } + } + return res; +} +function consumeMoreIndentedLines(text, i, indent) { + let end = i; + let start = i + 1; + let ch = text[start]; + while (ch === " " || ch === " ") { + if (i < start + indent) { + ch = text[++i]; + } else { + do { + ch = text[++i]; + } while (ch && ch !== "\n"); + end = i; + start = i + 1; + ch = text[start]; + } + } + return end; +} + +// node_modules/yaml/browser/dist/stringify/stringifyString.js +var getFoldOptions = (ctx, isBlock2) => ({ + indentAtStart: isBlock2 ? ctx.indent.length : ctx.indentAtStart, + lineWidth: ctx.options.lineWidth, + minContentWidth: ctx.options.minContentWidth +}); +var containsDocumentMarker = (str) => /^(%|---|\.\.\.)/m.test(str); +function lineLengthOverLimit(str, lineWidth, indentLength) { + if (!lineWidth || lineWidth < 0) + return false; + const limit = lineWidth - indentLength; + const strLen = str.length; + if (strLen <= limit) + return false; + for (let i = 0, start = 0; i < strLen; ++i) { + if (str[i] === "\n") { + if (i - start > limit) + return true; + start = i + 1; + if (strLen - start <= limit) + return false; + } + } + return true; +} +function doubleQuotedString(value, ctx) { + const json = JSON.stringify(value); + if (ctx.options.doubleQuotedAsJSON) + return json; + const { implicitKey } = ctx; + const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength; + const indent = ctx.indent || (containsDocumentMarker(value) ? " " : ""); + let str = ""; + let start = 0; + for (let i = 0, ch = json[i]; ch; ch = json[++i]) { + if (ch === " " && json[i + 1] === "\\" && json[i + 2] === "n") { + str += json.slice(start, i) + "\\ "; + i += 1; + start = i; + ch = "\\"; + } + if (ch === "\\") + switch (json[i + 1]) { + case "u": + { + str += json.slice(start, i); + const code = json.substr(i + 2, 4); + switch (code) { + case "0000": + str += "\\0"; + break; + case "0007": + str += "\\a"; + break; + case "000b": + str += "\\v"; + break; + case "001b": + str += "\\e"; + break; + case "0085": + str += "\\N"; + break; + case "00a0": + str += "\\_"; + break; + case "2028": + str += "\\L"; + break; + case "2029": + str += "\\P"; + break; + default: + if (code.substr(0, 2) === "00") + str += "\\x" + code.substr(2); + else + str += json.substr(i, 6); + } + i += 5; + start = i + 1; + } + break; + case "n": + if (implicitKey || json[i + 2] === '"' || json.length < minMultiLineLength) { + i += 1; + } else { + str += json.slice(start, i) + "\n\n"; + while (json[i + 2] === "\\" && json[i + 3] === "n" && json[i + 4] !== '"') { + str += "\n"; + i += 2; + } + str += indent; + if (json[i + 2] === " ") + str += "\\"; + i += 1; + start = i + 1; + } + break; + default: + i += 1; + } + } + str = start ? str + json.slice(start) : json; + return implicitKey ? str : foldFlowLines(str, indent, FOLD_QUOTED, getFoldOptions(ctx, false)); +} +function singleQuotedString(value, ctx) { + if (ctx.options.singleQuote === false || ctx.implicitKey && value.includes("\n") || /[ \t]\n|\n[ \t]/.test(value)) + return doubleQuotedString(value, ctx); + const indent = ctx.indent || (containsDocumentMarker(value) ? " " : ""); + const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$& +${indent}`) + "'"; + return ctx.implicitKey ? res : foldFlowLines(res, indent, FOLD_FLOW, getFoldOptions(ctx, false)); +} +function quotedString(value, ctx) { + const { singleQuote } = ctx.options; + let qs; + if (singleQuote === false) + qs = doubleQuotedString; + else { + const hasDouble = value.includes('"'); + const hasSingle = value.includes("'"); + if (hasDouble && !hasSingle) + qs = singleQuotedString; + else if (hasSingle && !hasDouble) + qs = doubleQuotedString; + else + qs = singleQuote ? singleQuotedString : doubleQuotedString; + } + return qs(value, ctx); +} +var blockEndNewlines; +try { + blockEndNewlines = new RegExp("(^|(?\n"; + let chomp; + let endStart; + for (endStart = value.length; endStart > 0; --endStart) { + const ch = value[endStart - 1]; + if (ch !== "\n" && ch !== " " && ch !== " ") + break; + } + let end = value.substring(endStart); + const endNlPos = end.indexOf("\n"); + if (endNlPos === -1) { + chomp = "-"; + } else if (value === end || endNlPos !== end.length - 1) { + chomp = "+"; + if (onChompKeep) + onChompKeep(); + } else { + chomp = ""; + } + if (end) { + value = value.slice(0, -end.length); + if (end[end.length - 1] === "\n") + end = end.slice(0, -1); + end = end.replace(blockEndNewlines, `$&${indent}`); + } + let startWithSpace = false; + let startEnd; + let startNlPos = -1; + for (startEnd = 0; startEnd < value.length; ++startEnd) { + const ch = value[startEnd]; + if (ch === " ") + startWithSpace = true; + else if (ch === "\n") + startNlPos = startEnd; + else + break; + } + let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd); + if (start) { + value = value.substring(start.length); + start = start.replace(/\n+/g, `$&${indent}`); + } + const indentSize = indent ? "2" : "1"; + let header = (startWithSpace ? indentSize : "") + chomp; + if (comment) { + header += " " + commentString(comment.replace(/ ?[\r\n]+/g, " ")); + if (onComment) + onComment(); + } + if (!literal) { + const foldedValue = value.replace(/\n+/g, "\n$&").replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, "$1$2").replace(/\n+/g, `$&${indent}`); + let literalFallback = false; + const foldOptions = getFoldOptions(ctx, true); + if (blockQuote !== "folded" && type !== Scalar.BLOCK_FOLDED) { + foldOptions.onOverflow = () => { + literalFallback = true; + }; + } + const body = foldFlowLines(`${start}${foldedValue}${end}`, indent, FOLD_BLOCK, foldOptions); + if (!literalFallback) + return `>${header} +${indent}${body}`; + } + value = value.replace(/\n+/g, `$&${indent}`); + return `|${header} +${indent}${start}${value}${end}`; +} +function plainString(item, ctx, onComment, onChompKeep) { + const { type, value } = item; + const { actualString, implicitKey, indent, indentStep, inFlow } = ctx; + if (implicitKey && value.includes("\n") || inFlow && /[[\]{},]/.test(value)) { + return quotedString(value, ctx); + } + if (/^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) { + return implicitKey || inFlow || !value.includes("\n") ? quotedString(value, ctx) : blockString(item, ctx, onComment, onChompKeep); + } + if (!implicitKey && !inFlow && type !== Scalar.PLAIN && value.includes("\n")) { + return blockString(item, ctx, onComment, onChompKeep); + } + if (containsDocumentMarker(value)) { + if (indent === "") { + ctx.forceBlockIndent = true; + return blockString(item, ctx, onComment, onChompKeep); + } else if (implicitKey && indent === indentStep) { + return quotedString(value, ctx); + } + } + const str = value.replace(/\n+/g, `$& +${indent}`); + if (actualString) { + const test = (tag) => tag.default && tag.tag !== "tag:yaml.org,2002:str" && tag.test?.test(str); + const { compat, tags } = ctx.doc.schema; + if (tags.some(test) || compat?.some(test)) + return quotedString(value, ctx); + } + return implicitKey ? str : foldFlowLines(str, indent, FOLD_FLOW, getFoldOptions(ctx, false)); +} +function stringifyString(item, ctx, onComment, onChompKeep) { + const { implicitKey, inFlow } = ctx; + const ss = typeof item.value === "string" ? item : Object.assign({}, item, { value: String(item.value) }); + let { type } = item; + if (type !== Scalar.QUOTE_DOUBLE) { + if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value)) + type = Scalar.QUOTE_DOUBLE; + } + const _stringify = (_type) => { + switch (_type) { + case Scalar.BLOCK_FOLDED: + case Scalar.BLOCK_LITERAL: + return implicitKey || inFlow ? quotedString(ss.value, ctx) : blockString(ss, ctx, onComment, onChompKeep); + case Scalar.QUOTE_DOUBLE: + return doubleQuotedString(ss.value, ctx); + case Scalar.QUOTE_SINGLE: + return singleQuotedString(ss.value, ctx); + case Scalar.PLAIN: + return plainString(ss, ctx, onComment, onChompKeep); + default: + return null; + } + }; + let res = _stringify(type); + if (res === null) { + const { defaultKeyType, defaultStringType } = ctx.options; + const t = implicitKey && defaultKeyType || defaultStringType; + res = _stringify(t); + if (res === null) + throw new Error(`Unsupported default string type ${t}`); + } + return res; +} + +// node_modules/yaml/browser/dist/stringify/stringify.js +function createStringifyContext(doc, options) { + const opt = Object.assign({ + blockQuote: true, + commentString: stringifyComment, + defaultKeyType: null, + defaultStringType: "PLAIN", + directives: null, + doubleQuotedAsJSON: false, + doubleQuotedMinMultiLineLength: 40, + falseStr: "false", + flowCollectionPadding: true, + indentSeq: true, + lineWidth: 80, + minContentWidth: 20, + nullStr: "null", + simpleKeys: false, + singleQuote: null, + trailingComma: false, + trueStr: "true", + verifyAliasOrder: true + }, doc.schema.toStringOptions, options); + let inFlow; + switch (opt.collectionStyle) { + case "block": + inFlow = false; + break; + case "flow": + inFlow = true; + break; + default: + inFlow = null; + } + return { + anchors: /* @__PURE__ */ new Set(), + doc, + flowCollectionPadding: opt.flowCollectionPadding ? " " : "", + indent: "", + indentStep: typeof opt.indent === "number" ? " ".repeat(opt.indent) : " ", + inFlow, + options: opt + }; +} +function getTagObject(tags, item) { + if (item.tag) { + const match = tags.filter((t) => t.tag === item.tag); + if (match.length > 0) + return match.find((t) => t.format === item.format) ?? match[0]; + } + let tagObj = void 0; + let obj; + if (isScalar(item)) { + obj = item.value; + let match = tags.filter((t) => t.identify?.(obj)); + if (match.length > 1) { + const testMatch = match.filter((t) => t.test); + if (testMatch.length > 0) + match = testMatch; + } + tagObj = match.find((t) => t.format === item.format) ?? match.find((t) => !t.format); + } else { + obj = item; + tagObj = tags.find((t) => t.nodeClass && obj instanceof t.nodeClass); + } + if (!tagObj) { + const name = obj?.constructor?.name ?? (obj === null ? "null" : typeof obj); + throw new Error(`Tag not resolved for ${name} value`); + } + return tagObj; +} +function stringifyProps(node, tagObj, { anchors, doc }) { + if (!doc.directives) + return ""; + const props = []; + const anchor = (isScalar(node) || isCollection(node)) && node.anchor; + if (anchor && anchorIsValid(anchor)) { + anchors.add(anchor); + props.push(`&${anchor}`); + } + const tag = node.tag ?? (tagObj.default ? null : tagObj.tag); + if (tag) + props.push(doc.directives.tagString(tag)); + return props.join(" "); +} +function stringify(item, ctx, onComment, onChompKeep) { + if (isPair(item)) + return item.toString(ctx, onComment, onChompKeep); + if (isAlias(item)) { + if (ctx.doc.directives) + return item.toString(ctx); + if (ctx.resolvedAliases?.has(item)) { + throw new TypeError(`Cannot stringify circular structure without alias nodes`); + } else { + if (ctx.resolvedAliases) + ctx.resolvedAliases.add(item); + else + ctx.resolvedAliases = /* @__PURE__ */ new Set([item]); + item = item.resolve(ctx.doc); + } + } + let tagObj = void 0; + const node = isNode(item) ? item : ctx.doc.createNode(item, { onTagObj: (o) => tagObj = o }); + tagObj ?? (tagObj = getTagObject(ctx.doc.schema.tags, node)); + const props = stringifyProps(node, tagObj, ctx); + if (props.length > 0) + ctx.indentAtStart = (ctx.indentAtStart ?? 0) + props.length + 1; + const str = typeof tagObj.stringify === "function" ? tagObj.stringify(node, ctx, onComment, onChompKeep) : isScalar(node) ? stringifyString(node, ctx, onComment, onChompKeep) : node.toString(ctx, onComment, onChompKeep); + if (!props) + return str; + return isScalar(node) || str[0] === "{" || str[0] === "[" ? `${props} ${str}` : `${props} +${ctx.indent}${str}`; +} + +// node_modules/yaml/browser/dist/stringify/stringifyPair.js +function stringifyPair({ key, value }, ctx, onComment, onChompKeep) { + const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx; + let keyComment = isNode(key) && key.comment || null; + if (simpleKeys) { + if (keyComment) { + throw new Error("With simple keys, key nodes cannot have comments"); + } + if (isCollection(key) || !isNode(key) && typeof key === "object") { + const msg = "With simple keys, collection cannot be used as a key value"; + throw new Error(msg); + } + } + let explicitKey = !simpleKeys && (!key || keyComment && value == null && !ctx.inFlow || isCollection(key) || (isScalar(key) ? key.type === Scalar.BLOCK_FOLDED || key.type === Scalar.BLOCK_LITERAL : typeof key === "object")); + ctx = Object.assign({}, ctx, { + allNullValues: false, + implicitKey: !explicitKey && (simpleKeys || !allNullValues), + indent: indent + indentStep + }); + let keyCommentDone = false; + let chompKeep = false; + let str = stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true); + if (!explicitKey && !ctx.inFlow && str.length > 1024) { + if (simpleKeys) + throw new Error("With simple keys, single line scalar must not span more than 1024 characters"); + explicitKey = true; + } + if (ctx.inFlow) { + if (allNullValues || value == null) { + if (keyCommentDone && onComment) + onComment(); + return str === "" ? "?" : explicitKey ? `? ${str}` : str; + } + } else if (allNullValues && !simpleKeys || value == null && explicitKey) { + str = `? ${str}`; + if (keyComment && !keyCommentDone) { + str += lineComment(str, ctx.indent, commentString(keyComment)); + } else if (chompKeep && onChompKeep) + onChompKeep(); + return str; + } + if (keyCommentDone) + keyComment = null; + if (explicitKey) { + if (keyComment) + str += lineComment(str, ctx.indent, commentString(keyComment)); + str = `? ${str} +${indent}:`; + } else { + str = `${str}:`; + if (keyComment) + str += lineComment(str, ctx.indent, commentString(keyComment)); + } + let vsb, vcb, valueComment; + if (isNode(value)) { + vsb = !!value.spaceBefore; + vcb = value.commentBefore; + valueComment = value.comment; + } else { + vsb = false; + vcb = null; + valueComment = null; + if (value && typeof value === "object") + value = doc.createNode(value); + } + ctx.implicitKey = false; + if (!explicitKey && !keyComment && isScalar(value)) + ctx.indentAtStart = str.length + 1; + chompKeep = false; + if (!indentSeq && indentStep.length >= 2 && !ctx.inFlow && !explicitKey && isSeq(value) && !value.flow && !value.tag && !value.anchor) { + ctx.indent = ctx.indent.substring(2); + } + let valueCommentDone = false; + const valueStr = stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true); + let ws = " "; + if (keyComment || vsb || vcb) { + ws = vsb ? "\n" : ""; + if (vcb) { + const cs = commentString(vcb); + ws += ` +${indentComment(cs, ctx.indent)}`; + } + if (valueStr === "" && !ctx.inFlow) { + if (ws === "\n" && valueComment) + ws = "\n\n"; + } else { + ws += ` +${ctx.indent}`; + } + } else if (!explicitKey && isCollection(value)) { + const vs0 = valueStr[0]; + const nl0 = valueStr.indexOf("\n"); + const hasNewline = nl0 !== -1; + const flow = ctx.inFlow ?? value.flow ?? value.items.length === 0; + if (hasNewline || !flow) { + let hasPropsLine = false; + if (hasNewline && (vs0 === "&" || vs0 === "!")) { + let sp0 = valueStr.indexOf(" "); + if (vs0 === "&" && sp0 !== -1 && sp0 < nl0 && valueStr[sp0 + 1] === "!") { + sp0 = valueStr.indexOf(" ", sp0 + 1); + } + if (sp0 === -1 || nl0 < sp0) + hasPropsLine = true; + } + if (!hasPropsLine) + ws = ` +${ctx.indent}`; + } + } else if (valueStr === "" || valueStr[0] === "\n") { + ws = ""; + } + str += ws + valueStr; + if (ctx.inFlow) { + if (valueCommentDone && onComment) + onComment(); + } else if (valueComment && !valueCommentDone) { + str += lineComment(str, ctx.indent, commentString(valueComment)); + } else if (chompKeep && onChompKeep) { + onChompKeep(); + } + return str; +} + +// node_modules/yaml/browser/dist/log.js +function warn(logLevel, warning) { + if (logLevel === "debug" || logLevel === "warn") { + console.warn(warning); + } +} + +// node_modules/yaml/browser/dist/schema/yaml-1.1/merge.js +var MERGE_KEY = "<<"; +var merge = { + identify: (value) => value === MERGE_KEY || typeof value === "symbol" && value.description === MERGE_KEY, + default: "key", + tag: "tag:yaml.org,2002:merge", + test: /^<<$/, + resolve: () => Object.assign(new Scalar(Symbol(MERGE_KEY)), { + addToJSMap: addMergeToJSMap + }), + stringify: () => MERGE_KEY +}; +var isMergeKey = (ctx, key) => (merge.identify(key) || isScalar(key) && (!key.type || key.type === Scalar.PLAIN) && merge.identify(key.value)) && ctx?.doc.schema.tags.some((tag) => tag.tag === merge.tag && tag.default); +function addMergeToJSMap(ctx, map2, value) { + const source = resolveAliasValue(ctx, value); + if (isSeq(source)) + for (const it of source.items) + mergeValue(ctx, map2, it); + else if (Array.isArray(source)) + for (const it of source) + mergeValue(ctx, map2, it); + else + mergeValue(ctx, map2, source); +} +function mergeValue(ctx, map2, value) { + const source = resolveAliasValue(ctx, value); + if (!isMap(source)) + throw new Error("Merge sources must be maps or map aliases"); + const srcMap = source.toJSON(null, ctx, Map); + for (const [key, value2] of srcMap) { + if (map2 instanceof Map) { + if (!map2.has(key)) + map2.set(key, value2); + } else if (map2 instanceof Set) { + map2.add(key); + } else if (!Object.prototype.hasOwnProperty.call(map2, key)) { + Object.defineProperty(map2, key, { + value: value2, + writable: true, + enumerable: true, + configurable: true + }); + } + } + return map2; +} +function resolveAliasValue(ctx, value) { + return ctx && isAlias(value) ? value.resolve(ctx.doc, ctx) : value; +} + +// node_modules/yaml/browser/dist/nodes/addPairToJSMap.js +function addPairToJSMap(ctx, map2, { key, value }) { + if (isNode(key) && key.addToJSMap) + key.addToJSMap(ctx, map2, value); + else if (isMergeKey(ctx, key)) + addMergeToJSMap(ctx, map2, value); + else { + const jsKey = toJS(key, "", ctx); + if (map2 instanceof Map) { + map2.set(jsKey, toJS(value, jsKey, ctx)); + } else if (map2 instanceof Set) { + map2.add(jsKey); + } else { + const stringKey = stringifyKey(key, jsKey, ctx); + const jsValue = toJS(value, stringKey, ctx); + if (stringKey in map2) + Object.defineProperty(map2, stringKey, { + value: jsValue, + writable: true, + enumerable: true, + configurable: true + }); + else + map2[stringKey] = jsValue; + } + } + return map2; +} +function stringifyKey(key, jsKey, ctx) { + if (jsKey === null) + return ""; + if (typeof jsKey !== "object") + return String(jsKey); + if (isNode(key) && ctx?.doc) { + const strCtx = createStringifyContext(ctx.doc, {}); + strCtx.anchors = /* @__PURE__ */ new Set(); + for (const node of ctx.anchors.keys()) + strCtx.anchors.add(node.anchor); + strCtx.inFlow = true; + strCtx.inStringifyKey = true; + const strKey = key.toString(strCtx); + if (!ctx.mapKeyWarned) { + let jsonStr = JSON.stringify(strKey); + if (jsonStr.length > 40) + jsonStr = jsonStr.substring(0, 36) + '..."'; + warn(ctx.doc.options.logLevel, `Keys with collection values will be stringified due to JS Object restrictions: ${jsonStr}. Set mapAsMap: true to use object keys.`); + ctx.mapKeyWarned = true; + } + return strKey; + } + return JSON.stringify(jsKey); +} + +// node_modules/yaml/browser/dist/nodes/Pair.js +function createPair(key, value, ctx) { + const k = createNode(key, void 0, ctx); + const v = createNode(value, void 0, ctx); + return new Pair(k, v); +} +var Pair = class _Pair { + constructor(key, value = null) { + Object.defineProperty(this, NODE_TYPE, { value: PAIR }); + this.key = key; + this.value = value; + } + clone(schema4) { + let { key, value } = this; + if (isNode(key)) + key = key.clone(schema4); + if (isNode(value)) + value = value.clone(schema4); + return new _Pair(key, value); + } + toJSON(_, ctx) { + const pair = ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {}; + return addPairToJSMap(ctx, pair, this); + } + toString(ctx, onComment, onChompKeep) { + return ctx?.doc ? stringifyPair(this, ctx, onComment, onChompKeep) : JSON.stringify(this); + } +}; + +// node_modules/yaml/browser/dist/stringify/stringifyCollection.js +function stringifyCollection(collection, ctx, options) { + const flow = ctx.inFlow ?? collection.flow; + const stringify4 = flow ? stringifyFlowCollection : stringifyBlockCollection; + return stringify4(collection, ctx, options); +} +function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) { + const { indent, options: { commentString } } = ctx; + const itemCtx = Object.assign({}, ctx, { indent: itemIndent, type: null }); + let chompKeep = false; + const lines = []; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + let comment2 = null; + if (isNode(item)) { + if (!chompKeep && item.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, item.commentBefore, chompKeep); + if (item.comment) + comment2 = item.comment; + } else if (isPair(item)) { + const ik = isNode(item.key) ? item.key : null; + if (ik) { + if (!chompKeep && ik.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, ik.commentBefore, chompKeep); + } + } + chompKeep = false; + let str2 = stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true); + if (comment2) + str2 += lineComment(str2, itemIndent, commentString(comment2)); + if (chompKeep && comment2) + chompKeep = false; + lines.push(blockItemPrefix + str2); + } + let str; + if (lines.length === 0) { + str = flowChars.start + flowChars.end; + } else { + str = lines[0]; + for (let i = 1; i < lines.length; ++i) { + const line = lines[i]; + str += line ? ` +${indent}${line}` : "\n"; + } + } + if (comment) { + str += "\n" + indentComment(commentString(comment), indent); + if (onComment) + onComment(); + } else if (chompKeep && onChompKeep) + onChompKeep(); + return str; +} +function stringifyFlowCollection({ items }, ctx, { flowChars, itemIndent }) { + const { indent, indentStep, flowCollectionPadding: fcPadding, options: { commentString } } = ctx; + itemIndent += indentStep; + const itemCtx = Object.assign({}, ctx, { + indent: itemIndent, + inFlow: true, + type: null + }); + let reqNewline = false; + let linesAtValue = 0; + const lines = []; + for (let i = 0; i < items.length; ++i) { + const item = items[i]; + let comment = null; + if (isNode(item)) { + if (item.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, item.commentBefore, false); + if (item.comment) + comment = item.comment; + } else if (isPair(item)) { + const ik = isNode(item.key) ? item.key : null; + if (ik) { + if (ik.spaceBefore) + lines.push(""); + addCommentBefore(ctx, lines, ik.commentBefore, false); + if (ik.comment) + reqNewline = true; + } + const iv = isNode(item.value) ? item.value : null; + if (iv) { + if (iv.comment) + comment = iv.comment; + if (iv.commentBefore) + reqNewline = true; + } else if (item.value == null && ik?.comment) { + comment = ik.comment; + } + } + if (comment) + reqNewline = true; + let str = stringify(item, itemCtx, () => comment = null); + reqNewline || (reqNewline = lines.length > linesAtValue || str.includes("\n")); + if (i < items.length - 1) { + str += ","; + } else if (ctx.options.trailingComma) { + if (ctx.options.lineWidth > 0) { + reqNewline || (reqNewline = lines.reduce((sum, line) => sum + line.length + 2, 2) + (str.length + 2) > ctx.options.lineWidth); + } + if (reqNewline) { + str += ","; + } + } + if (comment) + str += lineComment(str, itemIndent, commentString(comment)); + lines.push(str); + linesAtValue = lines.length; + } + const { start, end } = flowChars; + if (lines.length === 0) { + return start + end; + } else { + if (!reqNewline) { + const len = lines.reduce((sum, line) => sum + line.length + 2, 2); + reqNewline = ctx.options.lineWidth > 0 && len > ctx.options.lineWidth; + } + if (reqNewline) { + let str = start; + for (const line of lines) + str += line ? ` +${indentStep}${indent}${line}` : "\n"; + return `${str} +${indent}${end}`; + } else { + return `${start}${fcPadding}${lines.join(" ")}${fcPadding}${end}`; + } + } +} +function addCommentBefore({ indent, options: { commentString } }, lines, comment, chompKeep) { + if (comment && chompKeep) + comment = comment.replace(/^\n+/, ""); + if (comment) { + const ic = indentComment(commentString(comment), indent); + lines.push(ic.trimStart()); + } +} + +// node_modules/yaml/browser/dist/nodes/YAMLMap.js +function findPair(items, key) { + const k = isScalar(key) ? key.value : key; + for (const it of items) { + if (isPair(it)) { + if (it.key === key || it.key === k) + return it; + if (isScalar(it.key) && it.key.value === k) + return it; + } + } + return void 0; +} +var YAMLMap = class extends Collection { + static get tagName() { + return "tag:yaml.org,2002:map"; + } + constructor(schema4) { + super(MAP, schema4); + this.items = []; + } + /** + * A generic collection parsing method that can be extended + * to other node classes that inherit from YAMLMap + */ + static from(schema4, obj, ctx) { + const { keepUndefined, replacer } = ctx; + const map2 = new this(schema4); + const add = (key, value) => { + if (typeof replacer === "function") + value = replacer.call(obj, key, value); + else if (Array.isArray(replacer) && !replacer.includes(key)) + return; + if (value !== void 0 || keepUndefined) + map2.items.push(createPair(key, value, ctx)); + }; + if (obj instanceof Map) { + for (const [key, value] of obj) + add(key, value); + } else if (obj && typeof obj === "object") { + for (const key of Object.keys(obj)) + add(key, obj[key]); + } + if (typeof schema4.sortMapEntries === "function") { + map2.items.sort(schema4.sortMapEntries); + } + return map2; + } + /** + * Adds a value to the collection. + * + * @param overwrite - If not set `true`, using a key that is already in the + * collection will throw. Otherwise, overwrites the previous value. + */ + add(pair, overwrite) { + let _pair; + if (isPair(pair)) + _pair = pair; + else if (!pair || typeof pair !== "object" || !("key" in pair)) { + _pair = new Pair(pair, pair?.value); + } else + _pair = new Pair(pair.key, pair.value); + const prev = findPair(this.items, _pair.key); + const sortEntries = this.schema?.sortMapEntries; + if (prev) { + if (!overwrite) + throw new Error(`Key ${_pair.key} already set`); + if (isScalar(prev.value) && isScalarValue(_pair.value)) + prev.value.value = _pair.value; + else + prev.value = _pair.value; + } else if (sortEntries) { + const i = this.items.findIndex((item) => sortEntries(_pair, item) < 0); + if (i === -1) + this.items.push(_pair); + else + this.items.splice(i, 0, _pair); + } else { + this.items.push(_pair); + } + } + delete(key) { + const it = findPair(this.items, key); + if (!it) + return false; + const del = this.items.splice(this.items.indexOf(it), 1); + return del.length > 0; + } + get(key, keepScalar) { + const it = findPair(this.items, key); + const node = it?.value; + return (!keepScalar && isScalar(node) ? node.value : node) ?? void 0; + } + has(key) { + return !!findPair(this.items, key); + } + set(key, value) { + this.add(new Pair(key, value), true); + } + /** + * @param ctx - Conversion context, originally set in Document#toJS() + * @param {Class} Type - If set, forces the returned collection type + * @returns Instance of Type, Map, or Object + */ + toJSON(_, ctx, Type) { + const map2 = Type ? new Type() : ctx?.mapAsMap ? /* @__PURE__ */ new Map() : {}; + if (ctx?.onCreate) + ctx.onCreate(map2); + for (const item of this.items) + addPairToJSMap(ctx, map2, item); + return map2; + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + for (const item of this.items) { + if (!isPair(item)) + throw new Error(`Map items must all be pairs; found ${JSON.stringify(item)} instead`); + } + if (!ctx.allNullValues && this.hasAllNullValues(false)) + ctx = Object.assign({}, ctx, { allNullValues: true }); + return stringifyCollection(this, ctx, { + blockItemPrefix: "", + flowChars: { start: "{", end: "}" }, + itemIndent: ctx.indent || "", + onChompKeep, + onComment + }); + } +}; + +// node_modules/yaml/browser/dist/schema/common/map.js +var map = { + collection: "map", + default: true, + nodeClass: YAMLMap, + tag: "tag:yaml.org,2002:map", + resolve(map2, onError) { + if (!isMap(map2)) + onError("Expected a mapping for this tag"); + return map2; + }, + createNode: (schema4, obj, ctx) => YAMLMap.from(schema4, obj, ctx) +}; + +// node_modules/yaml/browser/dist/nodes/YAMLSeq.js +var YAMLSeq = class extends Collection { + static get tagName() { + return "tag:yaml.org,2002:seq"; + } + constructor(schema4) { + super(SEQ, schema4); + this.items = []; + } + add(value) { + this.items.push(value); + } + /** + * Removes a value from the collection. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + * + * @returns `true` if the item was found and removed. + */ + delete(key) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + return false; + const del = this.items.splice(idx, 1); + return del.length > 0; + } + get(key, keepScalar) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + return void 0; + const it = this.items[idx]; + return !keepScalar && isScalar(it) ? it.value : it; + } + /** + * Checks if the collection includes a value with the key `key`. + * + * `key` must contain a representation of an integer for this to succeed. + * It may be wrapped in a `Scalar`. + */ + has(key) { + const idx = asItemIndex(key); + return typeof idx === "number" && idx < this.items.length; + } + /** + * Sets a value in this collection. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + * + * If `key` does not contain a representation of an integer, this will throw. + * It may be wrapped in a `Scalar`. + */ + set(key, value) { + const idx = asItemIndex(key); + if (typeof idx !== "number") + throw new Error(`Expected a valid index, not ${key}.`); + const prev = this.items[idx]; + if (isScalar(prev) && isScalarValue(value)) + prev.value = value; + else + this.items[idx] = value; + } + toJSON(_, ctx) { + const seq2 = []; + if (ctx?.onCreate) + ctx.onCreate(seq2); + let i = 0; + for (const item of this.items) + seq2.push(toJS(item, String(i++), ctx)); + return seq2; + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + return stringifyCollection(this, ctx, { + blockItemPrefix: "- ", + flowChars: { start: "[", end: "]" }, + itemIndent: (ctx.indent || "") + " ", + onChompKeep, + onComment + }); + } + static from(schema4, obj, ctx) { + const { replacer } = ctx; + const seq2 = new this(schema4); + if (obj && Symbol.iterator in Object(obj)) { + let i = 0; + for (let it of obj) { + if (typeof replacer === "function") { + const key = obj instanceof Set ? it : String(i++); + it = replacer.call(obj, key, it); + } + seq2.items.push(createNode(it, void 0, ctx)); + } + } + return seq2; + } +}; +function asItemIndex(key) { + let idx = isScalar(key) ? key.value : key; + if (idx && typeof idx === "string") + idx = Number(idx); + return typeof idx === "number" && Number.isInteger(idx) && idx >= 0 ? idx : null; +} + +// node_modules/yaml/browser/dist/schema/common/seq.js +var seq = { + collection: "seq", + default: true, + nodeClass: YAMLSeq, + tag: "tag:yaml.org,2002:seq", + resolve(seq2, onError) { + if (!isSeq(seq2)) + onError("Expected a sequence for this tag"); + return seq2; + }, + createNode: (schema4, obj, ctx) => YAMLSeq.from(schema4, obj, ctx) +}; + +// node_modules/yaml/browser/dist/schema/common/string.js +var string = { + identify: (value) => typeof value === "string", + default: true, + tag: "tag:yaml.org,2002:str", + resolve: (str) => str, + stringify(item, ctx, onComment, onChompKeep) { + ctx = Object.assign({ actualString: true }, ctx); + return stringifyString(item, ctx, onComment, onChompKeep); + } +}; + +// node_modules/yaml/browser/dist/schema/common/null.js +var nullTag = { + identify: (value) => value == null, + createNode: () => new Scalar(null), + default: true, + tag: "tag:yaml.org,2002:null", + test: /^(?:~|[Nn]ull|NULL)?$/, + resolve: () => new Scalar(null), + stringify: ({ source }, ctx) => typeof source === "string" && nullTag.test.test(source) ? source : ctx.options.nullStr +}; + +// node_modules/yaml/browser/dist/schema/core/bool.js +var boolTag = { + identify: (value) => typeof value === "boolean", + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/, + resolve: (str) => new Scalar(str[0] === "t" || str[0] === "T"), + stringify({ source, value }, ctx) { + if (source && boolTag.test.test(source)) { + const sv = source[0] === "t" || source[0] === "T"; + if (value === sv) + return source; + } + return value ? ctx.options.trueStr : ctx.options.falseStr; + } +}; + +// node_modules/yaml/browser/dist/stringify/stringifyNumber.js +function stringifyNumber({ format, minFractionDigits, tag, value }) { + if (typeof value === "bigint") + return String(value); + const num = typeof value === "number" ? value : Number(value); + if (!isFinite(num)) + return isNaN(num) ? ".nan" : num < 0 ? "-.inf" : ".inf"; + let n = Object.is(value, -0) ? "-0" : JSON.stringify(value); + if (!format && minFractionDigits && (!tag || tag === "tag:yaml.org,2002:float") && /^-?\d/.test(n) && !n.includes("e")) { + let i = n.indexOf("."); + if (i < 0) { + i = n.length; + n += "."; + } + let d = minFractionDigits - (n.length - i - 1); + while (d-- > 0) + n += "0"; + } + return n; +} + +// node_modules/yaml/browser/dist/schema/core/float.js +var floatNaN = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/, + resolve: (str) => str.slice(-3).toLowerCase() === "nan" ? NaN : str[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, + stringify: stringifyNumber +}; +var floatExp = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "EXP", + test: /^[-+]?(?:\.[0-9]+|[0-9]+(?:\.[0-9]*)?)[eE][-+]?[0-9]+$/, + resolve: (str) => parseFloat(str), + stringify(node) { + const num = Number(node.value); + return isFinite(num) ? num.toExponential() : stringifyNumber(node); + } +}; +var float = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^[-+]?(?:\.[0-9]+|[0-9]+\.[0-9]*)$/, + resolve(str) { + const node = new Scalar(parseFloat(str)); + const dot = str.indexOf("."); + if (dot !== -1 && str[str.length - 1] === "0") + node.minFractionDigits = str.length - dot - 1; + return node; + }, + stringify: stringifyNumber +}; + +// node_modules/yaml/browser/dist/schema/core/int.js +var intIdentify = (value) => typeof value === "bigint" || Number.isInteger(value); +var intResolve = (str, offset, radix, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str.substring(offset), radix); +function intStringify(node, radix, prefix) { + const { value } = node; + if (intIdentify(value) && value >= 0) + return prefix + value.toString(radix); + return stringifyNumber(node); +} +var intOct = { + identify: (value) => intIdentify(value) && value >= 0, + default: true, + tag: "tag:yaml.org,2002:int", + format: "OCT", + test: /^0o[0-7]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 8, opt), + stringify: (node) => intStringify(node, 8, "0o") +}; +var int = { + identify: intIdentify, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^[-+]?[0-9]+$/, + resolve: (str, _onError, opt) => intResolve(str, 0, 10, opt), + stringify: stringifyNumber +}; +var intHex = { + identify: (value) => intIdentify(value) && value >= 0, + default: true, + tag: "tag:yaml.org,2002:int", + format: "HEX", + test: /^0x[0-9a-fA-F]+$/, + resolve: (str, _onError, opt) => intResolve(str, 2, 16, opt), + stringify: (node) => intStringify(node, 16, "0x") +}; + +// node_modules/yaml/browser/dist/schema/core/schema.js +var schema = [ + map, + seq, + string, + nullTag, + boolTag, + intOct, + int, + intHex, + floatNaN, + floatExp, + float +]; + +// node_modules/yaml/browser/dist/schema/json/schema.js +function intIdentify2(value) { + return typeof value === "bigint" || Number.isInteger(value); +} +var stringifyJSON = ({ value }) => JSON.stringify(value); +var jsonScalars = [ + { + identify: (value) => typeof value === "string", + default: true, + tag: "tag:yaml.org,2002:str", + resolve: (str) => str, + stringify: stringifyJSON + }, + { + identify: (value) => value == null, + createNode: () => new Scalar(null), + default: true, + tag: "tag:yaml.org,2002:null", + test: /^null$/, + resolve: () => null, + stringify: stringifyJSON + }, + { + identify: (value) => typeof value === "boolean", + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^true$|^false$/, + resolve: (str) => str === "true", + stringify: stringifyJSON + }, + { + identify: intIdentify2, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^-?(?:0|[1-9][0-9]*)$/, + resolve: (str, _onError, { intAsBigInt }) => intAsBigInt ? BigInt(str) : parseInt(str, 10), + stringify: ({ value }) => intIdentify2(value) ? value.toString() : JSON.stringify(value) + }, + { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][-+]?[0-9]+)?$/, + resolve: (str) => parseFloat(str), + stringify: stringifyJSON + } +]; +var jsonError = { + default: true, + tag: "", + test: /^/, + resolve(str, onError) { + onError(`Unresolved plain scalar ${JSON.stringify(str)}`); + return str; + } +}; +var schema2 = [map, seq].concat(jsonScalars, jsonError); + +// node_modules/yaml/browser/dist/schema/yaml-1.1/binary.js +var binary = { + identify: (value) => value instanceof Uint8Array, + // Buffer inherits from Uint8Array + default: false, + tag: "tag:yaml.org,2002:binary", + /** + * Returns a Buffer in node and an Uint8Array in browsers + * + * To use the resulting buffer as an image, you'll want to do something like: + * + * const blob = new Blob([buffer], { type: 'image/jpeg' }) + * document.querySelector('#photo').src = URL.createObjectURL(blob) + */ + resolve(src, onError) { + if (typeof atob === "function") { + const str = atob(src.replace(/[\n\r]/g, "")); + const buffer = new Uint8Array(str.length); + for (let i = 0; i < str.length; ++i) + buffer[i] = str.charCodeAt(i); + return buffer; + } else { + onError("This environment does not support reading binary tags; either Buffer or atob is required"); + return src; + } + }, + stringify({ comment, type, value }, ctx, onComment, onChompKeep) { + if (!value) + return ""; + const buf = value; + let str; + if (typeof btoa === "function") { + let s = ""; + for (let i = 0; i < buf.length; ++i) + s += String.fromCharCode(buf[i]); + str = btoa(s); + } else { + throw new Error("This environment does not support writing binary tags; either Buffer or btoa is required"); + } + type ?? (type = Scalar.BLOCK_LITERAL); + if (type !== Scalar.QUOTE_DOUBLE) { + const lineWidth = Math.max(ctx.options.lineWidth - ctx.indent.length, ctx.options.minContentWidth); + const n = Math.ceil(str.length / lineWidth); + const lines = new Array(n); + for (let i = 0, o = 0; i < n; ++i, o += lineWidth) { + lines[i] = str.substr(o, lineWidth); + } + str = lines.join(type === Scalar.BLOCK_LITERAL ? "\n" : " "); + } + return stringifyString({ comment, type, value: str }, ctx, onComment, onChompKeep); + } +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/pairs.js +function resolvePairs(seq2, onError) { + if (isSeq(seq2)) { + for (let i = 0; i < seq2.items.length; ++i) { + let item = seq2.items[i]; + if (isPair(item)) + continue; + else if (isMap(item)) { + if (item.items.length > 1) + onError("Each pair must have its own sequence indicator"); + const pair = item.items[0] || new Pair(new Scalar(null)); + if (item.commentBefore) + pair.key.commentBefore = pair.key.commentBefore ? `${item.commentBefore} +${pair.key.commentBefore}` : item.commentBefore; + if (item.comment) { + const cn = pair.value ?? pair.key; + cn.comment = cn.comment ? `${item.comment} +${cn.comment}` : item.comment; + } + item = pair; + } + seq2.items[i] = isPair(item) ? item : new Pair(item); + } + } else + onError("Expected a sequence for this tag"); + return seq2; +} +function createPairs(schema4, iterable, ctx) { + const { replacer } = ctx; + const pairs2 = new YAMLSeq(schema4); + pairs2.tag = "tag:yaml.org,2002:pairs"; + let i = 0; + if (iterable && Symbol.iterator in Object(iterable)) + for (let it of iterable) { + if (typeof replacer === "function") + it = replacer.call(iterable, String(i++), it); + let key, value; + if (Array.isArray(it)) { + if (it.length === 2) { + key = it[0]; + value = it[1]; + } else + throw new TypeError(`Expected [key, value] tuple: ${it}`); + } else if (it && it instanceof Object) { + const keys = Object.keys(it); + if (keys.length === 1) { + key = keys[0]; + value = it[key]; + } else { + throw new TypeError(`Expected tuple with one key, not ${keys.length} keys`); + } + } else { + key = it; + } + pairs2.items.push(createPair(key, value, ctx)); + } + return pairs2; +} +var pairs = { + collection: "seq", + default: false, + tag: "tag:yaml.org,2002:pairs", + resolve: resolvePairs, + createNode: createPairs +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/omap.js +var YAMLOMap = class _YAMLOMap extends YAMLSeq { + constructor() { + super(); + this.add = YAMLMap.prototype.add.bind(this); + this.delete = YAMLMap.prototype.delete.bind(this); + this.get = YAMLMap.prototype.get.bind(this); + this.has = YAMLMap.prototype.has.bind(this); + this.set = YAMLMap.prototype.set.bind(this); + this.tag = _YAMLOMap.tag; + } + /** + * If `ctx` is given, the return type is actually `Map`, + * but TypeScript won't allow widening the signature of a child method. + */ + toJSON(_, ctx) { + if (!ctx) + return super.toJSON(_); + const map2 = /* @__PURE__ */ new Map(); + if (ctx?.onCreate) + ctx.onCreate(map2); + for (const pair of this.items) { + let key, value; + if (isPair(pair)) { + key = toJS(pair.key, "", ctx); + value = toJS(pair.value, key, ctx); + } else { + key = toJS(pair, "", ctx); + } + if (map2.has(key)) + throw new Error("Ordered maps must not include duplicate keys"); + map2.set(key, value); + } + return map2; + } + static from(schema4, iterable, ctx) { + const pairs2 = createPairs(schema4, iterable, ctx); + const omap2 = new this(); + omap2.items = pairs2.items; + return omap2; + } +}; +YAMLOMap.tag = "tag:yaml.org,2002:omap"; +var omap = { + collection: "seq", + identify: (value) => value instanceof Map, + nodeClass: YAMLOMap, + default: false, + tag: "tag:yaml.org,2002:omap", + resolve(seq2, onError) { + const pairs2 = resolvePairs(seq2, onError); + const seenKeys = []; + for (const { key } of pairs2.items) { + if (isScalar(key)) { + if (seenKeys.includes(key.value)) { + onError(`Ordered maps must not include duplicate keys: ${key.value}`); + } else { + seenKeys.push(key.value); + } + } + } + return Object.assign(new YAMLOMap(), pairs2); + }, + createNode: (schema4, iterable, ctx) => YAMLOMap.from(schema4, iterable, ctx) +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/bool.js +function boolStringify({ value, source }, ctx) { + const boolObj = value ? trueTag : falseTag; + if (source && boolObj.test.test(source)) + return source; + return value ? ctx.options.trueStr : ctx.options.falseStr; +} +var trueTag = { + identify: (value) => value === true, + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/, + resolve: () => new Scalar(true), + stringify: boolStringify +}; +var falseTag = { + identify: (value) => value === false, + default: true, + tag: "tag:yaml.org,2002:bool", + test: /^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/, + resolve: () => new Scalar(false), + stringify: boolStringify +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/float.js +var floatNaN2 = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/, + resolve: (str) => str.slice(-3).toLowerCase() === "nan" ? NaN : str[0] === "-" ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, + stringify: stringifyNumber +}; +var floatExp2 = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "EXP", + test: /^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/, + resolve: (str) => parseFloat(str.replace(/_/g, "")), + stringify(node) { + const num = Number(node.value); + return isFinite(num) ? num.toExponential() : stringifyNumber(node); + } +}; +var float2 = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + test: /^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/, + resolve(str) { + const node = new Scalar(parseFloat(str.replace(/_/g, ""))); + const dot = str.indexOf("."); + if (dot !== -1) { + const f = str.substring(dot + 1).replace(/_/g, ""); + if (f[f.length - 1] === "0") + node.minFractionDigits = f.length; + } + return node; + }, + stringify: stringifyNumber +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/int.js +var intIdentify3 = (value) => typeof value === "bigint" || Number.isInteger(value); +function intResolve2(str, offset, radix, { intAsBigInt }) { + const sign = str[0]; + if (sign === "-" || sign === "+") + offset += 1; + str = str.substring(offset).replace(/_/g, ""); + if (intAsBigInt) { + switch (radix) { + case 2: + str = `0b${str}`; + break; + case 8: + str = `0o${str}`; + break; + case 16: + str = `0x${str}`; + break; + } + const n2 = BigInt(str); + return sign === "-" ? BigInt(-1) * n2 : n2; + } + const n = parseInt(str, radix); + return sign === "-" ? -1 * n : n; +} +function intStringify2(node, radix, prefix) { + const { value } = node; + if (intIdentify3(value)) { + const str = value.toString(radix); + return value < 0 ? "-" + prefix + str.substr(1) : prefix + str; + } + return stringifyNumber(node); +} +var intBin = { + identify: intIdentify3, + default: true, + tag: "tag:yaml.org,2002:int", + format: "BIN", + test: /^[-+]?0b[0-1_]+$/, + resolve: (str, _onError, opt) => intResolve2(str, 2, 2, opt), + stringify: (node) => intStringify2(node, 2, "0b") +}; +var intOct2 = { + identify: intIdentify3, + default: true, + tag: "tag:yaml.org,2002:int", + format: "OCT", + test: /^[-+]?0[0-7_]+$/, + resolve: (str, _onError, opt) => intResolve2(str, 1, 8, opt), + stringify: (node) => intStringify2(node, 8, "0") +}; +var int2 = { + identify: intIdentify3, + default: true, + tag: "tag:yaml.org,2002:int", + test: /^[-+]?[0-9][0-9_]*$/, + resolve: (str, _onError, opt) => intResolve2(str, 0, 10, opt), + stringify: stringifyNumber +}; +var intHex2 = { + identify: intIdentify3, + default: true, + tag: "tag:yaml.org,2002:int", + format: "HEX", + test: /^[-+]?0x[0-9a-fA-F_]+$/, + resolve: (str, _onError, opt) => intResolve2(str, 2, 16, opt), + stringify: (node) => intStringify2(node, 16, "0x") +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/set.js +var YAMLSet = class _YAMLSet extends YAMLMap { + constructor(schema4) { + super(schema4); + this.tag = _YAMLSet.tag; + } + add(key) { + let pair; + if (isPair(key)) + pair = key; + else if (key && typeof key === "object" && "key" in key && "value" in key && key.value === null) + pair = new Pair(key.key, null); + else + pair = new Pair(key, null); + const prev = findPair(this.items, pair.key); + if (!prev) + this.items.push(pair); + } + /** + * If `keepPair` is `true`, returns the Pair matching `key`. + * Otherwise, returns the value of that Pair's key. + */ + get(key, keepPair) { + const pair = findPair(this.items, key); + return !keepPair && isPair(pair) ? isScalar(pair.key) ? pair.key.value : pair.key : pair; + } + set(key, value) { + if (typeof value !== "boolean") + throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof value}`); + const prev = findPair(this.items, key); + if (prev && !value) { + this.items.splice(this.items.indexOf(prev), 1); + } else if (!prev && value) { + this.items.push(new Pair(key)); + } + } + toJSON(_, ctx) { + return super.toJSON(_, ctx, Set); + } + toString(ctx, onComment, onChompKeep) { + if (!ctx) + return JSON.stringify(this); + if (this.hasAllNullValues(true)) + return super.toString(Object.assign({}, ctx, { allNullValues: true }), onComment, onChompKeep); + else + throw new Error("Set items must all have null values"); + } + static from(schema4, iterable, ctx) { + const { replacer } = ctx; + const set2 = new this(schema4); + if (iterable && Symbol.iterator in Object(iterable)) + for (let value of iterable) { + if (typeof replacer === "function") + value = replacer.call(iterable, value, value); + set2.items.push(createPair(value, null, ctx)); + } + return set2; + } +}; +YAMLSet.tag = "tag:yaml.org,2002:set"; +var set = { + collection: "map", + identify: (value) => value instanceof Set, + nodeClass: YAMLSet, + default: false, + tag: "tag:yaml.org,2002:set", + createNode: (schema4, iterable, ctx) => YAMLSet.from(schema4, iterable, ctx), + resolve(map2, onError) { + if (isMap(map2)) { + if (map2.hasAllNullValues(true)) + return Object.assign(new YAMLSet(), map2); + else + onError("Set items must all have null values"); + } else + onError("Expected a mapping for this tag"); + return map2; + } +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/timestamp.js +function parseSexagesimal(str, asBigInt) { + const sign = str[0]; + const parts = sign === "-" || sign === "+" ? str.substring(1) : str; + const num = (n) => asBigInt ? BigInt(n) : Number(n); + const res = parts.replace(/_/g, "").split(":").reduce((res2, p) => res2 * num(60) + num(p), num(0)); + return sign === "-" ? num(-1) * res : res; +} +function stringifySexagesimal(node) { + let { value } = node; + let num = (n) => n; + if (typeof value === "bigint") + num = (n) => BigInt(n); + else if (isNaN(value) || !isFinite(value)) + return stringifyNumber(node); + let sign = ""; + if (value < 0) { + sign = "-"; + value *= num(-1); + } + const _60 = num(60); + const parts = [value % _60]; + if (value < 60) { + parts.unshift(0); + } else { + value = (value - parts[0]) / _60; + parts.unshift(value % _60); + if (value >= 60) { + value = (value - parts[0]) / _60; + parts.unshift(value); + } + } + return sign + parts.map((n) => String(n).padStart(2, "0")).join(":").replace(/000000\d*$/, ""); +} +var intTime = { + identify: (value) => typeof value === "bigint" || Number.isInteger(value), + default: true, + tag: "tag:yaml.org,2002:int", + format: "TIME", + test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+$/, + resolve: (str, _onError, { intAsBigInt }) => parseSexagesimal(str, intAsBigInt), + stringify: stringifySexagesimal +}; +var floatTime = { + identify: (value) => typeof value === "number", + default: true, + tag: "tag:yaml.org,2002:float", + format: "TIME", + test: /^[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]*$/, + resolve: (str) => parseSexagesimal(str, false), + stringify: stringifySexagesimal +}; +var timestamp = { + identify: (value) => value instanceof Date, + default: true, + tag: "tag:yaml.org,2002:timestamp", + // If the time zone is omitted, the timestamp is assumed to be specified in UTC. The time part + // may be omitted altogether, resulting in a date format. In such a case, the time part is + // assumed to be 00:00:00Z (start of day, UTC). + test: RegExp("^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})(?:(?:t|T|[ \\t]+)([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2}(\\.[0-9]+)?)(?:[ \\t]*(Z|[-+][012]?[0-9](?::[0-9]{2})?))?)?$"), + resolve(str) { + const match = str.match(timestamp.test); + if (!match) + throw new Error("!!timestamp expects a date, starting with yyyy-mm-dd"); + const [, year, month, day, hour, minute, second] = match.map(Number); + const millisec = match[7] ? Number((match[7] + "00").substr(1, 3)) : 0; + let date = Date.UTC(year, month - 1, day, hour || 0, minute || 0, second || 0, millisec); + const tz = match[8]; + if (tz && tz !== "Z") { + let d = parseSexagesimal(tz, false); + if (Math.abs(d) < 30) + d *= 60; + date -= 6e4 * d; + } + return new Date(date); + }, + stringify: ({ value }) => value?.toISOString().replace(/(T00:00:00)?\.000Z$/, "") ?? "" +}; + +// node_modules/yaml/browser/dist/schema/yaml-1.1/schema.js +var schema3 = [ + map, + seq, + string, + nullTag, + trueTag, + falseTag, + intBin, + intOct2, + int2, + intHex2, + floatNaN2, + floatExp2, + float2, + binary, + merge, + omap, + pairs, + set, + intTime, + floatTime, + timestamp +]; + +// node_modules/yaml/browser/dist/schema/tags.js +var schemas = /* @__PURE__ */ new Map([ + ["core", schema], + ["failsafe", [map, seq, string]], + ["json", schema2], + ["yaml11", schema3], + ["yaml-1.1", schema3] +]); +var tagsByName = { + binary, + bool: boolTag, + float, + floatExp, + floatNaN, + floatTime, + int, + intHex, + intOct, + intTime, + map, + merge, + null: nullTag, + omap, + pairs, + seq, + set, + timestamp +}; +var coreKnownTags = { + "tag:yaml.org,2002:binary": binary, + "tag:yaml.org,2002:merge": merge, + "tag:yaml.org,2002:omap": omap, + "tag:yaml.org,2002:pairs": pairs, + "tag:yaml.org,2002:set": set, + "tag:yaml.org,2002:timestamp": timestamp +}; +function getTags(customTags, schemaName, addMergeTag) { + const schemaTags = schemas.get(schemaName); + if (schemaTags && !customTags) { + return addMergeTag && !schemaTags.includes(merge) ? schemaTags.concat(merge) : schemaTags.slice(); + } + let tags = schemaTags; + if (!tags) { + if (Array.isArray(customTags)) + tags = []; + else { + const keys = Array.from(schemas.keys()).filter((key) => key !== "yaml11").map((key) => JSON.stringify(key)).join(", "); + throw new Error(`Unknown schema "${schemaName}"; use one of ${keys} or define customTags array`); + } + } + if (Array.isArray(customTags)) { + for (const tag of customTags) + tags = tags.concat(tag); + } else if (typeof customTags === "function") { + tags = customTags(tags.slice()); + } + if (addMergeTag) + tags = tags.concat(merge); + return tags.reduce((tags2, tag) => { + const tagObj = typeof tag === "string" ? tagsByName[tag] : tag; + if (!tagObj) { + const tagName = JSON.stringify(tag); + const keys = Object.keys(tagsByName).map((key) => JSON.stringify(key)).join(", "); + throw new Error(`Unknown custom tag ${tagName}; use one of ${keys}`); + } + if (!tags2.includes(tagObj)) + tags2.push(tagObj); + return tags2; + }, []); +} + +// node_modules/yaml/browser/dist/schema/Schema.js +var sortMapEntriesByKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0; +var Schema = class _Schema { + constructor({ compat, customTags, merge: merge2, resolveKnownTags, schema: schema4, sortMapEntries, toStringDefaults }) { + this.compat = Array.isArray(compat) ? getTags(compat, "compat") : compat ? getTags(null, compat) : null; + this.name = typeof schema4 === "string" && schema4 || "core"; + this.knownTags = resolveKnownTags ? coreKnownTags : {}; + this.tags = getTags(customTags, this.name, merge2); + this.toStringOptions = toStringDefaults ?? null; + Object.defineProperty(this, MAP, { value: map }); + Object.defineProperty(this, SCALAR, { value: string }); + Object.defineProperty(this, SEQ, { value: seq }); + this.sortMapEntries = typeof sortMapEntries === "function" ? sortMapEntries : sortMapEntries === true ? sortMapEntriesByKey : null; + } + clone() { + const copy = Object.create(_Schema.prototype, Object.getOwnPropertyDescriptors(this)); + copy.tags = this.tags.slice(); + return copy; + } +}; + +// node_modules/yaml/browser/dist/stringify/stringifyDocument.js +function stringifyDocument(doc, options) { + const lines = []; + let hasDirectives = options.directives === true; + if (options.directives !== false && doc.directives) { + const dir = doc.directives.toString(doc); + if (dir) { + lines.push(dir); + hasDirectives = true; + } else if (doc.directives.docStart) + hasDirectives = true; + } + if (hasDirectives) + lines.push("---"); + const ctx = createStringifyContext(doc, options); + const { commentString } = ctx.options; + if (doc.commentBefore) { + if (lines.length !== 1) + lines.unshift(""); + const cs = commentString(doc.commentBefore); + lines.unshift(indentComment(cs, "")); + } + let chompKeep = false; + let contentComment = null; + if (doc.contents) { + if (isNode(doc.contents)) { + if (doc.contents.spaceBefore && hasDirectives) + lines.push(""); + if (doc.contents.commentBefore) { + const cs = commentString(doc.contents.commentBefore); + lines.push(indentComment(cs, "")); + } + ctx.forceBlockIndent = !!doc.comment; + contentComment = doc.contents.comment; + } + const onChompKeep = contentComment ? void 0 : () => chompKeep = true; + let body = stringify(doc.contents, ctx, () => contentComment = null, onChompKeep); + if (contentComment) + body += lineComment(body, "", commentString(contentComment)); + if ((body[0] === "|" || body[0] === ">") && lines[lines.length - 1] === "---") { + lines[lines.length - 1] = `--- ${body}`; + } else + lines.push(body); + } else { + lines.push(stringify(doc.contents, ctx)); + } + if (doc.directives?.docEnd) { + if (doc.comment) { + const cs = commentString(doc.comment); + if (cs.includes("\n")) { + lines.push("..."); + lines.push(indentComment(cs, "")); + } else { + lines.push(`... ${cs}`); + } + } else { + lines.push("..."); + } + } else { + let dc = doc.comment; + if (dc && chompKeep) + dc = dc.replace(/^\n+/, ""); + if (dc) { + if ((!chompKeep || contentComment) && lines[lines.length - 1] !== "") + lines.push(""); + lines.push(indentComment(commentString(dc), "")); + } + } + return lines.join("\n") + "\n"; +} + +// node_modules/yaml/browser/dist/doc/Document.js +var Document = class _Document { + constructor(value, replacer, options) { + this.commentBefore = null; + this.comment = null; + this.errors = []; + this.warnings = []; + Object.defineProperty(this, NODE_TYPE, { value: DOC }); + let _replacer = null; + if (typeof replacer === "function" || Array.isArray(replacer)) { + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + replacer = void 0; + } + const opt = Object.assign({ + intAsBigInt: false, + keepSourceTokens: false, + logLevel: "warn", + prettyErrors: true, + strict: true, + stringKeys: false, + uniqueKeys: true, + version: "1.2" + }, options); + this.options = opt; + let { version } = opt; + if (options?._directives) { + this.directives = options._directives.atDocument(); + if (this.directives.yaml.explicit) + version = this.directives.yaml.version; + } else + this.directives = new Directives({ version }); + this.setSchema(version, options); + this.contents = value === void 0 ? null : this.createNode(value, _replacer, options); + } + /** + * Create a deep copy of this Document and its contents. + * + * Custom Node values that inherit from `Object` still refer to their original instances. + */ + clone() { + const copy = Object.create(_Document.prototype, { + [NODE_TYPE]: { value: DOC } + }); + copy.commentBefore = this.commentBefore; + copy.comment = this.comment; + copy.errors = this.errors.slice(); + copy.warnings = this.warnings.slice(); + copy.options = Object.assign({}, this.options); + if (this.directives) + copy.directives = this.directives.clone(); + copy.schema = this.schema.clone(); + copy.contents = isNode(this.contents) ? this.contents.clone(copy.schema) : this.contents; + if (this.range) + copy.range = this.range.slice(); + return copy; + } + /** Adds a value to the document. */ + add(value) { + if (assertCollection(this.contents)) + this.contents.add(value); + } + /** Adds a value to the document. */ + addIn(path, value) { + if (assertCollection(this.contents)) + this.contents.addIn(path, value); + } + /** + * Create a new `Alias` node, ensuring that the target `node` has the required anchor. + * + * If `node` already has an anchor, `name` is ignored. + * Otherwise, the `node.anchor` value will be set to `name`, + * or if an anchor with that name is already present in the document, + * `name` will be used as a prefix for a new unique anchor. + * If `name` is undefined, the generated anchor will use 'a' as a prefix. + */ + createAlias(node, name) { + if (!node.anchor) { + const prev = anchorNames(this); + node.anchor = // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + !name || prev.has(name) ? findNewAnchor(name || "a", prev) : name; + } + return new Alias(node.anchor); + } + createNode(value, replacer, options) { + let _replacer = void 0; + if (typeof replacer === "function") { + value = replacer.call({ "": value }, "", value); + _replacer = replacer; + } else if (Array.isArray(replacer)) { + const keyToStr = (v) => typeof v === "number" || v instanceof String || v instanceof Number; + const asStr = replacer.filter(keyToStr).map(String); + if (asStr.length > 0) + replacer = replacer.concat(asStr); + _replacer = replacer; + } else if (options === void 0 && replacer) { + options = replacer; + replacer = void 0; + } + const { aliasDuplicateObjects, anchorPrefix, flow, keepUndefined, onTagObj, tag } = options ?? {}; + const { onAnchor, setAnchors, sourceObjects } = createNodeAnchors( + this, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + anchorPrefix || "a" + ); + const ctx = { + aliasDuplicateObjects: aliasDuplicateObjects ?? true, + keepUndefined: keepUndefined ?? false, + onAnchor, + onTagObj, + replacer: _replacer, + schema: this.schema, + sourceObjects + }; + const node = createNode(value, tag, ctx); + if (flow && isCollection(node)) + node.flow = true; + setAnchors(); + return node; + } + /** + * Convert a key and a value into a `Pair` using the current schema, + * recursively wrapping all values as `Scalar` or `Collection` nodes. + */ + createPair(key, value, options = {}) { + const k = this.createNode(key, null, options); + const v = this.createNode(value, null, options); + return new Pair(k, v); + } + /** + * Removes a value from the document. + * @returns `true` if the item was found and removed. + */ + delete(key) { + return assertCollection(this.contents) ? this.contents.delete(key) : false; + } + /** + * Removes a value from the document. + * @returns `true` if the item was found and removed. + */ + deleteIn(path) { + if (isEmptyPath(path)) { + if (this.contents == null) + return false; + this.contents = null; + return true; + } + return assertCollection(this.contents) ? this.contents.deleteIn(path) : false; + } + /** + * Returns item at `key`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + get(key, keepScalar) { + return isCollection(this.contents) ? this.contents.get(key, keepScalar) : void 0; + } + /** + * Returns item at `path`, or `undefined` if not found. By default unwraps + * scalar values from their surrounding node; to disable set `keepScalar` to + * `true` (collections are always returned intact). + */ + getIn(path, keepScalar) { + if (isEmptyPath(path)) + return !keepScalar && isScalar(this.contents) ? this.contents.value : this.contents; + return isCollection(this.contents) ? this.contents.getIn(path, keepScalar) : void 0; + } + /** + * Checks if the document includes a value with the key `key`. + */ + has(key) { + return isCollection(this.contents) ? this.contents.has(key) : false; + } + /** + * Checks if the document includes a value at `path`. + */ + hasIn(path) { + if (isEmptyPath(path)) + return this.contents !== void 0; + return isCollection(this.contents) ? this.contents.hasIn(path) : false; + } + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + set(key, value) { + if (this.contents == null) { + this.contents = collectionFromPath(this.schema, [key], value); + } else if (assertCollection(this.contents)) { + this.contents.set(key, value); + } + } + /** + * Sets a value in this document. For `!!set`, `value` needs to be a + * boolean to add/remove the item from the set. + */ + setIn(path, value) { + if (isEmptyPath(path)) { + this.contents = value; + } else if (this.contents == null) { + this.contents = collectionFromPath(this.schema, Array.from(path), value); + } else if (assertCollection(this.contents)) { + this.contents.setIn(path, value); + } + } + /** + * Change the YAML version and schema used by the document. + * A `null` version disables support for directives, explicit tags, anchors, and aliases. + * It also requires the `schema` option to be given as a `Schema` instance value. + * + * Overrides all previously set schema options. + */ + setSchema(version, options = {}) { + if (typeof version === "number") + version = String(version); + let opt; + switch (version) { + case "1.1": + if (this.directives) + this.directives.yaml.version = "1.1"; + else + this.directives = new Directives({ version: "1.1" }); + opt = { resolveKnownTags: false, schema: "yaml-1.1" }; + break; + case "1.2": + case "next": + if (this.directives) + this.directives.yaml.version = version; + else + this.directives = new Directives({ version }); + opt = { resolveKnownTags: true, schema: "core" }; + break; + case null: + if (this.directives) + delete this.directives; + opt = null; + break; + default: { + const sv = JSON.stringify(version); + throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`); + } + } + if (options.schema instanceof Object) + this.schema = options.schema; + else if (opt) + this.schema = new Schema(Object.assign(opt, options)); + else + throw new Error(`With a null YAML version, the { schema: Schema } option is required`); + } + // json & jsonArg are only used from toJSON() + toJS({ json, jsonArg, mapAsMap, maxAliasCount, onAnchor, reviver } = {}) { + const ctx = { + anchors: /* @__PURE__ */ new Map(), + doc: this, + keep: !json, + mapAsMap: mapAsMap === true, + mapKeyWarned: false, + maxAliasCount: typeof maxAliasCount === "number" ? maxAliasCount : 100 + }; + const res = toJS(this.contents, jsonArg ?? "", ctx); + if (typeof onAnchor === "function") + for (const { count, res: res2 } of ctx.anchors.values()) + onAnchor(res2, count); + return typeof reviver === "function" ? applyReviver(reviver, { "": res }, "", res) : res; + } + /** + * A JSON representation of the document `contents`. + * + * @param jsonArg Used by `JSON.stringify` to indicate the array index or + * property name. + */ + toJSON(jsonArg, onAnchor) { + return this.toJS({ json: true, jsonArg, mapAsMap: false, onAnchor }); + } + /** A YAML representation of the document. */ + toString(options = {}) { + if (this.errors.length > 0) + throw new Error("Document with errors cannot be stringified"); + if ("indent" in options && (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) { + const s = JSON.stringify(options.indent); + throw new Error(`"indent" option must be a positive integer, not ${s}`); + } + return stringifyDocument(this, options); + } +}; +function assertCollection(contents) { + if (isCollection(contents)) + return true; + throw new Error("Expected a YAML collection as document contents"); +} + +// node_modules/yaml/browser/dist/errors.js +var YAMLError = class extends Error { + constructor(name, pos, code, message) { + super(); + this.name = name; + this.code = code; + this.message = message; + this.pos = pos; + } +}; +var YAMLParseError = class extends YAMLError { + constructor(pos, code, message) { + super("YAMLParseError", pos, code, message); + } +}; +var YAMLWarning = class extends YAMLError { + constructor(pos, code, message) { + super("YAMLWarning", pos, code, message); + } +}; +var prettifyError = (src, lc) => (error) => { + if (error.pos[0] === -1) + return; + error.linePos = error.pos.map((pos) => lc.linePos(pos)); + const { line, col } = error.linePos[0]; + error.message += ` at line ${line}, column ${col}`; + let ci = col - 1; + let lineStr = src.substring(lc.lineStarts[line - 1], lc.lineStarts[line]).replace(/[\n\r]+$/, ""); + if (ci >= 60 && lineStr.length > 80) { + const trimStart = Math.min(ci - 39, lineStr.length - 79); + lineStr = "…" + lineStr.substring(trimStart); + ci -= trimStart - 1; + } + if (lineStr.length > 80) + lineStr = lineStr.substring(0, 79) + "…"; + if (line > 1 && /^ *$/.test(lineStr.substring(0, ci))) { + let prev = src.substring(lc.lineStarts[line - 2], lc.lineStarts[line - 1]); + if (prev.length > 80) + prev = prev.substring(0, 79) + "…\n"; + lineStr = prev + lineStr; + } + if (/[^ ]/.test(lineStr)) { + let count = 1; + const end = error.linePos[1]; + if (end?.line === line && end.col > col) { + count = Math.max(1, Math.min(end.col - col, 80 - ci)); + } + const pointer = " ".repeat(ci) + "^".repeat(count); + error.message += `: + +${lineStr} +${pointer} +`; + } +}; + +// node_modules/yaml/browser/dist/compose/resolve-props.js +function resolveProps(tokens, { flow, indicator, next, offset, onError, parentIndent, startOnNewline }) { + let spaceBefore = false; + let atNewline = startOnNewline; + let hasSpace = startOnNewline; + let comment = ""; + let commentSep = ""; + let hasNewline = false; + let reqSpace = false; + let tab = null; + let anchor = null; + let tag = null; + let newlineAfterProp = null; + let comma = null; + let found = null; + let start = null; + for (const token of tokens) { + if (reqSpace) { + if (token.type !== "space" && token.type !== "newline" && token.type !== "comma") + onError(token.offset, "MISSING_CHAR", "Tags and anchors must be separated from the next token by white space"); + reqSpace = false; + } + if (tab) { + if (atNewline && token.type !== "comment" && token.type !== "newline") { + onError(tab, "TAB_AS_INDENT", "Tabs are not allowed as indentation"); + } + tab = null; + } + switch (token.type) { + case "space": + if (!flow && (indicator !== "doc-start" || next?.type !== "flow-collection") && token.source.includes(" ")) { + tab = token; + } + hasSpace = true; + break; + case "comment": { + if (!hasSpace) + onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters"); + const cb = token.source.substring(1) || " "; + if (!comment) + comment = cb; + else + comment += commentSep + cb; + commentSep = ""; + atNewline = false; + break; + } + case "newline": + if (atNewline) { + if (comment) + comment += token.source; + else if (!found || indicator !== "seq-item-ind") + spaceBefore = true; + } else + commentSep += token.source; + atNewline = true; + hasNewline = true; + if (anchor || tag) + newlineAfterProp = token; + hasSpace = true; + break; + case "anchor": + if (anchor) + onError(token, "MULTIPLE_ANCHORS", "A node can have at most one anchor"); + if (token.source.endsWith(":")) + onError(token.offset + token.source.length - 1, "BAD_ALIAS", "Anchor ending in : is ambiguous", true); + anchor = token; + start ?? (start = token.offset); + atNewline = false; + hasSpace = false; + reqSpace = true; + break; + case "tag": { + if (tag) + onError(token, "MULTIPLE_TAGS", "A node can have at most one tag"); + tag = token; + start ?? (start = token.offset); + atNewline = false; + hasSpace = false; + reqSpace = true; + break; + } + case indicator: + if (anchor || tag) + onError(token, "BAD_PROP_ORDER", `Anchors and tags must be after the ${token.source} indicator`); + if (found) + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${token.source} in ${flow ?? "collection"}`); + found = token; + atNewline = indicator === "seq-item-ind" || indicator === "explicit-key-ind"; + hasSpace = false; + break; + case "comma": + if (flow) { + if (comma) + onError(token, "UNEXPECTED_TOKEN", `Unexpected , in ${flow}`); + comma = token; + atNewline = false; + hasSpace = false; + break; + } + // else fallthrough + default: + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${token.type} token`); + atNewline = false; + hasSpace = false; + } + } + const last = tokens[tokens.length - 1]; + const end = last ? last.offset + last.source.length : offset; + if (reqSpace && next && next.type !== "space" && next.type !== "newline" && next.type !== "comma" && (next.type !== "scalar" || next.source !== "")) { + onError(next.offset, "MISSING_CHAR", "Tags and anchors must be separated from the next token by white space"); + } + if (tab && (atNewline && tab.indent <= parentIndent || next?.type === "block-map" || next?.type === "block-seq")) + onError(tab, "TAB_AS_INDENT", "Tabs are not allowed as indentation"); + return { + comma, + found, + spaceBefore, + comment, + hasNewline, + anchor, + tag, + newlineAfterProp, + end, + start: start ?? end + }; +} + +// node_modules/yaml/browser/dist/compose/util-contains-newline.js +function containsNewline(key) { + if (!key) + return null; + switch (key.type) { + case "alias": + case "scalar": + case "double-quoted-scalar": + case "single-quoted-scalar": + if (key.source.includes("\n")) + return true; + if (key.end) { + for (const st of key.end) + if (st.type === "newline") + return true; + } + return false; + case "flow-collection": + for (const it of key.items) { + for (const st of it.start) + if (st.type === "newline") + return true; + if (it.sep) { + for (const st of it.sep) + if (st.type === "newline") + return true; + } + if (containsNewline(it.key) || containsNewline(it.value)) + return true; + } + return false; + default: + return true; + } +} + +// node_modules/yaml/browser/dist/compose/util-flow-indent-check.js +function flowIndentCheck(indent, fc, onError) { + if (fc?.type === "flow-collection") { + const end = fc.end[0]; + if (end.indent === indent && (end.source === "]" || end.source === "}") && containsNewline(fc)) { + const msg = "Flow end indicator should be more indented than parent"; + onError(end, "BAD_INDENT", msg, true); + } + } +} + +// node_modules/yaml/browser/dist/compose/util-map-includes.js +function mapIncludes(ctx, items, search) { + const { uniqueKeys } = ctx.options; + if (uniqueKeys === false) + return false; + const isEqual2 = typeof uniqueKeys === "function" ? uniqueKeys : (a, b) => a === b || isScalar(a) && isScalar(b) && a.value === b.value; + return items.some((pair) => isEqual2(pair.key, search)); +} + +// node_modules/yaml/browser/dist/compose/resolve-block-map.js +var startColMsg = "All mapping items must start at the same column"; +function resolveBlockMap({ composeNode: composeNode2, composeEmptyNode: composeEmptyNode2 }, ctx, bm, onError, tag) { + const NodeClass = tag?.nodeClass ?? YAMLMap; + const map2 = new NodeClass(ctx.schema); + if (ctx.atRoot) + ctx.atRoot = false; + let offset = bm.offset; + let commentEnd = null; + for (const collItem of bm.items) { + const { start, key, sep, value } = collItem; + const keyProps = resolveProps(start, { + indicator: "explicit-key-ind", + next: key ?? sep?.[0], + offset, + onError, + parentIndent: bm.indent, + startOnNewline: true + }); + const implicitKey = !keyProps.found; + if (implicitKey) { + if (key) { + if (key.type === "block-seq") + onError(offset, "BLOCK_AS_IMPLICIT_KEY", "A block sequence may not be used as an implicit map key"); + else if ("indent" in key && key.indent !== bm.indent) + onError(offset, "BAD_INDENT", startColMsg); + } + if (!keyProps.anchor && !keyProps.tag && !sep) { + commentEnd = keyProps.end; + if (keyProps.comment) { + if (map2.comment) + map2.comment += "\n" + keyProps.comment; + else + map2.comment = keyProps.comment; + } + continue; + } + if (keyProps.newlineAfterProp || containsNewline(key)) { + onError(key ?? start[start.length - 1], "MULTILINE_IMPLICIT_KEY", "Implicit keys need to be on a single line"); + } + } else if (keyProps.found?.indent !== bm.indent) { + onError(offset, "BAD_INDENT", startColMsg); + } + ctx.atKey = true; + const keyStart = keyProps.end; + const keyNode = key ? composeNode2(ctx, key, keyProps, onError) : composeEmptyNode2(ctx, keyStart, start, null, keyProps, onError); + if (ctx.schema.compat) + flowIndentCheck(bm.indent, key, onError); + ctx.atKey = false; + if (mapIncludes(ctx, map2.items, keyNode)) + onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique"); + const valueProps = resolveProps(sep ?? [], { + indicator: "map-value-ind", + next: value, + offset: keyNode.range[2], + onError, + parentIndent: bm.indent, + startOnNewline: !key || key.type === "block-scalar" + }); + offset = valueProps.end; + if (valueProps.found) { + if (implicitKey) { + if (value?.type === "block-map" && !valueProps.hasNewline) + onError(offset, "BLOCK_AS_IMPLICIT_KEY", "Nested mappings are not allowed in compact mappings"); + if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024) + onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key"); + } + const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : composeEmptyNode2(ctx, offset, sep, null, valueProps, onError); + if (ctx.schema.compat) + flowIndentCheck(bm.indent, value, onError); + offset = valueNode.range[2]; + const pair = new Pair(keyNode, valueNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + map2.items.push(pair); + } else { + if (implicitKey) + onError(keyNode.range, "MISSING_CHAR", "Implicit map keys need to be followed by map values"); + if (valueProps.comment) { + if (keyNode.comment) + keyNode.comment += "\n" + valueProps.comment; + else + keyNode.comment = valueProps.comment; + } + const pair = new Pair(keyNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + map2.items.push(pair); + } + } + if (commentEnd && commentEnd < offset) + onError(commentEnd, "IMPOSSIBLE", "Map comment with trailing content"); + map2.range = [bm.offset, offset, commentEnd ?? offset]; + return map2; +} + +// node_modules/yaml/browser/dist/compose/resolve-block-seq.js +function resolveBlockSeq({ composeNode: composeNode2, composeEmptyNode: composeEmptyNode2 }, ctx, bs, onError, tag) { + const NodeClass = tag?.nodeClass ?? YAMLSeq; + const seq2 = new NodeClass(ctx.schema); + if (ctx.atRoot) + ctx.atRoot = false; + if (ctx.atKey) + ctx.atKey = false; + let offset = bs.offset; + let commentEnd = null; + for (const { start, value } of bs.items) { + const props = resolveProps(start, { + indicator: "seq-item-ind", + next: value, + offset, + onError, + parentIndent: bs.indent, + startOnNewline: true + }); + if (!props.found) { + if (props.anchor || props.tag || value) { + if (value?.type === "block-seq") + onError(props.end, "BAD_INDENT", "All sequence items must start at the same column"); + else + onError(offset, "MISSING_CHAR", "Sequence item without - indicator"); + } else { + commentEnd = props.end; + if (props.comment) + seq2.comment = props.comment; + continue; + } + } + const node = value ? composeNode2(ctx, value, props, onError) : composeEmptyNode2(ctx, props.end, start, null, props, onError); + if (ctx.schema.compat) + flowIndentCheck(bs.indent, value, onError); + offset = node.range[2]; + seq2.items.push(node); + } + seq2.range = [bs.offset, offset, commentEnd ?? offset]; + return seq2; +} + +// node_modules/yaml/browser/dist/compose/resolve-end.js +function resolveEnd(end, offset, reqSpace, onError) { + let comment = ""; + if (end) { + let hasSpace = false; + let sep = ""; + for (const token of end) { + const { source, type } = token; + switch (type) { + case "space": + hasSpace = true; + break; + case "comment": { + if (reqSpace && !hasSpace) + onError(token, "MISSING_CHAR", "Comments must be separated from other tokens by white space characters"); + const cb = source.substring(1) || " "; + if (!comment) + comment = cb; + else + comment += sep + cb; + sep = ""; + break; + } + case "newline": + if (comment) + sep += source; + hasSpace = true; + break; + default: + onError(token, "UNEXPECTED_TOKEN", `Unexpected ${type} at node end`); + } + offset += source.length; + } + } + return { comment, offset }; +} + +// node_modules/yaml/browser/dist/compose/resolve-flow-collection.js +var blockMsg = "Block collections are not allowed within flow collections"; +var isBlock = (token) => token && (token.type === "block-map" || token.type === "block-seq"); +function resolveFlowCollection({ composeNode: composeNode2, composeEmptyNode: composeEmptyNode2 }, ctx, fc, onError, tag) { + const isMap2 = fc.start.source === "{"; + const fcName = isMap2 ? "flow map" : "flow sequence"; + const NodeClass = tag?.nodeClass ?? (isMap2 ? YAMLMap : YAMLSeq); + const coll = new NodeClass(ctx.schema); + coll.flow = true; + const atRoot = ctx.atRoot; + if (atRoot) + ctx.atRoot = false; + if (ctx.atKey) + ctx.atKey = false; + let offset = fc.offset + fc.start.source.length; + for (let i = 0; i < fc.items.length; ++i) { + const collItem = fc.items[i]; + const { start, key, sep, value } = collItem; + const props = resolveProps(start, { + flow: fcName, + indicator: "explicit-key-ind", + next: key ?? sep?.[0], + offset, + onError, + parentIndent: fc.indent, + startOnNewline: false + }); + if (!props.found) { + if (!props.anchor && !props.tag && !sep && !value) { + if (i === 0 && props.comma) + onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`); + else if (i < fc.items.length - 1) + onError(props.start, "UNEXPECTED_TOKEN", `Unexpected empty item in ${fcName}`); + if (props.comment) { + if (coll.comment) + coll.comment += "\n" + props.comment; + else + coll.comment = props.comment; + } + offset = props.end; + continue; + } + if (!isMap2 && ctx.options.strict && containsNewline(key)) + onError( + key, + // checked by containsNewline() + "MULTILINE_IMPLICIT_KEY", + "Implicit keys of flow sequence pairs need to be on a single line" + ); + } + if (i === 0) { + if (props.comma) + onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`); + } else { + if (!props.comma) + onError(props.start, "MISSING_CHAR", `Missing , between ${fcName} items`); + if (props.comment) { + let prevItemComment = ""; + loop: for (const st of start) { + switch (st.type) { + case "comma": + case "space": + break; + case "comment": + prevItemComment = st.source.substring(1); + break loop; + default: + break loop; + } + } + if (prevItemComment) { + let prev = coll.items[coll.items.length - 1]; + if (isPair(prev)) + prev = prev.value ?? prev.key; + if (prev.comment) + prev.comment += "\n" + prevItemComment; + else + prev.comment = prevItemComment; + props.comment = props.comment.substring(prevItemComment.length + 1); + } + } + } + if (!isMap2 && !sep && !props.found) { + const valueNode = value ? composeNode2(ctx, value, props, onError) : composeEmptyNode2(ctx, props.end, sep, null, props, onError); + coll.items.push(valueNode); + offset = valueNode.range[2]; + if (isBlock(value)) + onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg); + } else { + ctx.atKey = true; + const keyStart = props.end; + const keyNode = key ? composeNode2(ctx, key, props, onError) : composeEmptyNode2(ctx, keyStart, start, null, props, onError); + if (isBlock(key)) + onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg); + ctx.atKey = false; + const valueProps = resolveProps(sep ?? [], { + flow: fcName, + indicator: "map-value-ind", + next: value, + offset: keyNode.range[2], + onError, + parentIndent: fc.indent, + startOnNewline: false + }); + if (valueProps.found) { + if (!isMap2 && !props.found && ctx.options.strict) { + if (sep) + for (const st of sep) { + if (st === valueProps.found) + break; + if (st.type === "newline") { + onError(st, "MULTILINE_IMPLICIT_KEY", "Implicit keys of flow sequence pairs need to be on a single line"); + break; + } + } + if (props.start < valueProps.found.offset - 1024) + onError(valueProps.found, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit flow sequence key"); + } + } else if (value) { + if ("source" in value && value.source?.[0] === ":") + onError(value, "MISSING_CHAR", `Missing space after : in ${fcName}`); + else + onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`); + } + const valueNode = value ? composeNode2(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode2(ctx, valueProps.end, sep, null, valueProps, onError) : null; + if (valueNode) { + if (isBlock(value)) + onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg); + } else if (valueProps.comment) { + if (keyNode.comment) + keyNode.comment += "\n" + valueProps.comment; + else + keyNode.comment = valueProps.comment; + } + const pair = new Pair(keyNode, valueNode); + if (ctx.options.keepSourceTokens) + pair.srcToken = collItem; + if (isMap2) { + const map2 = coll; + if (mapIncludes(ctx, map2.items, keyNode)) + onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique"); + map2.items.push(pair); + } else { + const map2 = new YAMLMap(ctx.schema); + map2.flow = true; + map2.items.push(pair); + const endRange = (valueNode ?? keyNode).range; + map2.range = [keyNode.range[0], endRange[1], endRange[2]]; + coll.items.push(map2); + } + offset = valueNode ? valueNode.range[2] : valueProps.end; + } + } + const expectedEnd = isMap2 ? "}" : "]"; + const [ce, ...ee] = fc.end; + let cePos = offset; + if (ce?.source === expectedEnd) + cePos = ce.offset + ce.source.length; + else { + const name = fcName[0].toUpperCase() + fcName.substring(1); + const msg = atRoot ? `${name} must end with a ${expectedEnd}` : `${name} in block collection must be sufficiently indented and end with a ${expectedEnd}`; + onError(offset, atRoot ? "MISSING_CHAR" : "BAD_INDENT", msg); + if (ce && ce.source.length !== 1) + ee.unshift(ce); + } + if (ee.length > 0) { + const end = resolveEnd(ee, cePos, ctx.options.strict, onError); + if (end.comment) { + if (coll.comment) + coll.comment += "\n" + end.comment; + else + coll.comment = end.comment; + } + coll.range = [fc.offset, cePos, end.offset]; + } else { + coll.range = [fc.offset, cePos, cePos]; + } + return coll; +} + +// node_modules/yaml/browser/dist/compose/compose-collection.js +function resolveCollection(CN2, ctx, token, onError, tagName, tag) { + const coll = token.type === "block-map" ? resolveBlockMap(CN2, ctx, token, onError, tag) : token.type === "block-seq" ? resolveBlockSeq(CN2, ctx, token, onError, tag) : resolveFlowCollection(CN2, ctx, token, onError, tag); + const Coll = coll.constructor; + if (tagName === "!" || tagName === Coll.tagName) { + coll.tag = Coll.tagName; + return coll; + } + if (tagName) + coll.tag = tagName; + return coll; +} +function composeCollection(CN2, ctx, token, props, onError) { + const tagToken = props.tag; + const tagName = !tagToken ? null : ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)); + if (token.type === "block-seq") { + const { anchor, newlineAfterProp: nl } = props; + const lastProp = anchor && tagToken ? anchor.offset > tagToken.offset ? anchor : tagToken : anchor ?? tagToken; + if (lastProp && (!nl || nl.offset < lastProp.offset)) { + const message = "Missing newline after block sequence props"; + onError(lastProp, "MISSING_CHAR", message); + } + } + const expType = token.type === "block-map" ? "map" : token.type === "block-seq" ? "seq" : token.start.source === "{" ? "map" : "seq"; + if (!tagToken || !tagName || tagName === "!" || tagName === YAMLMap.tagName && expType === "map" || tagName === YAMLSeq.tagName && expType === "seq") { + return resolveCollection(CN2, ctx, token, onError, tagName); + } + let tag = ctx.schema.tags.find((t) => t.tag === tagName && t.collection === expType); + if (!tag) { + const kt = ctx.schema.knownTags[tagName]; + if (kt?.collection === expType) { + ctx.schema.tags.push(Object.assign({}, kt, { default: false })); + tag = kt; + } else { + if (kt) { + onError(tagToken, "BAD_COLLECTION_TYPE", `${kt.tag} used for ${expType} collection, but expects ${kt.collection ?? "scalar"}`, true); + } else { + onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, true); + } + return resolveCollection(CN2, ctx, token, onError, tagName); + } + } + const coll = resolveCollection(CN2, ctx, token, onError, tagName, tag); + const res = tag.resolve?.(coll, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg), ctx.options) ?? coll; + const node = isNode(res) ? res : new Scalar(res); + node.range = coll.range; + node.tag = tagName; + if (tag?.format) + node.format = tag.format; + return node; +} + +// node_modules/yaml/browser/dist/compose/resolve-block-scalar.js +function resolveBlockScalar(ctx, scalar, onError) { + const start = scalar.offset; + const header = parseBlockScalarHeader(scalar, ctx.options.strict, onError); + if (!header) + return { value: "", type: null, comment: "", range: [start, start, start] }; + const type = header.mode === ">" ? Scalar.BLOCK_FOLDED : Scalar.BLOCK_LITERAL; + const lines = scalar.source ? splitLines(scalar.source) : []; + let chompStart = lines.length; + for (let i = lines.length - 1; i >= 0; --i) { + const content = lines[i][1]; + if (content === "" || content === "\r") + chompStart = i; + else + break; + } + if (chompStart === 0) { + const value2 = header.chomp === "+" && lines.length > 0 ? "\n".repeat(Math.max(1, lines.length - 1)) : ""; + let end2 = start + header.length; + if (scalar.source) + end2 += scalar.source.length; + return { value: value2, type, comment: header.comment, range: [start, end2, end2] }; + } + let trimIndent = scalar.indent + header.indent; + let offset = scalar.offset + header.length; + let contentStart = 0; + for (let i = 0; i < chompStart; ++i) { + const [indent, content] = lines[i]; + if (content === "" || content === "\r") { + if (header.indent === 0 && indent.length > trimIndent) + trimIndent = indent.length; + } else { + if (indent.length < trimIndent) { + const message = "Block scalars with more-indented leading empty lines must use an explicit indentation indicator"; + onError(offset + indent.length, "MISSING_CHAR", message); + } + if (header.indent === 0) + trimIndent = indent.length; + contentStart = i; + if (trimIndent === 0 && !ctx.atRoot) { + const message = "Block scalar values in collections must be indented"; + onError(offset, "BAD_INDENT", message); + } + break; + } + offset += indent.length + content.length + 1; + } + for (let i = lines.length - 1; i >= chompStart; --i) { + if (lines[i][0].length > trimIndent) + chompStart = i + 1; + } + let value = ""; + let sep = ""; + let prevMoreIndented = false; + for (let i = 0; i < contentStart; ++i) + value += lines[i][0].slice(trimIndent) + "\n"; + for (let i = contentStart; i < chompStart; ++i) { + let [indent, content] = lines[i]; + offset += indent.length + content.length + 1; + const crlf = content[content.length - 1] === "\r"; + if (crlf) + content = content.slice(0, -1); + if (content && indent.length < trimIndent) { + const src = header.indent ? "explicit indentation indicator" : "first line"; + const message = `Block scalar lines must not be less indented than their ${src}`; + onError(offset - content.length - (crlf ? 2 : 1), "BAD_INDENT", message); + indent = ""; + } + if (type === Scalar.BLOCK_LITERAL) { + value += sep + indent.slice(trimIndent) + content; + sep = "\n"; + } else if (indent.length > trimIndent || content[0] === " ") { + if (sep === " ") + sep = "\n"; + else if (!prevMoreIndented && sep === "\n") + sep = "\n\n"; + value += sep + indent.slice(trimIndent) + content; + sep = "\n"; + prevMoreIndented = true; + } else if (content === "") { + if (sep === "\n") + value += "\n"; + else + sep = "\n"; + } else { + value += sep + content; + sep = " "; + prevMoreIndented = false; + } + } + switch (header.chomp) { + case "-": + break; + case "+": + for (let i = chompStart; i < lines.length; ++i) + value += "\n" + lines[i][0].slice(trimIndent); + if (value[value.length - 1] !== "\n") + value += "\n"; + break; + default: + value += "\n"; + } + const end = start + header.length + scalar.source.length; + return { value, type, comment: header.comment, range: [start, end, end] }; +} +function parseBlockScalarHeader({ offset, props }, strict, onError) { + if (props[0].type !== "block-scalar-header") { + onError(props[0], "IMPOSSIBLE", "Block scalar header not found"); + return null; + } + const { source } = props[0]; + const mode = source[0]; + let indent = 0; + let chomp = ""; + let error = -1; + for (let i = 1; i < source.length; ++i) { + const ch = source[i]; + if (!chomp && (ch === "-" || ch === "+")) + chomp = ch; + else { + const n = Number(ch); + if (!indent && n) + indent = n; + else if (error === -1) + error = offset + i; + } + } + if (error !== -1) + onError(error, "UNEXPECTED_TOKEN", `Block scalar header includes extra characters: ${source}`); + let hasSpace = false; + let comment = ""; + let length = source.length; + for (let i = 1; i < props.length; ++i) { + const token = props[i]; + switch (token.type) { + case "space": + hasSpace = true; + // fallthrough + case "newline": + length += token.source.length; + break; + case "comment": + if (strict && !hasSpace) { + const message = "Comments must be separated from other tokens by white space characters"; + onError(token, "MISSING_CHAR", message); + } + length += token.source.length; + comment = token.source.substring(1); + break; + case "error": + onError(token, "UNEXPECTED_TOKEN", token.message); + length += token.source.length; + break; + /* istanbul ignore next should not happen */ + default: { + const message = `Unexpected token in block scalar header: ${token.type}`; + onError(token, "UNEXPECTED_TOKEN", message); + const ts = token.source; + if (ts && typeof ts === "string") + length += ts.length; + } + } + } + return { mode, indent, chomp, comment, length }; +} +function splitLines(source) { + const split = source.split(/\n( *)/); + const first = split[0]; + const m = first.match(/^( *)/); + const line0 = m?.[1] ? [m[1], first.slice(m[1].length)] : ["", first]; + const lines = [line0]; + for (let i = 1; i < split.length; i += 2) + lines.push([split[i], split[i + 1]]); + return lines; +} + +// node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +function resolveFlowScalar(scalar, strict, onError) { + const { offset, type, source, end } = scalar; + let _type; + let value; + const _onError = (rel, code, msg) => onError(offset + rel, code, msg); + switch (type) { + case "scalar": + _type = Scalar.PLAIN; + value = plainValue(source, _onError); + break; + case "single-quoted-scalar": + _type = Scalar.QUOTE_SINGLE; + value = singleQuotedValue(source, _onError); + break; + case "double-quoted-scalar": + _type = Scalar.QUOTE_DOUBLE; + value = doubleQuotedValue(source, _onError); + break; + /* istanbul ignore next should not happen */ + default: + onError(scalar, "UNEXPECTED_TOKEN", `Expected a flow scalar value, but found: ${type}`); + return { + value: "", + type: null, + comment: "", + range: [offset, offset + source.length, offset + source.length] + }; + } + const valueEnd = offset + source.length; + const re = resolveEnd(end, valueEnd, strict, onError); + return { + value, + type: _type, + comment: re.comment, + range: [offset, valueEnd, re.offset] + }; +} +function plainValue(source, onError) { + let badChar = ""; + switch (source[0]) { + /* istanbul ignore next should not happen */ + case " ": + badChar = "a tab character"; + break; + case ",": + badChar = "flow indicator character ,"; + break; + case "%": + badChar = "directive indicator character %"; + break; + case "|": + case ">": { + badChar = `block scalar indicator ${source[0]}`; + break; + } + case "@": + case "`": { + badChar = `reserved character ${source[0]}`; + break; + } + } + if (badChar) + onError(0, "BAD_SCALAR_START", `Plain value cannot start with ${badChar}`); + return unfoldLines(source); +} +function singleQuotedValue(source, onError) { + if (source[source.length - 1] !== "'" || source.length === 1) + onError(source.length, "MISSING_CHAR", "Missing closing 'quote"); + return unfoldLines(source.slice(1, -1)).replace(/''/g, "'"); +} +function unfoldLines(source) { + const line = /(.*?)\r?\n/sy; + let match = line.exec(source); + if (!match) + return source; + let trimEnd, trimBoth; + try { + trimEnd = new RegExp("(? wsStart ? source.slice(wsStart, i + 1) : ch; + } else { + res += ch; + } + } + if (source[source.length - 1] !== '"' || source.length === 1) + onError(source.length, "MISSING_CHAR", 'Missing closing "quote'); + return res; +} +function foldNewline(source, offset) { + let fold = ""; + let ch = source[offset + 1]; + while (ch === " " || ch === " " || ch === "\n" || ch === "\r") { + if (ch === "\r" && source[offset + 2] !== "\n") + break; + if (ch === "\n") + fold += "\n"; + offset += 1; + ch = source[offset + 1]; + } + if (!fold) + fold = " "; + return { fold, offset }; +} +var escapeCodes = { + "0": "\0", + // null character + a: "\x07", + // bell character + b: "\b", + // backspace + e: "\x1B", + // escape character + f: "\f", + // form feed + n: "\n", + // line feed + r: "\r", + // carriage return + t: " ", + // horizontal tab + v: "\v", + // vertical tab + N: "…", + // Unicode next line + _: " ", + // Unicode non-breaking space + L: "\u2028", + // Unicode line separator + P: "\u2029", + // Unicode paragraph separator + " ": " ", + '"': '"', + "/": "/", + "\\": "\\", + " ": " " +}; +function parseCharCode(source, offset, length, onError) { + const cc = source.substr(offset, length); + const ok = cc.length === length && /^[0-9a-fA-F]+$/.test(cc); + const code = ok ? parseInt(cc, 16) : NaN; + try { + return String.fromCodePoint(code); + } catch { + const raw = source.substr(offset - 2, length + 2); + onError(offset - 2, "BAD_DQ_ESCAPE", `Invalid escape sequence ${raw}`); + return raw; + } +} + +// node_modules/yaml/browser/dist/compose/compose-scalar.js +function composeScalar(ctx, token, tagToken, onError) { + const { value, type, comment, range } = token.type === "block-scalar" ? resolveBlockScalar(ctx, token, onError) : resolveFlowScalar(token, ctx.options.strict, onError); + const tagName = tagToken ? ctx.directives.tagName(tagToken.source, (msg) => onError(tagToken, "TAG_RESOLVE_FAILED", msg)) : null; + let tag; + if (ctx.options.stringKeys && ctx.atKey) { + tag = ctx.schema[SCALAR]; + } else if (tagName) + tag = findScalarTagByName(ctx.schema, value, tagName, tagToken, onError); + else if (token.type === "scalar") + tag = findScalarTagByTest(ctx, value, token, onError); + else + tag = ctx.schema[SCALAR]; + let scalar; + try { + const res = tag.resolve(value, (msg) => onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg), ctx.options); + scalar = isScalar(res) ? res : new Scalar(res); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + onError(tagToken ?? token, "TAG_RESOLVE_FAILED", msg); + scalar = new Scalar(value); + } + scalar.range = range; + scalar.source = value; + if (type) + scalar.type = type; + if (tagName) + scalar.tag = tagName; + if (tag.format) + scalar.format = tag.format; + if (comment) + scalar.comment = comment; + return scalar; +} +function findScalarTagByName(schema4, value, tagName, tagToken, onError) { + if (tagName === "!") + return schema4[SCALAR]; + const matchWithTest = []; + for (const tag of schema4.tags) { + if (!tag.collection && tag.tag === tagName) { + if (tag.default && tag.test) + matchWithTest.push(tag); + else + return tag; + } + } + for (const tag of matchWithTest) + if (tag.test?.test(value)) + return tag; + const kt = schema4.knownTags[tagName]; + if (kt && !kt.collection) { + schema4.tags.push(Object.assign({}, kt, { default: false, test: void 0 })); + return kt; + } + onError(tagToken, "TAG_RESOLVE_FAILED", `Unresolved tag: ${tagName}`, tagName !== "tag:yaml.org,2002:str"); + return schema4[SCALAR]; +} +function findScalarTagByTest({ atKey, directives, schema: schema4 }, value, token, onError) { + const tag = schema4.tags.find((tag2) => (tag2.default === true || atKey && tag2.default === "key") && tag2.test?.test(value)) || schema4[SCALAR]; + if (schema4.compat) { + const compat = schema4.compat.find((tag2) => tag2.default && tag2.test?.test(value)) ?? schema4[SCALAR]; + if (tag.tag !== compat.tag) { + const ts = directives.tagString(tag.tag); + const cs = directives.tagString(compat.tag); + const msg = `Value may be parsed as either ${ts} or ${cs}`; + onError(token, "TAG_RESOLVE_FAILED", msg, true); + } + } + return tag; +} + +// node_modules/yaml/browser/dist/compose/util-empty-scalar-position.js +function emptyScalarPosition(offset, before, pos) { + if (before) { + pos ?? (pos = before.length); + for (let i = pos - 1; i >= 0; --i) { + let st = before[i]; + switch (st.type) { + case "space": + case "comment": + case "newline": + offset -= st.source.length; + continue; + } + st = before[++i]; + while (st?.type === "space") { + offset += st.source.length; + st = before[++i]; + } + break; + } + } + return offset; +} + +// node_modules/yaml/browser/dist/compose/compose-node.js +var CN = { composeNode, composeEmptyNode }; +function composeNode(ctx, token, props, onError) { + const atKey = ctx.atKey; + const { spaceBefore, comment, anchor, tag } = props; + let node; + let isSrcToken = true; + switch (token.type) { + case "alias": + node = composeAlias(ctx, token, onError); + if (anchor || tag) + onError(token, "ALIAS_PROPS", "An alias node must not specify any properties"); + break; + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + case "block-scalar": + node = composeScalar(ctx, token, tag, onError); + if (anchor) + node.anchor = anchor.source.substring(1); + break; + case "block-map": + case "block-seq": + case "flow-collection": + try { + node = composeCollection(CN, ctx, token, props, onError); + if (anchor) + node.anchor = anchor.source.substring(1); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + onError(token, "RESOURCE_EXHAUSTION", message); + } + break; + default: { + const message = token.type === "error" ? token.message : `Unsupported token (type: ${token.type})`; + onError(token, "UNEXPECTED_TOKEN", message); + isSrcToken = false; + } + } + node ?? (node = composeEmptyNode(ctx, token.offset, void 0, null, props, onError)); + if (anchor && node.anchor === "") + onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string"); + if (atKey && ctx.options.stringKeys && (!isScalar(node) || typeof node.value !== "string" || node.tag && node.tag !== "tag:yaml.org,2002:str")) { + const msg = "With stringKeys, all keys must be strings"; + onError(tag ?? token, "NON_STRING_KEY", msg); + } + if (spaceBefore) + node.spaceBefore = true; + if (comment) { + if (token.type === "scalar" && token.source === "") + node.comment = comment; + else + node.commentBefore = comment; + } + if (ctx.options.keepSourceTokens && isSrcToken) + node.srcToken = token; + return node; +} +function composeEmptyNode(ctx, offset, before, pos, { spaceBefore, comment, anchor, tag, end }, onError) { + const token = { + type: "scalar", + offset: emptyScalarPosition(offset, before, pos), + indent: -1, + source: "" + }; + const node = composeScalar(ctx, token, tag, onError); + if (anchor) { + node.anchor = anchor.source.substring(1); + if (node.anchor === "") + onError(anchor, "BAD_ALIAS", "Anchor cannot be an empty string"); + } + if (spaceBefore) + node.spaceBefore = true; + if (comment) { + node.comment = comment; + node.range[2] = end; + } + return node; +} +function composeAlias({ options }, { offset, source, end }, onError) { + const alias = new Alias(source.substring(1)); + if (alias.source === "") + onError(offset, "BAD_ALIAS", "Alias cannot be an empty string"); + if (alias.source.endsWith(":")) + onError(offset + source.length - 1, "BAD_ALIAS", "Alias ending in : is ambiguous", true); + const valueEnd = offset + source.length; + const re = resolveEnd(end, valueEnd, options.strict, onError); + alias.range = [offset, valueEnd, re.offset]; + if (re.comment) + alias.comment = re.comment; + return alias; +} + +// node_modules/yaml/browser/dist/compose/compose-doc.js +function composeDoc(options, directives, { offset, start, value, end }, onError) { + const opts = Object.assign({ _directives: directives }, options); + const doc = new Document(void 0, opts); + const ctx = { + atKey: false, + atRoot: true, + directives: doc.directives, + options: doc.options, + schema: doc.schema + }; + const props = resolveProps(start, { + indicator: "doc-start", + next: value ?? end?.[0], + offset, + onError, + parentIndent: 0, + startOnNewline: true + }); + if (props.found) { + doc.directives.docStart = true; + if (value && (value.type === "block-map" || value.type === "block-seq") && !props.hasNewline) + onError(props.end, "MISSING_CHAR", "Block collection cannot start on same line with directives-end marker"); + } + doc.contents = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, start, null, props, onError); + const contentEnd = doc.contents.range[2]; + const re = resolveEnd(end, contentEnd, false, onError); + if (re.comment) + doc.comment = re.comment; + doc.range = [offset, contentEnd, re.offset]; + return doc; +} + +// node_modules/yaml/browser/dist/compose/composer.js +function getErrorPos(src) { + if (typeof src === "number") + return [src, src + 1]; + if (Array.isArray(src)) + return src.length === 2 ? src : [src[0], src[1]]; + const { offset, source } = src; + return [offset, offset + (typeof source === "string" ? source.length : 1)]; +} +function parsePrelude(prelude) { + let comment = ""; + let atComment = false; + let afterEmptyLine = false; + for (let i = 0; i < prelude.length; ++i) { + const source = prelude[i]; + switch (source[0]) { + case "#": + comment += (comment === "" ? "" : afterEmptyLine ? "\n\n" : "\n") + (source.substring(1) || " "); + atComment = true; + afterEmptyLine = false; + break; + case "%": + if (prelude[i + 1]?.[0] !== "#") + i += 1; + atComment = false; + break; + default: + if (!atComment) + afterEmptyLine = true; + atComment = false; + } + } + return { comment, afterEmptyLine }; +} +var Composer = class { + constructor(options = {}) { + this.doc = null; + this.atDirectives = false; + this.prelude = []; + this.errors = []; + this.warnings = []; + this.onError = (source, code, message, warning) => { + const pos = getErrorPos(source); + if (warning) + this.warnings.push(new YAMLWarning(pos, code, message)); + else + this.errors.push(new YAMLParseError(pos, code, message)); + }; + this.directives = new Directives({ version: options.version || "1.2" }); + this.options = options; + } + decorate(doc, afterDoc) { + const { comment, afterEmptyLine } = parsePrelude(this.prelude); + if (comment) { + const dc = doc.contents; + if (afterDoc) { + doc.comment = doc.comment ? `${doc.comment} +${comment}` : comment; + } else if (afterEmptyLine || doc.directives.docStart || !dc) { + doc.commentBefore = comment; + } else if (isCollection(dc) && !dc.flow && dc.items.length > 0) { + let it = dc.items[0]; + if (isPair(it)) + it = it.key; + const cb = it.commentBefore; + it.commentBefore = cb ? `${comment} +${cb}` : comment; + } else { + const cb = dc.commentBefore; + dc.commentBefore = cb ? `${comment} +${cb}` : comment; + } + } + if (afterDoc) { + for (let i = 0; i < this.errors.length; ++i) + doc.errors.push(this.errors[i]); + for (let i = 0; i < this.warnings.length; ++i) + doc.warnings.push(this.warnings[i]); + } else { + doc.errors = this.errors; + doc.warnings = this.warnings; + } + this.prelude = []; + this.errors = []; + this.warnings = []; + } + /** + * Current stream status information. + * + * Mostly useful at the end of input for an empty stream. + */ + streamInfo() { + return { + comment: parsePrelude(this.prelude).comment, + directives: this.directives, + errors: this.errors, + warnings: this.warnings + }; + } + /** + * Compose tokens into documents. + * + * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. + * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. + */ + *compose(tokens, forceDoc = false, endOffset = -1) { + for (const token of tokens) + yield* this.next(token); + yield* this.end(forceDoc, endOffset); + } + /** Advance the composer by one CST token. */ + *next(token) { + switch (token.type) { + case "directive": + this.directives.add(token.source, (offset, message, warning) => { + const pos = getErrorPos(token); + pos[0] += offset; + this.onError(pos, "BAD_DIRECTIVE", message, warning); + }); + this.prelude.push(token.source); + this.atDirectives = true; + break; + case "document": { + const doc = composeDoc(this.options, this.directives, token, this.onError); + if (this.atDirectives && !doc.directives.docStart) + this.onError(token, "MISSING_CHAR", "Missing directives-end/doc-start indicator line"); + this.decorate(doc, false); + if (this.doc) + yield this.doc; + this.doc = doc; + this.atDirectives = false; + break; + } + case "byte-order-mark": + case "space": + break; + case "comment": + case "newline": + this.prelude.push(token.source); + break; + case "error": { + const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message; + const error = new YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg); + if (this.atDirectives || !this.doc) + this.errors.push(error); + else + this.doc.errors.push(error); + break; + } + case "doc-end": { + if (!this.doc) { + const msg = "Unexpected doc-end without preceding document"; + this.errors.push(new YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", msg)); + break; + } + this.doc.directives.docEnd = true; + const end = resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError); + this.decorate(this.doc, true); + if (end.comment) { + const dc = this.doc.comment; + this.doc.comment = dc ? `${dc} +${end.comment}` : end.comment; + } + this.doc.range[2] = end.offset; + break; + } + default: + this.errors.push(new YAMLParseError(getErrorPos(token), "UNEXPECTED_TOKEN", `Unsupported token ${token.type}`)); + } + } + /** + * Call at end of input to yield any remaining document. + * + * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document. + * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly. + */ + *end(forceDoc = false, endOffset = -1) { + if (this.doc) { + this.decorate(this.doc, true); + yield this.doc; + this.doc = null; + } else if (forceDoc) { + const opts = Object.assign({ _directives: this.directives }, this.options); + const doc = new Document(void 0, opts); + if (this.atDirectives) + this.onError(endOffset, "MISSING_CHAR", "Missing directives-end indicator line"); + doc.range = [0, endOffset, endOffset]; + this.decorate(doc, false); + yield doc; + } + } +}; + +// node_modules/yaml/browser/dist/parse/cst-visit.js +var BREAK2 = /* @__PURE__ */ Symbol("break visit"); +var SKIP2 = /* @__PURE__ */ Symbol("skip children"); +var REMOVE2 = /* @__PURE__ */ Symbol("remove item"); +function visit2(cst, visitor) { + if ("type" in cst && cst.type === "document") + cst = { start: cst.start, value: cst.value }; + _visit(Object.freeze([]), cst, visitor); +} +visit2.BREAK = BREAK2; +visit2.SKIP = SKIP2; +visit2.REMOVE = REMOVE2; +visit2.itemAtPath = (cst, path) => { + let item = cst; + for (const [field, index] of path) { + const tok = item?.[field]; + if (tok && "items" in tok) { + item = tok.items[index]; + } else + return void 0; + } + return item; +}; +visit2.parentCollection = (cst, path) => { + const parent = visit2.itemAtPath(cst, path.slice(0, -1)); + const field = path[path.length - 1][0]; + const coll = parent?.[field]; + if (coll && "items" in coll) + return coll; + throw new Error("Parent collection not found"); +}; +function _visit(path, item, visitor) { + let ctrl = visitor(item, path); + if (typeof ctrl === "symbol") + return ctrl; + for (const field of ["key", "value"]) { + const token = item[field]; + if (token && "items" in token) { + for (let i = 0; i < token.items.length; ++i) { + const ci = _visit(Object.freeze(path.concat([[field, i]])), token.items[i], visitor); + if (typeof ci === "number") + i = ci - 1; + else if (ci === BREAK2) + return BREAK2; + else if (ci === REMOVE2) { + token.items.splice(i, 1); + i -= 1; + } + } + if (typeof ctrl === "function" && field === "key") + ctrl = ctrl(item, path); + } + } + return typeof ctrl === "function" ? ctrl(item, path) : ctrl; +} + +// node_modules/yaml/browser/dist/parse/cst.js +var BOM = "\uFEFF"; +var DOCUMENT = ""; +var FLOW_END = ""; +var SCALAR2 = ""; +function tokenType(source) { + switch (source) { + case BOM: + return "byte-order-mark"; + case DOCUMENT: + return "doc-mode"; + case FLOW_END: + return "flow-error-end"; + case SCALAR2: + return "scalar"; + case "---": + return "doc-start"; + case "...": + return "doc-end"; + case "": + case "\n": + case "\r\n": + return "newline"; + case "-": + return "seq-item-ind"; + case "?": + return "explicit-key-ind"; + case ":": + return "map-value-ind"; + case "{": + return "flow-map-start"; + case "}": + return "flow-map-end"; + case "[": + return "flow-seq-start"; + case "]": + return "flow-seq-end"; + case ",": + return "comma"; + } + switch (source[0]) { + case " ": + case " ": + return "space"; + case "#": + return "comment"; + case "%": + return "directive-line"; + case "*": + return "alias"; + case "&": + return "anchor"; + case "!": + return "tag"; + case "'": + return "single-quoted-scalar"; + case '"': + return "double-quoted-scalar"; + case "|": + case ">": + return "block-scalar-header"; + } + return null; +} + +// node_modules/yaml/browser/dist/parse/lexer.js +function isEmpty(ch) { + switch (ch) { + case void 0: + case " ": + case "\n": + case "\r": + case " ": + return true; + default: + return false; + } +} +var hexDigits = new Set("0123456789ABCDEFabcdef"); +var tagChars = new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()"); +var flowIndicatorChars = new Set(",[]{}"); +var invalidAnchorChars = new Set(" ,[]{}\n\r "); +var isNotAnchorChar = (ch) => !ch || invalidAnchorChars.has(ch); +var Lexer = class { + constructor() { + this.atEnd = false; + this.blockScalarIndent = -1; + this.blockScalarKeep = false; + this.buffer = ""; + this.flowKey = false; + this.flowLevel = 0; + this.indentNext = 0; + this.indentValue = 0; + this.lineEndPos = null; + this.next = null; + this.pos = 0; + } + /** + * Generate YAML tokens from the `source` string. If `incomplete`, + * a part of the last line may be left as a buffer for the next call. + * + * @returns A generator of lexical tokens + */ + *lex(source, incomplete = false) { + if (source) { + if (typeof source !== "string") + throw TypeError("source is not a string"); + this.buffer = this.buffer ? this.buffer + source : source; + this.lineEndPos = null; + } + this.atEnd = !incomplete; + let next = this.next ?? "stream"; + while (next && (incomplete || this.hasChars(1))) + next = yield* this.parseNext(next); + } + atLineEnd() { + let i = this.pos; + let ch = this.buffer[i]; + while (ch === " " || ch === " ") + ch = this.buffer[++i]; + if (!ch || ch === "#" || ch === "\n") + return true; + if (ch === "\r") + return this.buffer[i + 1] === "\n"; + return false; + } + charAt(n) { + return this.buffer[this.pos + n]; + } + continueScalar(offset) { + let ch = this.buffer[offset]; + if (this.indentNext > 0) { + let indent = 0; + while (ch === " ") + ch = this.buffer[++indent + offset]; + if (ch === "\r") { + const next = this.buffer[indent + offset + 1]; + if (next === "\n" || !next && !this.atEnd) + return offset + indent + 1; + } + return ch === "\n" || indent >= this.indentNext || !ch && !this.atEnd ? offset + indent : -1; + } + if (ch === "-" || ch === ".") { + const dt = this.buffer.substr(offset, 3); + if ((dt === "---" || dt === "...") && isEmpty(this.buffer[offset + 3])) + return -1; + } + return offset; + } + getLine() { + let end = this.lineEndPos; + if (typeof end !== "number" || end !== -1 && end < this.pos) { + end = this.buffer.indexOf("\n", this.pos); + this.lineEndPos = end; + } + if (end === -1) + return this.atEnd ? this.buffer.substring(this.pos) : null; + if (this.buffer[end - 1] === "\r") + end -= 1; + return this.buffer.substring(this.pos, end); + } + hasChars(n) { + return this.pos + n <= this.buffer.length; + } + setNext(state) { + this.buffer = this.buffer.substring(this.pos); + this.pos = 0; + this.lineEndPos = null; + this.next = state; + return null; + } + peek(n) { + return this.buffer.substr(this.pos, n); + } + *parseNext(next) { + switch (next) { + case "stream": + return yield* this.parseStream(); + case "line-start": + return yield* this.parseLineStart(); + case "block-start": + return yield* this.parseBlockStart(); + case "doc": + return yield* this.parseDocument(); + case "flow": + return yield* this.parseFlowCollection(); + case "quoted-scalar": + return yield* this.parseQuotedScalar(); + case "block-scalar": + return yield* this.parseBlockScalar(); + case "plain-scalar": + return yield* this.parsePlainScalar(); + } + } + *parseStream() { + let line = this.getLine(); + if (line === null) + return this.setNext("stream"); + if (line[0] === BOM) { + yield* this.pushCount(1); + line = line.substring(1); + } + if (line[0] === "%") { + let dirEnd = line.length; + let cs = line.indexOf("#"); + while (cs !== -1) { + const ch = line[cs - 1]; + if (ch === " " || ch === " ") { + dirEnd = cs - 1; + break; + } else { + cs = line.indexOf("#", cs + 1); + } + } + while (true) { + const ch = line[dirEnd - 1]; + if (ch === " " || ch === " ") + dirEnd -= 1; + else + break; + } + const n = (yield* this.pushCount(dirEnd)) + (yield* this.pushSpaces(true)); + yield* this.pushCount(line.length - n); + this.pushNewline(); + return "stream"; + } + if (this.atLineEnd()) { + const sp = yield* this.pushSpaces(true); + yield* this.pushCount(line.length - sp); + yield* this.pushNewline(); + return "stream"; + } + yield DOCUMENT; + return yield* this.parseLineStart(); + } + *parseLineStart() { + const ch = this.charAt(0); + if (!ch && !this.atEnd) + return this.setNext("line-start"); + if (ch === "-" || ch === ".") { + if (!this.atEnd && !this.hasChars(4)) + return this.setNext("line-start"); + const s = this.peek(3); + if ((s === "---" || s === "...") && isEmpty(this.charAt(3))) { + yield* this.pushCount(3); + this.indentValue = 0; + this.indentNext = 0; + return s === "---" ? "doc" : "stream"; + } + } + this.indentValue = yield* this.pushSpaces(false); + if (this.indentNext > this.indentValue && !isEmpty(this.charAt(1))) + this.indentNext = this.indentValue; + return yield* this.parseBlockStart(); + } + *parseBlockStart() { + const [ch0, ch1] = this.peek(2); + if (!ch1 && !this.atEnd) + return this.setNext("block-start"); + if ((ch0 === "-" || ch0 === "?" || ch0 === ":") && isEmpty(ch1)) { + const n = (yield* this.pushCount(1)) + (yield* this.pushSpaces(true)); + this.indentNext = this.indentValue + 1; + this.indentValue += n; + return "block-start"; + } + return "doc"; + } + *parseDocument() { + yield* this.pushSpaces(true); + const line = this.getLine(); + if (line === null) + return this.setNext("doc"); + let n = yield* this.pushIndicators(); + switch (line[n]) { + case "#": + yield* this.pushCount(line.length - n); + // fallthrough + case void 0: + yield* this.pushNewline(); + return yield* this.parseLineStart(); + case "{": + case "[": + yield* this.pushCount(1); + this.flowKey = false; + this.flowLevel = 1; + return "flow"; + case "}": + case "]": + yield* this.pushCount(1); + return "doc"; + case "*": + yield* this.pushUntil(isNotAnchorChar); + return "doc"; + case '"': + case "'": + return yield* this.parseQuotedScalar(); + case "|": + case ">": + n += yield* this.parseBlockScalarHeader(); + n += yield* this.pushSpaces(true); + yield* this.pushCount(line.length - n); + yield* this.pushNewline(); + return yield* this.parseBlockScalar(); + default: + return yield* this.parsePlainScalar(); + } + } + *parseFlowCollection() { + let nl, sp; + let indent = -1; + do { + nl = yield* this.pushNewline(); + if (nl > 0) { + sp = yield* this.pushSpaces(false); + this.indentValue = indent = sp; + } else { + sp = 0; + } + sp += yield* this.pushSpaces(true); + } while (nl + sp > 0); + const line = this.getLine(); + if (line === null) + return this.setNext("flow"); + if (indent !== -1 && indent < this.indentNext && line[0] !== "#" || indent === 0 && (line.startsWith("---") || line.startsWith("...")) && isEmpty(line[3])) { + const atFlowEndMarker = indent === this.indentNext - 1 && this.flowLevel === 1 && (line[0] === "]" || line[0] === "}"); + if (!atFlowEndMarker) { + this.flowLevel = 0; + yield FLOW_END; + return yield* this.parseLineStart(); + } + } + let n = 0; + while (line[n] === ",") { + n += yield* this.pushCount(1); + n += yield* this.pushSpaces(true); + this.flowKey = false; + } + n += yield* this.pushIndicators(); + switch (line[n]) { + case void 0: + return "flow"; + case "#": + yield* this.pushCount(line.length - n); + return "flow"; + case "{": + case "[": + yield* this.pushCount(1); + this.flowKey = false; + this.flowLevel += 1; + return "flow"; + case "}": + case "]": + yield* this.pushCount(1); + this.flowKey = true; + this.flowLevel -= 1; + return this.flowLevel ? "flow" : "doc"; + case "*": + yield* this.pushUntil(isNotAnchorChar); + return "flow"; + case '"': + case "'": + this.flowKey = true; + return yield* this.parseQuotedScalar(); + case ":": { + const next = this.charAt(1); + if (this.flowKey || isEmpty(next) || next === ",") { + this.flowKey = false; + yield* this.pushCount(1); + yield* this.pushSpaces(true); + return "flow"; + } + } + // fallthrough + default: + this.flowKey = false; + return yield* this.parsePlainScalar(); + } + } + *parseQuotedScalar() { + const quote = this.charAt(0); + let end = this.buffer.indexOf(quote, this.pos + 1); + if (quote === "'") { + while (end !== -1 && this.buffer[end + 1] === "'") + end = this.buffer.indexOf("'", end + 2); + } else { + while (end !== -1) { + let n = 0; + while (this.buffer[end - 1 - n] === "\\") + n += 1; + if (n % 2 === 0) + break; + end = this.buffer.indexOf('"', end + 1); + } + } + const qb = this.buffer.substring(0, end); + let nl = qb.indexOf("\n", this.pos); + if (nl !== -1) { + while (nl !== -1) { + const cs = this.continueScalar(nl + 1); + if (cs === -1) + break; + nl = qb.indexOf("\n", cs); + } + if (nl !== -1) { + end = nl - (qb[nl - 1] === "\r" ? 2 : 1); + } + } + if (end === -1) { + if (!this.atEnd) + return this.setNext("quoted-scalar"); + end = this.buffer.length; + } + yield* this.pushToIndex(end + 1, false); + return this.flowLevel ? "flow" : "doc"; + } + *parseBlockScalarHeader() { + this.blockScalarIndent = -1; + this.blockScalarKeep = false; + let i = this.pos; + while (true) { + const ch = this.buffer[++i]; + if (ch === "+") + this.blockScalarKeep = true; + else if (ch > "0" && ch <= "9") + this.blockScalarIndent = Number(ch) - 1; + else if (ch !== "-") + break; + } + return yield* this.pushUntil((ch) => isEmpty(ch) || ch === "#"); + } + *parseBlockScalar() { + let nl = this.pos - 1; + let indent = 0; + let ch; + loop: for (let i2 = this.pos; ch = this.buffer[i2]; ++i2) { + switch (ch) { + case " ": + indent += 1; + break; + case "\n": + nl = i2; + indent = 0; + break; + case "\r": { + const next = this.buffer[i2 + 1]; + if (!next && !this.atEnd) + return this.setNext("block-scalar"); + if (next === "\n") + break; + } + // fallthrough + default: + break loop; + } + } + if (!ch && !this.atEnd) + return this.setNext("block-scalar"); + if (indent >= this.indentNext) { + if (this.blockScalarIndent === -1) + this.indentNext = indent; + else { + this.indentNext = this.blockScalarIndent + (this.indentNext === 0 ? 1 : this.indentNext); + } + do { + const cs = this.continueScalar(nl + 1); + if (cs === -1) + break; + nl = this.buffer.indexOf("\n", cs); + } while (nl !== -1); + if (nl === -1) { + if (!this.atEnd) + return this.setNext("block-scalar"); + nl = this.buffer.length; + } + } + let i = nl + 1; + ch = this.buffer[i]; + while (ch === " ") + ch = this.buffer[++i]; + if (ch === " ") { + while (ch === " " || ch === " " || ch === "\r" || ch === "\n") + ch = this.buffer[++i]; + nl = i - 1; + } else if (!this.blockScalarKeep) { + do { + let i2 = nl - 1; + let ch2 = this.buffer[i2]; + if (ch2 === "\r") + ch2 = this.buffer[--i2]; + const lastChar = i2; + while (ch2 === " ") + ch2 = this.buffer[--i2]; + if (ch2 === "\n" && i2 >= this.pos && i2 + 1 + indent > lastChar) + nl = i2; + else + break; + } while (true); + } + yield SCALAR2; + yield* this.pushToIndex(nl + 1, true); + return yield* this.parseLineStart(); + } + *parsePlainScalar() { + const inFlow = this.flowLevel > 0; + let end = this.pos - 1; + let i = this.pos - 1; + let ch; + while (ch = this.buffer[++i]) { + if (ch === ":") { + const next = this.buffer[i + 1]; + if (isEmpty(next) || inFlow && flowIndicatorChars.has(next)) + break; + end = i; + } else if (isEmpty(ch)) { + let next = this.buffer[i + 1]; + if (ch === "\r") { + if (next === "\n") { + i += 1; + ch = "\n"; + next = this.buffer[i + 1]; + } else + end = i; + } + if (next === "#" || inFlow && flowIndicatorChars.has(next)) + break; + if (ch === "\n") { + const cs = this.continueScalar(i + 1); + if (cs === -1) + break; + i = Math.max(i, cs - 2); + } + } else { + if (inFlow && flowIndicatorChars.has(ch)) + break; + end = i; + } + } + if (!ch && !this.atEnd) + return this.setNext("plain-scalar"); + yield SCALAR2; + yield* this.pushToIndex(end + 1, true); + return inFlow ? "flow" : "doc"; + } + *pushCount(n) { + if (n > 0) { + yield this.buffer.substr(this.pos, n); + this.pos += n; + return n; + } + return 0; + } + *pushToIndex(i, allowEmpty) { + const s = this.buffer.slice(this.pos, i); + if (s) { + yield s; + this.pos += s.length; + return s.length; + } else if (allowEmpty) + yield ""; + return 0; + } + *pushIndicators() { + let n = 0; + loop: while (true) { + switch (this.charAt(0)) { + case "!": + n += yield* this.pushTag(); + n += yield* this.pushSpaces(true); + continue loop; + case "&": + n += yield* this.pushUntil(isNotAnchorChar); + n += yield* this.pushSpaces(true); + continue loop; + case "-": + // this is an error + case "?": + // this is an error outside flow collections + case ":": { + const inFlow = this.flowLevel > 0; + const ch1 = this.charAt(1); + if (isEmpty(ch1) || inFlow && flowIndicatorChars.has(ch1)) { + if (!inFlow) + this.indentNext = this.indentValue + 1; + else if (this.flowKey) + this.flowKey = false; + n += yield* this.pushCount(1); + n += yield* this.pushSpaces(true); + continue loop; + } + } + } + break loop; + } + return n; + } + *pushTag() { + if (this.charAt(1) === "<") { + let i = this.pos + 2; + let ch = this.buffer[i]; + while (!isEmpty(ch) && ch !== ">") + ch = this.buffer[++i]; + return yield* this.pushToIndex(ch === ">" ? i + 1 : i, false); + } else { + let i = this.pos + 1; + let ch = this.buffer[i]; + while (ch) { + if (tagChars.has(ch)) + ch = this.buffer[++i]; + else if (ch === "%" && hexDigits.has(this.buffer[i + 1]) && hexDigits.has(this.buffer[i + 2])) { + ch = this.buffer[i += 3]; + } else + break; + } + return yield* this.pushToIndex(i, false); + } + } + *pushNewline() { + const ch = this.buffer[this.pos]; + if (ch === "\n") + return yield* this.pushCount(1); + else if (ch === "\r" && this.charAt(1) === "\n") + return yield* this.pushCount(2); + else + return 0; + } + *pushSpaces(allowTabs) { + let i = this.pos - 1; + let ch; + do { + ch = this.buffer[++i]; + } while (ch === " " || allowTabs && ch === " "); + const n = i - this.pos; + if (n > 0) { + yield this.buffer.substr(this.pos, n); + this.pos = i; + } + return n; + } + *pushUntil(test) { + let i = this.pos; + let ch = this.buffer[i]; + while (!test(ch)) + ch = this.buffer[++i]; + return yield* this.pushToIndex(i, false); + } +}; + +// node_modules/yaml/browser/dist/parse/line-counter.js +var LineCounter = class { + constructor() { + this.lineStarts = []; + this.addNewLine = (offset) => this.lineStarts.push(offset); + this.linePos = (offset) => { + let low = 0; + let high = this.lineStarts.length; + while (low < high) { + const mid = low + high >> 1; + if (this.lineStarts[mid] < offset) + low = mid + 1; + else + high = mid; + } + if (this.lineStarts[low] === offset) + return { line: low + 1, col: 1 }; + if (low === 0) + return { line: 0, col: offset }; + const start = this.lineStarts[low - 1]; + return { line: low, col: offset - start + 1 }; + }; + } +}; + +// node_modules/yaml/browser/dist/parse/parser.js +function includesToken(list, type) { + for (let i = 0; i < list.length; ++i) + if (list[i].type === type) + return true; + return false; +} +function findNonEmptyIndex(list) { + for (let i = 0; i < list.length; ++i) { + switch (list[i].type) { + case "space": + case "comment": + case "newline": + break; + default: + return i; + } + } + return -1; +} +function isFlowToken(token) { + switch (token?.type) { + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + case "flow-collection": + return true; + default: + return false; + } +} +function getPrevProps(parent) { + switch (parent.type) { + case "document": + return parent.start; + case "block-map": { + const it = parent.items[parent.items.length - 1]; + return it.sep ?? it.start; + } + case "block-seq": + return parent.items[parent.items.length - 1].start; + /* istanbul ignore next should not happen */ + default: + return []; + } +} +function getFirstKeyStartProps(prev) { + if (prev.length === 0) + return []; + let i = prev.length; + loop: while (--i >= 0) { + switch (prev[i].type) { + case "doc-start": + case "explicit-key-ind": + case "map-value-ind": + case "seq-item-ind": + case "newline": + break loop; + } + } + while (prev[++i]?.type === "space") { + } + return prev.splice(i, prev.length); +} +function arrayPushArray(target, source) { + if (source.length < 1e5) + Array.prototype.push.apply(target, source); + else + for (let i = 0; i < source.length; ++i) + target.push(source[i]); +} +function fixFlowSeqItems(fc) { + if (fc.start.type === "flow-seq-start") { + for (const it of fc.items) { + if (it.sep && !it.value && !includesToken(it.start, "explicit-key-ind") && !includesToken(it.sep, "map-value-ind")) { + if (it.key) + it.value = it.key; + delete it.key; + if (isFlowToken(it.value)) { + if (it.value.end) + arrayPushArray(it.value.end, it.sep); + else + it.value.end = it.sep; + } else + arrayPushArray(it.start, it.sep); + delete it.sep; + } + } + } +} +var Parser = class { + /** + * @param onNewLine - If defined, called separately with the start position of + * each new line (in `parse()`, including the start of input). + */ + constructor(onNewLine) { + this.atNewLine = true; + this.atScalar = false; + this.indent = 0; + this.offset = 0; + this.onKeyLine = false; + this.stack = []; + this.source = ""; + this.type = ""; + this.lexer = new Lexer(); + this.onNewLine = onNewLine; + } + /** + * Parse `source` as a YAML stream. + * If `incomplete`, a part of the last line may be left as a buffer for the next call. + * + * Errors are not thrown, but yielded as `{ type: 'error', message }` tokens. + * + * @returns A generator of tokens representing each directive, document, and other structure. + */ + *parse(source, incomplete = false) { + if (this.onNewLine && this.offset === 0) + this.onNewLine(0); + for (const lexeme of this.lexer.lex(source, incomplete)) + yield* this.next(lexeme); + if (!incomplete) + yield* this.end(); + } + /** + * Advance the parser by the `source` of one lexical token. + */ + *next(source) { + this.source = source; + if (this.atScalar) { + this.atScalar = false; + yield* this.step(); + this.offset += source.length; + return; + } + const type = tokenType(source); + if (!type) { + const message = `Not a YAML token: ${source}`; + yield* this.pop({ type: "error", offset: this.offset, message, source }); + this.offset += source.length; + } else if (type === "scalar") { + this.atNewLine = false; + this.atScalar = true; + this.type = "scalar"; + } else { + this.type = type; + yield* this.step(); + switch (type) { + case "newline": + this.atNewLine = true; + this.indent = 0; + if (this.onNewLine) + this.onNewLine(this.offset + source.length); + break; + case "space": + if (this.atNewLine && source[0] === " ") + this.indent += source.length; + break; + case "explicit-key-ind": + case "map-value-ind": + case "seq-item-ind": + if (this.atNewLine) + this.indent += source.length; + break; + case "doc-mode": + case "flow-error-end": + return; + default: + this.atNewLine = false; + } + this.offset += source.length; + } + } + /** Call at end of input to push out any remaining constructions */ + *end() { + while (this.stack.length > 0) + yield* this.pop(); + } + get sourceToken() { + const st = { + type: this.type, + offset: this.offset, + indent: this.indent, + source: this.source + }; + return st; + } + *step() { + const top = this.peek(1); + if (this.type === "doc-end" && top?.type !== "doc-end") { + while (this.stack.length > 0) + yield* this.pop(); + this.stack.push({ + type: "doc-end", + offset: this.offset, + source: this.source + }); + return; + } + if (!top) + return yield* this.stream(); + switch (top.type) { + case "document": + return yield* this.document(top); + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return yield* this.scalar(top); + case "block-scalar": + return yield* this.blockScalar(top); + case "block-map": + return yield* this.blockMap(top); + case "block-seq": + return yield* this.blockSequence(top); + case "flow-collection": + return yield* this.flowCollection(top); + case "doc-end": + return yield* this.documentEnd(top); + } + yield* this.pop(); + } + peek(n) { + return this.stack[this.stack.length - n]; + } + *pop(error) { + const token = error ?? this.stack.pop(); + if (!token) { + const message = "Tried to pop an empty stack"; + yield { type: "error", offset: this.offset, source: "", message }; + } else if (this.stack.length === 0) { + yield token; + } else { + const top = this.peek(1); + if (token.type === "block-scalar") { + token.indent = "indent" in top ? top.indent : 0; + } else if (token.type === "flow-collection" && top.type === "document") { + token.indent = 0; + } + if (token.type === "flow-collection") + fixFlowSeqItems(token); + switch (top.type) { + case "document": + top.value = token; + break; + case "block-scalar": + top.props.push(token); + break; + case "block-map": { + const it = top.items[top.items.length - 1]; + if (it.value) { + top.items.push({ start: [], key: token, sep: [] }); + this.onKeyLine = true; + return; + } else if (it.sep) { + it.value = token; + } else { + Object.assign(it, { key: token, sep: [] }); + this.onKeyLine = !it.explicitKey; + return; + } + break; + } + case "block-seq": { + const it = top.items[top.items.length - 1]; + if (it.value) + top.items.push({ start: [], value: token }); + else + it.value = token; + break; + } + case "flow-collection": { + const it = top.items[top.items.length - 1]; + if (!it || it.value) + top.items.push({ start: [], key: token, sep: [] }); + else if (it.sep) + it.value = token; + else + Object.assign(it, { key: token, sep: [] }); + return; + } + /* istanbul ignore next should not happen */ + default: + yield* this.pop(); + yield* this.pop(token); + } + if ((top.type === "document" || top.type === "block-map" || top.type === "block-seq") && (token.type === "block-map" || token.type === "block-seq")) { + const last = token.items[token.items.length - 1]; + if (last && !last.sep && !last.value && last.start.length > 0 && findNonEmptyIndex(last.start) === -1 && (token.indent === 0 || last.start.every((st) => st.type !== "comment" || st.indent < token.indent))) { + if (top.type === "document") + top.end = last.start; + else + top.items.push({ start: last.start }); + token.items.splice(-1, 1); + } + } + } + } + *stream() { + switch (this.type) { + case "directive-line": + yield { type: "directive", offset: this.offset, source: this.source }; + return; + case "byte-order-mark": + case "space": + case "comment": + case "newline": + yield this.sourceToken; + return; + case "doc-mode": + case "doc-start": { + const doc = { + type: "document", + offset: this.offset, + start: [] + }; + if (this.type === "doc-start") + doc.start.push(this.sourceToken); + this.stack.push(doc); + return; + } + } + yield { + type: "error", + offset: this.offset, + message: `Unexpected ${this.type} token in YAML stream`, + source: this.source + }; + } + *document(doc) { + if (doc.value) + return yield* this.lineEnd(doc); + switch (this.type) { + case "doc-start": { + if (findNonEmptyIndex(doc.start) !== -1) { + yield* this.pop(); + yield* this.step(); + } else + doc.start.push(this.sourceToken); + return; + } + case "anchor": + case "tag": + case "space": + case "comment": + case "newline": + doc.start.push(this.sourceToken); + return; + } + const bv = this.startBlockValue(doc); + if (bv) + this.stack.push(bv); + else { + yield { + type: "error", + offset: this.offset, + message: `Unexpected ${this.type} token in YAML document`, + source: this.source + }; + } + } + *scalar(scalar) { + if (this.type === "map-value-ind") { + const prev = getPrevProps(this.peek(2)); + const start = getFirstKeyStartProps(prev); + let sep; + if (scalar.end) { + sep = scalar.end; + sep.push(this.sourceToken); + delete scalar.end; + } else + sep = [this.sourceToken]; + const map2 = { + type: "block-map", + offset: scalar.offset, + indent: scalar.indent, + items: [{ start, key: scalar, sep }] + }; + this.onKeyLine = true; + this.stack[this.stack.length - 1] = map2; + } else + yield* this.lineEnd(scalar); + } + *blockScalar(scalar) { + switch (this.type) { + case "space": + case "comment": + case "newline": + scalar.props.push(this.sourceToken); + return; + case "scalar": + scalar.source = this.source; + this.atNewLine = true; + this.indent = 0; + if (this.onNewLine) { + let nl = this.source.indexOf("\n") + 1; + while (nl !== 0) { + this.onNewLine(this.offset + nl); + nl = this.source.indexOf("\n", nl) + 1; + } + } + yield* this.pop(); + break; + /* istanbul ignore next should not happen */ + default: + yield* this.pop(); + yield* this.step(); + } + } + *blockMap(map2) { + const it = map2.items[map2.items.length - 1]; + switch (this.type) { + case "newline": + this.onKeyLine = false; + if (it.value) { + const end = "end" in it.value ? it.value.end : void 0; + const last = Array.isArray(end) ? end[end.length - 1] : void 0; + if (last?.type === "comment") + end?.push(this.sourceToken); + else + map2.items.push({ start: [this.sourceToken] }); + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + it.start.push(this.sourceToken); + } + return; + case "space": + case "comment": + if (it.value) { + map2.items.push({ start: [this.sourceToken] }); + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + if (this.atIndentedComment(it.start, map2.indent)) { + const prev = map2.items[map2.items.length - 2]; + const end = prev?.value?.end; + if (Array.isArray(end)) { + arrayPushArray(end, it.start); + end.push(this.sourceToken); + map2.items.pop(); + return; + } + } + it.start.push(this.sourceToken); + } + return; + } + if (this.indent >= map2.indent) { + const atMapIndent = !this.onKeyLine && this.indent === map2.indent; + const atNextItem = atMapIndent && (it.sep || it.explicitKey) && this.type !== "seq-item-ind"; + let start = []; + if (atNextItem && it.sep && !it.value) { + const nl = []; + for (let i = 0; i < it.sep.length; ++i) { + const st = it.sep[i]; + switch (st.type) { + case "newline": + nl.push(i); + break; + case "space": + break; + case "comment": + if (st.indent > map2.indent) + nl.length = 0; + break; + default: + nl.length = 0; + } + } + if (nl.length >= 2) + start = it.sep.splice(nl[1]); + } + switch (this.type) { + case "anchor": + case "tag": + if (atNextItem || it.value) { + start.push(this.sourceToken); + map2.items.push({ start }); + this.onKeyLine = true; + } else if (it.sep) { + it.sep.push(this.sourceToken); + } else { + it.start.push(this.sourceToken); + } + return; + case "explicit-key-ind": + if (!it.sep && !it.explicitKey) { + it.start.push(this.sourceToken); + it.explicitKey = true; + } else if (atNextItem || it.value) { + start.push(this.sourceToken); + map2.items.push({ start, explicitKey: true }); + } else { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: [this.sourceToken], explicitKey: true }] + }); + } + this.onKeyLine = true; + return; + case "map-value-ind": + if (it.explicitKey) { + if (!it.sep) { + if (includesToken(it.start, "newline")) { + Object.assign(it, { key: null, sep: [this.sourceToken] }); + } else { + const start2 = getFirstKeyStartProps(it.start); + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: start2, key: null, sep: [this.sourceToken] }] + }); + } + } else if (it.value) { + map2.items.push({ start: [], key: null, sep: [this.sourceToken] }); + } else if (includesToken(it.sep, "map-value-ind")) { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, key: null, sep: [this.sourceToken] }] + }); + } else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) { + const start2 = getFirstKeyStartProps(it.start); + const key = it.key; + const sep = it.sep; + sep.push(this.sourceToken); + delete it.key; + delete it.sep; + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: start2, key, sep }] + }); + } else if (start.length > 0) { + it.sep = it.sep.concat(start, this.sourceToken); + } else { + it.sep.push(this.sourceToken); + } + } else { + if (!it.sep) { + Object.assign(it, { key: null, sep: [this.sourceToken] }); + } else if (it.value || atNextItem) { + map2.items.push({ start, key: null, sep: [this.sourceToken] }); + } else if (includesToken(it.sep, "map-value-ind")) { + this.stack.push({ + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start: [], key: null, sep: [this.sourceToken] }] + }); + } else { + it.sep.push(this.sourceToken); + } + } + this.onKeyLine = true; + return; + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": { + const fs = this.flowScalar(this.type); + if (atNextItem || it.value) { + map2.items.push({ start, key: fs, sep: [] }); + this.onKeyLine = true; + } else if (it.sep) { + this.stack.push(fs); + } else { + Object.assign(it, { key: fs, sep: [] }); + this.onKeyLine = true; + } + return; + } + default: { + const bv = this.startBlockValue(map2); + if (bv) { + if (bv.type === "block-seq") { + if (!it.explicitKey && it.sep && !includesToken(it.sep, "newline")) { + yield* this.pop({ + type: "error", + offset: this.offset, + message: "Unexpected block-seq-ind on same line with key", + source: this.source + }); + return; + } + } else if (atMapIndent) { + map2.items.push({ start }); + } + this.stack.push(bv); + return; + } + } + } + } + yield* this.pop(); + yield* this.step(); + } + *blockSequence(seq2) { + const it = seq2.items[seq2.items.length - 1]; + switch (this.type) { + case "newline": + if (it.value) { + const end = "end" in it.value ? it.value.end : void 0; + const last = Array.isArray(end) ? end[end.length - 1] : void 0; + if (last?.type === "comment") + end?.push(this.sourceToken); + else + seq2.items.push({ start: [this.sourceToken] }); + } else + it.start.push(this.sourceToken); + return; + case "space": + case "comment": + if (it.value) + seq2.items.push({ start: [this.sourceToken] }); + else { + if (this.atIndentedComment(it.start, seq2.indent)) { + const prev = seq2.items[seq2.items.length - 2]; + const end = prev?.value?.end; + if (Array.isArray(end)) { + arrayPushArray(end, it.start); + end.push(this.sourceToken); + seq2.items.pop(); + return; + } + } + it.start.push(this.sourceToken); + } + return; + case "anchor": + case "tag": + if (it.value || this.indent <= seq2.indent) + break; + it.start.push(this.sourceToken); + return; + case "seq-item-ind": + if (this.indent !== seq2.indent) + break; + if (it.value || includesToken(it.start, "seq-item-ind")) + seq2.items.push({ start: [this.sourceToken] }); + else + it.start.push(this.sourceToken); + return; + } + if (this.indent > seq2.indent) { + const bv = this.startBlockValue(seq2); + if (bv) { + this.stack.push(bv); + return; + } + } + yield* this.pop(); + yield* this.step(); + } + *flowCollection(fc) { + const it = fc.items[fc.items.length - 1]; + if (this.type === "flow-error-end") { + let top; + do { + yield* this.pop(); + top = this.peek(1); + } while (top?.type === "flow-collection"); + } else if (fc.end.length === 0) { + switch (this.type) { + case "comma": + case "explicit-key-ind": + if (!it || it.sep) + fc.items.push({ start: [this.sourceToken] }); + else + it.start.push(this.sourceToken); + return; + case "map-value-ind": + if (!it || it.value) + fc.items.push({ start: [], key: null, sep: [this.sourceToken] }); + else if (it.sep) + it.sep.push(this.sourceToken); + else + Object.assign(it, { key: null, sep: [this.sourceToken] }); + return; + case "space": + case "comment": + case "newline": + case "anchor": + case "tag": + if (!it || it.value) + fc.items.push({ start: [this.sourceToken] }); + else if (it.sep) + it.sep.push(this.sourceToken); + else + it.start.push(this.sourceToken); + return; + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": { + const fs = this.flowScalar(this.type); + if (!it || it.value) + fc.items.push({ start: [], key: fs, sep: [] }); + else if (it.sep) + this.stack.push(fs); + else + Object.assign(it, { key: fs, sep: [] }); + return; + } + case "flow-map-end": + case "flow-seq-end": + fc.end.push(this.sourceToken); + return; + } + const bv = this.startBlockValue(fc); + if (bv) + this.stack.push(bv); + else { + yield* this.pop(); + yield* this.step(); + } + } else { + const parent = this.peek(2); + if (parent.type === "block-map" && (this.type === "map-value-ind" && parent.indent === fc.indent || this.type === "newline" && !parent.items[parent.items.length - 1].sep)) { + yield* this.pop(); + yield* this.step(); + } else if (this.type === "map-value-ind" && parent.type !== "flow-collection") { + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + fixFlowSeqItems(fc); + const sep = fc.end.splice(1, fc.end.length); + sep.push(this.sourceToken); + const map2 = { + type: "block-map", + offset: fc.offset, + indent: fc.indent, + items: [{ start, key: fc, sep }] + }; + this.onKeyLine = true; + this.stack[this.stack.length - 1] = map2; + } else { + yield* this.lineEnd(fc); + } + } + } + flowScalar(type) { + if (this.onNewLine) { + let nl = this.source.indexOf("\n") + 1; + while (nl !== 0) { + this.onNewLine(this.offset + nl); + nl = this.source.indexOf("\n", nl) + 1; + } + } + return { + type, + offset: this.offset, + indent: this.indent, + source: this.source + }; + } + startBlockValue(parent) { + switch (this.type) { + case "alias": + case "scalar": + case "single-quoted-scalar": + case "double-quoted-scalar": + return this.flowScalar(this.type); + case "block-scalar-header": + return { + type: "block-scalar", + offset: this.offset, + indent: this.indent, + props: [this.sourceToken], + source: "" + }; + case "flow-map-start": + case "flow-seq-start": + return { + type: "flow-collection", + offset: this.offset, + indent: this.indent, + start: this.sourceToken, + items: [], + end: [] + }; + case "seq-item-ind": + return { + type: "block-seq", + offset: this.offset, + indent: this.indent, + items: [{ start: [this.sourceToken] }] + }; + case "explicit-key-ind": { + this.onKeyLine = true; + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + start.push(this.sourceToken); + return { + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, explicitKey: true }] + }; + } + case "map-value-ind": { + this.onKeyLine = true; + const prev = getPrevProps(parent); + const start = getFirstKeyStartProps(prev); + return { + type: "block-map", + offset: this.offset, + indent: this.indent, + items: [{ start, key: null, sep: [this.sourceToken] }] + }; + } + } + return null; + } + atIndentedComment(start, indent) { + if (this.type !== "comment") + return false; + if (this.indent <= indent) + return false; + return start.every((st) => st.type === "newline" || st.type === "space"); + } + *documentEnd(docEnd) { + if (this.type !== "doc-mode") { + if (docEnd.end) + docEnd.end.push(this.sourceToken); + else + docEnd.end = [this.sourceToken]; + if (this.type === "newline") + yield* this.pop(); + } + } + *lineEnd(token) { + switch (this.type) { + case "comma": + case "doc-start": + case "doc-end": + case "flow-seq-end": + case "flow-map-end": + case "map-value-ind": + yield* this.pop(); + yield* this.step(); + break; + case "newline": + this.onKeyLine = false; + // fallthrough + case "space": + case "comment": + default: + if (token.end) + token.end.push(this.sourceToken); + else + token.end = [this.sourceToken]; + if (this.type === "newline") + yield* this.pop(); + } + } +}; + +// node_modules/yaml/browser/dist/public-api.js +function parseOptions(options) { + const prettyErrors = options.prettyErrors !== false; + const lineCounter = options.lineCounter || prettyErrors && new LineCounter() || null; + return { lineCounter, prettyErrors }; +} +function parseDocument(source, options = {}) { + const { lineCounter, prettyErrors } = parseOptions(options); + const parser = new Parser(lineCounter?.addNewLine); + const composer = new Composer(options); + let doc = null; + for (const _doc of composer.compose(parser.parse(source), true, source.length)) { + if (!doc) + doc = _doc; + else if (doc.options.logLevel !== "silent") { + doc.errors.push(new YAMLParseError(_doc.range.slice(0, 2), "MULTIPLE_DOCS", "Source contains multiple documents; please use YAML.parseAllDocuments()")); + break; + } + } + if (prettyErrors && lineCounter) { + doc.errors.forEach(prettifyError(source, lineCounter)); + doc.warnings.forEach(prettifyError(source, lineCounter)); + } + return doc; +} +function parse(src, reviver, options) { + let _reviver = void 0; + if (typeof reviver === "function") { + _reviver = reviver; + } else if (options === void 0 && reviver && typeof reviver === "object") { + options = reviver; + } + const doc = parseDocument(src, options); + if (!doc) + return null; + doc.warnings.forEach((warning) => warn(doc.options.logLevel, warning)); + if (doc.errors.length > 0) { + if (doc.options.logLevel !== "silent") + throw doc.errors[0]; + else + doc.errors = []; + } + return doc.toJS(Object.assign({ reviver: _reviver }, options)); +} + +// library/repository-validator/src/validator/adapters/workflow-files.mjs +var CORE_GHA_PATH = /^opensesame\/core-github-actions\/\.github\/(workflows\/[^/@]+?\.ya?ml|actions\/[^@]+?)(?:\/action\.ya?ml)?$/i; +var LOCAL_COMPONENT_PATH = /^\.\/\.github\/(workflows\/[^/@]+?\.ya?ml|actions\/[^@]+?)(?:\/action\.ya?ml)?$/i; +function loadWorkflowFiles(source, githubActions) { + const workflowPattern = new RegExp(githubActions.workflowPattern); + const actionPattern = new RegExp(githubActions.actionPattern); + const files = []; + const findings = []; + for (const path of source.files) { + const kind = workflowPattern.test(path) ? "workflow" : actionPattern.test(path) ? "action" : null; + if (!kind) continue; + let text; + try { + text = source.read(path); + } catch (error) { + findings.push( + createRuleFinding( + "error", + "GHA002", + "workflow-syntax", + path, + `Cannot read ${path}: ${error?.message ?? "unknown error"}.` + ) + ); + continue; + } + const lineCounter = new LineCounter(); + const document = parseDocument(text, { lineCounter, prettyErrors: false }); + let data = null; + let conversionError = null; + if (document.errors.length === 0) { + try { + data = document.toJS({ maxAliasCount: 1e3 }); + } catch (error) { + conversionError = error; + } + } + if (!isObject(data)) { + const detail = (document.errors[0] ?? conversionError)?.message?.split("\n")[0] ?? "the document is not a map"; + findings.push( + createRuleFinding( + "error", + "GHA003", + "workflow-syntax", + path, + `${path} is not a valid workflow or action file: ${detail}.` + ) + ); + continue; + } + files.push({ + path, + kind, + lines: text.split(/\r?\n/), + document, + data, + lineOf: (node) => node?.range ? lineCounter.linePos(node.range[0]).line : null + }); + } + return { files, findings }; +} +function readTriggers(data) { + const triggers = data.on; + if (typeof triggers === "string") return { [triggers]: {} }; + if (Array.isArray(triggers)) return Object.fromEntries(triggers.map((name) => [name, {}])); + if (!isObject(triggers)) return {}; + return Object.fromEntries( + Object.entries(triggers).map(([name, value]) => [name, isObject(value) ? value : {}]) + ); +} +function readJobs(data) { + return isObject(data.jobs) ? data.jobs : {}; +} +function listSteps(file) { + if (file.kind === "action") { + const steps = file.data.runs?.steps; + return Array.isArray(steps) ? steps.map((step) => ({ jobId: null, step })) : []; + } + return Object.entries(readJobs(file.data)).flatMap( + ([jobId, job]) => Array.isArray(job?.steps) ? job.steps.map((step) => ({ jobId, step })) : [] + ); +} +function asList(value) { + if (value === void 0 || value === null) return []; + return Array.isArray(value) ? value : [value]; +} +function normalizeExpression(value) { + return String(value ?? "").replace(/\$\{\{\s*/g, "${{ ").replace(/\s*\}\}/g, " }}").replace(/\s+/g, " ").trim(); +} +function stripRef(uses) { + return String(uses ?? "").trim().split("@")[0].toLowerCase(); +} +function toComponent(match) { + return match ? match[1].replace(/\.ya?ml$/, "").replace(/\/$/, "") : null; +} +function coreGhaComponent(uses) { + return toComponent(stripRef(uses).match(CORE_GHA_PATH)); +} +function createComponentResolver(isCoreGithubActions) { + let provider; + return (uses) => { + const local = String(uses ?? "").trim().match(LOCAL_COMPONENT_PATH); + if (!local) return coreGhaComponent(uses); + provider ??= isCoreGithubActions(); + return provider ? toComponent(local) : null; + }; +} +function isCommentLine(line) { + return line.trim().startsWith("#"); +} + +// library/repository-validator/src/validator/rules/ci-workflow.mjs +var FILTER_KEYS = ["branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore"]; +function finding(severity, code, requirement, path, message) { + return createRuleFinding(severity, code, requirement, path, message); +} +function jobRequirement(jobSpec) { + return jobSpec.waivable ? `ci-job:${jobSpec.id}` : "ci-jobs"; +} +function hasSurface(jobSpec, discovered) { + return !jobSpec.surfaces || jobSpec.surfaces.some((surface) => discovered.has(surface)); +} +function hasScripts(jobSpec, packageScripts) { + return !jobSpec.requiredWithScripts || packageScripts.unreadable || jobSpec.requiredWithScripts.some( + (name) => packageScripts.names.has(name) || [...packageScripts.names].some((script) => script.startsWith(`${name}:`)) + ); +} +function isRequired(jobSpec, discovered, packageScripts) { + return hasSurface(jobSpec, discovered) && hasScripts(jobSpec, packageScripts); +} +function validateTriggers(file, policy) { + const findings = []; + const triggers = readTriggers(file.data); + const push = triggers.push; + const filters = push ? FILTER_KEYS.filter((key) => push[key] !== void 0) : []; + if (!push || filters.length > 0) { + findings.push( + finding( + "error", + "GHA010", + "ci-triggers", + file.path, + push ? `CI push trigger must run on every push, including main; remove ${filters.join(", ")}.` : "CI must run on push." + ) + ); + } + for (const trigger of policy.requiredTriggers.filter((name) => name !== "push")) { + if (!triggers[trigger]) { + findings.push( + finding("error", "GHA011", "ci-triggers", file.path, `CI must run on ${trigger}.`) + ); + } + } + const inputs = triggers.workflow_dispatch?.inputs; + if (triggers.workflow_dispatch && !isObject(inputs?.[policy.dispatchRevisionInput])) { + findings.push( + finding( + "error", + "GHA012", + "dispatch-revision-input", + file.path, + `workflow_dispatch must define the ${policy.dispatchRevisionInput} input.` + ) + ); + } + if (findings.length === 0) { + findings.push( + finding( + "pass", + "GHA013", + "ci-triggers", + file.path, + `CI runs on ${policy.requiredTriggers.join(", ")} with a ${policy.dispatchRevisionInput} dispatch input.` + ) + ); + } + return findings; +} +function isGated(condition, gate) { + const bare = normalizeExpression(condition).replace(/^\$\{\{ (.*) \}\}$/, "$1").trim(); + return bare === gate || bare === `!cancelled() && ${gate}`; +} +function matchesName(jobSpec, name) { + if (typeof name !== "string") return false; + return jobSpec.namePattern ? new RegExp(jobSpec.namePattern).test(name) : name === jobSpec.name; +} +function readWith(job, key) { + return isObject(job.with) ? job.with[key] : void 0; +} +function findCheckoutRef(job) { + const checkout = asList(job.steps).find( + (step) => stripRef(step?.uses).startsWith("actions/checkout") + ); + return checkout ? isObject(checkout.with) ? checkout.with.ref : void 0 : null; +} +function describePermissions(permissions) { + return Object.entries(permissions).map(([scope, level]) => `${scope}: ${level}`).join(", "); +} +function checkPermissions(jobSpec, job) { + if (!jobSpec.permissions) return []; + const actual = isObject(job.permissions) ? job.permissions : {}; + const expected = jobSpec.permissions; + const matches = Object.keys(actual).length === Object.keys(expected).length && Object.entries(expected).every(([scope, level]) => String(actual[scope]) === level); + if (matches) return []; + return [ + [ + "GHA028", + `Job ${jobSpec.id} must grant exactly ${describePermissions(expected)}; found ${isObject(job.permissions) ? describePermissions(job.permissions) || "{}" : "no permissions"}.` + ] + ]; +} +function checkSecrets(jobSpec, job) { + if (!jobSpec.secrets) return []; + const secrets = isObject(job.secrets) ? job.secrets : {}; + return jobSpec.secrets.filter((name) => normalizeExpression(secrets[name]) !== `\${{ secrets.${name} }}`).map((name) => [ + "GHA029", + `Job ${jobSpec.id} must pass ${name}: \${{ secrets.${name} }} under secrets${job.secrets === "inherit" ? ", not secrets: inherit" : ""}.` + ]); +} +function validateJob(file, policy, jobSpec, job, requirement = "ci-jobs", componentOf = coreGhaComponent) { + const problems = []; + const label = `${jobSpec.id} (${jobSpec.name})`; + if (!matchesName(jobSpec, job.name)) { + problems.push([ + "GHA021", + `Job ${jobSpec.id} must be named ${jobSpec.namePattern ? `${jobSpec.name} or ${jobSpec.name} ()` : jobSpec.name} so its checks report as ${jobSpec.checks.join(", ")}; found ${job.name ?? "no name"}.` + ]); + } + if (jobSpec.uses) { + const component = componentOf(job.uses); + if (!jobSpec.uses.includes(component)) { + problems.push([ + "GHA022", + `Job ${jobSpec.id} must call core-github-actions ${jobSpec.uses.join(" or ")}; found ${job.uses ?? "no reusable workflow"}.` + ]); + } + } else if (job.uses || !job["runs-on"] || !Array.isArray(job.steps)) { + problems.push(["GHA023", `Job ${jobSpec.id} must be a repo-owned job with runs-on and steps.`]); + } + if (problems.some(([code]) => code === "GHA022" || code === "GHA023")) { + return problems.map( + ([code, message]) => finding("error", code, requirement, file.path, message) + ); + } + const context = policy.contextJob; + const revisionKey = policy.dispatchRevisionInput; + if (jobSpec.id === context) { + if (normalizeExpression(readWith(job, revisionKey)) !== policy.contextRevision) { + problems.push([ + "GHA026", + `Job ${context} must pass ${revisionKey}: ${policy.contextRevision}.` + ]); + } + } else { + if (!asList(job.needs).includes(context)) { + problems.push(["GHA024", `Job ${jobSpec.id} must need ${context}.`]); + } + if (!isGated(job.if, policy.jobCondition)) { + problems.push([ + "GHA025", + `Job ${jobSpec.id} must use if: ${policy.jobCondition}, optionally after !cancelled() &&.` + ]); + } + const revision = jobSpec.uses ? readWith(job, revisionKey) : findCheckoutRef(job); + if (normalizeExpression(revision) !== policy.jobRevision) { + problems.push([ + "GHA026", + jobSpec.uses ? `Job ${jobSpec.id} must pass ${revisionKey}: ${policy.jobRevision}.` : `Job ${jobSpec.id} must check out ref: ${policy.jobRevision}.` + ]); + } + } + problems.push(...checkPermissions(jobSpec, job), ...checkSecrets(jobSpec, job)); + if (problems.length === 0) { + return [ + finding( + "pass", + "GHA027", + requirement, + file.path, + `${label} reports ${jobSpec.checks.join(", ")}.` + ) + ]; + } + return problems.map(([code, message]) => finding("error", code, requirement, file.path, message)); +} +function validateResultJob(file, policy, jobs, componentOf) { + const spec = policy.resultJob; + const job = jobs[spec.id]; + if (!isObject(job)) { + return [ + finding( + "error", + "GHA030", + "ci-result", + file.path, + `Missing ${spec.id} job (${spec.name}) that aggregates every CI job.` + ) + ]; + } + const problems = []; + if (job.name !== spec.name) { + problems.push([ + "GHA031", + `Job ${spec.id} must be named ${spec.name}; found ${job.name ?? "no name"}.` + ]); + } + const needs = new Set(asList(job.needs)); + const missing = Object.keys(jobs).filter((id) => id !== spec.id && !needs.has(id)); + if (missing.length > 0) { + problems.push([ + "GHA032", + `${spec.name} must need every other CI job; missing ${missing.join(", ")}.` + ]); + } + if (normalizeExpression(job.if) !== normalizeExpression(spec.if)) { + problems.push(["GHA033", `${spec.name} must use if: ${spec.if}.`]); + } + if (!isObject(job.permissions) || Object.keys(job.permissions).length > 0) { + problems.push(["GHA034", `${spec.name} must set permissions: {}.`]); + } + const steps = asList(job.steps); + const resultStep = steps.find((step) => componentOf(step?.uses) === spec.action); + if (!resultStep) { + problems.push(["GHA035", `${spec.name} must run core-github-actions ${spec.action}.`]); + } else { + for (const [key, value] of Object.entries(spec.with)) { + const actual = isObject(resultStep.with) ? resultStep.with[key] : void 0; + if (normalizeExpression(actual) !== normalizeExpression(value)) { + problems.push(["GHA035", `${spec.action} must receive ${key}: ${value}.`]); + } + } + if (resultStep.name !== spec.stepName) { + problems.push(["GHA035", `The ${spec.action} step must be named ${spec.stepName}.`]); + } + } + if (problems.length === 0) { + return [ + finding( + "pass", + "GHA036", + "ci-result", + file.path, + `${spec.name} needs all ${needs.size} other jobs and runs ${spec.action}.` + ) + ]; + } + return problems.map(([code, message]) => finding("error", code, "ci-result", file.path, message)); +} +function describeSurfaces(policy, discovered, packageScripts) { + const surfaces = [...discovered].filter((surface) => policy.jobs.some((jobSpec) => jobSpec.surfaces?.includes(surface))).sort(); + const required = [ + ...policy.jobs.filter((jobSpec) => isRequired(jobSpec, discovered, packageScripts)).map(({ id }) => id), + policy.resultJob.id + ]; + const withoutScripts = policy.jobs.filter((jobSpec) => hasSurface(jobSpec, discovered) && !hasScripts(jobSpec, packageScripts)).map( + (jobSpec) => `; ${jobSpec.id} is optional because no package.json defines ${jobSpec.requiredWithScripts.join(" or ")}` + ).join(""); + return finding( + "pass", + "GHA009", + "ci-jobs", + policy.path, + `Detected CI surfaces: ${surfaces.join(", ") || "none"}; required jobs: ${required.join(", ")}${withoutScripts}.` + ); +} +function validateCiWorkflow(workflowFiles, policy, discovered, packageScripts = { names: /* @__PURE__ */ new Set(), unreadable: false }, componentOf = coreGhaComponent) { + const summary = describeSurfaces(policy, discovered, packageScripts); + const file = workflowFiles.find((candidate) => candidate.path === policy.path); + if (!file) { + return [ + summary, + finding( + "error", + "GHA001", + "ci-workflow", + policy.path, + `Missing or unparseable CI workflow: ${policy.path}.` + ) + ]; + } + const findings = [summary, ...validateTriggers(file, policy)]; + const jobs = readJobs(file.data); + for (const jobSpec of policy.jobs) { + if (!isRequired(jobSpec, discovered, packageScripts)) continue; + const job = jobs[jobSpec.id]; + if (!isObject(job)) { + findings.push( + finding( + "error", + "GHA020", + jobRequirement(jobSpec), + file.path, + `Missing required job ${jobSpec.id} (${jobSpec.name}, reports ${jobSpec.checks.join(", ")}).` + ) + ); + continue; + } + findings.push(...validateJob(file, policy, jobSpec, job, jobRequirement(jobSpec), componentOf)); + } + findings.push(...validateResultJob(file, policy, jobs, componentOf)); + return findings; +} + +// library/repository-validator/src/validator/rules/github-actions.mjs +var CORE_GHA_USES = /uses:\s*['"]?(opensesame\/core-github-actions\/[^@\s'"]+)(?:@([^\s'"#]*))?['"]?(?:\s+#\s*(.*?))?\s*$/i; +var RELEASE_COMMENT = /^((?:actions|workflows)\/[\w-]+)\/(\d+\.\d+\.\d+)(?:\s|$)/; +var NPM_INSTALL_COMMANDS = /* @__PURE__ */ new Set([ + "ci", + "clean-install", + "ic", + "install", + "i", + "add", + "install-clean", + "install-test", + "it", + "install-ci-test", + "cit" +]); +var NPM_FLAGS_WITH_VALUES = /* @__PURE__ */ new Set([ + "--prefix", + "-C", + "--workspace", + "-w", + "--userconfig", + "--globalconfig", + "--loglevel", + "--registry", + "--cache", + "--tag", + "--scope", + "--omit", + "--include", + "--install-strategy", + "--audit-level", + "--fetch-timeout", + "--fetch-retries", + "--otp", + "--location" +]); +function compareVersions(left, right) { + const a = left.split(".").map(Number); + const b = right.split(".").map(Number); + for (let index = 0; index < 3; index += 1) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return 0; +} +function summarize(findings, code, requirement, path, message) { + return findings.some((finding2) => finding2.severity === "error") ? findings : [...findings, createRuleFinding("pass", code, requirement, path, message)]; +} +function validateCoreGhaPins(workflowFiles, policy, releaseTags = null) { + const requirement = "core-gha-pins"; + const findings = []; + let pins = 0; + for (const file of workflowFiles) { + file.lines.forEach((line, index) => { + if (isCommentLine(line)) return; + const match = line.match(CORE_GHA_USES); + if (!match) return; + pins += 1; + const [, usesPath, ref = "", comment = ""] = match; + const where = `${file.path}:${index + 1}`; + const component = coreGhaComponent(usesPath); + const error = (code, message) => findings.push( + createRuleFinding("error", code, requirement, file.path, `${where} ${message}`) + ); + if (!component) { + error( + "GHA046", + `${usesPath} is not a released core-github-actions component; move to a .github/workflows/* or .github/actions/* release.` + ); + return; + } + if (!/^[0-9a-f]{40}$/.test(ref)) { + error( + "GHA041", + `${usesPath} must be pinned to a release commit SHA, not ${ref || "no ref"}.` + ); + return; + } + const release = comment.trim().match(RELEASE_COMMENT); + if (!release) { + error("GHA042", `${usesPath} needs a trailing # ${component}/ comment.`); + return; + } + const [, commentComponent, version] = release; + if (component !== commentComponent) { + error("GHA043", `pins ${component} but its comment names ${commentComponent}.`); + return; + } + const minimum = policy.minimumReleases[component]; + if (minimum && compareVersions(version, minimum) < 0) { + error("GHA044", `pins ${component}/${version}; the minimum release is ${minimum}.`); + } + if (releaseTags) { + const tagCommit = releaseTags.get(`${component}/${version}`); + if (!tagCommit) { + error("GHA045", `pins ${component}/${version}, which is not a core-github-actions tag.`); + } else if (tagCommit !== ref) { + error( + "GHA045", + `pins ${ref.slice(0, 7)} but ${component}/${version} is ${tagCommit.slice(0, 7)}.` + ); + } + } + }); + } + return summarize( + findings, + "GHA040", + requirement, + ".github", + `All ${pins} core-github-actions calls are SHA-pinned at or above their minimum releases${releaseTags ? " and match their release tags" : ""}.` + ); +} +function hasReasonComment(lines, lineNumber) { + const line = lines[lineNumber - 1] ?? ""; + if (/\s#\s*\S/.test(line)) return true; + return isCommentLine(lines[lineNumber - 2] ?? ""); +} +function validatePermissions(workflowFiles) { + const requirement = "workflow-permissions"; + const findings = []; + const workflows = workflowFiles.filter((file) => file.kind === "workflow"); + for (const file of workflows) { + const top = file.data.permissions; + if (!isObject(top) || Object.keys(top).length > 0) { + findings.push( + createRuleFinding( + "error", + "GHA050", + requirement, + file.path, + `${file.path} must set top-level permissions: {} and grant permissions per job.` + ) + ); + } + for (const [jobId, job] of Object.entries(readJobs(file.data))) { + if (job?.permissions === void 0) continue; + if (!isObject(job.permissions)) { + findings.push( + createRuleFinding( + "error", + "GHA051", + requirement, + file.path, + `${file.path} job ${jobId} must list individual permission grants, not ${job.permissions}.` + ) + ); + continue; + } + const node = file.document.getIn(["jobs", jobId, "permissions"], true); + for (const pair of node?.items ?? []) { + const key = String(pair.key?.value ?? pair.key); + const value = String(pair.value?.value ?? pair.value); + if (value === "none") continue; + const lineNumber = file.lineOf(pair.key); + if (lineNumber && hasReasonComment(file.lines, lineNumber)) continue; + findings.push( + createRuleFinding( + "error", + "GHA052", + requirement, + file.path, + `${file.path}:${lineNumber} job ${jobId} grants ${key}: ${value} without a reason comment.` + ) + ); + } + } + } + return summarize( + findings, + "GHA053", + requirement, + ".github/workflows", + `All ${workflows.length} workflows set permissions: {} and comment every granted permission.` + ); +} +function runnerLabels(runsOn) { + if (isObject(runsOn)) return asList(runsOn.labels); + return asList(runsOn); +} +function validateRunners(workflowFiles, policy) { + const requirement = "runner"; + const findings = []; + let jobs = 0; + for (const file of workflowFiles.filter((candidate) => candidate.kind === "workflow")) { + for (const [jobId, job] of Object.entries(readJobs(file.data))) { + if (!isObject(job) || job["runs-on"] === void 0) continue; + jobs += 1; + const labels = runnerLabels(job["runs-on"]).map(String); + if (labels.some((label) => label.includes("${{"))) { + findings.push( + createRuleFinding( + "warning", + "GHA061", + requirement, + file.path, + `${file.path} job ${jobId} computes runs-on (${labels.join(", ")}); confirm it resolves to ${policy.label}.` + ) + ); + } else if (!labels.includes(policy.label)) { + findings.push( + createRuleFinding( + "error", + "GHA060", + requirement, + file.path, + `${file.path} job ${jobId} must run on ${policy.label}; found ${labels.join(", ") || "no label"}.` + ) + ); + } + } + } + for (const file of workflowFiles) { + file.lines.forEach((line, index) => { + if (isCommentLine(line) || /^\s*runs-on\s*:/.test(line)) return; + for (const label of policy.forbiddenLabels) { + if (!line.includes(label)) continue; + findings.push( + createRuleFinding( + "error", + "GHA062", + requirement, + file.path, + `${file.path}:${index + 1} references ${label}; use ${policy.label}.` + ) + ); + } + }); + } + return summarize( + findings, + "GHA063", + requirement, + ".github/workflows", + `All ${jobs} runner jobs use ${policy.label}.` + ); +} +function splitShellCommands(script) { + return String(script).replace(/\\\r?\n/g, " ").split(/\r?\n|&&|\|\||;|\|/).map((command) => command.trim()).filter(Boolean); +} +function readNpmInstall(command) { + const tokens = command.split(/\s+/); + const commentStart = tokens.findIndex((token) => token.startsWith("#")); + const words = commentStart === -1 ? tokens : tokens.slice(0, commentStart); + const npm = words.findIndex((token) => token === "npm" || token.endsWith("/npm")); + if (npm === -1) return null; + for (let index = npm + 1; index < words.length; index += 1) { + const token = words[index]; + if (token.startsWith("-")) { + if (NPM_FLAGS_WITH_VALUES.has(token)) index += 1; + continue; + } + return NPM_INSTALL_COMMANDS.has(token) ? words : null; + } + return null; +} +function validateNpmInstalls(workflowFiles, policy) { + const requirement = "npm-ignore-scripts"; + const findings = []; + let installs = 0; + for (const file of workflowFiles) { + for (const { jobId, step } of listSteps(file)) { + if (!isObject(step)) continue; + const where = `${file.path}${jobId ? ` job ${jobId}` : ""} step "${step.name ?? step.uses ?? "unnamed"}"`; + for (const command of step.run ? splitShellCommands(step.run) : []) { + const words = readNpmInstall(command); + if (!words) continue; + installs += 1; + if (words.includes(policy.requiredFlag) || words.includes(`${policy.requiredFlag}=true`)) + continue; + findings.push( + createRuleFinding( + "error", + "GHA070", + requirement, + file.path, + `${where} runs "${command}" without ${policy.requiredFlag}.` + ) + ); + } + const action = stripRef(step.uses); + if (policy.actionsRequiringInstallFalse.includes(action) && String(isObject(step.with) ? step.with.install : void 0) !== "false") { + findings.push( + createRuleFinding( + "error", + "GHA071", + requirement, + file.path, + `${where} uses ${action}, which runs its own npm install unless install: false is set.` + ) + ); + } + } + } + return summarize( + findings, + "GHA072", + requirement, + ".github", + `All ${installs} npm installs in workflows pass ${policy.requiredFlag}.` + ); +} +function validateInputNames(workflowFiles, policy) { + const requirement = "workflow-input-names"; + const pattern = new RegExp(policy.pattern); + const findings = []; + for (const file of workflowFiles.filter((candidate) => candidate.kind === "workflow")) { + const triggers = readTriggers(file.data); + const names = ["workflow_dispatch", "workflow_call"].flatMap( + (trigger) => Object.keys(isObject(triggers[trigger]?.inputs) ? triggers[trigger].inputs : {}) + ); + const invalid = [...new Set(names.filter((name) => !pattern.test(name)))]; + if (invalid.length > 0) { + findings.push( + createRuleFinding( + "error", + "GHA090", + requirement, + file.path, + `${file.path} inputs must be kebab-case: ${invalid.join(", ")}.` + ) + ); + } + } + return summarize( + findings, + "GHA091", + requirement, + ".github/workflows", + "All workflow_dispatch and workflow_call inputs are kebab-case." + ); +} +function validateFilePrefixes(workflowFiles, policy) { + const requirement = "workflow-file-prefixes"; + const findings = []; + const error = (code, path, message) => findings.push(createRuleFinding("error", code, requirement, path, message)); + for (const file of workflowFiles.filter((candidate) => candidate.kind === "workflow")) { + const name = file.path.split("/").at(-1); + const triggers = Object.keys(readTriggers(file.data)).sort(); + const callOnly = triggers.length === 1 && triggers[0] === "workflow_call"; + const dispatchAndCall = triggers.length === 2 && triggers[0] === "workflow_call" && triggers[1] === "workflow_dispatch"; + if (/\s/.test(name)) + error("GHA103", file.path, `Workflow file name contains whitespace: "${name}".`); + if (callOnly && !name.startsWith(policy.callOnly)) { + error("GHA100", file.path, `${name} is call-only; name it ${policy.callOnly}*.`); + } else if (dispatchAndCall && !name.startsWith(policy.dispatchAndCall)) { + error( + "GHA101", + file.path, + `${name} runs on dispatch and call; name it ${policy.dispatchAndCall}*.` + ); + } else if (!callOnly && name.startsWith(policy.callOnly)) { + error( + "GHA102", + file.path, + `${name} uses the ${policy.callOnly} prefix but is not call-only.` + ); + } + } + return summarize( + findings, + "GHA104", + requirement, + ".github/workflows", + `Reusable workflow files use the ${policy.callOnly} and ${policy.dispatchAndCall} prefixes.` + ); +} + +// library/repository-validator/src/validator/rules/baseline.mjs +function tryRead(source, path) { + try { + return source.read(path); + } catch { + return null; + } +} +function summarize2(findings, code, requirement, path, message) { + return findings.some((finding2) => finding2.severity === "error") ? findings : [...findings, createRuleFinding("pass", code, requirement, path, message)]; +} +function parseJsonc(text) { + let output = ""; + let inString = false; + for (let index = 0; index < text.length; index += 1) { + const char = text[index]; + const next = text[index + 1]; + if (inString) { + output += char; + if (char === "\\") { + output += next ?? ""; + index += 1; + } else if (char === '"') inString = false; + } else if (char === '"') { + inString = true; + output += char; + } else if (char === "/" && next === "/") { + while (index < text.length && text[index] !== "\n") index += 1; + output += "\n"; + } else if (char === "/" && next === "*") { + index += 2; + while (index < text.length && !(text[index] === "*" && text[index + 1] === "/")) index += 1; + index += 1; + } else output += char; + } + return JSON.parse(output.replace(/,(\s*[}\]])/g, "$1")); +} +function isEqual(left, right) { + return JSON.stringify(left) === JSON.stringify(right); +} +function hasContent(source, path) { + return source.has(path) && (tryRead(source, path) ?? "").trim().length > 0; +} +function validateRequiredFiles(source, policy) { + const findings = []; + for (const { paths } of policy.requiredFiles) { + if (paths.some((path) => hasContent(source, path))) continue; + findings.push( + createRuleFinding( + "error", + "REPO001", + "required-files", + paths[0], + paths.length === 1 ? `Missing ${paths[0]}, or it is empty.` : `Missing ${paths.join(" or ")}, or it is empty.` + ) + ); + } + return summarize2( + findings, + "REPO002", + "required-files", + policy.requiredFiles[0].paths[0], + `All ${policy.requiredFiles.length} baseline files are present.` + ); +} +function validateCommittedFiles(source, policy) { + const findings = []; + const rules = policy.committedFiles.map((rule) => ({ + ...rule, + include: rule.include.map((pattern) => new RegExp(pattern, "i")), + exclude: (rule.exclude ?? []).map((pattern) => new RegExp(pattern, "i")) + })); + for (const path of source.files) { + for (const rule of rules) { + if (!rule.include.some((pattern) => pattern.test(path))) continue; + if (rule.exclude.some((pattern) => pattern.test(path))) continue; + findings.push( + createRuleFinding( + "error", + "REPO010", + "committed-secrets", + path, + `${path} is tracked but looks like ${rule.description}; remove it from Git and ignore it.` + ) + ); + break; + } + } + return summarize2( + findings, + "REPO011", + "committed-secrets", + ".gitignore", + `No tracked file looks like a secret, credential, or Terraform state or plan.` + ); +} +function parseEditorConfig(text) { + const preamble = /* @__PURE__ */ new Map(); + const sections = /* @__PURE__ */ new Map(); + let current = preamble; + for (const raw of text.split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith("#") || line.startsWith(";")) continue; + const section = line.match(/^\[(.+)\]$/); + if (section) { + const name = section[1].trim(); + if (!sections.has(name)) sections.set(name, /* @__PURE__ */ new Map()); + current = sections.get(name); + continue; + } + const property = line.match(/^([^=:]+?)\s*[=:]\s*(.*)$/); + if (property) current.set(property[1].trim().toLowerCase(), property[2].trim().toLowerCase()); + } + return { preamble, sections }; +} +function validateEditorConfig(source, policy) { + const requirement = "editorconfig"; + const { path } = policy; + const text = source.has(path) ? tryRead(source, path) : null; + if (text === null) return []; + const { preamble, sections } = parseEditorConfig(text); + const findings = []; + const expect = (properties, actual, where) => { + for (const [property, value] of Object.entries(properties)) { + const found = actual?.get(property); + if (found === String(value).toLowerCase()) continue; + findings.push( + createRuleFinding( + "error", + "REPO020", + requirement, + path, + `${path} ${where} must set ${property} = ${value}; found ${found ?? "nothing"}.` + ) + ); + } + }; + expect(policy.preamble, preamble, "preamble"); + for (const [section, properties] of Object.entries(policy.sections)) { + expect(properties, sections.get(section), `[${section}]`); + } + for (const conditional of policy.conditionalSections) { + const include = conditional.include.map((pattern) => new RegExp(pattern)); + if (!source.files.some((file) => include.some((pattern) => pattern.test(file)))) continue; + expect(conditional.properties, sections.get(conditional.section), `[${conditional.section}]`); + } + return summarize2(findings, "REPO021", requirement, path, `${path} sets the Core defaults.`); +} +function forTechnologies(groups, technologies) { + return ["all", ...technologies].flatMap((id) => groups[id] ?? []); +} +function readJsonc(source, path, requirement, code, findings) { + const text = source.has(path) ? tryRead(source, path) : null; + if (text === null) return null; + try { + const value = parseJsonc(text); + if (isObject(value)) return value; + } catch { + } + findings.push( + createRuleFinding("error", code, requirement, path, `${path} is not a JSON object.`) + ); + return null; +} +function validateVscodeExtensions(source, policy, technologies) { + const requirement = "vscode-extensions"; + const findings = []; + const document = readJsonc(source, policy.extensionsPath, requirement, "REPO030", findings); + if (!document) return findings; + const recommended = new Set( + (Array.isArray(document.recommendations) ? document.recommendations : []).filter((value) => typeof value === "string").map((value) => value.toLowerCase()) + ); + const unwanted = new Set( + (Array.isArray(document.unwantedRecommendations) ? document.unwantedRecommendations : []).filter((value) => typeof value === "string").map((value) => value.toLowerCase()) + ); + const required = [...new Set(forTechnologies(policy.extensions, technologies))]; + const missing = required.filter((id) => !recommended.has(id.toLowerCase())); + if (missing.length > 0) { + findings.push( + createRuleFinding( + "error", + "REPO031", + requirement, + policy.extensionsPath, + `${policy.extensionsPath} must recommend ${missing.join(", ")}.` + ) + ); + } + const blocked = required.filter((id) => unwanted.has(id.toLowerCase())); + if (blocked.length > 0) { + findings.push( + createRuleFinding( + "error", + "REPO031", + requirement, + policy.extensionsPath, + `${policy.extensionsPath} marks required ${blocked.join(", ")} as unwanted.` + ) + ); + } + return summarize2( + findings, + "REPO032", + requirement, + policy.extensionsPath, + `${policy.extensionsPath} recommends all ${required.length} Core extensions.` + ); +} +function describe(value) { + return value === void 0 ? "nothing" : JSON.stringify(value); +} +function validateVscodeSettings(source, policy, technologies) { + const requirement = "vscode-settings"; + const findings = []; + const settings = readJsonc(source, policy.settingsPath, requirement, "REPO040", findings); + if (!settings) return findings; + const required = Object.assign({}, ...forTechnologies(policy.settings, technologies)); + for (const [key, expected] of Object.entries(required)) { + const actual = settings[key]; + const problems = isObject(expected) ? Object.entries(expected).filter(([inner, value]) => !isEqual(isObject(actual) ? actual[inner] : void 0, value)).map( + ([inner, value]) => `"${key}": { "${inner}": ${JSON.stringify(value)} }; found ${describe(isObject(actual) ? actual[inner] : void 0)}` + ) : isEqual(actual, expected) ? [] : [`"${key}": ${JSON.stringify(expected)}; found ${describe(actual)}`]; + for (const problem of problems) { + findings.push( + createRuleFinding( + "error", + "REPO041", + requirement, + policy.settingsPath, + `${policy.settingsPath} must set ${problem}.` + ) + ); + } + } + return summarize2( + findings, + "REPO042", + requirement, + policy.settingsPath, + `${policy.settingsPath} sets all ${Object.keys(required).length} Core settings.` + ); +} +function hasConfig(source, candidate) { + const [path, key] = candidate.split("#"); + if (!key) return hasContent(source, path); + try { + const value = JSON.parse(tryRead(source, path) ?? "")?.[key]; + return isObject(value) || typeof value === "string" && value.trim().length > 0; + } catch { + return false; + } +} +function validateToolConfigs(source, policy, technologies) { + const requirement = "tool-configs"; + const findings = []; + let checked = 0; + for (const technology of technologies) { + for (const { tool, paths } of policy[technology] ?? []) { + checked += 1; + if (paths.some((candidate) => hasConfig(source, candidate))) continue; + findings.push( + createRuleFinding( + "error", + "REPO050", + requirement, + paths[0].split("#")[0], + `${tool} needs a config for Repo Checks; add ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", or another supported name" : ""}.` + ) + ); + } + } + return summarize2( + findings, + "REPO051", + requirement, + ".", + `All ${checked} tools Repo Checks runs have their configs.` + ); +} + +// library/repository-validator/src/validator/rules/kit.mjs +import { createHash } from "node:crypto"; +function compareVersions2(left, right) { + const a = left.split(".").map(Number); + const b = right.split(".").map(Number); + for (let index = 0; index < 3; index += 1) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return 0; +} +function tryRead2(source, path) { + try { + return source.read(path); + } catch { + return null; + } +} +function readManifest(source) { + const contents = tryRead2(source, MANIFEST_PATH); + if (contents === null) return { manifest: null, problem: null }; + try { + const manifest = JSON.parse(contents); + if (!isObject(manifest) || !/^\d+\.\d+\.\d+$/.test(manifest.validatorVersion ?? "") || !isObject(manifest.managedFiles) || ![BUNDLE_PATH, STANDARD_PATH].every( + (path) => /^[0-9a-f]{64}$/.test(manifest.managedFiles[path] ?? "") + ) || !Object.values(manifest.managedFiles).every((checksum) => /^[0-9a-f]{64}$/.test(checksum))) { + return { manifest: null, problem: `${MANIFEST_PATH} is not a validator manifest.` }; + } + return { manifest, problem: null }; + } catch (error) { + return { manifest: null, problem: `${MANIFEST_PATH} is not valid JSON: ${error.message}` }; + } +} +function validateIntegrity(source, manifest) { + const requirement = "kit-integrity"; + const findings = []; + for (const [path, expected] of Object.entries(manifest.managedFiles)) { + const contents = tryRead2(source, path); + const problem = contents === null ? "is missing" : createHash("sha256").update(contents).digest("hex") !== expected ? "differs from the checksum in the manifest" : null; + if (problem) { + findings.push( + createRuleFinding( + "error", + "GHA121", + requirement, + path, + `Managed validator file ${path} ${problem}; restore it or run update.` + ) + ); + } + } + if (findings.length === 0) { + findings.push( + createRuleFinding( + "pass", + "GHA123", + requirement, + MANIFEST_PATH, + `All ${Object.keys(manifest.managedFiles).length} managed validator files match the manifest.` + ) + ); + } + return findings; +} +function validateVersion(manifest, { external, latestVersion }) { + const installed = manifest.validatorVersion; + const installedLabel = `${installed}${manifest.sourceCommit ? ` (${String(manifest.sourceCommit).slice(0, 7)})` : ""}`; + if (!external && installed !== VALIDATOR_VERSION) { + return [ + createRuleFinding( + "error", + "GHA122", + "kit-integrity", + MANIFEST_PATH, + `The manifest records validator ${installed}, but the installed bundle is ${VALIDATOR_VERSION}; run update.` + ) + ]; + } + const references = [latestVersion, external ? VALIDATOR_VERSION : null].filter(Boolean); + const newest = references.sort(compareVersions2).at(-1); + if (newest && compareVersions2(installed, newest) < 0) { + return [ + createRuleFinding( + "warning", + "GHA130", + "kit-version", + MANIFEST_PATH, + `Installed validator ${installedLabel} is behind ${newest}; run update.` + ) + ]; + } + return [ + createRuleFinding( + "pass", + "GHA131", + "kit-version", + MANIFEST_PATH, + `Installed validator ${installedLabel} is current${newest ? ` with ${newest}` : ""}.` + ) + ]; +} +function validateRepoChecksJob(workflowFiles, ciPolicy, componentOf = coreGhaComponent) { + const requirement = "repo-validation"; + const file = workflowFiles.find((candidate) => candidate.path === ciPolicy.path); + if (!file) return []; + const spec = ciPolicy.jobs.find((job) => job.id === REPO_CHECKS_JOB); + const jobs = Object.entries(readJobs(file.data)).filter( + ([, job]) => spec.uses.includes(componentOf(job?.uses)) + ); + if (jobs.length === 0) { + return [ + createRuleFinding( + "error", + "GHA110", + requirement, + file.path, + `The validator is installed, so CI must have the ${spec.id} job (${spec.name}) that runs it through core-github-actions ${spec.uses.join(" or ")}.` + ) + ]; + } + const findings = []; + for (const [id, job] of jobs) { + if (job["continue-on-error"] !== void 0 && job["continue-on-error"] !== false) { + findings.push( + createRuleFinding( + "error", + "GHA111", + requirement, + file.path, + `Job ${id} must run Repo Checks without continue-on-error, so a failing Standard check fails CI.` + ) + ); + } + } + if (findings.length === 0) { + findings.push( + createRuleFinding( + "pass", + "GHA112", + requirement, + file.path, + `${jobs.map(([id]) => id).join(", ")} runs the installed validator in the Repo Checks / Standard / Validate check.` + ) + ); + } + return findings; +} +function validateInstalledKit(source, workflowFiles, standard, options) { + const { manifest, problem } = readManifest(source); + if (problem || !manifest && !options.external) { + return [ + createRuleFinding( + "error", + "GHA120", + "kit-integrity", + MANIFEST_PATH, + problem ?? `Missing ${MANIFEST_PATH}; install the validator with the lifecycle command.` + ) + ]; + } + if (!manifest) return []; + return [ + ...validateIntegrity(source, manifest), + ...validateVersion(manifest, options), + ...validateRepoChecksJob(workflowFiles, standard.githubActions.ciWorkflow, options.componentOf) + ]; +} + +// library/repository-validator/src/validator/rules/node-versions.mjs +var REQUIREMENT = "node-npm-versions"; +var VERSION = /^(\d+)\.(\d+)\.(\d+)$/; +var NODE_RANGE = /^>=\s*(\d+)\.(\d+)\.(\d+)\s+<\s*(\d+)(?:\.0\.0)?$/; +var NPM_FLOOR = /^>=\s*(\d+\.\d+\.\d+)$/; +var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["-g", "--global", "--location=global"]); +var INSTALL_COMMANDS = /* @__PURE__ */ new Set(["install", "i", "add", "update", "up", "upgrade"]); +function tryRead3(source, path) { + try { + return source.read(path); + } catch { + return null; + } +} +function compareVersions3(left, right) { + const a = left.split(".").map(Number); + const b = right.split(".").map(Number); + for (let index = 0; index < 3; index += 1) { + if (a[index] !== b[index]) return a[index] - b[index]; + } + return 0; +} +function bundledNpm(table, nodeVersion) { + const points = table?.[nodeVersion.split(".")[0]] ?? []; + const known = points.filter(([node]) => compareVersions3(node, nodeVersion) <= 0); + return known.at(-1)?.[1] ?? null; +} +function readNpmrc(text) { + const settings = /* @__PURE__ */ new Map(); + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue; + const separator = trimmed.indexOf("="); + if (separator === -1) continue; + const value = trimmed.slice(separator + 1).replace(/\s+[;#].*$/, ""); + settings.set(trimmed.slice(0, separator).trim(), value.trim()); + } + return settings; +} +function splitCommands(script) { + return String(script).replace(/\\\r?\n/g, " ").split(/\r?\n|&&|\|\||;|\|/).map((command) => command.trim().split(/\s+/).filter(Boolean)).filter((words) => words.length > 0); +} +function installsNpmGlobally(words) { + const npm = words.findIndex((word) => word === "npm" || word.endsWith("/npm")); + if (npm === -1) return false; + const rest = words.slice(npm + 1); + const command = rest.find((word) => !word.startsWith("-")); + if (!INSTALL_COMMANDS.has(command)) return false; + const global = rest.some( + (word, index) => GLOBAL_FLAGS.has(word) || word === "--location" && rest[index + 1] === "global" + ); + if (!global) return false; + return rest.some((word) => /^npm(?:@\S*)?$/.test(word.replace(/^["']|["']$/g, ""))); +} +function validateNodeVersions(source, workflowFiles, policy) { + const findings = []; + const error = (code, path, message) => findings.push(createRuleFinding("error", code, REQUIREMENT, path, message)); + if (!source.has(policy.packageJson)) return []; + const nvmrc = source.has(policy.nodeVersionFile) ? tryRead3(source, policy.nodeVersionFile) : null; + const major = nvmrc?.trim().match(/^(\d+)$/)?.[1] ?? null; + if (nvmrc !== null && !major) { + error( + "GHA140", + policy.nodeVersionFile, + `${policy.nodeVersionFile} must name only the Node major, such as 24; found "${nvmrc.trim()}".` + ); + } + let manifest; + try { + manifest = JSON.parse(tryRead3(source, policy.packageJson) ?? ""); + } catch { + manifest = null; + } + if (isObject(manifest)) { + const engines = isObject(manifest.engines) ? manifest.engines : {}; + const node = typeof engines.node === "string" ? engines.node.trim() : ""; + const range = node.match(NODE_RANGE); + if (!range || Number(range[4]) !== Number(range[1]) + 1) { + error( + "GHA141", + policy.packageJson, + `package.json engines.node must be a floor with a major ceiling, such as ">=${major ?? 24}.15.0 <${Number(major ?? 24) + 1}"; found ${node ? `"${node}"` : "none"}.` + ); + } else if (major && range[1] !== major) { + error( + "GHA141", + policy.packageJson, + `package.json engines.node "${node}" must cover the Node ${major} that ${policy.nodeVersionFile} names.` + ); + } + const npm = typeof engines.npm === "string" ? engines.npm.trim() : ""; + const floor = npm.match(NPM_FLOOR)?.[1]; + if (!floor || !VERSION.test(floor) || compareVersions3(floor, policy.minimumNpm) < 0) { + error( + "GHA142", + policy.packageJson, + `package.json engines.npm must be only a floor at or above ${policy.minimumNpm}, the first npm with min-release-age, such as ">=${policy.minimumNpm}"; found ${npm ? `"${npm}"` : "none"}.` + ); + } + const nodeFloor = range ? `${range[1]}.${range[2]}.${range[3]}` : null; + if (nodeFloor && floor && VERSION.test(floor)) { + const bundled = bundledNpm(policy.bundledNpm, nodeFloor); + if (bundled && compareVersions3(floor, bundled) > 0) { + error( + "GHA147", + policy.packageJson, + `package.json engines.npm floor ${floor} is above npm ${bundled}, which Node ${nodeFloor} ships, so engine-strict makes npm ci fail on the lowest Node that engines.node allows; lower the npm floor or raise the Node floor.` + ); + } else if (!bundled) { + findings.push( + createRuleFinding( + "warning", + "GHA147", + REQUIREMENT, + policy.packageJson, + `This validator doesn't know which npm Node ${nodeFloor} ships, so it can't confirm that npm ${floor} works there.` + ) + ); + } + } + if (typeof manifest.packageManager === "string" && /^npm@/.test(manifest.packageManager)) { + error( + "GHA145", + policy.packageJson, + `package.json must not pin packageManager ${manifest.packageManager}; use the npm that ships with Node.` + ); + } + } + const npmrcText = source.has(policy.npmrc) ? tryRead3(source, policy.npmrc) : null; + const npmrc = readNpmrc(npmrcText ?? ""); + if (npmrc.get("engine-strict") !== "true") { + error( + "GHA143", + policy.npmrc, + `${npmrcText === null ? `Missing ${policy.npmrc}; it` : policy.npmrc} must set engine-strict=true so npm enforces the engines floors.` + ); + } + const releaseAge = npmrc.get("min-release-age"); + if (!/^\d+$/.test(releaseAge ?? "") || Number(releaseAge) < policy.minimumReleaseAgeDays) { + error( + "GHA144", + policy.npmrc, + `${policy.npmrc} must set min-release-age=${policy.minimumReleaseAgeDays} or more; found ${releaseAge ?? "none"}.` + ); + } + for (const file of workflowFiles) { + for (const { jobId, step } of listSteps(file)) { + if (!isObject(step) || typeof step.run !== "string") continue; + for (const words of splitCommands(step.run)) { + if (!installsNpmGlobally(words)) continue; + error( + "GHA146", + file.path, + `${file.path}${jobId ? ` job ${jobId}` : ""} step "${step.name ?? "unnamed"}" installs npm globally ("${words.join(" ")}"); use the npm that ships with Node.` + ); + } + } + } + if (!findings.some((finding2) => finding2.severity === "error") && isObject(manifest)) { + findings.push( + createRuleFinding( + "pass", + "GHA149", + REQUIREMENT, + policy.packageJson, + `Node ${major ?? "(no .nvmrc)"} and npm use floors with engine-strict and a ${policy.minimumReleaseAgeDays}-day minimum release age.` + ) + ); + } + return findings; +} + +// library/repository-validator/src/validator/rules/repo-checks.mjs +import { posix } from "node:path"; +var REQUIREMENT2 = "repo-checks-prerequisites"; +var RUN_ALL = /^(?:npm-run-all2?|run-s|run-p)$/; +var CONTINUE_ON_ERROR = /* @__PURE__ */ new Set(["--continue-on-error", "-c"]); +function tryRead4(source, path) { + try { + return source.read(path); + } catch { + return null; + } +} +function escapeRegex(text) { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +function detectTechnologies(source, policy) { + const excluded = new RegExp(policy.excludePattern); + const technologies = policy.technologies.map((technology) => ({ + technology, + include: new RegExp(technology.include), + shebang: technology.shebang ? new RegExp(technology.shebang) : null, + files: [] + })); + for (const path of source.files) { + if (excluded.test(path)) continue; + for (const entry of technologies) { + if (entry.include.test(path)) entry.files.push(path); + else if (entry.shebang && entry.shebang.test(source.shebangLine(path) ?? "")) { + entry.files.push(path); + } + } + } + return technologies.map(({ technology, files }) => ({ technology, files })); +} +function canonicalTableName(header) { + const parts = [...header.matchAll(/\s*("[^"]*"|'[^']*'|[^.\s"']+)\s*(?:\.|$)/g)].map( + ([, part]) => unquoteKey(part) + ); + return parts.map((part, index) => index === 0 ? part : `"${part}"`).join("."); +} +function readTomlSections(text) { + const sections = /* @__PURE__ */ new Map(); + let current = null; + let openQuote = null; + for (const line of text.split(/\r?\n/)) { + if (!openQuote) { + const header = line.match(/^\s*\[([^[\]]+)\]\s*(?:#.*)?$/); + if (header) { + current = canonicalTableName(header[1]); + sections.set(current, []); + continue; + } + } + if (current) sections.get(current).push(line); + for (const quote of line.match(/'''|"""/g) ?? []) { + if (!openQuote) openQuote = quote; + else if (openQuote === quote) openQuote = null; + } + } + return new Map([...sections].map(([name, lines]) => [name, lines.join("\n")])); +} +function unquoteKey(key) { + const trimmed = key.trim(); + return /^(["']).*\1$/.test(trimmed) ? trimmed.slice(1, -1) : trimmed; +} +function splitEntries(text) { + const entries = []; + let current = ""; + let quote = null; + let depth = 0; + const flush = () => { + const match = current.match(/^\s*("[^"]*"|'[^']*'|[A-Za-z0-9_.:/@-]+)\s*=\s*([\s\S]*?)\s*$/); + if (match) entries.push([unquoteKey(match[1]), match[2]]); + current = ""; + }; + for (let index = 0; index < text.length; index += 1) { + const char = text[index]; + const triple = text.slice(index, index + 3); + if (quote) { + if (quote.length === 3 ? triple === quote : char === quote) { + current += quote; + index += quote.length - 1; + quote = null; + } else { + if (char === "\\" && quote.startsWith('"')) { + current += char + (text[index + 1] ?? ""); + index += 1; + continue; + } + if (char === "\n" && quote.length === 1) quote = null; + current += char; + } + } else if (triple === "'''" || triple === '"""') { + quote = triple; + current += triple; + index += 2; + } else if (char === '"' || char === "'") { + quote = char; + current += char; + } else if (char === "#") { + while (index < text.length && text[index] !== "\n") index += 1; + index -= 1; + } else if (char === "[" || char === "{") { + depth += 1; + current += char; + } else if (char === "]" || char === "}") { + depth -= 1; + current += char; + } else if ((char === "\n" || char === ",") && depth === 0) { + flush(); + } else current += char; + } + flush(); + return entries; +} +function runCommandOf(raw) { + const value = raw.trim(); + if (value.startsWith("{")) { + const run = splitEntries(value.slice(1, -1)).find(([key]) => key === "run"); + return run ? runCommandOf(run[1]) : ""; + } + const strings = []; + for (const [, triple, double, single] of value.matchAll( + /('''[\s\S]*?'''|"""[\s\S]*?""")|"((?:[^"\\]|\\.)*)"|'([^']*)'/g + )) { + strings.push(triple ? triple.slice(3, -3) : double ?? single); + } + return strings.join("\n").split("\n").filter((line) => !line.trim().startsWith("#")).join("\n"); +} +function readMiseTasks(sections) { + const tasks = /* @__PURE__ */ new Map(); + for (const [name, body] of sections) { + if (name === "tasks") { + for (const [task, raw] of splitEntries(body)) tasks.set(task, runCommandOf(raw)); + continue; + } + const match = name.match(/^tasks\.(.+)$/); + if (!match) continue; + const run = splitEntries(body).find(([key]) => key === "run"); + tasks.set(unquoteKey(match[1]), run ? runCommandOf(run[1]) : ""); + } + return tasks; +} +function readMiseTools(sections) { + const body = sections.get("tools") ?? ""; + return body.split("\n").map((line) => line.match(/^\s*("[^"]+"|'[^']+'|[A-Za-z0-9_.:/@-]+)\s*=/)?.[1]).filter(Boolean).map(unquoteKey); +} +function readMiseLock(text) { + const tools = /* @__PURE__ */ new Map(); + const key = String.raw`("[^"]+"|'[^']+'|[A-Za-z0-9_.:/@-]+?)`; + const entryPattern = new RegExp(String.raw`^\s*\[\[tools\.${key}\]\]\s*$`); + const platformPattern = new RegExp( + String.raw`^\s*\[tools\.${key}\.(?:"platforms\.([^"]+)"|platforms\.([A-Za-z0-9_-]+))\]\s*$` + ); + const entry = (name) => { + const tool = unquoteKey(name); + if (!tools.has(tool)) tools.set(tool, { backend: null, platforms: /* @__PURE__ */ new Set() }); + return tools.get(tool); + }; + let current = null; + for (const line of text.split(/\r?\n/)) { + const header = line.match(entryPattern); + if (header) { + current = entry(header[1]); + continue; + } + const platform = line.match(platformPattern); + if (platform) { + entry(platform[1]).platforms.add(platform[2] ?? platform[3]); + current = null; + continue; + } + if (/^\s*\[/.test(line)) current = null; + const backend = current && line.match(/^\s*backend\s*=\s*["']([^"']+)["']/); + if (backend) current.backend = backend[1]; + } + return tools; +} +function backendOf(tool, locked) { + return (locked?.backend ?? tool).match(/^([a-z0-9-]+):/)?.[1] ?? null; +} +function scriptPattern(pattern) { + const body = escapeRegex(pattern).replace(/\\\{([^{}]*)\\\}/g, (_, options) => `(?:${options.split(",").join("|")})`).replace(/\\\*\\\*/g, ".+").replace(/\\\*/g, "[^:]+"); + return new RegExp(`^${body}$`); +} +function commandsOf(script) { + return String(script).split(/&&|\|\||;|\|/).map((command) => command.trim().split(/\s+/).filter(Boolean)).filter((words) => words.length > 0); +} +function referencedScripts(script, scripts) { + const names = Object.keys(scripts); + const referenced = []; + for (const words of commandsOf(script)) { + if (RUN_ALL.test(words[0])) { + for (const word of words.slice(1)) { + if (word === "--") break; + if (word.startsWith("-")) continue; + const pattern = scriptPattern(word.replace(/^["']|["']$/g, "")); + referenced.push(...names.filter((name) => pattern.test(name))); + } + } else if (words[0] === "npm" && ["run", "run-script"].includes(words[1]) && words[2]) { + referenced.push(words[2]); + } + } + return referenced; +} +function reachableScripts(start, scripts) { + const seen = /* @__PURE__ */ new Set(); + const pending = [start]; + while (pending.length > 0) { + const name = pending.pop(); + if (seen.has(name) || typeof scripts[name] !== "string") continue; + seen.add(name); + pending.push(...referencedScripts(scripts[name], scripts)); + } + return seen; +} +function continuesOnError(script) { + const commands = commandsOf(script); + return commands.length === 1 && RUN_ALL.test(commands[0][0]) && commands[0].some((word) => CONTINUE_ON_ERROR.has(word)); +} +function runsMiseTask(script, task) { + return new RegExp(`(?:^|[\\s;&|(])mise\\s+run\\s+${escapeRegex(task)}(?:\\s|$|[;&|)])`).test( + String(script) + ); +} +function stripHclComments(text) { + let output = ""; + let inString = false; + for (let index = 0; index < text.length; index += 1) { + const char = text[index]; + const next = text[index + 1]; + if (inString) { + output += char; + if (char === "\\") { + output += next ?? ""; + index += 1; + } else if (char === '"' || char === "\n") inString = false; + } else if (char === '"') { + inString = true; + output += char; + } else if (char === "#" || char === "/" && next === "/") { + while (index < text.length && text[index] !== "\n") index += 1; + output += "\n"; + } else if (char === "/" && next === "*") { + const end = text.indexOf("*/", index + 2); + index = end === -1 ? text.length : end + 1; + } else output += char; + } + return output; +} +function localModuleSources(text) { + const code = stripHclComments(text); + const sources = []; + for (const block of code.matchAll(/\bmodule\s+"[^"]*"\s*\{/g)) { + let depth = 1; + let index = block.index + block[0].length; + const start = index; + while (index < code.length && depth > 0) { + if (code[index] === "{") depth += 1; + else if (code[index] === "}") depth -= 1; + index += 1; + } + const source = code.slice(start, index).match(/\bsource\s*=\s*"(\.{1,2}\/[^"]*)"/); + if (source) sources.push(source[1]); + } + return sources; +} +function findTerraformRoots(source, files, policy) { + const modulePattern = new RegExp(policy.terraformModulePattern); + const directories = new Set(files.map((path) => posix.dirname(path))); + const calledModules = /* @__PURE__ */ new Set(); + for (const path of files) { + const contents = tryRead4(source, path) ?? ""; + for (const target of localModuleSources(contents)) { + calledModules.add( + posix.normalize(posix.join(posix.dirname(path), target)).replace(/\/$/, "") + ); + } + } + return [...directories].filter((directory) => !modulePattern.test(`${directory}/`) && !calledModules.has(directory)).sort(); +} +function readActionlintLabels(source, path) { + try { + return asList(parse(tryRead4(source, path) ?? "")?.["self-hosted-runner"]?.labels); + } catch { + return []; + } +} +function validateRepoChecksPrerequisites(source, policy, technologies = detectTechnologies(source, policy)) { + const findings = []; + const error = (code, path, message) => findings.push(createRuleFinding("error", code, REQUIREMENT2, path, message)); + const detected = technologies.filter(({ files }) => files.length > 0); + findings.push( + createRuleFinding( + "pass", + "GHA079", + REQUIREMENT2, + policy.miseConfig, + `Repo Checks detects ${detected.map(({ technology, files }) => `${technology.name} (${files.length} files)`).join(", ") || "no technologies"}.` + ) + ); + const miseTasks = detected.flatMap( + ({ technology }) => (technology.miseTasks ?? []).map((name) => ({ name, technology })) + ); + const npmScripts = detected.flatMap( + ({ technology }) => (technology.npmScripts ?? []).map((name) => ({ name, technology })) + ); + const requireFile = (path, purpose) => { + if (!source.has(path)) { + error("GHA080", path, `Repo Checks requires tracked ${path} ${purpose}.`); + return null; + } + const contents = tryRead4(source, path); + if (contents === null) { + error("GHA080", path, `Repo Checks requires ${path}, which is tracked but cannot be read.`); + } + return contents; + }; + const platforms = policy.lockPlatforms.join(","); + const miseToml = requireFile(policy.miseConfig, "to define its mise tasks"); + const miseLock = requireFile(policy.miseLock, `locked with mise lock --platform ${platforms}`); + if (miseToml !== null) { + const sections = readTomlSections(miseToml); + const tasks = readMiseTasks(sections); + for (const { name, technology } of miseTasks) { + const pattern = policy.miseTaskCommands[name]; + if (!tasks.has(name)) { + error( + "GHA081", + policy.miseConfig, + `mise.toml must define task ${name} for ${technology.name}.` + ); + } else if (pattern && !new RegExp(pattern).test(tasks.get(name))) { + error( + "GHA082", + policy.miseConfig, + `The ${name} task's run command must match ${pattern}, as run_repo_checks requires.` + ); + } + } + if (miseLock !== null) { + const locked = readMiseLock(miseLock); + for (const tool of readMiseTools(sections)) { + const entry = locked.get(tool); + const backend = backendOf(tool, entry); + const npmPackage = policy.npmBackends.includes(backend); + if (npmPackage) { + error( + "GHA089", + policy.miseConfig, + `${tool} is an npm package; add it to package.json devDependencies and drop it from mise.toml, which holds only native tools.` + ); + continue; + } + if (!entry) { + error( + "GHA085", + policy.miseLock, + `${policy.miseLock} has no entry for ${tool}; run mise lock --platform ${platforms}.` + ); + continue; + } + if (policy.unplatformedBackends.includes(backend)) continue; + const missing = policy.lockPlatforms.filter((platform) => !entry.platforms.has(platform)); + if (missing.length > 0) { + error( + "GHA085", + policy.miseLock, + `${policy.miseLock} has no ${missing.join(", ")} entry for ${tool}; run mise lock --platform ${platforms}.` + ); + } + } + } + } + const packageText = requireFile( + policy.packageJson, + "with scripts that run each check locally through npm run check" + ); + requireFile(policy.nodeVersionFile, "for the Node version Repo Checks sets up"); + if (npmScripts.length > 0) { + requireFile(policy.npmLockFile, `for npm ci in the ${npmScripts[0].technology.name} checks`); + } + let scripts = null; + if (packageText !== null) { + try { + const parsed = JSON.parse(packageText); + scripts = isObject(parsed?.scripts) ? parsed.scripts : {}; + } catch (parseError) { + error( + "GHA080", + policy.packageJson, + `${policy.packageJson} is not valid JSON: ${parseError.message}` + ); + } + } + if (scripts) { + for (const { name } of miseTasks) { + if (typeof scripts[name] !== "string") { + error( + "GHA086", + policy.packageJson, + `package.json must define script ${name}: mise run ${name}.` + ); + } else if (!runsMiseTask(scripts[name], name)) { + error( + "GHA086", + policy.packageJson, + `package.json script ${name} must run mise run ${name}; found "${scripts[name]}".` + ); + } + } + for (const { name, technology } of npmScripts) { + if (typeof scripts[name] !== "string") { + error( + "GHA086", + policy.packageJson, + `package.json must define script ${name} for ${technology.name}.` + ); + } + } + const required = [...miseTasks, ...npmScripts].map(({ name }) => name); + for (const aggregate of policy.aggregateScripts) { + const covers = aggregate === policy.checkScript ? required : required.filter((name) => name.startsWith(`${aggregate}:`)); + if (covers.length === 0) continue; + const script = scripts[aggregate]; + if (typeof script !== "string") { + error("GHA087", policy.packageJson, `package.json must define a ${aggregate} script.`); + continue; + } + if (!continuesOnError(script)) { + error( + "GHA087", + policy.packageJson, + `package.json script ${aggregate} must be one npm-run-all --continue-on-error command, so one failure does not hide the rest; found "${script}".` + ); + } + const reachable = reachableScripts(aggregate, scripts); + const unreached = covers.filter( + (name) => typeof scripts[name] === "string" && !reachable.has(name) + ); + if (unreached.length > 0) { + error( + "GHA087", + policy.packageJson, + `npm run ${aggregate} must run ${unreached.join(", ")}, as Repo Checks does.` + ); + } + } + } + const gha = detected.find(({ technology }) => technology.id === "gha"); + if (gha) { + const configPath = policy.actionlintConfigPaths.find((path) => source.has(path)); + const missing = configPath ? policy.runnerLabels.filter( + (label) => !readActionlintLabels(source, configPath).includes(label) + ) : policy.runnerLabels; + if (!configPath) { + error( + "GHA083", + policy.actionlintConfigPaths[0], + `Missing ${policy.actionlintConfigPaths[0]} declaring the ${missing.join(", ")} runner label.` + ); + } else if (missing.length > 0) { + error( + "GHA083", + configPath, + `${configPath} must list ${missing.join(", ")} under self-hosted-runner.labels.` + ); + } + } + const terraform = detected.find(({ technology }) => technology.id === "terraform"); + const roots = terraform ? findTerraformRoots(source, terraform.files, policy) : []; + for (const root of roots) { + const lockPath = root === "." ? policy.terraformLockFile : `${root}/${policy.terraformLockFile}`; + if (!source.has(lockPath)) { + error( + "GHA088", + lockPath, + `Terraform root ${root} must commit ${lockPath}, which init:terraform reads with -lockfile=readonly.` + ); + } + } + if (!findings.some((finding2) => finding2.severity === "error")) { + findings.push( + createRuleFinding( + "pass", + "GHA084", + REQUIREMENT2, + policy.miseConfig, + `Repo Checks prerequisites are present: ${miseTasks.length} mise tasks locked for ${platforms}, ${miseTasks.length + npmScripts.length} package.json scripts run by npm run ${policy.checkScript}${roots.length > 0 ? `, and lock files for ${roots.length} Terraform roots` : ""}.` + ) + ); + } + return findings; +} + +// library/repository-validator/src/validator/adapters/filesystem.mjs +import { readFileSync, statSync } from "node:fs"; +import { resolve } from "node:path"; +function readRepositoryFile(root, repositoryPath) { + return readFileSync(resolve(root, repositoryPath), "utf8"); +} + +// library/repository-validator/src/validator/rules/repository.mjs +var EXCLUDED_TRACKED_DIRECTORY = /(^|\/)(?:\.git|\.terraform|node_modules|vendor|vendored|dist|build|coverage|\.cache)(?:\/|$)/; +function loadJsonFile(root, repositoryPath, label) { + let contents; + try { + contents = readRepositoryFile(root, repositoryPath); + } catch (error) { + throw new ValidatorError( + `Cannot read ${label} ${repositoryPath}: ${error.code ?? error.message}` + ); + } + try { + return JSON.parse(contents); + } catch (error) { + throw new ValidatorError(`Invalid JSON in ${label} ${repositoryPath}: ${error.message}`); + } +} +function loadContract(root) { + return loadJsonFile(root, CONTRACT_PATH, "repository contract"); +} +function loadStandard(root, contract) { + const standardPath = contract?.standard?.path; + if (standardPath !== STANDARD_PATH) { + throw new ValidatorError( + `Repository contract must reference the embedded standard at ${STANDARD_PATH}.` + ); + } + return loadJsonFile(root, standardPath, "repository standard"); +} +var NPM_PLACEHOLDER_TEST = /^echo \\?"Error: no test specified\\?" && exit 1$/; +function discoverPackageScripts(standard, source) { + const detection = standard.surfaceDetection.npm; + const names = /* @__PURE__ */ new Set(); + let unreadable = false; + if (!detection) return { names, unreadable }; + const include = detection.include.map((pattern) => new RegExp(pattern)); + const exclude = (detection.exclude ?? []).map((pattern) => new RegExp(pattern)); + for (const path of source.files) { + if (EXCLUDED_TRACKED_DIRECTORY.test(path)) continue; + if (!include.some((pattern) => pattern.test(path))) continue; + if (exclude.some((pattern) => pattern.test(path))) continue; + try { + const scripts = JSON.parse(source.read(path))?.scripts; + for (const [name, command] of Object.entries(scripts ?? {})) { + if (typeof command !== "string" || !command.trim()) continue; + if (name === "test" && NPM_PLACEHOLDER_TEST.test(command.trim())) continue; + names.add(name); + } + } catch { + unreadable = true; + } + } + return { names, unreadable }; +} +function discoverRepositorySurfaces(standard, trackedFiles) { + const compiledDetection = Object.entries(standard.surfaceDetection).map( + ([surfaceName, detection]) => ({ + surfaceName, + include: detection.include.map((pattern) => new RegExp(pattern)), + exclude: (detection.exclude ?? []).map((pattern) => new RegExp(pattern)) + }) + ); + const surfaces = /* @__PURE__ */ new Set(); + for (const path of trackedFiles) { + if (EXCLUDED_TRACKED_DIRECTORY.test(path)) continue; + for (const detection of compiledDetection) { + const included = detection.include.some((pattern) => pattern.test(path)); + const excluded = detection.exclude.some((pattern) => pattern.test(path)); + if (included && !excluded) surfaces.add(detection.surfaceName); + } + } + return surfaces; +} + +// library/repository-validator/src/validator/adapters/subprocess.mjs +import { execFileSync } from "node:child_process"; +var DEFAULT_COMMAND_TIMEOUT_MS = 15e3; +var DEFAULT_COMMAND_MAX_BUFFER = 8 * 1024 * 1024; +function redactSensitive(value) { + return value.replace( + /\b[A-Z][A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|CREDENTIAL|PRIVATE_KEY)[A-Z0-9_]*=\S+/gi, + "[REDACTED]" + ).replace( + /\b(?:token|secret|password|credential|private[_ -]?key)\s*[:=]\s*\S+/gi, + "[REDACTED]" + ); +} +var REPOSITORY_SELECTING_GIT_VARIABLES = [ + "GIT_DIR", + "GIT_WORK_TREE", + "GIT_INDEX_FILE", + "GIT_OBJECT_DIRECTORY", + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + "GIT_COMMON_DIR", + "GIT_PREFIX" +]; +function isolatedEnvironment(environment = process.env) { + return Object.fromEntries( + Object.entries(environment).filter( + ([name]) => !REPOSITORY_SELECTING_GIT_VARIABLES.includes(name) + ) + ); +} +function runFile(program, args, root, options = {}) { + const timeout = options.timeout ?? DEFAULT_COMMAND_TIMEOUT_MS; + const maxBuffer = options.maxBuffer ?? DEFAULT_COMMAND_MAX_BUFFER; + try { + return execFileSync(program, args, { + cwd: root, + env: isolatedEnvironment(), + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + timeout, + maxBuffer, + killSignal: "SIGTERM" + }); + } catch (error) { + if (options.allowedStatuses?.includes(error?.status) && typeof error.stdout === "string") { + return error.stdout; + } + const detail = redactSensitive(String(error.stderr ?? error.message)).trim().split(/\r?\n/).slice(0, 3).join(" "); + const invocation = redactSensitive(`${program} ${args.join(" ")}`); + const timeoutDetail = error?.code === "ETIMEDOUT" ? ` timed out after ${timeout}ms` : ""; + throw new ValidatorError(`${invocation} failed${timeoutDetail}${detail ? `: ${detail}` : ""}`); + } +} + +// library/repository-validator/src/validator/adapters/git.mjs +function listTrackedFiles(root, runCommand = runFile) { + return runCommand("git", ["ls-files", "-z"], root).split("\0").filter(Boolean); +} +function readOriginRepository(root, runCommand = runFile) { + let url; + try { + url = runCommand("git", ["remote", "get-url", "origin"], root).trim(); + } catch { + return null; + } + const match = url.match(/[/:]([^/:]+\/[^/]+?)(?:\.git)?\/?$/); + return match ? match[1] : null; +} +function listTagCommits(root, runCommand = runFile) { + const output = runCommand( + "git", + ["for-each-ref", "--format=%(refname:short)%09%(objectname)%09%(*objectname)", "refs/tags"], + root + ); + return new Map( + output.split("\n").filter(Boolean).map((line) => { + const [tag, object, peeled] = line.split(" "); + return [tag, peeled || object]; + }) + ); +} + +// library/repository-validator/src/validator/adapters/source.mjs +import { closeSync, lstatSync, openSync, readSync } from "node:fs"; +import { resolve as resolve2 } from "node:path"; +var HEAD_BYTES = 512; +function firstLineOf(text) { + return text.split("\n", 1)[0]; +} +function readHead(root, path) { + try { + const absolute = resolve2(root, path); + if (!lstatSync(absolute).isFile()) return null; + const descriptor = openSync(absolute, "r"); + try { + const buffer = Buffer.alloc(HEAD_BYTES); + const length = readSync(descriptor, buffer, 0, HEAD_BYTES, 0); + return buffer.subarray(0, length).toString("utf8"); + } finally { + closeSync(descriptor); + } + } catch { + return null; + } +} +function createRepositorySource({ root, ref = null, runCommand = runFile, trackedFiles }) { + if (ref !== null) { + if (typeof ref !== "string" || !ref.trim() || ref.startsWith("-")) { + throw new ValidatorError(`Invalid Git ref: ${ref}`); + } + const commit = runCommand("git", ["rev-parse", "--verify", `${ref}^{commit}`], root).trim(); + const files2 = trackedFiles ?? runCommand("git", ["ls-tree", "-r", "-z", "--name-only", commit], root).split("\0").filter(Boolean); + const tracked2 = new Set(files2); + const read = (path) => runCommand("git", ["show", `${commit}:${path}`], root); + let candidates = null; + const findCandidates = () => { + const symlinks = new Set( + runCommand("git", ["ls-tree", "-r", "-z", commit], root).split("\0").filter((entry) => entry.startsWith("120000 ")).map((entry) => entry.slice(entry.indexOf(" ") + 1)) + ); + const prefix = `${commit}:`; + return new Set( + runCommand("git", ["grep", "-l", "-z", "-E", "^#!", commit, "--"], root, { + allowedStatuses: [1] + }).split("\0").filter(Boolean).map((entry) => entry.startsWith(prefix) ? entry.slice(prefix.length) : entry).filter((path) => !symlinks.has(path)) + ); + }; + return { + root, + ref, + commit, + files: files2, + has: (path) => tracked2.has(path), + read, + shebangLine: (path) => { + candidates ??= findCandidates(); + if (!candidates.has(path)) return null; + try { + const line = firstLineOf(read(path)); + return line.startsWith("#!") ? line : null; + } catch { + return null; + } + } + }; + } + const files = trackedFiles ?? listTrackedFiles(root, runCommand); + const tracked = new Set(files); + return { + root, + ref: null, + commit: null, + files, + has: (path) => tracked.has(path), + read: (path) => readRepositoryFile(root, path), + shebangLine: (path) => { + const head = readHead(root, path); + if (head === null) return null; + const line = firstLineOf(head); + return line.startsWith("#!") ? line : null; + } + }; +} + +// library/repository-validator/src/validator/schemas/primitives.mjs +function addShapeError(findings, code, path, field, expectation) { + findings.push(createFinding("error", code, path, `${field} must be ${expectation}.`)); +} +function validateObject(value, field, code, path, findings) { + if (isObject(value)) return true; + addShapeError(findings, code, path, field, "an object"); + return false; +} +function validateAllowedKeys(value, allowed, field, code, path, findings) { + if (!isObject(value)) return; + for (const key of Object.keys(value)) { + if (!allowed.includes(key)) { + findings.push(createFinding("error", code, path, `${field} contains unknown field: ${key}.`)); + } + } +} +function validateString(value, field, code, path, findings, { nullable = false } = {}) { + if (nullable && value === null || isNonemptyString(value)) return true; + addShapeError( + findings, + code, + path, + field, + nullable ? "null or a nonempty string" : "a nonempty string" + ); + return false; +} +function validateBoolean(value, field, code, path, findings, { optional = false } = {}) { + if (optional && value === void 0 || typeof value === "boolean") return true; + addShapeError(findings, code, path, field, optional ? "a boolean when present" : "a boolean"); + return false; +} +function validateStringArray(value, field, code, path, findings, { nonempty = false } = {}) { + if (!Array.isArray(value) || nonempty && value.length === 0 || value.some((item) => !isNonemptyString(item))) { + addShapeError( + findings, + code, + path, + field, + `${nonempty ? "a nonempty" : "an"} array of nonempty strings` + ); + return false; + } + if (new Set(value).size !== value.length) { + findings.push(createFinding("error", code, path, `${field} contains duplicate values.`)); + return false; + } + return true; +} + +// library/repository-validator/src/validator/schemas/contract.mjs +function isIsoDate(value) { + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false; + const date = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`); + return !Number.isNaN(date.valueOf()) && date.toISOString().slice(0, 10) === value; +} +function validateException(exception, field, standard, findings) { + if (!validateObject(exception, field, "CONTRACT008", CONTRACT_PATH, findings)) return; + validateAllowedKeys( + exception, + ["id", "requirement", "paths", "rationale", "owner", "reviewBy", "isWarning"], + field, + "CONTRACT008", + CONTRACT_PATH, + findings + ); + const validFields = ["id", "requirement", "rationale", "owner"].every( + (key) => validateString(exception[key], `${field}.${key}`, "CONTRACT008", CONTRACT_PATH, findings) + ); + if (!validFields) return; + validateBoolean( + exception.isWarning, + `${field}.isWarning`, + "CONTRACT008", + CONTRACT_PATH, + findings, + { + optional: true + } + ); + if (exception.paths !== void 0) { + validateStringArray(exception.paths, `${field}.paths`, "CONTRACT008", CONTRACT_PATH, findings, { + nonempty: true + }); + } + if (exception.reviewBy !== void 0 && validateString( + exception.reviewBy, + `${field}.reviewBy`, + "CONTRACT008", + CONTRACT_PATH, + findings + ) && !isIsoDate(exception.reviewBy)) { + findings.push( + createFinding( + "error", + "CONTRACT008", + CONTRACT_PATH, + `${field}.reviewBy must be an ISO date.` + ) + ); + } + if (standard.exceptions.nonOverridableRequirements.includes(exception.requirement)) { + findings.push( + createFinding( + "error", + "CONTRACT009", + CONTRACT_PATH, + `Exception ${exception.id} attempts to override non-overridable requirement ${exception.requirement}.` + ) + ); + } +} +function validateContract(contract, standard) { + const findings = []; + if (!validateObject(contract, "contract document", "CONTRACT001", CONTRACT_PATH, findings)) + return findings; + validateAllowedKeys( + contract, + ["standard", "exceptions"], + "contract document", + "CONTRACT001", + CONTRACT_PATH, + findings + ); + validateObject(contract.standard, "standard", "CONTRACT001", CONTRACT_PATH, findings); + if (!Array.isArray(contract.exceptions)) { + addShapeError(findings, "CONTRACT001", CONTRACT_PATH, "exceptions", "an array"); + } + if (findings.some((finding2) => finding2.code === "CONTRACT001")) return findings; + validateAllowedKeys( + contract.standard, + ["path", "id", "version"], + "standard", + "CONTRACT002", + CONTRACT_PATH, + findings + ); + for (const field of ["path", "id", "version"]) { + validateString( + contract.standard[field], + `standard.${field}`, + "CONTRACT002", + CONTRACT_PATH, + findings + ); + } + if (contract.standard.path !== STANDARD_PATH || contract.standard.id !== standard.standard.id || contract.standard.version !== standard.standard.version) { + findings.push( + createFinding( + "error", + "CONTRACT002", + CONTRACT_PATH, + "Contract standard identity, version, and path must match the embedded standard." + ) + ); + } + contract.exceptions.forEach( + (exception, index) => validateException(exception, `exceptions[${index}]`, standard, findings) + ); + return findings; +} + +// library/repository-validator/src/validator/schemas/repository.mjs +var CODE = "STANDARD031"; +function validateRegexes(patterns, field, findings) { + for (const pattern of patterns) { + try { + new RegExp(pattern); + } catch { + findings.push( + createFinding( + "error", + CODE, + STANDARD_PATH, + `${field} contains invalid pattern: ${pattern}.` + ) + ); + } + } +} +function validatePatternList(value, field, findings, { optional = false } = {}) { + if (optional && value === void 0) return; + if (validateStringArray(value, field, CODE, STANDARD_PATH, findings, { nonempty: true })) { + validateRegexes(value, field, findings); + } +} +function validateList(value, field, findings, validateItem) { + if (!Array.isArray(value) || value.length === 0) { + addShapeError(findings, CODE, STANDARD_PATH, field, "a nonempty array"); + return; + } + value.forEach((item, index) => { + const name = `${field}[${index}]`; + if (validateObject(item, name, CODE, STANDARD_PATH, findings)) validateItem(item, name); + }); +} +function validateStringMap(value, field, findings) { + if (!validateObject(value, field, CODE, STANDARD_PATH, findings)) return; + for (const [key, item] of Object.entries(value)) { + validateString(item, `${field}.${key}`, CODE, STANDARD_PATH, findings); + } +} +function validateGroups(value, field, findings, validateItem) { + if (!validateObject(value, field, CODE, STANDARD_PATH, findings)) return; + for (const [group, items] of Object.entries(value)) { + validateItem(items, `${field}.${group}`); + } +} +function validateRepositorySection(repository, findings) { + const field = "repository"; + if (!validateObject(repository, field, CODE, STANDARD_PATH, findings)) return; + validateAllowedKeys( + repository, + ["requiredFiles", "committedFiles", "editorConfig", "vscode", "toolConfigs"], + field, + CODE, + STANDARD_PATH, + findings + ); + validateList(repository.requiredFiles, `${field}.requiredFiles`, findings, (item, name) => { + validateAllowedKeys(item, ["paths"], name, CODE, STANDARD_PATH, findings); + validateStringArray(item.paths, `${name}.paths`, CODE, STANDARD_PATH, findings, { + nonempty: true + }); + }); + validateList(repository.committedFiles, `${field}.committedFiles`, findings, (item, name) => { + validateAllowedKeys( + item, + ["id", "description", "include", "exclude"], + name, + CODE, + STANDARD_PATH, + findings + ); + validateString(item.id, `${name}.id`, CODE, STANDARD_PATH, findings); + validateString(item.description, `${name}.description`, CODE, STANDARD_PATH, findings); + validatePatternList(item.include, `${name}.include`, findings); + validatePatternList(item.exclude, `${name}.exclude`, findings, { optional: true }); + }); + const editor = repository.editorConfig; + const editorField = `${field}.editorConfig`; + if (validateObject(editor, editorField, CODE, STANDARD_PATH, findings)) { + validateAllowedKeys( + editor, + ["path", "preamble", "sections", "conditionalSections"], + editorField, + CODE, + STANDARD_PATH, + findings + ); + validateString(editor.path, `${editorField}.path`, CODE, STANDARD_PATH, findings); + validateStringMap(editor.preamble, `${editorField}.preamble`, findings); + validateGroups( + editor.sections, + `${editorField}.sections`, + findings, + (items, name) => validateStringMap(items, name, findings) + ); + if (!Array.isArray(editor.conditionalSections)) { + addShapeError( + findings, + CODE, + STANDARD_PATH, + `${editorField}.conditionalSections`, + "an array" + ); + } else { + editor.conditionalSections.forEach((item, index) => { + const name = `${editorField}.conditionalSections[${index}]`; + if (!validateObject(item, name, CODE, STANDARD_PATH, findings)) return; + validateAllowedKeys( + item, + ["include", "section", "properties"], + name, + CODE, + STANDARD_PATH, + findings + ); + validatePatternList(item.include, `${name}.include`, findings); + validateString(item.section, `${name}.section`, CODE, STANDARD_PATH, findings); + validateStringMap(item.properties, `${name}.properties`, findings); + }); + } + } + const vscode = repository.vscode; + const vscodeField = `${field}.vscode`; + if (validateObject(vscode, vscodeField, CODE, STANDARD_PATH, findings)) { + validateAllowedKeys( + vscode, + ["extensionsPath", "settingsPath", "extensions", "settings"], + vscodeField, + CODE, + STANDARD_PATH, + findings + ); + for (const name of ["extensionsPath", "settingsPath"]) { + validateString(vscode[name], `${vscodeField}.${name}`, CODE, STANDARD_PATH, findings); + } + validateGroups( + vscode.extensions, + `${vscodeField}.extensions`, + findings, + (items, name) => validateStringArray(items, name, CODE, STANDARD_PATH, findings, { nonempty: true }) + ); + validateGroups(vscode.settings, `${vscodeField}.settings`, findings, (items, name) => { + if (!Array.isArray(items) || items.some((item) => !isObject(item))) { + addShapeError(findings, CODE, STANDARD_PATH, name, "an array of setting objects"); + } + }); + } + validateGroups( + repository.toolConfigs, + `${field}.toolConfigs`, + findings, + (items, name) => validateList(items, name, findings, (item, itemName) => { + validateAllowedKeys(item, ["tool", "paths"], itemName, CODE, STANDARD_PATH, findings); + validateString(item.tool, `${itemName}.tool`, CODE, STANDARD_PATH, findings); + validateStringArray(item.paths, `${itemName}.paths`, CODE, STANDARD_PATH, findings, { + nonempty: true + }); + }) + ); +} + +// library/repository-validator/src/validator/schemas/standard.mjs +function validateRegexes2(patterns, field, code, findings) { + for (const pattern of patterns) { + try { + new RegExp(pattern); + } catch { + findings.push( + createFinding( + "error", + code, + STANDARD_PATH, + `${field} contains invalid pattern: ${pattern}.` + ) + ); + } + } +} +function validateCiJobSpec(job, field, standard, findings) { + const code = "STANDARD021"; + if (!validateObject(job, field, code, STANDARD_PATH, findings)) return; + validateAllowedKeys( + job, + [ + "id", + "name", + "namePattern", + "checks", + "uses", + "permissions", + "secrets", + "surfaces", + "requiredWithScripts", + "waivable" + ], + field, + code, + STANDARD_PATH, + findings + ); + validateString(job.id, `${field}.id`, code, STANDARD_PATH, findings); + validateString(job.name, `${field}.name`, code, STANDARD_PATH, findings); + validateStringArray(job.checks, `${field}.checks`, code, STANDARD_PATH, findings, { + nonempty: true + }); + if (job.namePattern !== void 0) { + validateString(job.namePattern, `${field}.namePattern`, code, STANDARD_PATH, findings); + validateRegexes2([job.namePattern], `${field}.namePattern`, code, findings); + } + if (job.waivable !== void 0 && typeof job.waivable !== "boolean") { + addShapeError(findings, code, STANDARD_PATH, `${field}.waivable`, "a boolean when present"); + } + if (job.uses !== void 0) { + validateStringArray(job.uses, `${field}.uses`, code, STANDARD_PATH, findings, { + nonempty: true + }); + } + if (job.permissions !== void 0 && validateObject(job.permissions, `${field}.permissions`, code, STANDARD_PATH, findings)) { + for (const [scope, level] of Object.entries(job.permissions)) { + validateString(level, `${field}.permissions.${scope}`, code, STANDARD_PATH, findings); + } + } + if (job.requiredWithScripts !== void 0) { + validateStringArray( + job.requiredWithScripts, + `${field}.requiredWithScripts`, + code, + STANDARD_PATH, + findings, + { nonempty: true } + ); + } + if (job.secrets !== void 0) { + validateStringArray(job.secrets, `${field}.secrets`, code, STANDARD_PATH, findings, { + nonempty: true + }); + } + if (job.surfaces !== void 0 && validateStringArray(job.surfaces, `${field}.surfaces`, code, STANDARD_PATH, findings, { + nonempty: true + })) { + for (const surface of job.surfaces) { + if (!Object.hasOwn(standard.surfaceDetection, surface)) { + findings.push( + createFinding( + "error", + code, + STANDARD_PATH, + `${field}.surfaces references unknown surface ${surface}.` + ) + ); + } + } + } +} +var REPO_CHECKS_STRINGS = [ + "excludePattern", + "miseConfig", + "miseLock", + "packageJson", + "nodeVersionFile", + "npmLockFile", + "checkScript", + "terraformLockFile", + "terraformModulePattern" +]; +var REPO_CHECKS_LISTS = [ + "lockPlatforms", + "npmBackends", + "unplatformedBackends", + "aggregateScripts", + "actionlintConfigPaths", + "runnerLabels" +]; +function validateTechnology(technology, field, code, findings) { + if (!validateObject(technology, field, code, STANDARD_PATH, findings)) return; + validateAllowedKeys( + technology, + ["id", "name", "include", "shebang", "miseTasks", "npmScripts"], + field, + code, + STANDARD_PATH, + findings + ); + validateString(technology.id, `${field}.id`, code, STANDARD_PATH, findings); + validateString(technology.name, `${field}.name`, code, STANDARD_PATH, findings); + for (const name of ["include", "shebang"]) { + if (name === "shebang" && technology.shebang === void 0) continue; + if (validateString(technology[name], `${field}.${name}`, code, STANDARD_PATH, findings)) { + validateRegexes2([technology[name]], `${field}.${name}`, code, findings); + } + } + for (const name of ["miseTasks", "npmScripts"]) { + if (technology[name] === void 0) continue; + validateStringArray(technology[name], `${field}.${name}`, code, STANDARD_PATH, findings, { + nonempty: true + }); + } + if (technology.miseTasks === void 0 && technology.npmScripts === void 0) { + findings.push( + createFinding("error", code, STANDARD_PATH, `${field} must list miseTasks or npmScripts.`) + ); + } +} +function validateRepoChecks(repoChecks, field, code, findings) { + validateAllowedKeys( + repoChecks, + [...REPO_CHECKS_STRINGS, ...REPO_CHECKS_LISTS, "technologies", "miseTaskCommands"], + field, + code, + STANDARD_PATH, + findings + ); + for (const name of REPO_CHECKS_STRINGS) { + validateString(repoChecks[name], `${field}.${name}`, code, STANDARD_PATH, findings); + } + for (const name of ["excludePattern", "terraformModulePattern"]) { + if (typeof repoChecks[name] === "string") { + validateRegexes2([repoChecks[name]], `${field}.${name}`, code, findings); + } + } + for (const name of REPO_CHECKS_LISTS) { + validateStringArray(repoChecks[name], `${field}.${name}`, code, STANDARD_PATH, findings, { + nonempty: true + }); + } + if (!Array.isArray(repoChecks.technologies) || repoChecks.technologies.length === 0) { + addShapeError(findings, code, STANDARD_PATH, `${field}.technologies`, "a nonempty array"); + } else { + repoChecks.technologies.forEach( + (technology, index) => validateTechnology(technology, `${field}.technologies[${index}]`, code, findings) + ); + } + if (validateObject( + repoChecks.miseTaskCommands, + `${field}.miseTaskCommands`, + code, + STANDARD_PATH, + findings + )) { + for (const [task, pattern] of Object.entries(repoChecks.miseTaskCommands)) { + const name = `${field}.miseTaskCommands.${task}`; + if (validateString(pattern, name, code, STANDARD_PATH, findings)) { + validateRegexes2([pattern], name, code, findings); + } + } + } +} +function validateGithubActions(githubActions, standard, findings) { + const code = "STANDARD021"; + const field = "githubActions"; + const sections = [ + "ciWorkflow", + "coreGithubActions", + "runner", + "npmInstall", + "repoChecks", + "nodeVersions", + "inputNames", + "filePrefixes" + ]; + validateAllowedKeys( + githubActions, + ["workflowPattern", "actionPattern", ...sections], + field, + code, + STANDARD_PATH, + findings + ); + for (const name of ["workflowPattern", "actionPattern"]) { + if (validateString(githubActions[name], `${field}.${name}`, code, STANDARD_PATH, findings)) { + validateRegexes2([githubActions[name]], `${field}.${name}`, code, findings); + } + } + for (const name of sections) { + if (!validateObject(githubActions[name], `${field}.${name}`, code, STANDARD_PATH, findings)) + return; + } + const ci = githubActions.ciWorkflow; + validateAllowedKeys( + ci, + [ + "path", + "requiredTriggers", + "dispatchRevisionInput", + "contextJob", + "contextRevision", + "jobRevision", + "jobCondition", + "jobs", + "resultJob" + ], + `${field}.ciWorkflow`, + code, + STANDARD_PATH, + findings + ); + for (const name of [ + "path", + "dispatchRevisionInput", + "contextJob", + "contextRevision", + "jobRevision", + "jobCondition" + ]) { + validateString(ci[name], `${field}.ciWorkflow.${name}`, code, STANDARD_PATH, findings); + } + validateStringArray( + ci.requiredTriggers, + `${field}.ciWorkflow.requiredTriggers`, + code, + STANDARD_PATH, + findings, + { + nonempty: true + } + ); + if (!Array.isArray(ci.jobs) || ci.jobs.length === 0) { + addShapeError(findings, code, STANDARD_PATH, `${field}.ciWorkflow.jobs`, "a nonempty array"); + } else { + ci.jobs.forEach( + (job, index) => validateCiJobSpec(job, `${field}.ciWorkflow.jobs[${index}]`, standard, findings) + ); + if (!ci.jobs.some((job) => job?.id === ci.contextJob)) { + findings.push( + createFinding( + "error", + code, + STANDARD_PATH, + `${field}.ciWorkflow.jobs must include ${ci.contextJob}.` + ) + ); + } + if (!ci.jobs.some((job) => job?.id === REPO_CHECKS_JOB && Array.isArray(job.uses))) { + findings.push( + createFinding( + "error", + code, + STANDARD_PATH, + `${field}.ciWorkflow.jobs must include ${REPO_CHECKS_JOB} with the workflows it calls.` + ) + ); + } + } + const result = ci.resultJob; + if (validateObject(result, `${field}.ciWorkflow.resultJob`, code, STANDARD_PATH, findings)) { + validateAllowedKeys( + result, + ["id", "name", "if", "action", "stepName", "with"], + `${field}.ciWorkflow.resultJob`, + code, + STANDARD_PATH, + findings + ); + for (const name of ["id", "name", "if", "action", "stepName"]) { + validateString( + result[name], + `${field}.ciWorkflow.resultJob.${name}`, + code, + STANDARD_PATH, + findings + ); + } + validateObject( + result.with, + `${field}.ciWorkflow.resultJob.with`, + code, + STANDARD_PATH, + findings + ); + } + const core = githubActions.coreGithubActions; + validateAllowedKeys( + core, + ["repository", "minimumReleases"], + `${field}.coreGithubActions`, + code, + STANDARD_PATH, + findings + ); + validateString( + core.repository, + `${field}.coreGithubActions.repository`, + code, + STANDARD_PATH, + findings + ); + if (validateObject( + core.minimumReleases, + `${field}.coreGithubActions.minimumReleases`, + code, + STANDARD_PATH, + findings + )) { + for (const [component, version] of Object.entries(core.minimumReleases)) { + if (!/^(?:actions|workflows)\/[\w-]+$/.test(component) || !/^\d+\.\d+\.\d+$/.test(version)) { + findings.push( + createFinding( + "error", + code, + STANDARD_PATH, + `${field}.coreGithubActions.minimumReleases.${component} must map a component to x.y.z.` + ) + ); + } + } + } + const { runner, npmInstall, repoChecks, nodeVersions, inputNames, filePrefixes } = githubActions; + validateAllowedKeys( + runner, + ["label", "forbiddenLabels"], + `${field}.runner`, + code, + STANDARD_PATH, + findings + ); + validateString(runner.label, `${field}.runner.label`, code, STANDARD_PATH, findings); + validateStringArray( + runner.forbiddenLabels, + `${field}.runner.forbiddenLabels`, + code, + STANDARD_PATH, + findings + ); + validateAllowedKeys( + npmInstall, + ["requiredFlag", "actionsRequiringInstallFalse"], + `${field}.npmInstall`, + code, + STANDARD_PATH, + findings + ); + validateString( + npmInstall.requiredFlag, + `${field}.npmInstall.requiredFlag`, + code, + STANDARD_PATH, + findings + ); + validateStringArray( + npmInstall.actionsRequiringInstallFalse, + `${field}.npmInstall.actionsRequiringInstallFalse`, + code, + STANDARD_PATH, + findings + ); + validateRepoChecks(repoChecks, `${field}.repoChecks`, code, findings); + validateAllowedKeys( + nodeVersions, + [ + "nodeVersionFile", + "packageJson", + "npmrc", + "minimumNpm", + "minimumReleaseAgeDays", + "bundledNpm" + ], + `${field}.nodeVersions`, + code, + STANDARD_PATH, + findings + ); + for (const name of ["nodeVersionFile", "packageJson", "npmrc"]) { + validateString( + nodeVersions[name], + `${field}.nodeVersions.${name}`, + code, + STANDARD_PATH, + findings + ); + } + if (!/^\d+\.\d+\.\d+$/.test(String(nodeVersions.minimumNpm))) { + addShapeError( + findings, + code, + STANDARD_PATH, + `${field}.nodeVersions.minimumNpm`, + "an x.y.z version" + ); + } + if (validateObject( + nodeVersions.bundledNpm, + `${field}.nodeVersions.bundledNpm`, + code, + STANDARD_PATH, + findings + )) { + for (const [major, points] of Object.entries(nodeVersions.bundledNpm)) { + const valid = /^\d+$/.test(major) && Array.isArray(points) && points.length > 0 && points.every( + (point) => Array.isArray(point) && point.length === 2 && point.every((version) => /^\d+\.\d+\.\d+$/.test(String(version))) && String(point[0]).startsWith(`${major}.`) + ); + if (!valid) { + addShapeError( + findings, + code, + STANDARD_PATH, + `${field}.nodeVersions.bundledNpm.${major}`, + "a nonempty array of [node x.y.z, npm x.y.z] pairs in that major" + ); + } + } + } + if (!Number.isInteger(nodeVersions.minimumReleaseAgeDays) || nodeVersions.minimumReleaseAgeDays < 1) { + addShapeError( + findings, + code, + STANDARD_PATH, + `${field}.nodeVersions.minimumReleaseAgeDays`, + "a positive integer" + ); + } + validateAllowedKeys( + inputNames, + ["pattern"], + `${field}.inputNames`, + code, + STANDARD_PATH, + findings + ); + if (validateString(inputNames.pattern, `${field}.inputNames.pattern`, code, STANDARD_PATH, findings)) { + validateRegexes2([inputNames.pattern], `${field}.inputNames.pattern`, code, findings); + } + validateAllowedKeys( + filePrefixes, + ["callOnly", "dispatchAndCall"], + `${field}.filePrefixes`, + code, + STANDARD_PATH, + findings + ); + validateString( + filePrefixes.callOnly, + `${field}.filePrefixes.callOnly`, + code, + STANDARD_PATH, + findings + ); + validateString( + filePrefixes.dispatchAndCall, + `${field}.filePrefixes.dispatchAndCall`, + code, + STANDARD_PATH, + findings + ); +} +function validateStandard(standard) { + const findings = []; + const sections = ["standard", "surfaceDetection", "repository", "githubActions", "exceptions"]; + if (!validateObject(standard, "standard document", "STANDARD001", STANDARD_PATH, findings)) + return findings; + validateAllowedKeys( + standard, + sections, + "standard document", + "STANDARD001", + STANDARD_PATH, + findings + ); + for (const section of sections) + validateObject(standard[section], section, "STANDARD001", STANDARD_PATH, findings); + if (findings.some((finding2) => finding2.code === "STANDARD001")) return findings; + validateAllowedKeys( + standard.standard, + ["id", "version", "sourceRepository", "sourceCommit"], + "standard", + "STANDARD002", + STANDARD_PATH, + findings + ); + for (const field of ["id", "version", "sourceRepository", "sourceCommit"]) { + validateString( + standard.standard[field], + `standard.${field}`, + "STANDARD002", + STANDARD_PATH, + findings + ); + } + if (isNonemptyString(standard.standard.version) && standard.standard.version !== VALIDATOR_VERSION) { + findings.push( + createFinding( + "error", + "STANDARD014", + STANDARD_PATH, + `Validator ${VALIDATOR_VERSION} cannot validate standard ${standard.standard.version}.`, + "Install the validator version that exactly matches the embedded standard." + ) + ); + } + for (const [name, detection] of Object.entries(standard.surfaceDetection)) { + const field = `surfaceDetection.${name}`; + if (!validateObject(detection, field, "STANDARD006", STANDARD_PATH, findings)) continue; + validateAllowedKeys( + detection, + ["include", "exclude"], + field, + "STANDARD006", + STANDARD_PATH, + findings + ); + if (validateStringArray( + detection.include, + `${field}.include`, + "STANDARD006", + STANDARD_PATH, + findings, + { + nonempty: true + } + )) { + validateRegexes2(detection.include, `${field}.include`, "STANDARD007", findings); + } + if (detection.exclude !== void 0 && validateStringArray( + detection.exclude, + `${field}.exclude`, + "STANDARD006", + STANDARD_PATH, + findings + )) { + validateRegexes2(detection.exclude, `${field}.exclude`, "STANDARD007", findings); + } + } + validateRepositorySection(standard.repository, findings); + validateGithubActions(standard.githubActions, standard, findings); + validateAllowedKeys( + standard.exceptions, + ["nonOverridableRequirements"], + "exceptions", + "STANDARD003", + STANDARD_PATH, + findings + ); + validateStringArray( + standard.exceptions.nonOverridableRequirements, + "exceptions.nonOverridableRequirements", + "STANDARD003", + STANDARD_PATH, + findings + ); + if (standard.standard.sourceCommit === "repo-local-prototype") { + findings.push( + createFinding( + "warning", + "STALE001", + STANDARD_PATH, + "Embedded standard is a repo-local prototype.", + "Replace sourceCommit with the canonical commit SHA after promotion to core-services-ai-tooling." + ) + ); + } + return findings; +} + +// library/repository-validator/src/validator/validation.mjs +function buildResult(standard, findings, source = null) { + const uniqueFindings = deduplicateFindings(findings); + return { + result: { + provenance: createProvenance(standard), + repository: { + root: source?.root ?? null, + ref: source?.ref ?? null, + commit: source?.commit ?? null + }, + findings: uniqueFindings, + summary: summarizeFindings(uniqueFindings) + }, + exitCode: determineExitCode(uniqueFindings) + }; +} +function applyContractExceptions(findings, contract) { + return findings.map((finding2) => { + if (finding2.severity !== "error") return finding2; + const exception = contract.exceptions.find( + (candidate) => candidate.requirement === finding2.requirement && (!candidate.paths || candidate.paths.includes(finding2.path)) + ); + if (!exception) return finding2; + return { + ...finding2, + severity: determineExceptionSeverity(exception), + message: `${finding2.message} Waived by contract exception ${exception.id}.` + }; + }); +} +function createDefaultContract(standard) { + return { + standard: { path: STANDARD_PATH, id: standard.standard.id, version: standard.standard.version }, + exceptions: [] + }; +} +function loadExternalContract(contractPath, standard) { + if (!contractPath) return createDefaultContract(standard); + try { + return JSON.parse(readFileSync2(resolve3(contractPath), "utf8")); + } catch (error) { + throw new ValidatorError(`Cannot load repository contract ${contractPath}: ${error.message}`); + } +} +function runValidation({ + root = process.cwd(), + runCommand = runFile, + trackedFiles, + external = false, + ref = null, + contractPath = null, + coreGithubActionsRoot = null, + latestVersion = null, + standard: providedStandard = null +} = {}) { + let contract; + let standard; + if (external) { + standard = providedStandard ?? repository_standard_default; + contract = loadExternalContract(contractPath, standard); + } else { + contract = loadContract(root); + standard = loadStandard(root, contract); + } + const findings = validateStandard(standard); + if (findings.some((finding2) => finding2.severity === "error")) + return buildResult(standard, findings); + findings.push(...validateContract(contract, standard)); + if (findings.some((finding2) => finding2.severity === "error")) + return buildResult(standard, findings); + const source = createRepositorySource({ root, ref, runCommand, trackedFiles }); + const discovered = discoverRepositorySurfaces(standard, source.files); + const releaseTags = coreGithubActionsRoot ? listTagCommits(coreGithubActionsRoot, runCommand) : null; + const policy = standard.githubActions; + const workflows = loadWorkflowFiles(source, policy); + findings.push(...workflows.findings); + const packageScripts = discoverPackageScripts(standard, source); + const componentOf = createComponentResolver( + () => readOriginRepository(root, runCommand)?.toLowerCase() === policy.coreGithubActions.repository.toLowerCase() + ); + findings.push( + ...validateCiWorkflow( + workflows.files, + policy.ciWorkflow, + discovered, + packageScripts, + componentOf + ) + ); + findings.push(...validateCoreGhaPins(workflows.files, policy.coreGithubActions, releaseTags)); + findings.push(...validatePermissions(workflows.files)); + findings.push(...validateRunners(workflows.files, policy.runner)); + findings.push(...validateNpmInstalls(workflows.files, policy.npmInstall)); + const detected = detectTechnologies(source, policy.repoChecks); + findings.push(...validateRepoChecksPrerequisites(source, policy.repoChecks, detected)); + findings.push(...validateNodeVersions(source, workflows.files, policy.nodeVersions)); + const technologies = detected.filter(({ files }) => files.length > 0).map(({ technology }) => technology.id); + const baseline = standard.repository; + findings.push(...validateRequiredFiles(source, baseline)); + findings.push(...validateCommittedFiles(source, baseline)); + findings.push(...validateEditorConfig(source, baseline.editorConfig)); + findings.push(...validateVscodeExtensions(source, baseline.vscode, technologies)); + findings.push(...validateVscodeSettings(source, baseline.vscode, technologies)); + findings.push(...validateToolConfigs(source, baseline.toolConfigs, technologies)); + findings.push(...validateInputNames(workflows.files, policy.inputNames)); + findings.push(...validateFilePrefixes(workflows.files, policy.filePrefixes)); + findings.push( + ...validateInstalledKit(source, workflows.files, standard, { + external, + latestVersion, + componentOf + }) + ); + return buildResult(standard, applyContractExceptions(findings, contract), source); +} + +// library/repository-validator/src/validator/cli.mjs +var VALUE_OPTIONS = /* @__PURE__ */ new Map([ + ["--repo", "repo"], + ["--ref", "ref"], + ["--contract", "contract"], + ["--core-github-actions", "coreGithubActions"], + ["--latest-version", "latestVersion"] +]); +function parseArguments(argv) { + const options = { + json: false, + help: false, + version: false, + repo: null, + ref: null, + contract: null, + coreGithubActions: null, + latestVersion: null + }; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + const [flag, inlineValue] = argument.startsWith("--") ? argument.split(/=(.*)/s) : [argument]; + if (VALUE_OPTIONS.has(flag)) { + const value = inlineValue ?? argv[index += 1]; + if (!value) throw new ValidatorError(`${flag} requires a value.`); + options[VALUE_OPTIONS.get(flag)] = value; + } else if (argument === "--json") options.json = true; + else if (argument === "--help") options.help = true; + else if (argument === "--version") options.version = true; + else throw new ValidatorError(`Unknown argument: ${argument}`); + } + const hasValidationOptions = options.json || [...VALUE_OPTIONS.values()].some((name) => options[name] !== null); + if ([options.help, options.version].filter(Boolean).length > 1 || (options.help || options.version) && hasValidationOptions) { + throw new ValidatorError("--help and --version must be used alone."); + } + if (options.latestVersion && !/^\d+\.\d+\.\d+$/.test(options.latestVersion)) { + throw new ValidatorError("--latest-version must be x.y.z."); + } + if (!options.repo && (options.ref || options.contract)) { + throw new ValidatorError("--ref and --contract require --repo."); + } + return options; +} +function resolveDirectory(path, flag) { + const absolute = resolve4(path); + if (!existsSync(absolute) || !statSync2(absolute).isDirectory()) { + throw new ValidatorError(`${flag} is not a directory: ${path}`); + } + return absolute; +} +function renderJsonResults(result) { + return `${JSON.stringify(result, null, 2)} +`; +} +function renderRepository(repository) { + return repository?.ref ? [`Repository: ${repository.root} at ${repository.ref} (${repository.commit})`] : []; +} +function renderHumanResults(result) { + return `${[ + `Standard: ${result.provenance.standardId} ${result.provenance.standardVersion}`, + `Source: ${result.provenance.sourceRepository}@${result.provenance.sourceCommit}`, + ...renderRepository(result.repository), + "", + ...result.findings.map( + (finding2) => `${finding2.severity.toUpperCase()} ${finding2.code} ${finding2.message}` + ), + "", + `${result.summary.errors} errors, ${result.summary.warnings} warnings, ${result.summary.passes} passes` + ].join("\n")} +`; +} +function renderHelp() { + return [ + "Usage: node validate-repo.mjs [--json] [--repo [--ref ] [--contract ]]", + " [--core-github-actions ] [--latest-version ]", + " node validate-repo.mjs --help | --version", + "", + "Validate a repository against the Core Services standard without executing repository tasks.", + "", + "Without --repo, validates the current directory against its installed .core-services/ contract", + "and standard snapshot. With --repo, validates that repository externally against the standard", + "bundled with this validator; nothing is written to it.", + "", + " --repo Repository to validate externally.", + " --ref Validate the files at this ref instead of the working tree.", + " --contract Contract with exceptions for the external repository.", + " --core-github-actions Local core-github-actions clone; checks pins match release tags.", + " --latest-version Warn when the installed validator is older than this release.", + " --json Print machine-readable results.", + "" + ].join("\n"); +} +function runCli(argv = process.argv.slice(2), dependencies = {}) { + try { + const options = parseArguments(argv); + if (options.help) return { stdout: renderHelp(), stderr: "", exitCode: 0 }; + if (options.version) return { stdout: `${VALIDATOR_VERSION} +`, stderr: "", exitCode: 0 }; + const { result, exitCode } = runValidation({ + ...dependencies, + ...options.repo ? { root: resolveDirectory(options.repo, "--repo"), external: true } : {}, + ref: options.ref, + latestVersion: options.latestVersion, + contractPath: options.contract, + coreGithubActionsRoot: options.coreGithubActions ? resolveDirectory(options.coreGithubActions, "--core-github-actions") : null + }); + return { + stdout: options.json ? renderJsonResults(result) : renderHumanResults(result), + stderr: "", + exitCode + }; + } catch (error) { + const message = error instanceof ValidatorError ? error.message : "Unexpected validator failure."; + return { stdout: "", stderr: `Repository validator error: ${message} +`, exitCode: 2 }; + } +} + +// library/repository-validator/src/validate-repo.mjs +if (process.argv[1] && import.meta.url === pathToFileURL(resolve5(process.argv[1])).href) { + const outcome = runCli(); + process.stdout.write(outcome.stdout); + process.stderr.write(outcome.stderr); + process.exitCode = outcome.exitCode; +} +export { + CONTRACT_PATH, + STANDARD_PATH, + VALIDATOR_VERSION, + ValidatorError, + createFinding, + createRepositorySource, + detectTechnologies, + determineExitCode, + discoverPackageScripts, + discoverRepositorySurfaces, + isolatedEnvironment, + listTagCommits, + listTrackedFiles, + loadContract, + loadStandard, + loadWorkflowFiles, + parseArguments, + parseJsonc, + renderHelp, + renderHumanResults, + renderJsonResults, + runCli, + runFile, + runValidation, + summarizeFindings, + validateCiWorkflow, + validateCommittedFiles, + validateContract, + validateCoreGhaPins, + validateEditorConfig, + validateFilePrefixes, + validateInputNames, + validateNodeVersions, + validateNpmInstalls, + validatePermissions, + validateRepoChecksPrerequisites, + validateRequiredFiles, + validateRunners, + validateStandard, + validateToolConfigs, + validateVscodeExtensions, + validateVscodeSettings +}; diff --git a/.core-services/repository-contract.json b/.core-services/repository-contract.json new file mode 100644 index 0000000..50319a0 --- /dev/null +++ b/.core-services/repository-contract.json @@ -0,0 +1,54 @@ +{ + "standard": { + "path": ".core-services/repository-standard.json", + "id": "core-services-ai-ready-repository", + "version": "0.14.1" + }, + "exceptions": [ + { + "id": "published-workflow-names", + "requirement": "workflow-file-prefixes", + "paths": [ + ".github/workflows/deploy_environment.yml", + ".github/workflows/deploy_thru_prod.yml", + ".github/workflows/resolve_ci_context.yml", + ".github/workflows/run_gha_checks.yml", + ".github/workflows/run_infra_ci.yml", + ".github/workflows/run_js_checks.yml", + ".github/workflows/run_markdown_checks.yml", + ".github/workflows/run_npm_audit.yml", + ".github/workflows/run_npm_publish.yml", + ".github/workflows/run_repo_checks.yml", + ".github/workflows/run_repo_validation.yml", + ".github/workflows/run_semgrep_scan.yml", + ".github/workflows/run_shell_checks.yml", + ".github/workflows/run_sonar_scan.yml", + ".github/workflows/run_terraform_checks.yml", + ".github/workflows/tf_apply.yml", + ".github/workflows/tf_plan_env_roots.yml", + ".github/workflows/tf_plan_single_root.yml", + ".github/workflows/tf_validate_plan_env_roots.yml", + ".github/workflows/tf_validate_plan_single_root.yml", + ".github/workflows/trigger_workflow_and_wait.yml" + ], + "rationale": "These are the reusable workflows core-github-actions publishes. Consumers call them by file name and the release tags are named after them, so an on_call_ prefix would break every caller.", + "owner": "@OpenSesame/core-domain-operators" + }, + { + "id": "no-runtime-dependencies", + "requirement": "ci-job:dependency-audit", + "paths": [".github/workflows/on_push_ci.yml"], + "rationale": "Every dependency here is a devDependency, which run_npm_audit omits, so the job would audit nothing. App CI runs npm run audit:npm on the full tree instead.", + "owner": "@OpenSesame/core-domain-operators", + "isWarning": true + }, + { + "id": "no-sonar-project", + "requirement": "ci-job:sonar", + "paths": [".github/workflows/on_push_ci.yml"], + "rationale": "core-github-actions has no SonarCloud project; add the Sonar job when one exists.", + "owner": "@OpenSesame/core-domain-operators", + "isWarning": true + } + ] +} diff --git a/.core-services/repository-contract.schema.json b/.core-services/repository-contract.schema.json new file mode 100644 index 0000000..58cd2df --- /dev/null +++ b/.core-services/repository-contract.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/OpenSesame/core-services-ai-tooling/library/repository-validator/schemas/repository-contract.schema.json", + "title": "Core Services repository contract", + "description": "Repository-owned exceptions for the bundled Core Services repository standard.", + "type": "object", + "additionalProperties": false, + "required": ["standard", "exceptions"], + "properties": { + "standard": { + "$ref": "#/$defs/standardReference" + }, + "exceptions": { + "type": "array", + "items": { + "$ref": "#/$defs/exception" + } + } + }, + "$defs": { + "standardReference": { + "type": "object", + "additionalProperties": false, + "required": ["path", "id", "version"], + "properties": { + "path": { + "const": ".core-services/repository-standard.json" + }, + "id": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "minLength": 1 + } + } + }, + "exception": { + "type": "object", + "additionalProperties": false, + "required": ["id", "requirement", "rationale", "owner"], + "properties": { + "id": { + "type": "string", + "minLength": 1 + }, + "requirement": { + "type": "string", + "minLength": 1, + "description": "Requirement the exception waives, such as workflow-input-names or repo-checks-prerequisites." + }, + "rationale": { + "type": "string", + "minLength": 1 + }, + "owner": { + "type": "string", + "minLength": 1 + }, + "reviewBy": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + }, + "isWarning": { + "type": "boolean" + }, + "paths": { + "type": "array", + "description": "Repository-relative paths the exception covers; omit to cover every path.", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1, + "uniqueItems": true + } + } + } + } +} diff --git a/.core-services/repository-standard.json b/.core-services/repository-standard.json new file mode 100644 index 0000000..6d8be6d --- /dev/null +++ b/.core-services/repository-standard.json @@ -0,0 +1,751 @@ +{ + "standard": { + "id": "core-services-ai-ready-repository", + "version": "0.14.1", + "sourceRepository": "OpenSesame/core-services-ai-tooling", + "sourceCommit": "41300ef002ee4af0dd9862d4f29c74c482dd61ad" + }, + "surfaceDetection": { + "npm": { + "include": [ + "(^|/)package\\.json$" + ] + }, + "terraform": { + "include": [ + "\\.tf$" + ], + "exclude": [ + "(^|/)\\.terraform/" + ] + } + }, + "repository": { + "requiredFiles": [ + { + "paths": [ + "README.md" + ] + }, + { + "paths": [ + "AGENTS.md" + ] + }, + { + "paths": [ + ".gitignore" + ] + }, + { + "paths": [ + ".github/CODEOWNERS", + "CODEOWNERS", + "docs/CODEOWNERS" + ] + }, + { + "paths": [ + ".editorconfig" + ] + }, + { + "paths": [ + ".vscode/extensions.json" + ] + }, + { + "paths": [ + ".vscode/settings.json" + ] + } + ], + "committedFiles": [ + { + "id": "terraform-working-directory", + "description": "a Terraform working directory", + "include": [ + "(^|/)\\.terraform/" + ] + }, + { + "id": "terraform-state", + "description": "Terraform state", + "include": [ + "\\.tfstate(?:\\..+)?$" + ] + }, + { + "id": "terraform-plan", + "description": "a saved Terraform plan", + "include": [ + "\\.tfplan(?:\\.json)?$", + "(^|/)(?:tfplan|plan\\.out)$" + ] + }, + { + "id": "terraform-variable-values", + "description": "Terraform variable values", + "include": [ + "\\.tfvars(?:\\.json)?$" + ] + }, + { + "id": "terraform-local-credentials", + "description": "Terraform CLI credentials", + "include": [ + "(^|/)(?:credentials\\.tfrc\\.json|\\.terraformrc|terraform\\.rc)$" + ] + }, + { + "id": "local-environment", + "description": "a local .env file", + "include": [ + "(^|/)\\.env(?:\\.[^/]+)?$" + ], + "exclude": [ + "\\.env\\.(?:example|sample|template|tpl)$" + ] + }, + { + "id": "private-key-or-certificate", + "description": "a private key or certificate", + "include": [ + "\\.(?:cer|crt|der|key|p12|pem|pfx|ppk)$", + "(^|/)(?:id_rsa|id_dsa|id_ecdsa|id_ed25519)$" + ] + }, + { + "id": "cloud-credentials", + "description": "cloud credentials", + "include": [ + "(^|/)\\.aws/credentials$", + "(^|/)application_default_credentials\\.json$" + ] + } + ], + "editorConfig": { + "path": ".editorconfig", + "preamble": { + "root": "true" + }, + "sections": { + "*": { + "charset": "utf-8", + "end_of_line": "lf", + "indent_size": "2", + "indent_style": "space", + "insert_final_newline": "true", + "trim_trailing_whitespace": "true" + }, + "*.md": { + "trim_trailing_whitespace": "false" + } + }, + "conditionalSections": [ + { + "include": [ + "(^|/)Makefile$" + ], + "section": "Makefile", + "properties": { + "indent_style": "tab" + } + } + ] + }, + "vscode": { + "extensionsPath": ".vscode/extensions.json", + "settingsPath": ".vscode/settings.json", + "extensions": { + "all": [ + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker" + ], + "gha": [ + "github.vscode-github-actions", + "redhat.vscode-yaml" + ], + "terraform": [ + "hashicorp.terraform" + ], + "markdown": [ + "davidanson.vscode-markdownlint" + ], + "shell": [ + "timonwong.shellcheck", + "foxundermoon.shell-format" + ], + "js": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] + }, + "settings": { + "all": [ + { + "editor.formatOnSave": true, + "editor.rulers": [ + 100 + ] + } + ], + "terraform": [ + { + "[terraform]": { + "editor.defaultFormatter": "hashicorp.terraform" + }, + "[terraform-vars]": { + "editor.defaultFormatter": "hashicorp.terraform" + } + } + ], + "markdown": [ + { + "[markdown]": { + "editor.defaultFormatter": null + } + } + ], + "shell": [ + { + "[shellscript]": { + "editor.defaultFormatter": "foxundermoon.shell-format" + } + } + ], + "js": [ + { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.requireConfig": true, + "yaml.format.enable": false, + "eslint.format.enable": false, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + } + } + ] + } + }, + "toolConfigs": { + "terraform": [ + { + "tool": "tflint", + "paths": [ + ".tflint.hcl" + ] + }, + { + "tool": "Checkov", + "paths": [ + ".checkov.yml", + ".checkov.yaml" + ] + } + ], + "markdown": [ + { + "tool": "markdownlint-cli2", + "paths": [ + ".markdownlint-cli2.yaml", + ".markdownlint-cli2.jsonc", + ".markdownlint-cli2.cjs", + ".markdownlint-cli2.mjs", + ".markdownlint.yaml", + ".markdownlint.yml", + ".markdownlint.jsonc", + ".markdownlint.json", + ".markdownlint.cjs", + ".markdownlint.mjs" + ] + } + ], + "js": [ + { + "tool": "ESLint", + "paths": [ + "eslint.config.mjs", + "eslint.config.js", + "eslint.config.cjs", + "eslint.config.ts", + "eslint.config.mts", + "eslint.config.cts", + ".eslintrc.json", + ".eslintrc.js", + ".eslintrc.cjs", + ".eslintrc.yaml", + ".eslintrc.yml" + ] + }, + { + "tool": "Prettier", + "paths": [ + ".prettierrc", + ".prettierrc.json", + ".prettierrc.json5", + ".prettierrc.yaml", + ".prettierrc.yml", + ".prettierrc.toml", + ".prettierrc.js", + ".prettierrc.cjs", + ".prettierrc.mjs", + "prettier.config.js", + "prettier.config.cjs", + "prettier.config.mjs", + ".prettierrc.ts", + ".prettierrc.mts", + ".prettierrc.cts", + "prettier.config.ts", + "prettier.config.mts", + "prettier.config.cts", + "package.json#prettier" + ] + } + ] + } + }, + "githubActions": { + "workflowPattern": "^\\.github/workflows/[^/]+\\.ya?ml$", + "actionPattern": "^\\.github/actions/(?:[^/]+/)+action\\.ya?ml$", + "ciWorkflow": { + "path": ".github/workflows/on_push_ci.yml", + "requiredTriggers": [ + "push", + "pull_request", + "workflow_dispatch" + ], + "dispatchRevisionInput": "commit-identifier", + "contextJob": "ci-context", + "contextRevision": "${{ inputs.commit-identifier }}", + "jobRevision": "${{ needs.ci-context.outputs.commit-identifier }}", + "jobCondition": "needs.ci-context.outputs.should-run-ci == 'true'", + "jobs": [ + { + "id": "ci-context", + "name": "CI Context", + "checks": [ + "CI Context / Resolve" + ], + "uses": [ + "workflows/resolve_ci_context" + ] + }, + { + "id": "app-ci", + "name": "App CI", + "namePattern": "^App CI(?: \\([^()]+\\))?$", + "checks": [ + "App CI ()" + ], + "surfaces": [ + "npm" + ], + "requiredWithScripts": [ + "build", + "test" + ] + }, + { + "id": "dependency-audit", + "name": "Dependency Audit", + "checks": [ + "Dependency Audit / Audit" + ], + "uses": [ + "workflows/run_npm_audit" + ], + "surfaces": [ + "npm" + ], + "waivable": true + }, + { + "id": "repo-checks", + "name": "Repo Checks", + "checks": [ + "Repo Checks / Detect", + "Repo Checks / Standard / Validate", + "Repo Checks / Summary" + ], + "uses": [ + "workflows/run_repo_checks" + ], + "permissions": { + "contents": "read" + }, + "secrets": [ + "ORG_READ_ONLY_SSH_KEY" + ] + }, + { + "id": "terraform", + "name": "Terraform", + "checks": [ + "Terraform / / Validate Plan" + ], + "uses": [ + "workflows/tf_validate_plan_env_roots", + "workflows/tf_validate_plan_single_root" + ], + "surfaces": [ + "terraform" + ] + }, + { + "id": "semgrep", + "name": "Semgrep", + "checks": [ + "Semgrep / Scan" + ], + "uses": [ + "workflows/run_semgrep_scan" + ] + }, + { + "id": "sonar", + "name": "Sonar", + "checks": [ + "Sonar / Scan" + ], + "uses": [ + "workflows/run_sonar_scan" + ], + "waivable": true + } + ], + "resultJob": { + "id": "ci-result", + "name": "CI Result", + "if": "${{ !cancelled() && needs.ci-context.outputs.should-run-ci != 'false' }}", + "action": "actions/check-ci-result", + "stepName": "Check CI job results", + "with": { + "needs-json": "${{ toJSON(needs) }}" + } + } + }, + "coreGithubActions": { + "repository": "OpenSesame/core-github-actions", + "minimumReleases": { + "actions/pr-open-check": "2.1.0", + "workflows/resolve_ci_context": "1.0.0", + "workflows/run_npm_audit": "1.0.0", + "workflows/run_sonar_scan": "2.0.0", + "actions/run-semgrep": "2.0.0", + "workflows/run_semgrep_scan": "2.0.0", + "workflows/tf_validate_plan_env_roots": "1.0.0", + "workflows/tf_validate_plan_single_root": "1.0.0", + "workflows/run_infra_ci": "1.0.1", + "workflows/run_repo_checks": "1.0.2", + "actions/resolve-feature-workspace": "1.0.0", + "actions/configure-aws-oidc": "1.0.2", + "actions/check-ci-result": "1.0.0", + "workflows/trigger_workflow_and_wait": "1.0.0", + "workflows/tf_apply": "0.2.1", + "workflows/deploy_environment": "0.3.1", + "workflows/deploy_thru_prod": "0.3.1" + } + }, + "runner": { + "label": "ubuntu-26.04", + "forbiddenLabels": [ + "ubuntu-latest" + ] + }, + "npmInstall": { + "requiredFlag": "--ignore-scripts", + "actionsRequiringInstallFalse": [ + "cypress-io/github-action" + ] + }, + "repoChecks": { + "excludePattern": "(^|/)(\\.core-services|node_modules|\\.terraform|dist|build|coverage|vendor)/", + "technologies": [ + { + "id": "gha", + "name": "GitHub Actions", + "include": "^\\.github/(workflows/[^/]+|actions/.+/action)\\.ya?ml$", + "miseTasks": [ + "lint:gha", + "scan:gha" + ] + }, + { + "id": "terraform", + "name": "Terraform", + "include": "\\.tf$", + "miseTasks": [ + "init:terraform", + "validate:terraform", + "lint:terraform", + "format:terraform", + "scan:terraform" + ] + }, + { + "id": "markdown", + "name": "Markdown", + "include": "\\.md$", + "npmScripts": [ + "lint:markdown" + ] + }, + { + "id": "shell", + "name": "Shell", + "include": "\\.(sh|bash|zsh)$", + "shebang": "^#!\\s*(/usr/bin/env\\s+)?(/usr)?(/local)?(/bin/)?(sh|bash|zsh)(\\s|$)", + "miseTasks": [ + "lint:shell", + "format:shell" + ] + }, + { + "id": "js", + "name": "JS", + "include": "\\.(js|mjs|cjs|jsx|ts|mts|cts|tsx)$", + "npmScripts": [ + "lint:js", + "format:prettier" + ] + } + ], + "miseTaskCommands": { + "scan:gha": "\\bzizmor\\b[^\\n]*--offline", + "init:terraform": "-lockfile=readonly" + }, + "miseConfig": "mise.toml", + "miseLock": "mise.lock", + "lockPlatforms": [ + "linux-x64", + "macos-arm64" + ], + "npmBackends": [ + "npm" + ], + "unplatformedBackends": [ + "pipx", + "go", + "cargo", + "gem", + "dotnet", + "spm" + ], + "packageJson": "package.json", + "nodeVersionFile": ".nvmrc", + "npmLockFile": "package-lock.json", + "aggregateScripts": [ + "lint", + "format", + "scan", + "check" + ], + "checkScript": "check", + "terraformLockFile": ".terraform.lock.hcl", + "terraformModulePattern": "(^|/)modules/", + "actionlintConfigPaths": [ + ".github/actionlint.yaml", + ".github/actionlint.yml" + ], + "runnerLabels": [ + "ubuntu-26.04" + ] + }, + "nodeVersions": { + "nodeVersionFile": ".nvmrc", + "packageJson": "package.json", + "npmrc": ".npmrc", + "minimumNpm": "11.10.0", + "minimumReleaseAgeDays": 7, + "bundledNpm": { + "22": [ + [ + "22.0.0", + "10.5.1" + ], + [ + "22.1.0", + "10.7.0" + ], + [ + "22.3.0", + "10.8.1" + ], + [ + "22.5.0", + "10.8.2" + ], + [ + "22.9.0", + "10.8.3" + ], + [ + "22.10.0", + "10.9.0" + ], + [ + "22.13.0", + "10.9.2" + ], + [ + "22.18.0", + "10.9.3" + ], + [ + "22.21.0", + "10.9.4" + ], + [ + "22.22.2", + "10.9.7" + ], + [ + "22.22.3", + "10.9.8" + ], + [ + "22.23.3", + "10.9.9" + ] + ], + "24": [ + [ + "24.0.0", + "11.3.0" + ], + [ + "24.3.0", + "11.4.2" + ], + [ + "24.5.0", + "11.5.1" + ], + [ + "24.8.0", + "11.6.0" + ], + [ + "24.10.0", + "11.6.1" + ], + [ + "24.11.1", + "11.6.2" + ], + [ + "24.13.1", + "11.8.0" + ], + [ + "24.14.0", + "11.9.0" + ], + [ + "24.14.1", + "11.11.0" + ], + [ + "24.15.0", + "11.12.1" + ], + [ + "24.16.0", + "11.13.0" + ], + [ + "24.18.0", + "11.16.0" + ], + [ + "24.19.0", + "11.17.0" + ], + [ + "24.20.0", + "11.19.0" + ], + [ + "24.22.0", + "11.19.1" + ] + ], + "26": [ + [ + "26.0.0", + "11.12.1" + ], + [ + "26.1.0", + "11.13.0" + ], + [ + "26.3.0", + "11.16.0" + ], + [ + "26.4.0", + "11.17.0" + ], + [ + "26.6.0", + "11.18.0" + ], + [ + "26.7.0", + "11.19.0" + ], + [ + "26.8.2", + "11.19.1" + ], + [ + "26.11.0", + "11.20.0" + ] + ] + } + }, + "inputNames": { + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" + }, + "filePrefixes": { + "callOnly": "on_call_", + "dispatchAndCall": "on_dispatch_" + } + }, + "exceptions": { + "nonOverridableRequirements": [ + "ci-workflow", + "ci-jobs", + "ci-result", + "core-gha-pins", + "workflow-permissions", + "runner", + "npm-ignore-scripts", + "repo-validation", + "kit-integrity" + ] + } +} diff --git a/.core-services/templates/contract-exception.example.json b/.core-services/templates/contract-exception.example.json new file mode 100644 index 0000000..26c8c4f --- /dev/null +++ b/.core-services/templates/contract-exception.example.json @@ -0,0 +1,9 @@ +{ + "id": "example-legacy-input-names", + "requirement": "workflow-input-names", + "paths": [".github/workflows/example_legacy.yml"], + "rationale": "Replace this example with the repository-specific constraint and recovery plan.", + "owner": "team-name", + "reviewBy": "2027-01-31", + "isWarning": true +} diff --git a/.core-services/templates/repository-contract.minimal.json b/.core-services/templates/repository-contract.minimal.json new file mode 100644 index 0000000..6df9d1e --- /dev/null +++ b/.core-services/templates/repository-contract.minimal.json @@ -0,0 +1,8 @@ +{ + "standard": { + "path": ".core-services/repository-standard.json", + "id": "core-services-ai-ready-repository", + "version": "0.14.1" + }, + "exceptions": [] +} diff --git a/.core-services/validator-manifest.json b/.core-services/validator-manifest.json new file mode 100644 index 0000000..e1ffb02 --- /dev/null +++ b/.core-services/validator-manifest.json @@ -0,0 +1,15 @@ +{ + "validatorVersion": "0.14.1", + "standardId": "core-services-ai-ready-repository", + "standardVersion": "0.14.1", + "sourceRepository": "OpenSesame/core-services-ai-tooling", + "sourceCommit": "41300ef002ee4af0dd9862d4f29c74c482dd61ad", + "managedFiles": { + ".core-services/README.md": "bb1c365482c9652a17b81bd8e734db0f03164b0398e9a9edf3f7ed1b113992de", + ".core-services/generated/validate-repo.mjs": "b6045884b9f3a6ae9b24d8ead9e55f9dc0aac4c8d93bff7b5ecae481240ff186", + ".core-services/repository-contract.schema.json": "39bb3640e45e58865a4b0b916820a0bf48735577b0172356486ef02fd024ef7b", + ".core-services/repository-standard.json": "29b3eb40dfd08fe5db6fd0773e5c75cfae3e7eb2b19e1334047554740ff6c6e7", + ".core-services/templates/contract-exception.example.json": "4de592c6a5588db9233a0f20ba2a73d661a1c1835dbeab7a769d6f5d0d5292de", + ".core-services/templates/repository-contract.minimal.json": "ce9a21a8fd58b05a2cd890d3ca3b6610c98bb71398be0dbdbbfb99a325b97d84" + } +} diff --git a/.gitattributes b/.gitattributes index 2fe9951..6651528 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,4 @@ # SVG is text (keep diffs readable) *.svg text +.core-services/generated/** linguist-generated=true diff --git a/.github/.markdownlint.jsonc b/.github/.markdownlint.jsonc index f2a375a..7e34a0a 100644 --- a/.github/.markdownlint.jsonc +++ b/.github/.markdownlint.jsonc @@ -1,3 +1,4 @@ { + "extends": "../.markdownlint.jsonc", "MD024": false } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ad4f4ba..4224d07 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,9 @@ Jira: ## Versioning -⚠️ Components in this repo are used by multiple repos and teams. Breaking changes to non-versioned components are high-risk. Always apply correct versioning to versioned components to ensure safe, controlled updates. +⚠️ Components in this repo are used by multiple repos and teams. Breaking changes to non-versioned +components are high-risk. Always apply correct versioning to versioned components to ensure safe, +controlled updates. Versioned components live under `.github/actions` and `.github/workflows`. diff --git a/.github/actions/check-ci-result/CHANGELOG.md b/.github/actions/check-ci-result/CHANGELOG.md index b4b7a63..b1085e2 100644 --- a/.github/actions/check-ci-result/CHANGELOG.md +++ b/.github/actions/check-ci-result/CHANGELOG.md @@ -7,5 +7,7 @@ All notable changes to the `check-ci-result` composite action are documented in ### Added - Summarize each needed job's result in the step summary from the `needs-json` input. -- Pass when every result is `success` or `skipped`; fail with one `::error` line listing each job whose result is `failure` or `cancelled`. -- Fail with an `Invalid needs-json` error when `needs-json` is empty, not a single JSON value, an empty or non-object value, or has an entry without a valid job ID and a known result. +- Pass when every result is `success` or `skipped`; fail with one `::error` line listing each job + whose result is `failure` or `cancelled`. +- Fail with an `Invalid needs-json` error when `needs-json` is empty, not a single JSON value, an + empty or non-object value, or has an entry without a valid job ID and a known result. diff --git a/.github/actions/check-ci-result/README.md b/.github/actions/check-ci-result/README.md index f3e7991..f1e8400 100644 --- a/.github/actions/check-ci-result/README.md +++ b/.github/actions/check-ci-result/README.md @@ -2,12 +2,14 @@ ## Summary -Checks the results of the jobs a CI result job needs. It replaces the `ci-result` script copied into each standard `on_push_ci.yml`. +Checks the results of the jobs a CI result job needs. It replaces the `ci-result` script copied into +each standard `on_push_ci.yml`. - Writes a table of each needed job and its result to the step summary. - Passes when every result is `success` or `skipped`. - Fails when any result is `failure` or `cancelled`, with one `::error` line that lists those jobs. -- Fails with an `Invalid needs-json` error when `needs-json` is empty, is not valid JSON, has no jobs, or has an entry without a valid job ID and result. +- Fails with an `Invalid needs-json` error when `needs-json` is empty, is not valid JSON, has no + jobs, or has an entry without a valid job ID and result. ## Inputs @@ -33,13 +35,17 @@ ci-result: Replace `` with the full commit SHA of the tag: `git rev-list -n1 actions/check-ci-result/1.0.0`. -The action needs no checkout and no GitHub token permissions. It uses `jq`, which is preinstalled on GitHub-hosted Ubuntu runners. +The action needs no checkout and no GitHub token permissions. It uses `jq`, which is preinstalled on +GitHub-hosted Ubuntu runners. ## Why an action and not a reusable workflow -- A reusable workflow can't read the caller's `needs`, so each caller would still have to pass the results in. -- A reusable workflow reports its checks as `CI Result / `, which would rename the required check. As a step in the caller's own job, the check stays `CI Result`. +- A reusable workflow can't read the caller's `needs`, so each caller would still have to pass the + results in. +- A reusable workflow reports its checks as `CI Result / `, which would rename the required + check. As a step in the caller's own job, the check stays `CI Result`. ## Versioning -This action follows the repository's component versioning policy. The initial release uses the PR label `v:a/check-ci-result/1.0.0` and the namespaced tag `actions/check-ci-result/1.0.0`. +This action follows the repository's component versioning policy. The initial release uses the PR +label `v:a/check-ci-result/1.0.0` and the namespaced tag `actions/check-ci-result/1.0.0`. diff --git a/.github/actions/configure-aws-oidc/CHANGELOG.md b/.github/actions/configure-aws-oidc/CHANGELOG.md index 171f25f..06c0ed3 100644 --- a/.github/actions/configure-aws-oidc/CHANGELOG.md +++ b/.github/actions/configure-aws-oidc/CHANGELOG.md @@ -14,7 +14,8 @@ All notable changes to the `configure-aws-oidc` composite action are documented - Pinned all internal `uses:` steps to commit SHAs with a version comment for auditability (CORE-5974). - Bumped `aws-actions/configure-aws-credentials` from `v6.2.3` to `v6.2.4`. -- `OpenSesame/gha-oidc-access/get-role-arn` remains pinned to the same commit it already used (which is ahead of that repo's stale `v2` tag) — see PR notes. +- `OpenSesame/gha-oidc-access/get-role-arn` remains pinned to the same commit it already used (which + is ahead of that repo's stale `v2` tag) — see PR notes. ## 1.0.0 diff --git a/.github/actions/configure-aws-oidc/README.md b/.github/actions/configure-aws-oidc/README.md index 8f84218..5c0f1a3 100644 --- a/.github/actions/configure-aws-oidc/README.md +++ b/.github/actions/configure-aws-oidc/README.md @@ -1,6 +1,8 @@ # Configure AWS OIDC -> **Warning:** This action changes the runner workspace because `get-role-arn` checks out its own repository. Run this action before checking out the consumer repository, or run `actions/checkout` again immediately afterward before using consumer repository files. +> **Warning:** This action changes the runner workspace because `get-role-arn` checks out its own +> repository. Run this action before checking out the consumer repository, or run `actions/checkout` +> again immediately afterward before using consumer repository files. ## Summary @@ -10,7 +12,8 @@ Configures temporary AWS credentials through GitHub OIDC by performing three ope 2. Resolve the AWS role ARN and region. 3. Assume the resolved role and export temporary AWS credentials for later steps in the same job. -This cohesive utility is intended for jobs that always perform these operations together. It does not support workflows that resolve the role in one job and configure credentials in another. +This cohesive utility is intended for jobs that always perform these operations together. It does +not support workflows that resolve the role in one job and configure credentials in another. ## Permissions @@ -33,7 +36,9 @@ permissions: | `actor` | Yes | Triggering GitHub actor included in the role session name. | | `ORG_READ_ONLY_SSH_KEY` | Yes | Private SSH key required by `OpenSesame/gha-oidc-access/get-role-arn`. | -`ORG_READ_ONLY_SSH_KEY` is an actual private SSH key and must always be passed from the GitHub `secrets` context. It is declared as an action input only because composite actions do not support a `secrets:` interface. +`ORG_READ_ONLY_SSH_KEY` is an actual private SSH key and must always be passed from the GitHub +`secrets` context. It is declared as an action input only because composite actions do not support a +`secrets:` interface. ## Outputs @@ -43,11 +48,14 @@ permissions: | `region` | Resolved AWS region. | | `role-session-name` | Sanitized AWS role session name. | -The action does not output credentials or private key material. `aws-actions/configure-aws-credentials` exports temporary credentials into the current job environment; it does not create a persistent named AWS profile. +The action does not output credentials or private key material. +`aws-actions/configure-aws-credentials` exports temporary credentials into the current job +environment; it does not create a persistent named AWS profile. ## Usage with consumer checkout -Use the action before checking out the consumer repository. When later steps need consumer files, check out the consumer immediately afterward: +Use the action before checking out the consumer repository. When later steps need consumer files, +check out the consumer immediately afterward: ```yaml permissions: diff --git a/.github/actions/configure-github-ssh/CHANGELOG.md b/.github/actions/configure-github-ssh/CHANGELOG.md index cd5d273..4ae8a60 100644 --- a/.github/actions/configure-github-ssh/CHANGELOG.md +++ b/.github/actions/configure-github-ssh/CHANGELOG.md @@ -6,8 +6,13 @@ All notable changes to the **configure-github-ssh** action are documented in thi ### Added -- Install the `ssh-private-key` input and GitHub's published SSH host keys (ED25519, ECDSA and RSA, from `api.github.com/meta`) under `$RUNNER_TEMP/configure-github-ssh/`, as fresh `0600` files, so the key is removed when the runner cleans up after the job. -- Write `~/.ssh/config` with one `Host github.com` block that uses only that key and requires the pinned host keys, ignoring the runner's global `known_hosts`. -- Fail with an `Existing SSH config` error, before writing anything, when `~/.ssh/config` already exists with other content. The action's own config is accepted, so a job can run it more than once. +- Install the `ssh-private-key` input and GitHub's published SSH host keys (ED25519, ECDSA and RSA, + from `api.github.com/meta`) under `$RUNNER_TEMP/configure-github-ssh/`, as fresh `0600` files, so + the key is removed when the runner cleans up after the job. +- Write `~/.ssh/config` with one `Host github.com` block that uses only that key and requires the + pinned host keys, ignoring the runner's global `known_hosts`. +- Fail with an `Existing SSH config` error, before writing anything, when `~/.ssh/config` already + exists with other content. The action's own config is accepted, so a job can run it more than + once. - Remove the key, host keys and any config the action created when a step fails partway through. - Fail with a `Missing SSH key` error when `ssh-private-key` is empty or whitespace. diff --git a/.github/actions/configure-github-ssh/README.md b/.github/actions/configure-github-ssh/README.md index 29f1c6e..7413661 100644 --- a/.github/actions/configure-github-ssh/README.md +++ b/.github/actions/configure-github-ssh/README.md @@ -2,22 +2,39 @@ ## 🧭 Summary -Installs an SSH private key and GitHub's published host keys, so `git` and Terraform can fetch private github.com repositories over SSH, for example Terraform modules sourced from `git@github.com:OpenSesame/...`. - -It replaces the inline step that wrote the key and ran `ssh-keyscan github.com`. That step trusted whatever host answered at run time, so a spoofed github.com could serve code to a job holding the org's read-only key. - -- Writes the key and GitHub's host keys to `$RUNNER_TEMP/configure-github-ssh/`, as fresh files under `umask 077`, so each is readable only by the runner user. The runner empties `$RUNNER_TEMP` after every job, self-hosted runners included, so the key never outlives the job. -- Writes `~/.ssh/config` with one `Host github.com` block that uses only that key and requires the pinned host keys (`StrictHostKeyChecking yes`, `GlobalKnownHostsFile /dev/null`). The config holds no secret. On a reused runner it stays, pointing at files that no longer exist, and the next run accepts it. +Installs an SSH private key and GitHub's published host keys, so `git` and Terraform can fetch +private github.com repositories over SSH, for example Terraform modules sourced from +`git@github.com:OpenSesame/...`. + +It replaces the inline step that wrote the key and ran `ssh-keyscan github.com`. That step trusted +whatever host answered at run time, so a spoofed github.com could serve code to a job holding the +org's read-only key. + +- Writes the key and GitHub's host keys to `$RUNNER_TEMP/configure-github-ssh/`, as fresh files + under `umask 077`, so each is readable only by the runner user. The runner empties `$RUNNER_TEMP` + after every job, self-hosted runners included, so the key never outlives the job. +- Writes `~/.ssh/config` with one `Host github.com` block that uses only that key and requires the + pinned host keys (`StrictHostKeyChecking yes`, `GlobalKnownHostsFile /dev/null`). The config holds + no secret. On a reused runner it stays, pointing at files that no longer exist, and the next run + accepts it. - Removes the key, host keys and any config it created if a step fails partway through. -- Fails with an `Existing SSH config` error, before writing anything, when `~/.ssh/config` already exists with other content. Merging can't isolate github.com: an existing `IdentityFile` would still be offered to GitHub, and global settings would end up scoped to github.com. Running the action twice in a job is fine, because its own config is accepted. -- Fails with a `Missing SSH key` error when `ssh-private-key` is empty, for example when the caller didn't pass the secret. +- Fails with an `Existing SSH config` error, before writing anything, when `~/.ssh/config` already + exists with other content. Merging can't isolate github.com: an existing `IdentityFile` would + still be offered to GitHub, and global settings would end up scoped to github.com. Running the + action twice in a job is fine, because its own config is accepted. +- Fails with a `Missing SSH key` error when `ssh-private-key` is empty, for example when the caller + didn't pass the secret. ## Scope/Limitations -- Run it before any other SSH setup in the job, on a runner without an SSH config, such as a GitHub-hosted runner. +- Run it before any other SSH setup in the job, on a runner without an SSH config, such as a + GitHub-hosted runner. - Applies only to `github.com`. Other SSH hosts keep the runner's defaults. -- The host keys are the ones GitHub publishes at `https://api.github.com/meta` (`ssh_keys`), matching the fingerprints in GitHub's documentation. If GitHub rotates a key, SSH to github.com fails until this action is updated and released. -- Composite actions have no post step, so the key stays in `$RUNNER_TEMP` for the rest of the job, readable by any later step in it, until the runner empties that directory. +- The host keys are the ones GitHub publishes at `https://api.github.com/meta` (`ssh_keys`), + matching the fingerprints in GitHub's documentation. If GitHub rotates a key, SSH to github.com + fails until this action is updated and released. +- Composite actions have no post step, so the key stays in `$RUNNER_TEMP` for the rest of the job, + readable by any later step in it, until the runner empties that directory. ## 🔒 Permissions @@ -52,8 +69,10 @@ Replace `` with the full commit SHA of the tag: `git rev-list -n1 actions/c 1. Fetch the keys: `gh api meta --jq '.ssh_keys[]'`. 2. Check their fingerprints against [GitHub's SSH key fingerprints](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints). -3. Replace `GITHUB_KNOWN_HOSTS` in `action.yml` and the fingerprints in `action.unit.test.js`, then release a new version. +3. Replace `GITHUB_KNOWN_HOSTS` in `action.yml` and the fingerprints in `action.unit.test.js`, then + release a new version. ## Versioning -This action follows the repository's component versioning policy. The initial release uses the PR label `v:a/configure-github-ssh/1.0.0` and the namespaced tag `actions/configure-github-ssh/1.0.0`. +This action follows the repository's component versioning policy. The initial release uses the PR +label `v:a/configure-github-ssh/1.0.0` and the namespaced tag `actions/configure-github-ssh/1.0.0`. diff --git a/.github/actions/create-aws-role-session-name/README.md b/.github/actions/create-aws-role-session-name/README.md index 9b23557..6d1efc9 100644 --- a/.github/actions/create-aws-role-session-name/README.md +++ b/.github/actions/create-aws-role-session-name/README.md @@ -2,7 +2,8 @@ ## Summary -Builds a deterministic, AWS-compatible role session name from workflow context. This action only constructs and sanitizes the name; it does not configure AWS credentials. +Builds a deterministic, AWS-compatible role session name from workflow context. This action only +constructs and sanitizes the name; it does not configure AWS credentials. The unsanitized name has this form: @@ -28,11 +29,13 @@ The unsanitized name has this form: ## Sanitization contract - The allowed characters are `A-Z`, `a-z`, `0-9`, `_`, `+`, `=`, `,`, `.`, `@`, and `-`. -- Each invalid byte is replaced with `-`. For example, `renovate[bot]` becomes `renovate-bot-` within the complete session name. +- Each invalid byte is replaced with `-`. For example, `renovate[bot]` becomes `renovate-bot-` + within the complete session name. - The result is truncated to at most 64 characters. - The action fails if the result contains fewer than two characters. - Character processing uses the C locale for deterministic byte handling. -- Inputs are passed to the shell through environment variables and are never interpolated into shell source. +- Inputs are passed to the shell through environment variables and are never interpolated into shell + source. The output always matches `^[A-Za-z0-9_+=,.@-]{2,64}$`. @@ -61,4 +64,6 @@ This action does not require additional GitHub token permissions. ## Versioning -This action follows the repository's component versioning policy. The initial release uses the PR label `v:a/create-aws-role-session-name/1.0.0` and the namespaced tag `actions/create-aws-role-session-name/1.0.0`. +This action follows the repository's component versioning policy. The initial release uses the PR +label `v:a/create-aws-role-session-name/1.0.0` and the namespaced tag +`actions/create-aws-role-session-name/1.0.0`. diff --git a/.github/actions/pr-open-check/README.md b/.github/actions/pr-open-check/README.md index c2193d0..dae8e7f 100644 --- a/.github/actions/pr-open-check/README.md +++ b/.github/actions/pr-open-check/README.md @@ -3,7 +3,8 @@ ## 🧭 Summary Finds an open pull request associated with a given commit, typically the one triggering a workflow. -Used to determine whether a PR exists and to provide its number and URL for usage in later steps (e.g., posting summaries or comments). +Used to determine whether a PR exists and to provide its number and URL for usage in later steps +(e.g., posting summaries or comments). - Outputs are written to $GITHUB_OUTPUT so they can be consumed by subsequent steps. - Returns pr_exists=false when no open PR includes the given commit. @@ -22,8 +23,8 @@ branch. If two branches point at the same commit and only one has an open PR, a still reports that PR. Pass `head-ref: ${{ github.ref_name }}` when you use `pr_exists` to decide whether a push run is covered by a PR run. -The action reads one page of up to 100 PRs associated with the commit. If more than 100 PRs contain the -commit, a matching PR beyond the first 100 is not found. +The action reads one page of up to 100 PRs associated with the commit. If more than 100 PRs contain +the commit, a matching PR beyond the first 100 is not found. ## 🔒 Permissions @@ -111,7 +112,8 @@ run: echo "Found PR #${{ steps.pr_check.outputs.pr_number }}" ## 🧠 Notes -- Uses the GitHub API via `curl`, internally the step runs the following commands to detect an open pull request including the commit. +- Uses the GitHub API via `curl`, internally the step runs the following commands to detect an open + pull request including the commit. - It does rely on the `groot-preview` ```bash diff --git a/.github/actions/resolve-feature-workspace/CHANGELOG.md b/.github/actions/resolve-feature-workspace/CHANGELOG.md index 9b5dd2f..c85f020 100644 --- a/.github/actions/resolve-feature-workspace/CHANGELOG.md +++ b/.github/actions/resolve-feature-workspace/CHANGELOG.md @@ -6,5 +6,7 @@ All notable changes to the `resolve-feature-workspace` composite action are docu ### Added -- Resolve the feature-environment Terraform workspace name from a branch name: last `/` segment, lowercased, characters other than `a-z`, `0-9` and `-` replaced with `-`, repeated `-` collapsed, leading `-` trimmed, first 15 characters, trailing `-` trimmed. +- Resolve the feature-environment Terraform workspace name from a branch name: last `/` segment, + lowercased, characters other than `a-z`, `0-9` and `-` replaced with `-`, repeated `-` collapsed, + leading `-` trimmed, first 15 characters, trailing `-` trimmed. - Fail when the resolved name is empty or the branch name contains control characters. diff --git a/.github/actions/resolve-feature-workspace/README.md b/.github/actions/resolve-feature-workspace/README.md index cc12714..3579638 100644 --- a/.github/actions/resolve-feature-workspace/README.md +++ b/.github/actions/resolve-feature-workspace/README.md @@ -2,7 +2,9 @@ ## Summary -Derives a feature environment's Terraform workspace name from a branch name. The name uses only `a-z`, `0-9` and single hyphens. Use the same action in deploy and cleanup workflows so both always select the same workspace. It only computes names; it does not run Terraform. +Derives a feature environment's Terraform workspace name from a branch name. The name uses only +`a-z`, `0-9` and single hyphens. Use the same action in deploy and cleanup workflows so both always +select the same workspace. It only computes names; it does not run Terraform. ## Inputs @@ -20,7 +22,8 @@ Derives a feature environment's Terraform workspace name from a branch name. The 1. Keep the text after the last `/`. 2. Lowercase. -3. Replace each character other than `a-z`, `0-9` and `-` with `-` (including `_`, `.`, `+` and `[]`), then collapse repeated `-`. +3. Replace each character other than `a-z`, `0-9` and `-` with `-` (including `_`, `.`, `+` and + `[]`), then collapse repeated `-`. 4. Trim a leading `-`, keep the first 15 characters, then trim a trailing `-`. | `branch-name` | `workspace` | @@ -32,7 +35,8 @@ Derives a feature environment's Terraform workspace name from a branch name. The | `release/2026.10.21` | `2026-10-21` | | `dev` | `dev` | -The output starts and ends with a letter or digit. The action fails when the result is empty (for example `feature/` or `feature/---`) or when `branch-name` contains control characters. +The output starts and ends with a letter or digit. The action fails when the result is empty (for +example `feature/` or `feature/---`) or when `branch-name` contains control characters. ## Usage @@ -51,10 +55,15 @@ The output starts and ends with a letter or digit. The action fails when the res run: terraform workspace select -or-create "$TF_WORKSPACE_NAME" ``` -`github.head_ref` is set only for `pull_request` events. Pass `github.ref_name` on `push` and `github.event.ref` on `delete` (when `github.event.ref_type` is `branch`). +`github.head_ref` is set only for `pull_request` events. Pass `github.ref_name` on `push` and +`github.event.ref` on `delete` (when `github.event.ref_type` is `branch`). ## Migrating -Workspace names are case-sensitive, and this rule differs from the copied `awk | tr | cut | sed` pipeline: `feature/CORE-6135` moves from workspace `CORE-6135` to `core-6135`. Migrate deploy and cleanup together. Before migrating, compare each open feature environment's workspace with this action's output; if any differ, close those environments first, or cleanup can't find the old workspace and its resources are orphaned. +Workspace names are case-sensitive, and this rule differs from the copied `awk | tr | cut | sed` +pipeline: `feature/CORE-6135` moves from workspace `CORE-6135` to `core-6135`. Migrate deploy and +cleanup together. Before migrating, compare each open feature environment's workspace with this +action's output; if any differ, close those environments first, or cleanup can't find the old +workspace and its resources are orphaned. This action does not require additional GitHub token permissions. diff --git a/.github/actions/run-semgrep/CHANGELOG.md b/.github/actions/run-semgrep/CHANGELOG.md index 2087a11..3592bff 100644 --- a/.github/actions/run-semgrep/CHANGELOG.md +++ b/.github/actions/run-semgrep/CHANGELOG.md @@ -24,7 +24,8 @@ All notable changes to the run-semgrep GitHub Action will be documented in this ### Changed -- Run the action directly on the Node.js 24 action runtime while preserving its environment and output contracts +- Run the action directly on the Node.js 24 action runtime while preserving its environment and + output contracts ### Fixed diff --git a/.github/actions/upsert-pr-comment/CHANGELOG.md b/.github/actions/upsert-pr-comment/CHANGELOG.md index b236d68..7d5a09a 100644 --- a/.github/actions/upsert-pr-comment/CHANGELOG.md +++ b/.github/actions/upsert-pr-comment/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to the **upsert-pr-comment** action are documented in this f ### Changed -- Bumped `actions/github-script` from `v7` (Node 20, deprecated runtime) to `v9.0.0` (Node 24), pinned by commit SHA with a version comment (CORE-5974). +- Bumped `actions/github-script` from `v7` (Node 20, deprecated runtime) to `v9.0.0` (Node 24), + pinned by commit SHA with a version comment (CORE-5974). ## 1.0.0 diff --git a/.github/actions/upsert-pr-comment/README.md b/.github/actions/upsert-pr-comment/README.md index 35f438c..9efcb1b 100644 --- a/.github/actions/upsert-pr-comment/README.md +++ b/.github/actions/upsert-pr-comment/README.md @@ -2,7 +2,9 @@ ## 🧭 Summary -Creates or updates a comment on a GitHub Pull Request. The comments are identified by a unique hidden marker. This ensures only one comment per marker is present, updating the comment if one already exists for the marker or creating a new one if not. +Creates or updates a comment on a GitHub Pull Request. The comments are identified by a unique +hidden marker. This ensures only one comment per marker is present, updating the comment if one +already exists for the marker or creating a new one if not. ## Scope/Limitations @@ -58,7 +60,8 @@ Basic usage example: ## 🧠 Notes -- The comment marker is embedded as an HTML comment and should be unique for each comment type you want to upsert. +- The comment marker is embedded as an HTML comment and should be unique for each comment type you + want to upsert. - If multiple workflows use the same marker, they will overwrite each other's comments. - The action uses the GitHub REST API to list, update, or create comments. diff --git a/.github/workflows/CHANGELOGS/deploy_environment.md b/.github/workflows/CHANGELOGS/deploy_environment.md index 1db91f7..4615cc8 100644 --- a/.github/workflows/CHANGELOGS/deploy_environment.md +++ b/.github/workflows/CHANGELOGS/deploy_environment.md @@ -2,6 +2,14 @@ All notable changes to the **deploy_environment** reusable workflow are documented in this file. +## 0.4.2 + +### Changed + +- The workflow sets `permissions: {}` and grants `id-token: write` and `contents: read` to the + `tf_apply` job. Callers grant the same permissions as before. +- Composes `tf_apply` 0.3.2, which also grants permissions per job. + ## 0.4.1 ### Changed diff --git a/.github/workflows/CHANGELOGS/deploy_thru_prod.md b/.github/workflows/CHANGELOGS/deploy_thru_prod.md index 7dcfa01..48fd1f8 100644 --- a/.github/workflows/CHANGELOGS/deploy_thru_prod.md +++ b/.github/workflows/CHANGELOGS/deploy_thru_prod.md @@ -2,6 +2,15 @@ All notable changes to the **deploy_thru_prod** reusable workflow are documented in this file. +## 0.4.2 + +### Changed + +- The workflow sets `permissions: {}`, and each job requests only what it uses: `id-token: write` + and `contents: read` for the deploy jobs, `contents: write` for the release job, and + `pull-requests: write` for the status comment. Callers grant the same permissions as before. +- Composes `deploy_environment` 0.4.2. + ## 0.4.1 ### Changed diff --git a/.github/workflows/CHANGELOGS/run_gha_checks.md b/.github/workflows/CHANGELOGS/run_gha_checks.md index 2a1de84..4355f9a 100644 --- a/.github/workflows/CHANGELOGS/run_gha_checks.md +++ b/.github/workflows/CHANGELOGS/run_gha_checks.md @@ -2,6 +2,15 @@ All notable changes to the **run_gha_checks** reusable workflow are documented in this file. +## 1.0.4 + +### Fixed + +- actionlint and zizmor findings are annotated on their file and line again, as `run_infra_ci` + did. Lint passes `-format` with GitHub `::error` commands to `lint:gha`, and Scan passes + `--format=github` to `scan:gha`. Each task must end with the actionlint or zizmor command so + mise appends the arguments to it. + ## 1.0.3 ### Fixed @@ -32,5 +41,6 @@ All notable changes to the **run_gha_checks** reusable workflow are documented i - First release, extracted from the Repo Checks trial in identity-infrastructure-template (`on_call_repo_checks.yml`), with an optional `commit-identifier` input. - One job, `checks` ("Checks"), that runs the `lint:gha`, `scan:gha` mise tasks with `mise run`, - without Node or `npm ci`. Each step runs after earlier failures once mise set up. A missing task fails its own step. + without Node or `npm ci`. Each step runs after earlier failures once mise set up. A missing task + fails its own step. - Outputs `lint`, `scan` and `baselined` (inline `# zizmor: ignore[...]` comments under `.github`). diff --git a/.github/workflows/CHANGELOGS/run_js_checks.md b/.github/workflows/CHANGELOGS/run_js_checks.md index 7847335..a70cefb 100644 --- a/.github/workflows/CHANGELOGS/run_js_checks.md +++ b/.github/workflows/CHANGELOGS/run_js_checks.md @@ -22,8 +22,9 @@ All notable changes to the **run_js_checks** reusable workflow are documented in - First release, extracted from the Repo Checks trial in identity-infrastructure-template (`on_call_repo_checks.yml`), with an optional `commit-identifier` input. -- One job, `checks` ("Checks"), that runs the `lint:js`, `format:prettier` package.json scripts with `npm run`. Each step runs after earlier failures once `npm ci` succeeded, and a - missing script fails its own step. +- One job, `checks` ("Checks"), that runs the `lint:js`, `format:prettier` package.json scripts with + `npm run`. Each step runs after earlier failures once `npm ci` succeeded, and a missing script + fails its own step. - An optional `ORG_GITHUB_PACKAGES_READ_ONLY_TOKEN` secret, so `npm ci` can install private `@opensesame` packages from GitHub Packages. - Outputs `lint`, `format` and `baselined` (inline `eslint-disable` and `prettier-ignore` comments). diff --git a/.github/workflows/CHANGELOGS/run_markdown_checks.md b/.github/workflows/CHANGELOGS/run_markdown_checks.md index b7703e4..bf8526c 100644 --- a/.github/workflows/CHANGELOGS/run_markdown_checks.md +++ b/.github/workflows/CHANGELOGS/run_markdown_checks.md @@ -22,8 +22,9 @@ All notable changes to the **run_markdown_checks** reusable workflow are documen - First release, extracted from the Repo Checks trial in identity-infrastructure-template (`on_call_repo_checks.yml`), with an optional `commit-identifier` input. -- One job, `checks` ("Checks"), that runs the `lint:markdown` package.json script with `npm run`. Each step runs after earlier failures once `npm ci` succeeded, and a - missing script fails its own step. +- One job, `checks` ("Checks"), that runs the `lint:markdown` package.json script with `npm run`. + Each step runs after earlier failures once `npm ci` succeeded, and a missing script fails its own + step. - An optional `ORG_GITHUB_PACKAGES_READ_ONLY_TOKEN` secret, so `npm ci` can install private `@opensesame` packages from GitHub Packages. - Outputs `lint` and `baselined` (inline `markdownlint-disable` comments in `*.md` files). diff --git a/.github/workflows/CHANGELOGS/run_npm_audit.md b/.github/workflows/CHANGELOGS/run_npm_audit.md index d2c27d2..4009fa8 100644 --- a/.github/workflows/CHANGELOGS/run_npm_audit.md +++ b/.github/workflows/CHANGELOGS/run_npm_audit.md @@ -14,9 +14,9 @@ All notable changes to the **run_npm_audit** reusable workflow are documented in - First release of the `run_npm_audit` reusable workflow. - One job, `audit` ("Audit"), that runs - `npm audit --omit=dev --omit=optional --audit-level=` against the public npm registry in - each directory listed in `working-directories` and fails if any directory has a production advisory - at or above `audit-level`, has no `package-lock.json`, or returns an audit error. + `npm audit --omit=dev --omit=optional --audit-level=` against the public npm registry + in each directory listed in `working-directories` and fails if any directory has a production + advisory at or above `audit-level`, has no `package-lock.json`, or returns an audit error. - Inputs `commit-identifier`, `working-directories` (default `.`), `audit-level` (default `high`) and `node-version-file` (default `.nvmrc`). - Job summary table of advisory counts per directory, plus the failing advisories for each failed diff --git a/.github/workflows/CHANGELOGS/run_repo_checks.md b/.github/workflows/CHANGELOGS/run_repo_checks.md index 3e95d4a..57c43c4 100644 --- a/.github/workflows/CHANGELOGS/run_repo_checks.md +++ b/.github/workflows/CHANGELOGS/run_repo_checks.md @@ -2,6 +2,13 @@ All notable changes to the **run_repo_checks** reusable workflow are documented in this file. +## 1.0.4 + +### Fixed + +- Calls `run_gha_checks` 1.0.4 from the same commit, which annotates actionlint and zizmor + findings on their file and line. + ## 1.0.3 ### Fixed @@ -24,8 +31,8 @@ All notable changes to the **run_repo_checks** reusable workflow are documented - Calls the 1.0.1 per-technology workflows and `run_repo_validation` 1.0.1 from the same commit, which bump `jdx/mise-action` to 5.2.5 and `actions/setup-node` to 7.1.0. -- Its `run_repo_validation` 1.0.1 sets up SSH with `configure-github-ssh` 1.0.0, which pins GitHub's host - keys, for the latest-release lookup. +- Its `run_repo_validation` 1.0.1 sets up SSH with `configure-github-ssh` 1.0.0, which pins GitHub's + host keys, for the latest-release lookup. ## 1.0.0 @@ -43,5 +50,5 @@ All notable changes to the **run_repo_checks** reusable workflow are documented skipped when Detect finds none of its files. - `standard` ("Standard") calls `run_repo_validation` 1.0.0 from the same commit with only the `ORG_READ_ONLY_SSH_KEY` secret. -- `summary` ("Summary") writes a table of files, init, validate, lint, format and scan results, and baselined counts - per technology, plus the Standard result, even after failures. +- `summary` ("Summary") writes a table of files, init, validate, lint, format and scan results, and + baselined counts per technology, plus the Standard result, even after failures. diff --git a/.github/workflows/CHANGELOGS/run_semgrep_scan.md b/.github/workflows/CHANGELOGS/run_semgrep_scan.md index 707b8c8..72d9a3f 100644 --- a/.github/workflows/CHANGELOGS/run_semgrep_scan.md +++ b/.github/workflows/CHANGELOGS/run_semgrep_scan.md @@ -87,13 +87,16 @@ All notable changes to the **run_semgrep_scan** callable workflow are documented ### Changed -- Pinned `actions/checkout`, `actions/setup-node`, `actions/upload-artifact`, `reviewdog/action-setup`, and `actions/github-script` to Node 24-compatible releases by commit SHA (with version comments), addressing the GitHub Node 20 Actions runtime deprecation (CORE-5974). +- Pinned `actions/checkout`, `actions/setup-node`, `actions/upload-artifact`, + `reviewdog/action-setup`, and `actions/github-script` to Node 24-compatible releases by commit SHA + (with version comments), addressing the GitHub Node 20 Actions runtime deprecation (CORE-5974). ## 1.0.1 ### Changed -- Updated workflow to support cross-repository usage by repo qualifying the internal composite action calls. +- Updated workflow to support cross-repository usage by repo qualifying the internal composite + action calls. ## 1.0.0 diff --git a/.github/workflows/CHANGELOGS/run_shell_checks.md b/.github/workflows/CHANGELOGS/run_shell_checks.md index f089643..265b7d9 100644 --- a/.github/workflows/CHANGELOGS/run_shell_checks.md +++ b/.github/workflows/CHANGELOGS/run_shell_checks.md @@ -32,5 +32,6 @@ All notable changes to the **run_shell_checks** reusable workflow are documented - First release, extracted from the Repo Checks trial in identity-infrastructure-template (`on_call_repo_checks.yml`), with an optional `commit-identifier` input. - One job, `checks` ("Checks"), that runs the `lint:shell`, `format:shell` mise tasks with `mise run`, - without Node or `npm ci`. Each step runs after earlier failures once mise set up. A missing task fails its own step. + without Node or `npm ci`. Each step runs after earlier failures once mise set up. A missing task + fails its own step. - Outputs `lint`, `format` and `baselined` (inline `# shellcheck disable=` directives). diff --git a/.github/workflows/CHANGELOGS/run_sonar_scan.md b/.github/workflows/CHANGELOGS/run_sonar_scan.md index a6ca25a..a613ca5 100644 --- a/.github/workflows/CHANGELOGS/run_sonar_scan.md +++ b/.github/workflows/CHANGELOGS/run_sonar_scan.md @@ -42,7 +42,8 @@ All notable changes to the **run_sonar_scan** callable workflow are documented i ### Added -- Added optional `coverage-artifact-name` and `coverage-artifact-path` inputs to download a same-run coverage artifact before the SonarQube scan. +- Added optional `coverage-artifact-name` and `coverage-artifact-path` inputs to download a same-run + coverage artifact before the SonarQube scan. ## 1.2.0 @@ -62,5 +63,6 @@ All notable changes to the **run_sonar_scan** callable workflow are documented i ### Added - First release of the `run_sonar_scan` reusable workflow. -- Defaults to scanning the ref that triggered the caller's workflow and optionally accepts a commit SHA, tag, or branch. +- Defaults to scanning the ref that triggered the caller's workflow and optionally accepts a commit + SHA, tag, or branch. - Runs the SonarQube scan with full Git history and an explicitly mapped `SONAR_TOKEN` secret. diff --git a/.github/workflows/CHANGELOGS/run_terraform_checks.md b/.github/workflows/CHANGELOGS/run_terraform_checks.md index c762372..09b6b47 100644 --- a/.github/workflows/CHANGELOGS/run_terraform_checks.md +++ b/.github/workflows/CHANGELOGS/run_terraform_checks.md @@ -37,4 +37,6 @@ All notable changes to the **run_terraform_checks** reusable workflow are docume which needs Initialize to have succeeded. A missing task fails its own step. - `init:terraform` and `validate:terraform` must work without secrets or cloud credentials: `-backend=false`, and module sources that download without credentials. -- Outputs `init`, `validate`, `lint`, `format`, `scan` and `baselined` (findings in tracked `*.checkov.baseline` files, plus inline `#checkov:skip=` and `# tflint-ignore:` comments in `*.tf` files). +- Outputs `init`, `validate`, `lint`, `format`, `scan` and `baselined` (findings in tracked + `*.checkov.baseline` files, plus inline `#checkov:skip=` and `# tflint-ignore:` comments in `*.tf` + files). diff --git a/.github/workflows/CHANGELOGS/tf_apply.md b/.github/workflows/CHANGELOGS/tf_apply.md index ccff364..dad6093 100644 --- a/.github/workflows/CHANGELOGS/tf_apply.md +++ b/.github/workflows/CHANGELOGS/tf_apply.md @@ -2,6 +2,14 @@ All notable changes to the **tf_apply** reusable workflow are documented in this file. +## 0.3.2 + +### Changed + +- The workflow sets `permissions: {}`, and each job requests its own: `id-token: write` and + `contents: read` for the apply job, none for the summary job. Callers grant the same permissions + as before. + ## 0.3.1 ### Changed diff --git a/.github/workflows/CHANGELOGS/tf_plan_env_roots.md b/.github/workflows/CHANGELOGS/tf_plan_env_roots.md index 93b587b..9e2c55b 100644 --- a/.github/workflows/CHANGELOGS/tf_plan_env_roots.md +++ b/.github/workflows/CHANGELOGS/tf_plan_env_roots.md @@ -21,8 +21,8 @@ All notable changes to the **tf_plan_env_roots** reusable workflow are documente - Composes `tf_plan_single_root` 1.0.1 from the same commit, which bumps `actions/upload-artifact` to 7.0.2. -- Its `tf_plan_single_root` 1.0.1 sets up SSH with `configure-github-ssh` 1.0.0, which pins GitHub's host - keys, instead of running `ssh-keyscan`. +- Its `tf_plan_single_root` 1.0.1 sets up SSH with `configure-github-ssh` 1.0.0, which pins GitHub's + host keys, instead of running `ssh-keyscan`. ## 1.0.0 diff --git a/.github/workflows/CHANGELOGS/tf_validate_plan_env_roots.md b/.github/workflows/CHANGELOGS/tf_validate_plan_env_roots.md index 753e7cc..d499b6e 100644 --- a/.github/workflows/CHANGELOGS/tf_validate_plan_env_roots.md +++ b/.github/workflows/CHANGELOGS/tf_validate_plan_env_roots.md @@ -10,7 +10,7 @@ file. ### Changed - **Breaking:** composes `tf_validate_plan_single_root` 1.0.0, and the matrix job is renamed from - `TF-Validate-Plan-Roots` ("TF Validate/Plan ENV Roots (, false)") to `environment`, + `TF-Validate-Plan-Roots` (`TF Validate/Plan ENV Roots (, false)`) to `environment`, named after its environment. With a caller job named `Terraform`, checks read `Terraform / / Validate Plan`. Required status checks that use the old names must be updated. diff --git a/.github/workflows/CHANGELOGS/tf_validate_plan_single_root.md b/.github/workflows/CHANGELOGS/tf_validate_plan_single_root.md index 93bea52..cb069fe 100644 --- a/.github/workflows/CHANGELOGS/tf_validate_plan_single_root.md +++ b/.github/workflows/CHANGELOGS/tf_validate_plan_single_root.md @@ -9,9 +9,10 @@ this file. ### Changed -- **Breaking:** the job is renamed from `TF-Validate-Plan` ("Terraform Validate and Plan - ") to `validate-plan` ("Validate Plan"), so the check reads - ` / Validate Plan`. Required status checks that use the old name must be updated. +- **Breaking:** the job is renamed from `TF-Validate-Plan` + (`Terraform Validate and Plan `) to `validate-plan` ("Validate Plan"), so the check + reads ` / Validate Plan`. Required status checks that use the old name must be + updated. - **Breaking:** the plan artifact is named `terraform-plan-` instead of `-tfplan-`. Read the name from the `plan-artifact-name` output. - **Breaking:** workflow-level permissions are `{}` and the job requests `id-token: write` and diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 2e6b7f1..b92cf6e 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -1,14 +1,17 @@ # GitHub Workflows Directory -This directory contains externally reusable and internal, project-specific GitHub Actions workflows for this repository. +This directory contains externally reusable and internal, project-specific GitHub Actions workflows +for this repository. ## Internal Workflows -Internal workflows are used by this repository for the pipeline of its products. They must never expose `workflow_call` and must be prefixed with `internal_` +Internal workflows are used by this repository for the pipeline of its products. They must never +expose `workflow_call` and must be prefixed with `internal_` ## Reusable Workflows -This repository exposes externally reusable workflows, those that expose `workflow_call`. These workflows are treated as **products** of this repo. +This repository exposes externally reusable workflows, those that expose `workflow_call`. These +workflows are treated as **products** of this repo. ### Requirements diff --git a/.github/workflows/READMES/deploy_environment.md b/.github/workflows/READMES/deploy_environment.md index e595c33..a9dc03a 100644 --- a/.github/workflows/READMES/deploy_environment.md +++ b/.github/workflows/READMES/deploy_environment.md @@ -11,12 +11,12 @@ for example with `tf_plan_single_root`. ## Pairing plan and apply -This version composes `tf_apply` 0.3.1, whose `terraform init` installs the provider versions in each root's committed -`.terraform.lock.hcl`. Plan in CI with [`tf_plan_single_root`](tf_plan_single_root.md) or -[`tf_plan_env_roots`](tf_plan_env_roots.md), which do the same. The deprecated `tf_validate_plan_*` -workflows still run `terraform init -upgrade`, so with them CI can plan with newer providers than -this apply uses, and the apply may not match the reviewed plan. Move plan and apply to the -lock-file versions together. +This version composes `tf_apply` 0.3.1, whose `terraform init` installs the provider versions in +each root's committed `.terraform.lock.hcl`. Plan in CI with +[`tf_plan_single_root`](tf_plan_single_root.md) or [`tf_plan_env_roots`](tf_plan_env_roots.md), +which do the same. The deprecated `tf_validate_plan_*` workflows still run +`terraform init -upgrade`, so with them CI can plan with newer providers than this apply uses, and +the apply may not match the reviewed plan. Move plan and apply to the lock-file versions together. Even when paired, the apply doesn't run the CI plan. `terraform apply` makes a new plan at deploy time, with `TF_VAR_release_name` and `TF_VAR_IACDeploymentRef` set, which `tf_plan_*` don't set. @@ -37,7 +37,7 @@ apply differ from what CI showed. ```yaml jobs: deploy-dev: - uses: OpenSesame/core-github-actions/.github/workflows/deploy_environment.yml@workflows/deploy_environment/0.4.1 + uses: OpenSesame/core-github-actions/.github/workflows/deploy_environment.yml@workflows/deploy_environment/0.4.2 with: environment: dev commit-identifier: ${{ github.sha }} @@ -75,8 +75,8 @@ jobs: 1. Calls `tf_apply` with the target ref, environment, workspace, root, Terraform version, and release tag. -Runs for the same repository and environment share a concurrency group. The workflow declares -`id-token: write` and `contents: read` permissions for its called workflows. +Runs for the same repository and environment share a concurrency group. The workflow sets +`permissions: {}` and grants `id-token: write` and `contents: read` to the `tf_apply` job. The composed apply job runs on Ubuntu 26.04. ## Contribution diff --git a/.github/workflows/READMES/deploy_thru_prod.md b/.github/workflows/READMES/deploy_thru_prod.md index d83c84b..54ae9e2 100644 --- a/.github/workflows/READMES/deploy_thru_prod.md +++ b/.github/workflows/READMES/deploy_thru_prod.md @@ -10,12 +10,12 @@ GitHub release, and posts the final stage, prod, and release status to the assoc ## Pairing plan and apply -This version composes `deploy_environment` 0.4.1 and `tf_apply` 0.3.1, whose `terraform init` installs the provider versions in each root's committed -`.terraform.lock.hcl`. Plan in CI with [`tf_plan_single_root`](tf_plan_single_root.md) or -[`tf_plan_env_roots`](tf_plan_env_roots.md), which do the same. The deprecated `tf_validate_plan_*` -workflows still run `terraform init -upgrade`, so with them CI can plan with newer providers than -this apply uses, and the apply may not match the reviewed plan. Move plan and apply to the -lock-file versions together. +This version composes `deploy_environment` 0.4.1 and `tf_apply` 0.3.1, whose `terraform init` +installs the provider versions in each root's committed `.terraform.lock.hcl`. Plan in CI with +[`tf_plan_single_root`](tf_plan_single_root.md) or [`tf_plan_env_roots`](tf_plan_env_roots.md), +which do the same. The deprecated `tf_validate_plan_*` workflows still run +`terraform init -upgrade`, so with them CI can plan with newer providers than this apply uses, and +the apply may not match the reviewed plan. Move plan and apply to the lock-file versions together. Even when paired, the apply doesn't run the CI plan. `terraform apply` makes a new plan at deploy time, with `TF_VAR_release_name` and `TF_VAR_IACDeploymentRef` set, which `tf_plan_*` don't set. @@ -36,7 +36,7 @@ apply differ from what CI showed. ```yaml jobs: deploy-through-prod: - uses: OpenSesame/core-github-actions/.github/workflows/deploy_thru_prod.yml@workflows/deploy_thru_prod/0.4.1 + uses: OpenSesame/core-github-actions/.github/workflows/deploy_thru_prod.yml@workflows/deploy_thru_prod/0.4.2 with: commit-identifier: ${{ github.sha }} oidc-domain: core @@ -79,7 +79,10 @@ The release-tag job only runs when the caller's event is a merged pull request o `workflow_dispatch`. Other caller event contexts cause the deployment chain to be skipped. Runs for the same repository share one concurrency group. -The workflow requests `id-token: write`, `contents: write`, and `pull-requests: write` permissions. +The workflow sets `permissions: {}`, so callers still grant `id-token: write`, `contents: write`, +and `pull-requests: write`, and each job requests only what it uses: the deploy jobs +`id-token: write` and `contents: read`, the release job `contents: write`, and the status job +`pull-requests: write`. Its direct and composed jobs run on Ubuntu 26.04. ## Contribution diff --git a/.github/workflows/READMES/resolve_ci_context.md b/.github/workflows/READMES/resolve_ci_context.md index 8d40ae6..17eee1d 100644 --- a/.github/workflows/READMES/resolve_ci_context.md +++ b/.github/workflows/READMES/resolve_ci_context.md @@ -1,8 +1,8 @@ # Resolve CI Context Decides which commit CI runs against and whether to run CI, so a CI workflow can trigger on both -`push` and `pull_request` without running twice. A push to a branch with an open PR is skipped (the PR -run covers it); every other run executes CI. +`push` and `pull_request` without running twice. A push to a branch with an open PR is skipped (the +PR run covers it); every other run executes CI. ## Usage diff --git a/.github/workflows/READMES/run_gha_checks.md b/.github/workflows/READMES/run_gha_checks.md index 09a3a46..af92463 100644 --- a/.github/workflows/READMES/run_gha_checks.md +++ b/.github/workflows/READMES/run_gha_checks.md @@ -13,7 +13,7 @@ jobs: name: GHA needs: ci-context if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} - uses: OpenSesame/core-github-actions/.github/workflows/run_gha_checks.yml@ # workflows/run_gha_checks/1.0.3 + uses: OpenSesame/core-github-actions/.github/workflows/run_gha_checks.yml@ # workflows/run_gha_checks/1.0.4 permissions: contents: read # Required to check out the code to check. with: @@ -30,10 +30,14 @@ The check is reported as `GHA / Checks`. The job sets up mise 2026.10.25, which installs the tools pinned in the repo's `mise.toml`, then runs each mise task with `mise run`. It doesn't need Node, `package.json` or `npm ci`: -| mise task | Step | Typical command | -| ---------- | ---- | -------------------------- | -| `lint:gha` | Lint | actionlint | -| `scan:gha` | Scan | `zizmor --offline .github` | +| mise task | Step | Typical command | Arguments the step adds | +| ---------- | ---- | -------------------------- | ---------------------------------------- | +| `lint:gha` | Lint | actionlint | `-format` with GitHub `::error` commands | +| `scan:gha` | Scan | `zizmor --offline .github` | `--format=github` | + +Each step passes those arguments after `--`, so each finding is annotated on its file and line. mise +appends them to the task's `run` command, so the task must end with the actionlint or zizmor +command, as the typical commands do. Run locally, the tasks keep their usual output. - Every task step runs even when an earlier one failed, as long as mise set up. - A missing task fails its own step, and mise logs that the task doesn't exist. diff --git a/.github/workflows/READMES/run_infra_ci.md b/.github/workflows/READMES/run_infra_ci.md index 870fc84..532a318 100644 --- a/.github/workflows/READMES/run_infra_ci.md +++ b/.github/workflows/READMES/run_infra_ci.md @@ -54,8 +54,8 @@ needs. 4. npm repos: add `"lint:gha": "mise run lint:gha"` and `"scan:gha": "mise run scan:gha"`. CI calls specific scripts, never the `lint`/`scan` aggregates, so CI jobs don't need mise. -Run locally with `mise run infra-ci`. zizmor runs `--offline` locally and in CI: its online audits need a -token that can read the org's private action repos. +Run locally with `mise run infra-ci`. zizmor runs `--offline` locally and in CI: its online audits +need a token that can read the org's private action repos. ## Findings diff --git a/.github/workflows/READMES/run_js_checks.md b/.github/workflows/READMES/run_js_checks.md index fc1775e..6a2d084 100644 --- a/.github/workflows/READMES/run_js_checks.md +++ b/.github/workflows/READMES/run_js_checks.md @@ -1,6 +1,9 @@ # Run JS Checks -Lints and format-checks JavaScript and TypeScript with the repo's `lint:js` and `format:prettier` scripts. It doesn't look for JavaScript or TypeScript files itself, so call it only when the repository has them. [`run_repo_checks`](run_repo_checks.md) does that, and most repositories should call it instead. +Lints and format-checks JavaScript and TypeScript with the repo's `lint:js` and `format:prettier` +scripts. It doesn't look for JavaScript or TypeScript files itself, so call it only when the +repository has them. [`run_repo_checks`](run_repo_checks.md) does that, and most repositories should +call it instead. ## Usage diff --git a/.github/workflows/READMES/run_markdown_checks.md b/.github/workflows/READMES/run_markdown_checks.md index 5698720..0a65d0e 100644 --- a/.github/workflows/READMES/run_markdown_checks.md +++ b/.github/workflows/READMES/run_markdown_checks.md @@ -1,6 +1,8 @@ # Run Markdown Checks -Lints Markdown with the repo's `lint:markdown` script. It doesn't look for Markdown files itself, so call it only when the repository has them. [`run_repo_checks`](run_repo_checks.md) does that, and most repositories should call it instead. +Lints Markdown with the repo's `lint:markdown` script. It doesn't look for Markdown files itself, so +call it only when the repository has them. [`run_repo_checks`](run_repo_checks.md) does that, and +most repositories should call it instead. ## Usage diff --git a/.github/workflows/READMES/run_npm_publish.md b/.github/workflows/READMES/run_npm_publish.md index 9fa88ae..f59d338 100644 --- a/.github/workflows/READMES/run_npm_publish.md +++ b/.github/workflows/READMES/run_npm_publish.md @@ -9,8 +9,8 @@ Only plain `x.y.z` tags count; `v`-prefixed tags are ignored. A repository that tags should add an `x.y.z` tag on its last released commit before the first run, so released markers don't count again. -- `prerelease: false`, for merged pull requests or pushes to `main`: publishes `latest` and creates a - GitHub release. A merged pull request publishes its merge commit. +- `prerelease: false`, for merged pull requests or pushes to `main`: publishes `latest` and creates + a GitHub release. A merged pull request publishes its merge commit. - `prerelease: true`, for other branches: publishes a beta with the `experimental` dist-tag and creates a beta tag. diff --git a/.github/workflows/READMES/run_repo_checks.md b/.github/workflows/READMES/run_repo_checks.md index ac78813..47fc3fa 100644 --- a/.github/workflows/READMES/run_repo_checks.md +++ b/.github/workflows/READMES/run_repo_checks.md @@ -15,7 +15,7 @@ jobs: name: Repo Checks needs: ci-context if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} - uses: OpenSesame/core-github-actions/.github/workflows/run_repo_checks.yml@ # workflows/run_repo_checks/1.0.3 + uses: OpenSesame/core-github-actions/.github/workflows/run_repo_checks.yml@ # workflows/run_repo_checks/1.0.4 permissions: contents: read # Required to check out the code to lint, format-check, scan and validate. with: @@ -26,7 +26,7 @@ jobs: ``` Add `repo-checks` to `ci-result`'s needs. Replace `` with the full commit SHA of the -tag: `git rev-list -n1 workflows/run_repo_checks/1.0.3`. +tag: `git rev-list -n1 workflows/run_repo_checks/1.0.4`. ## Jobs diff --git a/.github/workflows/READMES/run_semgrep_scan.md b/.github/workflows/READMES/run_semgrep_scan.md index 2b40ea9..74c96aa 100644 --- a/.github/workflows/READMES/run_semgrep_scan.md +++ b/.github/workflows/READMES/run_semgrep_scan.md @@ -22,8 +22,8 @@ jobs: ``` This runs a full scan on the default branch and a diff scan elsewhere. The check is reported as -`Semgrep / Scan`. Pair it with [`resolve_ci_context`](resolve_ci_context.md) so pushes covered by a PR -run are skipped. +`Semgrep / Scan`. Pair it with [`resolve_ci_context`](resolve_ci_context.md) so pushes covered by a +PR run are skipped. ## Inputs diff --git a/.github/workflows/READMES/run_shell_checks.md b/.github/workflows/READMES/run_shell_checks.md index 9ef4f71..8f86ff4 100644 --- a/.github/workflows/READMES/run_shell_checks.md +++ b/.github/workflows/READMES/run_shell_checks.md @@ -36,8 +36,9 @@ each mise task with `mise run`. It doesn't need Node, `package.json` or `npm ci` - Every task step runs even when an earlier one failed, as long as mise set up. - A missing task fails its own step, and mise logs that the task doesn't exist. -- Keep a matching `package.json` script for each task, such as `"lint:shell": "mise run lint:shell"`, so - `npm run check` still runs them locally. CI doesn't use those scripts. +- Keep a matching `package.json` script for each task, such as + `"lint:shell": "mise run lint:shell"`, so `npm run check` still runs them locally. CI doesn't use + those scripts. - The job fails if any step failed. ## Inputs diff --git a/.github/workflows/READMES/run_terraform_checks.md b/.github/workflows/READMES/run_terraform_checks.md index f9f46a8..b343bff 100644 --- a/.github/workflows/READMES/run_terraform_checks.md +++ b/.github/workflows/READMES/run_terraform_checks.md @@ -46,8 +46,9 @@ each mise task with `mise run`. It doesn't need Node, `package.json` or `npm ci` This check only protects a deploy that waits for it. `tf_plan_single_root` and `tf_plan_env_roots` also init with `-lockfile=readonly`, so a plan can't select its own provider versions. `tf_apply` doesn't, so gate deploys on a passing CI run for the same commit. -- Keep a matching `package.json` script for each task, such as `"lint:terraform": "mise run lint:terraform"`, so - `npm run check` still runs them locally. CI doesn't use those scripts. +- Keep a matching `package.json` script for each task, such as + `"lint:terraform": "mise run lint:terraform"`, so `npm run check` still runs them locally. CI + doesn't use those scripts. - The job has no secrets or cloud credentials, so the `init:terraform` and `validate:terraform` tasks must work without them: initialize with `-backend=false`, and use module sources that download without credentials, such as public registries or public git. Roots with private git, S3 or other diff --git a/.github/workflows/READMES/tf_apply.md b/.github/workflows/READMES/tf_apply.md index 2525d59..5807b23 100644 --- a/.github/workflows/READMES/tf_apply.md +++ b/.github/workflows/READMES/tf_apply.md @@ -36,7 +36,7 @@ apply differ from what CI showed. ```yaml jobs: terraform-apply: - uses: OpenSesame/core-github-actions/.github/workflows/tf_apply.yml@workflows/tf_apply/0.3.1 + uses: OpenSesame/core-github-actions/.github/workflows/tf_apply.yml@workflows/tf_apply/0.3.2 with: environment: dev oidc-domain: core @@ -84,7 +84,8 @@ jobs: The apply job sets `TF_VAR_IACDeploymentRef` to the current Actions run URL and `TF_VAR_release_name` to `release-tag`. Runs for the same repository and environment share a -concurrency group. The workflow requests `id-token: write` and `contents: read` permissions. +concurrency group. The workflow sets `permissions: {}`; the apply job requests `id-token: write` +and `contents: read`, and the summary job requests none. Both workflow jobs run on Ubuntu 26.04. ## Contribution diff --git a/.github/workflows/READMES/tf_plan_single_root.md b/.github/workflows/READMES/tf_plan_single_root.md index c0f4e53..7aab901 100644 --- a/.github/workflows/READMES/tf_plan_single_root.md +++ b/.github/workflows/READMES/tf_plan_single_root.md @@ -55,8 +55,8 @@ and creates each directory that a string starting with `${path.module}`, `${path - comes from a string with no other interpolation, such as `${var.name}` It skips remote modules, commented lines, `output_path` values, which Terraform writes, and -`fileexists()` arguments, because it fails on a directory. It annotates what it doesn't create. List any -other directories in `build-artifact-directory`, or set `detect-build-artifact-directories` to +`fileexists()` arguments, because it fails on a directory. It annotates what it doesn't create. List +any other directories in `build-artifact-directory`, or set `detect-build-artifact-directories` to `false` to turn detection off. Each created directory, and each empty directory from `build-artifact-directory`, gets an empty diff --git a/.github/workflows/READMES/tf_validate_plan_env_roots.md b/.github/workflows/READMES/tf_validate_plan_env_roots.md index 72db87a..9ad8e3d 100644 --- a/.github/workflows/READMES/tf_validate_plan_env_roots.md +++ b/.github/workflows/READMES/tf_validate_plan_env_roots.md @@ -1,8 +1,9 @@ # Terraform Validate and Plan Environment Roots -> **Deprecated:** use [`tf_plan_env_roots`](tf_plan_env_roots.md) instead. This workflow gets no further releases, -> and there's no direct upgrade path: `tf_plan_env_roots` drops `terraform validate` (it runs in Repo -> Checks) and `init -upgrade`, and renames the checks to `… / Plan`. +> **Deprecated:** use [`tf_plan_env_roots`](tf_plan_env_roots.md) instead. This workflow gets no +> further releases, and there's no direct upgrade path: `tf_plan_env_roots` drops +> `terraform validate` (it runs in Repo Checks) and `init -upgrade`, and renames the checks to +> `… / Plan`. Plans `terraform/dev`, `terraform/stage`, and `terraform/prod` in parallel, without state locking, through [`tf_validate_plan_single_root`](tf_validate_plan_single_root.md). It never applies, but diff --git a/.github/workflows/READMES/tf_validate_plan_single_root.md b/.github/workflows/READMES/tf_validate_plan_single_root.md index 5ad72f7..e537cbd 100644 --- a/.github/workflows/READMES/tf_validate_plan_single_root.md +++ b/.github/workflows/READMES/tf_validate_plan_single_root.md @@ -1,8 +1,9 @@ # Terraform Validate and Plan a Single Root -> **Deprecated:** use [`tf_plan_single_root`](tf_plan_single_root.md) instead. This workflow gets no further releases, -> and there's no direct upgrade path: `tf_plan_single_root` drops `terraform validate` (it runs in Repo -> Checks) and `init -upgrade`, and renames the checks to `… / Plan`. +> **Deprecated:** use [`tf_plan_single_root`](tf_plan_single_root.md) instead. This workflow gets no +> further releases, and there's no direct upgrade path: `tf_plan_single_root` drops +> `terraform validate` (it runs in Repo Checks) and `init -upgrade`, and renames the checks to +> `… / Plan`. Runs `terraform init`, `validate`, and `plan` for one root in a GitHub environment and writes the plan to the job summary. It never applies, but providers and data sources may contact live systems. diff --git a/.github/workflows/TESTS/run_gha_checks.unit.test.js b/.github/workflows/TESTS/run_gha_checks.unit.test.js index 3b8cfe6..1ec6bc8 100644 --- a/.github/workflows/TESTS/run_gha_checks.unit.test.js +++ b/.github/workflows/TESTS/run_gha_checks.unit.test.js @@ -1,3 +1,7 @@ +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + const { readWorkflow, runStepInRepository } = require('./workflow-test-helpers'); const workflowFile = 'run_gha_checks.yml'; @@ -39,3 +43,54 @@ describe('run_gha_checks baselined count', () => { expect(result.outputs.count).toBe('0'); }); }); + +describe('run_gha_checks annotations', () => { + const runWithStubMise = (stepName, status) => { + const bin = fs.mkdtempSync(path.join(os.tmpdir(), 'stub-mise-')); + fs.writeFileSync( + path.join(bin, 'mise'), + `#!/usr/bin/env bash\nprintf '%s\\n' "$@"\nexit ${status}\n`, + { mode: 0o755 } + ); + try { + return runStepInRepository( + workflowFile, + 'checks', + stepName, + { 'README.md': '# Fixture\n' }, + { + PATH: `${bin}:${process.env.PATH}`, + } + ); + } finally { + fs.rmSync(bin, { recursive: true, force: true }); + } + }; + + test('Lint passes actionlint a format that writes GitHub error annotations', () => { + const result = runWithStubMise('Lint', 0); + + expect(result.status).toBe(0); + const args = result.stdout.trimEnd().split('\n'); + expect(args.slice(0, 4)).toEqual(['run', 'lint:gha', '--', '-format']); + expect(args[4]).toContain( + '::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}},title=actionlint {{$err.Kind}}::{{$err.Message}}' + ); + }); + + test('Scan passes zizmor the GitHub annotation format', () => { + const result = runWithStubMise('Scan', 0); + + expect(result.status).toBe(0); + expect(result.stdout.trimEnd().split('\n')).toEqual([ + 'run', + 'scan:gha', + '--', + '--format=github', + ]); + }); + + test.each(['Lint', 'Scan'])('%s fails when its task fails', stepName => { + expect(runWithStubMise(stepName, 1).status).toBe(1); + }); +}); diff --git a/.github/workflows/deploy_environment.yml b/.github/workflows/deploy_environment.yml index 92bc8ee..f9fb22e 100644 --- a/.github/workflows/deploy_environment.yml +++ b/.github/workflows/deploy_environment.yml @@ -41,14 +41,15 @@ on: concurrency: ${{ github.repository }}-${{ inputs.environment }} -permissions: - id-token: write - contents: read +permissions: {} jobs: TF-Apply: name: Terraform Apply uses: ./.github/workflows/tf_apply.yml + permissions: + id-token: write # Required to assume the Terraform apply role through OIDC. + contents: read # Required to check out the Terraform code. with: environment: ${{ inputs.environment }} oidc-domain: ${{ inputs.oidc-domain }} diff --git a/.github/workflows/deploy_thru_prod.yml b/.github/workflows/deploy_thru_prod.yml index efb2578..3a2488d 100644 --- a/.github/workflows/deploy_thru_prod.yml +++ b/.github/workflows/deploy_thru_prod.yml @@ -40,14 +40,12 @@ on: concurrency: ${{ github.repository }} -permissions: - id-token: write - contents: write - pull-requests: write +permissions: {} jobs: Set-Release-Tag: runs-on: ubuntu-26.04 + permissions: {} if: ${{ github.event.pull_request.merged || github.event_name == 'workflow_dispatch' }} outputs: release-tag: ${{ steps.releaseTag.outputs.release-tag }} @@ -61,6 +59,9 @@ jobs: name: Apply Dev needs: [Set-Release-Tag] uses: ./.github/workflows/deploy_environment.yml + permissions: + id-token: write # Required to assume the Terraform apply role through OIDC. + contents: read # Required to check out the Terraform code. with: environment: dev oidc-domain: ${{ inputs.oidc-domain }} @@ -77,6 +78,9 @@ jobs: name: Apply Stage needs: [Set-Release-Tag, DeployDev] uses: ./.github/workflows/deploy_environment.yml + permissions: + id-token: write # Required to assume the Terraform apply role through OIDC. + contents: read # Required to check out the Terraform code. with: environment: stage oidc-domain: ${{ inputs.oidc-domain }} @@ -93,6 +97,9 @@ jobs: name: Apply Prod needs: [Set-Release-Tag, DeployStage] uses: ./.github/workflows/deploy_environment.yml + permissions: + id-token: write # Required to assume the Terraform apply role through OIDC. + contents: read # Required to check out the Terraform code. with: environment: prod oidc-domain: ${{ inputs.oidc-domain }} @@ -109,6 +116,8 @@ jobs: name: Create New Release needs: [Set-Release-Tag, DeployProd] runs-on: ubuntu-26.04 + permissions: + contents: write # Required to create the release. steps: - name: Create release @@ -124,6 +133,8 @@ jobs: runs-on: ubuntu-26.04 needs: [DeployDev, DeployStage, DeployProd, CreateRelease] if: always() + permissions: + pull-requests: write # Required to comment the deploy status on the pull request. env: BUILD_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/internal_on_push_ci.yml b/.github/workflows/on_push_ci.yml similarity index 79% rename from .github/workflows/internal_on_push_ci.yml rename to .github/workflows/on_push_ci.yml index 1e8af40..3eb5b53 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/on_push_ci.yml @@ -49,7 +49,7 @@ jobs: - name: Install dependencies id: install - run: npm ci + run: npm ci --ignore-scripts # The audit runs here, not through run_npm_audit: this repository has only devDependencies, # and run_npm_audit audits production dependencies only (--omit=dev), so it would check nothing. @@ -61,23 +61,17 @@ jobs: if: ${{ !cancelled() && steps.install.outcome == 'success' }} run: npm run test - - name: Lint - if: ${{ !cancelled() && steps.install.outcome == 'success' }} - run: npm run lint:js - - - name: Check formatting - if: ${{ !cancelled() && steps.install.outcome == 'success' }} - run: npm run format:prettier - - infra-ci: - name: Infra CI + repo-checks: + name: Repo Checks needs: ci-context if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} - uses: ./.github/workflows/run_infra_ci.yml + uses: ./.github/workflows/run_repo_checks.yml permissions: - contents: read # Required to check out the workflows to lint and audit. + contents: read # Required to check out the code to lint, format-check, scan and validate. with: commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} + secrets: + ORG_READ_ONLY_SSH_KEY: ${{ secrets.ORG_READ_ONLY_SSH_KEY }} semgrep: name: Semgrep @@ -96,20 +90,13 @@ jobs: ci-result: name: CI Result - needs: [ci-context, app-ci, infra-ci, semgrep] - if: ${{ !cancelled() }} + needs: [ci-context, app-ci, repo-checks, semgrep] + if: ${{ !cancelled() && needs.ci-context.outputs.should-run-ci != 'false' }} runs-on: ubuntu-26.04 - permissions: - contents: read # Required to check out the local check-ci-result action. + permissions: {} steps: - - name: Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: false - - name: Check CI job results - uses: ./.github/actions/check-ci-result + uses: OpenSesame/core-github-actions/.github/actions/check-ci-result@95f95aac6fe51a55e656e82ba17ba213c530d13a # actions/check-ci-result/1.0.0 with: needs-json: ${{ toJSON(needs) }} diff --git a/.github/workflows/run_gha_checks.yml b/.github/workflows/run_gha_checks.yml index 5fdb631..f3260d7 100644 --- a/.github/workflows/run_gha_checks.yml +++ b/.github/workflows/run_gha_checks.yml @@ -53,12 +53,16 @@ jobs: - name: Lint id: lint if: ${{ !cancelled() && steps.mise.outcome == 'success' }} - run: mise run lint:gha + run: | + # shellcheck disable=SC2016 + mise run lint:gha -- \ + -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}},title=actionlint {{$err.Kind}}::{{$err.Message}}\n{{end}}' - name: Scan id: scan if: ${{ !cancelled() && steps.mise.outcome == 'success' }} - run: mise run scan:gha + run: | + mise run scan:gha -- --format=github - name: Count baselined issues id: baseline diff --git a/.github/workflows/tf_apply.yml b/.github/workflows/tf_apply.yml index 65e91d1..fd3c92f 100644 --- a/.github/workflows/tf_apply.yml +++ b/.github/workflows/tf_apply.yml @@ -41,9 +41,7 @@ on: concurrency: group: ${{ github.repository }}-${{ inputs.environment }}-TF-Apply -permissions: - id-token: write - contents: read +permissions: {} env: TF_INPUT: false @@ -52,6 +50,7 @@ env: jobs: Summary: runs-on: ubuntu-26.04 + permissions: {} steps: - name: Markdown Summary env: @@ -78,6 +77,9 @@ jobs: TF-Apply: name: Terraform Apply runs-on: ubuntu-26.04 + permissions: + id-token: write # Required to assume the Terraform apply role through OIDC. + contents: read # Required to check out the Terraform code. environment: ${{ inputs.environment}} defaults: run: diff --git a/.gitignore b/.gitignore index ad96889..a850710 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,4 @@ planout .DS_Store +.core-services/debug-source/ diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 0000000..53b8c49 --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,7 @@ +globs: + - "**/*.md" +ignores: + - "**/node_modules/**" + - "**/.terraform/**" + - "**/coverage/**" + - ".core-services/**" diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..c24ac9f --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,7 @@ +{ + "line-length": { + "line_length": 100, + "code_blocks": false, + "tables": false + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..854c82b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +.core-services/* +!.core-services/repository-contract.json diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..f463870 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,28 @@ +# A .semgrepignore replaces Semgrep's default ignore list, so the defaults are repeated here. +.git/ +.svn/ +.hg/ +_darcs/ +CVS/ +node_modules/ +build/ +dist/ +vendor/ +.env/ +.venv/ +_opam/ +_build/ +_cargo/ +.tox/ +*.min.js +.npm/ +.yarn/ +test/ +tests/ +testsuite/ +*_test.go +.semgrep +.semgrep_logs/ + +.core-services/* +!.core-services/repository-contract.json diff --git a/.vscode/settings.json b/.vscode/settings.json index ec5f418..ea748b7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", // Use Prettier for these languages - "[typescript]": { "editor.defaultFormatter": "vscode.typescript-language-features" }, + "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, diff --git a/README.md b/README.md index a6cb1d4..36b9d90 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ The Core Services team uses GitHub Actions to standardize our CI/CD process. ## ❌ Deprecation Notice: Legacy Composite Actions -An earlier version of the 'Core' team wrote root-level composite actions in this repository to provide a standardized way for teams to build, test, and deploy software. -These actions have not been actively maintained in years and are considered deprecated by the current Core Services team. +An earlier version of the 'Core' team wrote root-level composite actions in this repository to +provide a standardized way for teams to build, test, and deploy software. These actions have not +been actively maintained in years and are considered deprecated by the current Core Services team. - [build](./build) - [build-apply](./build-apply) @@ -20,12 +21,16 @@ These actions have not been actively maintained in years and are considered depr ### Why deprecated -1. They hide important pipeline details making it difficult for engineers to understand or troubleshoot CI/CD pipelines. +1. They hide important pipeline details making it difficult for engineers to understand or + troubleshoot CI/CD pipelines. 2. They have accumulated technical debt and do not reflect our current best practices. ### What we're doing instead -The Core Services team uses reusable workflows for shared CI/CD orchestration so standard pipeline phases remain visible. Narrowly scoped utility composite actions are also acceptable when they live under `.github/actions`, have a cohesive contract, and follow this repository's component versioning policy. +The Core Services team uses reusable workflows for shared CI/CD orchestration so standard pipeline +phases remain visible. Narrowly scoped utility composite actions are also acceptable when they live +under `.github/actions`, have a cohesive contract, and follow this repository's component versioning +policy. ### Maintenance ownership of the old composite actions @@ -37,7 +42,8 @@ The Core Services team uses reusable workflows for shared CI/CD orchestration so - Copy the composite action code directly into your workflow in place of calling the composite action. - Use a reusable workflow for multi-phase CI/CD orchestration. -- Use a versioned utility composite action under `.github/actions` when the behavior is cohesive and does not hide standard pipeline phases. +- Use a versioned utility composite action under `.github/actions` when the behavior is cohesive and + does not hide standard pipeline phases. ## ⚠️ Versioning Warning @@ -69,8 +75,8 @@ To get started: - install semgrep globally `brew install semgrep` - install [mise](https://mise.jdx.dev/) (`brew install mise`), then run `mise install` for actionlint, - shellcheck and zizmor, pinned in `mise.toml`. `npm run lint:gha` and `npm run scan:gha` run them, and - `npm run check` includes both. + shellcheck and zizmor, pinned in `mise.toml`. `npm run lint:gha` and `npm run scan:gha` run them, + and `npm run check` includes both. - use Node.js 24.15.0 or newer in the Node.js 24 release line, with the npm it ships (11.10.0 or newer) (`nvm use`) - install project dependencies `npm ci` diff --git a/VERSIONING.md b/VERSIONING.md index 8c1732c..7c9e3a4 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -1,15 +1,17 @@ # Versioning Policy -This repository contains **reusable GitHub Actions and reusable workflows** that may be consumed by multiple repos or teams. To ensure safe updates and backward compatibility, all versioned components follow the standards defined below. +This repository contains **reusable GitHub Actions and reusable workflows** that may be consumed by +multiple repos or teams. To ensure safe updates and backward compatibility, all versioned components +follow the standards defined below. ## Scope -**Current** +### Current - Composite Actions that live under `./github/actions` - Reusable workflows that live under `./github/workflows` -**Excluded** +### Excluded - Composite Actions that live in the root of this repository. @@ -25,15 +27,16 @@ Each versioned component must: ## 📌 Namespaced Tags -Versioned components use namespaced semantic version tags so multiple components can be versioned independently within this repository. +Versioned components use namespaced semantic version tags so multiple components can be versioned +independently within this repository. -**Format** +### Tag Format ```text actions//vX.Y.Z ``` -**Examples** +### Tag Examples ```text actions/pr-open-check/2.0.0 @@ -41,7 +44,7 @@ actions/upsert-pr-comment/1.0.0 workflows/run_semgrep_scan/1.0.0 ``` -**Semantic Versioning** +### Semantic Versioning Version numbers follow SemVer: @@ -60,7 +63,7 @@ The version declared in a PR label must match both: PRs that modify a versioned component must include a label per component modified. -**Formats** +#### Label Formats - Actions: `v:a//X.Y.Z` - Workflows: `v:wf//X.Y.Z` @@ -74,7 +77,7 @@ The compact component type is used only in PR labels to stay within GitHub's 50-character label-name limit. Generated tags retain the canonical `actions/` or `workflows/` namespace. -**Example** +#### Label Example ```text v:a/pr-open-check/1.1.0 @@ -85,9 +88,11 @@ Multiple version labels are allowed on a PR, one per component. ### Special Case: Untracked Versions -Use an untracked version label only when a PR modifies files outside of any versioned component, or modifies a component in a way that does not change its behavior (e.g., documentation-only changes, tests, comments). +Use an untracked version label only when a PR modifies files outside of any versioned component, or +modifies a component in a way that does not change its behavior (e.g., documentation-only changes, +tests, comments). -**Format** +#### Untracked Label Format ```text v:untracked @@ -117,9 +122,11 @@ A PR cannot merge until version validation passes. ## 📝 CHANGELOG Requirements -Each versioned component must have its own changelog. The changelog is the authoritative source of truth for released versions of that component. +Each versioned component must have its own changelog. The changelog is the authoritative source of +truth for released versions of that component. + +### Location -**Located At** Action changelogs live in their component directory under `.github/actions/`. ```text @@ -132,7 +139,7 @@ Workflow changelogs live in a component file under the `.github/workflows/CHANGE .github/workflows/CHANGELOGS/.md ``` -**Examples** +### Path Examples ```text .github/actions/pr-open-check/CHANGELOG.md @@ -165,8 +172,8 @@ Each released version must have a heading that includes the version label Minimum requirements: -- The header must be a line that is exactly `## X.Y.Z`, optionally followed by ` - title` or - ` — title` (this is what validation looks for). +- The header must be a line that is exactly `## X.Y.Z`, `## X.Y.Z - title` or `## X.Y.Z — title` + (this is what validation looks for). - The PR must add or update an entry for the version used in the label (`v:a/{component-name}/X.Y.Z` or `v:wf/{component-name}/X.Y.Z`). @@ -177,7 +184,8 @@ The rest of the content (sections and bullets) is for humans, but strongly recom For versioned releases, a label like `v:a/pr-open-check/1.2.0` requires that the `CHANGELOG.md` file under `.github/actions/pr-open-check/` contain a `## 1.2.0` entry. -A label like `v:wf/run_semgrep_scan/1.0.0` requires a `run_semgrep_scan.md` file under `.github/workflows/CHANGELOGS` containing a `## 1.0.0` entry. +A label like `v:wf/run_semgrep_scan/1.0.0` requires a `run_semgrep_scan.md` file under +`.github/workflows/CHANGELOGS` containing a `## 1.0.0` entry. Validation will fail if: diff --git a/build-apply/README.md b/build-apply/README.md index ff9b3da..eaa00a6 100644 --- a/build-apply/README.md +++ b/build-apply/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 @@ -21,7 +22,8 @@ The action will do the following: 1. Configure github credentials so that terraform can read from our private repos 2. Run npm commands to build and test 3. Run a script to read secrets and write them to a `.env` file (git ignored) -4. Run terraform commands using the `.env` file to apply state to the target environment under the provided workspace +4. Run terraform commands using the `.env` file to apply state to the target environment under the + provided workspace ### Requirements @@ -35,4 +37,5 @@ The following items must be in the repository: * `build` - build the code * `test` - test the code (pre-deployment) * `terraform` folder containing roots for each environment (`dev`, `stage`, and `prod`) -* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. These will be written to `.env` file. +* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. + These will be written to `.env` file. diff --git a/build-publish/README.md b/build-publish/README.md index a53ba56..5b4fca6 100644 --- a/build-publish/README.md +++ b/build-publish/README.md @@ -4,8 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. -Known Consumers - Audited Sept 2026 +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 * [core-build-library](https://github.com/OpenSesame/core-build-library) * [core-repo-template](https://github.com/OpenSesame/core-repo-template) @@ -28,4 +28,7 @@ You will need to add this as a script in your `package.json`: "postversion": "cp package.json " ``` -You can utilize the cp command, or install and use copyfiles to be system agnostic. After the `npm version` command bumps the version in the `package.json`, you need to copy the updated `package.json` into your output folder (default is `dist`) before `npm publish` runs to ensure it will publish the new version. +You can utilize the cp command, or install and use copyfiles to be system agnostic. After the +`npm version` command bumps the version in the `package.json`, you need to copy the updated +`package.json` into your output folder (default is `dist`) before `npm publish` runs to ensure it +will publish the new version. diff --git a/build/README.md b/build/README.md index a85f1cd..4cf6dbb 100644 --- a/build/README.md +++ b/build/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 @@ -29,4 +30,5 @@ The following items must be in the repository: * `core-build-library` - for core-build commands * `package.json` scripts: * `build` - build the code -* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. These will be written to `.env` file. +* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. + These will be written to `.env` file. diff --git a/cleanup/README.md b/cleanup/README.md index 2c77bc7..18af37c 100644 --- a/cleanup/README.md +++ b/cleanup/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 @@ -21,7 +22,8 @@ The action will do the following: 1. Configure githug credentials so that terraform can read from our private repos 2. Run `npm ci` 3. Run a script to read secrets and write them to a `.env` file (git ignored) -4. Run terraform commands using the `.env` file to destroy state in the target environment under the provided workspace +4. Run terraform commands using the `.env` file to destroy state in the target environment under the + provided workspace ### Requirements @@ -35,4 +37,5 @@ The following items must be in the repository: * `build` - build the code * `test` - test the code (pre-deployment) * `terraform` folder containing roots for each environment (`dev`, `stage`, and `prod`) -* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. These will be written to `.env` file. +* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. + These will be written to `.env` file. diff --git a/deploy/README.md b/deploy/README.md index c0af6c3..6093cfa 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 @@ -14,7 +15,8 @@ Known Consumers - Audited Sept 2026 ### Overview -The deploy composite action runs npm commands to deploy to a target environment, followed by post-deployment contract tests. +The deploy composite action runs npm commands to deploy to a target environment, followed by +post-deployment contract tests. The action will do the following: @@ -33,4 +35,5 @@ The following items must be in the repository: * `build` - build the code * `test` - test the code (pre-deployment) * `terraform` folder containing roots for each environment (`dev`, `stage`, and `prod`) -* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. These will be written to `.env` file. +* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. + These will be written to `.env` file. diff --git a/eslint.config.mjs b/eslint.config.mjs index f019ae8..a7f2743 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,6 +4,7 @@ import { defineConfig } from 'eslint/config'; import globals from 'globals'; export default defineConfig([ + { ignores: ['.core-services/**'] }, { files: ['**/*.{js,mjs,cjs}'], plugins: { js }, diff --git a/package-lock.json b/package-lock.json index 221039e..96d7bf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "eslint": "10.14.0", "globals": "17.13.0", "jest": "30.5.2", + "markdownlint-cli2": "0.24.0", "npm-run-all2": "9.0.4", "prettier": "3.9.10", "semgrep": "0.0.1" @@ -1464,6 +1465,44 @@ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@parcel/watcher": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", @@ -1800,6 +1839,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -2613,6 +2665,19 @@ "node": "20 || >=22" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.29.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.1.tgz", @@ -2761,6 +2826,28 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ci-info": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", @@ -3011,6 +3098,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -3279,6 +3379,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3293,6 +3423,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fault": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", @@ -3345,6 +3485,19 @@ "flat-cache": "^6.1.23" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3440,6 +3593,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3514,6 +3680,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.4.tgz", + "integrity": "sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "micromatch": "^4.0.8", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3608,6 +3819,32 @@ "node": ">=0.8.19" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3615,6 +3852,17 @@ "dev": true, "license": "MIT" }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3658,6 +3906,40 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4421,6 +4703,23 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/katex": { "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", @@ -4479,6 +4778,26 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4545,6 +4864,51 @@ "node": ">=10" } }, + "node_modules/markdown-it": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -4556,6 +4920,147 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/markdownlint": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.42.0.tgz", + "integrity": "sha512-kzsCV/rPWRFn3jUhcB919YF4Qgeh0g9tcogoXFadGSo6Zxj6662YOqWXv0jDn8nv5KCCxDfMKJZv6N4HkNkvTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "4.0.3", + "micromark-core-commonmark": "2.0.4", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.2", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.3", + "re2js": "2.8.6", + "string-width": "8.3.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.24.0.tgz", + "integrity": "sha512-c5hU2MDoTlS230MLzdNlkIyxImXq5acxGKz4nYz4aDhH/2BgFtpsA7iK9/OoTHMgZ4FuR0wedTQA31K6Lp9I/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "16.2.4", + "js-yaml": "5.4.3", + "jsonc-parser": "3.3.1", + "jsonpointer": "5.0.1", + "markdown-it": "15.0.3", + "markdownlint": "0.42.0", + "markdownlint-cli2-formatter-default": "0.0.6", + "micromatch": "4.0.8", + "smol-toml": "1.9.1" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2-bin.mjs" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + }, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/markdownlint-cli2/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/markdownlint-cli2/node_modules/js-yaml": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz", + "integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/markdownlint/node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/markdownlint/node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/mdast-util-find-and-replace": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", @@ -4821,6 +5326,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -4837,10 +5349,20 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", "dev": true, "funding": [ { @@ -4864,6 +5386,7 @@ "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", @@ -4874,9 +5397,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", "dev": true, "funding": [ { @@ -4894,18 +5417,39 @@ "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", + "micromark-factory-space": "^2.1.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-types": "^2.0.3" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/micromark-extension-frontmatter": { @@ -5004,9 +5548,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", "dev": true, "license": "MIT", "dependencies": { @@ -5119,9 +5663,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", "dev": true, "funding": [ { @@ -5312,6 +5856,26 @@ "micromark-util-symbol": "^2.0.0" } }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", @@ -5449,9 +6013,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", "dev": true, "funding": [ { @@ -5465,6 +6029,33 @@ ], "license": "MIT" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5737,6 +6328,33 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -5980,6 +6598,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -6017,6 +6645,37 @@ "dev": true, "license": "MIT" }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/re2js": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.8.tgz", + "integrity": "sha512-E2FVgqD2mzJ+ZmMSHLnkOvZ7PiZ4FgMdfVCA3YsJWs9gtyoaKmUMxyVPCXS1yK209/mz3XkdT7kr3OlMWW2Fww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/read-package-json-fast": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-6.0.0.tgz", @@ -6074,6 +6733,41 @@ "node": ">=8" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/semgrep": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/semgrep/-/semgrep-0.0.1.tgz", @@ -6144,6 +6838,19 @@ "node": ">=8" } }, + "node_modules/smol-toml": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6402,6 +7109,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6446,6 +7166,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", @@ -6453,6 +7180,19 @@ "dev": true, "license": "MIT" }, + "node_modules/unicorn-magic": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.1.tgz", + "integrity": "sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", diff --git a/package.json b/package.json index 1ff0a20..f105ea1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "eslint": "10.14.0", "globals": "17.13.0", "jest": "30.5.2", + "markdownlint-cli2": "0.24.0", "npm-run-all2": "9.0.4", "prettier": "3.9.10", "semgrep": "0.0.1" @@ -33,8 +34,10 @@ "lint": "npm-run-all --continue-on-error lint:*", "lint:js": "eslint *.js --ext .js,.json", "lint:gha": "mise run lint:gha", + "lint:markdown": "markdownlint-cli2", "lint-fix": "npm-run-all --continue-on-error lint-fix:*", "lint-fix:js": "eslint *.js --ext .js,.json --fix", + "lint-fix:markdown": "markdownlint-cli2 --fix", "format": "npm-run-all --continue-on-error format:*", "format:prettier": "prettier --check './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", "format-fix": "npm-run-all --continue-on-error format-fix:*", @@ -43,5 +46,8 @@ "scan:semgrep": "semgrep --config=p/ci --config=p/security-audit --config=p/javascript ./*.js ./*.mjs ./*.json scripts/ .github/actions/", "scan:gha": "mise run scan:gha", "check": "npm-run-all --continue-on-error audit lint format test scan" + }, + "overrides": { + "re2js": "2.8.8" } } diff --git a/select-branch-workspace/README.md b/select-branch-workspace/README.md index e7a6437..646f6fa 100644 --- a/select-branch-workspace/README.md +++ b/select-branch-workspace/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 diff --git a/semgrep/README.md b/semgrep/README.md index 76638de..79841e6 100644 --- a/semgrep/README.md +++ b/semgrep/README.md @@ -6,7 +6,9 @@ This action runs Semgrep security scanning with configurable options and reporti This composite action is no longer maintained by the Core Services team. Use at your own risk. -We suggest consumers switch over to the reusable workflow for [semgrep scan](https://github.com/OpenSesame/.github/blob/main/.github/workflows/semgrep-scan.yml) so it can be run in parallel with your other jobs. +We suggest consumers switch over to the reusable workflow for [semgrep +scan](https://github.com/OpenSesame/.github/blob/main/.github/workflows/semgrep-scan.yml) so it can +be run in parallel with your other jobs. Known Consumers - Audited Sept 2026 diff --git a/tf-apply/README.md b/tf-apply/README.md index 420f689..d97cb61 100644 --- a/tf-apply/README.md +++ b/tf-apply/README.md @@ -4,19 +4,22 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. No Known Consumers - Audited Sept 2026 ### Overview -The tf-apply composite action initializes, validates and apply terraform resources whilst selecting the desired workspace. +The tf-apply composite action initializes, validates and apply terraform resources whilst selecting +the desired workspace. The action will do the following: 1. Configure github credentials so that terraform can read from our private repos 2. Run a script to read secrets and write them to a `.env` file (git ignored) -3. Run terraform commands uses the `.env` file to validate and apply state to the target environment under the provided workspace +3. Run terraform commands uses the `.env` file to validate and apply state to the target environment + under the provided workspace ### Requirements @@ -30,4 +33,5 @@ The following items must be in the repository: * `build` - build the code * `test` - test the code (pre-deployment) * `terraform` folder containing roots for each environment (`dev`, `stage`, and `prod`) -* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. These will be written to `.env` file. +* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. + These will be written to `.env` file. diff --git a/tf-plan-comment/README.md b/tf-plan-comment/README.md index 51fc514..5db6613 100644 --- a/tf-plan-comment/README.md +++ b/tf-plan-comment/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 @@ -30,7 +31,9 @@ Known Consumers - Audited Sept 2026 ### Overview -The tf-plan-comment composite action initializes actions/github-scripts. Bringing the script under core-github-actions allows us to be more modularized. We can now make any changes to the desired output in one area and have it apply to all the repos utilizing it. +The tf-plan-comment composite action initializes actions/github-scripts. Bringing the script under +core-github-actions allows us to be more modularized. We can now make any changes to the desired +output in one area and have it apply to all the repos utilizing it. The action will do the following: diff --git a/tf-plan/README.md b/tf-plan/README.md index 0be37f9..163ab77 100644 --- a/tf-plan/README.md +++ b/tf-plan/README.md @@ -4,7 +4,8 @@ This composite action is no longer maintained by the Core Services team. Use at your own risk. -If your team still relies on this action, you may request CODEOWNER status for this directory to maintain it. +If your team still relies on this action, you may request CODEOWNER status for this directory to +maintain it. Known Consumers - Audited Sept 2026 @@ -14,13 +15,15 @@ Known Consumers - Audited Sept 2026 ### Overview -The tf-plan composite action initializes, validates and plans terraform resources whilst selecting the desired workspace. +The tf-plan composite action initializes, validates and plans terraform resources whilst selecting +the desired workspace. The action will do the following: 1. Configure github credentials so that terraform can read from our private repos 2. Run a script to read secrets and write them to a `.env` file (git ignored) -3. Run terraform commands uses the `.env` file to validate and plan state to the target environment under the provided workspace +3. Run terraform commands uses the `.env` file to validate and plan state to the target environment + under the provided workspace ### Requirements @@ -34,4 +37,5 @@ The following items must be in the repository: * `build` - build the code * `test` - test the code (pre-deployment) * `terraform` folder containing roots for each environment (`dev`, `stage`, and `prod`) -* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. These will be written to `.env` file. +* (optional) `secrets-map.json` file containing map of secrets and associated environment variables. + These will be written to `.env` file.