diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..78fc41b --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +self-hosted-runner: + # No actionlint release knows the ubuntu-26.04 GitHub-hosted runner label yet. + labels: + - ubuntu-26.04 diff --git a/.github/workflows/CHANGELOGS/run_infra_ci.md b/.github/workflows/CHANGELOGS/run_infra_ci.md new file mode 100644 index 0000000..89eec3d --- /dev/null +++ b/.github/workflows/CHANGELOGS/run_infra_ci.md @@ -0,0 +1,15 @@ +# Run Infra CI Workflow Changelog + +All notable changes to the **run_infra_ci** reusable workflow are documented in this file. + +## 1.0.0 + +### Added + +- First release of the `run_infra_ci` reusable workflow, with an optional `commit-identifier` input. +- Jobs `lint-gha` ("Lint GHA", actionlint with shellcheck) and `scan-gha` ("Scan GHA", zizmor), each + running the consuming repository's `lint:gha` or `scan:gha` mise task with GitHub annotation output. +- A contract check that fails with a README link when `mise.toml` or the job's task is missing. +- mise 2026.9.17 installed through `jdx/mise-action` 5.0.0, with the binary checksum pinned. +- zizmor runs offline (`scan:gha` uses `--offline`), so no token is needed. +- Job summaries with the result, the suppression count, and the local command. diff --git a/.github/workflows/READMES/run_infra_ci.md b/.github/workflows/READMES/run_infra_ci.md new file mode 100644 index 0000000..4edb88e --- /dev/null +++ b/.github/workflows/READMES/run_infra_ci.md @@ -0,0 +1,77 @@ +# Run Infra CI + +Lints GitHub Actions workflows with [actionlint](https://github.com/rhysd/actionlint) (and shellcheck) +and audits them with [zizmor](https://docs.zizmor.sh/), through tasks in the repo's `mise.toml`, so +CI and engineers run the same pinned tools. + +## Usage + +```yaml +jobs: + infra-ci: + name: Infra CI + needs: ci-context + if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} + uses: OpenSesame/core-github-actions/.github/workflows/run_infra_ci.yml@ # workflows/run_infra_ci/1.0.0 + permissions: + contents: read # Required to check out the workflows to lint and audit. + with: + commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} +``` + +Checks are reported as `Infra CI / Lint GHA` and `Infra CI / Scan GHA`. Add `infra-ci` to `ci-result`'s +needs. + +## Setup + +1. Commit this `mise.toml` at the repo root (infra tools only; Node stays on `.nvmrc`): + + ```toml + [tools] + "aqua:rhysd/actionlint" = "1.7.12" + "aqua:koalaman/shellcheck" = "0.11.0" + "aqua:zizmorcore/zizmor" = "1.29.0" + + [tasks."lint:gha"] + run = "actionlint" + + [tasks."scan:gha"] + run = "zizmor --offline .github" + + [tasks.infra-ci] + depends = ["lint:gha", "scan:gha"] + ``` + +2. Commit a lockfile for Macs and CI runners: `mise lock --platform linux-x64,macos-arm64`. +3. Declare the runner label actionlint doesn't know yet in `.github/actionlint.yaml`: + + ```yaml + self-hosted-runner: + labels: + - ubuntu-26.04 + ``` + +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. + +## Findings + +Fix findings. Suppress only false positives or intentional patterns, one specific finding at a time, +each with a comment explaining why: + +- actionlint: `.github/actionlint.yaml` `paths..ignore` +- zizmor: inline `# zizmor: ignore[] `, or `.github/zizmor.yml` `rules..ignore` + +## Inputs + +| Input | Default | Description | +| ------------------- | --------------------- | ----------------------------------- | +| `commit-identifier` | Triggering ref or SHA | Commit SHA, tag, or branch to check | + +## Contribution + +Update the workflow, README, and changelog together, and set a version label per +[VERSIONING.md](../../../VERSIONING.md). diff --git a/.github/workflows/internal_on_merge_tag_versions.yml b/.github/workflows/internal_on_merge_tag_versions.yml index 5b3a58d..d824b62 100644 --- a/.github/workflows/internal_on_merge_tag_versions.yml +++ b/.github/workflows/internal_on_merge_tag_versions.yml @@ -4,27 +4,30 @@ on: pull_request: types: [closed] -permissions: - contents: write - pull-requests: read +permissions: {} jobs: no-merge: if: github.event.pull_request.merged == false runs-on: ubuntu-26.04 + permissions: {} steps: - name: No Merge Detected run: | - const message = "Pull request was closed without merge. No version tags will be created." + message="Pull request was closed without merge. No version tags will be created." echo "ℹ️ $message" >> "$GITHUB_STEP_SUMMARY" echo "::notice title=No Merge::$message" set-version-tags: if: github.event.pull_request.merged == true runs-on: ubuntu-26.04 + permissions: + contents: write # Required to push the version tags. steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: true # Required for git push of the version tags. - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 @@ -60,7 +63,7 @@ jobs: - name: Untracked Version Check if: ${{ steps.get_tags.outputs.hasUntracked == 'true' }} run: | - const message = "Untracked version label detected. Skipping tag creation." + message="Untracked version label detected. Skipping tag creation." echo "ℹ️ $message" >> "$GITHUB_STEP_SUMMARY" echo "::notice title=Untracked Version::$message" @@ -69,7 +72,7 @@ jobs: env: COMPONENT_TAGS: ${{ steps.get_tags.outputs.componentTags }} run: | - IFS=',' read -r -a tagsArray <<< "${{ env.COMPONENT_TAGS }}" + IFS=',' read -r -a tagsArray <<< "$COMPONENT_TAGS" for tag in "${tagsArray[@]}"; do git tag "$tag" git push origin "$tag" diff --git a/.github/workflows/internal_on_pr_validate_component_version.yml b/.github/workflows/internal_on_pr_validate_component_version.yml index fde82de..a9ce3d1 100644 --- a/.github/workflows/internal_on_pr_validate_component_version.yml +++ b/.github/workflows/internal_on_pr_validate_component_version.yml @@ -11,18 +11,21 @@ on: - unlabeled - reopened -permissions: - contents: read - pull-requests: write +permissions: {} jobs: validate-version-labels: name: Validate PR Version Labels runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the changelogs and validation scripts. + pull-requests: write # Required to upsert the version summary comment. steps: - name: Checkout repo uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Get all labels from PR id: get_labels @@ -66,34 +69,36 @@ jobs: INVALID_COMPONENTS: ${{ steps.validate.outputs.invalidComponents }} MISSING_CHANGELOGS: ${{ steps.validate.outputs.missingChangelogs }} run: | - echo "## Validation Outcome" >> "$GITHUB_STEP_SUMMARY" - - if [ "${{ env.NO_LABELS }}" == "true" ]; then - echo "❌ No labels found on the PR. Add at least one version label." >> "$GITHUB_STEP_SUMMARY" - fi + { + echo "## Validation Outcome" - if [ "${{ env.IS_VALID }}" == "false" ]; then - echo "❌ Version label validation failed." >> "$GITHUB_STEP_SUMMARY" - if [ -n "${{ env.VALIDATION_MESSAGE }}" ]; then - echo "${{ env.VALIDATION_MESSAGE }}" >> "$GITHUB_STEP_SUMMARY" - echo "" >> "$GITHUB_STEP_SUMMARY" - fi - if [ -n "${{ env.INVALID_VERSION_LABELS }}" ]; then - echo "**Invalid version labels**: ${{ env.INVALID_VERSION_LABELS }}" >> "$GITHUB_STEP_SUMMARY" + if [ "$NO_LABELS" == "true" ]; then + echo "❌ No labels found on the PR. Add at least one version label." fi - if [ -n "${{ env.INVALID_COMPONENTS }}" ]; then - echo "**Nonexistent/Invalid components**: ${{ env.INVALID_COMPONENTS }}" >> "$GITHUB_STEP_SUMMARY" + + if [ "$IS_VALID" == "false" ]; then + echo "❌ Version label validation failed." + if [ -n "$VALIDATION_MESSAGE" ]; then + echo "$VALIDATION_MESSAGE" + echo "" + fi + if [ -n "$INVALID_VERSION_LABELS" ]; then + echo "**Invalid version labels**: $INVALID_VERSION_LABELS" + fi + if [ -n "$INVALID_COMPONENTS" ]; then + echo "**Nonexistent/Invalid components**: $INVALID_COMPONENTS" + fi + if [ -n "$MISSING_CHANGELOGS" ]; then + echo "**Components missing changelogs/entries**: $MISSING_CHANGELOGS" + fi + else + echo "✅ Version label validation passed" fi - if [ -n "${{ env.MISSING_CHANGELOGS }}" ]; then - echo "**Components missing changelogs/entries**: ${{ env.MISSING_CHANGELOGS }}" >> "$GITHUB_STEP_SUMMARY" + echo -e "\n**Untracked Version**: $HAS_UNTRACKED_VERSION" + if [ -n "$COMPONENT_VERSION_LABELS" ]; then + echo "**Component Versions**: $COMPONENT_VERSION_LABELS" fi - else - echo "✅ Version label validation passed" >> "$GITHUB_STEP_SUMMARY" - fi - echo -e "\n**Untracked Version**: ${{ env.HAS_UNTRACKED_VERSION }}" >> "$GITHUB_STEP_SUMMARY" - if [ -n "${{ env.COMPONENT_VERSION_LABELS }}" ]; then - echo "**Component Versions**: ${{ env.COMPONENT_VERSION_LABELS }}" >> "$GITHUB_STEP_SUMMARY" - fi + } >> "$GITHUB_STEP_SUMMARY" - name: Write Tags Job Summary id: tags_summary @@ -107,7 +112,7 @@ jobs: summary+="- No Tags will be created on main" else summary+="The following tags will be created on main after merge\n\n" - echo $VALID_COMPONENTS + echo "$VALID_COMPONENTS" IFS=',' read -ra components <<< "$VALID_COMPONENTS" for component_version in "${components[@]}"; do summary+="🏷️ \`$component_version\`\n" @@ -120,9 +125,11 @@ jobs: # Replace any literal \n with real newlines for correct PR comment formatting summary_with_eol="${summary//\\n/$'\n'}" - echo "summary<> "$GITHUB_OUTPUT" - echo "$summary_with_eol" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" + { + echo "summary<> "$GITHUB_OUTPUT" - name: Write Tags to PR Comment uses: ./.github/actions/upsert-pr-comment diff --git a/.github/workflows/internal_on_push_ci.yml b/.github/workflows/internal_on_push_ci.yml index edd88a3..912c413 100644 --- a/.github/workflows/internal_on_push_ci.yml +++ b/.github/workflows/internal_on_push_ci.yml @@ -64,11 +64,21 @@ jobs: - name: Lint if: ${{ !cancelled() && steps.install.outcome == 'success' }} - run: npm run lint + run: npm run lint:js - name: Check formatting if: ${{ !cancelled() && steps.install.outcome == 'success' }} - run: npm run format + run: npm run format:prettier + + infra-ci: + name: Infra CI + needs: ci-context + if: ${{ needs.ci-context.outputs.should-run-ci == 'true' }} + uses: ./.github/workflows/run_infra_ci.yml + permissions: + contents: read # Required to check out the workflows to lint and audit. + with: + commit-identifier: ${{ needs.ci-context.outputs.commit-identifier }} semgrep: name: Semgrep @@ -87,7 +97,7 @@ jobs: ci-result: name: CI Result - needs: [ci-context, app-ci, semgrep] + needs: [ci-context, app-ci, infra-ci, semgrep] if: ${{ !cancelled() }} runs-on: ubuntu-26.04 permissions: {} diff --git a/.github/workflows/run_infra_ci.yml b/.github/workflows/run_infra_ci.yml new file mode 100644 index 0000000..d3d5c73 --- /dev/null +++ b/.github/workflows/run_infra_ci.yml @@ -0,0 +1,161 @@ +name: Infra CI + +on: + workflow_call: + inputs: + commit-identifier: + description: Commit SHA or ref to check; defaults to the ref that triggered the workflow + type: string + required: false + default: '' + +permissions: {} + +defaults: + run: + shell: bash + +env: + CONTRACT_DOCS: https://github.com/OpenSesame/core-github-actions/blob/main/.github/workflows/READMES/run_infra_ci.md + +jobs: + lint-gha: + name: Lint GHA + runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the workflows to lint. + + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.commit-identifier }} + persist-credentials: false + + - name: Check for mise.toml + run: | + if [[ ! -f mise.toml ]]; then + echo "::error title=Infra CI contract::mise.toml is missing. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Set up mise + uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0 + with: + version: 2026.9.17 + sha256: 63049bc35fb9065e8dc35ac8b25fdae53e9bd6f1885a843aedeba398e046a1ee + + - name: Check for the lint:gha task + run: | + if ! mise tasks info lint:gha >/dev/null 2>&1; then + echo "::error title=Infra CI contract::mise.toml has no lint:gha task. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Run actionlint + id: run + run: | + status=0 + # 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}}' \ + || status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + exit "$status" + + - name: Write job summary + if: ${{ !cancelled() && steps.run.outcome != 'skipped' }} + env: + STATUS: ${{ steps.run.outputs.status }} + run: | + config_file=.github/actionlint.yaml + if [[ ! -f "$config_file" ]]; then + entries=0 + elif command -v yq >/dev/null 2>&1; then + entries=$(yq '[.paths[]?.ignore[]?] | length' "$config_file") + else + entries="unknown (yq not installed)" + fi + + { + echo "## Lint GHA (actionlint)" + echo "" + if [[ "$STATUS" == "0" ]]; then + echo "✅ No findings." + else + echo "❌ Findings reported as annotations on this run." + fi + echo "" + echo "- **Suppressions** (\`$config_file\` \`paths.*.ignore\`): $entries" + echo "- **Run locally**: \`mise run lint:gha\`" + } >> "$GITHUB_STEP_SUMMARY" + + scan-gha: + name: Scan GHA + runs-on: ubuntu-26.04 + permissions: + contents: read # Required to check out the workflows to audit. + + steps: + - name: Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.commit-identifier }} + persist-credentials: false + + - name: Check for mise.toml + run: | + if [[ ! -f mise.toml ]]; then + echo "::error title=Infra CI contract::mise.toml is missing. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Set up mise + uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0 + with: + version: 2026.9.17 + sha256: 63049bc35fb9065e8dc35ac8b25fdae53e9bd6f1885a843aedeba398e046a1ee + + - name: Check for the scan:gha task + run: | + if ! mise tasks info scan:gha >/dev/null 2>&1; then + echo "::error title=Infra CI contract::mise.toml has no scan:gha task. See $CONTRACT_DOCS" + exit 1 + fi + + - name: Run zizmor + id: run + run: | + status=0 + mise run scan:gha -- --format=github || status=$? + echo "status=$status" >> "$GITHUB_OUTPUT" + exit "$status" + + - name: Write job summary + if: ${{ !cancelled() && steps.run.outcome != 'skipped' }} + env: + STATUS: ${{ steps.run.outputs.status }} + run: | + config_file=.github/zizmor.yml + if [[ ! -f "$config_file" ]]; then + entries=0 + elif command -v yq >/dev/null 2>&1; then + entries=$(yq '[.rules[]?.ignore[]?] | length' "$config_file") + else + entries="unknown (yq not installed)" + fi + inline=$(grep -rhoE 'zizmor: ?ignore\[' .github 2>/dev/null | wc -l | tr -d ' ') + + { + echo "## Scan GHA (zizmor)" + echo "" + if [[ "$STATUS" == "0" ]]; then + echo "✅ No findings." + else + echo "❌ Findings reported as annotations on this run." + fi + echo "" + echo "- **Suppressions** (\`$config_file\` \`rules.*.ignore\`): $entries" + echo "- **Inline ignores** (\`# zizmor: ignore[...]\` under \`.github\`): $inline" + echo "- **Run locally**: \`mise run scan:gha\` " + } >> "$GITHUB_STEP_SUMMARY" diff --git a/AGENTS.md b/AGENTS.md index b160423..b550362 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ the repository map, consumer guidance, and migration context. - Use Node.js 24.15.0 or newer in the Node.js 24 release line: `nvm use` - Install the pinned npm version: `npm install --global npm@12.1.0` - Install dependencies: `npm ci` +- Install the GitHub Actions lint and scan tools (needs [mise](https://mise.jdx.dev/)): `mise install` - Run all non-mutating checks: `npm run ci` - Run tests in watch mode: `npm run watch` - Fix lint issues: `npm run lint-fix` diff --git a/README.md b/README.md index bab8185..dc8e83f 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,9 @@ There are recommended vscode extensions and settings included in the project. 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 ci` and the pre-commit hook include both. - use Node.js 24.15.0 or newer in the Node.js 24 release line (`nvm use`) - install the pinned npm version `npm install --global npm@12.1.0` - install project dependencies `npm ci` diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000..14a52a2 --- /dev/null +++ b/mise.lock @@ -0,0 +1,52 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 3 + +[[tools."aqua:koalaman/shellcheck"]] +version = "0.11.0" +backend = "aqua:koalaman/shellcheck" +specifiers = ["0.11.0"] + +[tools."aqua:koalaman/shellcheck"."platforms.linux-x64"] +checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942" + +[tools."aqua:koalaman/shellcheck"."platforms.macos-arm64"] +checksum = "sha256:56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79" +url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz" +url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056932" + +[[tools."aqua:rhysd/actionlint"]] +version = "1.7.12" +backend = "aqua:rhysd/actionlint" +specifiers = ["1.7.12"] + +[tools."aqua:rhysd/actionlint"."platforms.linux-x64"] +checksum = "sha256:8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924896" +provenance = "github-attestations" + +[tools."aqua:rhysd/actionlint"."platforms.macos-arm64"] +checksum = "sha256:aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f" +url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924893" +provenance = "github-attestations" + +[[tools."aqua:zizmorcore/zizmor"]] +version = "1.29.0" +backend = "aqua:zizmorcore/zizmor" +specifiers = ["1.29.0"] + +[tools."aqua:zizmorcore/zizmor"."platforms.linux-x64"] +checksum = "sha256:dd96df044a6e8538d5f423790f453bdd03d49e5b2bcc38214acc41a2f1297839" +url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-x86_64-unknown-linux-gnu.tar.gz" +url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263145" +provenance = "github-attestations" + +[tools."aqua:zizmorcore/zizmor"."platforms.macos-arm64"] +checksum = "sha256:720322fade9e83a9c7953944c438f2ba942636b86b96a8f0e6b15ce94c8a6b6f" +url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-aarch64-apple-darwin.tar.gz" +url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263141" +provenance = "github-attestations" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..5a1e3d6 --- /dev/null +++ b/mise.toml @@ -0,0 +1,16 @@ +[tools] +"aqua:rhysd/actionlint" = "1.7.12" +"aqua:koalaman/shellcheck" = "0.11.0" +"aqua:zizmorcore/zizmor" = "1.29.0" + +[tasks."lint:gha"] +description = "Lint GitHub Actions workflows" +run = "actionlint" + +[tasks."scan:gha"] +description = "Audit GitHub Actions workflows for security issues" +run = "zizmor --offline .github" + +[tasks.infra-ci] +description = "Run every infra-ci check" +depends = ["lint:gha", "scan:gha"] diff --git a/package-lock.json b/package-lock.json index cb02310..f20f79b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "globals": "17.13.0", "husky": "9.1.7", "jest": "30.5.2", + "npm-run-all2": "9.0.3", "prettier": "3.9.9", "semgrep": "0.0.1" }, @@ -4837,6 +4838,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -5572,6 +5582,82 @@ "node": ">=0.10.0" } }, + "node_modules/npm-normalize-package-bin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-6.0.0.tgz", + "integrity": "sha512-tdt4aFn9QamlhdN3HV2D2ccpBwO5/fyjjbXUxYA6uBjyekMZcZvDq0aSj9t5Jo+tih6AYFnt/cuIRn9013e0Uw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/npm-run-all2": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-9.0.3.tgz", + "integrity": "sha512-BQAEdU1PtYc48qYRdghW2BVTQT3VqWCoFQmO87NlM1h1PYwMCKQpUWaNyB20V26caNzFsXQDfyOdznLlHCih6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^7.0.0", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^1.0.0", + "read-package-json-fast": "^6.0.0", + "shell-quote": "^1.8.4", + "which": "^7.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0", + "npm": ">= 10" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz", + "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/npm-run-all2/node_modules/which": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-7.0.0.tgz", + "integrity": "sha512-RancgH2dmbLdHl6LRhEqvklWMgl/Hdnun0Y90KhBOLkMefg8Qa7/Zel8Sm+8HEcP6DEjzsWzpkuBQEZok58isA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -5754,6 +5840,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-1.0.0.tgz", + "integrity": "sha512-avfAvjB9Dd0wdj3rjJX//yS+G79OO0KrS5pJHFJENjYGX6N4SMgEDBBI/yFy0lloOYSaC6XQxzpOAMPfSYFV/Q==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -5935,6 +6034,30 @@ "dev": true, "license": "MIT" }, + "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", + "integrity": "sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^6.0.0", + "npm-normalize-package-bin": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", + "integrity": "sha512-2/8adwnK1/+Fdjyts4r6wSpfANWw8zdNhU9U/Llk59c6O+DjSisPWPykwoL8gZmocP9Dy64S7oie2g+Mia123A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6008,6 +6131,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz", + "integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", diff --git a/package.json b/package.json index 7279345..5f38764 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "globals": "17.13.0", "husky": "9.1.7", "jest": "30.5.2", + "npm-run-all2": "9.0.3", "prettier": "3.9.9", "semgrep": "0.0.1" }, @@ -31,11 +32,18 @@ "test": "jest", "watch": "jest --watch", "audit": "npm audit --audit-level=high", - "lint": "eslint *.js --ext .js,.json", - "lint-fix": "eslint *.js --ext .js,.json --fix", - "format": "prettier --check './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", - "format-fix": "prettier --write './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", - "scan": "semgrep --config=p/ci --config=p/security-audit --config=p/javascript ./*.js ./*.mjs ./*.json scripts/ .github/actions/", - "ci": "npm run audit && npm run test && npm run lint && npm run format && npm run scan" + "lint": "npm-run-all --continue-on-error lint:*", + "lint:js": "eslint *.js --ext .js,.json", + "lint:gha": "mise run lint:gha", + "lint-fix": "npm-run-all --continue-on-error lint-fix:*", + "lint-fix:js": "eslint *.js --ext .js,.json --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:*", + "format-fix:prettier": "prettier --write './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", + "scan": "npm-run-all --continue-on-error scan:*", + "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", + "ci": "npm-run-all --continue-on-error audit test lint format scan" } }