diff --git a/.github/scripts/simulate-fresh-merge.sh b/.github/scripts/simulate-fresh-merge.sh new file mode 100755 index 00000000..ebc7ef76 --- /dev/null +++ b/.github/scripts/simulate-fresh-merge.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Fresh merge simulation. +# +# A pull-request run checks out GitHub's merge preview, refs/pull/N/merge. That +# ref is computed when the pull request is opened or synchronised, so a run that +# starts after main has moved can still be validating an old merge base: the +# checks pass, the merge lands, and main breaks on code no job ever saw +# together. Merging the current base branch into the checkout before the checks +# run removes that window, and a merge conflict fails the job with a clear +# message instead of surfacing as a conflict at merge time. +# +# This is principle #7 ("Validate the actual merge result") of +# https://github.com/link-assistant/hive-mind/blob/main/docs/CI-CD-BEST-PRACTICES.md +# +# The merge is local to the runner: nothing is pushed, and the jobs that call +# this check out with persist-credentials: false. +# +# Requirements: the calling job must check out with `fetch-depth: 0`, otherwise +# the shallow clone has no merge base to work from. +# +# Environment: +# BASE_REF - the base branch to merge in (default: main). In GitHub Actions +# this is github.base_ref, which is set only for pull_request +# events; the calling step is guarded accordingly. +# +# Usage (locally, on a branch): +# BASE_REF=main bash .github/scripts/simulate-fresh-merge.sh +set -euo pipefail + +BASE_REF="${BASE_REF:-main}" + +# An identity is required for `git merge` to be able to write a merge commit. +# The 41898282+ prefix is the one that attributes a commit to github-actions[bot]; +# the commit never leaves the runner, but using the right identity keeps it out +# of the "unattributed" bucket if it ever does. +git config user.email '41898282+github-actions[bot]@users.noreply.github.com' +git config user.name 'github-actions[bot]' + +git fetch --no-tags origin "${BASE_REF}" + +behind="$(git rev-list --count "HEAD..origin/${BASE_REF}")" +if [ "${behind}" -eq 0 ]; then + echo "Merge preview already contains every commit on ${BASE_REF}; nothing to simulate." + exit 0 +fi + +echo "${BASE_REF} has ${behind} commit(s) that the merge preview does not contain." +echo "Merging origin/${BASE_REF} so the checks below run against the real merge result." + +if ! git merge "origin/${BASE_REF}" --no-edit; then + echo "::error::Merge conflict with ${BASE_REF}. Update this branch before it can be merged." + exit 1 +fi + +echo "Fresh merge succeeded; the checks below run against the merged tree." diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 8f8260f9..064f0535 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -6,6 +6,12 @@ on: - main paths: - 'js/**' + # ESLint runs from the repository root, so it also lints these; without + # them here a lint error in an experiment or in claude-profiles.mjs first + # surfaced on an unrelated pull request that happened to touch js/**. + - 'eslint.config.js' + - 'claude-profiles.mjs' + - 'experiments/**' - '.github/workflows/js.yml' - 'README.md' - 'LICENSE' @@ -13,6 +19,12 @@ on: types: [opened, synchronize, reopened] paths: - 'js/**' + # ESLint runs from the repository root, so it also lints these; without + # them here a lint error in an experiment or in claude-profiles.mjs first + # surfaced on an unrelated pull request that happened to touch js/**. + - 'eslint.config.js' + - 'claude-profiles.mjs' + - 'experiments/**' - '.github/workflows/js.yml' - 'README.md' - 'LICENSE' @@ -39,9 +51,15 @@ on: required: false type: string -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +# Least-privilege default. The jobs that write (release, instant-release, +# changeset-pr) raise their own scopes. +permissions: + contents: read + +# Concurrency is declared per job, not here: a workflow-level cancellable group +# would also cancel a release that has already begun publishing to npm. Read-only +# checks use cancellable `check-*` groups, writers share one repository-wide +# non-cancellable group so a started writer always finishes. jobs: changeset-check: @@ -49,13 +67,17 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 if: github.event_name == 'pull_request' + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-changeset-check + cancel-in-progress: true steps: - uses: actions/checkout@v6 with: fetch-depth: 0 + persist-credentials: false - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest @@ -69,8 +91,12 @@ jobs: GITHUB_BASE_REF: ${{ github.base_ref }} GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + # Read through the environment rather than expanded into the script: + # a branch name is attacker-controlled on a fork PR, and the expanded + # form also made shellcheck compare two literals (SC2193). + HEAD_REF: ${{ github.head_ref }} run: | - if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then + if [[ "$HEAD_REF" == changeset-release/* ]]; then echo "Skipping changeset check for automated release PR" exit 0 fi @@ -82,12 +108,31 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 needs: [changeset-check] - if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success') + # `!cancelled()` rather than `always()`: a cancelled changeset-check must not + # leave this job running. + if: ${{ !cancelled() && (github.event_name == 'push' || needs.changeset-check.result == 'success') }} + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-lint + cancel-in-progress: true steps: - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest @@ -108,11 +153,19 @@ jobs: run: bun run check:duplication test: - name: Test JavaScript (${{ matrix.runtime }} on ${{ matrix.os }}) + # The Node entries differ only by version, so the version has to be part of + # the name; without it three jobs shared the name "Test JavaScript (node on + # ubuntu-latest)" and a required-check rule could not name a single one. + name: Test JavaScript (${{ matrix.runtime }}${{ matrix.node-version && format(' {0}', matrix.node-version) || '' }} on ${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 30 needs: [changeset-check] - if: always() && (github.event_name == 'push' || needs.changeset-check.result == 'success') + if: ${{ !cancelled() && (github.event_name == 'push' || needs.changeset-check.result == 'success') }} + # The matrix values are part of the group so the entries stay parallel and + # only a superseded run of the *same* entry is cancelled. + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-test-${{ matrix.os }}-${{ matrix.runtime }}-${{ matrix.node-version || 'default' }} + cancel-in-progress: true strategy: fail-fast: false matrix: @@ -130,10 +183,24 @@ jobs: node-version: 24 steps: - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh - name: Setup Bun if: matrix.runtime == 'bun' - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest @@ -184,7 +251,7 @@ jobs: run: | node -e " import('./js/src/\$.mjs') - .then(() => console.log('Module loads successfully in Node.js ${{ matrix.node-version }}')) + .then(() => console.log('Module loads successfully in Node.js ' + process.version)) .catch((error) => { console.error('Module failed to load:', error.message); process.exit(1); @@ -196,7 +263,7 @@ jobs: console.error('CommonJS entry did not export a callable \$'); process.exit(1); } - console.log('CommonJS entry loads successfully in Node.js ${{ matrix.node-version }}'); + console.log('CommonJS entry loads successfully in Node.js ' + process.version); " node --test js/tests/node-terminal-artifacts.mjs node --test js/tests/node-commonjs-entry.mjs @@ -207,7 +274,7 @@ jobs: # Required because lint/test depend on the pull-request-only changeset-check # job, which is skipped on push events. if: | - always() && !cancelled() && + !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && @@ -218,8 +285,19 @@ jobs: contents: write pull-requests: write id-token: write + # Shared by every job in this repository that pushes to main or publishes. + # `cancel-in-progress: false` lets a started release finish; the next writer + # waits instead of interrupting a half-published version. + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false steps: - - uses: actions/checkout@v6 + # This job pushes to main and creates the release, so it needs the + # credential actions/checkout persists in .git/config. Everything else in + # this repository sets persist-credentials: false; zizmor runs at + # --min-confidence low so a checkout that forgets it fails the audit, and + # the six writer jobs that legitimately need it say so here. + - uses: actions/checkout@v6 # zizmor: ignore[artipacked] with: fetch-depth: 0 @@ -230,7 +308,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest @@ -248,8 +326,8 @@ jobs: run: | CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l) echo "Found $CHANGESET_COUNT JavaScript changeset file(s)" - echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT - echo "changeset_count=$CHANGESET_COUNT" >> $GITHUB_OUTPUT + echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT" + echo "changeset_count=$CHANGESET_COUNT" >> "$GITHUB_OUTPUT" - name: Check if release is needed id: check_release @@ -290,21 +368,28 @@ jobs: working-directory: js env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bun scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix js-v + PUBLISHED_VERSION: ${{ steps.publish.outputs.published_version }} + REPOSITORY: ${{ github.repository }} + run: bun scripts/create-github-release.mjs --release-version "$PUBLISHED_VERSION" --repository "$REPOSITORY" --tag-prefix js-v - name: Format JavaScript GitHub release notes if: steps.publish.outputs.published == 'true' working-directory: js env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bun scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix js-v + PUBLISHED_VERSION: ${{ steps.publish.outputs.published_version }} + REPOSITORY: ${{ github.repository }} + COMMIT_SHA: ${{ github.sha }} + run: bun scripts/format-github-release.mjs --release-version "$PUBLISHED_VERSION" --repository "$REPOSITORY" --commit-sha "$COMMIT_SHA" --tag-prefix js-v - name: Verify npm availability # Guards against the #166 false positive: a release/tag must correspond # to a version that is actually installable from npm. if: steps.publish.outputs.published == 'true' working-directory: js - run: bun scripts/wait-for-npm.mjs --release-version "${{ steps.publish.outputs.published_version }}" + env: + PUBLISHED_VERSION: ${{ steps.publish.outputs.published_version }} + run: bun scripts/wait-for-npm.mjs --release-version "$PUBLISHED_VERSION" instant-release: name: Instant JavaScript release @@ -315,8 +400,16 @@ jobs: contents: write pull-requests: write id-token: write + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false steps: - - uses: actions/checkout@v6 + # This job pushes to main and creates the release, so it needs the + # credential actions/checkout persists in .git/config. Everything else in + # this repository sets persist-credentials: false; zizmor runs at + # --min-confidence low so a checkout that forgets it fails the audit, and + # the six writer jobs that legitimately need it say so here. + - uses: actions/checkout@v6 # zizmor: ignore[artipacked] with: fetch-depth: 0 @@ -327,7 +420,7 @@ jobs: registry-url: 'https://registry.npmjs.org' - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest @@ -342,7 +435,10 @@ jobs: - name: Version package and commit to main id: version working-directory: js - run: bun scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + env: + BUMP_TYPE: ${{ github.event.inputs.bump_type }} + RELEASE_DESCRIPTION: ${{ github.event.inputs.description }} + run: bun scripts/version-and-commit.mjs --mode instant --bump-type "$BUMP_TYPE" --description "$RELEASE_DESCRIPTION" - name: Publish to npm if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' @@ -355,21 +451,28 @@ jobs: working-directory: js env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bun scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix js-v + PUBLISHED_VERSION: ${{ steps.publish.outputs.published_version }} + REPOSITORY: ${{ github.repository }} + run: bun scripts/create-github-release.mjs --release-version "$PUBLISHED_VERSION" --repository "$REPOSITORY" --tag-prefix js-v - name: Format JavaScript GitHub release notes if: steps.publish.outputs.published == 'true' working-directory: js env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: bun scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix js-v + PUBLISHED_VERSION: ${{ steps.publish.outputs.published_version }} + REPOSITORY: ${{ github.repository }} + COMMIT_SHA: ${{ github.sha }} + run: bun scripts/format-github-release.mjs --release-version "$PUBLISHED_VERSION" --repository "$REPOSITORY" --commit-sha "$COMMIT_SHA" --tag-prefix js-v - name: Verify npm availability # Guards against the #166 false positive: a release/tag must correspond # to a version that is actually installable from npm. if: steps.publish.outputs.published == 'true' working-directory: js - run: bun scripts/wait-for-npm.mjs --release-version "${{ steps.publish.outputs.published_version }}" + env: + PUBLISHED_VERSION: ${{ steps.publish.outputs.published_version }} + run: bun scripts/wait-for-npm.mjs --release-version "$PUBLISHED_VERSION" changeset-pr: name: Create JavaScript changeset PR @@ -379,13 +482,21 @@ jobs: permissions: contents: write pull-requests: write + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false steps: - - uses: actions/checkout@v6 + # This job pushes to main and creates the release, so it needs the + # credential actions/checkout persists in .git/config. Everything else in + # this repository sets persist-credentials: false; zizmor runs at + # --min-confidence low so a checkout that forgets it fails the audit, and + # the six writer jobs that legitimately need it say so here. + - uses: actions/checkout@v6 # zizmor: ignore[artipacked] with: fetch-depth: 0 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: latest @@ -395,14 +506,17 @@ jobs: - name: Create changeset file working-directory: js - run: bun scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + env: + BUMP_TYPE: ${{ github.event.inputs.bump_type }} + RELEASE_DESCRIPTION: ${{ github.event.inputs.description }} + run: bun scripts/create-manual-changeset.mjs --bump-type "$BUMP_TYPE" --description "$RELEASE_DESCRIPTION" - name: Format changeset with Prettier working-directory: js run: bunx prettier --write ".changeset/*.md" - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: 'chore: add changeset for manual JavaScript ${{ github.event.inputs.bump_type }} release' diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 00000000..cd4e509a --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,68 @@ +name: Link check + +# External link rot is checked here, on a schedule, and deliberately not on +# pull requests. +# +# The pipeline templates run lychee as a pull-request gate. Doing the same here +# would have made every pull request red for reasons no pull request caused: +# a run over this tree reports 20 errors, and all 20 are links that are correct +# in the document and unreachable from a runner -- npmjs.com answers 403 to any +# non-browser client, and GitHub serves the stargazers list and the settings +# pages only to a signed-in session. That is exactly the class of false positive +# issue #199 is about, so the split is: +# +# - relative links, which a change can actually break, are resolved offline on +# every pull request by js/tests/docs-validation.test.mjs (quality.yml), +# - external links, which only the rest of the world can break, are fetched +# here once a week and on demand. +# +# A failure of this workflow therefore never blocks a merge; it says a link that +# used to work has stopped working. Known-unreachable-but-correct URLs go in +# .lycheeignore, one commented entry each. + +on: + schedule: + # Mondays at 06:00 UTC, an hour after the weekly security audit. + - cron: '0 6 * * 1' + workflow_dispatch: + +# Least-privilege default; nothing here writes. +permissions: + contents: read + +jobs: + links: + name: Check external links + runs-on: ubuntu-latest + # A run over this repository takes seconds; the budget is for a slow or + # unresponsive third-party host. + timeout-minutes: 15 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-links + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + # dev/log and docs/case-studies hold verbatim copies of other + # repositories' documents, kept as evidence: their relative links point + # into the tree they came from and are not this repository's to fix. The + # same directories are skipped by js/tests/docs-validation.test.mjs. + - name: Check links with lychee + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --verbose + --no-progress + --max-retries 2 + --timeout 30 + --exclude-path dev/log + --exclude-path docs/case-studies + './**/*.md' + fail: true + jobSummary: true + env: + # Raises GitHub's anonymous rate limit, which this repository's + # documents would otherwise hit on their own issue and pull links. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/parity.yml b/.github/workflows/parity.yml index 5d1a136d..121cf59a 100644 --- a/.github/workflows/parity.yml +++ b/.github/workflows/parity.yml @@ -12,10 +12,6 @@ on: pull_request: types: [opened, synchronize, reopened, labeled, unlabeled] -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - permissions: contents: read @@ -26,10 +22,15 @@ jobs: timeout-minutes: 10 # Skip entirely when the PR is explicitly marked as a single-language change. if: ${{ !contains(github.event.pull_request.labels.*.name, 'parity-exempt') }} + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-parity + cancel-in-progress: true steps: - uses: actions/checkout@v6 with: fetch-depth: 0 + # Read-only job: the parity script only diffs the checked-out history. + persist-credentials: false - name: Check JavaScript/Rust source parity env: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000..78dbfa46 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,134 @@ +name: Repository quality checks + +# The checks in here apply to the whole repository, so they deliberately carry +# no `paths:` filter. +# +# js.yml only runs on `js/**`, rust.yml on `rust/**` and workflows.yml on +# `.github/**`, which left whole classes of change with no check at all +# (issue #199): a pull request that only touched `docs/**` ran nothing, and the +# formatter -- which reads every tracked file, including the workflows and the +# markdown -- only ever ran behind the `js/**` filter, so a formatting violation +# introduced in a workflow file first turned red on somebody else's JavaScript +# pull request. + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +# Least-privilege default; nothing here writes. +permissions: + contents: read + +# Concurrency is per job, matching the other workflows. Every job here is +# read-only, so all of them are cancellable `check-*` groups. + +jobs: + format: + name: Check formatting of every tracked file + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-format + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + # Prettier lives in js/package.json but formats the whole tree: the + # `format:check` script steps out of js/ to run it from the repository + # root, where .prettierrc and .prettierignore are. + - name: Install dependencies + working-directory: js + run: bun install + + - name: Check formatting + working-directory: js + run: bun run format:check + + docs: + name: Validate documentation + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-docs + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history, and the + # test itself lists the tracked markdown files with `git ls-files`. + fetch-depth: 0 + persist-credentials: false + + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + # No `bun install`: the test reads the tree with the standard library and + # git only. Documentation validation is principle #12 of the hive-mind + # CI/CD best practices; the merge simulation matters here because a link + # breaks when *either* side of the merge moves the file. + - name: Check documentation links, size and required sections + run: bun test js/tests/docs-validation.test.mjs --timeout 10000 + + hygiene: + name: Check workflow invariants + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-hygiene + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + # actionlint and zizmor cover syntax and workflow security; this test + # covers the repository-specific invariants they cannot know about + # (concurrency shape, writer jobs, matrix job names, which quality gates + # are wired up). It ran only behind js.yml's `js/**` filter before, which + # is the one filter guaranteed not to match a workflow-only change. + - name: Check CI/CD invariants + run: bun test js/tests/workflow-hygiene.test.mjs --timeout 10000 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d4dbf15e..fc9c56e4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,28 +39,45 @@ on: required: false type: string -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +# Least-privilege default. The jobs that write (release, instant-release, +# changelog-pr) raise their own scopes. +permissions: + contents: read + +# Concurrency is declared per job, not here: a workflow-level cancellable group +# would also cancel a release that has already begun publishing to crates.io. +# Read-only checks use cancellable `check-*` groups, writers share one +# repository-wide non-cancellable group so a started writer always finishes. env: CARGO_TERM_COLOR: always - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} - CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + # Warnings must fail the build. Without this, `cargo clippy` reports problems + # and still exits 0, so every Rust check was a false negative (issue #199). + # Registry dependencies are compiled with `--cap-lints allow`, so this only + # denies warnings in this crate's own code. + RUSTFLAGS: -Dwarnings + RUSTDOCFLAGS: -Dwarnings + # The crates.io credentials are deliberately *not* here: a workflow-level env + # hands them to every job, including the ones that only run `cargo test` on + # a pull request. They are declared on the two publishing jobs instead. jobs: changelog: - name: Rust changelog fragment check + name: Rust changelog and version checks runs-on: ubuntu-latest timeout-minutes: 10 if: github.event_name == 'pull_request' + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-changelog + cancel-in-progress: true steps: - uses: actions/checkout@v6 with: fetch-depth: 0 + persist-credentials: false - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Install rust-script run: cargo install rust-script @@ -70,17 +87,45 @@ jobs: GITHUB_BASE_REF: ${{ github.base_ref }} run: rust-script rust/scripts/check-changelog-fragment.rs + - name: Check for manual version changes + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + # rust/Cargo.toml's version field is written by the release job from the + # changelog fragments. A hand-edited version in a pull request silently + # fights that and republishes or skips a version. The script shipped with + # the pipeline but no workflow ran it, so the guard was dead (issue #199). + run: rust-script rust/scripts/check-version-modification.rs + lint: name: Lint and format Rust runs-on: ubuntu-latest timeout-minutes: 10 needs: [changelog] - if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') + # `!cancelled()` rather than `always()`: a cancelled changelog check must not + # leave this job running. + if: ${{ !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') }} + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-lint + cancel-in-progress: true steps: - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 with: components: rustfmt, clippy @@ -101,23 +146,51 @@ jobs: - name: Run Clippy working-directory: rust - run: cargo clippy --all-targets --all-features + # Explicit despite the workflow-level RUSTFLAGS: clippy's own lints are + # not covered by RUSTFLAGS, only rustc's. + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Build documentation + working-directory: rust + # rustdoc-only lints (broken intra-doc links, unclosed HTML tags) are + # reported by neither clippy nor `cargo test --doc`, so they need their + # own gate. RUSTDOCFLAGS=-Dwarnings comes from the workflow env. + run: cargo doc --no-deps --all-features test: name: Test Rust (${{ matrix.os }}) runs-on: ${{ matrix.os }} timeout-minutes: 30 needs: [changelog] - if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') + if: ${{ !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') }} + # The matrix value is part of the group so the per-OS entries stay parallel + # and only a superseded run of the *same* entry is cancelled. + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-test-${{ matrix.os }} + cancel-in-progress: true strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Cache cargo registry uses: actions/cache@v5 @@ -139,16 +212,33 @@ jobs: run: cargo test --doc --all-features --verbose scripts: - name: Test Rust release scripts + name: Test Rust scripts and check sizes runs-on: ubuntu-latest timeout-minutes: 15 needs: [changelog] - if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') + if: ${{ !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') }} + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-scripts + cancel-in-progress: true steps: - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Cache cargo registry uses: actions/cache@v5 @@ -181,17 +271,49 @@ jobs: done exit $status + - name: Check Rust file sizes + # Best practice: no source file over ~1000 lines. The JavaScript side + # enforces this through eslint's `max-lines`; the Rust side shipped + # check-file-size.rs but nothing invoked it (issue #199). Run from the + # repository root so rust/src, rust/tests and rust/scripts are all in + # scope. Files in the 900-1000 band are reported as warnings only. + run: rust-script rust/scripts/check-file-size.rs + + - name: Check packaged crate size + # crates.io rejects uploads over 10 MiB with an HTTP 413 during publish, + # i.e. after the release commit and tag already exist. This packages the + # crate with --no-verify (the build job does the verifying package run) + # and fails early instead. Also previously dead (issue #199). + run: rust-script rust/scripts/check-crate-size.rs + build: name: Build Rust package runs-on: ubuntu-latest timeout-minutes: 10 needs: [lint, test] - if: always() && !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' + if: ${{ !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' }} + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-build + cancel-in-progress: true steps: - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Cache cargo registry uses: actions/cache@v5 @@ -218,7 +340,7 @@ jobs: # Required because lint/test depend on the pull-request-only changelog job, # which is skipped on push events. if: | - always() && !cancelled() && + !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' && needs.lint.result == 'success' && @@ -229,13 +351,27 @@ jobs: timeout-minutes: 30 permissions: contents: write + # Shared by every job in this repository that pushes to main or publishes. + # `cancel-in-progress: false` lets a started release finish; the next writer + # waits instead of interrupting a half-published version. + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} steps: - - uses: actions/checkout@v6 + # This job pushes to main and creates the release, so it needs the + # credential actions/checkout persists in .git/config. Everything else in + # this repository sets persist-credentials: false; zizmor runs at + # --min-confidence low so a checkout that forgets it fails the audit, and + # the six writer jobs that legitimately need it say so here. + - uses: actions/checkout@v6 # zizmor: ignore[artipacked] with: fetch-depth: 0 - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Install rust-script run: cargo install rust-script @@ -255,7 +391,9 @@ jobs: - name: Version Rust crate and commit to main if: steps.release_needed.outputs.should_release == 'true' id: version - run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ steps.bump.outputs.bump_type }}" --tag-prefix rust-v --release-label Rust + env: + BUMP_TYPE: ${{ steps.bump.outputs.bump_type }} + run: rust-script rust/scripts/version-and-commit.rs --bump-type "$BUMP_TYPE" --tag-prefix rust-v --release-label Rust - name: Read Rust release version if: steps.release_needed.outputs.should_release == 'true' @@ -271,11 +409,15 @@ jobs: if: steps.publish_crate.outputs.publish_result == 'success' || steps.publish_crate.outputs.publish_result == 'already_exists' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: rust-script rust/scripts/create-github-release.rs --release-version "${{ steps.version.outputs.new_version || steps.current_version.outputs.version }}" --repository "${{ github.repository }}" --tag-prefix rust-v --language Rust --release-label Rust + RELEASE_VERSION: ${{ steps.version.outputs.new_version || steps.current_version.outputs.version }} + REPOSITORY: ${{ github.repository }} + run: rust-script rust/scripts/create-github-release.rs --release-version "$RELEASE_VERSION" --repository "$REPOSITORY" --tag-prefix rust-v --language Rust --release-label Rust - name: Wait for crate availability if: steps.publish_crate.outputs.publish_result == 'success' - run: rust-script rust/scripts/wait-for-crate.rs --version "${{ steps.version.outputs.new_version || steps.current_version.outputs.version }}" + env: + RELEASE_VERSION: ${{ steps.version.outputs.new_version || steps.current_version.outputs.version }} + run: rust-script rust/scripts/wait-for-crate.rs --version "$RELEASE_VERSION" instant-release: name: Instant Rust release @@ -284,20 +426,34 @@ jobs: timeout-minutes: 30 permissions: contents: write + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} steps: - - uses: actions/checkout@v6 + # This job pushes to main and creates the release, so it needs the + # credential actions/checkout persists in .git/config. Everything else in + # this repository sets persist-credentials: false; zizmor runs at + # --min-confidence low so a checkout that forgets it fails the audit, and + # the six writer jobs that legitimately need it say so here. + - uses: actions/checkout@v6 # zizmor: ignore[artipacked] with: fetch-depth: 0 - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Install rust-script run: cargo install rust-script - name: Version Rust crate and commit to main id: version - run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" --tag-prefix rust-v --release-label Rust + env: + BUMP_TYPE: ${{ github.event.inputs.bump_type }} + RELEASE_DESCRIPTION: ${{ github.event.inputs.description }} + run: rust-script rust/scripts/version-and-commit.rs --bump-type "$BUMP_TYPE" --description "$RELEASE_DESCRIPTION" --tag-prefix rust-v --release-label Rust - name: Publish to crates.io if: steps.version.outputs.version_committed == 'true' @@ -308,11 +464,15 @@ jobs: if: steps.publish_crate.outputs.publish_result == 'success' || steps.publish_crate.outputs.publish_result == 'already_exists' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: rust-script rust/scripts/create-github-release.rs --release-version "${{ steps.version.outputs.new_version }}" --repository "${{ github.repository }}" --tag-prefix rust-v --language Rust --release-label Rust + RELEASE_VERSION: ${{ steps.version.outputs.new_version }} + REPOSITORY: ${{ github.repository }} + run: rust-script rust/scripts/create-github-release.rs --release-version "$RELEASE_VERSION" --repository "$REPOSITORY" --tag-prefix rust-v --language Rust --release-label Rust - name: Wait for crate availability if: steps.publish_crate.outputs.publish_result == 'success' - run: rust-script rust/scripts/wait-for-crate.rs --version "${{ steps.version.outputs.new_version }}" + env: + RELEASE_VERSION: ${{ steps.version.outputs.new_version }} + run: rust-script rust/scripts/wait-for-crate.rs --version "$RELEASE_VERSION" changelog-pr: name: Create Rust changelog PR @@ -322,22 +482,33 @@ jobs: permissions: contents: write pull-requests: write + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false steps: - - uses: actions/checkout@v6 + # This job pushes to main and creates the release, so it needs the + # credential actions/checkout persists in .git/config. Everything else in + # this repository sets persist-credentials: false; zizmor runs at + # --min-confidence low so a checkout that forgets it fails the audit, and + # the six writer jobs that legitimately need it say so here. + - uses: actions/checkout@v6 # zizmor: ignore[artipacked] with: fetch-depth: 0 - name: Setup Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03 - name: Install rust-script run: cargo install rust-script - name: Create changelog fragment - run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + env: + BUMP_TYPE: ${{ github.event.inputs.bump_type }} + RELEASE_DESCRIPTION: ${{ github.event.inputs.description }} + run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "$BUMP_TYPE" --description "$RELEASE_DESCRIPTION" - name: Create Pull Request - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: 'chore: add changelog fragment for manual Rust ${{ github.event.inputs.bump_type }} release' diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..3c8a0fdd --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,273 @@ +name: Security + +# Nothing in this repository read its own dependency tree for known +# vulnerabilities, and no static analysis ran over the sources at all, so a +# fully green build said nothing about either (issue #199). The audits below are +# the two pipeline templates' security workflows merged, because this repository +# ships both a JavaScript package and a Rust crate from the same tree: +# link-foundation/js-ai-driven-development-pipeline-template and +# link-foundation/rust-ai-driven-development-pipeline-template. + +on: + push: + branches: [main] + pull_request: + # Advisories are published against code that has not changed, so a + # change-triggered audit alone goes stale between releases. + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +# Least-privilege default; the jobs that need more raise it themselves. +permissions: + contents: read + +# Concurrency is per job, matching js.yml and rust.yml. Every job here is +# read-only, so all of them are cancellable `check-*` groups. + +jobs: + codeql: + name: CodeQL (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-codeql-${{ matrix.language }} + cancel-in-progress: true + permissions: + # Reading the workflow run is what lets CodeQL attribute results to it. + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + # `actions` analyses the workflows in this directory, which is where the + # findings fixed for #199 lived. + language: [javascript-typescript, rust, actions] + steps: + # No fresh-merge simulation here, unlike the other tree-reading jobs (see + # .github/scripts/simulate-fresh-merge.sh). The upload step keys the + # results to the commit it finds in the checkout, and GitHub rejects + # results for a commit it has never seen; a merge commit created on the + # runner is exactly that. CodeQL therefore analyses the merge preview. + - uses: actions/checkout@v6 + with: + # Analysis reads the tree; it never pushes. + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # All three languages here are extracted without compiling, so there + # is no autobuild step: asking CodeQL to build the crate would spend + # several minutes reproducing what the rust workflow already does. + build-mode: none + + - name: Analyze + uses: github/codeql-action/analyze@v4 + + dependency-review: + name: Dependency Review + # Reviews the diff of a pull request; there is no diff to review on push. + if: ${{ github.event_name == 'pull_request' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-dependency-review + cancel-in-progress: true + permissions: + contents: read + # Only used to post the summary comment below, never to push. + pull-requests: write + steps: + # No fresh-merge simulation here either: this job never reads the working + # tree. It asks the API to compare two commit SHAs, so merging the base + # branch into the checkout would change nothing it looks at. + - uses: actions/checkout@v6 + with: + persist-credentials: false + + # actions/dependency-review-action fails with "Dependency review is not + # supported on this repository" when the dependency graph is off, which is + # the state this repository is in: the graph is disabled at the + # organisation level and cannot be turned on from a workflow. Shipping a + # check that can only ever be red trains reviewers to ignore red, so probe + # for the feature and skip with a warning when it is missing. Any other + # HTTP status is still a hard failure, and the job starts reviewing by + # itself the moment an admin enables the graph under + # Settings -> Code security -> Dependency graph. + - name: Check whether the dependency graph is enabled + id: graph + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -uo pipefail + code="$(curl -sS -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "$GITHUB_API_URL/repos/$REPOSITORY/dependency-graph/compare/$BASE_SHA...$HEAD_SHA")" + echo "Dependency graph compare endpoint answered HTTP $code" + if [ "$code" = '200' ]; then + echo 'enabled=true' >> "$GITHUB_OUTPUT" + elif [ "$code" = '403' ]; then + echo 'enabled=false' >> "$GITHUB_OUTPUT" + echo "::warning::Dependency graph is disabled for $REPOSITORY, so dependency review cannot run. The npm, bun and cargo audit jobs still check the committed lockfiles. Enable the graph under Settings -> Code security to restore per-pull-request review." + else + echo "::error::Unexpected HTTP $code from the dependency-graph compare endpoint" + exit 1 + fi + + - name: Review dependency changes + if: ${{ steps.graph.outputs.enabled == 'true' }} + uses: actions/dependency-review-action@v5 + with: + fail-on-severity: high + comment-summary-in-pr: on-failure + + npm-audit: + name: Audit npm lockfile + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-npm-audit + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + - uses: actions/setup-node@v6 + with: + node-version: 24 + + # --package-lock-only audits what is committed, without installing it, so + # the result describes the lockfile a consumer would resolve rather than + # whatever the runner happened to fetch. + - name: Audit the committed package-lock.json + working-directory: js + run: npm audit --package-lock-only --audit-level=high + + bun-audit: + name: Audit bun lockfile + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-bun-audit + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: latest + + # bun.lock resolves versions independently of package-lock.json, so a + # clean npm audit does not imply a clean bun one: while fixing #199 the + # two lockfiles disagreed by 8 high-severity advisories. + - name: Audit the committed bun.lock + working-directory: js + run: bun audit --audit-level=high + + cargo-audit: + name: Audit Cargo lockfile + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-cargo-audit + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + - uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4 + with: + tool: cargo-audit@0.22.2 + + - name: Audit the committed Cargo.lock + working-directory: rust + run: cargo audit --file Cargo.lock + + secret-scan: + name: Scan for committed secrets + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-secret-scan + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + # Nothing checked whether a credential had been committed. CodeQL does not + # look for them and the audit jobs only read lockfiles, so a token pasted + # into a script, a fixture or a case study would have reached main with a + # green run (issue #199, best practice #11). + # + # Reproduce locally with the same command, and see + # experiments/secretlint-scope.sh for the probe that shows the glob does + # reach dot-directories such as .github/. Files that are generated rather + # than authored here are listed in .secretlintignore; the rule set is in + # .secretlintrc.json. The versions are pinned so a new rule release cannot + # turn an unrelated pull request red -- bump them deliberately. + - name: Run secretlint over the working tree + run: | + npx --yes \ + -p secretlint@13.0.5 \ + -p @secretlint/secretlint-rule-preset-recommend@13.0.5 \ + secretlint "**/*" diff --git a/.github/workflows/workflows.yml b/.github/workflows/workflows.yml new file mode 100644 index 00000000..d7698af4 --- /dev/null +++ b/.github/workflows/workflows.yml @@ -0,0 +1,106 @@ +name: Workflows + +# Lints the workflows themselves. Nothing else in CI reads .github/workflows/**, +# so before this existed a broken `if:` condition, an unquoted variable in a +# `run:` block or an unpinned third-party action reached main unchallenged. + +on: + push: + branches: [main] + paths: ['.github/**'] + pull_request: + paths: ['.github/**'] + workflow_dispatch: + +# Keep the workflow read-only unless a job explicitly needs an additional scope. +permissions: + contents: read + +jobs: + actionlint: + name: Actionlint + runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-actionlint + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + # The Docker image bundles shellcheck and pyflakes, so this also lints + # every `run:` block. A *native* actionlint binary without shellcheck on + # PATH silently skips the shell checks and exits 0. Reproduce locally with + # the same image: + # docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.7 -color + - uses: docker://rhysd/actionlint:1.7.7 + with: + args: -color + + zizmor: + name: Zizmor + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-zizmor + cancel-in-progress: true + steps: + - uses: actions/checkout@v6 + with: + # The merge simulation below needs the base branch's history. + fetch-depth: 0 + persist-credentials: false + + # Run the checks below against the real merge result rather than a + # possibly stale merge preview, and fail on a conflict here rather than at + # merge time. Rationale in .github/scripts/simulate-fresh-merge.sh. + - name: Simulate a fresh merge with the base branch + if: github.event_name == 'pull_request' + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/simulate-fresh-merge.sh + + # actionlint cannot see workflow *security* defects: unpinned third-party + # actions, checkouts that persist `GITHUB_TOKEN` in `.git/config`, or + # template injection into `run:` blocks. zizmor covers those. Reproduce + # locally with the same configuration: + # pipx run zizmor --config .github/zizmor.yml \ + # --min-confidence medium --persona regular .github/workflows + # + # Annotations instead of SARIF: code scanning is not enabled on forks, and + # the job should fail loudly either way. + - uses: zizmorcore/zizmor-action@v0.6.2 + with: + advanced-security: false + annotations: true + config: .github/zizmor.yml + # Low, not medium: `artipacked` is a Low-confidence audit, so + # `medium` hides every checkout that persists credentials -- the exact + # blind spot reported upstream as js-template#160. At `low` the six + # writer jobs that need the credential carry an + # inline artipacked suppression, and everything else has to set + # persist-credentials: false. + min-confidence: low + # Audit the workflows this repository runs, not every workflow file + # in the tree. The action's default input is `.`, which also collects + # docs/case-studies/**/templates/** -- verbatim archived copies of + # other repositories' workflows, kept as evidence and deliberately not + # edited. Auditing those reported 30 findings in files that never run + # here and that a fix would falsify. + inputs: .github/workflows diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..3e7d7577 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,21 @@ +# zizmor configuration — https://docs.zizmor.sh/configuration/ +# +# Mirrors the policy used by the pipeline templates +# (link-foundation/js-ai-driven-development-pipeline-template and +# link-foundation/rust-ai-driven-development-pipeline-template) so that a +# workflow copied from either side keeps passing here. +rules: + unpinned-uses: + config: + policies: + # These publishers are trusted at tag granularity: their release tags + # are the reference this repository is meant to read at a glance. + # Everything else must be pinned to a full commit hash. + actions/*: ref-pin + github/*: ref-pin + docker/*: ref-pin + astral-sh/*: ref-pin + lycheeverse/*: ref-pin + zizmorcore/*: ref-pin + changesets/*: ref-pin + '*': hash-pin diff --git a/.gitignore b/.gitignore index a459aec3..24fb1998 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,11 @@ logs # Case-study CI evidence is committed intentionally (downloaded run logs). !docs/case-studies/**/ci-logs/ !docs/case-studies/**/ci-logs/*.log +# Per-issue investigation evidence under dev/log is committed intentionally. +# Raw multi-megabyte run logs stay ignored; commit them gzipped (*.log.gz), +# which git does not ignore. Small tool outputs are committed verbatim. +!dev/log/**/analysis/ +!dev/log/**/analysis/*.log npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/.husky/pre-commit b/.husky/pre-commit index 28dc8b35..bff058c3 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,5 @@ -cd js && npx lint-staged +# Run from the repository root so staged files outside js/ (experiments/, +# claude-profiles.mjs) go through the same gates as CI. lint-staged only +# considers files under its working directory, so the previous `cd js` made the +# hook silently skip every root-level file. +js/node_modules/.bin/lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 00000000..fae73d18 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,11 @@ +{ + "*.{js,mjs,cjs}": [ + "js/node_modules/.bin/eslint --fix --max-warnings 0 --no-warn-ignored", + "js/node_modules/.bin/prettier --write --ignore-unknown", + "js/node_modules/.bin/prettier --check --ignore-unknown" + ], + "*.md": [ + "js/node_modules/.bin/prettier --write --ignore-unknown", + "js/node_modules/.bin/prettier --check --ignore-unknown" + ] +} diff --git a/.lycheeignore b/.lycheeignore new file mode 100644 index 00000000..5fea86d1 --- /dev/null +++ b/.lycheeignore @@ -0,0 +1,19 @@ +# Patterns (regular expressions) that .github/workflows/links.yml excludes from +# the external link check. Every entry needs the comment above it saying why the +# link is unreachable from a runner but still correct in the document -- the +# workflow-hygiene test fails on an uncommented line, so nothing can be muted +# silently. + +# npmjs.com answers every unauthenticated non-browser request with 403, whether +# the package exists or not: +# curl -sIL -o /dev/null -w '%{http_code}\n' https://www.npmjs.com/package/command-stream +# 403 +# The README badges and the comparison table link there on purpose. +^https://(www\.)?npmjs\.com/ + +# GitHub serves the stargazers list and every /settings/ page only to a signed-in +# session; anonymously both answer 404 even for this public repository: +# curl -sIL -o /dev/null -w '%{http_code}\n' https://github.com/link-foundation/command-stream/stargazers +# 404 +# docs/CI-CD.md links to the settings page as the place a maintainer has to visit. +^https://github\.com/link-foundation/command-stream/(stargazers|settings/) diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..abdefba2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +node_modules +coverage +reports +dist +*.min.js +package-lock.json +.eslintcache +CLAUDE.md +# Build output. +rust/target +# Generated by changesets / the Rust changelog tooling. +js/CHANGELOG.md +rust/CHANGELOG.md +# Collected investigation evidence, kept byte-for-byte as downloaded. +dev/log +# Archived investigation records: downloaded logs, verbatim copies of upstream +# template files, and the write-ups quoting them. Kept byte-for-byte so diffs +# against the templates stay meaningful, and so reformatting never rewrites +# quoted evidence (prettier re-parses the ambiguous nested backticks in +# issue-166/upstream-issue.md and destroys the list it is quoting). +docs/case-studies diff --git a/js/.prettierrc b/.prettierrc similarity index 100% rename from js/.prettierrc rename to .prettierrc diff --git a/.secretlintignore b/.secretlintignore new file mode 100644 index 00000000..dfdd6af7 --- /dev/null +++ b/.secretlintignore @@ -0,0 +1,6 @@ +# Generated or vendored trees: nothing here is authored in this repository, and +# scanning them turns a sub-minute check into a multi-minute one. +node_modules/** +rust/target/** +js/reports/** +js/coverage/** diff --git a/.secretlintrc.json b/.secretlintrc.json new file mode 100644 index 00000000..7a1a5df3 --- /dev/null +++ b/.secretlintrc.json @@ -0,0 +1,7 @@ +{ + "rules": [ + { + "id": "@secretlint/secretlint-rule-preset-recommend" + } + ] +} diff --git a/claude-profiles.mjs b/claude-profiles.mjs index b2fb02fe..58731a71 100755 --- a/claude-profiles.mjs +++ b/claude-profiles.mjs @@ -19,11 +19,12 @@ import fs, { createWriteStream, promises as fsPromises } from 'fs'; import path from 'path'; import os from 'os'; import { createHash } from 'crypto'; +import { loadUseM } from './js/scripts/use-m-loader.mjs'; -// Dynamically load dependencies using use-m -const { use } = eval( - await fetch('https://unpkg.com/use-m/use.js').then((r) => r.text()) -); +// Dynamically load dependencies using use-m, through the shared loader: an +// inline fetch here would die at module load with a bare `TypeError: fetch +// failed` when the CDN blinks (see js/scripts/use-m-loader.mjs). +const use = await loadUseM(); // Load required packages dynamically with specific versions const [yargs, yargsHelpers, archiver] = await Promise.all([ diff --git a/dev/log/issues/199/pulls/200/analysis/README.md b/dev/log/issues/199/pulls/200/analysis/README.md new file mode 100644 index 00000000..19a7a133 --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/README.md @@ -0,0 +1,701 @@ +# Issue #199 — deep analysis + +> Evidence root: `dev/log/issues/199/pulls/200/` +> Issue: +> Pull request: + +## 1. Evidence collected + +| Path | What it is | +| --- | --- | +| `api/issue-199.json`, `api/pr-200.json` | Issue and PR metadata + comments | +| `api/repo.json` | Repository settings | +| `api/branch-protection.json` | `404 Branch not protected` for `main` | +| `api/rulesets.json` | `[]` — no rulesets configured | +| `api/runs-recent.json` | Last workflow runs on `main` | +| `api/run-.json` | Per-run job/step conclusions (7 runs) | +| `ci-logs/run-.log` | Full raw logs of the same 7 runs (0.8–1.9 MB each) | +| `workflows/` | Snapshot of `.github/workflows/*` before the fix | +| `templates/` | `CI-CD-BEST-PRACTICES.md` + file trees of both pipeline templates | +| `analysis/actionlint-before.log` | actionlint 1.7.7 run against the workflows (4 findings) | +| `analysis/zizmor-before.log` | zizmor `--min-confidence medium` run (58 findings) | +| `analysis/clippy-before.log` | `cargo clippy --all-targets --all-features` (15 warnings, exit 0) | +| `analysis/js-scripts-diff.log` | `js/scripts/*` vs js template | +| `analysis/rust-scripts-diff.log` | `rust/scripts/*` vs rust template | + +Every file under this evidence root is stored non-executable, including +`workflows/check-language-parity.sh`. The snapshots are read as evidence, never +run from here — the runnable copy is `.github/scripts/check-language-parity.sh` +— and a tracked executable bit on an inert copy is a mode the environment can +flip without touching a byte of content, which shows up as an unexplained dirty +working tree. + +## 2. Timeline of the failing release (run 33914574283) + +Reconstructed from `ci-logs/run-33914574283.log`. + +| Time (UTC) | Event | +| --- | --- | +| 20:07:5x | `release` job starts on `main` @ `1975cd7` after `lint` + `test` pass | +| 20:08:36 | `changeset publish` prints `🦋 success packages published successfully: command-stream@0.20.1` and `🦋 New tag: v0.20.1` — **the publish really succeeded** | +| 20:08:36 | `publish-to-npm.mjs` sleeps `VERIFY_DELAY` = 2 s | +| 20:08:38 | `npm view command-stream@0.20.1 version` → `npm error code E404 / 404 No match found for version 0.20.1` — registry read replica has not caught up yet | +| 20:08:38 | Script logs `Publish failed: version not found on npm after publish attempt, waiting 10s before retry...` | +| 20:08:48 | Attempt 2: `changeset publish` → `npm error code E409 / 409 Conflict — PUT https://registry.npmjs.org/command-stream — Cannot publish over previously staged version "0.20.1"` | +| 20:08:5x | `E409` text matches `FAILURE_PATTERNS` (`'npm error code e'`) → treated as a hard failure | +| 20:09:0x | Attempt 3: identical `E409` | +| 20:09:0x | `❌ Failed to publish after 3 attempts` → job **red** | + +Independent verification performed during this investigation: + +``` +$ npm view command-stream versions # includes 0.20.1 +$ npm view command-stream dist-tags # latest: 0.20.1 +``` + +**The npm release succeeded; CI reported a failure. This is the false positive +named in the issue title.** + +## 3. Requirements extracted from issue #199 + +| # | Requirement (verbatim intent) | Where addressed | +| --- | --- | --- | +| R1 | Check for **all false positives** in CI/CD and fix them | §4.1, §4.9, §4.13, §4.17, §4.21, §4.22, §4.25 | +| R2 | Check for **all false negatives** in CI/CD and fix them | §4.2, §4.3, §4.4, §4.6, §4.10, §4.11, §4.16, §4.17, §4.18, §4.19, §4.20, §4.23 | +| R3 | Check for **all warnings** in CI/CD and fix them | §4.2, §4.3, §4.5, §4.16 | +| R4 | Check for **all errors** in CI/CD and fix them | §4.1, §4.5, §4.12, §4.14, §4.15 | +| R5 | Compare **all files** (full tree, all workflows and CI/CD scripts) against `link-foundation/js-ai-driven-development-pipeline-template` | §5 | +| R6 | …and against `link-foundation/rust-ai-driven-development-pipeline-template` | §5 | +| R7 | Reuse **all their best practices** | §5, §6 | +| R8 | If the same issue exists in a template, **report an issue in the template too** | §7 | +| R9 | Follow `link-assistant/hive-mind/docs/CI-CD-BEST-PRACTICES.md` | §6 | +| R10 | Apply every fix **everywhere** the problem occurs (JS *and* Rust, every workflow, every job) | §4 (each row lists all sites) | + +## 4. Root causes + +### 4.1 FALSE POSITIVE — npm publish reported as failed after a successful publish + +* **Site:** `js/scripts/publish-to-npm.mjs` +* **Root cause A — single-shot verification.** `attemptPublish()` sleeps a fixed + `VERIFY_DELAY` (2 s) and then calls `npm view` exactly once. npm's registry is + read-replicated; a freshly published version routinely takes longer than 2 s + to become visible to `npm view`. One miss is treated as "publish failed". +* **Root cause B — E409 is misclassified.** On the retry, npm answers + `E409 Cannot publish over previously staged version`, which *proves* the + version is already there. The generic pattern `'npm error code e'` in + `FAILURE_PATTERNS` swallows it as a failure instead of an idempotent success. +* **Solution:** replace one-shot verification with bounded exponential-backoff + polling against the registry metadata endpoint, and classify + "already published / already staged" as success. Ported from the js template's + `publish-retry.mjs` + `npm-registry.mjs` + `publish-failure-classifier.mjs`, + **plus the E409 `previously staged version` pattern that the template is also + missing** (see §7). +* **The Rust side is already correct:** `rust/scripts/publish-crate.rs` + `classify_failure()` maps `already uploaded` / `already exists` to + `FailureKind::AlreadyExists` and the workflow accepts + `publish_result == 'already_exists'`. No change needed there; verified by + reading the script and `analysis/rust-scripts-diff.log`. + +### 4.2 FALSE NEGATIVE — Rust clippy warnings never fail the build + +* **Site:** `.github/workflows/rust.yml` `Run Clippy` step; `rust/Cargo.toml`. +* **Root cause:** `cargo clippy --all-targets --all-features` without + `-D warnings`, and `Cargo.toml` has no `[lints]` section. Clippy exits 0 while + printing warnings, so CI is green with 15 outstanding warnings + (`analysis/clippy-before.log`). +* **Solution:** add `-- -D warnings`, add `[lints.rust]` / `[lints.clippy]` to + `rust/Cargo.toml` mirroring the Rust template, and fix every warning. + +### 4.3 FALSE NEGATIVE — ESLint warnings never fail the build + +* **Site:** `js/package.json` `"lint": "eslint ."`. +* **Root cause:** no `--max-warnings 0`. `js/eslint.config.js` sets ~15 rules to + `warn`, so any regression under those rules passes CI silently. +* **Solution:** `eslint . --max-warnings 0`. The tree is currently clean, so the + gate can be closed without churn and prevents future regressions. + +### 4.4 FALSE NEGATIVE — the workflows themselves are never linted + +* **Site:** `.github/workflows/` — there is no `workflows.yml`. +* **Root cause:** no actionlint and no zizmor job exists, so shell defects and + workflow security defects reach `main` unreviewed. Baselines: + actionlint 4 findings, zizmor 58 findings. +* **Solution:** add `.github/workflows/workflows.yml` (actionlint via the Docker + image so shellcheck/pyflakes actually run, + zizmor with a repo config), plus + `.github/zizmor.yml` and `.github/actionlint.yaml`, and fix every finding. + +### 4.5 ERRORS/WARNINGS found by the new linters + +| Tool | Finding | Sites | +| --- | --- | --- | +| actionlint | SC2193 — `[[ "..." == "changeset-release/"* ]]` comparison always false-ish form | `js.yml:73` | +| actionlint | untrusted `github.head_ref` interpolated into `run:` | `js.yml:73` | +| actionlint | SC2086 — unquoted `$GITHUB_OUTPUT` (×2) | `js.yml:251,252` | +| zizmor | `template-injection` ×9 | `js.yml:73,345,398`; `rust.yml:300,337` | +| zizmor | `excessive-permissions` ×10 | every job without `permissions:` in `js.yml`, `rust.yml` | +| zizmor | `unpinned-uses` ×39 | every `uses:` in `js.yml`, `rust.yml`, `parity.yml` | + +### 4.6 FALSE NEGATIVE — no branch protection on `main` + +`api/branch-protection.json` = `404 Branch not protected`; `api/rulesets.json` = `[]`. +Nothing prevents merging a PR whose checks are red. This is a repository +*setting*, not a file, so it cannot be fixed inside this PR; it is documented in +`docs/CI-CD.md` as a required manual step. + +### 4.7 Concurrency model violates best practice #10 + +`js.yml` and `rust.yml` declare a **workflow-level** cancellable concurrency +group while the same workflow contains `release` / `instant-release` / +`changeset-pr` write jobs. A cancelled release can leave a committed version bump +without a published artifact. Best practice: cancellable groups only on check +jobs; a single non-cancellable `main-writer-*` group for every writer job. + +### 4.8 `always()` instead of `!cancelled()` + +`js.yml:85,115` and `rust.yml:78,111,146` use `if: always() && ...`, which keeps +jobs running after the run is cancelled. `!cancelled()` is the correct guard for +"run even though an upstream job was skipped". + +### 4.9 FALSE POSITIVE risk — matrix job-name collision + +`js.yml` names the test job `Test JavaScript (${{ matrix.runtime }} on ${{ matrix.os }})` +but the Node entries differ only by `matrix.node-version`. Result: three checks +all named `Test JavaScript (node on ubuntu-latest)`. Required-status-check rules +and humans cannot tell them apart, and a failure in one is indistinguishable +from a failure in another. + +### 4.10 FALSE NEGATIVE — the duplication gate analysed zero files + +`.jscpd.json` carried `"format": "console"`. In jscpd, `format` is the list of +**languages** to analyse, not the reporter: `@jscpd/finder` filters every +candidate with `options.format.includes(detectedLanguage)`. No file's language +is `console`, so the detector matched nothing: + +``` +"format": "console" -> exit 0, 0 clones, 0 files analysed +"format": ["javascript"] -> exit 1, 1 clone, 2 files analysed +``` + +Reproduction: `experiments/jscpd-format/run.mjs`. With the correct language list +the repository reports 65 files / 47 clones, 4.84 % of lines and 5.55 % of +tokens duplicated. `threshold` was `0`, which the check had never had to honour; +turning the gate on at `0` would have failed on the existing code rather than on +a regression, so it is set to `6` — just above today's measurement, so any +increase in duplication fails the job. `js/tests/duplication-check.test.mjs` +asserts the threshold stays in that range, so the gate cannot be disabled by +raising it. The same `format` defect is in the JavaScript template — reported +upstream (§7). + +### 4.11 FALSE NEGATIVE — half the repository was outside the lint base path + +`eslint.config.js`, `.prettierrc`, `.prettierignore` and `.lintstagedrc.json` +lived in `js/`. Both tools treat the directory holding their configuration as +the project base path, so repository-root JavaScript (`claude-profiles.mjs`, +`experiments/**`) was reported as "ignored because it is located outside of the +base path" and was silently never linted; lint-staged likewise only considered +files under `js/`. `js/.prettierignore` also listed +`docs/case-studies/**/{data,templates}/**`, which resolved against `js/`: +`js/docs/case-studies` has no `data/` or `templates/` subdirectory, and the +archived upstream copies those rules exist to protect live under the +repository-root `docs/case-studies` — outside prettier's reach entirely. The +configs now live at the root and `js/eslint.config.js` remains the rule set they +re-export. + +### 4.12 ERROR — nothing audited dependencies or analysed sources + +There was no security workflow at all: no dependency advisory check for any of +the three lockfiles, no static analysis, no scheduled re-run. `cargo audit` +found a live advisory on the first run: + +``` +RUSTSEC-2026-0007 bytes 1.11.0 integer overflow in BytesMut::reserve +``` + +Fixed by `cargo update -p bytes` (1.12.1). `npm audit` and `bun audit` were +cleared by the dev-dependency refresh. `.github/workflows/security.yml` now runs +CodeQL (`javascript-typescript`, `rust`, `actions`, all with `build-mode: none`), +dependency review, and the three audits, weekly as well as per push and pull +request — a lockfile that is clean today is not clean in a month. + +### 4.13 FALSE POSITIVE — zizmor audited archived evidence + +The first CI run of the Zizmor job reported 30 findings, all in `release.yml` — +a file `.github/workflows/` does not contain. `zizmorcore/zizmor-action` defaults +to `inputs: .`, walking the whole tree and collecting the 14 verbatim copies of +other repositories' workflows archived under `docs/case-studies/**/templates/**`. +Those never execute here and editing them would falsify the evidence they exist +to preserve, so the audit is scoped to `.github/workflows` and the scope is +pinned by a test. + +### 4.14 ERROR — dependency review cannot run on this repository + +``` +Dependency review is not supported on this repository. Please ensure that +Dependency graph is enabled +``` + +`GET /repos/link-foundation/command-stream` returns no `dependency_graph` key +under `security_and_analysis`; the compare endpoint returns `403 Forbidden` and +the SBOM endpoint `404 Not Found`. A `PATCH` with +`security_and_analysis[dependency_graph][status]=enabled` was accepted but had +no effect — the setting is controlled at the organisation level and could not be +changed from here. + +**Manual step required:** enable the dependency graph at +. + +Until then the job probes the compare endpoint and skips with a warning on 403, +rather than failing forever. A check that can only ever be red is itself a false +positive: it teaches reviewers to ignore red. Any status other than 200 or 403 +still fails the job, and the review starts running by itself once the graph is +on. The three audit jobs cover the committed lockfiles in the meantime. + +### 4.15 ERROR — a publish token was handed to every pull-request job + +`rust.yml` declared `CARGO_REGISTRY_TOKEN` in the workflow-level `env:`. That +block is inherited by every job, so the crates.io token was in the environment of +`cargo test` and `cargo clippy` on `pull_request` — both of which compile and run +code from the branch under review, via `build.rs`, proc macros or the tests +themselves. Publishing credentials now sit on the publishing job only, and a +test asserts no workflow-level `env:` value references `secrets.`. The same +defect is in the Rust template — reported upstream (§7). + +### 4.16 FALSE NEGATIVE — two warnings that only exist off Linux + +Denying warnings surfaced two defects the pipeline had never been able to see, +both of which failed the Rust job on `a00126b`: + +- `tests/cd_invocation_isolation.rs`: `output_env` is read only by the + `#[cfg(unix)]` assertions, because the dump it parses comes from + `/usr/bin/env`. On Windows it was an unused function and the test crate would + not compile. Confirmed both ways with + `cargo check --target x86_64-pc-windows-msvc --all-targets`. +- `scripts/version-and-commit.rs`: `rust-script --test` builds the script as a + test harness, where `main` is not the entry point, so the twelve helpers + reachable only from `main` are unreferenced — exactly the twelve errors CI + reported. The imports were already gated on `not(test)` for the same reason. + `#![cfg_attr(test, allow(dead_code))]` covers the test build; the real build + still denies dead code. + +The second one exists in the Rust template too, where it is invisible because +nothing there runs the script suites at all — reported upstream (§7). + +### 4.17 FALSE NEGATIVE — repository-wide checks were hidden behind `paths:` filters + +`js.yml` ran on `js/**`, `rust.yml` on `rust/**`, `workflows.yml` on +`.github/**`, `parity.yml` on both source trees. The union of those filters is +not the repository, so a pull request touching only `docs/**` matched no +workflow and ran **nothing at all**. Worse, three checks that read the whole +tree lived behind the `js/**` filter: + +* `format:check` runs prettier from the repository root over every tracked file, +* `workflow-hygiene.test.mjs` parses `.github/workflows/*`, +* the documentation checks added for §4.20. + +A formatting violation introduced in a workflow file or a markdown document +therefore first turned red on the next unrelated JavaScript pull request — the +textbook false positive: a red check on a change that did not cause it. + +Fix: `quality.yml`, deliberately without a `paths:` filter, runs those three +checks on every pull request; `js.yml`'s filter gained the root-level files +eslint reaches (`eslint.config.js`, `claude-profiles.mjs`, `experiments/**`); +and two hygiene invariants keep it that way — every file eslint lints outside +`js/` must appear in `js.yml`'s trigger, and a workflow's `push:` and +`pull_request:` filters must be identical, so a green pull request keeps +predicting a green `main`. + +### 4.18 FALSE NEGATIVE — three shipped quality gates were never invoked + +`rust/scripts/` contained `check-file-size.rs`, `check-crate-size.rs` and +`check-version-modification.rs`. No workflow, script or document referenced any +of them (`grep -rn` across the tree returned only their own definitions), so the +pipeline reported "all checks passed" for gates that never ran — including the +file-size limit that best practice #2 requires and that eslint already enforces +on the JavaScript side. `rust.yml` now runs all three, and a hygiene test fails +when a script under `rust/scripts/` is neither referenced by a workflow nor +listed as a documented exception. + +### 4.19 FALSE NEGATIVE — nothing scanned the tree for committed credentials + +Best practice #11. CodeQL does not look for secrets, and the audit jobs only +read lockfiles, so a committed credential would have reached `main` unnoticed. +`security.yml` now runs secretlint with the recommended preset over every file +on each pull request; `.secretlintrc.json` holds the rule set and +`.secretlintignore` only generated trees — a hygiene test rejects any ignore +pattern outside `node_modules/`, `rust/target/` and `js/{reports,coverage}/`. + +### 4.20 FALSE NEGATIVE — documentation was never validated + +Best practice #12. `js/tests/docs-validation.test.mjs` now enforces the +2500-line ceiling, resolves every relative link in every authored markdown file +and checks that the documents other automation points readers at still carry +their sections. It found two real breakages on the first run: two case-study +links pointed at release markers the release process had consumed, and one +pointed one directory level too high. + +### 4.21 FALSE POSITIVE/NEGATIVE — the checks validated a stale merge preview + +Best practice #7. A `pull_request` run checks out `refs/pull/N/merge`, computed +when the branch was last synchronised. If `main` moved since, every check +validated a combination that will not exist after the merge — green pull +request, broken `main`. Every pull-request job that reads the tree now runs +`.github/scripts/simulate-fresh-merge.sh` first, which also turns a merge +conflict into a clear failure instead of a surprise at merge time. Five jobs are +exempt with the reason recorded next to them and in the hygiene test +(`changeset-check`, the Rust changelog checks and `parity` are diff-based; +`dependency-review` compares two SHAs through the API; CodeQL uploads results +keyed to a commit GitHub has to know). + +### 4.22 FALSE POSITIVE — a link checker on pull requests reports 20 unfixable errors + +Best practice #12 names `lychee`, and both templates run it as a pull-request +gate. Copying that verbatim would have imported a false-positive generator. A +run over this tree (`lychee-run.log`): + +``` +🔍 114 Total 🔗 73 Unique ✅ 94 OK 🚫 20 Errors +``` + +All 20 are links that are correct in the document and unreachable from a +runner — npmjs.com answers `403` to any non-browser client (verified with +`curl -A 'Mozilla/5.0'`, still 403) and GitHub serves the stargazers list and +`/settings/` pages only to a signed-in session (`404` anonymously, even though +the repository is public). Including the archived trees adds five more, from a +verbatim copy of hive-mind's own best-practices document whose links point into +the repository it came from. + +The split is by who can break the link. Relative links — the only ones a change +here can break — are resolved offline on every pull request (§4.20). External +links are fetched weekly and on demand by `links.yml`, whose failure means a +link that used to work has stopped working and blocks no merge. +`.lycheeignore` records the known-unreachable URLs, one commented entry each, +and with it the same run reports `0 Errors, 20 Excluded` +(`lychee-with-ignore.log`). + +### 4.23 FALSE NEGATIVE — the new checks validated nothing on Windows + +Found by the Windows leg of the matrix on run 33930261205: + +``` +(fail) documentation validation > the file list is not empty ... Expected: > 20, Received: 0 +(fail) every file eslint lints outside js/ triggers the lint job ... Received [""] +``` + +`execSync("git ls-files '*.md'")` goes through the platform shell. `/bin/sh` +strips the single quotes; `cmd.exe` does not, so git looked for a path literally +named `'*.md'`, matched nothing and exited 0 — the documentation checks were +validating an empty list. `execFileSync('git', ['ls-files', '*.md'])` uses no +shell, so git expands the pattern itself everywhere. +`experiments/git-ls-files-quoting.mjs` reproduces both behaviours on Linux: + +``` +execSync, shell strips the quotes (POSIX): 37 file(s) +execSync, quotes reach git (what cmd.exe does): 0 file(s) +execFileSync, no shell at all: 37 file(s) +``` + +Only the assertion that the list is non-empty made this visible, which is the +argument for writing that assertion into every check that discovers its own +inputs. + +### 4.24 FALSE NEGATIVE — the workflow audit could not see credential persistence + +Found while writing the upstream report for the JavaScript template (§7, #160) +and then checked here, because the same setting had been copied over. zizmor's +`artipacked` audit — a checkout that leaves the job token in `.git/config`, +where any later step or uploaded artifact can read it — is a **Low**-confidence +check, and the job ran with `min-confidence: medium`. Every finding of that +class was therefore invisible: + +``` +zizmor --config .github/zizmor.yml --min-confidence medium .github/workflows -> No findings +zizmor --config .github/zizmor.yml --min-confidence low .github/workflows -> 6 findings, all artipacked +``` + +All six are the release jobs, which push to `main` and publish, so they do need +the credential. The fix is not to silence them again by raising the threshold: +the job now runs at `--min-confidence low`, the six carry an inline +`ignore[artipacked]` next to the reason, and two hygiene tests hold the line — +every `actions/checkout` in every workflow either sets +`persist-credentials: false` or carries that suppression, and the number of +suppressions is asserted, so a seventh cannot arrive by copy-paste. + +### 4.25 FALSE POSITIVE — the publish tests fail opaquely when a CDN blinks + +Found by running the suite twice, unchanged, during this iteration: the first +run reported + +``` +(fail) reports published for a version already on npm (legit success path) +(fail) issue #199: registry propagation lag after a clean publish is not a failure + Expected to contain: "published=true" + Received: "" +``` + +and the second run passed all six. The tests are subprocess integration tests: +they spawn the real `publish-to-npm.mjs`, whose **first** statement is a +module-scope `await fetch('https://unpkg.com/use-m/use.js')`. That await is +outside `main()`'s `try/catch`, so an unreachable CDN kills the script during +module initialisation — before it writes a single line to `GITHUB_OUTPUT` and +before its first `console.log`. + +Three defects compound: + +* **The offline guard probed the wrong endpoint.** `beforeAll` ran + `npm view command-stream version` and treated success as "we are online". + npm's registry and unpkg are different services that fail independently, so a + reachable registry cleared the guard while the dependency the script needs at + startup was down. Both endpoints are probed now, and the suite skips when + either is unreachable — the behaviour the guard was written to provide. +* **The failure named nothing.** With the script dead before any output, + every assertion degenerated to `Received: ""`, which points at the publish + logic rather than at the network. `assertScriptStarted()` now checks for the + script's first log line and, when it is absent, raises with the child's exit + status, stderr and `GITHUB_OUTPUT` contents, so the next occurrence is + diagnosable from the CI log alone. +* **The load itself had no deadline, no retry and no diagnostics.** Eleven + scripts in `js/scripts/` and the root-level `claude-profiles.mjs` opened with + the same inline statement, so the hardening had to be applied in all twelve, + not only in the one the tests spawn. Three failure modes follow from that shape: a network failure rejects + with a bare `TypeError: fetch failed` thrown during module initialisation; a + CDN *error page* is HTML, and eval-ing HTML raises `SyntaxError: Unexpected + token '<'`, which points at this repository's code for a response it never + inspected; and a stalled connection has no deadline of its own — undici bounds + only the connect (10 s), while `headersTimeout` defaults to 300 s, so one + fetch can burn five minutes of a job's `timeout-minutes`. + + `js/scripts/use-m-loader.mjs` is now the single place that loads use-m: a + per-attempt `AbortSignal.timeout(15000)`, three attempts with exponential + backoff (a CDN blip is transient by nature), `response.ok` checked before the + eval, one `::debug::` line per attempt (off by default, per the verbose-mode + requirement), and a final error naming the URL, the attempt count and the + cause. All twelve callers use it; `js/tests/use-m-loader.test.mjs` pins the + behaviour and scans every tracked `.mjs`/`.js`/`.cjs` file — excluding the + loader, the tests, the experiment and the archived copies, which quote the old + statement as the thing they are about — to assert that nothing fetches use.js + inline again. + +`experiments/publish-cdn-unreachable.mjs` reproduces it on demand by pointing +the fetch at an unroutable TEST-NET-3 address (RFC 5737), instead of waiting for +a real outage, and runs both shapes side by side: + +``` +legacy inline fetch: + exit status 1 + elapsed 10718ms + stdout "" + failure reported "TypeError: fetch failed" + +shared loader: + exit status 1 + elapsed 6314ms + stdout "" + failure reported "Error: Failed to load use-m from https://203.0.113.1/use-m/use.js + after 2 attempt(s): The operation was aborted due to timeout. This + is a network dependency of the release scripts, not a defect in the + published package; re-run the job when the CDN answers again." +``` + +Neither run reaches the script body — the load is still at module scope, so the +script cannot write `published=false` itself — but the second one says who +failed, which is the difference between re-running a job and investigating a +publish defect. Moving the loads inside `main()` is the remaining step; it is +listed as follow-up 1 in the upstream report, because the same module-scope +shape is what the template ships. + +With the offline guard fixed, an unreachable CDN skips all six tests rather than +failing them: + +``` +HTTPS_PROXY=http://127.0.0.1:1 bun test js/tests/publish-to-npm.test.mjs -> 6 pass, 0 fail +``` + +This is the same class as §4.14 and §4.22: a check that goes red for a reason +the pull request did not cause teaches reviewers to ignore red. + +## 5. File-by-file comparison against both templates + +Scripts (`analysis/js-scripts-diff.log`, `analysis/rust-scripts-diff.log`): + +* `rust/scripts/` — 8 of 17 files byte-identical to the template. The 5 that + differ are *ahead* of the template (e.g. `publish-crate.rs` uses + `--manifest-path` instead of `cd`, which is strictly better) or repo-specific. + Template-only scripts that matter for CI hygiene: + `check-cargo-lock.rs`, `simulate-fresh-merge.sh`, `install-rust-script.sh`. +* `js/scripts/` — every shared file differs; the important structural gap is + that the template splits publishing into `publish-retry.mjs`, + `npm-registry.mjs` and `publish-failure-classifier.mjs` with unit tests, while + this repo has a single monolithic `publish-to-npm.mjs` with the defect in §4.1. + +Workflows — present in **both** templates, absent here: + +| Template file | Purpose | +| --- | --- | +| `.github/workflows/workflows.yml` | actionlint + zizmor | +| `.github/workflows/security.yml` | CodeQL, dependency review, ecosystem audit | +| `.github/zizmor.yml` | `unpinned-uses` policy | +| `.github/actionlint.yaml` | known-runner-label allowlist | +| `.github/workflows/links.yml` | lychee link check | +| `.github/scripts/simulate-fresh-merge.sh` (js) / `scripts/simulate-fresh-merge.sh` (rust) | merge the base branch before checking | +| `.secretlintrc.json`, `.secretlintignore` | committed-credential scan | + +All of them are now present here, with two deliberate divergences, both recorded +in `docs/CI-CD.md` and enforced by the hygiene test: + +* **`links.yml` runs weekly, not on pull requests** (§4.22). Both templates gate + merges on it; on this tree that gate reports 20 errors that no change here can + fix. +* **zizmor's input is `.github/workflows`, not `.`** (§4.13), because this + repository archives other projects' workflows under `docs/case-studies/`. + +Two defects in the templates' own copies of these files were reported upstream +(§7): the js template's `links.yml` `paths:` filter omits the very files the job +reads, and its zizmor job runs at `min-confidence: medium`, which hides the +`artipacked` findings for all 25 of its checkouts that persist credentials. + +## 6. Best practices applied from `CI-CD-BEST-PRACTICES.md` + +1. Least-privilege `permissions:` at workflow level (`contents: read`) with + per-job elevation only where a write is genuinely needed. +2. No `${{ }}` interpolation inside `run:` — every value passes through `env:`. +3. Third-party actions hash-pinned; trusted namespaces ref-pinned by policy. +4. `persist-credentials: false` on every `actions/checkout` that does not push. +5. Cancellable `check-*` concurrency for checks; single non-cancellable + `main-writer-*` group for writers. +6. `!cancelled()` rather than `always()`. +7. Every job has `timeout-minutes`. +8. Workflow linting (actionlint **with shellcheck**) and workflow security + auditing (zizmor) as first-class CI jobs. +9. Warnings are errors (`-D warnings`, `--max-warnings 0`). +10. Verification of published artifacts (`wait-for-npm.mjs`, + `wait-for-crate.rs`) so a green release means an installable artifact. +11. #7 *Validate the actual merge result* — every pull-request job that reads + the tree merges the base branch first + (`.github/scripts/simulate-fresh-merge.sh`), so a green pull request is a + statement about what `main` will contain; the five diff- or API-based jobs + that must not are exempt with the reason recorded (§4.21). +12. #11 *Secrets detection* — secretlint with the recommended preset over every + file on each pull request (§4.19). +13. #12 *Documentation validation* — size ceiling, relative-link resolution and + required-section checks offline on every pull request, external links + weekly (§4.20, §4.22). +14. Repository-wide checks run without a `paths:` filter (`quality.yml`), and a + workflow's `push:` and `pull_request:` filters must be identical, so no + change class is left unchecked and a green pull request keeps predicting a + green `main` (§4.17). +15. Every shipped quality gate is invoked by a workflow, or listed as + deliberately unwired (§4.18). + +## 7. Upstream issues reported + +Seven defects found here also exist in the templates the issue asks to compare +against, so each was reported with a reproducible example, a workaround and the +code-level fix: + +| Issue | Repository | Defect | +| --- | --- | --- | +| [#157](https://github.com/link-foundation/js-ai-driven-development-pipeline-template/issues/157) | js template | `.jscpd.json` `"format": "console"` makes the duplication check analyse zero files and always pass (§4.10) | +| [#158](https://github.com/link-foundation/js-ai-driven-development-pipeline-template/issues/158) | js template | `publish-retry.mjs` misses npm's E409 "Cannot publish over previously staged version", turning successful releases into failed jobs (§4.1) | +| [#149](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/issues/149) | rust template | `release.yml` puts `CARGO_REGISTRY_TOKEN`/`CARGO_TOKEN` in the workflow-level `env:`, handing the publish token to seven jobs that compile pull-request code (§4.15) | +| [#150](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/issues/150) | rust template | No workflow runs `rust-script --test`, so 78 tests across 9 scripts never execute; `create-github-release.rs` does not compile in test mode and `version-and-commit.rs` fails under the template's own `RUSTFLAGS: -Dwarnings` (§4.16) | +| [#159](https://github.com/link-foundation/js-ai-driven-development-pipeline-template/issues/159) | js template | `links.yml`'s `paths:` filter omits `.lycheeignore` and `scripts/check-web-archive.mjs`, so editing the ignore list or the archive helper does not re-run the job that reads them | +| [#160](https://github.com/link-foundation/js-ai-driven-development-pipeline-template/issues/160) | js template | 25 of 27 checkouts persist credentials, and `min-confidence: medium` hides every `artipacked` finding (it is a Low-confidence check), so the audit reports 3 findings instead of 28 | +| [#161](https://github.com/link-foundation/js-ai-driven-development-pipeline-template/issues/161) | js template | `use-module.mjs` fetches use-m with no timeout and no retry, and eight scripts call it at module scope, so a CDN blip kills them with a bare `TypeError: fetch failed` and an empty `GITHUB_OUTPUT` (§4.25) | + +Checked and deliberately **not** reported, because they are correct as written: +the Rust template's `pipeline-status: if: always()` (it intentionally reports +cancelled jobs), matrix job names that omit a `runner` key when another key +already disambiguates them, and the JavaScript template's three low-confidence +`self-repository` zizmor findings. Two more were tested and dropped: + +* the js template's `secretlint "**/*"` runs after `npm install`, but with a + 131 MB `node_modules/` present the exact command finishes in 19 s with zero + findings, so there is nothing to report; +* both templates' `if: always() && steps.lychee.outputs.exit_code != 0` is + redundant rather than wrong — when the step is skipped the output is `''` and + `'' != 0` is false — and the js template's own + `tests/links-workflow.test.js` asserts that exact string, so changing it would + break its test suite for no behavioural gain. + +### Detail: #158 + +**Repository:** `link-foundation/js-ai-driven-development-pipeline-template` + +`scripts/publish-retry.mjs` defines `ALREADY_PUBLISHED_PATTERNS` as: + +```js +const ALREADY_PUBLISHED_PATTERNS = [ + 'epublishconflict', + 'cannot publish over the previously published version', + 'cannot publish over previously published version', + 'you cannot publish over the previously published versions', + 'already published', +]; +``` + +npm's real E409 message for a re-publish of a version whose tarball was already +staged is: + +``` +npm error code E409 +npm error 409 Conflict - PUT https://registry.npmjs.org/ - Cannot publish over previously staged version "0.20.1" +``` + +`staged` ≠ `published`, so none of the patterns match, and `E409` is not handled +anywhere in the template (`grep -rn 'staged\|E409\|409' scripts/` → no hits). +A retry after a slow-propagating publish therefore fails the release even though +the version is live. Reproducible example, workaround and the code fix are in +the issue text (see §9 of the PR description). + +### Detail: #161 + +**Repository:** `link-foundation/js-ai-driven-development-pipeline-template` +**Archived report body:** `../upstream/js-template-use-m-timeout.md` + +`scripts/use-module.mjs:113` is `const response = await fetchImpl(url);` — no +`signal`, no retry — and seven scripts call it through `loadCommandStream()` at +module scope (`publish-to-npm.mjs:38`, `changeset-version.mjs:30`, +`create-manual-changeset.mjs:22`, `instant-version-bump.mjs:36`, +`format-github-release.mjs:23`, `format-release-notes.mjs:33`, +`version-and-commit.mjs:31`); only `setup-npm.mjs:257` loads inside a function +and can therefore report the failure itself. Checked at `7ae16b0` (0.11.28). + +Two reproductions were run against the template checkout. An unreachable CDN: + +``` +$ node repro.mjs # loadUse({ url: 'https://203.0.113.1/use-m/use.js' }) +elapsed 10620ms +name TypeError +message fetch failed +cause Connect Timeout Error (attempted address: 203.0.113.1:443, timeout: 10000ms) +``` + +The 10 s bound is undici's connect timeout, not the template's. A CDN that +accepts the connection and never answers is not bounded at all: + +``` +$ node stall.mjs # server accepts, never responds +after 25047ms: still waiting +``` + +The report includes both reproductions, the workaround (re-run the job; probe +unpkg as well as `npm view` in test guards), the diff adding the timeout and the +bounded retry, and two follow-ups: move the module-scope loads into `main()` so +the script can still write to `GITHUB_OUTPUT`, and keep the worst case inside +`timeout-minutes`. + +## 8. Existing components / libraries surveyed + +| Component | Used for | +| --- | --- | +| [`rhysd/actionlint`](https://github.com/rhysd/actionlint) 1.7.7 (Docker image) | Workflow syntax + embedded shellcheck/pyflakes. The Docker image is required: a bare binary without shellcheck on `PATH` silently skips shell checks. | +| [`zizmor`](https://docs.zizmor.sh/) via `zizmorcore/zizmor-action` | Workflow *security* audit — unpinned actions, template injection, excessive permissions, credential persistence. | +| [`github/codeql-action`](https://github.com/github/codeql-action) | Static analysis for `javascript-typescript` and `actions`. | +| [`actions/dependency-review-action`](https://github.com/actions/dependency-review-action) | Blocks PRs introducing high-severity advisories. | +| `npm audit --package-lock-only --audit-level=high` | Dependency advisories without a network install. | +| [`@changesets/cli`](https://github.com/changesets/changesets) | Already in use for versioning/publishing. | +| npm registry metadata endpoint (`https://registry.npmjs.org/`) | Publication check that does not depend on `npm view`'s cache/replica behaviour. | +| [`secretlint`](https://github.com/secretlint/secretlint) + `@secretlint/secretlint-rule-preset-recommend` | Committed-credential scan (best practice #11). Chosen over gitleaks/trufflehog because it needs no extra toolchain in a repository that already runs npm, and its ignore file is reviewable text. | +| [`lychee`](https://lychee.cloudflare.dev/) via `lycheeverse/lychee-action` | External link checking, weekly rather than per-pull-request (§4.22), with `.lycheeignore` for endpoints that answer only to a browser session. | +| `AbortSignal.timeout()` (Node 18+, Bun, Deno) | Per-attempt deadline for the use-m CDN fetch (§4.25). Chosen over `p-retry`/`node-fetch-retry`/`fetch-retry`, which do the same job well, because these scripts run with **no `package.json` dependencies at all** — that constraint is the reason use-m exists — so a retry library would have to be fetched over the very network path it is meant to protect. The whole policy is 40 lines of `js/scripts/use-m-loader.mjs`. | +| `git ls-files` via `execFileSync` | Input discovery for the documentation and hygiene tests. Deliberately not a glob library: git already knows what is tracked, and going through a shell is what broke it on Windows (§4.23). | diff --git a/dev/log/issues/199/pulls/200/analysis/actionlint-before.log b/dev/log/issues/199/pulls/200/analysis/actionlint-before.log new file mode 100644 index 00000000..8d23dfc5 --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/actionlint-before.log @@ -0,0 +1,33 @@ +Unable to find image 'rhysd/actionlint:1.7.7' locally +1.7.7: Pulling from rhysd/actionlint +1f3e46996e29: Pulling fs layer +4e6599d62119: Pulling fs layer +46b6d0e9fe9d: Pulling fs layer +f23002ece875: Pulling fs layer +f23002ece875: Waiting +4e6599d62119: Download complete +1f3e46996e29: Verifying Checksum +1f3e46996e29: Download complete +46b6d0e9fe9d: Verifying Checksum +46b6d0e9fe9d: Download complete +f23002ece875: Verifying Checksum +f23002ece875: Download complete +1f3e46996e29: Pull complete +4e6599d62119: Pull complete +46b6d0e9fe9d: Pull complete +f23002ece875: Pull complete +Digest: sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 +Status: Downloaded newer image for rhysd/actionlint:1.7.7 +.github/workflows/js.yml:72:9: shellcheck reported issue in this script: SC2193:warning:1:32: The arguments to this comparison can never be equal. Make sure your syntax is correct [shellcheck] + | +72 |  run: | + |  ^~~~ +.github/workflows/js.yml:72:25: "github.head_ref" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions for more details [expression] +.github/workflows/js.yml:248:9: shellcheck reported issue in this script: SC2086:info:3:89: Double quote to prevent globbing and word splitting [shellcheck] + | +248 |  run: | + |  ^~~~ +.github/workflows/js.yml:248:9: shellcheck reported issue in this script: SC2086:info:4:44: Double quote to prevent globbing and word splitting [shellcheck] + | +248 |  run: | + |  ^~~~ diff --git a/dev/log/issues/199/pulls/200/analysis/actionlint-step2.log b/dev/log/issues/199/pulls/200/analysis/actionlint-step2.log new file mode 100644 index 00000000..3371cd42 --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/actionlint-step2.log @@ -0,0 +1,13 @@ +.github/workflows/js.yml:72:9: shellcheck reported issue in this script: SC2193:warning:1:32: The arguments to this comparison can never be equal. Make sure your syntax is correct [shellcheck] + | +72 |  run: | + |  ^~~~ +.github/workflows/js.yml:72:25: "github.head_ref" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions for more details [expression] +.github/workflows/js.yml:248:9: shellcheck reported issue in this script: SC2086:info:3:89: Double quote to prevent globbing and word splitting [shellcheck] + | +248 |  run: | + |  ^~~~ +.github/workflows/js.yml:248:9: shellcheck reported issue in this script: SC2086:info:4:44: Double quote to prevent globbing and word splitting [shellcheck] + | +248 |  run: | + |  ^~~~ diff --git a/dev/log/issues/199/pulls/200/analysis/js-scripts-diff.log b/dev/log/issues/199/pulls/200/analysis/js-scripts-diff.log new file mode 100644 index 00000000..739c2f84 --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/js-scripts-diff.log @@ -0,0 +1,2950 @@ +########## changeset-version.mjs ########## +--- js/scripts/changeset-version.mjs 2026-09-04 20:28:25.871207709 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/changeset-version.mjs 2026-09-04 20:31:16.597474164 +0000 +@@ -5,36 +5,64 @@ + * with package.json after version bumps. + * + * This script: +- * 1. Runs `changeset version` to update the JavaScript package version +- * 2. Runs `npm install` to synchronize package-lock.json with the new version ++ * 1. Detects the JavaScript package root (supports both single-language and multi-language repos) ++ * 2. Runs `changeset version` to update package versions ++ * 3. Runs `npm install` to synchronize package-lock.json with the new versions ++ * ++ * Configuration: ++ * - CLI: --js-root to explicitly set JavaScript root ++ * - Environment: JS_ROOT= + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support ++ * ++ * Addresses issues documented in: ++ * - Issue #21: Supporting both single and multi-language repository structures ++ * - Reference: link-assistant/agent PR #112 (--legacy-peer-deps fix) ++ * - Reference: link-assistant/agent PR #114 (configurable package root) + */ + +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { getJsRoot, needsCd, parseJsRootConfig } from './js-paths.mjs'; ++import { loadCommandStream } from './use-module.mjs'; + + // Import command-stream for shell command execution +-const { $ } = await use('command-stream'); ++const { $ } = await loadCommandStream(); ++ ++// Store the original working directory to restore after cd commands ++// IMPORTANT: command-stream's cd is a virtual command that calls process.chdir() ++const originalCwd = process.cwd(); + + try { ++ // Get JavaScript package root (auto-detect or use explicit config) ++ const jsRootConfig = parseJsRootConfig(); ++ const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++ + console.log('Running changeset version...'); +- await $`bunx changeset version`; ++ ++ // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after ++ if (needsCd({ jsRoot })) { ++ await $`cd ${jsRoot} && npx changeset version`; ++ process.chdir(originalCwd); ++ } else { ++ await $`npx changeset version`; ++ } + + console.log('\nSynchronizing package-lock.json...'); +- try { +- await $`npm install --package-lock-only`; +- } catch { +- // No package-lock.json or npm not available, skip +- console.log('Skipping package-lock.json sync (not applicable)'); ++ ++ // Use --legacy-peer-deps to handle peer dependency conflicts ++ // This addresses npm ERESOLVE errors documented in issue #111 / PR #112 ++ if (needsCd({ jsRoot })) { ++ await $`cd ${jsRoot} && npm install --package-lock-only --legacy-peer-deps`; ++ process.chdir(originalCwd); ++ } else { ++ await $`npm install --package-lock-only --legacy-peer-deps`; + } + + console.log('\n✅ Version bump complete with synchronized package-lock.json'); + } catch (error) { ++ // Restore cwd on error ++ process.chdir(originalCwd); + console.error('Error during version bump:', error.message); + if (process.env.DEBUG) { + console.error('Stack trace:', error.stack); +########## check-release-needed.mjs ########## +--- js/scripts/check-release-needed.mjs 2026-09-04 20:28:25.871207709 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/check-release-needed.mjs 2026-09-04 20:31:16.598474154 +0000 +@@ -1,153 +1,118 @@ +-#!/usr/bin/env bun ++#!/usr/bin/env node + + /** +- * Check if a release is needed based on changesets and npm registry state. ++ * Check if a release is needed based on changesets and npm registry state + * + * This script checks: +- * 1. If there are changeset files to process, and +- * 2. If the current package.json version has already been published to npm. ++ * 1. If there are changeset files to process ++ * 2. If the current version has already been published to npm + * +- * IMPORTANT: This checks npm (the source of truth for JS packages), NOT git +- * tags / GitHub releases. This is critical because: +- * - Git tags and GitHub releases can exist without the package being on npm +- * (exactly what produced the false-positive `js-v0.10.1` release in #166), +- * - Only npm publication means users can actually `npm install` the package. +- * +- * Self-healing: if a previous release attempt bumped the version and committed +- * it to main but the npm publish failed or was skipped, the repo version is +- * left permanently ahead of npm. By comparing package.json against the +- * registry, the next push to main detects the unpublished version and triggers +- * a catch-up publish. +- * +- * IMPORTANT — this is a deliberate improvement over the upstream template +- * (link-foundation/js-ai-driven-development-pipeline-template, +- * scripts/check-release-needed.mjs, issue #36). The template only probes npm +- * when `has_changesets` is false. But issue #166's "failed to do any deploy" +- * restart (run 27224046292) had `has_changesets=true` *locally* while the +- * version bump had already been consumed on `origin/main` by a prior run — so +- * `changeset version` found nothing, the version step committed nothing, and +- * the publish was gated off, leaving v0.10.2 stranded (npm was still at 0.9.5). +- * To close that gap we ALWAYS probe npm and emit `current_unpublished`, which +- * the workflow uses to publish the current version whether or not a changeset +- * is present. See docs/case-studies/issue-166/. +- * +- * command-stream's `$` does NOT throw on a non-zero exit code (errexit is off +- * by default — see issue #156), so the registry probe checks the captured exit +- * code explicitly instead of relying on a thrown error. ++ * IMPORTANT: This script checks npm (the source of truth for JS packages), ++ * NOT git tags. This is critical because: ++ * - Git tags can exist without the package being published ++ * - GitHub releases create tags but don't publish to npm ++ * - Only npm publication means users can actually install the package ++ * ++ * This provides a self-healing mechanism: if a previous release attempt ++ * failed or was skipped, the next push to main will detect the unpublished ++ * version and trigger a release without requiring a changeset. ++ * ++ * Analogous to check-release-needed.rs in the Rust template. ++ * ++ * Supports both single-language and multi-language repository structures: ++ * - Single-language: package.json in repository root ++ * - Multi-language: package.json in js/ subfolder + * +- * Usage: bun scripts/check-release-needed.mjs +- * (run with working-directory: js, so ./package.json is the JS package) ++ * Usage: node scripts/check-release-needed.mjs [--js-root ] + * + * Environment variables: +- * - HAS_CHANGESETS: 'true' if changeset files exist (from the changeset check) ++ * - HAS_CHANGESETS: 'true' if changeset files exist (from check-changesets.mjs) + * + * Outputs (written to GITHUB_OUTPUT): + * - should_release: 'true' if a release should be created +- * - skip_bump: 'true' if the version bump should be skipped (version already +- * bumped but not yet published — self-healing path) +- * - current_unpublished: 'true' if the current package.json version is NOT on +- * npm. This is the authoritative publish trigger: it is true +- * whenever the committed version still needs to reach the +- * registry, regardless of whether a changeset is present, which +- * is what makes the self-heal cover the #166 restart case. ++ * - skip_bump: 'true' if version bump should be skipped (version not yet published) ++ * ++ * Addresses issues documented in: ++ * - Issue #36: Release job silently skips when PRs merge without changesets + */ + +-import { readFileSync, appendFileSync } from 'fs'; ++import { appendFileSync } from 'fs'; + +-// Load use-m dynamically (matches the other release scripts in this folder). +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { getJsRoot, parseJsRootConfig } from './js-paths.mjs'; ++import { isPackageVersionPublished } from './npm-registry.mjs'; ++import { readPackageInfo } from './package-info.mjs'; + +-const { $ } = await use('command-stream'); ++const jsRootConfig = parseJsRootConfig(); ++const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); + + /** +- * Append to the GitHub Actions output file (and echo for the run log). +- * @param {string} key +- * @param {string} value ++ * Write output to GitHub Actions output file ++ * @param {string} name - Output name ++ * @param {string} value - Output value + */ +-function setOutput(key, value) { ++function setOutput(name, value) { + const outputFile = process.env.GITHUB_OUTPUT; + if (outputFile) { +- appendFileSync(outputFile, `${key}=${value}\n`); ++ appendFileSync(outputFile, `${name}=${value}\n`); + } +- console.log(`Output: ${key}=${value}`); ++ console.log(`Output: ${name}=${value}`); + } + + /** +- * Read the package name and version from the local package.json. ++ * Get the package name and version from package.json + * @returns {{ name: string, version: string }} + */ + function getPackageInfo() { +- const packageJson = JSON.parse(readFileSync('./package.json', 'utf8')); +- return { name: packageJson.name, version: packageJson.version }; ++ return readPackageInfo({ jsRoot }); + } + + /** +- * Check whether a specific version is published on npm. +- * +- * command-stream's `$` does not throw on non-zero exit, so we inspect the +- * captured exit code: `npm view @ version` exits 0 and prints the +- * version when it exists, and exits non-zero (E404) when it does not. +- * ++ * Check if a specific version is published on npm + * @param {string} packageName + * @param {string} version + * @returns {Promise} + */ +-async function checkVersionOnNpm(packageName, version) { +- const result = await $`npm view "${packageName}@${version}" version`.run({ +- capture: true, +- }); +- return result.code === 0 && result.stdout.trim().includes(version); ++function checkVersionOnNpm(packageName, version) { ++ return isPackageVersionPublished(packageName, version); + } + + async function main() { +- try { +- const hasChangesets = process.env.HAS_CHANGESETS === 'true'; +- const { name: packageName, version: currentVersion } = getPackageInfo(); +- +- console.log(`Package: ${packageName}`); +- console.log(`Current version: ${currentVersion}`); +- console.log(`Has changesets: ${hasChangesets}`); ++ const hasChangesets = process.env.HAS_CHANGESETS === 'true'; ++ const { name: packageName, version: currentVersion } = getPackageInfo(); ++ ++ console.log(`Package: ${packageName}`); ++ console.log(`Current version: ${currentVersion}`); ++ console.log(`Has changesets: ${hasChangesets}`); ++ ++ if (hasChangesets) { ++ console.log('Found changesets, proceeding with release'); ++ setOutput('should_release', 'true'); ++ setOutput('skip_bump', 'false'); ++ return; ++ } ++ ++ console.log( ++ `Checking if ${packageName}@${currentVersion} is published on npm...` ++ ); ++ const isPublished = await checkVersionOnNpm(packageName, currentVersion); ++ console.log(`Published on npm: ${isPublished}`); + +- // Always probe npm — even with changesets — so a committed-but-unpublished +- // version is detected no matter how the release got into that state (#166). ++ if (isPublished) { ++ console.log( ++ `No changesets and v${currentVersion} already published on npm — no release needed` ++ ); ++ setOutput('should_release', 'false'); ++ setOutput('skip_bump', 'false'); ++ } else { + console.log( +- `Checking if ${packageName}@${currentVersion} is published on npm...` ++ `No changesets but v${currentVersion} not yet published to npm — release needed (self-healing)` + ); +- const isPublished = await checkVersionOnNpm(packageName, currentVersion); +- console.log(`Published on npm: ${isPublished}`); +- setOutput('current_unpublished', isPublished ? 'false' : 'true'); +- +- if (hasChangesets) { +- // A changeset normally produces a NEW version via the bump step, so let +- // the bump run (skip_bump=false). If the changeset turns out to be already +- // consumed on the remote, the bump is a no-op and `current_unpublished` +- // (emitted above) still drives the catch-up publish of the current +- // version — the #166 restart case the template's design missed. +- console.log('Found changesets, proceeding with release'); +- setOutput('should_release', 'true'); +- setOutput('skip_bump', 'false'); +- return; +- } +- +- if (isPublished) { +- console.log( +- `No changesets and v${currentVersion} already published on npm — no release needed` +- ); +- setOutput('should_release', 'false'); +- setOutput('skip_bump', 'false'); +- } else { +- console.log( +- `No changesets but v${currentVersion} not yet published to npm — release needed (self-healing)` +- ); +- setOutput('should_release', 'true'); +- setOutput('skip_bump', 'true'); +- } +- } catch (error) { +- console.error('Error:', error.message); +- process.exit(1); ++ setOutput('should_release', 'true'); ++ setOutput('skip_bump', 'true'); + } + } + +-main(); ++main().catch((error) => { ++ console.error('Error:', error.message); ++ process.exit(1); ++}); +########## create-github-release.mjs ########## +--- js/scripts/create-github-release.mjs 2026-09-04 20:28:25.871207709 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/create-github-release.mjs 2026-09-04 20:31:16.598474154 +0000 +@@ -1,183 +1,309 @@ + #!/usr/bin/env bun + + /** +- * Create a JavaScript GitHub Release from CHANGELOG.md +- * Usage: bun scripts/create-github-release.mjs --release-version --repository [--tag-prefix js-v] ++ * Create GitHub Release from CHANGELOG.md ++ * Usage: node scripts/create-github-release.mjs --release-version --repository [--tag-prefix ] [--language ] [--js-root ] + * release-version: Version number (e.g., 1.0.0) + * repository: GitHub repository (e.g., owner/repo) +- * +- * Uses link-foundation libraries: +- * - use-m: Dynamic package loading without package.json dependencies +- * - command-stream: Modern shell command execution with streaming support +- * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files ++ * tag-prefix: Prefix for the git tag (default: auto-detect from layout) ++ * language: Human-readable language name for the release title (default: "JavaScript") ++ * js-root: JavaScript package root directory (auto-detected if not specified) + */ + +-import { readFileSync } from 'fs'; ++import { spawnSync } from 'node:child_process'; ++import { readFileSync } from 'node:fs'; ++import path from 'node:path'; ++import { fileURLToPath } from 'node:url'; ++ ++import { getJsRoot } from './js-paths.mjs'; ++import { readPackageInfo } from './package-info.mjs'; ++import { ++ buildReleaseTag, ++ buildReleaseTitle, ++ normalizeReleaseVersion, ++} from './release-naming.mjs'; ++ ++const USAGE = ++ 'Usage: node scripts/create-github-release.mjs --release-version --repository [--tag-prefix ] [--language ] [--js-root ]'; ++const OPTION_CONFIG_KEYS = new Map([ ++ ['--release-version', 'releaseVersion'], ++ ['--repository', 'repository'], ++ ['--tag-prefix', 'tagPrefix'], ++ ['--language', 'language'], ++ ['--js-root', 'jsRoot'], ++]); + +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); +- +-// Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); +- +-// Parse CLI arguments using lino-arguments +-// Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +-const config = makeConfig({ +- yargs: ({ yargs, getenv }) => +- yargs +- .option('release-version', { +- type: 'string', +- default: getenv('VERSION', ''), +- describe: 'Version number (e.g., 1.0.0)', +- }) +- .option('repository', { +- type: 'string', +- default: getenv('REPOSITORY', ''), +- describe: 'GitHub repository (e.g., owner/repo)', +- }) +- .option('tag-prefix', { +- type: 'string', +- default: getenv('TAG_PREFIX', 'js-v'), +- describe: 'Git tag prefix for JavaScript releases', +- }), +-}); +- +-const { releaseVersion: version, repository, tagPrefix } = config; +- +-if (!version || !repository) { +- console.error('Error: Missing required arguments'); +- console.error( +- 'Usage: bun scripts/create-github-release.mjs --release-version --repository ' +- ); +- process.exit(1); ++// Keep comfortably below GitHub's observed 125000-character release body limit. ++export const GITHUB_RELEASE_BODY_MAX_BYTES = 120_000; ++const textEncoder = new globalThis.TextEncoder(); ++ ++export function parseArgs(argv, env = process.env) { ++ const config = { ++ jsRoot: env.JS_ROOT ?? '', ++ language: env.LANGUAGE ?? 'JavaScript', ++ releaseVersion: env.VERSION ?? '', ++ repository: env.REPOSITORY ?? '', ++ tagPrefix: env.TAG_PREFIX, ++ }; ++ ++ for (let index = 0; index < argv.length; index++) { ++ const arg = argv[index]; ++ const inlineValueIndex = arg.indexOf('='); ++ ++ if (inlineValueIndex !== -1) { ++ assignOptionValue( ++ config, ++ arg.slice(0, inlineValueIndex), ++ arg.slice(inlineValueIndex + 1) ++ ); ++ continue; ++ } ++ ++ if (OPTION_CONFIG_KEYS.has(arg)) { ++ assignOptionValue(config, arg, readOptionValue(argv, index, arg)); ++ index++; ++ } ++ } ++ ++ return config; + } + +-const tag = `${tagPrefix}${version}`; ++function assignOptionValue(config, optionName, value) { ++ const configKey = OPTION_CONFIG_KEYS.get(optionName); + +-// Keep comfortably below GitHub's observed ~125000-character release-body limit. +-// A long CHANGELOG section would otherwise make the release API return 422 and +-// fail the (now correctly exit-code-checked) step even though npm already +-// published — turning a successful publish into a red job. Mirrors the js +-// pipeline template's limitReleaseNotesBytes(). +-const GITHUB_RELEASE_BODY_MAX_BYTES = 120_000; +-const textEncoder = new globalThis.TextEncoder(); ++ if (configKey) { ++ config[configKey] = value; ++ } ++} ++ ++function readOptionValue(argv, index, optionName) { ++ const value = argv[index + 1]; ++ ++ if (value === undefined || value.startsWith('--')) { ++ throw new Error(`Missing value for ${optionName}`); ++ } ++ ++ return value; ++} ++ ++function escapeRegex(value) { ++ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); ++} ++ ++export function extractReleaseNotes(changelog, version) { ++ // Read from CHANGELOG.md between this version header and the next version header. ++ const versionHeaderRegex = new RegExp( ++ `## ${escapeRegex(version)}(?=\\s|$)[\\s\\S]*?(?=## \\d|$)` ++ ); ++ const match = changelog.match(versionHeaderRegex); ++ ++ if (!match) { ++ return `Release ${version}`; ++ } ++ ++ const releaseNotes = match[0].replace(`## ${version}`, '').trim(); ++ ++ return releaseNotes || `Release ${version}`; ++} + +-/** +- * UTF-8 byte length of a string. +- * @param {string} value +- * @returns {number} +- */ + function getUtf8ByteLength(value) { + return textEncoder.encode(value).byteLength; + } + +-/** +- * Truncate a string so its UTF-8 encoding does not exceed maxBytes, never +- * splitting a multi-byte character. +- * @param {string} value +- * @param {number} maxBytes +- * @returns {string} +- */ + function truncateToUtf8Bytes(value, maxBytes) { + const chunks = []; + let usedBytes = 0; ++ + for (const character of value) { + const characterBytes = getUtf8ByteLength(character); ++ + if (usedBytes + characterBytes > maxBytes) { + break; + } ++ + chunks.push(character); + usedBytes += characterBytes; + } ++ + return chunks.join(''); + } + +-/** +- * Cap release notes to the GitHub body limit, appending a pointer to the full +- * tagged CHANGELOG when truncation happens. +- * @param {string} releaseNotes +- * @returns {string} +- */ +-function limitReleaseNotesBytes(releaseNotes) { +- if (getUtf8ByteLength(releaseNotes) <= GITHUB_RELEASE_BODY_MAX_BYTES) { ++function buildTaggedChangelogUrl(repository, tag) { ++ return `https://github.com/${repository}/blob/${tag}/CHANGELOG.md`; ++} ++ ++function buildTruncatedReleaseNotesNotice({ repository, tag }) { ++ const changelogUrl = buildTaggedChangelogUrl(repository, tag); ++ ++ return `Release notes were shortened to fit GitHub's release body limit. See the full tagged CHANGELOG.md: ${changelogUrl}`; ++} ++ ++export function limitReleaseNotesBytes({ ++ maxBytes = GITHUB_RELEASE_BODY_MAX_BYTES, ++ releaseNotes, ++ repository, ++ tag, ++}) { ++ if (getUtf8ByteLength(releaseNotes) <= maxBytes) { + return releaseNotes; + } +- const changelogUrl = `https://github.com/${repository}/blob/${tag}/CHANGELOG.md`; +- const suffix = `\n\n...\n\nRelease notes were shortened to fit GitHub's release body limit. See the full tagged CHANGELOG.md: ${changelogUrl}`; +- const availableBytes = Math.max( +- 0, +- GITHUB_RELEASE_BODY_MAX_BYTES - getUtf8ByteLength(suffix) +- ); +- const shortened = truncateToUtf8Bytes(releaseNotes, availableBytes).trimEnd(); +- const limited = `${shortened}${suffix}`; +- return getUtf8ByteLength(limited) <= GITHUB_RELEASE_BODY_MAX_BYTES +- ? limited +- : truncateToUtf8Bytes(limited, GITHUB_RELEASE_BODY_MAX_BYTES); +-} +- +-console.log(`Creating JavaScript GitHub release for ${tag}...`); +- +-try { +- // Read CHANGELOG.md +- const changelog = readFileSync('./CHANGELOG.md', 'utf8'); +- +- // Extract changelog entry for this version +- // Read from CHANGELOG.md between this version header and the next version header +- const versionHeaderRegex = new RegExp(`## ${version}[\\s\\S]*?(?=## \\d|$)`); +- const match = changelog.match(versionHeaderRegex); + +- let releaseNotes = ''; +- if (match) { +- // Remove the version header itself and trim +- releaseNotes = match[0].replace(`## ${version}`, '').trim(); +- } ++ const suffix = `\n\n...\n\n${buildTruncatedReleaseNotesNotice({ ++ repository, ++ tag, ++ })}`; ++ const suffixBytes = getUtf8ByteLength(suffix); ++ const availableBytes = Math.max(0, maxBytes - suffixBytes); ++ const shortenedNotes = truncateToUtf8Bytes( ++ releaseNotes, ++ availableBytes ++ ).trimEnd(); ++ const limitedNotes = `${shortenedNotes}${suffix}`; + +- if (!releaseNotes) { +- releaseNotes = `Release ${version}`; ++ if (getUtf8ByteLength(limitedNotes) <= maxBytes) { ++ return limitedNotes; + } + +- // Create release using GitHub API with JSON input +- // This avoids shell escaping issues that occur when passing text via command-line arguments +- // (Previously caused apostrophes like "didn't" to appear as "didn'''" in releases) +- const payload = JSON.stringify({ ++ return truncateToUtf8Bytes(limitedNotes, maxBytes); ++} ++ ++export function buildReleasePayload({ ++ changelog, ++ jsRoot = '.', ++ language, ++ packageName, ++ repository, ++ tag, ++ version, ++}) { ++ const normalizedVersion = normalizeReleaseVersion(version); ++ const releaseNotes = extractReleaseNotes(changelog, normalizedVersion); ++ ++ return JSON.stringify({ + tag_name: tag, +- name: `JavaScript ${version}`, +- body: limitReleaseNotesBytes(releaseNotes), ++ name: buildReleaseTitle(tag, { ++ jsRoot, ++ language: language ?? 'JavaScript', ++ packageName, ++ }), ++ body: limitReleaseNotesBytes({ releaseNotes, repository, tag }), + }); ++} ++ ++function formatGhOutput(result) { ++ return [result.stderr, result.stdout] ++ .filter((output) => typeof output === 'string' && output.trim()) ++ .map((output) => output.trim()) ++ .join('\n'); ++} ++ ++function getGhExitDescription(result) { ++ if (result.signal) { ++ return `signal ${result.signal}`; ++ } ++ ++ if (typeof result.status === 'number') { ++ return `code ${result.status}`; ++ } ++ ++ return 'unknown exit status'; ++} ++ ++export function createRelease({ payload, repository, spawn = spawnSync }) { ++ const result = spawn( ++ 'gh', ++ ['api', `repos/${repository}/releases`, '-X', 'POST', '--input', '-'], ++ { ++ encoding: 'utf8', ++ input: payload, ++ } ++ ); + +- // command-stream's `$` does NOT throw on a non-zero exit (errexit is off by +- // default — see issue #156), so we must inspect the result code explicitly. +- // Otherwise a failed `gh api` call would be silently reported as a created +- // release (the same false-positive class that produced #166). +- const result = +- await $`gh api repos/${repository}/releases -X POST --input -`.run({ +- stdin: payload, +- capture: true, ++ if (result.error) { ++ throw new Error(`gh api failed to start: ${result.error.message}`); ++ } ++ ++ if (result.status === 0) { ++ return { alreadyExists: false }; ++ } ++ ++ const output = formatGhOutput(result); ++ ++ if (/already_exists/i.test(output)) { ++ return { alreadyExists: true }; ++ } ++ ++ const details = output ? `:\n${output}` : ''; ++ throw new Error( ++ `gh api failed with ${getGhExitDescription(result)}${details}` ++ ); ++} ++ ++export function main({ ++ argv = process.argv.slice(2), ++ cwd = process.cwd(), ++ env = process.env, ++ spawn = spawnSync, ++ stderr = console.error, ++ stdout = console.log, ++} = {}) { ++ try { ++ const { ++ language, ++ jsRoot: configuredJsRoot, ++ releaseVersion: version, ++ repository, ++ tagPrefix, ++ } = parseArgs(argv, env); ++ ++ if (!version || !repository) { ++ stderr('Error: Missing required arguments'); ++ stderr(USAGE); ++ return 1; ++ } ++ ++ const jsRoot = getJsRoot({ jsRoot: configuredJsRoot || undefined }); ++ const tag = buildReleaseTag(version, { jsRoot, tagPrefix }); ++ const normalizedVersion = normalizeReleaseVersion(version); ++ const { name: packageName } = readPackageInfo({ jsRoot }); ++ ++ stdout(`Creating GitHub release for ${tag}...`); ++ ++ const changelogPath = ++ jsRoot === '.' ? 'CHANGELOG.md' : path.join(jsRoot, 'CHANGELOG.md'); ++ const changelog = readFileSync(path.join(cwd, changelogPath), 'utf8'); ++ const payload = buildReleasePayload({ ++ changelog, ++ jsRoot, ++ language, ++ packageName, ++ repository, ++ tag, ++ version: normalizedVersion, + }); ++ const result = createRelease({ payload, repository, spawn }); + +- if (result.code !== 0) { +- // Idempotency: a self-healing re-run (or a retried job) may try to create a +- // release whose tag already exists. GitHub returns 422 already_exists; that +- // is a success for our purposes, not a failure — so the publish does not +- // get turned into a red job on re-run. Mirrors the template's behaviour. +- const combinedOutput = +- `${result.stderr || ''}\n${result.stdout || ''}`.trim(); +- if (/already_exists/i.test(combinedOutput)) { +- console.log( +- `JavaScript GitHub release already exists: ${tag}. Skipping creation.` +- ); +- } else { +- throw new Error( +- `gh api failed to create release ${tag} (exit code ${result.code}): ${result.stderr?.trim() || 'no stderr'}` +- ); ++ if (result.alreadyExists) { ++ stdout(`GitHub release already exists: ${tag}. Skipping creation.`); ++ return 0; + } +- } else { +- console.log(`Created JavaScript GitHub release: ${tag}`); ++ ++ stdout(`\u2705 Created GitHub release: ${tag}`); ++ return 0; ++ } catch (error) { ++ stderr(`Error creating release: ${error.message}`); ++ return 1; + } +-} catch (error) { +- console.error('Error creating release:', error.message); +- process.exit(1); ++} ++ ++function isCliEntryPoint() { ++ return ( ++ typeof process !== 'undefined' && ++ process.argv?.[1] && ++ fileURLToPath(import.meta.url) === path.resolve(process.argv[1]) ++ ); ++} ++ ++if (isCliEntryPoint()) { ++ process.exitCode = main(); + } +########## create-manual-changeset.mjs ########## +--- js/scripts/create-manual-changeset.mjs 2026-09-04 20:28:25.871207709 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/create-manual-changeset.mjs 2026-09-04 20:31:16.598474154 +0000 +@@ -2,9 +2,7 @@ + + /** + * Create a changeset file for manual releases +- * Usage: bun scripts/create-manual-changeset.mjs --bump-type [--description ] +- * +- * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json ++ * Usage: node scripts/create-manual-changeset.mjs --bump-type [--description ] [--js-root ] + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies +@@ -12,19 +10,17 @@ + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +-import { writeFileSync } from 'fs'; ++import { mkdirSync, writeFileSync } from 'fs'; + import { randomBytes } from 'crypto'; ++import { join } from 'path'; + +-const PACKAGE_NAME = 'command-stream'; +- +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { getChangesetDir, getJsRoot, parseJsRootConfig } from './js-paths.mjs'; ++import { formatChangesetHeader, readPackageInfo } from './package-info.mjs'; ++import { loadCommandStream, loadLinoArguments } from './use-module.mjs'; + + // Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); ++const { $ } = await loadCommandStream(); ++const { makeConfig } = await loadLinoArguments(); + + // Parse CLI arguments using lino-arguments + const config = makeConfig({ +@@ -40,34 +36,46 @@ + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Description for the changeset', ++ }) ++ .option('js-root', { ++ type: 'string', ++ default: getenv('JS_ROOT', ''), ++ describe: ++ 'JavaScript package root directory (auto-detected if not specified)', + }), + }); + + try { +- const { bumpType, description: descriptionArg } = config; ++ const { bumpType, description: descriptionArg, jsRoot: jsRootArg } = config; + + // Use provided description or default based on bump type + const description = descriptionArg || `Manual ${bumpType} release`; + + if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( +- 'Usage: bun scripts/create-manual-changeset.mjs --bump-type [--description ]' ++ 'Usage: node scripts/create-manual-changeset.mjs --bump-type [--description ]' + ); + process.exit(1); + } + ++ const jsRootConfig = jsRootArg || parseJsRootConfig(); ++ const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++ const changesetDir = getChangesetDir({ jsRoot }); ++ const { name: packageName } = readPackageInfo({ jsRoot }); ++ + // Generate a random changeset ID + const changesetId = randomBytes(4).toString('hex'); +- const changesetFile = `.changeset/manual-release-${changesetId}.md`; ++ const changesetFile = join(changesetDir, `manual-release-${changesetId}.md`); + + // Create the changeset file with single quotes to match Prettier config + const content = `--- +-'${PACKAGE_NAME}': ${bumpType} ++${formatChangesetHeader(packageName, bumpType)} + --- + + ${description} + `; + ++ mkdirSync(changesetDir, { recursive: true }); + writeFileSync(changesetFile, content, 'utf-8'); + + console.log(`Created changeset: ${changesetFile}`); +@@ -76,7 +84,7 @@ + + // Format with Prettier + console.log('\nFormatting with Prettier...'); +- await $`bunx prettier --write "${changesetFile}"`; ++ await $`npx prettier --write "${changesetFile}"`; + + console.log('\n✅ Changeset created and formatted successfully'); + } catch (error) { +########## format-github-release.mjs ########## +--- js/scripts/format-github-release.mjs 2026-09-04 20:28:25.871207709 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/format-github-release.mjs 2026-09-04 20:31:16.598474154 +0000 +@@ -1,11 +1,13 @@ + #!/usr/bin/env bun + + /** +- * Format JavaScript GitHub release notes using the format-release-notes.mjs script +- * Usage: bun scripts/format-github-release.mjs --release-version --repository --commit-sha [--tag-prefix js-v] ++ * Format GitHub release notes using the format-release-notes.mjs script ++ * Usage: node scripts/format-github-release.mjs --release-version --repository --commit-sha [--tag-prefix ] [--js-root ] + * release-version: Version number (e.g., 1.0.0) + * repository: GitHub repository (e.g., owner/repo) + * commit_sha: Commit SHA for PR detection ++ * tag-prefix: Prefix for the git tag (default: auto-detect from layout) ++ * js-root: JavaScript package root directory (auto-detected if not specified) + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies +@@ -13,14 +15,13 @@ + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files + */ + +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { getJsRoot, parseJsRootConfig } from './js-paths.mjs'; ++import { buildReleaseTag, normalizeReleaseVersion } from './release-naming.mjs'; ++import { loadCommandStream, loadLinoArguments } from './use-module.mjs'; + + // Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); ++const { $ } = await loadCommandStream(); ++const { makeConfig } = await loadLinoArguments(); + + // Parse CLI arguments using lino-arguments + // Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +@@ -44,22 +45,41 @@ + }) + .option('tag-prefix', { + type: 'string', +- default: getenv('TAG_PREFIX', 'js-v'), +- describe: 'Git tag prefix for JavaScript releases', ++ default: getenv('TAG_PREFIX', ''), ++ describe: 'Prefix for the git tag (auto-detected when omitted)', ++ }) ++ .option('js-root', { ++ type: 'string', ++ default: getenv('JS_ROOT', ''), ++ describe: ++ 'JavaScript package root directory (auto-detected if not specified)', + }), + }); + +-const { releaseVersion: version, repository, commitSha, tagPrefix } = config; ++const { ++ releaseVersion: version, ++ repository, ++ commitSha, ++ tagPrefix: configuredTagPrefix, ++ jsRoot: configuredJsRoot, ++} = config; + + if (!version || !repository || !commitSha) { + console.error('Error: Missing required arguments'); + console.error( +- 'Usage: bun scripts/format-github-release.mjs --release-version --repository --commit-sha ' ++ 'Usage: node scripts/format-github-release.mjs --release-version --repository --commit-sha [--tag-prefix ] [--js-root ]' + ); + process.exit(1); + } + +-const tag = `${tagPrefix}${version}`; ++const jsRoot = getJsRoot({ ++ jsRoot: configuredJsRoot || parseJsRootConfig() || undefined, ++}); ++const tag = buildReleaseTag(version, { ++ jsRoot, ++ tagPrefix: configuredTagPrefix || undefined, ++}); ++const normalizedVersion = normalizeReleaseVersion(version); + + try { + // Get the release ID for this version +@@ -76,11 +96,11 @@ + } + + if (releaseId) { +- console.log(`Formatting JavaScript release notes for ${tag}...`); ++ console.log(`Formatting release notes for ${tag}...`); + // Pass the trigger commit SHA for PR detection + // This allows proper PR lookup even if the changelog doesn't have a commit hash +- await $`bun scripts/format-release-notes.mjs --release-id "${releaseId}" --release-version "${tag}" --repository "${repository}" --commit-sha "${commitSha}"`; +- console.log(`Formatted JavaScript release notes for ${tag}`); ++ await $`node scripts/format-release-notes.mjs --release-id "${releaseId}" --release-version "${normalizedVersion}" --repository "${repository}" --commit-sha "${commitSha}"`; ++ console.log(`\u2705 Formatted release notes for ${tag}`); + } + } catch (error) { + console.error('Error formatting release:', error.message); +########## format-release-notes.mjs ########## +--- js/scripts/format-release-notes.mjs 2026-09-04 20:28:25.871207709 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/format-release-notes.mjs 2026-09-04 20:31:16.598474154 +0000 +@@ -1,4 +1,4 @@ +-#!/usr/bin/env bun ++#!/usr/bin/env node + + /** + * Script to format GitHub release notes with proper formatting: +@@ -7,8 +7,6 @@ + * - Add shields.io NPM version badge + * - Format nicely with proper markdown + * +- * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json +- * + * PR Detection Logic: + * 1. Extract commit hash from changelog entry (if present) + * 2. Fall back to --commit-sha argument (passed from workflow) +@@ -23,16 +21,17 @@ + * Note: Uses --release-version instead of --version to avoid conflict with yargs' built-in --version flag. + */ + +-const PACKAGE_NAME = 'command-stream'; +- +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { getJsRoot, parseJsRootConfig } from './js-paths.mjs'; ++import { readPackageInfo } from './package-info.mjs'; ++import { ++ buildNpmVersionBadge, ++ normalizeReleaseVersionForBadge, ++} from './format-release-notes-helpers.mjs'; ++import { loadCommandStream, loadLinoArguments } from './use-module.mjs'; + + // Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); ++const { $ } = await loadCommandStream(); ++const { makeConfig } = await loadLinoArguments(); + + // Parse CLI arguments using lino-arguments + // Note: Using --release-version instead of --version to avoid conflict with yargs' built-in --version flag +@@ -58,6 +57,12 @@ + type: 'string', + default: getenv('COMMIT_SHA', ''), + describe: 'Commit SHA for PR detection', ++ }) ++ .option('js-root', { ++ type: 'string', ++ default: getenv('JS_ROOT', ''), ++ describe: ++ 'JavaScript package root directory (auto-detected if not specified)', + }), + }); + +@@ -65,6 +70,9 @@ + const version = config.releaseVersion; + const repository = config.repository; + const passedCommitSha = config.commitSha; ++const jsRootConfig = config.jsRoot || parseJsRootConfig(); ++const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++const { name: packageName } = readPackageInfo({ jsRoot }); + + if (!releaseId || !version || !repository) { + console.error( +@@ -188,8 +196,8 @@ + } + + // Build formatted release notes +- const versionWithoutV = version.replace(/^js-v/, '').replace(/^v/, ''); +- const npmBadge = `[![npm version](https://img.shields.io/badge/npm-${versionWithoutV}-blue.svg)](https://www.npmjs.com/package/${PACKAGE_NAME}/v/${versionWithoutV})`; ++ const versionWithoutV = normalizeReleaseVersionForBadge(version); ++ const npmBadge = buildNpmVersionBadge(packageName, version); + + let formattedBody = `${cleanDescription}`; + +########## instant-version-bump.mjs ########## +--- js/scripts/instant-version-bump.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/instant-version-bump.mjs 2026-09-04 20:31:16.598474154 +0000 +@@ -4,24 +4,37 @@ + * Instant version bump script for manual releases + * Bypasses the changeset workflow and directly updates version and changelog + * +- * Usage: bun scripts/instant-version-bump.mjs --bump-type [--description ] ++ * Usage: node scripts/instant-version-bump.mjs --bump-type [--description ] [--js-root ] ++ * ++ * Configuration: ++ * - CLI: --js-root to explicitly set JavaScript root ++ * - Environment: JS_ROOT= + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files ++ * ++ * Addresses issues documented in: ++ * - Issue #21: Supporting both single and multi-language repository structures ++ * - Reference: link-assistant/agent PR #112 (--legacy-peer-deps fix) ++ * - Reference: link-assistant/agent PR #114 (configurable package root) + */ + + import { readFileSync, writeFileSync } from 'fs'; ++import { join } from 'path'; + +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { ++ getJsRoot, ++ getPackageJsonPath, ++ needsCd, ++ parseJsRootConfig, ++} from './js-paths.mjs'; ++import { loadCommandStream, loadLinoArguments } from './use-module.mjs'; + + // Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); ++const { $ } = await loadCommandStream(); ++const { makeConfig } = await loadLinoArguments(); + + // Parse CLI arguments using lino-arguments + const config = makeConfig({ +@@ -37,16 +50,31 @@ + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Description for the version bump', ++ }) ++ .option('js-root', { ++ type: 'string', ++ default: getenv('JS_ROOT', ''), ++ describe: ++ 'JavaScript package root directory (auto-detected if not specified)', + }), + }); + ++// Store the original working directory to restore after cd commands ++// IMPORTANT: command-stream's cd is a virtual command that calls process.chdir() ++const originalCwd = process.cwd(); ++ + try { +- const { bumpType, description } = config; ++ const { bumpType, description, jsRoot: jsRootArg } = config; ++ ++ // Get JavaScript package root (auto-detect or use explicit config) ++ const jsRootConfig = jsRootArg || parseJsRootConfig(); ++ const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++ + const finalDescription = description || `Manual ${bumpType} release`; + + if (!bumpType || !['major', 'minor', 'patch'].includes(bumpType)) { + console.error( +- 'Usage: bun scripts/instant-version-bump.mjs --bump-type [--description ]' ++ 'Usage: node scripts/instant-version-bump.mjs --bump-type [--description ] [--js-root ]' + ); + process.exit(1); + } +@@ -54,21 +82,29 @@ + console.log(`\nBumping version (${bumpType})...`); + + // Get current version +- const packageJson = JSON.parse(readFileSync('package.json', 'utf-8')); ++ const packageJsonPath = getPackageJsonPath({ jsRoot }); ++ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const oldVersion = packageJson.version; + console.log(`Current version: ${oldVersion}`); + + // Bump version using npm version (doesn't create git tag) +- await $`npm version ${bumpType} --no-git-tag-version`; ++ // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after ++ if (needsCd({ jsRoot })) { ++ await $`cd ${jsRoot} && npm version ${bumpType} --no-git-tag-version`; ++ process.chdir(originalCwd); ++ } else { ++ await $`npm version ${bumpType} --no-git-tag-version`; ++ } + + // Get new version +- const updatedPackageJson = JSON.parse(readFileSync('package.json', 'utf-8')); ++ const updatedPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const newVersion = updatedPackageJson.version; + console.log(`New version: ${newVersion}`); + + // Update CHANGELOG.md + console.log('\nUpdating CHANGELOG.md...'); +- const changelogPath = 'CHANGELOG.md'; ++ const changelogPath = ++ jsRoot === '.' ? 'CHANGELOG.md' : join(jsRoot, 'CHANGELOG.md'); + let changelog = readFileSync(changelogPath, 'utf-8'); + + // Create new changelog entry +@@ -106,18 +142,24 @@ + writeFileSync(changelogPath, changelog, 'utf-8'); + console.log('✅ CHANGELOG.md updated'); + +- // Synchronize package-lock.json if it exists +- try { +- console.log('\nSynchronizing package-lock.json...'); +- await $`npm install --package-lock-only`; +- } catch { +- // No package-lock.json or npm not available, skip +- console.log('Skipping package-lock.json sync (not applicable)'); ++ // Synchronize package-lock.json ++ console.log('\nSynchronizing package-lock.json...'); ++ ++ // Use --legacy-peer-deps to handle peer dependency conflicts ++ // This addresses npm ERESOLVE errors documented in issue #111 / PR #112 ++ // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after ++ if (needsCd({ jsRoot })) { ++ await $`cd ${jsRoot} && npm install --package-lock-only --legacy-peer-deps`; ++ process.chdir(originalCwd); ++ } else { ++ await $`npm install --package-lock-only --legacy-peer-deps`; + } + + console.log('\n✅ Instant version bump complete'); + console.log(`Version: ${oldVersion} → ${newVersion}`); + } catch (error) { ++ // Restore cwd on error ++ process.chdir(originalCwd); + console.error('Error during instant version bump:', error.message); + if (process.env.DEBUG) { + console.error('Stack trace:', error.stack); +########## merge-changesets.mjs ########## +--- js/scripts/merge-changesets.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/merge-changesets.mjs 2026-09-04 20:31:16.600474133 +0000 +@@ -9,11 +9,10 @@ + * - Preserves all descriptions in chronological order (by file modification time) + * - Removes the individual changeset files after merging + * - Does nothing if there's only one or no changesets ++ * - Fails if any changeset cannot be parsed + * + * This script is run before `changeset version` to ensure a clean release + * even when multiple PRs have merged before a release cycle. +- * +- * IMPORTANT: Update the package name below to match your package.json + */ + + import { +@@ -25,8 +24,12 @@ + } from 'fs'; + import { join } from 'path'; + +-const PACKAGE_NAME = 'command-stream'; +-const CHANGESET_DIR = '.changeset'; ++import { getChangesetDir, getJsRoot, parseJsRootConfig } from './js-paths.mjs'; ++import { ++ formatChangesetHeader, ++ getChangesetVersionTypeRegex, ++ readPackageInfo, ++} from './package-info.mjs'; + + // Version bump type priority (higher number = higher priority) + const BUMP_PRIORITY = { +@@ -112,41 +115,45 @@ + /** + * Parse a changeset file and extract its metadata + * @param {string} filePath +- * @returns {{type: string, description: string, mtime: Date} | null} ++ * @param {string} packageName ++ * @returns {{type: string, description: string, mtime: Date}} + */ +-function parseChangeset(filePath) { ++function parseChangeset(filePath, packageName) { ++ let content; ++ let stats; ++ + try { +- const content = readFileSync(filePath, 'utf-8'); +- const stats = statSync(filePath); ++ content = readFileSync(filePath, 'utf-8'); ++ stats = statSync(filePath); ++ } catch (error) { ++ throw new Error(`Failed to read ${filePath}: ${error.message}`); ++ } + +- // Extract version type - support both quoted and unquoted package names +- const versionTypeRegex = new RegExp( +- `^['"]?${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]?:\\s+(major|minor|patch)`, +- 'm' ++ // Extract version type - support both quoted and unquoted package names ++ const versionTypeRegex = getChangesetVersionTypeRegex(packageName, { ++ requireQuotes: false, ++ }); ++ const versionTypeMatch = content.match(versionTypeRegex); ++ ++ if (!versionTypeMatch) { ++ throw new Error( ++ `Could not parse version type from ${filePath}. Expected a changeset header like ${formatChangesetHeader( ++ packageName, ++ 'patch' ++ )} with version type major, minor, or patch.` + ); +- const versionTypeMatch = content.match(versionTypeRegex); +- +- if (!versionTypeMatch) { +- console.warn( +- `Warning: Could not parse version type from ${filePath}, skipping` +- ); +- return null; +- } +- +- // Extract description +- const parts = content.split('---'); +- const description = +- parts.length >= 3 ? parts.slice(2).join('---').trim() : ''; +- +- return { +- type: versionTypeMatch[1], +- description, +- mtime: stats.mtime, +- }; +- } catch (error) { +- console.warn(`Warning: Failed to parse ${filePath}: ${error.message}`); +- return null; + } ++ ++ // Extract description ++ const parts = content.split('---'); ++ const description = ++ parts.length >= 3 ? parts.slice(2).join('---').trim() : ''; ++ ++ return { ++ type: versionTypeMatch[1], ++ description, ++ mtime: stats.mtime, ++ }; + } + + /** +@@ -168,13 +175,14 @@ + * Create a merged changeset file + * @param {string} type + * @param {string[]} descriptions ++ * @param {string} packageName + * @returns {string} + */ +-function createMergedChangeset(type, descriptions) { ++function createMergedChangeset(type, descriptions, packageName) { + const combinedDescription = descriptions.join('\n\n'); + + return `--- +-'${PACKAGE_NAME}': ${type} ++${formatChangesetHeader(packageName, type)} + --- + + ${combinedDescription} +@@ -183,9 +191,14 @@ + + function main() { + console.log('Checking for multiple changesets to merge...'); ++ const jsRootConfig = parseJsRootConfig(); ++ const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++ const changesetDir = getChangesetDir({ jsRoot }); ++ const { name: packageName } = readPackageInfo({ jsRoot }); ++ console.log(`Package: ${packageName}`); + + // Get all changeset files +- const changesetFiles = readdirSync(CHANGESET_DIR).filter( ++ const changesetFiles = readdirSync(changesetDir).filter( + (file) => file.endsWith('.md') && file !== 'README.md' + ); + +@@ -203,8 +216,8 @@ + // Parse all changesets + const parsedChangesets = []; + for (const file of changesetFiles) { +- const filePath = join(CHANGESET_DIR, file); +- const parsed = parseChangeset(filePath); ++ const filePath = join(changesetDir, file); ++ const parsed = parseChangeset(filePath, packageName); + if (parsed) { + parsedChangesets.push({ + file, +@@ -238,11 +251,15 @@ + console.log(` Descriptions to merge: ${descriptions.length}`); + + // Create merged changeset content +- const mergedContent = createMergedChangeset(highestBumpType, descriptions); ++ const mergedContent = createMergedChangeset( ++ highestBumpType, ++ descriptions, ++ packageName ++ ); + + // Generate a unique name for the merged changeset + const mergedFileName = `merged-${generateChangesetName()}.md`; +- const mergedFilePath = join(CHANGESET_DIR, mergedFileName); ++ const mergedFilePath = join(changesetDir, mergedFileName); + + // Write the merged changeset + writeFileSync(mergedFilePath, mergedContent); +@@ -259,4 +276,12 @@ + console.log(`\nMerged changeset content:\n${mergedContent}`); + } + +-main(); ++try { ++ main(); ++} catch (error) { ++ console.error(`::error::${error.message}`); ++ if (process.env.DEBUG) { ++ console.error('Stack trace:', error.stack); ++ } ++ process.exit(1); ++} +########## publish-to-npm.mjs ########## +--- js/scripts/publish-to-npm.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/publish-to-npm.mjs 2026-09-04 20:31:16.600474133 +0000 +@@ -2,86 +2,108 @@ + + /** + * Publish to npm using OIDC trusted publishing +- * Usage: bun scripts/publish-to-npm.mjs [--should-pull] ++ * Usage: node scripts/publish-to-npm.mjs [--should-pull] [--js-root ] + * should_pull: Optional flag to pull latest changes before publishing (for release job) + * +- * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json +- * +- * Reliable success detection (prevents false-positive releases): +- * command-stream's `$` does NOT throw on a non-zero exit code (errexit is +- * off by default — see issue #156). A bare `await $`cmd`` therefore never +- * rejects, so a try/catch around it can never observe a failure. The previous +- * version of this script relied on that catch, so a failed `changeset publish` +- * (e.g. npm E404) was silently reported as a success — which created a +- * GitHub release (`js-v0.10.1`) for a version that never reached npm (#166). +- * +- * This version mirrors the multi-layer detection used by the pipeline +- * template (link-foundation/js-ai-driven-development-pipeline-template, +- * originally link-assistant/agent PR #116): +- * 1. scan the captured output for known failure patterns, +- * 2. check the captured exit code, and +- * 3. verify the version is actually visible on npm with `npm view`. +- * A publish is only reported when all three layers pass. ++ * Configuration: ++ * - CLI: --js-root to explicitly set JavaScript root ++ * - Environment: JS_ROOT= + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files ++ * ++ * Supports both single and multi-language repository structures via a ++ * configurable package root. + */ + +-import { readFileSync, appendFileSync } from 'fs'; ++import { appendFileSync } from 'fs'; + +-const PACKAGE_NAME = 'command-stream'; +- +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { getJsRoot, needsCd, parseJsRootConfig } from './js-paths.mjs'; ++import { isPackageVersionPublished } from './npm-registry.mjs'; ++import { readPackageInfo } from './package-info.mjs'; ++import { ++ buildAuthFailureGuidance, ++ isNonRetryableFailure, ++} from './publish-failure-classifier.mjs'; ++import { ++ isAlreadyPublishedError, ++ publishWithRetry, ++ sleep, ++} from './publish-retry.mjs'; ++import { loadCommandStream, loadLinoArguments } from './use-module.mjs'; + + // Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); ++const { $ } = await loadCommandStream(); ++const { makeConfig } = await loadLinoArguments(); + + // Parse CLI arguments using lino-arguments + const config = makeConfig({ + yargs: ({ yargs, getenv }) => +- yargs.option('should-pull', { +- type: 'boolean', +- default: getenv('SHOULD_PULL', false), +- describe: 'Pull latest changes before publishing', +- }), ++ yargs ++ .option('should-pull', { ++ type: 'boolean', ++ default: getenv('SHOULD_PULL', false), ++ describe: 'Pull latest changes before publishing', ++ }) ++ .option('js-root', { ++ type: 'string', ++ default: getenv('JS_ROOT', ''), ++ describe: ++ 'JavaScript package root directory (auto-detected if not specified)', ++ }), + }); + +-const { shouldPull } = config; ++const { shouldPull, jsRoot: jsRootArg } = config; ++ ++// Get JavaScript package root (auto-detect or use explicit config) ++const jsRootConfig = jsRootArg || parseJsRootConfig(); ++const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++ + const MAX_RETRIES = 3; +-// Configurable so tests can run the retry loop without waiting (see +-// tests/publish-to-npm.test.mjs). Defaults to 10s for real CI runs. +-const RETRY_DELAY = Number(process.env.PUBLISH_RETRY_DELAY ?? 10000); // ms +-// Wait for the npm registry to propagate before verifying a fresh publish. +-const VERIFY_DELAY = Number(process.env.PUBLISH_VERIFY_DELAY ?? 2000); // ms +- +-// Patterns that indicate a publish failure in the changeset/npm output. +-// `changeset publish` can print these and still exit 0 in some npm versions, +-// so output scanning is the most reliable first line of defense. +-// Reference: link-assistant/agent PR #116 — prevent false positives in CI/CD. ++const RETRY_DELAY = 10000; // 10 seconds ++ ++// Store the original working directory to restore after cd commands ++// IMPORTANT: command-stream's cd is a virtual command that calls process.chdir() ++const originalCwd = process.cwd(); ++ ++// Patterns that indicate publish failure in changeset output ++// Guards against false positives in CI/CD output parsing. + const FAILURE_PATTERNS = [ + 'packages failed to publish', + 'error occurred while publishing', +- 'npm error code e', ++ 'npm error code E', + 'npm error 404', + 'npm error 401', + 'npm error 403', +- 'access token expired', +- 'eneedauth', +- 'exited with code 1', ++ 'Access token expired', ++ 'ENEEDAUTH', + ]; + + /** +- * Sleep for specified milliseconds +- * @param {number} ms ++ * Check if the output contains any failure patterns ++ * @param {string} output - Combined stdout and stderr ++ * @returns {string|null} - The matched failure pattern or null if no failure detected + */ +-function sleep(ms) { +- return new Promise((resolve) => globalThis.setTimeout(resolve, ms)); ++function detectPublishFailure(output) { ++ const lowerOutput = output.toLowerCase(); ++ for (const pattern of FAILURE_PATTERNS) { ++ if (lowerOutput.includes(pattern.toLowerCase())) { ++ return pattern; ++ } ++ } ++ return null; ++} ++ ++/** ++ * Verify that a package version is published on npm ++ * @param {string} packageName ++ * @param {string} version ++ * @returns {Promise} ++ */ ++function verifyPublished(packageName, version) { ++ return isPackageVersionPublished(packageName, version); + } + + /** +@@ -97,78 +119,107 @@ + } + + /** +- * Check if the combined output contains any known failure pattern. +- * @param {string} output - Combined stdout and stderr +- * @returns {string|null} - The matched failure pattern, or null when clean ++ * Run changeset:publish command with output capture ++ * @param {Function} shell ++ * @param {string} jsRoot ++ * @param {string} originalCwd ++ * @returns {Promise<{result: object|null, error: Error|null}>} + */ +-function detectPublishFailure(output) { +- const lowerOutput = output.toLowerCase(); +- for (const pattern of FAILURE_PATTERNS) { +- if (lowerOutput.includes(pattern)) { +- return pattern; ++async function runChangesetPublish(shell, jsRoot, originalCwd) { ++ try { ++ // Run changeset:publish from the js directory where package.json with this script exists ++ // IMPORTANT: Use .run({ capture: true }) to capture output for failure detection ++ // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after ++ if (needsCd({ jsRoot })) { ++ const result = await shell`cd ${jsRoot} && npm run changeset:publish`.run( ++ { ++ capture: true, ++ } ++ ); ++ process.chdir(originalCwd); ++ return { result, error: null }; ++ } ++ const result = await shell`npm run changeset:publish`.run({ ++ capture: true, ++ }); ++ return { result, error: null }; ++ } catch (error) { ++ // Restore cwd on error before retry ++ if (needsCd({ jsRoot })) { ++ process.chdir(originalCwd); + } ++ return { result: null, error }; + } +- return null; + } + + /** +- * Verify a package version is actually published on npm. +- * @param {string} packageName +- * @param {string} version +- * @returns {Promise} ++ * Analyze publish result for failures using multi-layer detection ++ * @param {object|null} publishResult - The result from runChangesetPublish ++ * @param {Error|null} commandError - Error thrown by the command ++ * @returns {Error|null} - Error if failure detected, null otherwise + */ +-async function verifyPublished(packageName, version) { +- const result = await $`npm view "${packageName}@${version}" version`.run({ +- capture: true, +- }); +- return result.code === 0 && result.stdout.trim().includes(version); ++function analyzePublishResult(publishResult, commandError) { ++ if (commandError) { ++ return commandError; ++ } ++ ++ const combinedOutput = publishResult ++ ? `${publishResult.stdout || ''}\n${publishResult.stderr || ''}` ++ : ''; ++ ++ // Log the output for debugging ++ if (combinedOutput.trim()) { ++ console.log('Changeset output:', combinedOutput); ++ } ++ ++ // Check for failure patterns in output (most reliable for changeset) ++ const failurePattern = detectPublishFailure(combinedOutput); ++ if (failurePattern) { ++ console.error(`Detected publish failure: "${failurePattern}"`); ++ return new Error(`Publish failed: detected "${failurePattern}" in output`); ++ } ++ ++ // Check exit code (if available and non-zero) ++ if (publishResult && publishResult.code !== 0) { ++ console.error(`Changeset exited with code ${publishResult.code}`); ++ return new Error(`Publish failed with exit code ${publishResult.code}`); ++ } ++ ++ return null; + } + + /** +- * Run `changeset:publish` once and decide whether it really succeeded. +- * +- * command-stream does not throw on non-zero exits, so we capture the output +- * and apply three independent checks before trusting the result. +- * +- * @param {string} packageName +- * @param {string} version +- * @returns {Promise<{success: boolean, error: Error|null}>} ++ * Run a single publish command invocation (no verification). ++ * Verification is a separate failure domain handled by publishWithRetry. ++ * @param {Function} shell ++ * @param {string} jsRoot ++ * @param {string} originalCwd ++ * @returns {Promise<{success: boolean, error: Error|null, output: string}>} + */ +-async function attemptPublish(packageName, version) { +- // IMPORTANT: capture:true mirrors output to the console *and* returns it, +- // so CI logs stay readable while we still get the text and exit code. +- const result = await $`bun run changeset:publish`.run({ capture: true }); +- +- const combinedOutput = `${result.stdout || ''}\n${result.stderr || ''}`; ++async function runPublishCommand(shell, jsRoot, originalCwd) { ++ const { result, error } = await runChangesetPublish( ++ shell, ++ jsRoot, ++ originalCwd ++ ); ++ const analysisError = analyzePublishResult(result, error); ++ const output = [ ++ analysisError?.message || '', ++ result?.stdout || '', ++ result?.stderr || '', ++ ].join('\n'); ++ ++ if (analysisError) { ++ // Mark authentication / registry-configuration failures as non-retryable so ++ // the retry loop can fail fast with actionable guidance without burning ++ // through MAX_RETRIES. ++ if (!isAlreadyPublishedError(output) && isNonRetryableFailure(output)) { ++ analysisError.nonRetryable = true; ++ } ++ return { success: false, error: analysisError, output }; ++ } + +- // Layer 1: scan output for known failure signatures. +- const failurePattern = detectPublishFailure(combinedOutput); +- if (failurePattern) { +- return { +- success: false, +- error: new Error(`detected "${failurePattern}" in publish output`), +- }; +- } +- +- // Layer 2: trust the exit code when it is non-zero. +- if (result.code !== 0) { +- return { +- success: false, +- error: new Error(`changeset publish exited with code ${result.code}`), +- }; +- } +- +- // Layer 3: confirm the version is really on npm (the ultimate check). +- console.log('Verifying package was published to npm...'); +- await sleep(VERIFY_DELAY); +- if (await verifyPublished(packageName, version)) { +- return { success: true, error: null }; +- } +- +- return { +- success: false, +- error: new Error('version not found on npm after publish attempt'), +- }; ++ return { success: true, error: null, output }; + } + + async function main() { +@@ -179,74 +230,68 @@ + } + + // Get current version +- const packageJson = JSON.parse(readFileSync('./package.json', 'utf8')); +- const currentVersion = packageJson.version; ++ const { name: packageName, version: currentVersion } = readPackageInfo({ ++ jsRoot, ++ }); ++ console.log(`Package to publish: ${packageName}`); + console.log(`Current version to publish: ${currentVersion}`); + + // Check if this version is already published on npm + console.log( + `Checking if version ${currentVersion} is already published...` + ); +- const checkResult = +- await $`npm view "${PACKAGE_NAME}@${currentVersion}" version`.run({ +- capture: true, +- }); +- +- // command-stream returns { code: 0 } on success, { code: 1 } on failure (e.g., E404) +- // Exit code 0 means version exists, non-zero means version not found +- if (checkResult.code === 0) { ++ const isAlreadyPublished = await isPackageVersionPublished( ++ packageName, ++ currentVersion ++ ); ++ ++ if (isAlreadyPublished) { + console.log(`Version ${currentVersion} is already published to npm`); + setOutput('published', 'true'); + setOutput('published_version', currentVersion); + setOutput('already_published', 'true'); + return; +- } else { +- // Version not found on npm (E404), proceed with publish +- console.log( +- `Version ${currentVersion} not found on npm, proceeding with publish...` +- ); + } + +- // Publish to npm using OIDC trusted publishing with retry logic. +- // Multi-layer failure detection prevents false-positive releases (#166). +- for (let i = 1; i <= MAX_RETRIES; i++) { +- console.log(`Publish attempt ${i} of ${MAX_RETRIES}...`); +- const { success, error } = await attemptPublish( +- PACKAGE_NAME, +- currentVersion +- ); +- +- if (success) { +- setOutput('published', 'true'); +- setOutput('published_version', currentVersion); +- console.log(`✅ Published ${PACKAGE_NAME}@${currentVersion} to npm`); +- return; +- } +- +- if (i < MAX_RETRIES) { +- console.log( +- `Publish failed: ${error.message}, waiting ${RETRY_DELAY / 1000}s before retry...` +- ); +- await sleep(RETRY_DELAY); +- } else { +- console.error(`Publish attempt ${i} failed: ${error.message}`); +- } +- } +- +- console.error(`❌ Failed to publish after ${MAX_RETRIES} attempts`); +- console.error( +- 'Hint: an npm E404 on PUT usually means OIDC trusted publishing is not ' + +- 'configured for this workflow file. npm allows only one workflow file ' + +- 'as a trusted publisher; if the release workflow was renamed (e.g. ' + +- 'release.yml -> js.yml), update the trusted publisher on npmjs.com. ' + +- 'See docs/case-studies/issue-166/README.md.' ++ // Version not found on npm (E404), proceed with publish ++ console.log( ++ `Version ${currentVersion} not found on npm, proceeding with publish...` + ); +- // Ensure no false-positive output leaks to the release job. +- setOutput('published', 'false'); ++ ++ // Publish to npm using OIDC trusted publishing with retry logic ++ // Multi-layer failure detection guards against a publish command that ++ // reports success without actually publishing. ++ // ++ // The publish command is retried only when the publish itself failed. ++ // A verification miss is handled by bounded polling and never triggers a ++ // republish. ++ const { success, error } = await publishWithRetry({ ++ publish: () => runPublishCommand($, jsRoot, originalCwd), ++ verify: () => verifyPublished(packageName, currentVersion), ++ maxRetries: MAX_RETRIES, ++ retryDelay: RETRY_DELAY, ++ sleepFn: sleep, ++ log: (message) => console.log(message), ++ }); ++ ++ if (success) { ++ setOutput('published', 'true'); ++ setOutput('published_version', currentVersion); ++ console.log(`\u2705 Published ${packageName}@${currentVersion} to npm`); ++ return; ++ } ++ ++ console.error(`\u274C Publish failed: ${error.message}`); ++ // Authentication / registry-configuration errors will not be fixed by ++ // retrying, so print actionable guidance for the operator. ++ if (error?.nonRetryable && !error?.verificationFailed) { ++ console.error(buildAuthFailureGuidance(packageName)); ++ } + process.exit(1); + } catch (error) { ++ // Restore cwd on error ++ process.chdir(originalCwd); + console.error('Error:', error.message); +- setOutput('published', 'false'); + process.exit(1); + } + } +########## setup-npm.mjs ########## +--- js/scripts/setup-npm.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/setup-npm.mjs 2026-09-04 20:31:16.601474123 +0000 +@@ -1,42 +1,24 @@ +-#!/usr/bin/env bun ++#!/usr/bin/env node ++ ++import { resolve } from 'node:path'; ++import process from 'node:process'; ++import { fileURLToPath } from 'node:url'; + + /** +- * Update npm for OIDC trusted publishing. +- * +- * npm trusted publishing (the keyless, secret-free flow this repo relies on) +- * requires npm >= 11.5.1. The Node.js runtimes used by GitHub Actions ship with +- * older npm, so we must update before publishing. If npm cannot be brought to a +- * version that supports OIDC, publishing would silently fall back to (missing) +- * token auth and fail with an opaque E404 on PUT — one of the failure modes +- * behind issue #166. So this script asserts the final npm version and fails +- * loudly when OIDC is unsupported, instead of letting the publish step discover +- * it later. +- * +- * Ported from the js pipeline template +- * (link-foundation/js-ai-driven-development-pipeline-template, +- * scripts/setup-npm.mjs), adapted to bun. The pure version helpers are exported +- * so tests can exercise the gating logic without fetching dependencies or +- * mutating the global npm install. ++ * Update npm for OIDC trusted publishing ++ * npm trusted publishing requires npm >= 11.5.1 ++ * Node.js 20.x ships with npm 10.x, so we need to update + * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support + */ + +-import { resolve } from 'node:path'; +-import process from 'node:process'; +-import { fileURLToPath } from 'node:url'; +- + export const NPM_MIN_VERSION = '11.5.1'; + export const NODE_MIN_VERSION = '22.14.0'; + export const NPM_TARGET_MAJOR = 11; + export const NPM_REGISTRY_METADATA_URL = 'https://registry.npmjs.org/npm'; + +-/** +- * Parse a semantic version string into numeric components. +- * @param {string} version +- * @returns {{major: number, minor: number, patch: number, prerelease: string}} +- */ + export function parseVersion(version) { + const match = String(version) + .trim() +@@ -56,12 +38,6 @@ + }; + } + +-/** +- * Compare two semantic versions (-1, 0, 1). +- * @param {string} leftVersion +- * @param {string} rightVersion +- * @returns {number} +- */ + export function compareVersions(leftVersion, rightVersion) { + const left = parseVersion(leftVersion); + const right = parseVersion(rightVersion); +@@ -88,40 +64,18 @@ + return left.prerelease > right.prerelease ? 1 : -1; + } + +-/** +- * Whether version >= minimumVersion. +- * @param {string} version +- * @param {string} minimumVersion +- * @returns {boolean} +- */ + export function isVersionAtLeast(version, minimumVersion) { + return compareVersions(version, minimumVersion) >= 0; + } + +-/** +- * Whether the npm version supports OIDC trusted publishing. +- * @param {string} version +- * @returns {boolean} +- */ + export function isSupportedNpmVersion(version) { + return isVersionAtLeast(version, NPM_MIN_VERSION); + } + +-/** +- * Whether the Node.js version is new enough for the OIDC setup path. +- * @param {string} version +- * @returns {boolean} +- */ + export function isSupportedNodeVersion(version) { + return isVersionAtLeast(version, NODE_MIN_VERSION); + } + +-/** +- * Pick the newest stable npm 11.x release (at or above the OIDC minimum) from +- * registry metadata. +- * @param {{versions?: Object}} metadata +- * @returns {{version: string, tarballUrl: string}} +- */ + export function selectLatestSupportedNpmRelease(metadata) { + const releases = Object.entries(metadata?.versions || {}) + .filter(([version, release]) => { +@@ -166,12 +120,12 @@ + return selectLatestSupportedNpmRelease(metadata); + } + +-// Update npm for OIDC trusted publishing (requires >= 11.5.1). +-// Pin to npm@11 to avoid breaking changes from future major versions. ++// Update npm for OIDC trusted publishing (requires >= 11.5.1) ++// Pin to npm@11 to avoid breaking changes from future major versions + // +-// Known issue: some GitHub Actions runner images ship a broken npm that is +-// missing the 'promise-retry' module, causing `npm install -g` to fail with +-// MODULE_NOT_FOUND. ++// Known issue: Node.js 22.22.2 on GitHub Actions (ubuntu-24.04 image >= 20260329.72.1) ++// ships with a broken npm 10.9.7 that is missing the 'promise-retry' module, ++// causing `npm install -g` to fail with MODULE_NOT_FOUND. + // See: https://github.com/actions/runner-images/issues/13883 + // See: https://github.com/nodejs/node/issues/62430 + // See: https://github.com/npm/cli/issues/9151 +@@ -236,12 +190,6 @@ + process.exit(1); + } + +-/** +- * Bring npm to an OIDC-capable version, trying multiple strategies, and assert +- * the result. command-stream's `$` is passed in so this stays testable. +- * @param {Function} $ - command-stream tagged template +- * @param {Function} [fetchFn] - fetch implementation (overridable in tests) +- */ + export async function setupNpm($, fetchFn = fetch) { + const nodeVersion = process.version; + console.log(`Current Node.js version: ${nodeVersion}`); +@@ -269,12 +217,16 @@ + break; + } + console.warn( +- 'This may be a broken bundled-npm runner image (actions/runner-images#13883).' ++ 'This may be the Node.js 22.22.2 broken npm issue (actions/runner-images#13883).' + ); + } + +- if (!success && isSupportedNpmVersion(currentVersion)) { +- console.log('Current npm version already supports OIDC trusted publishing'); ++ if (!success) { ++ if (isSupportedNpmVersion(currentVersion)) { ++ console.log( ++ 'Current npm version already supports OIDC trusted publishing' ++ ); ++ } + } + + const updatedResult = await $`npm --version`.run({ capture: true }); +@@ -298,12 +250,11 @@ + failUnsupportedNodeVersion(process.version); + } + +- // Load use-m dynamically only for CLI execution, so tests can import the +- // pure version helpers without fetching dependencies or mutating npm. +- const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +- ); +- const { $ } = await use('command-stream'); ++ // Load command-stream dynamically only for CLI execution, so tests can ++ // import the pure version helpers without fetching dependencies or ++ // mutating npm. ++ const { loadCommandStream } = await import('./use-module.mjs'); ++ const { $ } = await loadCommandStream(); + + await setupNpm($); + } catch (error) { +########## validate-changeset.mjs ########## +--- js/scripts/validate-changeset.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/validate-changeset.mjs 2026-09-04 20:31:16.602474113 +0000 +@@ -8,19 +8,17 @@ + * - Uses git diff to compare PR head against base branch + * - Validates that the PR adds exactly one changeset with proper format + * - Falls back to checking all changesets for local development +- * +- * IMPORTANT: Update the package name below to match your package.json + */ + + import { execSync } from 'child_process'; + import { readFileSync, readdirSync, existsSync } from 'fs'; + import { join } from 'path'; + +-const PACKAGE_NAME = 'command-stream'; +-const CHANGESET_DIR = '.changeset'; +-const GIT_CHANGESET_DIR = existsSync('../.git') +- ? 'js/.changeset' +- : CHANGESET_DIR; ++import { getChangesetDir, getJsRoot, parseJsRootConfig } from './js-paths.mjs'; ++import { ++ getChangesetVersionTypeRegex, ++ readPackageInfo, ++} from './package-info.mjs'; + + /** + * Ensure a git commit is available locally, fetching if necessary +@@ -42,10 +40,14 @@ + /** + * Parse git diff output and extract added changeset files + * @param {string} diffOutput Output from git diff --name-status ++ * @param {string} changesetDir Path to the changeset directory + * @returns {string[]} Array of added changeset file names + */ +-function parseAddedChangesets(diffOutput) { ++function parseAddedChangesets(diffOutput, changesetDir) { + const addedChangesets = []; ++ const changesetGitPath = changesetDir ++ .replace(/\\/g, '/') ++ .replace(/^\.\//, ''); + for (const line of diffOutput.trim().split('\n')) { + if (!line) { + continue; +@@ -53,16 +55,11 @@ + const [status, filePath] = line.split('\t'); + if ( + status === 'A' && +- (filePath.startsWith(`${CHANGESET_DIR}/`) || +- filePath.startsWith(`${GIT_CHANGESET_DIR}/`)) && ++ filePath.startsWith(`${changesetGitPath}/`) && + filePath.endsWith('.md') && + !filePath.endsWith('README.md') + ) { +- addedChangesets.push( +- filePath +- .replace(`${GIT_CHANGESET_DIR}/`, '') +- .replace(`${CHANGESET_DIR}/`, '') +- ); ++ addedChangesets.push(filePath.replace(`${changesetGitPath}/`, '')); + } + } + return addedChangesets; +@@ -72,9 +69,10 @@ + * Try to get changesets using explicit SHA comparison + * @param {string} baseSha Base commit SHA + * @param {string} headSha Head commit SHA ++ * @param {string} changesetDir Path to the changeset directory + * @returns {string[] | null} Array of changeset files or null if failed + */ +-function tryExplicitShaComparison(baseSha, headSha) { ++function tryExplicitShaComparison(baseSha, headSha, changesetDir) { + console.log(`Comparing ${baseSha}...${headSha}`); + try { + ensureCommitAvailable(baseSha); +@@ -82,7 +80,7 @@ + `git diff --name-status ${baseSha} ${headSha}`, + { encoding: 'utf-8' } + ); +- return parseAddedChangesets(diffOutput); ++ return parseAddedChangesets(diffOutput, changesetDir); + } catch (error) { + console.log(`Git diff with explicit SHAs failed: ${error.message}`); + return null; +@@ -92,9 +90,10 @@ + /** + * Try to get changesets using base branch comparison + * @param {string} prBase Base branch name ++ * @param {string} changesetDir Path to the changeset directory + * @returns {string[] | null} Array of changeset files or null if failed + */ +-function tryBaseBranchComparison(prBase) { ++function tryBaseBranchComparison(prBase, changesetDir) { + console.log(`Comparing against base branch: ${prBase}`); + try { + try { +@@ -106,7 +105,7 @@ + `git diff --name-status origin/${prBase}...HEAD`, + { encoding: 'utf-8' } + ); +- return parseAddedChangesets(diffOutput); ++ return parseAddedChangesets(diffOutput, changesetDir); + } catch (error) { + console.log(`Git diff with base ref failed: ${error.message}`); + return null; +@@ -115,31 +114,33 @@ + + /** + * Fallback: get all changesets in directory ++ * @param {string} changesetDir Path to the changeset directory + * @returns {string[]} Array of all changeset file names + */ +-function getAllChangesets() { ++function getAllChangesets(changesetDir) { + console.log( + 'Warning: Could not determine PR diff, checking all changesets in directory' + ); +- if (!existsSync(CHANGESET_DIR)) { ++ if (!existsSync(changesetDir)) { + return []; + } +- return readdirSync(CHANGESET_DIR).filter( ++ return readdirSync(changesetDir).filter( + (file) => file.endsWith('.md') && file !== 'README.md' + ); + } + + /** + * Get changeset files added in the current PR using git diff ++ * @param {string} changesetDir Path to the changeset directory + * @returns {string[]} Array of added changeset file names + */ +-function getAddedChangesetFiles() { ++function getAddedChangesetFiles(changesetDir) { + const baseSha = process.env.GITHUB_BASE_SHA || process.env.BASE_SHA; + const headSha = process.env.GITHUB_HEAD_SHA || process.env.HEAD_SHA; + + // Try explicit SHAs first + if (baseSha && headSha) { +- const result = tryExplicitShaComparison(baseSha, headSha); ++ const result = tryExplicitShaComparison(baseSha, headSha, changesetDir); + if (result !== null) { + return result; + } +@@ -148,36 +149,34 @@ + // Try base branch comparison + const prBase = process.env.GITHUB_BASE_REF; + if (prBase) { +- const result = tryBaseBranchComparison(prBase); ++ const result = tryBaseBranchComparison(prBase, changesetDir); + if (result !== null) { + return result; + } + } + + // Fallback to checking all changesets +- return getAllChangesets(); ++ return getAllChangesets(changesetDir); + } + + /** + * Validate a single changeset file + * @param {string} filePath Full path to the changeset file ++ * @param {string} packageName + * @returns {{valid: boolean, type?: string, description?: string, error?: string}} + */ +-function validateChangesetFile(filePath) { ++function validateChangesetFile(filePath, packageName) { + try { + const content = readFileSync(filePath, 'utf-8'); + + // Check if changeset has a valid type (major, minor, or patch) +- const versionTypeRegex = new RegExp( +- `^['"]${PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]:\\s+(major|minor|patch)`, +- 'm' +- ); ++ const versionTypeRegex = getChangesetVersionTypeRegex(packageName); + const versionTypeMatch = content.match(versionTypeRegex); + + if (!versionTypeMatch) { + return { + valid: false, +- error: `Changeset must specify a version type: major, minor, or patch\nExpected format:\n---\n'${PACKAGE_NAME}': patch\n---\n\nYour description here`, ++ error: `Changeset must specify a version type: major, minor, or patch\nExpected format:\n---\n'${packageName}': patch\n---\n\nYour description here`, + }; + } + +@@ -214,9 +213,14 @@ + + try { + console.log('Validating changesets added by this PR...'); ++ const jsRootConfig = parseJsRootConfig(); ++ const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); ++ const changesetDir = getChangesetDir({ jsRoot }); ++ const { name: packageName } = readPackageInfo({ jsRoot }); ++ console.log(`Package: ${packageName}`); + + // Get changeset files added in this PR +- const addedChangesetFiles = getAddedChangesetFiles(); ++ const addedChangesetFiles = getAddedChangesetFiles(changesetDir); + const changesetCount = addedChangesetFiles.length; + + console.log(`Found ${changesetCount} changeset file(s) added by this PR`); +@@ -228,7 +232,7 @@ + // Ensure exactly one changeset file was added + if (changesetCount === 0) { + console.error( +- "::error::No changeset found in this PR. Please add a changeset by running 'bun run changeset' and commit the result." ++ "::error::No changeset found in this PR. Please add a changeset by running 'npm run changeset' and commit the result." + ); + process.exit(1); + } else if (changesetCount > 1) { +@@ -244,10 +248,10 @@ + } + + // Validate the single changeset file +- const changesetFile = join(CHANGESET_DIR, addedChangesetFiles[0]); ++ const changesetFile = join(changesetDir, addedChangesetFiles[0]); + console.log(`Validating changeset: ${changesetFile}`); + +- const validation = validateChangesetFile(changesetFile); ++ const validation = validateChangesetFile(changesetFile, packageName); + + if (!validation.valid) { + console.error(`::error::${validation.error}`); +########## version-and-commit.mjs ########## +--- js/scripts/version-and-commit.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/version-and-commit.mjs 2026-09-04 20:31:16.603474103 +0000 +@@ -1,11 +1,15 @@ + #!/usr/bin/env bun + + /** +- * Version the JavaScript package and commit to main +- * Usage: bun scripts/version-and-commit.mjs --mode [--bump-type ] [--description ] ++ * Version packages and commit to main ++ * Usage: node scripts/version-and-commit.mjs --mode [--bump-type ] [--description ] [--js-root ] + * changeset: Run changeset version + * instant: Run instant version bump with bump_type (patch|minor|major) and optional description + * ++ * Configuration: ++ * - CLI: --js-root to explicitly set JavaScript root ++ * - Environment: JS_ROOT= ++ * + * Uses link-foundation libraries: + * - use-m: Dynamic package loading without package.json dependencies + * - command-stream: Modern shell command execution with streaming support +@@ -14,14 +18,18 @@ + + import { readFileSync, appendFileSync, readdirSync } from 'fs'; + +-// Load use-m dynamically +-const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +-); ++import { ++ getJsRoot, ++ getPackageJsonPath, ++ getChangesetDir, ++ needsCd, ++ parseJsRootConfig, ++} from './js-paths.mjs'; ++import { loadCommandStream, loadLinoArguments } from './use-module.mjs'; + + // Import link-foundation libraries +-const { $ } = await use('command-stream'); +-const { makeConfig } = await use('lino-arguments'); ++const { $ } = await loadCommandStream(); ++const { makeConfig } = await loadLinoArguments(); + + // Parse CLI arguments using lino-arguments + const config = makeConfig({ +@@ -42,16 +50,27 @@ + type: 'string', + default: getenv('DESCRIPTION', ''), + describe: 'Description for instant version bump', ++ }) ++ .option('js-root', { ++ type: 'string', ++ default: getenv('JS_ROOT', ''), ++ describe: ++ 'JavaScript package root directory (auto-detected if not specified)', + }), + }); + +-const { mode, bumpType, description } = config; ++const { mode, bumpType, description, jsRoot: jsRootArg } = config; ++ ++// Get JavaScript package root (auto-detect or use explicit config) ++const jsRootConfig = jsRootArg || parseJsRootConfig(); ++const jsRoot = getJsRoot({ jsRoot: jsRootConfig, verbose: true }); + + // Debug: Log parsed configuration + console.log('Parsed configuration:', { + mode, + bumpType, + description: description || '(none)', ++ jsRoot, + }); + + // Detect if positional arguments were used (common mistake) +@@ -61,21 +80,26 @@ + console.error('Command line arguments:', args); + console.error(''); + console.error( +- 'This script requires named arguments (--mode, --bump-type, --description).' ++ 'This script requires named arguments (--mode, --bump-type, --description, --js-root).' + ); + console.error('Usage:'); + console.error(' Changeset mode:'); +- console.error(' bun scripts/version-and-commit.mjs --mode changeset'); ++ console.error( ++ ' node scripts/version-and-commit.mjs --mode changeset [--js-root ]' ++ ); + console.error(' Instant mode:'); + console.error( +- ' bun scripts/version-and-commit.mjs --mode instant --bump-type [--description ]' ++ ' node scripts/version-and-commit.mjs --mode instant --bump-type [--description ] [--js-root ]' + ); + console.error(''); + console.error('Examples:'); + console.error( +- ' bun scripts/version-and-commit.mjs --mode instant --bump-type patch --description "Fix bug"' ++ ' node scripts/version-and-commit.mjs --mode instant --bump-type patch --description "Fix bug"' ++ ); ++ console.error(' node scripts/version-and-commit.mjs --mode changeset'); ++ console.error( ++ ' node scripts/version-and-commit.mjs --mode changeset --js-root js' + ); +- console.error(' bun scripts/version-and-commit.mjs --mode changeset'); + process.exit(1); + } + +@@ -90,11 +114,15 @@ + if (mode === 'instant' && !bumpType) { + console.error('Error: --bump-type is required for instant mode'); + console.error( +- 'Usage: bun scripts/version-and-commit.mjs --mode instant --bump-type [--description ]' ++ 'Usage: node scripts/version-and-commit.mjs --mode instant --bump-type [--description ] [--js-root ]' + ); + process.exit(1); + } + ++// Store the original working directory to restore after cd commands ++// IMPORTANT: command-stream's cd is a virtual command that calls process.chdir() ++const originalCwd = process.cwd(); ++ + /** + * Append to GitHub Actions output file + * @param {string} key +@@ -112,7 +140,7 @@ + */ + function countChangesets() { + try { +- const changesetDir = '.changeset'; ++ const changesetDir = getChangesetDir({ jsRoot }); + const files = readdirSync(changesetDir); + return files.filter((f) => f.endsWith('.md') && f !== 'README.md').length; + } catch { +@@ -125,20 +153,25 @@ + * @param {string} source - 'local' or 'remote' + */ + async function getVersion(source = 'local') { ++ const packageJsonPath = getPackageJsonPath({ jsRoot }); + if (source === 'remote') { +- const result = await $`git show origin/main:js/package.json`.run({ ++ const result = await $`git show origin/main:${packageJsonPath}`.run({ + capture: true, + }); + return JSON.parse(result.stdout).version; + } +- return JSON.parse(readFileSync('./package.json', 'utf8')).version; ++ return JSON.parse(readFileSync(packageJsonPath, 'utf8')).version; + } + + async function main() { + try { + // Configure git ++ // The 41898282+ prefix is what links the commit to the github-actions[bot] ++ // account. Without it the commit is "unattributed", and a ruleset with ++ // require_extra_approval_for_unattributed_changes will demand a human ++ // approval before an automated release pull request can be merged. + await $`git config user.name "github-actions[bot]"`; +- await $`git config user.email "github-actions[bot]@users.noreply.github.com"`; ++ await $`git config user.email "41898282+github-actions[bot]@users.noreply.github.com"`; + + // Check if remote main has advanced (handles re-runs after partial success) + console.log('Checking for remote changes...'); +@@ -187,16 +220,23 @@ + if (mode === 'instant') { + console.log('Running instant version bump...'); + // Run instant version bump script ++ // Pass --js-root to ensure consistent path handling + // Rely on command-stream's auto-quoting for proper argument handling + if (description) { +- await $`bun scripts/instant-version-bump.mjs --bump-type ${bumpType} --description ${description}`; ++ await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --description ${description} --js-root ${jsRoot}`; + } else { +- await $`bun scripts/instant-version-bump.mjs --bump-type ${bumpType}`; ++ await $`node scripts/instant-version-bump.mjs --bump-type ${bumpType} --js-root ${jsRoot}`; + } + } else { + console.log('Running changeset version...'); + // Run changeset version to bump versions and update CHANGELOG +- await $`bun run changeset:version`; ++ // IMPORTANT: cd is a virtual command that calls process.chdir(), so we restore after ++ if (needsCd({ jsRoot })) { ++ await $`cd ${jsRoot} && npm run changeset:version`; ++ process.chdir(originalCwd); ++ } else { ++ await $`npm run changeset:version`; ++ } + } + + // Get new version after bump +@@ -219,26 +259,34 @@ + const escapedMessage = commitMessage.replace(/"/g, '\\"'); + await $`git commit -m "${escapedMessage}"`; + +- // Push directly to main. +- // command-stream's `$` does NOT throw on a non-zero exit (errexit is off +- // by default, see issue #156), so we check the result code explicitly. +- // A silently-failed push would otherwise report version_committed=true and +- // let the release job publish/release a version that is not on main (the +- // same false-positive class that produced #166). +- const pushResult = await $`git push origin main`.run({ capture: true }); ++ // Push directly to main, rebasing and retrying if another main writer won ++ // the race between this commit and the push, and landing the commit ++ // through a pull request when a repository ruleset declines direct ++ // pushes to main (link-foundation/js-ai-driven-development-pipeline-template#143). ++ // ++ // command-stream's `$` resolves (it does not throw) on a non-zero exit ++ // code, so the exit code is checked explicitly: reporting ++ // version_committed=true for a push that never landed would let the ++ // publish job work from a version that exists only in the runner. ++ const pushResult = ++ await $`node scripts/push-main-with-rebase-retry.mjs origin main --label ${newVersion}`.run( ++ { capture: true, mirror: true } ++ ); + if (pushResult.code !== 0) { + throw new Error( +- `git push origin main failed (exit code ${pushResult.code}): ${pushResult.stderr?.trim() || 'no stderr'}` ++ `Failed to push version ${newVersion} to main (exit ${pushResult.code})` + ); + } + +- console.log('✅ Version bump committed and pushed to main'); ++ console.log('\u2705 Version bump committed and pushed to main'); + setOutput('version_committed', 'true'); + } else { + console.log('No changes to commit'); + setOutput('version_committed', 'false'); + } + } catch (error) { ++ // Restore cwd on error ++ process.chdir(originalCwd); + console.error('Error:', error.message); + process.exit(1); + } +########## wait-for-npm.mjs ########## +--- js/scripts/wait-for-npm.mjs 2026-09-04 20:28:25.872207699 +0000 ++++ /tmp/tmpl/js-tmpl/scripts/wait-for-npm.mjs 2026-09-04 20:31:16.603474103 +0000 +@@ -1,44 +1,236 @@ +-#!/usr/bin/env bun ++#!/usr/bin/env node + + /** + * Wait for a package version to become available on npm. + * +- * Issue #166 was a *false-positive* release: a git tag and GitHub release +- * existed for js-v0.10.1, but the package was never installable from npm. This +- * step closes that gap by asserting, after publish, that the exact version is +- * actually queryable on the registry (npm visibility can lag a few seconds +- * after a successful publish). If it never appears, the job fails loudly +- * instead of leaving behind a release nobody can `npm install`. +- * +- * Ported from the js pipeline template +- * (link-foundation/js-ai-driven-development-pipeline-template, +- * scripts/wait-for-npm.mjs), adapted to command-stream's `$` and bun. The pure +- * polling logic is exported so it can be unit-tested without hitting npm. +- * +- * Uses link-foundation libraries: +- * - use-m: Dynamic package loading without package.json dependencies +- * - command-stream: Modern shell command execution with streaming support +- * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files +- * +- * Usage: bun scripts/wait-for-npm.mjs --release-version [--package-name ] +- * [--max-attempts ] [--sleep-seconds ] ++ * The Docker publish job runs after npm publishing, but npm registry visibility ++ * can lag briefly. Waiting here keeps Docker tags tied to an installable npm ++ * version. + */ + +-import { appendFileSync, readFileSync } from 'node:fs'; ++import { appendFileSync } from 'node:fs'; + import path from 'node:path'; +-import process from 'node:process'; + import { fileURLToPath } from 'node:url'; + +-export const DEFAULT_MAX_ATTEMPTS = 30; +-export const DEFAULT_SLEEP_SECONDS = 10; ++import { buildPackageMetadataUrl } from './npm-registry.mjs'; ++import { formatNpmPackageVersion, readPackageInfo } from './package-info.mjs'; ++ ++const DEFAULT_MAX_ATTEMPTS = 30; ++const NPM_REGISTRY_USER_AGENT = ++ 'js-ai-driven-development-pipeline-template wait-for-npm'; ++const DEFAULT_SLEEP_SECONDS = 10; ++const USAGE = ++ 'Usage: node scripts/wait-for-npm.mjs --release-version [--package-name ] [--max-attempts ] [--sleep-seconds ] [--js-root ]'; ++ ++function parsePositiveInteger(value, optionName) { ++ const parsed = Number(value); ++ if (!Number.isInteger(parsed) || parsed < 1) { ++ throw new Error(`${optionName} must be a positive integer`); ++ } ++ return parsed; ++} ++ ++function readCliOptions(argv) { ++ const options = {}; ++ ++ for (let index = 0; index < argv.length; index++) { ++ const arg = argv[index]; ++ if (!arg.startsWith('--')) { ++ continue; ++ } ++ ++ const inlineValueIndex = arg.indexOf('='); ++ if (inlineValueIndex !== -1) { ++ options[arg.slice(2, inlineValueIndex)] = arg.slice(inlineValueIndex + 1); ++ continue; ++ } ++ ++ const value = argv[index + 1]; ++ if (value === undefined || value.startsWith('--')) { ++ throw new Error(`Missing value for ${arg}`); ++ } ++ ++ options[arg.slice(2)] = value; ++ index++; ++ } ++ ++ return options; ++} ++ ++export function parseArgs(argv, env = process.env) { ++ const cliOptions = readCliOptions(argv); ++ ++ const config = { ++ jsRoot: cliOptions['js-root'] ?? env.JS_ROOT ?? '', ++ maxAttempts: parsePositiveInteger( ++ cliOptions['max-attempts'] || ++ env.MAX_ATTEMPTS || ++ String(DEFAULT_MAX_ATTEMPTS), ++ '--max-attempts' ++ ), ++ packageName: cliOptions['package-name'] ?? env.PACKAGE_NAME ?? '', ++ releaseVersion: cliOptions['release-version'] ?? env.VERSION ?? '', ++ sleepSeconds: parsePositiveInteger( ++ cliOptions['sleep-seconds'] || ++ env.SLEEP_SECONDS || ++ String(DEFAULT_SLEEP_SECONDS), ++ '--sleep-seconds' ++ ), ++ }; ++ ++ return config; ++} ++ ++/** ++ * Build the registry URL for a single package version document. ++ * @param {string} packageName ++ * @param {string} version ++ * @param {string} [registryUrl] ++ * @returns {string} ++ */ ++export function buildPackageVersionUrl(packageName, version, registryUrl) { ++ return `${buildPackageMetadataUrl(packageName, registryUrl)}/${encodeURIComponent(version)}`; ++} + + /** +- * Append a step output (and echo for the run log). +- * @param {string} name +- * @param {string} value ++ * Normalize a check result so callers always see the same shape. ++ * A boolean from an injected `checkAvailability` is also accepted. ++ * @param {boolean|object} result ++ * @returns {{available: boolean, status: string, error?: string, url?: string, httpStatus?: number}} + */ ++export function normalizeCheckResult(result) { ++ if (typeof result === 'boolean') { ++ return { available: result, status: result ? 'ok' : 'not-published' }; ++ } ++ ++ return { ++ available: Boolean(result?.available), ++ status: result?.status ?? 'unknown', ++ ...(result?.error === undefined ? {} : { error: result.error }), ++ ...(result?.url === undefined ? {} : { url: result.url }), ++ ...(result?.httpStatus === undefined ++ ? {} ++ : { httpStatus: result.httpStatus }), ++ }; ++} ++ ++/** ++ * Describe a check result for a per-attempt log line. ++ * @param {{status: string, httpStatus?: number, error?: string}} result ++ * @returns {string} ++ */ ++export function describeCheckResult(result) { ++ const httpStatus = ++ result.httpStatus === undefined ? '' : `HTTP ${result.httpStatus}`; ++ ++ if (result.status === 'ok') { ++ return result.available ++ ? `available${httpStatus ? ` (${httpStatus})` : ''}` ++ : `version mismatch${httpStatus ? ` (${httpStatus})` : ''}`; ++ } ++ ++ if (result.status === 'not-published') { ++ return `not published yet${httpStatus ? ` (${httpStatus})` : ''}`; ++ } ++ ++ return `check failed: ${[httpStatus, result.error].filter(Boolean).join(' ')}`; ++} ++ ++/** ++ * Ask the npm registry whether a package version exists. ++ * ++ * Queries the registry over HTTP so the real status code is available: a ++ * genuine "not published" (404) is reported separately from "we could not get ++ * an answer" (5xx, rate limits, DNS, proxy errors), which says nothing about ++ * whether the publish succeeded. ++ * ++ * @param {string} packageName ++ * @param {string} version ++ * @param {object} [options] ++ * @param {Function} [options.fetchFn] ++ * @param {string} [options.registryUrl] ++ * @returns {Promise<{available: boolean, status: 'ok'|'not-published'|'unknown', httpStatus?: number, url: string, error?: string}>} ++ */ ++export async function checkNpmVersion( ++ packageName, ++ version, ++ { fetchFn = fetch, registryUrl } = {} ++) { ++ let url; ++ try { ++ url = buildPackageVersionUrl(packageName, version, registryUrl); ++ } catch (error) { ++ return { available: false, status: 'unknown', error: error.message }; ++ } ++ ++ try { ++ const response = await fetchFn(url, { ++ headers: { ++ accept: 'application/json', ++ // Some registries reject requests without a User-Agent with 403. ++ 'user-agent': NPM_REGISTRY_USER_AGENT, ++ }, ++ }); ++ ++ if (response.status === 404) { ++ return { ++ available: false, ++ status: 'not-published', ++ httpStatus: 404, ++ url, ++ }; ++ } ++ ++ if (!response.ok) { ++ return { ++ available: false, ++ status: 'unknown', ++ httpStatus: response.status, ++ url, ++ error: `${response.status} ${response.statusText ?? ''}`.trim(), ++ }; ++ } ++ ++ const metadata = await response.json(); ++ return { ++ available: metadata?.version === version, ++ status: 'ok', ++ httpStatus: response.status, ++ url, ++ }; ++ } catch (error) { ++ // `fetch` reports transport failures as a bare "fetch failed"; the cause ++ // holds the actual reason (ECONNREFUSED, EAI_AGAIN, certificate errors). ++ const cause = error?.cause?.message; ++ const message = error?.message ?? String(error); ++ ++ return { ++ available: false, ++ status: 'unknown', ++ url, ++ error: cause && cause !== message ? `${message}: ${cause}` : message, ++ }; ++ } ++} ++ ++function sleep(seconds) { ++ return new Promise((resolve) => ++ globalThis.setTimeout(resolve, seconds * 1000) ++ ); ++} ++ ++function readGithubOutputPath() { ++ try { ++ return process.env.GITHUB_OUTPUT || ''; ++ } catch { ++ // Runtimes with restricted environment access (Deno without --allow-env) ++ // throw here; step outputs are simply unavailable then. ++ return ''; ++ } ++} ++ + function setOutput(name, value) { +- const outputFile = process.env.GITHUB_OUTPUT; ++ const outputFile = readGithubOutputPath(); + if (outputFile) { + appendFileSync(outputFile, `${name}=${value}\n`); + } +@@ -46,38 +238,37 @@ + } + + /** +- * Poll until a specific package version is visible on npm, or attempts run out. +- * +- * The availability check and sleep are injectable so the loop can be unit-tested +- * deterministically without network access or real delays. +- * +- * @param {object} options +- * @param {string} options.packageName +- * @param {string} options.version +- * @param {(packageName: string, version: string) => (boolean | Promise)} options.checkAvailability +- * @param {number} [options.maxAttempts] +- * @param {number} [options.sleepSeconds] +- * @param {(seconds: number) => Promise} [options.sleepFn] +- * @param {(message: string) => void} [options.stdout] +- * @returns {Promise} ++ * Poll the registry until the version shows up or attempts run out. ++ * @returns {Promise<{available: boolean, status: string, error?: string, url?: string, httpStatus?: number, attempts: number}>} + */ + export async function waitForNpmVersion({ +- packageName, +- version, +- checkAvailability, ++ checkAvailability = checkNpmVersion, + maxAttempts = DEFAULT_MAX_ATTEMPTS, ++ packageName, ++ registryUrl, ++ sleepFn = sleep, + sleepSeconds = DEFAULT_SLEEP_SECONDS, +- sleepFn = (seconds) => +- new Promise((resolve) => globalThis.setTimeout(resolve, seconds * 1000)), + stdout = console.log, ++ version, + }) { ++ let lastResult = { ++ available: false, ++ status: 'unknown', ++ error: 'no attempts were made', ++ }; ++ + for (let attempt = 1; attempt <= maxAttempts; attempt++) { ++ lastResult = normalizeCheckResult( ++ await checkAvailability(packageName, version, { registryUrl }) ++ ); ++ + stdout( +- `Checking npm for ${packageName}@${version} (attempt ${attempt}/${maxAttempts})` ++ `Checking npm for ${formatNpmPackageVersion(packageName, version)} ` + ++ `(attempt ${attempt}/${maxAttempts}): ${describeCheckResult(lastResult)}` + ); + +- if (await checkAvailability(packageName, version)) { +- return true; ++ if (lastResult.available) { ++ return { ...lastResult, attempts: attempt }; + } + + if (attempt < maxAttempts) { +@@ -85,7 +276,29 @@ + } + } + +- return false; ++ return { ...lastResult, attempts: maxAttempts }; ++} ++ ++/** ++ * Build the failure message for an exhausted wait. ++ * @param {string} packageSpecifier ++ * @param {{status: string, error?: string, url?: string}} result ++ * @param {number} maxAttempts ++ * @returns {string} ++ */ ++export function formatFailureMessage(packageSpecifier, result, maxAttempts) { ++ if (result.status !== 'unknown') { ++ return `${packageSpecifier} did not become available on npm`; ++ } ++ ++ const reason = result.error ? ` The last error was: ${result.error}.` : ''; ++ const where = result.url ? ` Check ${result.url} directly.` : ''; ++ ++ return ( ++ `Could not determine whether ${packageSpecifier} is on npm: ` + ++ `all ${maxAttempts} attempts failed to reach the registry.${reason} ` + ++ `This does NOT mean the publish failed.${where}` ++ ); + } + + function isCliEntryPoint() { +@@ -96,85 +309,57 @@ + ); + } + +-async function runCli() { +- // Load use-m dynamically (matches the other release scripts in this folder). +- const { use } = eval( +- await (await fetch('https://unpkg.com/use-m/use.js')).text() +- ); +- const { $ } = await use('command-stream'); +- const { makeConfig } = await use('lino-arguments'); +- +- const config = makeConfig({ +- yargs: ({ yargs, getenv }) => +- yargs +- .option('release-version', { +- type: 'string', +- default: getenv('VERSION', ''), +- describe: 'Version number to wait for (e.g., 1.0.0)', +- }) +- .option('package-name', { +- type: 'string', +- default: getenv('PACKAGE_NAME', ''), +- describe: 'npm package name (defaults to ./package.json name)', +- }) +- .option('max-attempts', { +- type: 'number', +- default: Number(getenv('MAX_ATTEMPTS', String(DEFAULT_MAX_ATTEMPTS))), +- describe: 'Maximum number of polling attempts', +- }) +- .option('sleep-seconds', { +- type: 'number', +- default: Number( +- getenv('SLEEP_SECONDS', String(DEFAULT_SLEEP_SECONDS)) +- ), +- describe: 'Seconds to wait between attempts', +- }), +- }); +- +- const version = config.releaseVersion; +- if (!version) { +- console.error('Error: Missing required --release-version'); +- return 1; +- } ++export async function main({ ++ argv = process.argv.slice(2), ++ env = process.env, ++ stderr = console.error, ++ stdout = console.log, ++} = {}) { ++ try { ++ const config = parseArgs(argv, env); ++ if (!config.releaseVersion) { ++ stderr('Error: Missing required --release-version'); ++ stderr(USAGE); ++ return 1; ++ } + +- const packageName = +- config.packageName || +- JSON.parse(readFileSync('./package.json', 'utf8')).name; +- +- // command-stream's `$` does NOT throw on non-zero exit (errexit off by +- // default — see issue #156); `npm view @ version` exits 0 and +- // prints the version when published, non-zero (E404) otherwise. +- const checkAvailability = async (name, ver) => { +- const result = await $`npm view "${name}@${ver}" version`.run({ +- capture: true, ++ const packageInfo = config.packageName ++ ? { name: config.packageName } ++ : readPackageInfo({ jsRoot: config.jsRoot || undefined }); ++ ++ const result = await waitForNpmVersion({ ++ maxAttempts: config.maxAttempts, ++ packageName: packageInfo.name, ++ registryUrl: ++ env.NPM_CONFIG_REGISTRY || env.npm_config_registry || undefined, ++ sleepSeconds: config.sleepSeconds, ++ stdout, ++ version: config.releaseVersion, + }); +- return result.code === 0 && result.stdout.trim() === ver; +- }; + +- const available = await waitForNpmVersion({ +- packageName, +- version, +- checkAvailability, +- maxAttempts: config.maxAttempts, +- sleepSeconds: config.sleepSeconds, +- }); ++ const packageSpecifier = formatNpmPackageVersion( ++ packageInfo.name, ++ config.releaseVersion ++ ); ++ ++ setOutput('npm_available', result.available ? 'true' : 'false'); ++ setOutput('npm_check_status', result.status); + +- setOutput('npm_available', available ? 'true' : 'false'); ++ if (!result.available) { ++ stderr( ++ formatFailureMessage(packageSpecifier, result, config.maxAttempts) ++ ); ++ return 1; ++ } + +- if (!available) { +- console.error(`${packageName}@${version} did not become available on npm`); ++ stdout(`${packageSpecifier} is available on npm`); ++ return 0; ++ } catch (error) { ++ stderr(`Error: ${error.message}`); + return 1; + } +- +- console.log(`${packageName}@${version} is available on npm`); +- return 0; + } + + if (isCliEntryPoint()) { +- try { +- process.exitCode = await runCli(); +- } catch (error) { +- console.error(`Error: ${error.message}`); +- process.exitCode = 1; +- } ++ process.exitCode = await main(); + } diff --git a/dev/log/issues/199/pulls/200/analysis/rust-scripts-diff.log b/dev/log/issues/199/pulls/200/analysis/rust-scripts-diff.log new file mode 100644 index 00000000..933d0d74 --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/rust-scripts-diff.log @@ -0,0 +1,794 @@ +########## check-crate-size.rs ########## +--- rust/scripts/check-crate-size.rs 2026-09-04 20:28:25.904207376 +0000 ++++ /tmp/tmpl/rust-tmpl/scripts/check-crate-size.rs 2026-09-04 20:31:17.683463071 +0000 +@@ -42,6 +42,11 @@ + /// Warn once the archive grows past 80% of the limit so projects can react + /// before a release is actually blocked. + const WARN_CRATE_BYTES: u64 = MAX_CRATE_BYTES * 8 / 10; ++/// `cargo package` may contact the registry index while resolving package ++/// metadata, so retry it before treating a transient network failure as final. ++const CARGO_PACKAGE_MAX_ATTEMPTS: u8 = 3; ++#[cfg(not(test))] ++const CARGO_PACKAGE_RETRY_DELAY_SECONDS: u64 = 5; + + #[derive(Debug, PartialEq, Eq)] + enum SizeStatus { +@@ -66,6 +71,33 @@ + format!("{mib:.2} MiB ({size_bytes} bytes)") + } + ++fn run_cargo_package_with_retries( ++ mut run_package: F, ++ max_attempts: u8, ++ mut wait_before_retry: S, ++) -> std::io::Result ++where ++ F: FnMut() -> std::io::Result, ++ S: FnMut(u8), ++{ ++ let max_attempts = max_attempts.max(1); ++ ++ for attempt in 1..=max_attempts { ++ if run_package()? { ++ return Ok(true); ++ } ++ ++ if attempt < max_attempts { ++ eprintln!( ++ "::warning::cargo package failed on attempt {attempt}/{max_attempts}; retrying" ++ ); ++ wait_before_retry(attempt); ++ } ++ } ++ ++ Ok(false) ++} ++ + #[cfg(not(test))] + fn set_output(key: &str, value: &str) { + if let Ok(output_file) = env::var("GITHUB_OUTPUT") { +@@ -139,8 +171,22 @@ + cmd.current_dir(&rust_root); + } + +- let status = cmd.status().expect("Failed to execute cargo package"); +- if !status.success() { ++ let package_succeeded = match run_cargo_package_with_retries( ++ || cmd.status().map(|status| status.success()), ++ CARGO_PACKAGE_MAX_ATTEMPTS, ++ |_| { ++ std::thread::sleep(std::time::Duration::from_secs( ++ CARGO_PACKAGE_RETRY_DELAY_SECONDS, ++ )) ++ }, ++ ) { ++ Ok(success) => success, ++ Err(e) => { ++ eprintln!("::error::Failed to execute cargo package: {e}"); ++ exit(1); ++ } ++ }; ++ if !package_succeeded { + eprintln!("::error::cargo package failed; cannot determine crate archive size"); + exit(1); + } +@@ -233,6 +279,44 @@ + } + + #[test] ++ fn cargo_package_is_retried_after_transient_failure() { ++ let mut attempts = 0; ++ let package_succeeded = run_cargo_package_with_retries( ++ || { ++ attempts += 1; ++ Ok(attempts == 2) ++ }, ++ CARGO_PACKAGE_MAX_ATTEMPTS, ++ |_| {}, ++ ) ++ .unwrap(); ++ ++ assert!(package_succeeded); ++ assert_eq!(attempts, 2); ++ } ++ ++ #[test] ++ fn cargo_package_failure_is_reported_after_all_retries() { ++ let mut attempts = 0; ++ let mut waits = 0; ++ let package_succeeded = run_cargo_package_with_retries( ++ || { ++ attempts += 1; ++ Ok(false) ++ }, ++ CARGO_PACKAGE_MAX_ATTEMPTS, ++ |_| { ++ waits += 1; ++ }, ++ ) ++ .unwrap(); ++ ++ assert!(!package_succeeded); ++ assert_eq!(attempts, usize::from(CARGO_PACKAGE_MAX_ATTEMPTS)); ++ assert_eq!(waits, usize::from(CARGO_PACKAGE_MAX_ATTEMPTS - 1)); ++ } ++ ++ #[test] + fn find_crate_archive_returns_none_when_missing() { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) +########## check-file-size.rs ########## +--- rust/scripts/check-file-size.rs 2026-09-04 20:28:25.904207376 +0000 ++++ /tmp/tmpl/rust-tmpl/scripts/check-file-size.rs 2026-09-04 20:31:17.683463071 +0000 +@@ -9,6 +9,7 @@ + //! walkdir = "2" + //! ``` + ++use std::collections::BTreeSet; + use std::fs; + use std::path::Path; + #[cfg(not(test))] +@@ -153,15 +154,43 @@ + ) + } + ++/// Parses the changed-file list provided by CI (newline or space separated). ++/// ++/// Returns `None` when no list is configured, which means every warning is ++/// annotated (the behaviour used for local runs and pushes to the default ++/// branch). ++fn changed_files(raw: Option<&str>) -> Option> { ++ let raw = raw?; ++ if raw.trim().is_empty() { ++ return Some(BTreeSet::new()); ++ } ++ ++ Some( ++ raw.split(['\n', '\r', ' ', '\t']) ++ .map(str::trim) ++ .filter(|entry| !entry.is_empty()) ++ .map(|entry| entry.replace('\\', "/")) ++ .collect(), ++ ) ++} ++ ++/// Warning-band findings only produce GitHub annotations when the file was ++/// changed by the current pull request, so unchanged files stop repeating the ++/// same warning on every run. The hard limit stays repository-wide. ++fn should_annotate(finding: &Finding, changed: Option<&BTreeSet>) -> bool { ++ changed.map_or(true, |changed| changed.contains(&finding.file)) ++} ++ + #[cfg(not(test))] +-fn print_warnings(warnings: &[Finding]) { ++fn print_warnings(warnings: &[Finding], changed: Option<&BTreeSet>) { + if warnings.is_empty() { + return; + } + + for warning in warnings { +- let annotation = warning_annotation(warning); +- println!("{annotation}"); ++ if should_annotate(warning, changed) { ++ println!("{}", warning_annotation(warning)); ++ } + println!( + "WARNING: {} has {} lines (approaching limit of {MAX_LINES}, warning threshold: {WARN_LINES})", + warning.file, warning.lines +@@ -203,7 +232,9 @@ + let cwd = std::env::current_dir().expect("Failed to get current directory"); + let result = check_directory(&cwd); + +- print_warnings(&result.warnings); ++ let raw_changed = std::env::var("CHANGED_FILES").ok(); ++ let changed = changed_files(raw_changed.as_deref()); ++ print_warnings(&result.warnings, changed.as_ref()); + + if result.violations.is_empty() { + println!("All files are within the line limit\n"); +@@ -278,6 +309,74 @@ + ); + } + ++ #[test] ++ fn only_changed_files_in_the_warning_band_are_annotated() { ++ let repo = temp_dir("changed-only"); ++ let src_dir = repo.join("src"); ++ fs::create_dir_all(&src_dir).unwrap(); ++ write_rust_file_with_lines(&src_dir.join("changed.rs"), WARN_LINES + 1); ++ write_rust_file_with_lines(&src_dir.join("unchanged.rs"), WARN_LINES + 1); ++ write_rust_file_with_lines(&src_dir.join("over_limit.rs"), MAX_LINES + 1); ++ ++ let result = check_directory(&repo); ++ let changed = changed_files(Some("src/changed.rs\n")).unwrap(); ++ ++ let annotated: Vec<&str> = result ++ .warnings ++ .iter() ++ .filter(|finding| should_annotate(finding, Some(&changed))) ++ .map(|finding| finding.file.as_str()) ++ .collect(); ++ assert_eq!(annotated, vec!["src/changed.rs"]); ++ ++ // The hard limit stays repository-wide regardless of what changed. ++ assert_eq!( ++ result.violations, ++ vec![Finding { ++ file: "src/over_limit.rs".to_string(), ++ lines: MAX_LINES + 1, ++ }] ++ ); ++ // Both warning-band files remain in the baseline report. ++ assert_eq!(result.warnings.len(), 2); ++ } ++ ++ #[test] ++ fn missing_changed_file_list_annotates_every_warning() { ++ let finding = Finding { ++ file: "src/near_limit.rs".to_string(), ++ lines: WARN_LINES + 1, ++ }; ++ ++ assert_eq!(changed_files(None), None); ++ assert!(should_annotate(&finding, None)); ++ } ++ ++ #[test] ++ fn empty_changed_file_list_annotates_nothing() { ++ let finding = Finding { ++ file: "src/near_limit.rs".to_string(), ++ lines: WARN_LINES + 1, ++ }; ++ let changed = changed_files(Some(" \n")).unwrap(); ++ ++ assert!(changed.is_empty()); ++ assert!(!should_annotate(&finding, Some(&changed))); ++ } ++ ++ #[test] ++ fn changed_file_list_accepts_space_and_newline_separators() { ++ let changed = changed_files(Some("src/a.rs src/b.rs\nsrc\\c.rs\n")).unwrap(); ++ ++ assert_eq!( ++ changed, ++ ["src/a.rs", "src/b.rs", "src/c.rs"] ++ .into_iter() ++ .map(String::from) ++ .collect::>() ++ ); ++ } ++ + #[test] + fn warning_annotation_uses_github_actions_format() { + let finding = Finding { +########## check-release-needed.rs ########## +--- rust/scripts/check-release-needed.rs 2026-09-04 20:28:25.904207376 +0000 ++++ /tmp/tmpl/rust-tmpl/scripts/check-release-needed.rs 2026-09-04 20:31:17.683463071 +0000 +@@ -48,6 +48,8 @@ + + #[path = "rust-paths.rs"] + mod rust_paths; ++#[path = "release-naming.rs"] ++mod release_naming; + + fn get_arg(name: &str) -> Option { + let args: Vec = env::args().collect(); +@@ -312,7 +314,8 @@ + + if !has_fragments { + let crate_published = check_version_on_crates_io(&crate_name, ¤t_version); +- let tag_prefix = get_arg("tag-prefix").unwrap_or_else(|| "v".to_string()); ++ let tag_prefix = get_arg("tag-prefix") ++ .unwrap_or_else(|| release_naming::tag_prefix_for_rust_root(&rust_root).to_string()); + let dockerhub_image = docker_hub_image_to_check(); + let dockerhub_required = dockerhub_image.is_some(); + let dockerhub_published = dockerhub_image +########## publish-crate.rs ########## +--- rust/scripts/publish-crate.rs 2026-09-04 20:28:25.907207345 +0000 ++++ /tmp/tmpl/rust-tmpl/scripts/publish-crate.rs 2026-09-04 20:31:17.685463051 +0000 +@@ -50,6 +50,10 @@ + None + } + ++fn needs_cd(rust_root: &str) -> bool { ++ rust_root != "." ++} ++ + fn set_output(key: &str, value: &str) { + if let Ok(output_file) = env::var("GITHUB_OUTPUT") { + if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(&output_file) { +@@ -179,18 +183,19 @@ + println!("Using provided authentication token"); + } + +- // Build the cargo publish command. Use the manifest path so this works for +- // both a standalone crate and a crate nested under rust/. ++ // Build the cargo publish command + let mut cmd = Command::new("cargo"); +- cmd.arg("publish") +- .arg("--allow-dirty") +- .arg("--manifest-path") +- .arg(&package_manifest); ++ cmd.arg("publish").arg("--allow-dirty").arg("-p").arg(&name); + + if let Some(t) = &token { + cmd.arg("--token").arg(t); + } + ++ // For multi-language repos, change to the rust directory ++ if needs_cd(&rust_root) { ++ cmd.current_dir(&rust_root); ++ } ++ + let output = cmd.output().expect("Failed to execute cargo publish"); + + if output.status.success() { +@@ -259,7 +264,7 @@ + // A rate-limit is a deferred, automatically-recoverable outcome: exit + // successfully so the release job does not go red over a transient + // crates.io throttle. Downstream release-artifact steps are gated on a +- // successful publish (see .github/workflows/rust.yml), so a deferred ++ // successful publish (see .github/workflows/release.yml), so a deferred + // upload never produces partial Docker/GitHub release artifacts. + if kind.is_deferred() { + return; +########## wait-for-crate.rs ########## +--- rust/scripts/wait-for-crate.rs 2026-09-04 20:28:25.908207335 +0000 ++++ /tmp/tmpl/rust-tmpl/scripts/wait-for-crate.rs 2026-09-04 20:31:17.686463041 +0000 +@@ -5,6 +5,13 @@ + //! lag briefly. Waiting here makes Docker Hub tags and GitHub releases point at a + //! crate version that users can already resolve. + //! ++//! Visibility is probed against the sparse index (`https://index.crates.io`), ++//! which is what `cargo` resolves dependencies against and which is not rate ++//! limited, with the JSON API as a fallback. A probe that fails for a reason ++//! other than "this version does not exist" (403, 429, 5xx, DNS, TLS) is ++//! reported as *unknown*, never as "not published": a failed probe says nothing ++//! about whether `cargo publish` succeeded. See issue #143. ++//! + //! Usage: + //! rust-script scripts/wait-for-crate.rs --release-version + //! +@@ -32,6 +39,25 @@ + #[path = "rust-paths.rs"] + mod rust_paths; + ++/// crates.io answers 403 to clients that do not identify themselves, and asks ++/// that the contact address in the `User-Agent` be reachable. ++const USER_AGENT: &str = "rust-script-wait-for-crate (+https://github.com/link-foundation/rust-ai-driven-development-pipeline-template)"; ++ ++/// What a crates.io probe actually established. ++/// ++/// A bare `bool` cannot carry the difference between "crates.io said this ++/// version does not exist" and "crates.io did not answer", which is why a ++/// throttled probe used to be reported as a failed release. ++#[derive(Debug, Clone, PartialEq, Eq)] ++enum Visibility { ++ /// The version is resolvable by `cargo`. ++ Published, ++ /// crates.io answered, and the version is not there (yet). ++ NotPublishedYet, ++ /// crates.io could not be consulted; this says nothing about the release. ++ Unknown(String), ++} ++ + fn get_arg(name: &str) -> Option { + let args: Vec = env::args().collect(); + let flag = format!("--{}", name); +@@ -78,26 +104,152 @@ + .unwrap_or(default) + } + +-fn crate_version_exists(crate_name: &str, version: &str) -> bool { +- let url = format!("https://crates.io/api/v1/crates/{}/{}", crate_name, version); ++/// Sparse index path for a crate, following the `1/x`, `2/xy`, `3/x/xyz`, ++/// `ab/cd/name` layout documented by the registry index specification. ++fn index_path(crate_name: &str) -> String { ++ let name = crate_name.to_lowercase(); ++ let chars: Vec = name.chars().collect(); ++ ++ match chars.len() { ++ 0 => name, ++ 1 => format!("1/{}", name), ++ 2 => format!("2/{}", name), ++ 3 => format!("3/{}/{}", chars[0], name), ++ _ => format!("{}{}/{}{}/{}", chars[0], chars[1], chars[2], chars[3], name), ++ } ++} ++ ++fn index_url(crate_name: &str) -> String { ++ format!("https://index.crates.io/{}", index_path(crate_name)) ++} + +- match ureq::get(&url) +- .set("User-Agent", "rust-script-wait-for-crate") ++fn api_url(crate_name: &str, version: &str) -> String { ++ format!("https://crates.io/api/v1/crates/{}/{}", crate_name, version) ++} ++ ++/// The sparse index returns 200 for any existing crate, so the version has to be ++/// matched inside the newline-delimited JSON body rather than inferred from the ++/// status code. ++fn index_body_has_version(body: &str, version: &str) -> bool { ++ body.lines().any(|line| { ++ line.split("\"vers\"").skip(1).any(|rest| { ++ let rest = rest.trim_start(); ++ let Some(rest) = rest.strip_prefix(':') else { ++ return false; ++ }; ++ let rest = rest.trim_start(); ++ rest.strip_prefix('"') ++ .and_then(|rest| rest.split('"').next()) ++ .is_some_and(|found| found == version) ++ }) ++ }) ++} ++ ++/// Classify a sparse index response. A 404 there means the crate has never been ++/// published under that name; any other non-200 is an unusable answer. ++fn classify_index_response(status: u16, body: &str, version: &str) -> Visibility { ++ match status { ++ 200 => { ++ if index_body_has_version(body, version) { ++ Visibility::Published ++ } else { ++ Visibility::NotPublishedYet ++ } ++ } ++ 404 => Visibility::NotPublishedYet, ++ other => Visibility::Unknown(format!("index.crates.io responded HTTP {}", other)), ++ } ++} ++ ++/// Classify a JSON API response for a specific version. ++fn classify_api_status(status: u16) -> Visibility { ++ match status { ++ 200 => Visibility::Published, ++ 404 => Visibility::NotPublishedYet, ++ other => Visibility::Unknown(format!("crates.io API responded HTTP {}", other)), ++ } ++} ++ ++/// Prefer a definitive answer from either source; only report `Unknown` when ++/// neither source could be consulted. ++fn combine(index: Visibility, api: Visibility) -> Visibility { ++ match (index, api) { ++ (Visibility::Published, _) | (_, Visibility::Published) => Visibility::Published, ++ (Visibility::NotPublishedYet, _) | (_, Visibility::NotPublishedYet) => { ++ Visibility::NotPublishedYet ++ } ++ (Visibility::Unknown(index_reason), Visibility::Unknown(api_reason)) => { ++ Visibility::Unknown(format!("{}; {}", index_reason, api_reason)) ++ } ++ } ++} ++ ++/// The message printed when the wait runs out of attempts. `last_unknown` is ++/// `Some(..)` when no attempt ever got a definitive answer, which means the ++/// release status is unknown rather than broken. ++fn failure_message( ++ crate_name: &str, ++ version: &str, ++ max_attempts: u64, ++ last_unknown: Option<&str>, ++) -> String { ++ match last_unknown { ++ Some(reason) => format!( ++ "Error: could not determine whether {}@{} is on crates.io; \ ++ all {} attempts failed to get an answer, the last one with: {}. \ ++ This does NOT mean the publish failed. Check the sparse index before \ ++ treating the release as broken: curl -s -A 'ci (+https://example.com)' {} | grep '\"vers\":\"{}\"'", ++ crate_name, version, max_attempts, reason, index_url(crate_name), version ++ ), ++ None => format!( ++ "Error: {}@{} was not visible on crates.io after {} attempts", ++ crate_name, version, max_attempts ++ ), ++ } ++} ++ ++#[cfg(not(test))] ++fn check_index(crate_name: &str, version: &str) -> Visibility { ++ match ureq::get(&index_url(crate_name)) ++ .set("User-Agent", USER_AGENT) + .call() + { +- Ok(response) => response.status() == 200, +- Err(ureq::Error::Status(404, _)) => false, +- Err(e) => { +- eprintln!("Warning: Could not check crates.io: {}", e); +- false ++ Ok(response) => { ++ let status = response.status(); ++ let body = response.into_string().unwrap_or_default(); ++ classify_index_response(status, &body, version) + } ++ Err(ureq::Error::Status(status, _)) => classify_index_response(status, "", version), ++ Err(e) => Visibility::Unknown(format!("index.crates.io request failed: {}", e)), ++ } ++} ++ ++#[cfg(not(test))] ++fn check_api(crate_name: &str, version: &str) -> Visibility { ++ match ureq::get(&api_url(crate_name, version)) ++ .set("User-Agent", USER_AGENT) ++ .call() ++ { ++ Ok(response) => classify_api_status(response.status()), ++ Err(ureq::Error::Status(status, _)) => classify_api_status(status), ++ Err(e) => Visibility::Unknown(format!("crates.io API request failed: {}", e)), ++ } ++} ++ ++#[cfg(not(test))] ++fn crate_version_visibility(crate_name: &str, version: &str) -> Visibility { ++ let index = check_index(crate_name, version); ++ if index == Visibility::Published { ++ return index; + } ++ combine(index, check_api(crate_name, version)) + } + + fn should_skip_crate_wait(crate_name: &str) -> bool { + crate_name == "example-sum-package-name" + } + ++#[cfg(not(test))] + fn main() { + let rust_root = match rust_paths::get_rust_root(None, true) { + Ok(root) => root, +@@ -136,35 +288,68 @@ + return; + } + ++ let mut last_unknown: Option = None; ++ let mut saw_definitive_answer = false; ++ + for attempt in 1..=max_attempts { +- if crate_version_exists(&crate_name, &version) { +- println!( +- "{}@{} is visible on crates.io after attempt {}", +- crate_name, version, attempt +- ); +- set_output("crate_available", "true"); +- return; ++ match crate_version_visibility(&crate_name, &version) { ++ Visibility::Published => { ++ println!( ++ "{}@{} is visible on crates.io after attempt {}", ++ crate_name, version, attempt ++ ); ++ set_output("crate_available", "true"); ++ return; ++ } ++ Visibility::NotPublishedYet => { ++ saw_definitive_answer = true; ++ if attempt < max_attempts { ++ println!( ++ "{}@{} is not visible on crates.io yet (attempt {}/{}); waiting {}s", ++ crate_name, version, attempt, max_attempts, sleep_seconds ++ ); ++ } ++ } ++ Visibility::Unknown(reason) => { ++ eprintln!( ++ "Warning: could not check crates.io on attempt {}/{}: {}", ++ attempt, max_attempts, reason ++ ); ++ last_unknown = Some(reason); ++ if attempt < max_attempts { ++ println!("Retrying in {}s", sleep_seconds); ++ } ++ } + } + + if attempt < max_attempts { +- println!( +- "{}@{} is not visible on crates.io yet (attempt {}/{}); waiting {}s", +- crate_name, version, attempt, max_attempts, sleep_seconds +- ); + thread::sleep(Duration::from_secs(sleep_seconds)); + } + } + + eprintln!( +- "Error: {}@{} was not visible on crates.io after {} attempts", +- crate_name, version, max_attempts ++ "{}", ++ failure_message( ++ &crate_name, ++ &version, ++ max_attempts, ++ if saw_definitive_answer { ++ None ++ } else { ++ last_unknown.as_deref() ++ }, ++ ) + ); + exit(1); + } + + #[cfg(test)] + mod tests { +- use super::should_skip_crate_wait; ++ use super::{ ++ api_url, classify_api_status, classify_index_response, combine, failure_message, ++ index_body_has_version, index_path, index_url, should_skip_crate_wait, Visibility, ++ USER_AGENT, ++ }; + + #[test] + fn skips_template_default_package_name() { +@@ -175,4 +360,171 @@ + fn waits_for_real_package_names() { + assert!(!should_skip_crate_wait("real-package-name")); + } ++ ++ #[test] ++ fn index_paths_follow_the_registry_layout() { ++ assert_eq!(index_path("a"), "1/a"); ++ assert_eq!(index_path("ab"), "2/ab"); ++ assert_eq!(index_path("abc"), "3/a/abc"); ++ assert_eq!(index_path("serde"), "se/rd/serde"); ++ assert_eq!( ++ index_path("links-notation"), ++ "li/nk/links-notation", ++ "the layout used by the crate from issue #143" ++ ); ++ assert_eq!(index_path("Serde"), "se/rd/serde", "names are lowercased"); ++ } ++ ++ #[test] ++ fn index_url_points_at_the_sparse_index() { ++ assert_eq!(index_url("serde"), "https://index.crates.io/se/rd/serde"); ++ } ++ ++ #[test] ++ fn api_url_points_at_the_version_endpoint() { ++ assert_eq!( ++ api_url("serde", "1.0.228"), ++ "https://crates.io/api/v1/crates/serde/1.0.228" ++ ); ++ } ++ ++ #[test] ++ fn user_agent_carries_contact_information() { ++ assert!( ++ USER_AGENT.contains("+https://"), ++ "crates.io asks that clients be reachable" ++ ); ++ } ++ ++ #[test] ++ fn index_body_matches_the_published_version_only() { ++ let body = concat!( ++ r#"{"name":"links-notation","vers":"0.15.0","deps":[]}"#, ++ "\n", ++ r#"{"name":"links-notation","vers":"0.16.0","deps":[]}"#, ++ "\n" ++ ); ++ ++ assert!(index_body_has_version(body, "0.16.0")); ++ assert!(index_body_has_version(body, "0.15.0")); ++ assert!(!index_body_has_version(body, "0.17.0")); ++ assert!( ++ !index_body_has_version(body, "0.16"), ++ "a prefix of a published version is not that version" ++ ); ++ } ++ ++ #[test] ++ fn index_body_tolerates_whitespace_around_the_version_field() { ++ let body = r#"{"name":"demo", "vers" : "1.2.3" }"#; ++ assert!(index_body_has_version(body, "1.2.3")); ++ } ++ ++ /// The regression from issue #143: a throttled or forbidden probe must not ++ /// be classified as "not published". ++ #[test] ++ fn throttled_and_forbidden_probes_are_unknown_not_missing() { ++ for status in [403_u16, 429, 500, 502, 503] { ++ assert!( ++ matches!( ++ classify_index_response(status, "", "1.0.0"), ++ Visibility::Unknown(_) ++ ), ++ "index HTTP {} must not be reported as a missing version", ++ status ++ ); ++ assert!( ++ matches!(classify_api_status(status), Visibility::Unknown(_)), ++ "API HTTP {} must not be reported as a missing version", ++ status ++ ); ++ } ++ } ++ ++ #[test] ++ fn definitive_answers_are_classified_as_such() { ++ assert_eq!( ++ classify_index_response(200, r#"{"vers":"1.0.0"}"#, "1.0.0"), ++ Visibility::Published ++ ); ++ assert_eq!( ++ classify_index_response(200, r#"{"vers":"0.9.0"}"#, "1.0.0"), ++ Visibility::NotPublishedYet ++ ); ++ assert_eq!( ++ classify_index_response(404, "", "1.0.0"), ++ Visibility::NotPublishedYet ++ ); ++ assert_eq!(classify_api_status(200), Visibility::Published); ++ assert_eq!(classify_api_status(404), Visibility::NotPublishedYet); ++ } ++ ++ #[test] ++ fn a_definitive_answer_from_either_source_wins() { ++ assert_eq!( ++ combine( ++ Visibility::Unknown("index.crates.io responded HTTP 429".into()), ++ Visibility::Published ++ ), ++ Visibility::Published ++ ); ++ assert_eq!( ++ combine( ++ Visibility::NotPublishedYet, ++ Visibility::Unknown("crates.io API responded HTTP 403".into()) ++ ), ++ Visibility::NotPublishedYet ++ ); ++ } ++ ++ #[test] ++ fn unknown_is_reported_only_when_neither_source_answered() { ++ let combined = combine( ++ Visibility::Unknown("index.crates.io responded HTTP 429".into()), ++ Visibility::Unknown("crates.io API responded HTTP 403".into()), ++ ); ++ ++ let Visibility::Unknown(reason) = combined else { ++ panic!("two unusable answers must combine into Unknown"); ++ }; ++ assert!(reason.contains("429") && reason.contains("403")); ++ } ++ ++ /// The false negative from issue #143: the failure message must not claim ++ /// the release did not happen when nothing was ever established. ++ #[test] ++ fn failure_message_distinguishes_unknown_from_missing() { ++ let unknown = failure_message( ++ "links-notation", ++ "0.16.0", ++ 30, ++ Some("crates.io API responded HTTP 403"), ++ ); ++ assert!( ++ unknown.contains("could not determine"), ++ "unknown outcome must not be phrased as a missing version: {}", ++ unknown ++ ); ++ assert!( ++ unknown.contains("does NOT mean the publish failed"), ++ "the message must point the reader away from the publish step: {}", ++ unknown ++ ); ++ assert!( ++ unknown.contains("https://index.crates.io/li/nk/links-notation"), ++ "the message must show how to verify against the sparse index: {}", ++ unknown ++ ); ++ assert!( ++ !unknown.contains("was not visible on crates.io after"), ++ "unknown outcome must not reuse the missing-version wording: {}", ++ unknown ++ ); ++ ++ let missing = failure_message("links-notation", "0.16.0", 30, None); ++ assert_eq!( ++ missing, ++ "Error: links-notation@0.16.0 was not visible on crates.io after 30 attempts" ++ ); ++ } + } diff --git a/dev/log/issues/199/pulls/200/analysis/zizmor-before.log b/dev/log/issues/199/pulls/200/analysis/zizmor-before.log new file mode 100644 index 00000000..37cc1efa --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/zizmor-before.log @@ -0,0 +1,622 @@ + INFO zizmor: 🌈 zizmor v1.30.0 + WARN audit: zizmor: zizmor is running in offline mode by default; some audits and auto-fixes will not be available. see https://docs.zizmor.sh/usage/#operating-modes for details + INFO audit: zizmor: 🌈 completed .github/workflows/js.yml + INFO audit: zizmor: 🌈 completed .github/workflows/parity.yml + INFO audit: zizmor: 🌈 completed .github/workflows/rust.yml +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:1:1 + | + 1 | / name: JavaScript checks and release + 2 | | + 3 | | on: + 4 | | push: +... | +419 | | - Description: ${{ github.event.inputs.description || 'Manual JavaScript release' }} +420 | | - Triggered by: @${{ github.actor }} + | |_________________________________________________^ default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:47:3 + | +47 | / changeset-check: +48 | | name: Check for JavaScript changesets +49 | | runs-on: ubuntu-latest +50 | | timeout-minutes: 10 +... | +78 | | bun scripts/validate-changeset.mjs + | | ^ + | | | + | |____________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:80:3 + | + 80 | / lint: + 81 | | name: Lint and format JavaScript + 82 | | runs-on: ubuntu-latest + 83 | | timeout-minutes: 10 +... | +107 | | working-directory: js +108 | | run: bun run check:duplication + | | ^ + | | | + | |______________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:110:3 + | +110 | / test: +111 | | name: Test JavaScript (${{ matrix.runtime }} on ${{ matrix.os }}) +112 | | runs-on: ${{ matrix.os }} +113 | | timeout-minutes: 30 +... | +201 | | node --test js/tests/node-terminal-artifacts.mjs +202 | | node --test js/tests/node-commonjs-entry.mjs + | | ^ + | | | + | |______________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:73:22 + | +72 | run: | + | --- this run block +73 | if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then + | ^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:345:81 + | +345 | run: bun scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:345:134 + | +345 | ... run: bun scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:398:71 + | +398 | run: bun scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.e... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:398:124 + | +398 | ... run: bun scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:53:15 + | +53 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:58:15 + | +58 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:87:15 + | +87 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:90:15 + | +90 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:132:15 + | +132 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:136:15 + | +136 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:142:15 + | +142 | uses: actions/setup-node@v6 + | ^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:173:15 + | +173 | uses: actions/setup-node@v6 + | ^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:222:15 + | +222 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:227:15 + | +227 | uses: actions/setup-node@v6 + | ^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:233:15 + | +233 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:319:15 + | +319 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:324:15 + | +324 | uses: actions/setup-node@v6 + | ^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:330:15 + | +330 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:383:15 + | +383 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:388:15 + | +388 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:405:15 + | +405 | uses: peter-evans/create-pull-request@v8 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/parity.yml:30:15 + | +30 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:1:1 + | + 1 | / name: Rust checks and release + 2 | | + 3 | | on: + 4 | | push: +... | +354 | | - Description: ${{ github.event.inputs.description || 'Manual Rust release' }} +355 | | - Triggered by: @${{ github.actor }} + | |_________________________________________________^ default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:52:3 + | +52 | / changelog: +53 | | name: Rust changelog fragment check +54 | | runs-on: ubuntu-latest +55 | | timeout-minutes: 10 +... | +70 | | GITHUB_BASE_REF: ${{ github.base_ref }} +71 | | run: rust-script rust/scripts/check-changelog-fragment.rs + | | ^ + | | | + | |_________________________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:73:3 + | + 73 | / lint: + 74 | | name: Lint and format Rust + 75 | | runs-on: ubuntu-latest + 76 | | timeout-minutes: 10 +... | +103 | | working-directory: rust +104 | | run: cargo clippy --all-targets --all-features + | | ^ + | | | + | |______________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:106:3 + | +106 | / test: +107 | | name: Test Rust (${{ matrix.os }}) +108 | | runs-on: ${{ matrix.os }} +109 | | timeout-minutes: 30 +... | +138 | | working-directory: rust +139 | | run: cargo test --doc --all-features --verbose + | | ^ + | | | + | |______________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:141:3 + | +141 | / scripts: +142 | | name: Test Rust release scripts +143 | | runs-on: ubuntu-latest +144 | | timeout-minutes: 15 +... | +181 | | done +182 | | exit $status + | | ^ + | | | + | |______________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:184:3 + | +184 | / build: +185 | | name: Build Rust package +186 | | runs-on: ubuntu-latest +187 | | timeout-minutes: 10 +... | +212 | | working-directory: rust +213 | | run: cargo package --allow-dirty + | | ^ + | | | + | |________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:300:78 + | +300 | run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ g... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:300:131 + | +300 | ... run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:337:85 + | +337 | run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:337:138 + | +337 | ... run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:58:15 + | +58 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:63:15 + | +63 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:80:15 + | +80 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:83:15 + | +83 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:88:15 + | +88 | uses: actions/cache@v5 + | ^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:117:15 + | +117 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:120:15 + | +120 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:123:15 + | +123 | uses: actions/cache@v5 + | ^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:148:15 + | +148 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:151:15 + | +151 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:154:15 + | +154 | uses: actions/cache@v5 + | ^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:191:15 + | +191 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:194:15 + | +194 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:197:15 + | +197 | uses: actions/cache@v5 + | ^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:233:15 + | +233 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:238:15 + | +238 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:288:15 + | +288 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:293:15 + | +293 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:326:15 + | +326 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:331:15 + | +331 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:340:15 + | +340 | uses: peter-evans/create-pull-request@v8 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +112 findings (28 ignored, 26 suppressed, 9 unsafe fixes): 0 informational, 0 low, 10 medium, 48 high diff --git a/dev/log/issues/199/pulls/200/analysis/zizmor-step2.log b/dev/log/issues/199/pulls/200/analysis/zizmor-step2.log new file mode 100644 index 00000000..ed284d6d --- /dev/null +++ b/dev/log/issues/199/pulls/200/analysis/zizmor-step2.log @@ -0,0 +1,416 @@ + INFO zizmor: 🌈 zizmor v1.30.0 + WARN audit: zizmor: zizmor is running in offline mode by default; some audits and auto-fixes will not be available. see https://docs.zizmor.sh/usage/#operating-modes for details + INFO audit: zizmor: 🌈 completed .github/workflows/js.yml + INFO audit: zizmor: 🌈 completed .github/workflows/parity.yml + INFO audit: zizmor: 🌈 completed .github/workflows/rust.yml + INFO audit: zizmor: 🌈 completed .github/workflows/workflows.yml +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:1:1 + | + 1 | / name: JavaScript checks and release + 2 | | + 3 | | on: + 4 | | push: +... | +419 | | - Description: ${{ github.event.inputs.description || 'Manual JavaScript release' }} +420 | | - Triggered by: @${{ github.actor }} + | |_________________________________________________^ default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:47:3 + | +47 | / changeset-check: +48 | | name: Check for JavaScript changesets +49 | | runs-on: ubuntu-latest +50 | | timeout-minutes: 10 +... | +78 | | bun scripts/validate-changeset.mjs + | | ^ + | | | + | |____________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:80:3 + | + 80 | / lint: + 81 | | name: Lint and format JavaScript + 82 | | runs-on: ubuntu-latest + 83 | | timeout-minutes: 10 +... | +107 | | working-directory: js +108 | | run: bun run check:duplication + | | ^ + | | | + | |______________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/js.yml:110:3 + | +110 | / test: +111 | | name: Test JavaScript (${{ matrix.runtime }} on ${{ matrix.os }}) +112 | | runs-on: ${{ matrix.os }} +113 | | timeout-minutes: 30 +... | +201 | | node --test js/tests/node-terminal-artifacts.mjs +202 | | node --test js/tests/node-commonjs-entry.mjs + | | ^ + | | | + | |______________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:73:22 + | +72 | run: | + | --- this run block +73 | if [[ "${{ github.head_ref }}" == "changeset-release/"* ]]; then + | ^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:345:81 + | +345 | run: bun scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:345:134 + | +345 | ... run: bun scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:398:71 + | +398 | run: bun scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.e... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/js.yml:398:124 + | +398 | ... run: bun scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:58:15 + | +58 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:90:15 + | +90 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:136:15 + | +136 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:233:15 + | +233 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:330:15 + | +330 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:388:15 + | +388 | uses: oven-sh/setup-bun@v2 + | ^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/js.yml:405:15 + | +405 | uses: peter-evans/create-pull-request@v8 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:1:1 + | + 1 | / name: Rust checks and release + 2 | | + 3 | | on: + 4 | | push: +... | +369 | | - Description: ${{ github.event.inputs.description || 'Manual Rust release' }} +370 | | - Triggered by: @${{ github.actor }} + | |_________________________________________________^ default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:58:3 + | +58 | / changelog: +59 | | name: Rust changelog fragment check +60 | | runs-on: ubuntu-latest +61 | | timeout-minutes: 10 +... | +76 | | GITHUB_BASE_REF: ${{ github.base_ref }} +77 | | run: rust-script rust/scripts/check-changelog-fragment.rs + | | ^ + | | | + | |_________________________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:79:3 + | + 79 | / lint: + 80 | | name: Lint and format Rust + 81 | | runs-on: ubuntu-latest + 82 | | timeout-minutes: 10 +... | +118 | | # own gate. RUSTDOCFLAGS=-Dwarnings comes from the workflow env. +119 | | run: cargo doc --no-deps --all-features + | | ^ + | | | + | |_______________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:121:3 + | +121 | / test: +122 | | name: Test Rust (${{ matrix.os }}) +123 | | runs-on: ${{ matrix.os }} +124 | | timeout-minutes: 30 +... | +153 | | working-directory: rust +154 | | run: cargo test --doc --all-features --verbose + | | ^ + | | | + | |______________________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:156:3 + | +156 | / scripts: +157 | | name: Test Rust release scripts +158 | | runs-on: ubuntu-latest +159 | | timeout-minutes: 15 +... | +196 | | done +197 | | exit $status + | | ^ + | | | + | |______________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +warning[excessive-permissions]: overly broad permissions + --> .github/workflows/rust.yml:199:3 + | +199 | / build: +200 | | name: Build Rust package +201 | | runs-on: ubuntu-latest +202 | | timeout-minutes: 10 +... | +227 | | working-directory: rust +228 | | run: cargo package --allow-dirty + | | ^ + | | | + | |________________________________________this job + | default permissions used due to no permissions: block + | + = note: audit confidence → Medium + = help: audit documentation → https://docs.zizmor.sh/audits/#excessive-permissions + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:315:78 + | +315 | run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ g... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:315:131 + | +315 | ... run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:352:85 + | +352 | run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description... + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[template-injection]: code injection via template expansion + --> .github/workflows/rust.yml:352:138 + | +352 | ... run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + | --- this run block ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + = note: this finding has an auto-fix + = help: audit documentation → https://docs.zizmor.sh/audits/#template-injection + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:69:15 + | +69 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:89:15 + | +89 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:135:15 + | +135 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:166:15 + | +166 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:209:15 + | +209 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:253:15 + | +253 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:308:15 + | +308 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:346:15 + | +346 | uses: dtolnay/rust-toolchain@stable + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +error[unpinned-uses]: unpinned action reference + --> .github/workflows/rust.yml:355:15 + | +355 | uses: peter-evans/create-pull-request@v8 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action is not pinned to a hash (required by blanket policy) + | + = note: audit confidence → High + = help: audit documentation → https://docs.zizmor.sh/audits/#unpinned-uses + +90 findings (28 ignored, 27 suppressed, 9 unsafe fixes): 0 informational, 0 low, 10 medium, 25 high diff --git a/dev/log/issues/199/pulls/200/api/branch-protection.json b/dev/log/issues/199/pulls/200/api/branch-protection.json new file mode 100644 index 00000000..405d446e --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/branch-protection.json @@ -0,0 +1 @@ +{"message":"Branch not protected","documentation_url":"https://docs.github.com/rest/branches/branch-protection#get-branch-protection","status":"404"}gh: Branch not protected (HTTP 404) diff --git a/dev/log/issues/199/pulls/200/api/issue-199.json b/dev/log/issues/199/pulls/200/api/issue-199.json new file mode 100644 index 00000000..20f0ac59 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/issue-199.json @@ -0,0 +1 @@ +{"author":{"id":"MDQ6VXNlcjE0MzE5MDQ=","is_bot":false,"login":"konard","name":"Konstantin Diachenko"},"body":"### Recent CI/CD runs on `main`\n\n| Workflow | Status | Conclusion | Commit | Run |\n| --- | --- | --- | --- | --- |\n| JavaScript checks and release | completed | failure | `000dbea` | [run](https://github.com/link-foundation/command-stream/actions/runs/33914574283) |\n| Rust checks and release | completed | success | `000dbea` | [run](https://github.com/link-foundation/command-stream/actions/runs/33914574263) |\n| Checks and release | completed | success | `2cd09e6` | [run](https://github.com/link-foundation/command-stream/actions/runs/27137676052) |\n| Deploy | completed | success | `e68c710` | [run](https://github.com/link-foundation/command-stream/actions/runs/20540466607) |\n| CI | completed | success | `e68c710` | [run](https://github.com/link-foundation/command-stream/actions/runs/20540466606) |\n\nUse all the best practices from CI/CD templates (check full file tree to compare for all GitHub workflow and CI/CD scripts file), if the same issue is found in template report issue also in templates:\n\n- https://github.com/link-foundation/js-ai-driven-development-pipeline-template\n- https://github.com/link-foundation/rust-ai-driven-development-pipeline-template\n\nWe should compare all files, so we don't have more CI/CD errors in the future and reuse all the best practices from these templates.\n\nFollow the CI/CD best practices collected in [https://github.com/link-assistant/hive-mind/blob/main/docs/CI-CD-BEST-PRACTICES.md](https://github.com/link-assistant/hive-mind/blob/main/docs/CI-CD-BEST-PRACTICES.md).\n\nPlease plan and execute everything in this single pull request, you have unlimited time and context, as context auto-compacts and you can continue indefinitely, until it is each and every requirement fully addressed, and everything is totally done.\n\n---\n\n
\nContext collected by /fix --ci-cd\n\n- **Repository:** [link-foundation/command-stream](https://github.com/link-foundation/command-stream)\n- **Default branch:** `main`\n- **Latest commit:** `4511c4d` ([commit](https://github.com/link-foundation/command-stream/commit/4511c4df6b358a44dc334dfd3b13916a441a946f)) — chore: release rust-v0.17.1 (Rust)\n- **CI/CD runs found:** 5 (1 not passing)\n\n**Detected languages**\n\n- **JavaScript** — 76.3%\n- **Rust** — 23.0%\n- **Shell** — 0.7%\n\n**Recommended CI/CD templates**\n\nApply the best practices from these templates, in priority order (most-used language first):\n\n1. **JavaScript / TypeScript** — [link-foundation/js-ai-driven-development-pipeline-template](https://github.com/link-foundation/js-ai-driven-development-pipeline-template) _(detected: JavaScript)_\n2. **Rust** — [link-foundation/rust-ai-driven-development-pipeline-template](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template) _(detected: Rust)_\n\nOther detected languages without a dedicated template: Shell.\n\n
","comments":[],"createdAt":"2026-09-04T20:27:53Z","title":"Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all"} diff --git a/dev/log/issues/199/pulls/200/api/pr-200.json b/dev/log/issues/199/pulls/200/api/pr-200.json new file mode 100644 index 00000000..caf97c2a --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/pr-200.json @@ -0,0 +1 @@ +{"body":"## 🤖 AI-Powered Solution Draft\n\nThis pull request is being automatically generated to solve issue #199.\n\n### 📋 Issue Reference\nFixes #199\n\n### 🚧 Status\n**Work in Progress** - The AI assistant is currently analyzing and implementing the solution draft.\n\n### 📝 Implementation Details\n_Details will be added as the solution draft is developed..._\n\n---\n*This PR was created automatically by the AI issue solver*","comments":[],"createdAt":"2026-09-04T20:28:37Z","headRefName":"issue-199-32c07917fc87","isDraft":true,"title":"[WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all"} diff --git a/dev/log/issues/199/pulls/200/api/repo.json b/dev/log/issues/199/pulls/200/api/repo.json new file mode 100644 index 00000000..5a71810b --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/repo.json @@ -0,0 +1 @@ +{"id":1036989446,"node_id":"R_kgDOPc80Bg","name":"command-stream","full_name":"link-foundation/command-stream","private":false,"owner":{"login":"link-foundation","id":176174013,"node_id":"O_kgDOCoAzvQ","avatar_url":"https://avatars.githubusercontent.com/u/176174013?v=4","gravatar_id":"","url":"https://api.github.com/users/link-foundation","html_url":"https://github.com/link-foundation","followers_url":"https://api.github.com/users/link-foundation/followers","following_url":"https://api.github.com/users/link-foundation/following{/other_user}","gists_url":"https://api.github.com/users/link-foundation/gists{/gist_id}","starred_url":"https://api.github.com/users/link-foundation/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/link-foundation/subscriptions","organizations_url":"https://api.github.com/users/link-foundation/orgs","repos_url":"https://api.github.com/users/link-foundation/repos","events_url":"https://api.github.com/users/link-foundation/events{/privacy}","received_events_url":"https://api.github.com/users/link-foundation/received_events","type":"Organization","user_view_type":"public","site_admin":false},"html_url":"https://github.com/link-foundation/command-stream","description":"$treamable commands executor","fork":false,"url":"https://api.github.com/repos/link-foundation/command-stream","forks_url":"https://api.github.com/repos/link-foundation/command-stream/forks","keys_url":"https://api.github.com/repos/link-foundation/command-stream/keys{/key_id}","collaborators_url":"https://api.github.com/repos/link-foundation/command-stream/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/link-foundation/command-stream/teams","hooks_url":"https://api.github.com/repos/link-foundation/command-stream/hooks","issue_events_url":"https://api.github.com/repos/link-foundation/command-stream/issues/events{/number}","events_url":"https://api.github.com/repos/link-foundation/command-stream/events","assignees_url":"https://api.github.com/repos/link-foundation/command-stream/assignees{/user}","branches_url":"https://api.github.com/repos/link-foundation/command-stream/branches{/branch}","tags_url":"https://api.github.com/repos/link-foundation/command-stream/tags","blobs_url":"https://api.github.com/repos/link-foundation/command-stream/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/link-foundation/command-stream/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/link-foundation/command-stream/git/refs{/sha}","trees_url":"https://api.github.com/repos/link-foundation/command-stream/git/trees{/sha}","statuses_url":"https://api.github.com/repos/link-foundation/command-stream/statuses/{sha}","languages_url":"https://api.github.com/repos/link-foundation/command-stream/languages","stargazers_url":"https://api.github.com/repos/link-foundation/command-stream/stargazers","contributors_url":"https://api.github.com/repos/link-foundation/command-stream/contributors","subscribers_url":"https://api.github.com/repos/link-foundation/command-stream/subscribers","subscription_url":"https://api.github.com/repos/link-foundation/command-stream/subscription","commits_url":"https://api.github.com/repos/link-foundation/command-stream/commits{/sha}","git_commits_url":"https://api.github.com/repos/link-foundation/command-stream/git/commits{/sha}","comments_url":"https://api.github.com/repos/link-foundation/command-stream/comments{/number}","issue_comment_url":"https://api.github.com/repos/link-foundation/command-stream/issues/comments{/number}","contents_url":"https://api.github.com/repos/link-foundation/command-stream/contents/{+path}","compare_url":"https://api.github.com/repos/link-foundation/command-stream/compare/{base}...{head}","merges_url":"https://api.github.com/repos/link-foundation/command-stream/merges","archive_url":"https://api.github.com/repos/link-foundation/command-stream/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/link-foundation/command-stream/downloads","issues_url":"https://api.github.com/repos/link-foundation/command-stream/issues{/number}","pulls_url":"https://api.github.com/repos/link-foundation/command-stream/pulls{/number}","milestones_url":"https://api.github.com/repos/link-foundation/command-stream/milestones{/number}","notifications_url":"https://api.github.com/repos/link-foundation/command-stream/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/link-foundation/command-stream/labels{/name}","releases_url":"https://api.github.com/repos/link-foundation/command-stream/releases{/id}","deployments_url":"https://api.github.com/repos/link-foundation/command-stream/deployments","created_at":"2025-08-12T22:30:25Z","updated_at":"2026-09-04T20:10:41Z","pushed_at":"2026-09-04T20:28:29Z","git_url":"git://github.com/link-foundation/command-stream.git","ssh_url":"git@github.com:link-foundation/command-stream.git","clone_url":"https://github.com/link-foundation/command-stream.git","svn_url":"https://github.com/link-foundation/command-stream","homepage":null,"size":11814,"stargazers_count":4,"watchers_count":4,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":false,"has_wiki":true,"has_pages":false,"has_discussions":false,"forks_count":1,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":62,"license":{"key":"unlicense","name":"The Unlicense","spdx_id":"Unlicense","url":"https://api.github.com/licenses/unlicense","node_id":"MDc6TGljZW5zZTE1"},"allow_forking":true,"is_template":false,"web_commit_signoff_required":false,"has_pull_requests":true,"pull_request_creation_policy":"all","topics":[],"visibility":"public","forks":1,"open_issues":62,"watchers":4,"default_branch":"main","permissions":{"admin":true,"maintain":true,"push":true,"triage":true,"pull":true},"temp_clone_token":"","allow_squash_merge":true,"allow_merge_commit":true,"allow_rebase_merge":true,"allow_auto_merge":false,"delete_branch_on_merge":false,"allow_update_branch":false,"use_squash_pr_title_as_default":false,"squash_merge_commit_message":"COMMIT_MESSAGES","squash_merge_commit_title":"COMMIT_OR_PR_TITLE","merge_commit_message":"PR_TITLE","merge_commit_title":"MERGE_MESSAGE","custom_properties":{},"organization":{"login":"link-foundation","id":176174013,"node_id":"O_kgDOCoAzvQ","avatar_url":"https://avatars.githubusercontent.com/u/176174013?v=4","gravatar_id":"","url":"https://api.github.com/users/link-foundation","html_url":"https://github.com/link-foundation","followers_url":"https://api.github.com/users/link-foundation/followers","following_url":"https://api.github.com/users/link-foundation/following{/other_user}","gists_url":"https://api.github.com/users/link-foundation/gists{/gist_id}","starred_url":"https://api.github.com/users/link-foundation/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/link-foundation/subscriptions","organizations_url":"https://api.github.com/users/link-foundation/orgs","repos_url":"https://api.github.com/users/link-foundation/repos","events_url":"https://api.github.com/users/link-foundation/events{/privacy}","received_events_url":"https://api.github.com/users/link-foundation/received_events","type":"Organization","user_view_type":"public","site_admin":false},"security_and_analysis":{"secret_scanning":{"status":"disabled"},"secret_scanning_push_protection":{"status":"disabled"},"dependabot_security_updates":{"status":"disabled"},"secret_scanning_non_provider_patterns":{"status":"disabled"},"secret_scanning_validity_checks":{"status":"disabled"}},"network_count":1,"subscribers_count":0} \ No newline at end of file diff --git a/dev/log/issues/199/pulls/200/api/rulesets.json b/dev/log/issues/199/pulls/200/api/rulesets.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/rulesets.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/dev/log/issues/199/pulls/200/api/run-33897699209.json b/dev/log/issues/199/pulls/200/api/run-33897699209.json new file mode 100644 index 00000000..5227110a --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33897699209.json @@ -0,0 +1 @@ +{"conclusion":"failure","createdAt":"2026-09-04T16:54:18Z","databaseId":33897699209,"event":"push","headBranch":"main","headSha":"7e2b7018494b9ad9129391829a7abd5e4d085228","jobs":[{"completedAt":"2026-09-04T16:54:40Z","conclusion":"success","databaseId":101104129837,"name":"Lint and format JavaScript","startedAt":"2026-09-04T16:54:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:23Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:22Z","status":"completed"},{"completedAt":"2026-09-04T16:54:24Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:23Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:24Z","status":"completed"},{"completedAt":"2026-09-04T16:54:26Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:32Z","conclusion":"success","name":"Run ESLint","number":5,"startedAt":"2026-09-04T16:54:26Z","status":"completed"},{"completedAt":"2026-09-04T16:54:37Z","conclusion":"success","name":"Check formatting","number":6,"startedAt":"2026-09-04T16:54:32Z","status":"completed"},{"completedAt":"2026-09-04T16:54:37Z","conclusion":"success","name":"Check code duplication","number":7,"startedAt":"2026-09-04T16:54:37Z","status":"completed"},{"completedAt":"2026-09-04T16:54:37Z","conclusion":"success","name":"Post Setup Bun","number":13,"startedAt":"2026-09-04T16:54:37Z","status":"completed"},{"completedAt":"2026-09-04T16:54:38Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":14,"startedAt":"2026-09-04T16:54:37Z","status":"completed"},{"completedAt":"2026-09-04T16:54:38Z","conclusion":"success","name":"Complete job","number":15,"startedAt":"2026-09-04T16:54:38Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104129837"},{"completedAt":"2026-09-04T16:54:42Z","conclusion":"success","databaseId":101104130005,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T16:54:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:23Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:22Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:23Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:33Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:33Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T16:54:33Z","status":"completed"},{"completedAt":"2026-09-04T16:54:33Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T16:54:33Z","status":"completed"},{"completedAt":"2026-09-04T16:54:33Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T16:54:33Z","status":"completed"},{"completedAt":"2026-09-04T16:54:33Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T16:54:33Z","status":"completed"},{"completedAt":"2026-09-04T16:54:34Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T16:54:33Z","status":"completed"},{"completedAt":"2026-09-04T16:54:39Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T16:54:34Z","status":"completed"},{"completedAt":"2026-09-04T16:54:40Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T16:54:39Z","status":"completed"},{"completedAt":"2026-09-04T16:54:40Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T16:54:40Z","status":"completed"},{"completedAt":"2026-09-04T16:54:40Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T16:54:40Z","status":"completed"},{"completedAt":"2026-09-04T16:54:40Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T16:54:40Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130005"},{"completedAt":"2026-09-04T16:55:27Z","conclusion":"success","databaseId":101104130053,"name":"Test JavaScript (bun on ubuntu-latest)","startedAt":"2026-09-04T16:54:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:23Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:22Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:23Z","status":"completed"},{"completedAt":"2026-09-04T16:54:27Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:28Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T16:54:27Z","status":"completed"},{"completedAt":"2026-09-04T16:54:35Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T16:54:28Z","status":"completed"},{"completedAt":"2026-09-04T16:54:35Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T16:54:35Z","status":"completed"},{"completedAt":"2026-09-04T16:54:35Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T16:54:35Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T16:54:35Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T16:54:36Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T16:55:24Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T16:55:24Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T16:55:24Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T16:55:24Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T16:55:24Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T16:55:24Z","status":"completed"},{"completedAt":"2026-09-04T16:55:24Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T16:55:24Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130053"},{"completedAt":"2026-09-04T16:54:38Z","conclusion":"success","databaseId":101104130103,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T16:54:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:23Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:22Z","status":"completed"},{"completedAt":"2026-09-04T16:54:24Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:23Z","status":"completed"},{"completedAt":"2026-09-04T16:54:24Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:24Z","status":"completed"},{"completedAt":"2026-09-04T16:54:24Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T16:54:24Z","status":"completed"},{"completedAt":"2026-09-04T16:54:30Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T16:54:24Z","status":"completed"},{"completedAt":"2026-09-04T16:54:30Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T16:54:30Z","status":"completed"},{"completedAt":"2026-09-04T16:54:30Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T16:54:30Z","status":"completed"},{"completedAt":"2026-09-04T16:54:30Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T16:54:30Z","status":"completed"},{"completedAt":"2026-09-04T16:54:30Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T16:54:30Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T16:54:30Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T16:54:36Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T16:54:36Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T16:54:36Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T16:54:36Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130103"},{"completedAt":"2026-09-04T16:55:45Z","conclusion":"success","databaseId":101104130171,"name":"Test JavaScript (bun on windows-latest)","startedAt":"2026-09-04T16:54:23Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:27Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:33Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:27Z","status":"completed"},{"completedAt":"2026-09-04T16:54:44Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:33Z","status":"completed"},{"completedAt":"2026-09-04T16:54:48Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T16:54:44Z","status":"completed"},{"completedAt":"2026-09-04T16:54:48Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T16:54:48Z","status":"completed"},{"completedAt":"2026-09-04T16:54:48Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T16:54:48Z","status":"completed"},{"completedAt":"2026-09-04T16:54:54Z","conclusion":"success","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T16:54:48Z","status":"completed"},{"completedAt":"2026-09-04T16:55:04Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T16:54:54Z","status":"completed"},{"completedAt":"2026-09-04T16:55:41Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T16:55:04Z","status":"completed"},{"completedAt":"2026-09-04T16:55:41Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T16:55:41Z","status":"completed"},{"completedAt":"2026-09-04T16:55:41Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T16:55:41Z","status":"completed"},{"completedAt":"2026-09-04T16:55:41Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T16:55:41Z","status":"completed"},{"completedAt":"2026-09-04T16:55:41Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T16:55:41Z","status":"completed"},{"completedAt":"2026-09-04T16:55:41Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T16:55:41Z","status":"completed"},{"completedAt":"2026-09-04T16:55:43Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T16:55:41Z","status":"completed"},{"completedAt":"2026-09-04T16:55:43Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T16:55:43Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130171"},{"completedAt":"2026-09-04T16:55:55Z","conclusion":"success","databaseId":101104130184,"name":"Test JavaScript (bun on macos-latest)","startedAt":"2026-09-04T16:54:26Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:27Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:26Z","status":"completed"},{"completedAt":"2026-09-04T16:54:30Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:27Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:30Z","status":"completed"},{"completedAt":"2026-09-04T16:54:32Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:32Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T16:54:32Z","status":"completed"},{"completedAt":"2026-09-04T16:54:34Z","conclusion":"success","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T16:54:32Z","status":"completed"},{"completedAt":"2026-09-04T16:54:34Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T16:54:34Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T16:54:34Z","status":"completed"},{"completedAt":"2026-09-04T16:55:50Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T16:54:36Z","status":"completed"},{"completedAt":"2026-09-04T16:55:50Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T16:55:50Z","status":"completed"},{"completedAt":"2026-09-04T16:55:50Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T16:55:50Z","status":"completed"},{"completedAt":"2026-09-04T16:55:50Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T16:55:50Z","status":"completed"},{"completedAt":"2026-09-04T16:55:50Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T16:55:50Z","status":"completed"},{"completedAt":"2026-09-04T16:55:51Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T16:55:50Z","status":"completed"},{"completedAt":"2026-09-04T16:55:51Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T16:55:51Z","status":"completed"},{"completedAt":"2026-09-04T16:55:53Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T16:55:51Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130184"},{"completedAt":"2026-09-04T16:54:47Z","conclusion":"success","databaseId":101104130195,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T16:54:22Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:54:24Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:54:23Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:54:24Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:25Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T16:54:25Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:31Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:36Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T16:54:31Z","status":"completed"},{"completedAt":"2026-09-04T16:54:42Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T16:54:36Z","status":"completed"},{"completedAt":"2026-09-04T16:54:43Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T16:54:42Z","status":"completed"},{"completedAt":"2026-09-04T16:54:43Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T16:54:43Z","status":"completed"},{"completedAt":"2026-09-04T16:54:44Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T16:54:43Z","status":"completed"},{"completedAt":"2026-09-04T16:54:44Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T16:54:44Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130195"},{"completedAt":"2026-09-04T16:54:19Z","conclusion":"skipped","databaseId":101104130711,"name":"Create JavaScript changeset PR","startedAt":"2026-09-04T16:54:19Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104130711"},{"completedAt":"2026-09-04T16:54:19Z","conclusion":"skipped","databaseId":101104156006,"name":"Instant JavaScript release","startedAt":"2026-09-04T16:54:24Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104156006"},{"completedAt":"2026-09-04T16:54:19Z","conclusion":"skipped","databaseId":101104160882,"name":"Check for JavaScript changesets","startedAt":"2026-09-04T16:54:25Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104160882"},{"completedAt":"2026-09-04T16:57:00Z","conclusion":"failure","databaseId":101104595745,"name":"Release JavaScript package","startedAt":"2026-09-04T16:55:57Z","status":"completed","steps":[{"completedAt":"2026-09-04T16:55:59Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T16:55:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:01Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T16:55:59Z","status":"completed"},{"completedAt":"2026-09-04T16:56:01Z","conclusion":"success","name":"Setup Node.js","number":3,"startedAt":"2026-09-04T16:56:01Z","status":"completed"},{"completedAt":"2026-09-04T16:56:03Z","conclusion":"success","name":"Setup Bun","number":4,"startedAt":"2026-09-04T16:56:01Z","status":"completed"},{"completedAt":"2026-09-04T16:56:04Z","conclusion":"success","name":"Install dependencies","number":5,"startedAt":"2026-09-04T16:56:03Z","status":"completed"},{"completedAt":"2026-09-04T16:56:09Z","conclusion":"success","name":"Update npm for OIDC trusted publishing","number":6,"startedAt":"2026-09-04T16:56:04Z","status":"completed"},{"completedAt":"2026-09-04T16:56:09Z","conclusion":"success","name":"Check for changesets","number":7,"startedAt":"2026-09-04T16:56:09Z","status":"completed"},{"completedAt":"2026-09-04T16:56:10Z","conclusion":"success","name":"Check if release is needed","number":8,"startedAt":"2026-09-04T16:56:09Z","status":"completed"},{"completedAt":"2026-09-04T16:56:10Z","conclusion":"skipped","name":"Merge multiple changesets","number":9,"startedAt":"2026-09-04T16:56:10Z","status":"completed"},{"completedAt":"2026-09-04T16:56:15Z","conclusion":"success","name":"Version package and commit to main","number":10,"startedAt":"2026-09-04T16:56:10Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"failure","name":"Publish to npm","number":11,"startedAt":"2026-09-04T16:56:15Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"skipped","name":"Create JavaScript GitHub Release","number":12,"startedAt":"2026-09-04T16:56:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"skipped","name":"Format JavaScript GitHub release notes","number":13,"startedAt":"2026-09-04T16:56:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"skipped","name":"Verify npm availability","number":14,"startedAt":"2026-09-04T16:56:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"skipped","name":"Post Setup Bun","number":26,"startedAt":"2026-09-04T16:56:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"skipped","name":"Post Setup Node.js","number":27,"startedAt":"2026-09-04T16:56:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":28,"startedAt":"2026-09-04T16:56:58Z","status":"completed"},{"completedAt":"2026-09-04T16:56:58Z","conclusion":"success","name":"Complete job","number":29,"startedAt":"2026-09-04T16:56:58Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33897699209/job/101104595745"}],"workflowName":"JavaScript checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/run-33910180248.json b/dev/log/issues/199/pulls/200/api/run-33910180248.json new file mode 100644 index 00000000..cb5e4dad --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33910180248.json @@ -0,0 +1 @@ +{"conclusion":"failure","createdAt":"2026-09-04T19:15:05Z","databaseId":33910180248,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"e6a3eef7a59135824f249871756fae5c9aac872e","jobs":[{"completedAt":"2026-09-04T19:15:18Z","conclusion":"success","databaseId":101144503760,"name":"Check for JavaScript changesets","startedAt":"2026-09-04T19:15:08Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:10Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:09Z","status":"completed"},{"completedAt":"2026-09-04T19:15:12Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:10Z","status":"completed"},{"completedAt":"2026-09-04T19:15:13Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:12Z","status":"completed"},{"completedAt":"2026-09-04T19:15:14Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-09-04T19:15:13Z","status":"completed"},{"completedAt":"2026-09-04T19:15:14Z","conclusion":"success","name":"Check for changesets","number":5,"startedAt":"2026-09-04T19:15:14Z","status":"completed"},{"completedAt":"2026-09-04T19:15:14Z","conclusion":"success","name":"Post Setup Bun","number":9,"startedAt":"2026-09-04T19:15:14Z","status":"completed"},{"completedAt":"2026-09-04T19:15:15Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":10,"startedAt":"2026-09-04T19:15:14Z","status":"completed"},{"completedAt":"2026-09-04T19:15:15Z","conclusion":"success","name":"Complete job","number":11,"startedAt":"2026-09-04T19:15:15Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144503760"},{"completedAt":"2026-09-04T19:15:06Z","conclusion":"skipped","databaseId":101144505393,"name":"Create JavaScript changeset PR","startedAt":"2026-09-04T19:15:06Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144505393"},{"completedAt":"2026-09-04T19:15:06Z","conclusion":"skipped","databaseId":101144505539,"name":"Instant JavaScript release","startedAt":"2026-09-04T19:15:06Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144505539"},{"completedAt":"2026-09-04T19:15:40Z","conclusion":"success","databaseId":101144565888,"name":"Lint and format JavaScript","startedAt":"2026-09-04T19:15:20Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:22Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:21Z","status":"completed"},{"completedAt":"2026-09-04T19:15:23Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:22Z","status":"completed"},{"completedAt":"2026-09-04T19:15:26Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:27Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-09-04T19:15:26Z","status":"completed"},{"completedAt":"2026-09-04T19:15:33Z","conclusion":"success","name":"Run ESLint","number":5,"startedAt":"2026-09-04T19:15:27Z","status":"completed"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","name":"Check formatting","number":6,"startedAt":"2026-09-04T19:15:33Z","status":"completed"},{"completedAt":"2026-09-04T19:15:38Z","conclusion":"success","name":"Check code duplication","number":7,"startedAt":"2026-09-04T19:15:37Z","status":"completed"},{"completedAt":"2026-09-04T19:15:38Z","conclusion":"success","name":"Post Setup Bun","number":13,"startedAt":"2026-09-04T19:15:38Z","status":"completed"},{"completedAt":"2026-09-04T19:15:38Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":14,"startedAt":"2026-09-04T19:15:38Z","status":"completed"},{"completedAt":"2026-09-04T19:15:38Z","conclusion":"success","name":"Complete job","number":15,"startedAt":"2026-09-04T19:15:38Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144565888"},{"completedAt":"2026-09-04T19:15:40Z","conclusion":"success","databaseId":101144565938,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T19:15:20Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:23Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:21Z","status":"completed"},{"completedAt":"2026-09-04T19:15:24Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:24Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:24Z","status":"completed"},{"completedAt":"2026-09-04T19:15:24Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:15:24Z","status":"completed"},{"completedAt":"2026-09-04T19:15:30Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:15:24Z","status":"completed"},{"completedAt":"2026-09-04T19:15:30Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:15:30Z","status":"completed"},{"completedAt":"2026-09-04T19:15:30Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:15:30Z","status":"completed"},{"completedAt":"2026-09-04T19:15:30Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:15:30Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:15:30Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:15:37Z","status":"completed"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T19:15:37Z","status":"completed"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:15:37Z","status":"completed"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:15:37Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144565938"},{"completedAt":"2026-09-04T19:16:54Z","conclusion":"failure","databaseId":101144565940,"name":"Test JavaScript (bun on macos-latest)","startedAt":"2026-09-04T19:15:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:23Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:22Z","status":"completed"},{"completedAt":"2026-09-04T19:15:25Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:27Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:25Z","status":"completed"},{"completedAt":"2026-09-04T19:15:28Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:15:27Z","status":"completed"},{"completedAt":"2026-09-04T19:15:28Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:15:28Z","status":"completed"},{"completedAt":"2026-09-04T19:15:30Z","conclusion":"success","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:15:28Z","status":"completed"},{"completedAt":"2026-09-04T19:15:30Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:15:30Z","status":"completed"},{"completedAt":"2026-09-04T19:15:32Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:15:30Z","status":"completed"},{"completedAt":"2026-09-04T19:16:50Z","conclusion":"failure","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:15:32Z","status":"completed"},{"completedAt":"2026-09-04T19:16:50Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:16:50Z","status":"completed"},{"completedAt":"2026-09-04T19:16:50Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:16:50Z","status":"completed"},{"completedAt":"2026-09-04T19:16:50Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:16:50Z","status":"completed"},{"completedAt":"2026-09-04T19:16:50Z","conclusion":"skipped","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T19:16:50Z","status":"completed"},{"completedAt":"2026-09-04T19:16:50Z","conclusion":"skipped","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T19:16:50Z","status":"completed"},{"completedAt":"2026-09-04T19:16:51Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:16:50Z","status":"completed"},{"completedAt":"2026-09-04T19:16:52Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:16:51Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144565940"},{"completedAt":"2026-09-04T19:15:45Z","conclusion":"success","databaseId":101144565968,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T19:15:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:24Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:22Z","status":"completed"},{"completedAt":"2026-09-04T19:15:25Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:24Z","status":"completed"},{"completedAt":"2026-09-04T19:15:25Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:25Z","status":"completed"},{"completedAt":"2026-09-04T19:15:25Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:15:25Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:15:25Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:36Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:41Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:15:36Z","status":"completed"},{"completedAt":"2026-09-04T19:15:42Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:15:41Z","status":"completed"},{"completedAt":"2026-09-04T19:15:42Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T19:15:42Z","status":"completed"},{"completedAt":"2026-09-04T19:15:42Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:15:42Z","status":"completed"},{"completedAt":"2026-09-04T19:15:42Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:15:42Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144565968"},{"completedAt":"2026-09-04T19:16:19Z","conclusion":"failure","databaseId":101144566024,"name":"Test JavaScript (bun on ubuntu-latest)","startedAt":"2026-09-04T19:15:20Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:22Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:21Z","status":"completed"},{"completedAt":"2026-09-04T19:15:23Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:22Z","status":"completed"},{"completedAt":"2026-09-04T19:15:24Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:25Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:15:24Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:15:25Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:15:31Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"failure","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:15:31Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"skipped","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"skipped","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:16:18Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144566024"},{"completedAt":"2026-09-04T19:15:37Z","conclusion":"success","databaseId":101144566053,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T19:15:20Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:22Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:21Z","status":"completed"},{"completedAt":"2026-09-04T19:15:23Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:22Z","status":"completed"},{"completedAt":"2026-09-04T19:15:23Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:23Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:29Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:15:23Z","status":"completed"},{"completedAt":"2026-09-04T19:15:29Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:15:29Z","status":"completed"},{"completedAt":"2026-09-04T19:15:29Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:15:29Z","status":"completed"},{"completedAt":"2026-09-04T19:15:29Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:15:29Z","status":"completed"},{"completedAt":"2026-09-04T19:15:29Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:15:29Z","status":"completed"},{"completedAt":"2026-09-04T19:15:29Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:15:29Z","status":"completed"},{"completedAt":"2026-09-04T19:15:35Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:15:29Z","status":"completed"},{"completedAt":"2026-09-04T19:15:35Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:15:35Z","status":"completed"},{"completedAt":"2026-09-04T19:15:35Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T19:15:35Z","status":"completed"},{"completedAt":"2026-09-04T19:15:35Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:15:35Z","status":"completed"},{"completedAt":"2026-09-04T19:15:35Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:15:35Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144566053"},{"completedAt":"2026-09-04T19:17:26Z","conclusion":"failure","databaseId":101144566072,"name":"Test JavaScript (bun on windows-latest)","startedAt":"2026-09-04T19:15:21Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:24Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:22Z","status":"completed"},{"completedAt":"2026-09-04T19:15:35Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:24Z","status":"completed"},{"completedAt":"2026-09-04T19:15:38Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:15:35Z","status":"completed"},{"completedAt":"2026-09-04T19:15:54Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:15:38Z","status":"completed"},{"completedAt":"2026-09-04T19:15:54Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:15:54Z","status":"completed"},{"completedAt":"2026-09-04T19:15:54Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:15:54Z","status":"completed"},{"completedAt":"2026-09-04T19:16:06Z","conclusion":"success","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:15:54Z","status":"completed"},{"completedAt":"2026-09-04T19:16:17Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:16:06Z","status":"completed"},{"completedAt":"2026-09-04T19:17:21Z","conclusion":"failure","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:16:17Z","status":"completed"},{"completedAt":"2026-09-04T19:17:21Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:17:21Z","status":"completed"},{"completedAt":"2026-09-04T19:17:21Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:17:21Z","status":"completed"},{"completedAt":"2026-09-04T19:17:21Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:17:21Z","status":"completed"},{"completedAt":"2026-09-04T19:17:21Z","conclusion":"skipped","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T19:17:21Z","status":"completed"},{"completedAt":"2026-09-04T19:17:21Z","conclusion":"skipped","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T19:17:21Z","status":"completed"},{"completedAt":"2026-09-04T19:17:24Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:17:21Z","status":"completed"},{"completedAt":"2026-09-04T19:17:24Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:17:24Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101144566072"},{"completedAt":"2026-09-04T19:17:26Z","conclusion":"skipped","databaseId":101145176653,"name":"Release JavaScript package","startedAt":"2026-09-04T19:17:27Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180248/job/101145176653"}],"workflowName":"JavaScript checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/run-33910180769.json b/dev/log/issues/199/pulls/200/api/run-33910180769.json new file mode 100644 index 00000000..8131e814 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33910180769.json @@ -0,0 +1 @@ +{"conclusion":"failure","createdAt":"2026-09-04T19:15:06Z","databaseId":33910180769,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"e6a3eef7a59135824f249871756fae5c9aac872e","jobs":[{"completedAt":"2026-09-04T19:15:58Z","conclusion":"success","databaseId":101144506278,"name":"Rust changelog fragment check","startedAt":"2026-09-04T19:15:09Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:15:10Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:15:10Z","status":"completed"},{"completedAt":"2026-09-04T19:15:12Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:15:10Z","status":"completed"},{"completedAt":"2026-09-04T19:15:21Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:15:12Z","status":"completed"},{"completedAt":"2026-09-04T19:15:50Z","conclusion":"success","name":"Install rust-script","number":4,"startedAt":"2026-09-04T19:15:21Z","status":"completed"},{"completedAt":"2026-09-04T19:15:56Z","conclusion":"success","name":"Check for changelog fragments","number":5,"startedAt":"2026-09-04T19:15:50Z","status":"completed"},{"completedAt":"2026-09-04T19:15:56Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":10,"startedAt":"2026-09-04T19:15:56Z","status":"completed"},{"completedAt":"2026-09-04T19:15:56Z","conclusion":"success","name":"Complete job","number":11,"startedAt":"2026-09-04T19:15:56Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144506278"},{"completedAt":"2026-09-04T19:15:07Z","conclusion":"skipped","databaseId":101144509597,"name":"Create Rust changelog PR","startedAt":"2026-09-04T19:15:07Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144509597"},{"completedAt":"2026-09-04T19:15:07Z","conclusion":"skipped","databaseId":101144509650,"name":"Instant Rust release","startedAt":"2026-09-04T19:15:07Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144509650"},{"completedAt":"2026-09-04T19:16:33Z","conclusion":"success","databaseId":101144761375,"name":"Lint and format Rust","startedAt":"2026-09-04T19:16:01Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:16:03Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:16:02Z","status":"completed"},{"completedAt":"2026-09-04T19:16:04Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:16:03Z","status":"completed"},{"completedAt":"2026-09-04T19:16:16Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:16:04Z","status":"completed"},{"completedAt":"2026-09-04T19:16:19Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:16:16Z","status":"completed"},{"completedAt":"2026-09-04T19:16:19Z","conclusion":"success","name":"Check formatting","number":5,"startedAt":"2026-09-04T19:16:19Z","status":"completed"},{"completedAt":"2026-09-04T19:16:27Z","conclusion":"success","name":"Run Clippy","number":6,"startedAt":"2026-09-04T19:16:19Z","status":"completed"},{"completedAt":"2026-09-04T19:16:31Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:16:27Z","status":"completed"},{"completedAt":"2026-09-04T19:16:31Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:16:31Z","status":"completed"},{"completedAt":"2026-09-04T19:16:31Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:16:31Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144761375"},{"completedAt":"2026-09-04T19:17:05Z","conclusion":"success","databaseId":101144761406,"name":"Test Rust release scripts","startedAt":"2026-09-04T19:16:00Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:16:02Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:16:01Z","status":"completed"},{"completedAt":"2026-09-04T19:16:03Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:16:02Z","status":"completed"},{"completedAt":"2026-09-04T19:16:13Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:16:03Z","status":"completed"},{"completedAt":"2026-09-04T19:16:14Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:16:13Z","status":"completed"},{"completedAt":"2026-09-04T19:16:44Z","conclusion":"success","name":"Install rust-script","number":5,"startedAt":"2026-09-04T19:16:14Z","status":"completed"},{"completedAt":"2026-09-04T19:17:02Z","conclusion":"success","name":"Run release script unit tests","number":6,"startedAt":"2026-09-04T19:16:44Z","status":"completed"},{"completedAt":"2026-09-04T19:17:04Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:17:02Z","status":"completed"},{"completedAt":"2026-09-04T19:17:04Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:17:04Z","status":"completed"},{"completedAt":"2026-09-04T19:17:04Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:17:04Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144761406"},{"completedAt":"2026-09-04T19:16:31Z","conclusion":"failure","databaseId":101144761410,"name":"Test Rust (macos-latest)","startedAt":"2026-09-04T19:16:04Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:16:06Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:16:05Z","status":"completed"},{"completedAt":"2026-09-04T19:16:08Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:16:06Z","status":"completed"},{"completedAt":"2026-09-04T19:16:15Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:16:08Z","status":"completed"},{"completedAt":"2026-09-04T19:16:19Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:16:15Z","status":"completed"},{"completedAt":"2026-09-04T19:16:29Z","conclusion":"failure","name":"Run tests","number":5,"startedAt":"2026-09-04T19:16:19Z","status":"completed"},{"completedAt":"2026-09-04T19:16:29Z","conclusion":"skipped","name":"Run doc tests","number":6,"startedAt":"2026-09-04T19:16:29Z","status":"completed"},{"completedAt":"2026-09-04T19:16:29Z","conclusion":"skipped","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:16:29Z","status":"completed"},{"completedAt":"2026-09-04T19:16:30Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:16:29Z","status":"completed"},{"completedAt":"2026-09-04T19:16:30Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:16:30Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144761410"},{"completedAt":"2026-09-04T19:16:49Z","conclusion":"success","databaseId":101144761422,"name":"Test Rust (ubuntu-latest)","startedAt":"2026-09-04T19:16:01Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:16:03Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:16:01Z","status":"completed"},{"completedAt":"2026-09-04T19:16:04Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:16:03Z","status":"completed"},{"completedAt":"2026-09-04T19:16:14Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:16:04Z","status":"completed"},{"completedAt":"2026-09-04T19:16:18Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:16:14Z","status":"completed"},{"completedAt":"2026-09-04T19:16:42Z","conclusion":"success","name":"Run tests","number":5,"startedAt":"2026-09-04T19:16:18Z","status":"completed"},{"completedAt":"2026-09-04T19:16:43Z","conclusion":"success","name":"Run doc tests","number":6,"startedAt":"2026-09-04T19:16:42Z","status":"completed"},{"completedAt":"2026-09-04T19:16:47Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:16:43Z","status":"completed"},{"completedAt":"2026-09-04T19:16:47Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:16:47Z","status":"completed"},{"completedAt":"2026-09-04T19:16:47Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:16:47Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144761422"},{"completedAt":"2026-09-04T19:17:08Z","conclusion":"failure","databaseId":101144761454,"name":"Test Rust (windows-latest)","startedAt":"2026-09-04T19:16:00Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:16:02Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:16:01Z","status":"completed"},{"completedAt":"2026-09-04T19:16:09Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:16:02Z","status":"completed"},{"completedAt":"2026-09-04T19:16:31Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:16:09Z","status":"completed"},{"completedAt":"2026-09-04T19:16:46Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:16:31Z","status":"completed"},{"completedAt":"2026-09-04T19:17:03Z","conclusion":"failure","name":"Run tests","number":5,"startedAt":"2026-09-04T19:16:46Z","status":"completed"},{"completedAt":"2026-09-04T19:17:03Z","conclusion":"skipped","name":"Run doc tests","number":6,"startedAt":"2026-09-04T19:17:03Z","status":"completed"},{"completedAt":"2026-09-04T19:17:03Z","conclusion":"skipped","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:17:03Z","status":"completed"},{"completedAt":"2026-09-04T19:17:06Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:17:03Z","status":"completed"},{"completedAt":"2026-09-04T19:17:06Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:17:06Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101144761454"},{"completedAt":"2026-09-04T19:17:08Z","conclusion":"skipped","databaseId":101145091314,"name":"Build Rust package","startedAt":"2026-09-04T19:17:08Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101145091314"},{"completedAt":"2026-09-04T19:17:08Z","conclusion":"skipped","databaseId":101145092034,"name":"Release Rust crate","startedAt":"2026-09-04T19:17:09Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33910180769/job/101145092034"}],"workflowName":"Rust checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/run-33911660942.json b/dev/log/issues/199/pulls/200/api/run-33911660942.json new file mode 100644 index 00000000..115351d3 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33911660942.json @@ -0,0 +1 @@ +{"conclusion":"failure","createdAt":"2026-09-04T19:32:11Z","databaseId":33911660942,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"f9bf3decd6e9f52f5928777050e97439adc03b10","jobs":[{"completedAt":"2026-09-04T19:33:16Z","conclusion":"success","databaseId":101149244149,"name":"Rust changelog fragment check","startedAt":"2026-09-04T19:32:15Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:17Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:16Z","status":"completed"},{"completedAt":"2026-09-04T19:32:19Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:17Z","status":"completed"},{"completedAt":"2026-09-04T19:32:30Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:32:19Z","status":"completed"},{"completedAt":"2026-09-04T19:33:05Z","conclusion":"success","name":"Install rust-script","number":4,"startedAt":"2026-09-04T19:32:30Z","status":"completed"},{"completedAt":"2026-09-04T19:33:14Z","conclusion":"success","name":"Check for changelog fragments","number":5,"startedAt":"2026-09-04T19:33:05Z","status":"completed"},{"completedAt":"2026-09-04T19:33:14Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":10,"startedAt":"2026-09-04T19:33:14Z","status":"completed"},{"completedAt":"2026-09-04T19:33:14Z","conclusion":"success","name":"Complete job","number":11,"startedAt":"2026-09-04T19:33:14Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149244149"},{"completedAt":"2026-09-04T19:32:12Z","conclusion":"skipped","databaseId":101149245347,"name":"Instant Rust release","startedAt":"2026-09-04T19:32:12Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149245347"},{"completedAt":"2026-09-04T19:32:12Z","conclusion":"skipped","databaseId":101149245733,"name":"Create Rust changelog PR","startedAt":"2026-09-04T19:32:12Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149245733"},{"completedAt":"2026-09-04T19:33:57Z","conclusion":"failure","databaseId":101149531858,"name":"Test Rust (macos-latest)","startedAt":"2026-09-04T19:33:23Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:33:25Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:33:24Z","status":"completed"},{"completedAt":"2026-09-04T19:33:27Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:33:25Z","status":"completed"},{"completedAt":"2026-09-04T19:33:35Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:33:27Z","status":"completed"},{"completedAt":"2026-09-04T19:33:41Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:33:35Z","status":"completed"},{"completedAt":"2026-09-04T19:33:53Z","conclusion":"failure","name":"Run tests","number":5,"startedAt":"2026-09-04T19:33:41Z","status":"completed"},{"completedAt":"2026-09-04T19:33:53Z","conclusion":"skipped","name":"Run doc tests","number":6,"startedAt":"2026-09-04T19:33:53Z","status":"completed"},{"completedAt":"2026-09-04T19:33:53Z","conclusion":"skipped","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:33:53Z","status":"completed"},{"completedAt":"2026-09-04T19:33:54Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:33:53Z","status":"completed"},{"completedAt":"2026-09-04T19:33:54Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:33:54Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149531858"},{"completedAt":"2026-09-04T19:34:42Z","conclusion":"success","databaseId":101149531866,"name":"Test Rust release scripts","startedAt":"2026-09-04T19:33:19Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:33:21Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:33:19Z","status":"completed"},{"completedAt":"2026-09-04T19:33:22Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:33:21Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:33:22Z","status":"completed"},{"completedAt":"2026-09-04T19:33:35Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:34:11Z","conclusion":"success","name":"Install rust-script","number":5,"startedAt":"2026-09-04T19:33:35Z","status":"completed"},{"completedAt":"2026-09-04T19:34:39Z","conclusion":"success","name":"Run release script unit tests","number":6,"startedAt":"2026-09-04T19:34:11Z","status":"completed"},{"completedAt":"2026-09-04T19:34:39Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:34:39Z","status":"completed"},{"completedAt":"2026-09-04T19:34:40Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:34:39Z","status":"completed"},{"completedAt":"2026-09-04T19:34:40Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:34:40Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149531866"},{"completedAt":"2026-09-04T19:34:55Z","conclusion":"success","databaseId":101149531868,"name":"Test Rust (windows-latest)","startedAt":"2026-09-04T19:33:18Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:33:20Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:33:19Z","status":"completed"},{"completedAt":"2026-09-04T19:33:25Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:33:20Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:33:25Z","status":"completed"},{"completedAt":"2026-09-04T19:34:02Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:34:37Z","conclusion":"success","name":"Run tests","number":5,"startedAt":"2026-09-04T19:34:02Z","status":"completed"},{"completedAt":"2026-09-04T19:34:43Z","conclusion":"success","name":"Run doc tests","number":6,"startedAt":"2026-09-04T19:34:37Z","status":"completed"},{"completedAt":"2026-09-04T19:34:51Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:34:43Z","status":"completed"},{"completedAt":"2026-09-04T19:34:54Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:34:51Z","status":"completed"},{"completedAt":"2026-09-04T19:34:54Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:34:54Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149531868"},{"completedAt":"2026-09-04T19:34:00Z","conclusion":"success","databaseId":101149531908,"name":"Test Rust (ubuntu-latest)","startedAt":"2026-09-04T19:33:18Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:33:20Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:33:19Z","status":"completed"},{"completedAt":"2026-09-04T19:33:21Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:33:20Z","status":"completed"},{"completedAt":"2026-09-04T19:33:31Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:33:21Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:33:31Z","status":"completed"},{"completedAt":"2026-09-04T19:33:57Z","conclusion":"success","name":"Run tests","number":5,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:59Z","conclusion":"success","name":"Run doc tests","number":6,"startedAt":"2026-09-04T19:33:57Z","status":"completed"},{"completedAt":"2026-09-04T19:33:59Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:33:59Z","status":"completed"},{"completedAt":"2026-09-04T19:33:59Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:33:59Z","status":"completed"},{"completedAt":"2026-09-04T19:33:59Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:33:59Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149531908"},{"completedAt":"2026-09-04T19:33:36Z","conclusion":"success","databaseId":101149531924,"name":"Lint and format Rust","startedAt":"2026-09-04T19:33:18Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:33:20Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:33:19Z","status":"completed"},{"completedAt":"2026-09-04T19:33:21Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:33:20Z","status":"completed"},{"completedAt":"2026-09-04T19:33:30Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T19:33:21Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T19:33:30Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"success","name":"Check formatting","number":5,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:34Z","conclusion":"success","name":"Run Clippy","number":6,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:34Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T19:33:34Z","status":"completed"},{"completedAt":"2026-09-04T19:33:35Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T19:33:34Z","status":"completed"},{"completedAt":"2026-09-04T19:33:35Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T19:33:35Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149531924"},{"completedAt":"2026-09-04T19:34:55Z","conclusion":"skipped","databaseId":101149978503,"name":"Build Rust package","startedAt":"2026-09-04T19:34:55Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149978503"},{"completedAt":"2026-09-04T19:34:55Z","conclusion":"skipped","databaseId":101149978626,"name":"Release Rust crate","startedAt":"2026-09-04T19:34:55Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660942/job/101149978626"}],"workflowName":"Rust checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/run-33911660947.json b/dev/log/issues/199/pulls/200/api/run-33911660947.json new file mode 100644 index 00000000..24066f84 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33911660947.json @@ -0,0 +1 @@ +{"conclusion":"failure","createdAt":"2026-09-04T19:32:11Z","databaseId":33911660947,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"f9bf3decd6e9f52f5928777050e97439adc03b10","jobs":[{"completedAt":"2026-09-04T19:32:21Z","conclusion":"success","databaseId":101149244065,"name":"Check for JavaScript changesets","startedAt":"2026-09-04T19:32:13Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:15Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:14Z","status":"completed"},{"completedAt":"2026-09-04T19:32:16Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:15Z","status":"completed"},{"completedAt":"2026-09-04T19:32:18Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:16Z","status":"completed"},{"completedAt":"2026-09-04T19:32:19Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-09-04T19:32:18Z","status":"completed"},{"completedAt":"2026-09-04T19:32:19Z","conclusion":"success","name":"Check for changesets","number":5,"startedAt":"2026-09-04T19:32:19Z","status":"completed"},{"completedAt":"2026-09-04T19:32:19Z","conclusion":"success","name":"Post Setup Bun","number":9,"startedAt":"2026-09-04T19:32:19Z","status":"completed"},{"completedAt":"2026-09-04T19:32:19Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":10,"startedAt":"2026-09-04T19:32:19Z","status":"completed"},{"completedAt":"2026-09-04T19:32:19Z","conclusion":"success","name":"Complete job","number":11,"startedAt":"2026-09-04T19:32:19Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149244065"},{"completedAt":"2026-09-04T19:32:12Z","conclusion":"skipped","databaseId":101149245348,"name":"Instant JavaScript release","startedAt":"2026-09-04T19:32:12Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149245348"},{"completedAt":"2026-09-04T19:32:12Z","conclusion":"skipped","databaseId":101149245522,"name":"Create JavaScript changeset PR","startedAt":"2026-09-04T19:32:12Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149245522"},{"completedAt":"2026-09-04T19:32:43Z","conclusion":"success","databaseId":101149289076,"name":"Lint and format JavaScript","startedAt":"2026-09-04T19:32:24Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:26Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:25Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:29Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:30Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-09-04T19:32:29Z","status":"completed"},{"completedAt":"2026-09-04T19:32:37Z","conclusion":"success","name":"Run ESLint","number":5,"startedAt":"2026-09-04T19:32:30Z","status":"completed"},{"completedAt":"2026-09-04T19:32:41Z","conclusion":"success","name":"Check formatting","number":6,"startedAt":"2026-09-04T19:32:37Z","status":"completed"},{"completedAt":"2026-09-04T19:32:41Z","conclusion":"success","name":"Check code duplication","number":7,"startedAt":"2026-09-04T19:32:41Z","status":"completed"},{"completedAt":"2026-09-04T19:32:41Z","conclusion":"success","name":"Post Setup Bun","number":13,"startedAt":"2026-09-04T19:32:41Z","status":"completed"},{"completedAt":"2026-09-04T19:32:42Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":14,"startedAt":"2026-09-04T19:32:41Z","status":"completed"},{"completedAt":"2026-09-04T19:32:42Z","conclusion":"success","name":"Complete job","number":15,"startedAt":"2026-09-04T19:32:42Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289076"},{"completedAt":"2026-09-04T19:33:52Z","conclusion":"failure","databaseId":101149289153,"name":"Test JavaScript (bun on macos-latest)","startedAt":"2026-09-04T19:32:28Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:30Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:29Z","status":"completed"},{"completedAt":"2026-09-04T19:32:32Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:30Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:32Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:37Z","conclusion":"success","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:37Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:32:37Z","status":"completed"},{"completedAt":"2026-09-04T19:32:39Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:32:37Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"failure","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:32:39Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"skipped","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:33:47Z","conclusion":"skipped","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:33:48Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:33:47Z","status":"completed"},{"completedAt":"2026-09-04T19:33:49Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:33:48Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289153"},{"completedAt":"2026-09-04T19:32:46Z","conclusion":"success","databaseId":101149289155,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T19:32:23Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:26Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:24Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:40Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:44Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:32:40Z","status":"completed"},{"completedAt":"2026-09-04T19:32:44Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:32:44Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T19:32:44Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:32:45Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:32:45Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289155"},{"completedAt":"2026-09-04T19:33:59Z","conclusion":"success","databaseId":101149289192,"name":"Test JavaScript (bun on ubuntu-latest)","startedAt":"2026-09-04T19:33:00Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:33:01Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:33:00Z","status":"completed"},{"completedAt":"2026-09-04T19:33:02Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:33:01Z","status":"completed"},{"completedAt":"2026-09-04T19:33:04Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:33:02Z","status":"completed"},{"completedAt":"2026-09-04T19:33:04Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:33:04Z","status":"completed"},{"completedAt":"2026-09-04T19:33:10Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:33:04Z","status":"completed"},{"completedAt":"2026-09-04T19:33:10Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:33:10Z","status":"completed"},{"completedAt":"2026-09-04T19:33:10Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:33:10Z","status":"completed"},{"completedAt":"2026-09-04T19:33:11Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:33:10Z","status":"completed"},{"completedAt":"2026-09-04T19:33:56Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:33:11Z","status":"completed"},{"completedAt":"2026-09-04T19:33:56Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:33:56Z","status":"completed"},{"completedAt":"2026-09-04T19:33:56Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:33:56Z","status":"completed"},{"completedAt":"2026-09-04T19:33:56Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:33:56Z","status":"completed"},{"completedAt":"2026-09-04T19:33:57Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T19:33:56Z","status":"completed"},{"completedAt":"2026-09-04T19:33:57Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T19:33:57Z","status":"completed"},{"completedAt":"2026-09-04T19:33:57Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:33:57Z","status":"completed"},{"completedAt":"2026-09-04T19:33:57Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:33:57Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289192"},{"completedAt":"2026-09-04T19:32:46Z","conclusion":"success","databaseId":101149289225,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T19:32:24Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:26Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:25Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:28Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:36Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:32:28Z","status":"completed"},{"completedAt":"2026-09-04T19:32:36Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:32:36Z","status":"completed"},{"completedAt":"2026-09-04T19:32:36Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:32:36Z","status":"completed"},{"completedAt":"2026-09-04T19:32:36Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:32:36Z","status":"completed"},{"completedAt":"2026-09-04T19:32:36Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:32:36Z","status":"completed"},{"completedAt":"2026-09-04T19:32:37Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:32:36Z","status":"completed"},{"completedAt":"2026-09-04T19:32:42Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:32:37Z","status":"completed"},{"completedAt":"2026-09-04T19:32:44Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:32:42Z","status":"completed"},{"completedAt":"2026-09-04T19:32:44Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T19:32:44Z","status":"completed"},{"completedAt":"2026-09-04T19:32:44Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:32:44Z","status":"completed"},{"completedAt":"2026-09-04T19:32:44Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:32:44Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289225"},{"completedAt":"2026-09-04T19:33:38Z","conclusion":"success","databaseId":101149289236,"name":"Test JavaScript (bun on windows-latest)","startedAt":"2026-09-04T19:32:23Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:26Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:25Z","status":"completed"},{"completedAt":"2026-09-04T19:32:31Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:34Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:31Z","status":"completed"},{"completedAt":"2026-09-04T19:32:40Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:32:34Z","status":"completed"},{"completedAt":"2026-09-04T19:32:40Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:32:40Z","status":"completed"},{"completedAt":"2026-09-04T19:32:40Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:32:40Z","status":"completed"},{"completedAt":"2026-09-04T19:32:46Z","conclusion":"success","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:32:40Z","status":"completed"},{"completedAt":"2026-09-04T19:32:56Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:32:46Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:32:56Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:33Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:34Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T19:33:33Z","status":"completed"},{"completedAt":"2026-09-04T19:33:34Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T19:33:34Z","status":"completed"},{"completedAt":"2026-09-04T19:33:36Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:33:34Z","status":"completed"},{"completedAt":"2026-09-04T19:33:36Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:33:36Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289236"},{"completedAt":"2026-09-04T19:32:47Z","conclusion":"success","databaseId":101149289294,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T19:32:24Z","status":"completed","steps":[{"completedAt":"2026-09-04T19:32:25Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T19:32:24Z","status":"completed"},{"completedAt":"2026-09-04T19:32:26Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T19:32:25Z","status":"completed"},{"completedAt":"2026-09-04T19:32:26Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:26Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T19:32:26Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:35Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:36Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T19:32:35Z","status":"completed"},{"completedAt":"2026-09-04T19:32:43Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T19:32:36Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T19:32:43Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T19:32:45Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T19:32:45Z","status":"completed"},{"completedAt":"2026-09-04T19:32:45Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T19:32:45Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149289294"},{"completedAt":"2026-09-04T19:33:59Z","conclusion":"skipped","databaseId":101149726844,"name":"Release JavaScript package","startedAt":"2026-09-04T19:33:59Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33911660947/job/101149726844"}],"workflowName":"JavaScript checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/run-33914574263.json b/dev/log/issues/199/pulls/200/api/run-33914574263.json new file mode 100644 index 00000000..72c34b0a --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33914574263.json @@ -0,0 +1 @@ +{"conclusion":"success","createdAt":"2026-09-04T20:06:37Z","databaseId":33914574263,"event":"push","headBranch":"main","headSha":"000dbeab30fd580f07b4d21a73ac40c55ce2e7fc","jobs":[{"completedAt":"2026-09-04T20:08:00Z","conclusion":"success","databaseId":101158646286,"name":"Test Rust release scripts","startedAt":"2026-09-04T20:06:39Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:41Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:43Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:52Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:07:01Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T20:06:52Z","status":"completed"},{"completedAt":"2026-09-04T20:07:34Z","conclusion":"success","name":"Install rust-script","number":5,"startedAt":"2026-09-04T20:07:01Z","status":"completed"},{"completedAt":"2026-09-04T20:07:54Z","conclusion":"success","name":"Run release script unit tests","number":6,"startedAt":"2026-09-04T20:07:34Z","status":"completed"},{"completedAt":"2026-09-04T20:07:58Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T20:07:54Z","status":"completed"},{"completedAt":"2026-09-04T20:07:58Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T20:07:58Z","status":"completed"},{"completedAt":"2026-09-04T20:07:58Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T20:07:58Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158646286"},{"completedAt":"2026-09-04T20:08:16Z","conclusion":"success","databaseId":101158646616,"name":"Test Rust (windows-latest)","startedAt":"2026-09-04T20:06:40Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:42Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:48Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:42Z","status":"completed"},{"completedAt":"2026-09-04T20:07:09Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:06:48Z","status":"completed"},{"completedAt":"2026-09-04T20:07:24Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T20:07:09Z","status":"completed"},{"completedAt":"2026-09-04T20:07:57Z","conclusion":"success","name":"Run tests","number":5,"startedAt":"2026-09-04T20:07:24Z","status":"completed"},{"completedAt":"2026-09-04T20:08:03Z","conclusion":"success","name":"Run doc tests","number":6,"startedAt":"2026-09-04T20:07:57Z","status":"completed"},{"completedAt":"2026-09-04T20:08:12Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T20:08:03Z","status":"completed"},{"completedAt":"2026-09-04T20:08:14Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T20:08:12Z","status":"completed"},{"completedAt":"2026-09-04T20:08:14Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T20:08:14Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158646616"},{"completedAt":"2026-09-04T20:07:24Z","conclusion":"success","databaseId":101158646711,"name":"Test Rust (ubuntu-latest)","startedAt":"2026-09-04T20:06:39Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:41Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:42Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:51Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:06:42Z","status":"completed"},{"completedAt":"2026-09-04T20:06:53Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T20:06:51Z","status":"completed"},{"completedAt":"2026-09-04T20:07:17Z","conclusion":"success","name":"Run tests","number":5,"startedAt":"2026-09-04T20:06:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:19Z","conclusion":"success","name":"Run doc tests","number":6,"startedAt":"2026-09-04T20:07:17Z","status":"completed"},{"completedAt":"2026-09-04T20:07:23Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T20:07:19Z","status":"completed"},{"completedAt":"2026-09-04T20:07:23Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T20:07:23Z","status":"completed"},{"completedAt":"2026-09-04T20:07:23Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T20:07:23Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158646711"},{"completedAt":"2026-09-04T20:07:14Z","conclusion":"success","databaseId":101158646788,"name":"Lint and format Rust","startedAt":"2026-09-04T20:06:40Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:43Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:45Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:06:45Z","status":"completed"},{"completedAt":"2026-09-04T20:06:59Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T20:06:56Z","status":"completed"},{"completedAt":"2026-09-04T20:06:59Z","conclusion":"success","name":"Check formatting","number":5,"startedAt":"2026-09-04T20:06:59Z","status":"completed"},{"completedAt":"2026-09-04T20:07:07Z","conclusion":"success","name":"Run Clippy","number":6,"startedAt":"2026-09-04T20:06:59Z","status":"completed"},{"completedAt":"2026-09-04T20:07:12Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T20:07:07Z","status":"completed"},{"completedAt":"2026-09-04T20:07:12Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T20:07:12Z","status":"completed"},{"completedAt":"2026-09-04T20:07:12Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T20:07:12Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158646788"},{"completedAt":"2026-09-04T20:07:48Z","conclusion":"success","databaseId":101158646795,"name":"Test Rust (macos-latest)","startedAt":"2026-09-04T20:06:45Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:47Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:46Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:47Z","status":"completed"},{"completedAt":"2026-09-04T20:06:59Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:07:09Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T20:06:59Z","status":"completed"},{"completedAt":"2026-09-04T20:07:35Z","conclusion":"success","name":"Run tests","number":5,"startedAt":"2026-09-04T20:07:09Z","status":"completed"},{"completedAt":"2026-09-04T20:07:38Z","conclusion":"success","name":"Run doc tests","number":6,"startedAt":"2026-09-04T20:07:35Z","status":"completed"},{"completedAt":"2026-09-04T20:07:44Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T20:07:38Z","status":"completed"},{"completedAt":"2026-09-04T20:07:45Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T20:07:44Z","status":"completed"},{"completedAt":"2026-09-04T20:07:46Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T20:07:45Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158646795"},{"completedAt":"2026-09-04T20:06:37Z","conclusion":"skipped","databaseId":101158647300,"name":"Create Rust changelog PR","startedAt":"2026-09-04T20:06:38Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158647300"},{"completedAt":"2026-09-04T20:06:37Z","conclusion":"skipped","databaseId":101158647604,"name":"Instant Rust release","startedAt":"2026-09-04T20:06:38Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158647604"},{"completedAt":"2026-09-04T20:06:37Z","conclusion":"skipped","databaseId":101158679874,"name":"Rust changelog fragment check","startedAt":"2026-09-04T20:06:45Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101158679874"},{"completedAt":"2026-09-04T20:08:57Z","conclusion":"success","databaseId":101159094132,"name":"Build Rust package","startedAt":"2026-09-04T20:08:19Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:08:21Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:08:20Z","status":"completed"},{"completedAt":"2026-09-04T20:08:22Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:08:21Z","status":"completed"},{"completedAt":"2026-09-04T20:08:32Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:08:22Z","status":"completed"},{"completedAt":"2026-09-04T20:08:36Z","conclusion":"success","name":"Cache cargo registry","number":4,"startedAt":"2026-09-04T20:08:32Z","status":"completed"},{"completedAt":"2026-09-04T20:08:49Z","conclusion":"success","name":"Build release","number":5,"startedAt":"2026-09-04T20:08:36Z","status":"completed"},{"completedAt":"2026-09-04T20:08:51Z","conclusion":"success","name":"Check package","number":6,"startedAt":"2026-09-04T20:08:49Z","status":"completed"},{"completedAt":"2026-09-04T20:08:55Z","conclusion":"success","name":"Post Cache cargo registry","number":11,"startedAt":"2026-09-04T20:08:51Z","status":"completed"},{"completedAt":"2026-09-04T20:08:55Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":12,"startedAt":"2026-09-04T20:08:55Z","status":"completed"},{"completedAt":"2026-09-04T20:08:55Z","conclusion":"success","name":"Complete job","number":13,"startedAt":"2026-09-04T20:08:55Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101159094132"},{"completedAt":"2026-09-04T20:10:58Z","conclusion":"success","databaseId":101159276079,"name":"Release Rust crate","startedAt":"2026-09-04T20:08:59Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:09:01Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:09:00Z","status":"completed"},{"completedAt":"2026-09-04T20:09:03Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:09:01Z","status":"completed"},{"completedAt":"2026-09-04T20:09:13Z","conclusion":"success","name":"Setup Rust","number":3,"startedAt":"2026-09-04T20:09:03Z","status":"completed"},{"completedAt":"2026-09-04T20:09:49Z","conclusion":"success","name":"Install rust-script","number":4,"startedAt":"2026-09-04T20:09:13Z","status":"completed"},{"completedAt":"2026-09-04T20:09:58Z","conclusion":"success","name":"Determine bump type","number":5,"startedAt":"2026-09-04T20:09:49Z","status":"completed"},{"completedAt":"2026-09-04T20:10:23Z","conclusion":"success","name":"Check whether Rust release is needed","number":6,"startedAt":"2026-09-04T20:09:58Z","status":"completed"},{"completedAt":"2026-09-04T20:10:30Z","conclusion":"success","name":"Version Rust crate and commit to main","number":7,"startedAt":"2026-09-04T20:10:23Z","status":"completed"},{"completedAt":"2026-09-04T20:10:31Z","conclusion":"success","name":"Read Rust release version","number":8,"startedAt":"2026-09-04T20:10:30Z","status":"completed"},{"completedAt":"2026-09-04T20:10:49Z","conclusion":"success","name":"Publish to crates.io","number":9,"startedAt":"2026-09-04T20:10:31Z","status":"completed"},{"completedAt":"2026-09-04T20:10:54Z","conclusion":"success","name":"Create Rust GitHub Release","number":10,"startedAt":"2026-09-04T20:10:49Z","status":"completed"},{"completedAt":"2026-09-04T20:10:55Z","conclusion":"success","name":"Wait for crate availability","number":11,"startedAt":"2026-09-04T20:10:54Z","status":"completed"},{"completedAt":"2026-09-04T20:10:55Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":22,"startedAt":"2026-09-04T20:10:55Z","status":"completed"},{"completedAt":"2026-09-04T20:10:55Z","conclusion":"success","name":"Complete job","number":23,"startedAt":"2026-09-04T20:10:55Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574263/job/101159276079"}],"workflowName":"Rust checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/run-33914574283.json b/dev/log/issues/199/pulls/200/api/run-33914574283.json new file mode 100644 index 00000000..96cce220 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/run-33914574283.json @@ -0,0 +1 @@ +{"conclusion":"failure","createdAt":"2026-09-04T20:06:37Z","databaseId":33914574283,"event":"push","headBranch":"main","headSha":"000dbeab30fd580f07b4d21a73ac40c55ce2e7fc","jobs":[{"completedAt":"2026-09-04T20:06:58Z","conclusion":"success","databaseId":101158646560,"name":"Lint and format JavaScript","startedAt":"2026-09-04T20:06:39Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:40Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:41Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:43Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:44Z","conclusion":"success","name":"Install dependencies","number":4,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:51Z","conclusion":"success","name":"Run ESLint","number":5,"startedAt":"2026-09-04T20:06:44Z","status":"completed"},{"completedAt":"2026-09-04T20:06:55Z","conclusion":"success","name":"Check formatting","number":6,"startedAt":"2026-09-04T20:06:51Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","name":"Check code duplication","number":7,"startedAt":"2026-09-04T20:06:55Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","name":"Post Setup Bun","number":13,"startedAt":"2026-09-04T20:06:56Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":14,"startedAt":"2026-09-04T20:06:56Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","name":"Complete job","number":15,"startedAt":"2026-09-04T20:06:56Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158646560"},{"completedAt":"2026-09-04T20:07:38Z","conclusion":"success","databaseId":101158646745,"name":"Test JavaScript (bun on ubuntu-latest)","startedAt":"2026-09-04T20:06:39Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:41Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:42Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:43Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:42Z","status":"completed"},{"completedAt":"2026-09-04T20:06:44Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T20:06:44Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:07:36Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:07:36Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T20:07:36Z","status":"completed"},{"completedAt":"2026-09-04T20:07:36Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T20:07:36Z","status":"completed"},{"completedAt":"2026-09-04T20:07:36Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T20:07:36Z","status":"completed"},{"completedAt":"2026-09-04T20:07:36Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T20:07:36Z","status":"completed"},{"completedAt":"2026-09-04T20:07:37Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T20:07:36Z","status":"completed"},{"completedAt":"2026-09-04T20:07:37Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T20:07:37Z","status":"completed"},{"completedAt":"2026-09-04T20:07:37Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T20:07:37Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158646745"},{"completedAt":"2026-09-04T20:07:57Z","conclusion":"success","databaseId":101158646852,"name":"Test JavaScript (bun on windows-latest)","startedAt":"2026-09-04T20:06:39Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:41Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:47Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:47Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T20:06:56Z","status":"completed"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T20:06:56Z","status":"completed"},{"completedAt":"2026-09-04T20:07:03Z","conclusion":"success","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T20:06:56Z","status":"completed"},{"completedAt":"2026-09-04T20:07:15Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T20:07:03Z","status":"completed"},{"completedAt":"2026-09-04T20:07:53Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T20:07:15Z","status":"completed"},{"completedAt":"2026-09-04T20:07:53Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T20:07:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:53Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T20:07:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:53Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T20:07:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:53Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T20:07:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:53Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T20:07:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:56Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T20:07:53Z","status":"completed"},{"completedAt":"2026-09-04T20:07:56Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T20:07:56Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158646852"},{"completedAt":"2026-09-04T20:06:56Z","conclusion":"success","databaseId":101158646917,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T20:06:39Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:42Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:40Z","status":"completed"},{"completedAt":"2026-09-04T20:06:43Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:42Z","status":"completed"},{"completedAt":"2026-09-04T20:06:43Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:43Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:06:49Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:06:53Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T20:06:49Z","status":"completed"},{"completedAt":"2026-09-04T20:06:54Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T20:06:53Z","status":"completed"},{"completedAt":"2026-09-04T20:06:54Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T20:06:54Z","status":"completed"},{"completedAt":"2026-09-04T20:06:54Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T20:06:54Z","status":"completed"},{"completedAt":"2026-09-04T20:06:54Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T20:06:54Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158646917"},{"completedAt":"2026-09-04T20:07:12Z","conclusion":"success","databaseId":101158646956,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T20:06:40Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:44Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:46Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:44Z","status":"completed"},{"completedAt":"2026-09-04T20:06:46Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:46Z","status":"completed"},{"completedAt":"2026-09-04T20:06:46Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T20:06:46Z","status":"completed"},{"completedAt":"2026-09-04T20:06:55Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T20:06:46Z","status":"completed"},{"completedAt":"2026-09-04T20:06:55Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T20:06:55Z","status":"completed"},{"completedAt":"2026-09-04T20:06:55Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T20:06:55Z","status":"completed"},{"completedAt":"2026-09-04T20:06:55Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T20:06:55Z","status":"completed"},{"completedAt":"2026-09-04T20:06:55Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T20:06:55Z","status":"completed"},{"completedAt":"2026-09-04T20:07:01Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T20:06:55Z","status":"completed"},{"completedAt":"2026-09-04T20:07:06Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T20:07:01Z","status":"completed"},{"completedAt":"2026-09-04T20:07:09Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T20:07:06Z","status":"completed"},{"completedAt":"2026-09-04T20:07:09Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T20:07:09Z","status":"completed"},{"completedAt":"2026-09-04T20:07:10Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T20:07:09Z","status":"completed"},{"completedAt":"2026-09-04T20:07:10Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T20:07:10Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158646956"},{"completedAt":"2026-09-04T20:08:06Z","conclusion":"success","databaseId":101158646970,"name":"Test JavaScript (bun on macos-latest)","startedAt":"2026-09-04T20:06:40Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:42Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:41Z","status":"completed"},{"completedAt":"2026-09-04T20:06:44Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:42Z","status":"completed"},{"completedAt":"2026-09-04T20:06:46Z","conclusion":"success","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:44Z","status":"completed"},{"completedAt":"2026-09-04T20:06:48Z","conclusion":"success","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T20:06:46Z","status":"completed"},{"completedAt":"2026-09-04T20:06:48Z","conclusion":"skipped","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T20:06:48Z","status":"completed"},{"completedAt":"2026-09-04T20:06:51Z","conclusion":"success","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T20:06:48Z","status":"completed"},{"completedAt":"2026-09-04T20:06:51Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T20:06:51Z","status":"completed"},{"completedAt":"2026-09-04T20:06:52Z","conclusion":"success","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T20:06:51Z","status":"completed"},{"completedAt":"2026-09-04T20:08:02Z","conclusion":"success","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T20:06:52Z","status":"completed"},{"completedAt":"2026-09-04T20:08:02Z","conclusion":"skipped","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T20:08:02Z","status":"completed"},{"completedAt":"2026-09-04T20:08:02Z","conclusion":"skipped","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T20:08:02Z","status":"completed"},{"completedAt":"2026-09-04T20:08:02Z","conclusion":"skipped","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T20:08:02Z","status":"completed"},{"completedAt":"2026-09-04T20:08:02Z","conclusion":"success","name":"Post Setup Node.js PTY host","number":22,"startedAt":"2026-09-04T20:08:02Z","status":"completed"},{"completedAt":"2026-09-04T20:08:03Z","conclusion":"success","name":"Post Setup Bun","number":23,"startedAt":"2026-09-04T20:08:02Z","status":"completed"},{"completedAt":"2026-09-04T20:08:03Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T20:08:03Z","status":"completed"},{"completedAt":"2026-09-04T20:08:04Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T20:08:03Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158646970"},{"completedAt":"2026-09-04T20:07:01Z","conclusion":"success","databaseId":101158647093,"name":"Test JavaScript (node on ubuntu-latest)","startedAt":"2026-09-04T20:06:40Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:06:43Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:06:42Z","status":"completed"},{"completedAt":"2026-09-04T20:06:45Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:06:43Z","status":"completed"},{"completedAt":"2026-09-04T20:06:45Z","conclusion":"skipped","name":"Setup Bun","number":3,"startedAt":"2026-09-04T20:06:45Z","status":"completed"},{"completedAt":"2026-09-04T20:06:45Z","conclusion":"skipped","name":"Setup Node.js PTY host","number":4,"startedAt":"2026-09-04T20:06:45Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"success","name":"Install system dependencies (Ubuntu)","number":5,"startedAt":"2026-09-04T20:06:45Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"skipped","name":"Install system dependencies (macOS)","number":6,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"skipped","name":"Install system dependencies (Windows)","number":7,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"skipped","name":"Install dependencies (Bun)","number":8,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:50Z","conclusion":"skipped","name":"Run tests (Bun)","number":9,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:51Z","conclusion":"success","name":"Setup Node.js","number":10,"startedAt":"2026-09-04T20:06:50Z","status":"completed"},{"completedAt":"2026-09-04T20:06:57Z","conclusion":"success","name":"Install dependencies (Node)","number":11,"startedAt":"2026-09-04T20:06:51Z","status":"completed"},{"completedAt":"2026-09-04T20:06:58Z","conclusion":"success","name":"Test Node.js compatibility","number":12,"startedAt":"2026-09-04T20:06:57Z","status":"completed"},{"completedAt":"2026-09-04T20:06:59Z","conclusion":"success","name":"Post Setup Node.js","number":23,"startedAt":"2026-09-04T20:06:58Z","status":"completed"},{"completedAt":"2026-09-04T20:06:59Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":24,"startedAt":"2026-09-04T20:06:59Z","status":"completed"},{"completedAt":"2026-09-04T20:06:59Z","conclusion":"success","name":"Complete job","number":25,"startedAt":"2026-09-04T20:06:59Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158647093"},{"completedAt":"2026-09-04T20:06:37Z","conclusion":"skipped","databaseId":101158647419,"name":"Check for JavaScript changesets","startedAt":"2026-09-04T20:06:38Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158647419"},{"completedAt":"2026-09-04T20:06:37Z","conclusion":"skipped","databaseId":101158647639,"name":"Create JavaScript changeset PR","startedAt":"2026-09-04T20:06:38Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158647639"},{"completedAt":"2026-09-04T20:06:37Z","conclusion":"skipped","databaseId":101158672537,"name":"Instant JavaScript release","startedAt":"2026-09-04T20:06:43Z","status":"completed","steps":[],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101158672537"},{"completedAt":"2026-09-04T20:09:12Z","conclusion":"failure","databaseId":101159049363,"name":"Release JavaScript package","startedAt":"2026-09-04T20:08:11Z","status":"completed","steps":[{"completedAt":"2026-09-04T20:08:13Z","conclusion":"success","name":"Set up job","number":1,"startedAt":"2026-09-04T20:08:12Z","status":"completed"},{"completedAt":"2026-09-04T20:08:15Z","conclusion":"success","name":"Run actions/checkout@v6","number":2,"startedAt":"2026-09-04T20:08:13Z","status":"completed"},{"completedAt":"2026-09-04T20:08:16Z","conclusion":"success","name":"Setup Node.js","number":3,"startedAt":"2026-09-04T20:08:15Z","status":"completed"},{"completedAt":"2026-09-04T20:08:17Z","conclusion":"success","name":"Setup Bun","number":4,"startedAt":"2026-09-04T20:08:16Z","status":"completed"},{"completedAt":"2026-09-04T20:08:18Z","conclusion":"success","name":"Install dependencies","number":5,"startedAt":"2026-09-04T20:08:17Z","status":"completed"},{"completedAt":"2026-09-04T20:08:23Z","conclusion":"success","name":"Update npm for OIDC trusted publishing","number":6,"startedAt":"2026-09-04T20:08:18Z","status":"completed"},{"completedAt":"2026-09-04T20:08:23Z","conclusion":"success","name":"Check for changesets","number":7,"startedAt":"2026-09-04T20:08:23Z","status":"completed"},{"completedAt":"2026-09-04T20:08:24Z","conclusion":"success","name":"Check if release is needed","number":8,"startedAt":"2026-09-04T20:08:23Z","status":"completed"},{"completedAt":"2026-09-04T20:08:24Z","conclusion":"skipped","name":"Merge multiple changesets","number":9,"startedAt":"2026-09-04T20:08:24Z","status":"completed"},{"completedAt":"2026-09-04T20:08:29Z","conclusion":"success","name":"Version package and commit to main","number":10,"startedAt":"2026-09-04T20:08:24Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"failure","name":"Publish to npm","number":11,"startedAt":"2026-09-04T20:08:29Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"skipped","name":"Create JavaScript GitHub Release","number":12,"startedAt":"2026-09-04T20:09:09Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"skipped","name":"Format JavaScript GitHub release notes","number":13,"startedAt":"2026-09-04T20:09:09Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"skipped","name":"Verify npm availability","number":14,"startedAt":"2026-09-04T20:09:09Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"skipped","name":"Post Setup Bun","number":26,"startedAt":"2026-09-04T20:09:09Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"skipped","name":"Post Setup Node.js","number":27,"startedAt":"2026-09-04T20:09:09Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"success","name":"Post Run actions/checkout@v6","number":28,"startedAt":"2026-09-04T20:09:09Z","status":"completed"},{"completedAt":"2026-09-04T20:09:09Z","conclusion":"success","name":"Complete job","number":29,"startedAt":"2026-09-04T20:09:09Z","status":"completed"}],"url":"https://github.com/link-foundation/command-stream/actions/runs/33914574283/job/101159049363"}],"workflowName":"JavaScript checks and release"} diff --git a/dev/log/issues/199/pulls/200/api/runs-recent.json b/dev/log/issues/199/pulls/200/api/runs-recent.json new file mode 100644 index 00000000..c1e59373 --- /dev/null +++ b/dev/log/issues/199/pulls/200/api/runs-recent.json @@ -0,0 +1 @@ +[{"conclusion":"success","createdAt":"2026-09-04T20:28:41Z","databaseId":33916414387,"event":"pull_request","headBranch":"issue-199-32c07917fc87","headSha":"f1bb99c1badb81cc574ec3aa5054e7a086f47ba7","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"failure","createdAt":"2026-09-04T20:06:37Z","databaseId":33914574283,"event":"push","headBranch":"main","headSha":"000dbeab30fd580f07b4d21a73ac40c55ce2e7fc","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-09-04T20:06:37Z","databaseId":33914574263,"event":"push","headBranch":"main","headSha":"000dbeab30fd580f07b4d21a73ac40c55ce2e7fc","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-09-04T19:52:29Z","databaseId":33913378539,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"6027b7fd3c25055597cc3d44f7945da02a2d6251","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-09-04T19:52:28Z","databaseId":33913378338,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"6027b7fd3c25055597cc3d44f7945da02a2d6251","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-09-04T19:52:28Z","databaseId":33913378273,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"6027b7fd3c25055597cc3d44f7945da02a2d6251","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-09-04T19:32:11Z","databaseId":33911660948,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"f9bf3decd6e9f52f5928777050e97439adc03b10","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"failure","createdAt":"2026-09-04T19:32:11Z","databaseId":33911660947,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"f9bf3decd6e9f52f5928777050e97439adc03b10","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"failure","createdAt":"2026-09-04T19:32:11Z","databaseId":33911660942,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"f9bf3decd6e9f52f5928777050e97439adc03b10","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"failure","createdAt":"2026-09-04T19:15:06Z","databaseId":33910180769,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"e6a3eef7a59135824f249871756fae5c9aac872e","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-09-04T19:15:05Z","databaseId":33910180425,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"e6a3eef7a59135824f249871756fae5c9aac872e","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"failure","createdAt":"2026-09-04T19:15:05Z","databaseId":33910180248,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"e6a3eef7a59135824f249871756fae5c9aac872e","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-09-04T16:59:31Z","databaseId":33898153785,"event":"pull_request","headBranch":"issue-197-b748bb92cd2d","headSha":"7b39dd098f11d09b03f0c7643a8d393348d44b1f","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-09-04T16:54:18Z","databaseId":33897699303,"event":"push","headBranch":"main","headSha":"7e2b7018494b9ad9129391829a7abd5e4d085228","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"failure","createdAt":"2026-09-04T16:54:18Z","databaseId":33897699209,"event":"push","headBranch":"main","headSha":"7e2b7018494b9ad9129391829a7abd5e4d085228","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-09-04T16:46:58Z","databaseId":33897043761,"event":"pull_request","headBranch":"issue-49-08e57741","headSha":"9042b5d030e6d7c6274aca09ed3ebb1503cb27dd","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-09-04T16:46:57Z","databaseId":33897043609,"event":"pull_request","headBranch":"issue-49-08e57741","headSha":"9042b5d030e6d7c6274aca09ed3ebb1503cb27dd","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-09-04T16:46:57Z","databaseId":33897043480,"event":"pull_request","headBranch":"issue-49-08e57741","headSha":"9042b5d030e6d7c6274aca09ed3ebb1503cb27dd","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-08-11T11:26:02Z","databaseId":31486604993,"event":"push","headBranch":"main","headSha":"3b21e7642d86e4fe24c7bd471af425756886d9f0","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"skipped","createdAt":"2026-08-11T11:20:35Z","databaseId":31486196661,"event":"pull_request","headBranch":"issue-189-a73113905acf","headSha":"95184683d13aef0074e15c890e7d36fad8c9567a","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-08-11T11:20:35Z","databaseId":31486196636,"event":"pull_request","headBranch":"issue-189-a73113905acf","headSha":"95184683d13aef0074e15c890e7d36fad8c9567a","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-08-11T11:18:00Z","databaseId":31486007677,"event":"push","headBranch":"main","headSha":"391535afa9d8a73213fb05fa4c2dfb601a7b0fa8","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-08-11T11:11:20Z","databaseId":31485514037,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"cc7d6b7df2b974bb1ac18a3e4863ba1865e1a992","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"skipped","createdAt":"2026-08-11T11:11:20Z","databaseId":31485514025,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"cc7d6b7df2b974bb1ac18a3e4863ba1865e1a992","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"skipped","createdAt":"2026-08-11T11:06:41Z","databaseId":31485168814,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"e122cc04ff1bdcaee970a34f03c6bc9ed484b796","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-08-11T11:06:41Z","databaseId":31485168809,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"e122cc04ff1bdcaee970a34f03c6bc9ed484b796","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-08-11T11:02:20Z","databaseId":31484840295,"event":"push","headBranch":"main","headSha":"ba87d0c6399cd6cd1c10825a9aec53b38b44b531","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"skipped","createdAt":"2026-08-11T11:01:10Z","databaseId":31484756228,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"2fe1ed3e20eafa3064f8479dcea733bdf06b0480","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"cancelled","createdAt":"2026-08-11T11:00:48Z","databaseId":31484727690,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"2fe1ed3e20eafa3064f8479dcea733bdf06b0480","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-08-11T11:00:48Z","databaseId":31484727626,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"2fe1ed3e20eafa3064f8479dcea733bdf06b0480","name":"Rust checks and release","status":"completed","workflowName":"Rust checks and release"},{"conclusion":"success","createdAt":"2026-08-11T10:55:21Z","databaseId":31484329550,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"0130712acc64c6b7101f977ade58d97a4498ed77","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"skipped","createdAt":"2026-08-11T10:55:21Z","databaseId":31484329538,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"0130712acc64c6b7101f977ade58d97a4498ed77","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"skipped","createdAt":"2026-08-11T10:52:28Z","databaseId":31484124092,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"189920ffb51d0b0c1af8e001efde3dbd9dc7a7a5","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"success","createdAt":"2026-08-11T10:52:28Z","databaseId":31484124080,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"189920ffb51d0b0c1af8e001efde3dbd9dc7a7a5","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-08-11T10:42:40Z","databaseId":31483409297,"event":"pull_request","headBranch":"issue-190-3191ec24d668","headSha":"71178552016d2879274a60b0447f92c8ddcd5a2f","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"skipped","createdAt":"2026-08-11T10:41:49Z","databaseId":31483350432,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"2d67d773fe0bc108a4d2b5efc4f0e9c15411a57e","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"failure","createdAt":"2026-08-11T10:41:12Z","databaseId":31483304571,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"2d67d773fe0bc108a4d2b5efc4f0e9c15411a57e","name":"Language parity check","status":"completed","workflowName":"Language parity check"},{"conclusion":"failure","createdAt":"2026-08-11T10:41:12Z","databaseId":31483304561,"event":"pull_request","headBranch":"issue-191-70b38becaee6","headSha":"2d67d773fe0bc108a4d2b5efc4f0e9c15411a57e","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-08-11T10:24:37Z","databaseId":31482070648,"event":"push","headBranch":"main","headSha":"e43ba26cbd43f6976c7bba6b2dde510b42c7f0a6","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"},{"conclusion":"success","createdAt":"2026-08-11T10:18:17Z","databaseId":31481606311,"event":"pull_request","headBranch":"issue-192-3752acabac90","headSha":"8bad68c2b0ed6f52e40c4507eb348725f4d76838","name":"JavaScript checks and release","status":"completed","workflowName":"JavaScript checks and release"}] diff --git a/dev/log/issues/199/pulls/200/ci-logs/README.md b/dev/log/issues/199/pulls/200/ci-logs/README.md new file mode 100644 index 00000000..d36ddd56 --- /dev/null +++ b/dev/log/issues/199/pulls/200/ci-logs/README.md @@ -0,0 +1,22 @@ +# CI run evidence for issue #199 + +Downloaded with `gh run view --log`. Full logs are committed **gzipped** +(`*.log.gz`, ~1.2 MB total instead of ~9.8 MB) following the precedent set by +`docs/case-studies/issue-166/ci-logs/`. Read one with: + +```bash +gunzip -c dev/log/issues/199/pulls/200/ci-logs/run-33914574283.log.gz | less +``` + +Per-run metadata (workflow, conclusion, per-job conclusions) is in +`../api/run-.json`. + +| Run | Workflow | Branch | Commit | Event | Created (UTC) | Conclusion | Failed jobs | +| --- | --- | --- | --- | --- | --- | --- | --- | +| [33897699209](https://github.com/link-foundation/command-stream/actions/runs/33897699209) | JavaScript checks and release | `main` | `7e2b7018` | push | 2026-09-04T16:54:18Z | failure | `Release JavaScript package` | +| [33910180248](https://github.com/link-foundation/command-stream/actions/runs/33910180248) | JavaScript checks and release | `issue-197-b748bb92cd2d` | `e6a3eef7` | pull_request | 2026-09-04T19:15:05Z | failure | `Test JavaScript (bun on macos-latest)`, `Test JavaScript (bun on ubuntu-latest)`, `Test JavaScript (bun on windows-latest)` | +| [33910180769](https://github.com/link-foundation/command-stream/actions/runs/33910180769) | Rust checks and release | `issue-197-b748bb92cd2d` | `e6a3eef7` | pull_request | 2026-09-04T19:15:06Z | failure | `Test Rust (macos-latest)`, `Test Rust (windows-latest)` | +| [33911660942](https://github.com/link-foundation/command-stream/actions/runs/33911660942) | Rust checks and release | `issue-197-b748bb92cd2d` | `f9bf3dec` | pull_request | 2026-09-04T19:32:11Z | failure | `Test Rust (macos-latest)` | +| [33911660947](https://github.com/link-foundation/command-stream/actions/runs/33911660947) | JavaScript checks and release | `issue-197-b748bb92cd2d` | `f9bf3dec` | pull_request | 2026-09-04T19:32:11Z | failure | `Test JavaScript (bun on macos-latest)` | +| [33914574263](https://github.com/link-foundation/command-stream/actions/runs/33914574263) | Rust checks and release | `main` | `000dbeab` | push | 2026-09-04T20:06:37Z | success | — | +| [33914574283](https://github.com/link-foundation/command-stream/actions/runs/33914574283) | JavaScript checks and release | `main` | `000dbeab` | push | 2026-09-04T20:06:37Z | failure | `Release JavaScript package` | diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33897699209.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33897699209.log.gz new file mode 100644 index 00000000..b484d7ff Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33897699209.log.gz differ diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33910180248.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33910180248.log.gz new file mode 100644 index 00000000..b8818573 Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33910180248.log.gz differ diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33910180769.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33910180769.log.gz new file mode 100644 index 00000000..78614b60 Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33910180769.log.gz differ diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33911660942.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33911660942.log.gz new file mode 100644 index 00000000..635f66e7 Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33911660942.log.gz differ diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33911660947.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33911660947.log.gz new file mode 100644 index 00000000..09a57462 Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33911660947.log.gz differ diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33914574263.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33914574263.log.gz new file mode 100644 index 00000000..224a80d8 Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33914574263.log.gz differ diff --git a/dev/log/issues/199/pulls/200/ci-logs/run-33914574283.log.gz b/dev/log/issues/199/pulls/200/ci-logs/run-33914574283.log.gz new file mode 100644 index 00000000..e0702181 Binary files /dev/null and b/dev/log/issues/199/pulls/200/ci-logs/run-33914574283.log.gz differ diff --git a/dev/log/issues/199/pulls/200/templates/CI-CD-BEST-PRACTICES.md b/dev/log/issues/199/pulls/200/templates/CI-CD-BEST-PRACTICES.md new file mode 100644 index 00000000..2b57fb21 --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/CI-CD-BEST-PRACTICES.md @@ -0,0 +1,469 @@ +# CI/CD Best Practices for AI-Driven Development (languages: en • [zh](CI-CD-BEST-PRACTICES.zh.md) • [hi](CI-CD-BEST-PRACTICES.hi.md) • [ru](CI-CD-BEST-PRACTICES.ru.md)) + +This document describes CI/CD best practices that significantly improve the quality and reliability of AI-driven development workflows. When properly configured, Hive Mind AI solvers are forced to iterate with CI/CD checks until all tests pass, ensuring code quality meets the highest standards. + +## Why CI/CD Matters for AI Development + +Hive Mind's AI issue solver is instructed to pay attention to CI/CD checks in each pull request. This creates a powerful feedback loop: + +1. **AI creates a solution** - The solver generates code based on issue requirements +2. **CI/CD validates the solution** - Automated checks verify code quality +3. **AI iterates until passing** - The solver fixes issues until all checks pass +4. **Quality is guaranteed** - No code merges without passing all gates + +This approach ensures consistent quality regardless of whether the team consists of humans, AIs, or both. + +## Recommended CI/CD Templates + +We provide ready-to-use templates for multiple languages with all best practices pre-configured: + +| Language | Template Repository | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| JavaScript/TypeScript | [js-ai-driven-development-pipeline-template](https://github.com/link-foundation/js-ai-driven-development-pipeline-template) | +| Rust | [rust-ai-driven-development-pipeline-template](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template) | +| Python | [python-ai-driven-development-pipeline-template](https://github.com/link-foundation/python-ai-driven-development-pipeline-template) | +| Go | [go-ai-driven-development-pipeline-template](https://github.com/link-foundation/go-ai-driven-development-pipeline-template) | +| C# | [csharp-ai-driven-development-pipeline-template](https://github.com/link-foundation/csharp-ai-driven-development-pipeline-template) | +| Java | [java-ai-driven-development-pipeline-template](https://github.com/link-foundation/java-ai-driven-development-pipeline-template) | +| PHP | [php-ai-driven-development-pipeline-template](https://github.com/link-foundation/php-ai-driven-development-pipeline-template) | + +> **Tip:** You don't have to pick a template by hand. Run `fix --ci-cd` (see [Automatic CI/CD Remediation](#automatic-cicd-remediation)) and Hive Mind detects the repository's languages and selects the matching templates for you. + +## Key CI/CD Principles + +### 1. Run Checks Only on Relevant File Changes + +**Only trigger checks when relevant files change.** This dramatically reduces CI costs and run times. + +Use a `detect-changes` job at the start of your workflow to determine which file categories changed: + +```yaml +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + code-changed: ${{ steps.changes.outputs.code }} + docs-changed: ${{ steps.changes.outputs.docs }} + docker-changed: ${{ steps.changes.outputs.docker }} + workflow-changed: ${{ steps.changes.outputs.workflow }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Detect changes + id: changes + run: node scripts/detect-code-changes.mjs +``` + +Then gate each job on the relevant output: + +```yaml +test-suites: + needs: [detect-changes] + if: needs.detect-changes.outputs.code-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true' + # ... + +validate-docs: + needs: [detect-changes] + if: needs.detect-changes.outputs.docs-changed == 'true' + # ... + +docker-pr-check: + needs: [detect-changes] + if: needs.detect-changes.outputs.docker-changed == 'true' || needs.detect-changes.outputs.workflow-changed == 'true' + # ... +``` + +**What to exclude from "code changes" detection:** + +- Markdown files (`*.md`) — documentation-only changes don't need changeset files +- `.changeset/` folder — changeset metadata isn't code +- `data/` and `experiments/` folders — non-production content +- `.gitkeep` files — placeholder files with no functional impact + +**What always triggers checks when changed:** + +- Source code files (`.mjs`, `.ts`, `.py`, `.rs`, `.go`, etc.) +- `package.json` / dependency manifests +- CI/CD workflow files (`.github/workflows/*.yml`) +- `Dockerfile` and related infrastructure files + +### 2. File Size Limits + +**Enforce a maximum of 1000-1500 lines per code file.** + +This constraint benefits both AI and human developers: + +- AI models can read and understand entire files within context windows +- Humans can navigate and comprehend files without cognitive overload +- Forces modular, well-organized code architecture + +Example enforcement in CI (bash): + +```bash +find src/ -name "*.mjs" -type f | while read -r file; do + line_count=$(wc -l < "$file") + if [ "$line_count" -gt 1500 ]; then + echo "ERROR: $file has $line_count lines (limit: 1500)" + echo "::error file=$file::File has $line_count lines (limit: 1500)" + exit 1 + fi +done +``` + +**Synchronize the file-size ESLint rule with the CI check** to catch violations locally before CI: + +```js +// eslint.config.mjs +{ + rules: { + 'max-lines': ['error', { max: 1500 }] + } +} +``` + +### 3. Automated Code Formatting + +Consistent formatting eliminates style debates and reduces diff noise: + +| Language | Tool | +| --------------------- | ----------------------------- | +| JavaScript/TypeScript | ESLint + Prettier | +| Rust | rustfmt | +| Python | Ruff | +| Go | gofmt | +| C# | dotnet format | +| Java | Spotless (Google Java Format) | +| PHP | PHP CS Fixer | + +All templates include pre-commit hooks that run formatters automatically before each commit. + +### 4. Static Analysis & Linting + +Catch bugs and enforce patterns before code reaches review: + +| Language | Tools | +| --------------------- | ----------------------------------- | +| JavaScript/TypeScript | ESLint with strict rules | +| Rust | Clippy (pedantic + nursery) | +| Python | Ruff + mypy | +| Go | go vet + staticcheck | +| C# | .NET analyzers (warnings as errors) | +| Java | SpotBugs (maximum effort) | +| PHP | PHPStan (max level) | + +### 5. Fast-Fail Job Ordering + +**Run fast checks before slow checks** to give the fastest possible feedback: + +``` +Fast checks (~7-30s each): Slow checks (~1-10 min each): +├── test-compilation ├── test-suites (unit tests) +├── lint (format + ESLint) ├── test-execution (integration) +└── check-file-line-limits ├── docker-pr-check + └── helm-pr-check +``` + +Gate slow checks on fast checks: + +```yaml +test-suites: + needs: [test-compilation, lint, check-file-line-limits] + if: | + always() && + !cancelled() && + !contains(needs.*.result, 'failure') && + needs.test-compilation.result == 'success' && + needs.lint.result == 'success' && + needs.check-file-line-limits.result == 'success' +``` + +### 6. Changeset-Based Versioning + +All templates use a changeset system that: + +- **Eliminates merge conflicts** - Each PR creates an independent changeset file +- **Automates version bumps** - Highest bump type wins when merging +- **Generates changelogs** - Release notes are compiled automatically +- **Supports semantic versioning** - patch/minor/major bumps are explicit + +| Language | Tool | +| --------------------- | ---------------------------- | +| JavaScript/TypeScript | @changesets/cli | +| Rust | changelog.d + custom scripts | +| Python | Scriv | +| PHP | changelog.d + custom scripts | +| Go, C#, Java | Custom changeset workflows | + +**Exempt docs-only PRs from changeset requirements:** + +```yaml +changeset-check: + needs: [detect-changes] + if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' +``` + +Documentation-only changes (updating `.md` files) should not require a version bump. + +### 7. Validate the Actual Merge Result + +**CI must test what will actually be merged, not a stale PR snapshot.** + +When a PR is opened against a base branch that later receives new commits, the GitHub merge preview can become stale. Simulate a fresh merge before running checks: + +```yaml +- name: Simulate fresh merge with base branch (PR only) + if: github.event_name == 'pull_request' + env: + BASE_REF: ${{ github.base_ref }} + run: | + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + git fetch origin "$BASE_REF" + BEHIND_COUNT=$(git rev-list --count HEAD..origin/$BASE_REF) + if [ "$BEHIND_COUNT" -gt 0 ]; then + git merge origin/$BASE_REF --no-edit || \ + (echo "::error::Merge conflict! PR must be rebased before merging." && exit 1) + fi +``` + +This ensures lint, file-size, and other checks validate the final merged state. + +### 8. Pre-commit Hooks + +Local quality gates prevent broken commits from reaching CI: + +1. Format check and auto-fix +2. Lint and static analysis +3. Type checking (where applicable) +4. File size validation +5. Secrets detection + +This "shift left" approach catches issues immediately rather than waiting for CI. + +### 9. Release Automation + +Automated release workflows ensure: + +- **No manual version management** - Versions update automatically +- **OIDC trusted publishing** - No API tokens needed in CI (npm, PyPI, crates.io) +- **Validated releases only** - All checks must pass before publishing +- **Dual trigger modes** - Both automatic (on merge) and manual (workflow dispatch) + +**Prohibit manual version changes** in PRs — all version bumps should be managed by the CI release workflow: + +```yaml +version-check: + if: github.event_name == 'pull_request' + steps: + - name: Check for version changes in package.json + run: node scripts/check-version.mjs +``` + +### 10. Concurrency Control + +**Separate cancellable read-only checks from non-cancellable write jobs.** Configure concurrency at the job level when a workflow contains both kinds of work: + +```yaml +jobs: + lint: + # Include the job identity (and matrix values, when present) so unrelated + # checks remain parallel while a newer run replaces only the stale check. + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-lint + cancel-in-progress: true + # ... + + deploy: + needs: [lint] + if: ${{ !cancelled() && needs.lint.result == 'success' }} + # Every job that writes to main or an external deployment target uses this + # repository-wide group, even when the jobs live in different workflows. + concurrency: + group: main-writer-${{ github.repository }}-main + cancel-in-progress: false + # ... +``` + +- **Read-only jobs:** Cancel superseded checks on both pull requests and `main` to reduce runner load. Give each job a distinct suffix; include relevant matrix values so different matrix entries can still run in parallel. +- **Dependent writers:** Use `needs` and require successful prerequisites. A cancelled prerequisite must make its write job not start. +- **Active writers:** Give every release, deploy, tag, generated-content push, and other write job the same repository-scoped group with `cancel-in-progress: false`. An already started writer finishes while the next writer waits in the queue, including writers from another workflow file. +- **Workflow scope:** Do not put cancellable concurrency at workflow level when the workflow has write jobs. Cancelling the workflow would also interrupt a writer that has already started. + +By default, a concurrency group keeps at most one running and one pending job; a newer pending writer replaces the older pending writer. If every queued write must run, add `queue: max` to the writer's concurrency block (up to 100 jobs can wait). `queue: max` cannot be combined with `cancel-in-progress: true`, and execution order follows when jobs start waiting rather than workflow dispatch order, so write jobs should remain idempotent. See [GitHub's concurrency documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency) for the current queue limits and semantics. + +Use `!cancelled()` instead of `always()` in job conditions so cancellation propagates correctly through the job graph. A bare `always()` can keep downstream work running after cancellation. + +### 11. Secrets Detection + +Prevent accidental credential leaks in CI: + +- Include a secrets scan step using tools like `secretlint` or `truffleHog` +- Fail CI immediately if secrets are detected +- Never log environment variables or token values + +### 12. Documentation Validation + +**Validate documentation files in CI just like code:** + +- Check file size limits (e.g., max 2500 lines for docs) +- Verify required sections exist in key documents +- Check for broken links using tools like `lychee` + +```yaml +validate-docs: + needs: [detect-changes] + if: needs.detect-changes.outputs.docs-changed == 'true' + steps: + - run: node tests/docs-validation.mjs +``` + +### 13. Container Images: Native Runners per Architecture + +**Build each architecture on its own native runner.** GitHub provides free arm64 Linux runners for public repositories (`ubuntu-24.04-arm`). Emulating arm64 with QEMU on an x86 runner is much slower for compiled languages, and building two architectures inside one job makes them sequential instead of parallel. + +```yaml +build-image: + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - uses: docker/build-push-action@v7 + with: + platforms: ${{ matrix.platform }} + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + +merge-manifest: + needs: [build-image] + steps: + - run: docker buildx imagetools create -t $IMAGE:$VERSION $DIGESTS +``` + +- **No `setup-qemu-action`.** Its presence means an architecture is being emulated; use a native runner instead. +- **Publish images for every architecture your users run.** A single-architecture image silently excludes Apple Silicon, Graviton, and arm CI runners. +- **Always cache.** Set `cache-from: type=gha` and `cache-to: type=gha,mode=max` on every build step; otherwise every architecture rebuilds the full dependency tree for every release. +- **Never gate the release on the image push.** Publish the GitHub Release and language-registry package first, then attach images as they finish. Release notes contain no data derived from image bytes, so a slow or failed registry push must not hide an otherwise completed release. +- **Assert what you shipped.** Verify that the published manifest lists every intended platform and that each default-branch tag has a corresponding GitHub Release; a missing release is otherwise easy to overlook. + +Reference implementations: [`link-foundation/box`](https://github.com/link-foundation/box) and [`link-assistant/hive-mind`](https://github.com/link-assistant/hive-mind). + +### 14. Lint the Workflows Themselves + +**The pipeline is code, and nothing lints it by default.** Workflow files accumulate shell quoting bugs, over-broad `permissions`, unpinned actions and template-injection sinks that no job in the pipeline is looking for, because every job is busy checking the application. + +Two complementary tools, in their own workflow, triggered on changes to `.github/`: + +- [`actionlint`](https://github.com/rhysd/actionlint) — syntax, expressions, and (crucially) the shell inside every `run:` block. +- [`zizmor`](https://docs.zizmor.sh/) — security audits: `excessive-permissions`, `unpinned-uses`, `template-injection`, `artipacked`. + +```yaml +- uses: docker://rhysd/actionlint:1.7.12 + with: + args: -color +``` + +- **Run actionlint as the Docker image, not a bare binary.** The image bundles `shellcheck` and `pyflakes`. A binary without `shellcheck` on `PATH` silently skips every shell check and exits 0 — so a green local run means nothing. This one detail is the difference between finding fourteen shell bugs and finding none. +- **Prefer annotations to SARIF** for zizmor unless code scanning is enabled everywhere the workflow runs. SARIF upload fails silently on forks; annotations fail loudly in both. +- **Set a confidence floor, not a severity floor.** `--min-confidence medium` filters by how sure the tool is, not by how bad the finding is. Review what falls below the floor once and record the decision, rather than discovering later that the floor was hiding a real finding. +- **Scope suppressions to a file, and write down when they can be removed.** A blanket `ignore` is indistinguishable from no gate at all. + +### 15. Audit the Dependency Tree + +**Code scanning does not audit your dependencies, and PR-scoped dependency review does not audit the ones you already have.** These two jobs look like coverage together and leave a hole between them: CodeQL analyses your source, while `dependency-review-action` runs only on `pull_request` and only inspects the dependencies a PR _changes_. An advisory published against a package that has been pinned for a year is invisible to both, forever, because no PR touches that line. + +```yaml +- run: npm audit --package-lock-only --audit-level=high +``` + +- **Audit the lockfile as committed** (`--package-lock-only`). It reports what a consumer would get, and cannot be turned green by a resolution that only happens on this runner. +- **Put the job on the schedule**, not only on push. A scheduled run is the only thing that can notice an advisory published after the code stopped changing. +- **Set the level explicitly.** The default is `low`, which trains everyone to ignore the job; no flag at all is a different failure from a deliberate `--audit-level=high`. + +## Quality Enforcement Strategy + +The templates implement a defense-in-depth approach: + +``` +Developer Machine → CI/CD Pipeline → Release +├── Pre-commit hooks ├── detect-changes ├── All checks pass +├── Local tests ├── version-check ├── Version bump +└── IDE integration ├── changeset-check ├── Changelog update + ├── test-compilation └── Publish package + ├── lint (format+ESLint) + ├── check-file-line-limits + ├── test-suites + ├── test-execution + ├── validate-docs + └── docker-pr-check +``` + +Each layer catches different issues, ensuring no problematic code reaches production. + +## Getting Started + +1. **Choose a template** from the table above matching your language +2. **Use it as a GitHub template** to create your new repository +3. **Configure secrets** if needed for publishing (OIDC preferred) +4. **Start developing** with all best practices pre-configured + +The AI solvers will automatically respect and iterate with all configured checks, producing higher quality output than repositories without CI/CD enforcement. + +## Automatic CI/CD Remediation + +For an existing repository, you don't need to apply these practices by hand. The `fix` command automates the whole flow: + +```bash +fix https://github.com/owner/repo --ci-cd +``` + +This command: + +1. **Detects the repository's languages** using the GitHub Linguist API (`GET /repos/{owner}/{repo}/languages`), ordered by the number of bytes per language. +2. **Selects the matching CI/CD templates** from the table above, sorted so the template for the most-used language comes first. +3. **Inspects the latest default-branch commit** and collects its CI/CD runs (falling back to the most recent runs on the default branch when the latest commit has none). +4. **Creates a remediation issue** that lists the failing runs, the detected languages, the recommended templates, and a link back to this document. The issue is created as a **Bug** (with a `bug` label) and its title and text are taken from the [standard remediation template](https://github.com/link-assistant/web-capture/issues/139). +5. **Hands the issue off to `/solve --development-log --deep-analysis --auto-merge`**, which iterates until the fixes are merged. Every option `fix` does not consume itself (for example `--tool`, `--model`, `--think`) is forwarded to `/solve`. + +### Why the issue is a Bug, and what it leaves out + +`--development-log` replaces the template's retired case-study-folder instruction and collects artifacts under `./dev/log/issues/{issue-id}/pulls/{pull-id}`. `/fix` never emits the retired paragraph, including with `--no-solve` or partial option sets. `--deep-analysis` supplies the timeline, root-cause, debug-output, and upstream-reporting guidance, so `fix` conditionally omits the matching paragraphs instead of delivering them twice. + +That omission is only lossless because `/solve` emits the root-cause wording **only for bug-typed issues** — which is why `fix` creates the issue as a Bug. Issue types are configured per organization and labels per repository, so if the target repository accepts neither, the issue is still created without them. + +The retired paragraph cannot be restored by an option combination; `--development-log` is the only supported collection workflow. The remaining conditional omissions are controlled by `--deep-analysis`. + +### Language → Template Mapping + +The command maps detected languages to templates as follows (JavaScript and TypeScript share a single template): + +| Detected Language(s) | Template | +| --------------------- | ---------------------------------------------------------------- | +| JavaScript/TypeScript | `link-foundation/js-ai-driven-development-pipeline-template` | +| Rust | `link-foundation/rust-ai-driven-development-pipeline-template` | +| Python | `link-foundation/python-ai-driven-development-pipeline-template` | +| Go | `link-foundation/go-ai-driven-development-pipeline-template` | +| C# | `link-foundation/csharp-ai-driven-development-pipeline-template` | +| Java | `link-foundation/java-ai-driven-development-pipeline-template` | +| PHP | `link-foundation/php-ai-driven-development-pipeline-template` | + +Languages without a dedicated template (for example Shell or Dockerfile) are listed in the issue for awareness, and the closest matching template is recommended. + +Use `--dry-run` to preview the issue without creating it, and `--no-solve` to create the issue without starting `/solve`: + +```bash +fix owner/repo --ci-cd --dry-run +fix owner/repo --ci-cd --no-solve +``` + +## References + +- [Code Architecture Principles](https://github.com/link-foundation/code-architecture-principles) +- [Contributing Guidelines](./CONTRIBUTING.md) +- [Best Practices](./BEST-PRACTICES.md) diff --git a/dev/log/issues/199/pulls/200/templates/current/js-ai-driven-development-pipeline-template.links.yml b/dev/log/issues/199/pulls/200/templates/current/js-ai-driven-development-pipeline-template.links.yml new file mode 100644 index 00000000..5ff315d0 --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/current/js-ai-driven-development-pipeline-template.links.yml @@ -0,0 +1,104 @@ +name: Broken Link Checker + +on: + push: + branches: + - main + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' + pull_request: + types: [opened, synchronize, reopened] + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' + workflow_dispatch: + +# Least-privilege default; jobs escalate individually when needed. +permissions: + contents: read + +# Provide Git config to actions/checkout itself; checkout runs git init before +# any workflow step can configure Git. +env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + +jobs: + link-checker: + name: Check Links + runs-on: ubuntu-latest + # Typical run: <1min with lychee cache. 10min prevents slow + # external hosts or Wayback Machine probes from hanging the workflow. + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-link-checker + cancel-in-progress: true + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Check links with lychee + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + # Check all Markdown and HTML files + # Exclude case-studies directory - these are research documents from + # external repos with references to files and issues that don't exist + # in this repository (similar exclusion pattern as eslint.config.js) + # Exclude the Vite source HTML because its root-relative app asset + # URLs are only valid when served by Vite. + # Exclude tests/fixtures - the captured lychee reports there contain + # deliberately broken links used as parser test input. + args: >- + --verbose + --no-progress + --cache + --max-cache-age 1d + --max-retries 3 + --timeout 30 + --exclude-path docs/case-studies + --exclude-path examples/universal-app/index.html + --exclude-path tests/fixtures + './**/*.md' + './**/*.html' + # Don't fail the workflow immediately - we want to check web archive first + fail: false + # Output file for broken links report (used by check-web-archive.mjs) + output: lychee/out.md + # Write a job summary + jobSummary: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check broken links against Web Archive + if: steps.lychee.outputs.exit_code != 0 + id: webarchive + run: node scripts/check-web-archive.mjs + env: + LYCHEE_OUTPUT: lychee/out.md + + - name: Fail if broken links were found + if: always() && steps.lychee.outputs.exit_code != 0 + run: | + echo "::error::Broken live links were detected." + echo "" + echo "What happened:" + echo " lychee found one or more broken links in the *.md and *.html files of this repository." + echo " An archive is a suggested replacement; it does not make the live link valid." + echo "" + echo "How to fix:" + echo " 1. Review the 'Check links with lychee' step above for a full list of broken links." + echo " 2. For links marked with a '::notice::' annotation above, a Web Archive replacement exists." + echo " Replace those broken links with the suggested archive.org URL." + echo " 3. For links with no archive version, either:" + echo " a. Find an updated URL that points to the same or equivalent content." + echo " b. Remove the link if the content is no longer relevant." + echo " c. Add the URL to .lycheeignore if it is a known false positive." + echo "" + echo "Report location: lychee/out.md (available as a workflow artifact if configured)." + exit 1 diff --git a/dev/log/issues/199/pulls/200/templates/current/js-ai-driven-development-pipeline-template.simulate-fresh-merge.sh b/dev/log/issues/199/pulls/200/templates/current/js-ai-driven-development-pipeline-template.simulate-fresh-merge.sh new file mode 100644 index 00000000..b4e70187 --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/current/js-ai-driven-development-pipeline-template.simulate-fresh-merge.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# simulate-fresh-merge.sh +# +# Simulates a fresh merge of the current PR branch with the latest base branch. +# This ensures CI checks run against the actual merge result, not a stale merge preview. +# +# Usage: +# BASE_REF=main bash scripts/simulate-fresh-merge.sh +# +# Environment variables: +# BASE_REF The base branch to merge with (e.g. "main"). Required. +# +# Exit code 0 = merge succeeded or not needed; non-zero = merge conflict detected. +# +# See docs/case-studies/issue-23 for why this is critical. + +set -euo pipefail + +echo "=== Synchronizing PR with latest $BASE_REF ===" +echo "This prevents stale merge preview issues (see docs/case-studies/issue-23)" +echo "" + +# Configure git for merge +# The 41898282+ prefix is what links the commit to the github-actions[bot] +# account. Without it the commit is "unattributed", and a ruleset with +# require_extra_approval_for_unattributed_changes will demand a human +# approval before an automated release pull request can be merged. +git config user.email "41898282+github-actions[bot]@users.noreply.github.com" +git config user.name "github-actions[bot]" + +# Fetch the latest base branch +echo "Fetching latest $BASE_REF..." +git fetch origin "$BASE_REF" + +# Get current and base branch info +CURRENT_SHA=$(git rev-parse HEAD) +BASE_SHA=$(git rev-parse "origin/$BASE_REF") + +echo "Current checkout (merge preview): $CURRENT_SHA" +echo "Latest base branch ($BASE_REF): $BASE_SHA" +echo "" + +# Check if base branch has new commits not in the merge preview +BEHIND_COUNT=$(git rev-list --count "HEAD..origin/$BASE_REF") + +if [ "$BEHIND_COUNT" -eq 0 ]; then + echo "Merge preview is up-to-date with $BASE_REF. No simulation needed." +else + echo "Base branch has $BEHIND_COUNT new commit(s) since PR was opened/synced." + echo "Simulating fresh merge to validate actual merge result..." + echo "" + + # Attempt to merge the latest base branch + if git merge "origin/$BASE_REF" --no-edit; then + echo "" + echo "Fresh merge simulation successful!" + echo "Checks will now run against the up-to-date merged state." + else + echo "" + echo "::error::Merge conflict detected! PR needs to be rebased/updated before it can be merged." + echo "The PR branch is out of sync with $BASE_REF and cannot be automatically merged." + exit 1 + fi +fi +echo "" diff --git a/dev/log/issues/199/pulls/200/templates/current/rust-ai-driven-development-pipeline-template.links.yml b/dev/log/issues/199/pulls/200/templates/current/rust-ai-driven-development-pipeline-template.links.yml new file mode 100644 index 00000000..d0f8b1d8 --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/current/rust-ai-driven-development-pipeline-template.links.yml @@ -0,0 +1,113 @@ +name: Broken Link Checker + +on: + push: + branches: + - main + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' + - '.lycheeignore' + - 'scripts/check-web-archive.mjs' + - 'scripts/check-web-archive.test.mjs' + - 'scripts/fixtures/lychee-report.md' + pull_request: + types: [opened, synchronize, reopened] + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' + - '.lycheeignore' + - 'scripts/check-web-archive.mjs' + - 'scripts/check-web-archive.test.mjs' + - 'scripts/fixtures/lychee-report.md' + workflow_dispatch: + +# Least-privilege default; jobs escalate individually when needed. +permissions: + contents: read + +# Provide Git config to actions/checkout itself; checkout runs git init before +# any workflow step can configure Git. +env: + GIT_CONFIG_COUNT: '1' + GIT_CONFIG_KEY_0: init.defaultBranch + GIT_CONFIG_VALUE_0: main + +jobs: + link-checker: + name: Check Links + runs-on: ubuntu-latest + # Typical run: <1min with lychee cache. 10min prevents slow + # external hosts or Wayback Machine probes from hanging the workflow. + timeout-minutes: 10 + concurrency: + group: check-${{ github.workflow }}-${{ github.ref }}-link-checker + cancel-in-progress: true + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Test Web Archive report parser + run: node --test scripts/check-web-archive.test.mjs + + - name: Check links with lychee + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + # Check all Markdown and HTML files + # Exclude case-studies directory - these are research documents from + # external repos with references to files and issues that don't exist + # in this repository (similar exclusion pattern as eslint.config.js) + args: >- + --verbose + --no-progress + --cache + --max-cache-age 1d + --max-retries 3 + --timeout 30 + --exclude-path docs/case-studies + --exclude-path scripts/fixtures + './**/*.md' + './**/*.html' + # Don't fail the workflow immediately - we want to check web archive first + fail: false + # Output file for broken links report (used by check-web-archive.mjs) + output: lychee/out.md + # Write a job summary + jobSummary: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check broken links against Web Archive + if: steps.lychee.outputs.exit_code != 0 + id: webarchive + run: node scripts/check-web-archive.mjs + env: + LYCHEE_OUTPUT: lychee/out.md + + - name: Fail if broken links were found + if: always() && steps.lychee.outputs.exit_code != 0 + run: | + echo "::error::Broken live links were detected." + echo "" + echo "What happened:" + echo " lychee found one or more broken links in the *.md and *.html files of this repository." + echo " A Web Archive snapshot is a suggested replacement; it does not fix the broken source link." + echo "" + echo "How to fix:" + echo " 1. Review the 'Check links with lychee' step above for a full list of broken links." + echo " 2. Review the Web Archive step when it ran. For links marked with a '::notice::' annotation," + echo " a Web Archive version exists." + echo " Replace those broken links with the suggested archive.org URL." + echo " 3. For links with no archive version, either:" + echo " a. Find an updated URL that points to the same or equivalent content." + echo " b. Remove the link if the content is no longer relevant." + echo " c. Add the URL to .lycheeignore if it is a known false positive." + echo "" + echo "Report location: lychee/out.md (available as a workflow artifact if configured)." + exit 1 diff --git a/dev/log/issues/199/pulls/200/templates/current/rust-ai-driven-development-pipeline-template.simulate-fresh-merge.sh b/dev/log/issues/199/pulls/200/templates/current/rust-ai-driven-development-pipeline-template.simulate-fresh-merge.sh new file mode 100644 index 00000000..72f7f0e3 --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/current/rust-ai-driven-development-pipeline-template.simulate-fresh-merge.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Simulate merging the pull request head into a fresh copy of the base branch and +# run the fast checks against that merge result. +# +# A pull request can be green in isolation and still break the base branch when a +# semantic (non-textual) conflict is introduced: both sides merge cleanly, but the +# combined tree no longer compiles or passes tests. GitHub only tests the PR head +# (or a merge commit computed at PR creation time), so this script recreates the +# merge locally against the current tip of the base branch. +# +# Environment: +# GITHUB_BASE_REF - base branch name (set automatically for pull_request events) +# FRESH_MERGE_CHECKS - optional space-separated override of the commands to run +set -euo pipefail + +BASE_REF="${GITHUB_BASE_REF:-main}" + +if [ -z "${GITHUB_BASE_REF:-}" ]; then + echo "GITHUB_BASE_REF is not set; assuming base branch '${BASE_REF}'" +fi + +echo "Fetching origin/${BASE_REF}..." +git fetch --no-tags origin "${BASE_REF}" + +BASE_SHA="$(git rev-parse "origin/${BASE_REF}")" +HEAD_SHA="$(git rev-parse HEAD)" +echo "Base: ${BASE_REF} (${BASE_SHA})" +echo "Head: ${HEAD_SHA}" + +if git merge-base --is-ancestor "${HEAD_SHA}" "${BASE_SHA}"; then + echo "Head is already contained in origin/${BASE_REF}; nothing to simulate." + exit 0 +fi + +# Merge into a detached checkout of the base tip so the working branch is untouched. +git config user.name "${GIT_AUTHOR_NAME:-github-actions[bot]}" +git config user.email "${GIT_AUTHOR_EMAIL:-github-actions[bot]@users.noreply.github.com}" +git checkout --detach "${BASE_SHA}" + +if ! git merge --no-edit "${HEAD_SHA}"; then + echo "::error::Textual merge conflict with origin/${BASE_REF}. Merge the base branch into this pull request and resolve the conflicts." + git merge --abort || true + git checkout --force - + exit 1 +fi + +echo "Merge succeeded. Running checks on the merged tree..." + +status=0 +if [ -n "${FRESH_MERGE_CHECKS:-}" ]; then + # shellcheck disable=SC2086 + for check in ${FRESH_MERGE_CHECKS}; do + echo "::group::${check}" + eval "${check}" || status=1 + echo "::endgroup::" + done +else + echo "::group::cargo fmt --all -- --check" + cargo fmt --all -- --check || status=1 + echo "::endgroup::" + + echo "::group::cargo clippy --all-targets --all-features" + cargo clippy --all-targets --all-features || status=1 + echo "::endgroup::" + + echo "::group::cargo test --all-features" + cargo test --all-features || status=1 + echo "::endgroup::" +fi + +if [ "${status}" -ne 0 ]; then + echo "::error::Checks failed on the simulated merge with origin/${BASE_REF} even though they pass on the pull request head. This is a semantic merge conflict." +fi + +git checkout --force - >/dev/null 2>&1 || true +exit "${status}" diff --git a/dev/log/issues/199/pulls/200/templates/js-tmpl.file-tree.txt b/dev/log/issues/199/pulls/200/templates/js-tmpl.file-tree.txt new file mode 100644 index 00000000..5f58c496 --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/js-tmpl.file-tree.txt @@ -0,0 +1,374 @@ +./.changeset/README.md +./.changeset/config.json +./.github/actions/publish-dockerhub/action.yml +./.github/actions/setup-buildx-resilient/action.yml +./.github/workflows/example-app.yml +./.github/workflows/links.yml +./.github/workflows/release.yml +./.github/workflows/security.yml +./.github/workflows/workflows.yml +./.github/zizmor.yml +./.gitignore +./.gitkeep +./.husky/pre-commit +./.jscpd.json +./.lycheeignore +./.prettierignore +./.prettierrc +./.secretlintrc.json +./CHANGELOG.md +./LICENSE +./README.md +./bin/example-package-name.js +./bunfig.toml +./deno.json +./deno.lock +./docs/BEST-PRACTICES.md +./docs/CI-TIMEOUT-BUDGETS.md +./docs/CONTRIBUTING.md +./docs/case-studies/issue-13/README.md +./docs/case-studies/issue-13/hive-mind-issue-960.json +./docs/case-studies/issue-13/hive-mind-pr-961-diff.txt +./docs/case-studies/issue-13/hive-mind-pr-961.json +./docs/case-studies/issue-21/README.md +./docs/case-studies/issue-21/ci-logs/run-20803315337.txt +./docs/case-studies/issue-21/ci-logs/run-20885464993.txt +./docs/case-studies/issue-21/issue-111-data.txt +./docs/case-studies/issue-21/issue-113-data.txt +./docs/case-studies/issue-21/pr-112-data.json +./docs/case-studies/issue-21/pr-112-diff.patch +./docs/case-studies/issue-21/pr-114-data.json +./docs/case-studies/issue-21/pr-114-diff.patch +./docs/case-studies/issue-23/README.md +./docs/case-studies/issue-23/data/hive-mind-check-version.mjs +./docs/case-studies/issue-23/data/hive-mind-ci.yml +./docs/case-studies/issue-23/data/hive-mind-eslint.config.mjs +./docs/case-studies/issue-23/data/hive-mind-release.yml +./docs/case-studies/issue-23/data/issue-1126-details.txt +./docs/case-studies/issue-23/data/issue-1141-comments.json +./docs/case-studies/issue-23/data/issue-1141-details.txt +./docs/case-studies/issue-23/data/pr-1127-conversation-comments.json +./docs/case-studies/issue-23/data/pr-1127-diff.txt +./docs/case-studies/issue-23/data/pr-1127-review-comments.json +./docs/case-studies/issue-23/data/pr-1142-conversation-comments.json +./docs/case-studies/issue-23/data/pr-1142-diff.txt +./docs/case-studies/issue-23/data/pr-1142-review-comments.json +./docs/case-studies/issue-25/DETAILED-COMPARISON.md +./docs/case-studies/issue-25/README.md +./docs/case-studies/issue-25/data/hive-mind-file-tree.txt +./docs/case-studies/issue-25/data/issue-1274-case-study.md +./docs/case-studies/issue-25/data/issue-1278-case-study.md +./docs/case-studies/issue-25/data/template-file-tree.txt +./docs/case-studies/issue-29/README.md +./docs/case-studies/issue-3/README.md +./docs/case-studies/issue-3/created-issues.md +./docs/case-studies/issue-3/issue-data.json +./docs/case-studies/issue-3/original-format-release-notes.mjs +./docs/case-studies/issue-3/reference-pr-59-diff.txt +./docs/case-studies/issue-3/reference-pr-59.json +./docs/case-studies/issue-3/release-v0.1.0.json +./docs/case-studies/issue-3/repositories-with-same-script.json +./docs/case-studies/issue-3/research-notes.md +./docs/case-studies/issue-31/README.md +./docs/case-studies/issue-31/web-capture-pr49-commits.json +./docs/case-studies/issue-33/README.md +./docs/case-studies/issue-33/ci-logs/release-24395209194.txt +./docs/case-studies/issue-33/ci-logs/upstream-nodejs-62430.json +./docs/case-studies/issue-33/ci-logs/upstream-npm-cli-9151.json +./docs/case-studies/issue-33/ci-logs/upstream-runner-images-13883.json +./docs/case-studies/issue-36/README.md +./docs/case-studies/issue-36/ci-logs/release-24399965550.txt +./docs/case-studies/issue-38/CASE-STUDY.md +./docs/case-studies/issue-40/CICD-COMPARISON.md +./docs/case-studies/issue-40/README.md +./docs/case-studies/issue-40/data/ci-run-25212337438.json +./docs/case-studies/issue-40/data/ci-runs-branch.json +./docs/case-studies/issue-40/data/downstream-web-capture-issue-98.json +./docs/case-studies/issue-40/data/downstream-web-capture-pr-99.diff +./docs/case-studies/issue-40/data/downstream-web-capture-pr-99.json +./docs/case-studies/issue-40/data/issue-40.json +./docs/case-studies/issue-40/data/js-cicd-files.txt +./docs/case-studies/issue-40/data/js-template-file-tree.txt +./docs/case-studies/issue-40/data/pr-43.json +./docs/case-studies/issue-40/data/related-js-merged-prs.json +./docs/case-studies/issue-40/data/rust-cicd-files.txt +./docs/case-studies/issue-40/data/rust-template-file-tree.txt +./docs/case-studies/issue-40/data/rust-template-head.txt +./docs/case-studies/issue-40/data/shields-broken-prefixed-badge.svg +./docs/case-studies/issue-40/data/shields-broken-prefixed-prerelease-badge.svg +./docs/case-studies/issue-40/data/shields-working-normalized-badge.svg +./docs/case-studies/issue-40/data/shields-working-prerelease-badge.svg +./docs/case-studies/issue-40/rust-template/create-github-release.rs +./docs/case-studies/issue-40/rust-template/release.yml +./docs/case-studies/issue-41/README.md +./docs/case-studies/issue-41/data/hive-mind-check-file-line-limits.sh +./docs/case-studies/issue-41/data/hive-mind-file-tree.txt +./docs/case-studies/issue-41/data/hive-mind-issue-1593-case-study.md +./docs/case-studies/issue-41/data/hive-mind-issue-1593-comments.json +./docs/case-studies/issue-41/data/hive-mind-issue-1593.json +./docs/case-studies/issue-41/data/hive-mind-issue-1730-case-study.md +./docs/case-studies/issue-41/data/hive-mind-issue-1730-comments.json +./docs/case-studies/issue-41/data/hive-mind-issue-1730.json +./docs/case-studies/issue-41/data/js-template-check-file-line-limits-before.sh +./docs/case-studies/issue-41/data/js-template-eslint.config.js +./docs/case-studies/issue-41/data/js-template-file-tree.txt +./docs/case-studies/issue-41/data/js-template-issue-41-comments.json +./docs/case-studies/issue-41/data/js-template-issue-41.json +./docs/case-studies/issue-41/data/js-template-release.yml +./docs/case-studies/issue-41/data/js-template-warn-threshold-search-before.json +./docs/case-studies/issue-41/data/rust-template-check-file-size.rs +./docs/case-studies/issue-41/data/rust-template-created-issue-url.txt +./docs/case-studies/issue-41/data/rust-template-file-tree.txt +./docs/case-studies/issue-41/data/rust-template-issue-40.json +./docs/case-studies/issue-41/data/rust-template-issues.json +./docs/case-studies/issue-41/data/rust-template-max-lines-search.json +./docs/case-studies/issue-41/data/rust-template-release.yml +./docs/case-studies/issue-42/README.md +./docs/case-studies/issue-42/data/ci-runs-branch.json +./docs/case-studies/issue-42/data/issue-42-comments.json +./docs/case-studies/issue-42/data/issue-42.json +./docs/case-studies/issue-42/data/js-template-file-tree.txt +./docs/case-studies/issue-42/data/js-template-pre-fix-head.txt +./docs/case-studies/issue-42/data/link-foundation-my-package-search.txt +./docs/case-studies/issue-42/data/link-foundation-package-name-search.txt +./docs/case-studies/issue-42/data/pr-45-conversation-comments.json +./docs/case-studies/issue-42/data/pr-45-review-comments.json +./docs/case-studies/issue-42/data/pr-45-reviews.json +./docs/case-studies/issue-42/data/pr-45.json +./docs/case-studies/issue-42/data/related-merged-prs-check-release-needed.json +./docs/case-studies/issue-42/data/related-merged-prs-publish-to-npm.json +./docs/case-studies/issue-42/data/rust-template-ci-cd-findings.txt +./docs/case-studies/issue-42/data/rust-template-file-tree.txt +./docs/case-studies/issue-42/data/rust-template-head.txt +./docs/case-studies/issue-56/README.md +./docs/case-studies/issue-56/artifacts/universal-app-mobile.png +./docs/case-studies/issue-56/artifacts/universal-app-web.png +./docs/case-studies/issue-56/data/actions-checkout-release.json +./docs/case-studies/issue-56/data/actions-configure-pages-release.json +./docs/case-studies/issue-56/data/actions-deploy-pages-release.json +./docs/case-studies/issue-56/data/actions-setup-node-release.json +./docs/case-studies/issue-56/data/actions-upload-artifact-release.json +./docs/case-studies/issue-56/data/actions-upload-pages-artifact-release.json +./docs/case-studies/issue-56/data/bun-test-final.log +./docs/case-studies/issue-56/data/changeset-status-after-stage.log +./docs/case-studies/issue-56/data/check-file-line-limits-final-2.log +./docs/case-studies/issue-56/data/check-mjs-syntax-final-2.log +./docs/case-studies/issue-56/data/deep-sdk-capacitor.config.ts +./docs/case-studies/issue-56/data/deep-sdk-electron-package.json +./docs/case-studies/issue-56/data/deep-sdk-file-tree.txt +./docs/case-studies/issue-56/data/deep-sdk-gh-pages.yml +./docs/case-studies/issue-56/data/deep-sdk-package.json +./docs/case-studies/issue-56/data/deep-sdk-repo.json +./docs/case-studies/issue-56/data/deno-test-final.log +./docs/case-studies/issue-56/data/example-desktop-package-final-2.log +./docs/case-studies/issue-56/data/example-mobile-sync-final-2.log +./docs/case-studies/issue-56/data/example-web-build-final-2.log +./docs/case-studies/issue-56/data/issue-56-comments.json +./docs/case-studies/issue-56/data/issue-56.json +./docs/case-studies/issue-56/data/link-foundation-code-search.json +./docs/case-studies/issue-56/data/npm-capacitor-cli.json +./docs/case-studies/issue-56/data/npm-capacitor-core.json +./docs/case-studies/issue-56/data/npm-check-final-4.log +./docs/case-studies/issue-56/data/npm-electron-forge-cli.json +./docs/case-studies/issue-56/data/npm-install-root.log +./docs/case-studies/issue-56/data/npm-install-universal-app-node20-compatible.log +./docs/case-studies/issue-56/data/npm-test-final-3.log +./docs/case-studies/issue-56/data/npm-vite.json +./docs/case-studies/issue-56/data/pr-57.json +./docs/case-studies/issue-56/data/recent-merged-prs.json +./docs/case-studies/issue-56/data/universal-app-test-before.log +./docs/case-studies/issue-56/data/universal-app-test-final-2.log +./docs/case-studies/issue-56/data/validate-changeset-final.log +./docs/case-studies/issue-56/data/vk-bot-desktop-build-renderer.mjs +./docs/case-studies/issue-56/data/vk-bot-desktop-electron-main.cjs +./docs/case-studies/issue-56/data/vk-bot-desktop-file-tree.txt +./docs/case-studies/issue-56/data/vk-bot-desktop-js-workflow.yml +./docs/case-studies/issue-56/data/vk-bot-desktop-package.json +./docs/case-studies/issue-56/data/vk-bot-desktop-repo.json +./docs/case-studies/issue-58/README.md +./docs/case-studies/issue-58/data/actions-configure-pages-release.json +./docs/case-studies/issue-58/data/actions-deploy-pages-release.json +./docs/case-studies/issue-58/data/actions-upload-artifact-release.json +./docs/case-studies/issue-58/data/actions-upload-pages-artifact-release.json +./docs/case-studies/issue-58/data/bun-test.log +./docs/case-studies/issue-58/data/check-file-line-limits.log +./docs/case-studies/issue-58/data/check-mjs-syntax.log +./docs/case-studies/issue-58/data/checks-and-release-25733140225.json +./docs/case-studies/issue-58/data/checks-and-release-25733140225.log +./docs/case-studies/issue-58/data/checks-and-release-25743983223.log +./docs/case-studies/issue-58/data/ci-run-25743983223.json +./docs/case-studies/issue-58/data/csharp-template-file-tree.txt +./docs/case-studies/issue-58/data/csharp-template-release.yml +./docs/case-studies/issue-58/data/deno-test.log +./docs/case-studies/issue-58/data/example-app-25733140224.json +./docs/case-studies/issue-58/data/example-app-25733140224.log +./docs/case-studies/issue-58/data/example-desktop-package.log +./docs/case-studies/issue-58/data/example-mobile-sync.log +./docs/case-studies/issue-58/data/example-web-build.log +./docs/case-studies/issue-58/data/issue-58-comments.json +./docs/case-studies/issue-58/data/issue-58.json +./docs/case-studies/issue-58/data/js-template-file-tree.txt +./docs/case-studies/issue-58/data/link-foundation-example-package-name-search.json +./docs/case-studies/issue-58/data/main-ci-runs.json +./docs/case-studies/issue-58/data/npm-check-final.log +./docs/case-studies/issue-58/data/npm-example-package-name-view.json +./docs/case-studies/issue-58/data/npm-global-install.log +./docs/case-studies/issue-58/data/npm-install-after-metadata.log +./docs/case-studies/issue-58/data/npm-install-universal-app.log +./docs/case-studies/issue-58/data/npm-install.log +./docs/case-studies/issue-58/data/npm-pack-dry-run-final.json +./docs/case-studies/issue-58/data/npm-test-2.log +./docs/case-studies/issue-58/data/npm-whoami.log +./docs/case-studies/issue-58/data/pages-enable-result.json +./docs/case-studies/issue-58/data/pages-status-after-enable.json +./docs/case-studies/issue-58/data/pr-57.diff +./docs/case-studies/issue-58/data/pr-57.json +./docs/case-studies/issue-58/data/pr-59-conversation-comments.json +./docs/case-studies/issue-58/data/pr-59-review-comments.json +./docs/case-studies/issue-58/data/pr-59-reviews.json +./docs/case-studies/issue-58/data/pr-59.json +./docs/case-studies/issue-58/data/python-template-file-tree.txt +./docs/case-studies/issue-58/data/python-template-release.yml +./docs/case-studies/issue-58/data/regression-after-2.log +./docs/case-studies/issue-58/data/regression-after-3.log +./docs/case-studies/issue-58/data/regression-after.log +./docs/case-studies/issue-58/data/regression-before.log +./docs/case-studies/issue-58/data/rust-template-file-tree.txt +./docs/case-studies/issue-58/data/rust-template-release.yml +./docs/case-studies/issue-58/data/secretlint.log +./docs/case-studies/issue-58/data/validate-changeset.log +./docs/case-studies/issue-7/BEST-PRACTICES-COMPARISON.md +./docs/case-studies/issue-7/FORMATTER-COMPARISON.md +./docs/case-studies/issue-7/current-repository-analysis.json +./docs/case-studies/issue-7/effect-template-analysis.json +./docs/case-studies/issue-75/CASE-STUDY.md +./docs/case-studies/issue-93/README.md +./docs/case-studies/issue-93/data/ci-runs-issue-93.json +./docs/case-studies/issue-93/data/issue-93-comments.json +./docs/case-studies/issue-93/data/issue-93.json +./docs/case-studies/issue-93/data/link-foundation-eslint-rules-search.json +./docs/case-studies/issue-93/data/link-foundation-no-changelog-comments-search.json +./docs/case-studies/issue-93/data/merged-prs-case-study.json +./docs/case-studies/issue-93/data/merged-prs-eslint.json +./docs/case-studies/issue-93/data/npm-search-eslint-changelog-comments.json +./docs/case-studies/issue-93/data/pr-94.json +./docs/screenshots/example-app/example-app-en-dark.png +./docs/screenshots/example-app/example-app-en-light.png +./docs/screenshots/example-app/example-app-ru-dark.png +./docs/screenshots/example-app/example-app-ru-light.png +./docs/screenshots/example-app/example-app.png +./eslint-rules/no-changelog-comments.js +./eslint.config.js +./examples/basic-usage.js +./examples/universal-app/README.md +./examples/universal-app/capacitor.config.json +./examples/universal-app/electron/main.cjs +./examples/universal-app/electron/preload.cjs +./examples/universal-app/index.html +./examples/universal-app/package-lock.json +./examples/universal-app/package.json +./examples/universal-app/public/favicon.svg +./examples/universal-app/src/App.js +./examples/universal-app/src/main.js +./examples/universal-app/src/styles.css +./examples/universal-app/vite.config.js +./experiments/budget-runner-demo.sh +./experiments/issue-141-multilang-ignore-list.sh +./experiments/test-changeset-scripts.mjs +./experiments/test-check-release-needed.mjs +./experiments/test-detect-changes.mjs +./experiments/test-failure-detection.mjs +./experiments/test-format-major-changes.mjs +./experiments/test-format-minor-changes.mjs +./experiments/test-format-no-hash.mjs +./experiments/test-format-patch-changes.mjs +./experiments/test-issue75-buildx-mirror-fallback.sh +./package-lock.json +./package.json +./scripts/changeset-version.mjs +./scripts/check-changesets.mjs +./scripts/check-docker-build.mjs +./scripts/check-docker-publish.mjs +./scripts/check-file-line-limits.sh +./scripts/check-mjs-syntax.sh +./scripts/check-pipeline-status.sh +./scripts/check-release-needed.mjs +./scripts/check-version.mjs +./scripts/check-web-archive.mjs +./scripts/create-github-release.mjs +./scripts/create-manual-changeset.mjs +./scripts/debug-print.mjs +./scripts/detect-code-changes.mjs +./scripts/format-github-release.mjs +./scripts/format-release-notes-helpers.mjs +./scripts/format-release-notes.mjs +./scripts/instant-version-bump.mjs +./scripts/js-paths.mjs +./scripts/land-via-pull-request.mjs +./scripts/lint-changed-lines.mjs +./scripts/lint.mjs +./scripts/merge-changesets.mjs +./scripts/npm-registry.mjs +./scripts/package-info.mjs +./scripts/publish-failure-classifier.mjs +./scripts/publish-retry.mjs +./scripts/publish-to-npm.mjs +./scripts/push-failure-classifier.mjs +./scripts/push-main-with-rebase-retry.mjs +./scripts/release-naming.mjs +./scripts/run-command.mjs +./scripts/run-with-budget-warning.sh +./scripts/sanitize-npm-userconfig.mjs +./scripts/setup-npm.mjs +./scripts/simulate-fresh-merge.sh +./scripts/smoke-test-package.mjs +./scripts/update-preview-images.mjs +./scripts/use-module.mjs +./scripts/validate-changeset.mjs +./scripts/version-and-commit.mjs +./scripts/wait-for-npm.mjs +./src/index.d.ts +./src/index.js +./tests/bot-commit-attribution.test.js +./tests/check-changesets.test.js +./tests/check-file-line-limits.test.js +./tests/check-web-archive.test.js +./tests/ci-timeouts.test.js +./tests/create-github-release.test.js +./tests/debug-print.test.js +./tests/detect-code-changes.test.js +./tests/docker-build.test.js +./tests/docker-publish.test.js +./tests/fixtures/lychee-report.md +./tests/index.test.js +./tests/land-via-pull-request.test.js +./tests/links-workflow.test.js +./tests/lint-changed-lines.test.js +./tests/merge-changesets.test.js +./tests/no-changelog-comments.test.js +./tests/npm-registry.test.js +./tests/package-info.test.js +./tests/package-metadata.test.js +./tests/pipeline-status.test.js +./tests/publish-failure-classifier.test.js +./tests/publish-retry.test.js +./tests/push-failure-classifier.test.js +./tests/push-main-with-rebase-retry.test.js +./tests/release-badge.test.js +./tests/release-naming.test.js +./tests/run-with-budget-warning.test.js +./tests/sanitize-npm-userconfig.test.js +./tests/scripts-use-module-adoption.test.js +./tests/security-workflow.test.js +./tests/setup-buildx-resilient.test.js +./tests/setup-npm.test.js +./tests/simulate-fresh-merge.test.js +./tests/smoke-test-package.test.js +./tests/tag-prefix.test.js +./tests/universal-app.test.js +./tests/use-module-integration.test.js +./tests/use-module.test.js +./tests/wait-for-npm.test.js +./tests/workflow-permissions.test.js +./tests/workflow-reliability.test.js +./tests/workflows-lint.test.js diff --git a/dev/log/issues/199/pulls/200/templates/rust-tmpl.file-tree.txt b/dev/log/issues/199/pulls/200/templates/rust-tmpl.file-tree.txt new file mode 100644 index 00000000..df7339eb --- /dev/null +++ b/dev/log/issues/199/pulls/200/templates/rust-tmpl.file-tree.txt @@ -0,0 +1,146 @@ +./.github/actionlint.yaml +./.github/actions/setup-buildx-resilient/action.yml +./.github/workflows/desktop-release.yml +./.github/workflows/links.yml +./.github/workflows/release.yml +./.github/workflows/security.yml +./.github/workflows/workflows.yml +./.github/zizmor.yml +./.gitignore +./.gitkeep +./.lycheeignore +./.pre-commit-config.yaml +./.secretlintrc.json +./CHANGELOG.md +./CONTRIBUTING.md +./Cargo.lock +./Cargo.toml +./LICENSE +./README.md +./changelog.d/README.md +./docs/case-studies/issue-11/README.md +./docs/case-studies/issue-11/analysis-crates-io.md +./docs/case-studies/issue-11/analysis-set-output.md +./docs/case-studies/issue-11/analysis-workflow-dispatch.md +./docs/case-studies/issue-11/online-research.md +./docs/case-studies/issue-17/README.md +./docs/case-studies/issue-19/README.md +./docs/case-studies/issue-19/ci-logs/ci-run-20885464993.log.gz +./docs/case-studies/issue-19/pr-114-data/issue-113-details.txt +./docs/case-studies/issue-19/pr-114-data/pr-commits.json +./docs/case-studies/issue-19/pr-114-data/pr-conversation-comments.json +./docs/case-studies/issue-19/pr-114-data/pr-details.json +./docs/case-studies/issue-19/pr-114-data/pr-diff.patch +./docs/case-studies/issue-19/pr-114-data/pr-review-comments.json +./docs/case-studies/issue-19/pr-114-data/pr-reviews.json +./docs/case-studies/issue-19/pr-114-data/solution-draft-log-1.txt.gz +./docs/case-studies/issue-19/pr-114-data/solution-draft-log-2.txt.gz +./docs/case-studies/issue-21/README.md +./docs/case-studies/issue-21/browser-commander-issue-27.md +./docs/case-studies/issue-21/browser-commander-issue-29.md +./docs/case-studies/issue-21/browser-commander-issue-31.md +./docs/case-studies/issue-21/browser-commander-issue-33.md +./docs/case-studies/issue-21/browser-commander-rust.yml +./docs/case-studies/issue-25/README.md +./docs/case-studies/issue-29/README.md +./docs/case-studies/issue-32/README.md +./docs/case-studies/issue-34/README.md +./docs/case-studies/issue-38/README.md +./docs/case-studies/issue-38/raw-data/downstream-meta-after-run-24985948212.json +./docs/case-studies/issue-38/raw-data/downstream-meta-after-run-24985948212.log.gz +./docs/case-studies/issue-38/raw-data/downstream-meta-before-run-24983875003.json +./docs/case-studies/issue-38/raw-data/downstream-meta-before-run-24983875003.log.gz +./docs/case-studies/issue-38/raw-data/downstream-meta-ontology-issue-3.json +./docs/case-studies/issue-38/raw-data/downstream-meta-ontology-pr-4.json +./docs/case-studies/issue-38/raw-data/issue-38-comments.json +./docs/case-studies/issue-38/raw-data/issue-38.json +./docs/case-studies/issue-38/raw-data/js-template-issue-search.json +./docs/case-studies/issue-38/raw-data/main-run-24465255225.json +./docs/case-studies/issue-38/raw-data/main-run-24465255225.log.gz +./docs/case-studies/issue-38/raw-data/main-runs.json +./docs/case-studies/issue-38/raw-data/pr-39-conversation-comments.json +./docs/case-studies/issue-38/raw-data/pr-39-review-comments.json +./docs/case-studies/issue-38/raw-data/pr-39-reviews.json +./docs/case-studies/issue-38/raw-data/pr-39.json +./docs/case-studies/issue-38/raw-data/pr-branch-runs.json +./docs/case-studies/issue-38/raw-data/pr-run-25212295127.json +./docs/case-studies/issue-38/raw-data/pr-run-25212295127.log.gz +./docs/case-studies/issue-38/raw-data/rust-template-issue-search.json +./docs/case-studies/issue-38/template-data/js-template-ci-tree.txt +./docs/case-studies/issue-38/template-data/js-template-links.yml +./docs/case-studies/issue-38/template-data/js-template-release.yml +./docs/case-studies/issue-38/template-data/rust-template-ci-tree.txt +./docs/case-studies/issue-38/template-data/rust-template-release-after.yml +./docs/case-studies/issue-38/template-data/rust-template-release-before.yml +./docs/case-studies/issue-52/README.md +./docs/case-studies/issue-52/raw-data/issue-52-comments.json +./docs/case-studies/issue-52/raw-data/issue-52.json +./docs/case-studies/issue-52/raw-data/js-issue-62.json +./docs/case-studies/issue-52/raw-data/vk-bot-desktop-issue-51.json +./docs/case-studies/issue-52/raw-data/vk-bot-desktop-pr-52.json +./docs/case-studies/issue-69/README.md +./docs/ci-cd/troubleshooting.md +./docs/download/index.html +./docs/screenshots/desktop-download-page.png +./examples/basic_usage.rs +./experiments/issue-139-multi-language-detect-code-changes.sh +./experiments/test-changelog-parsing.rs +./experiments/test-crates-io-check.rs +./experiments/test-detect-code-changes.sh +./experiments/test-issue141-manifest-printf-quoting.sh +./experiments/test-issue143-throttled-crates-io-probe.rs +./experiments/test-issue69-buildx-mirror-fallback.sh +./experiments/test-version-check-dependencies.sh +./experiments/test-version-check.sh +./scripts/bump-version.rs +./scripts/check-cargo-lock.rs +./scripts/check-changelog-fragment.rs +./scripts/check-crate-size.rs +./scripts/check-file-size.rs +./scripts/check-pipeline-status.sh +./scripts/check-release-needed.rs +./scripts/check-version-modification.rs +./scripts/check-web-archive.mjs +./scripts/check-web-archive.test.mjs +./scripts/collect-changelog.rs +./scripts/create-changelog-fragment.rs +./scripts/create-github-release.rs +./scripts/desktop-release-resolve.sh +./scripts/detect-code-changes.rs +./scripts/fixtures/lychee-report.md +./scripts/get-bump-type.rs +./scripts/get-version.rs +./scripts/git-config.rs +./scripts/install-rust-script.sh +./scripts/package-desktop.sh +./scripts/publish-crate.rs +./scripts/release-naming.rs +./scripts/run-with-budget-warning.sh +./scripts/rust-paths.rs +./scripts/simulate-fresh-merge.sh +./scripts/smoke-test-published-crate.rs +./scripts/version-and-commit.rs +./scripts/wait-for-crate.rs +./src/lib.rs +./src/main.rs +./src/sum.rs +./tests/integration/mod.rs +./tests/integration/sum.rs +./tests/unit/ci-cd/changelog_parsing.rs +./tests/unit/ci-cd/desktop_release_resolve.rs +./tests/unit/ci-cd/issue_119.rs +./tests/unit/ci-cd/issue_127.rs +./tests/unit/ci-cd/issue_135.rs +./tests/unit/ci-cd/issue_141.rs +./tests/unit/ci-cd/issue_143.rs +./tests/unit/ci-cd/issue_147.rs +./tests/unit/ci-cd/mod.rs +./tests/unit/ci-cd/release_naming_tests.rs +./tests/unit/ci-cd/version_and_commit_behind_check.rs +./tests/unit/ci-cd/version_and_commit_tag_order.rs +./tests/unit/ci-cd/workflow_desktop_release.rs +./tests/unit/ci-cd/workflow_release.rs +./tests/unit/ci-cd/workflow_security.rs +./tests/unit/ci-cd/workspace_manifest_resolution.rs +./tests/unit/mod.rs +./tests/unit/sum.rs diff --git a/dev/log/issues/199/pulls/200/upstream/js-template-artipacked.md b/dev/log/issues/199/pulls/200/upstream/js-template-artipacked.md new file mode 100644 index 00000000..58cb85d6 --- /dev/null +++ b/dev/log/issues/199/pulls/200/upstream/js-template-artipacked.md @@ -0,0 +1,126 @@ +`actions/checkout` leaves the job token in `.git/config` unless it is told not +to. In this template 25 of the 27 checkouts leave it there, and the workflow +that is supposed to catch exactly that cannot report it. + +## The blind spot + +`.github/workflows/workflows.yml` runs: + +```yaml + - uses: zizmorcore/zizmor-action@v0.6.2 + with: + advanced-security: false + annotations: true + config: .github/zizmor.yml + min-confidence: medium +``` + +zizmor's `artipacked` audit reports at **Low** confidence, so +`min-confidence: medium` filters every instance out. Run against this +repository's own workflows with its own config: + +```console +$ zizmor --config .github/zizmor.yml --min-confidence medium .github/workflows +58 findings (33 ignored, 22 suppressed, 3 safe fixes): 0 informational, 3 low, 0 medium, 0 high + +$ zizmor --config .github/zizmor.yml --min-confidence low .github/workflows +58 findings (1 ignored, 22 suppressed, 3 safe fixes, 32 unsafe fixes): 7 informational, 28 low, 0 medium, 0 high + +$ zizmor --config .github/zizmor.yml --min-confidence low .github/workflows | grep -c 'help\[artipacked\]' +25 +``` + +(zizmor 1.30.0. The three findings still visible at `medium` are the +`self-repository` ones from #155.) + +Sample: + +``` +help[artipacked]: credential persistence through GitHub Actions artifacts + --> .github/workflows/links.yml:43:9 + | +43 | - uses: actions/checkout@v6 + | ^^^^^^^^^^^^^^^^^^^^^^^^^ does not set persist-credentials: false + | + = note: audit confidence → Low + = note: this finding has an auto-fix +``` + +## Where + +Every checkout except the two in `workflows.yml`: + +| Workflow | Jobs | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `release.yml` | `detect-changes`, `test-compilation`, `check-file-line-limits`, `version-check`, `changeset-check`, `lint`, `test`, `docker-build`, `validate-docs`, `docker-publish-config`, `docker-publish-build`, `docker-publish`, `pipeline-status` (read-only) and `release`, `instant-release`, `changeset-pr` (`contents: write`) | +| `security.yml` | `codeql`, `dependency-review`, `npm-audit` | +| `example-app.yml` | `web-build`, `desktop-package`, `android-build`, `ios-build`, `preview-regen` | +| `links.yml` | `link-checker` | + +The Rust template is the counter-example: there, every checkout sets +`persist-credentials: false` except the two jobs that actually push +(`auto-release`, `manual-release` in `release.yml`), which is what a deliberate +decision looks like. + +## Why it matters + +The read-only jobs are the ones worth fixing first, and `lint` and `test` most +of all: both check out a pull-request branch, run `npm install` on it, and then +run that branch's code. A `postinstall` script or a compromised transitive +dependency can read `.git/config` and use the token for as long as the job runs; +anything that uploads the workspace (or a `.git`-containing subdirectory) as an +artifact publishes it outright. That is the whole point of the `artipacked` +audit, and this repository has switched it off by accident. + +## Reproduction + +```console +$ git clone https://github.com/link-foundation/js-ai-driven-development-pipeline-template +$ cd js-ai-driven-development-pipeline-template +$ pipx run zizmor==1.30.0 --config .github/zizmor.yml --min-confidence medium .github/workflows # 0 artipacked +$ pipx run zizmor==1.30.0 --config .github/zizmor.yml --min-confidence low .github/workflows # 25 artipacked +``` + +Or inside a job, without zizmor: + +```yaml + - uses: actions/checkout@v6 + - run: git config --get http.https://github.com/.extraheader # prints AUTHORIZATION: basic *** +``` + +## Workaround + +Add `persist-credentials: false` to the checkouts by hand; the audit that would +have found the next one stays off. + +## Suggested fix + +1. Set `persist-credentials: false` on every checkout in a job that does not + push: + + ```diff + - uses: actions/checkout@v6 + + with: + + persist-credentials: false + ``` + + `zizmor --config .github/zizmor.yml --min-confidence low --fix=all + .github/workflows` applies this, but its fixes are `unsafe` here precisely + because of the three writer jobs — review those three (`release`, + `instant-release`, `changeset-pr`) and keep their credentials. + +2. Then lower the workflow's threshold so a new checkout cannot re-introduce it: + + ```diff + config: .github/zizmor.yml + - min-confidence: medium + + min-confidence: low + ``` + + After step 1 that leaves ~10 findings, all in the informational/low bucket + and each either fixable or suppressible with a `# zizmor: ignore[...]` + comment that records the reason — which is strictly better than a threshold + that hides an entire audit. + +Found while auditing CI/CD against this template for +link-foundation/command-stream#199. diff --git a/dev/log/issues/199/pulls/200/upstream/js-template-lycheeignore-paths.md b/dev/log/issues/199/pulls/200/upstream/js-template-lycheeignore-paths.md new file mode 100644 index 00000000..e974303f --- /dev/null +++ b/dev/log/issues/199/pulls/200/upstream/js-template-lycheeignore-paths.md @@ -0,0 +1,100 @@ +`links.yml` runs the link checker for changes to `**.md`, `**.html` and the +workflow file itself: + +```yaml +# .github/workflows/links.yml +on: + push: + branches: [main] + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' + pull_request: + types: [opened, synchronize, reopened] + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' +``` + +Two inputs the job actually reads are missing from both lists: + +- `.lycheeignore`, which the lychee step loads on every run, +- `scripts/check-web-archive.mjs`, which the job executes when lychee reports a + broken link. + +The Rust template lists all four ([`rust-ai-driven-development-pipeline-template/.github/workflows/links.yml`](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template/blob/main/.github/workflows/links.yml)): + +```yaml + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' + - '.lycheeignore' + - 'scripts/check-web-archive.mjs' + - 'scripts/check-web-archive.test.mjs' + - 'scripts/fixtures/lychee-report.md' +``` + +## Why it matters + +`.lycheeignore` is the documented escape hatch for a false positive — the +failure message the workflow prints says so itself: + +``` +echo " c. Add the URL to .lycheeignore if it is a known false positive." +``` + +Following that instruction does not work on a pull request whose only change is +the ignore entry. `Check Links` does not start, so the red result stays as it +was: with the job listed as a required check the pull request cannot go green +from the branch at all, and without it the author sees a stale failure and has +to push an unrelated `.md` edit, or run the workflow by hand, to clear it. + +The same gap hides a real regression: `scripts/check-web-archive.mjs` runs +inside this workflow, and a change to it does not trigger the workflow that +runs it. + +## Reproduction + +1. On a branch, add a line to `.lycheeignore` and change nothing else. +2. Open a pull request. +3. `Check Links` is not among the checks — the `paths:` filter matched nothing. + +## Workaround + +Push a whitespace change to any `.md` file in the same pull request, or trigger +the workflow manually through `workflow_dispatch`. + +## Suggested fix + +Add the missing inputs to both `paths:` lists, as the Rust template already +does: + +```diff + push: + branches: [main] + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' ++ - '.lycheeignore' ++ - 'scripts/check-web-archive.mjs' + pull_request: + types: [opened, synchronize, reopened] + paths: + - '**.md' + - '**.html' + - '.github/workflows/links.yml' ++ - '.lycheeignore' ++ - 'scripts/check-web-archive.mjs' +``` + +`tests/links-workflow.test.js` is a natural place to keep it from drifting +again — it already parses this workflow, so an assertion that every file the +job reads appears in both `paths:` lists (and that the two lists are equal) +would fail today. + +Found while auditing CI/CD against this template for +link-foundation/command-stream#199. diff --git a/dev/log/issues/199/pulls/200/upstream/js-template-use-m-timeout.md b/dev/log/issues/199/pulls/200/upstream/js-template-use-m-timeout.md new file mode 100644 index 00000000..693e0f0f --- /dev/null +++ b/dev/log/issues/199/pulls/200/upstream/js-template-use-m-timeout.md @@ -0,0 +1,151 @@ +## Summary + +`scripts/use-module.mjs`'s `loadUse()` fetches `https://unpkg.com/use-m/use.js` with a bare `fetch()` — **no deadline, no retry** — and seven release scripts call it (through `loadCommandStream()`) at **module scope**, outside their own `main()`/`try`. When the CDN is unreachable or slow, the script dies during module initialisation: it prints nothing, writes nothing to `GITHUB_OUTPUT`, and the job's only diagnostic is `TypeError: fetch failed`, which names neither the CDN nor the URL. + +That turns a third-party outage into what looks like a defect in the release logic. In `link-foundation/command-stream` (which uses this template's scripts) the same shape made the publish suite fail intermittently with + +``` +Expected to contain: "published=true" +Received: "" +``` + +— two runs of the same unchanged suite differing only in whether unpkg answered. + +## Where + +* `scripts/use-module.mjs:113` — `const response = await fetchImpl(url);` (no `signal`, no retry) +* Module-scope callers, all of which die before their first log line: + * `scripts/changeset-version.mjs:30` + * `scripts/create-manual-changeset.mjs:22` + * `scripts/instant-version-bump.mjs:36` + * `scripts/format-github-release.mjs:23` + * `scripts/format-release-notes.mjs:33` + * `scripts/publish-to-npm.mjs:38` + * `scripts/version-and-commit.mjs:31` + * (`scripts/setup-npm.mjs:257` is inside a function, so it is the one that can report the failure itself) + +Checked at `7ae16b0` (0.11.28). + +## Reproduction 1 — unreachable CDN: the message names nothing + +`203.0.113.0/24` is TEST-NET-3 (RFC 5737), guaranteed never routable, so it fails the way a real outage fails. + +```js +// repro.mjs +import { loadUse } from './scripts/use-module.mjs'; +const started = Date.now(); +try { + await loadUse({ url: 'https://203.0.113.1/use-m/use.js' }); +} catch (error) { + console.log(`elapsed ${Date.now() - started}ms`); + console.log(`name ${error.name}`); + console.log(`message ${error.message}`); + console.log(`cause ${error.cause?.message ?? error.cause}`); +} +``` + +``` +$ node repro.mjs +elapsed 10620ms +name TypeError +message fetch failed +cause Connect Timeout Error (attempted address: 203.0.113.1:443, timeout: 10000ms) +``` + +The 10 s bound comes from undici's connect timeout, not from this code, and the `cause` is only visible because the reproduction prints it — a script that dies at module scope shows `TypeError: fetch failed` and a stack inside `use-module.mjs`. + +## Reproduction 2 — stalled CDN: nothing bounds the wait + +A connect timeout does not cover a server that accepts the connection and then never answers. undici's `headersTimeout` default is 300 s, so one such fetch can burn five minutes of a job's `timeout-minutes`: + +```js +// stall.mjs +import { createServer } from 'node:http'; +import { loadUse } from './scripts/use-module.mjs'; + +const server = createServer(() => {}); // accepts, never responds +await new Promise((r) => server.listen(0, '127.0.0.1', r)); +const url = `http://127.0.0.1:${server.address().port}/use-m/use.js`; + +const started = Date.now(); +const outcome = await Promise.race([ + loadUse({ url }).then(() => 'resolved', (e) => `rejected: ${e.message}`), + new Promise((r) => setTimeout(() => r('still waiting'), 25000)), +]); +console.log(`after ${Date.now() - started}ms: ${outcome}`); +process.exit(0); +``` + +``` +$ node stall.mjs +after 25047ms: still waiting +``` + +## Workaround + +Re-run the job — the failure is transient — and, in test suites that spawn these scripts, probe `https://unpkg.com/use-m/use.js` (not only `npm view`) before asserting: npm's registry and unpkg are different services that fail independently, so a reachable registry does not mean the dependency the script needs at startup is reachable. + +## Suggested fix + +Give the load a deadline and a bounded retry, and make the final error say what failed. `AbortSignal.timeout()` is available on every runtime these scripts target (Node 18+, Bun): + +```diff ++/** Per-attempt deadline: a stalled connect must not consume the job's budget. */ ++export const DEFAULT_TIMEOUT_MS = 15000; ++/** Total attempts, including the first one. */ ++export const DEFAULT_ATTEMPTS = 3; ++/** Delay before the second attempt; doubled for each attempt after it. */ ++export const DEFAULT_RETRY_DELAY_MS = 2000; ++ + export async function loadUse(options = {}) { +- const { fetchImpl = fetch, url = USE_M_URL } = options; ++ const { ++ fetchImpl = fetch, ++ url = USE_M_URL, ++ attempts = DEFAULT_ATTEMPTS, ++ timeoutMs = DEFAULT_TIMEOUT_MS, ++ retryDelayMs = DEFAULT_RETRY_DELAY_MS, ++ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), ++ } = options; + if (cachedUse && !options.fetchImpl) { + return cachedUse; + } +- const response = await fetchImpl(url); +- ... ++ let lastError; ++ for (let attempt = 1; attempt <= attempts; attempt += 1) { ++ try { ++ const use = await fetchOnce({ fetchImpl, url, timeoutMs }); // signal: AbortSignal.timeout(timeoutMs) ++ debug('loaded use-m', { url, attempt }); ++ if (!options.fetchImpl) cachedUse = use; ++ return use; ++ } catch (error) { ++ lastError = error; ++ debug('use-m load attempt failed', { url, attempt, attempts, error: error?.message }); ++ if (attempt < attempts) await sleep(retryDelayMs * 2 ** (attempt - 1)); ++ } ++ } ++ throw new Error( ++ `Failed to load use-m from ${url} after ${attempts} attempt(s): ` + ++ `${lastError?.message ?? String(lastError)}. This is a network dependency ` + ++ 'of the release scripts, not a defect in the published package; re-run ' + ++ 'the job when the CDN answers again.', ++ { cause: lastError } ++ ); + } +``` + +With that in place the same reproduction reports: + +``` +Error: Failed to load use-m from https://203.0.113.1/use-m/use.js after 2 attempt(s): The operation was aborted due to timeout. This is a network dependency of the release scripts, not a defect in the published package; re-run the job when the CDN answers again. +``` + +Two follow-ups worth doing in the same change: + +1. **Move the module-scope `await loadCommandStream()` calls inside `main()`** (as `setup-npm.mjs` already does), so a load failure is caught by the script's own error handling and the script can still write `published=false` / an explanatory line to `GITHUB_OUTPUT` instead of dying silently. +2. Keep the retry bounded so the worst case stays well inside the job's `timeout-minutes` (3 × 15 s + backoff ≈ 51 s). + +## Reference implementation + +`link-foundation/command-stream` ships this as [`js/scripts/use-m-loader.mjs`](https://github.com/link-foundation/command-stream/blob/issue-199-32c07917fc87/js/scripts/use-m-loader.mjs), with unit tests in [`js/tests/use-m-loader.test.mjs`](https://github.com/link-foundation/command-stream/blob/issue-199-32c07917fc87/js/tests/use-m-loader.test.mjs) (deadline present on every attempt, retry-then-succeed, bounded attempts, error page reported by status instead of eval-ed, cause preserved) and a runnable before/after reproduction in [`experiments/publish-cdn-unreachable.mjs`](https://github.com/link-foundation/command-stream/blob/issue-199-32c07917fc87/experiments/publish-cdn-unreachable.mjs). Found while working on link-foundation/command-stream#199. diff --git a/dev/log/issues/199/pulls/200/workflows/check-language-parity.sh b/dev/log/issues/199/pulls/200/workflows/check-language-parity.sh new file mode 100644 index 00000000..e2db50a4 --- /dev/null +++ b/dev/log/issues/199/pulls/200/workflows/check-language-parity.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Language parity check. +# +# command-stream ships two implementations that must stay in lock-step: the +# JavaScript library under js/src/** and the Rust library under rust/src/**. +# This script fails when a pull request changes one language's source without +# touching the other's, so that behavioral changes are always made in both +# languages (see issue #155 review feedback). +# +# Escape hatch: add the `parity-exempt` label to the PR for changes that are +# legitimately single-language (the workflow skips this check when the label is +# present). +# +# Environment: +# BASE_REF - the base branch to diff against (default: main). In GitHub +# Actions this is github.base_ref. +# +# Usage (locally): +# BASE_REF=main bash .github/scripts/check-language-parity.sh +set -euo pipefail + +BASE_REF="${BASE_REF:-main}" + +# Make sure the base branch is available locally, then resolve a ref we can diff +# against. Prefer the remote-tracking ref; fall back to the bare branch name. +git fetch --no-tags origin "${BASE_REF}" >/dev/null 2>&1 || true +if git rev-parse --verify --quiet "origin/${BASE_REF}" >/dev/null; then + BASE="origin/${BASE_REF}" +elif git rev-parse --verify --quiet "${BASE_REF}" >/dev/null; then + BASE="${BASE_REF}" +else + echo "::warning::Could not resolve base ref '${BASE_REF}'; skipping parity check." + exit 0 +fi + +MERGE_BASE="$(git merge-base "${BASE}" HEAD 2>/dev/null || echo "${BASE}")" +CHANGED="$(git diff --name-only "${MERGE_BASE}" HEAD)" + +echo "Comparing against ${BASE} (merge-base ${MERGE_BASE})" +echo "Changed files:" +echo "${CHANGED}" | sed 's/^/ /' + +js_changed=false +rust_changed=false +while IFS= read -r f; do + [ -z "${f}" ] && continue + case "${f}" in + js/src/*) js_changed=true ;; + rust/src/*) rust_changed=true ;; + esac +done < console.log('Module loads successfully in Node.js ${{ matrix.node-version }}')) + .catch((error) => { + console.error('Module failed to load:', error.message); + process.exit(1); + }); + " + node -e " + const commandStream = require('./js/src/\$.cjs'); + if (typeof commandStream !== 'function') { + console.error('CommonJS entry did not export a callable \$'); + process.exit(1); + } + console.log('CommonJS entry loads successfully in Node.js ${{ matrix.node-version }}'); + " + node --test js/tests/node-terminal-artifacts.mjs + node --test js/tests/node-commonjs-entry.mjs + + release: + name: Release JavaScript package + needs: [lint, test] + # Required because lint/test depend on the pull-request-only changeset-check + # job, which is skipped on push events. + if: | + always() && !cancelled() && + github.ref == 'refs/heads/main' && + github.event_name == 'push' && + needs.lint.result == 'success' && + needs.test.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + working-directory: js + run: bun install + + - name: Update npm for OIDC trusted publishing + working-directory: js + run: bun scripts/setup-npm.mjs + + - name: Check for changesets + id: check_changesets + working-directory: js + run: | + CHANGESET_COUNT=$(find .changeset -name "*.md" ! -name "README.md" | wc -l) + echo "Found $CHANGESET_COUNT JavaScript changeset file(s)" + echo "has_changesets=$([[ $CHANGESET_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT + echo "changeset_count=$CHANGESET_COUNT" >> $GITHUB_OUTPUT + + - name: Check if release is needed + id: check_release + working-directory: js + env: + HAS_CHANGESETS: ${{ steps.check_changesets.outputs.has_changesets }} + run: bun scripts/check-release-needed.mjs + + - name: Merge multiple changesets + if: steps.check_changesets.outputs.has_changesets == 'true' && fromJSON(steps.check_changesets.outputs.changeset_count) > 1 + working-directory: js + run: bun scripts/merge-changesets.mjs + + - name: Version package and commit to main + if: steps.check_changesets.outputs.has_changesets == 'true' + id: version + working-directory: js + run: bun scripts/version-and-commit.mjs --mode changeset + + - name: Publish to npm + # Run if a changeset bumped+committed the version, if a previous attempt + # already committed it (re-run safety), or if check-release-needed found + # the current package.json version is not on npm. `current_unpublished` + # is the authoritative self-heal trigger: it fires whether or not a + # changeset is present, so it also covers the #166 "failed to do any + # deploy" restart, where a changeset existed locally but the bump had + # already been consumed on origin/main, leaving v0.10.2 stranded. + if: >- + steps.version.outputs.version_committed == 'true' || + steps.version.outputs.already_released == 'true' || + steps.check_release.outputs.current_unpublished == 'true' + id: publish + working-directory: js + run: bun scripts/publish-to-npm.mjs --should-pull + + - name: Create JavaScript GitHub Release + if: steps.publish.outputs.published == 'true' + working-directory: js + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix js-v + + - name: Format JavaScript GitHub release notes + if: steps.publish.outputs.published == 'true' + working-directory: js + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix js-v + + - name: Verify npm availability + # Guards against the #166 false positive: a release/tag must correspond + # to a version that is actually installable from npm. + if: steps.publish.outputs.published == 'true' + working-directory: js + run: bun scripts/wait-for-npm.mjs --release-version "${{ steps.publish.outputs.published_version }}" + + instant-release: + name: Instant JavaScript release + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + id-token: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24.x' + registry-url: 'https://registry.npmjs.org' + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + working-directory: js + run: bun install + + - name: Update npm for OIDC trusted publishing + working-directory: js + run: bun scripts/setup-npm.mjs + + - name: Version package and commit to main + id: version + working-directory: js + run: bun scripts/version-and-commit.mjs --mode instant --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Publish to npm + if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' + id: publish + working-directory: js + run: bun scripts/publish-to-npm.mjs + + - name: Create JavaScript GitHub Release + if: steps.publish.outputs.published == 'true' + working-directory: js + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun scripts/create-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --tag-prefix js-v + + - name: Format JavaScript GitHub release notes + if: steps.publish.outputs.published == 'true' + working-directory: js + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bun scripts/format-github-release.mjs --release-version "${{ steps.publish.outputs.published_version }}" --repository "${{ github.repository }}" --commit-sha "${{ github.sha }}" --tag-prefix js-v + + - name: Verify npm availability + # Guards against the #166 false positive: a release/tag must correspond + # to a version that is actually installable from npm. + if: steps.publish.outputs.published == 'true' + working-directory: js + run: bun scripts/wait-for-npm.mjs --release-version "${{ steps.publish.outputs.published_version }}" + + changeset-pr: + name: Create JavaScript changeset PR + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changeset-pr' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + working-directory: js + run: bun install + + - name: Create changeset file + working-directory: js + run: bun scripts/create-manual-changeset.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Format changeset with Prettier + working-directory: js + run: bunx prettier --write ".changeset/*.md" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: add changeset for manual JavaScript ${{ github.event.inputs.bump_type }} release' + branch: changeset-js-manual-release-${{ github.run_id }} + delete-branch: true + title: 'chore: manual JavaScript ${{ github.event.inputs.bump_type }} release' + body: | + ## Manual JavaScript Release Request + + This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** npm release. + + ### Release Details + - Type: ${{ github.event.inputs.bump_type }} + - Description: ${{ github.event.inputs.description || 'Manual JavaScript release' }} + - Triggered by: @${{ github.actor }} diff --git a/dev/log/issues/199/pulls/200/workflows/parity.yml b/dev/log/issues/199/pulls/200/workflows/parity.yml new file mode 100644 index 00000000..5d1a136d --- /dev/null +++ b/dev/log/issues/199/pulls/200/workflows/parity.yml @@ -0,0 +1,37 @@ +name: Language parity check + +# Ensure behavioral changes are made in both the JavaScript (js/src/**) and the +# Rust (rust/src/**) implementations. A PR that changes one without the other +# fails this check unless it carries the `parity-exempt` label. +# +# See issue #155 review feedback: "double check that all features that are +# supported in JavaScript are fully supported in Rust and we have CI/CD rules, +# that check that we do changes in both languages always". + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + parity: + name: JS/Rust source parity + runs-on: ubuntu-latest + timeout-minutes: 10 + # Skip entirely when the PR is explicitly marked as a single-language change. + if: ${{ !contains(github.event.pull_request.labels.*.name, 'parity-exempt') }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check JavaScript/Rust source parity + env: + BASE_REF: ${{ github.base_ref }} + run: bash .github/scripts/check-language-parity.sh diff --git a/dev/log/issues/199/pulls/200/workflows/rust.yml b/dev/log/issues/199/pulls/200/workflows/rust.yml new file mode 100644 index 00000000..d4dbf15e --- /dev/null +++ b/dev/log/issues/199/pulls/200/workflows/rust.yml @@ -0,0 +1,355 @@ +name: Rust checks and release + +on: + push: + branches: + - main + paths: + - 'rust/**' + - '.github/workflows/rust.yml' + - 'README.md' + - 'LICENSE' + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'rust/**' + - '.github/workflows/rust.yml' + - 'README.md' + - 'LICENSE' + workflow_dispatch: + inputs: + release_mode: + description: 'Manual release mode' + required: true + type: choice + default: 'instant' + options: + - instant + - changelog-pr + bump_type: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + description: + description: 'Release description (optional)' + required: false + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +env: + CARGO_TERM_COLOR: always + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} + CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} + +jobs: + changelog: + name: Rust changelog fragment check + runs-on: ubuntu-latest + timeout-minutes: 10 + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Check for changelog fragments + env: + GITHUB_BASE_REF: ${{ github.base_ref }} + run: rust-script rust/scripts/check-changelog-fragment.rs + + lint: + name: Lint and format Rust + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [changelog] + if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Check formatting + working-directory: rust + run: cargo fmt --all -- --check + + - name: Run Clippy + working-directory: rust + run: cargo clippy --all-targets --all-features + + test: + name: Test Rust (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + needs: [changelog] + if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run tests + working-directory: rust + run: cargo test --all-features --verbose + + - name: Run doc tests + working-directory: rust + run: cargo test --doc --all-features --verbose + + scripts: + name: Test Rust release scripts + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: [changelog] + if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success') + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-scripts-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-scripts- + + - name: Install rust-script + run: cargo install rust-script + + - name: Run release script unit tests + # Run the inline `#[cfg(test)]` suites for every rust-script under + # rust/scripts/. `cargo test` only covers the library crate, so without + # this step the release-script regression tests (e.g. the rebase + # ordering guard) never execute in CI. + run: | + set -euo pipefail + status=0 + for f in rust/scripts/*.rs; do + if grep -q 'cfg(test)' "$f"; then + echo "::group::rust-script --test $f" + rust-script --test "$f" || status=1 + echo "::endgroup::" + fi + done + exit $status + + build: + name: Build Rust package + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [lint, test] + if: always() && !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build- + + - name: Build release + working-directory: rust + run: cargo build --release --verbose + + - name: Check package + working-directory: rust + run: cargo package --allow-dirty + + release: + name: Release Rust crate + needs: [lint, test, scripts, build] + # Required because lint/test depend on the pull-request-only changelog job, + # which is skipped on push events. + if: | + always() && !cancelled() && + github.ref == 'refs/heads/main' && + github.event_name == 'push' && + needs.lint.result == 'success' && + needs.test.result == 'success' && + needs.scripts.result == 'success' && + needs.build.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Determine bump type + id: bump + run: rust-script rust/scripts/get-bump-type.rs + + - name: Check whether Rust release is needed + id: release_needed + env: + HAS_FRAGMENTS: ${{ steps.bump.outputs.has_fragments }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: rust-script rust/scripts/check-release-needed.rs --tag-prefix rust-v + + - name: Version Rust crate and commit to main + if: steps.release_needed.outputs.should_release == 'true' + id: version + run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ steps.bump.outputs.bump_type }}" --tag-prefix rust-v --release-label Rust + + - name: Read Rust release version + if: steps.release_needed.outputs.should_release == 'true' + id: current_version + run: rust-script rust/scripts/get-version.rs + + - name: Publish to crates.io + if: steps.version.outputs.version_committed == 'true' || steps.release_needed.outputs.skip_bump == 'true' + id: publish_crate + run: rust-script rust/scripts/publish-crate.rs + + - name: Create Rust GitHub Release + if: steps.publish_crate.outputs.publish_result == 'success' || steps.publish_crate.outputs.publish_result == 'already_exists' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: rust-script rust/scripts/create-github-release.rs --release-version "${{ steps.version.outputs.new_version || steps.current_version.outputs.version }}" --repository "${{ github.repository }}" --tag-prefix rust-v --language Rust --release-label Rust + + - name: Wait for crate availability + if: steps.publish_crate.outputs.publish_result == 'success' + run: rust-script rust/scripts/wait-for-crate.rs --version "${{ steps.version.outputs.new_version || steps.current_version.outputs.version }}" + + instant-release: + name: Instant Rust release + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'instant' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Version Rust crate and commit to main + id: version + run: rust-script rust/scripts/version-and-commit.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" --tag-prefix rust-v --release-label Rust + + - name: Publish to crates.io + if: steps.version.outputs.version_committed == 'true' + id: publish_crate + run: rust-script rust/scripts/publish-crate.rs + + - name: Create Rust GitHub Release + if: steps.publish_crate.outputs.publish_result == 'success' || steps.publish_crate.outputs.publish_result == 'already_exists' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: rust-script rust/scripts/create-github-release.rs --release-version "${{ steps.version.outputs.new_version }}" --repository "${{ github.repository }}" --tag-prefix rust-v --language Rust --release-label Rust + + - name: Wait for crate availability + if: steps.publish_crate.outputs.publish_result == 'success' + run: rust-script rust/scripts/wait-for-crate.rs --version "${{ steps.version.outputs.new_version }}" + + changelog-pr: + name: Create Rust changelog PR + if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changelog-pr' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install rust-script + run: cargo install rust-script + + - name: Create changelog fragment + run: rust-script rust/scripts/create-changelog-fragment.rs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: add changelog fragment for manual Rust ${{ github.event.inputs.bump_type }} release' + branch: changelog-rust-manual-release-${{ github.run_id }} + delete-branch: true + title: 'chore: manual Rust ${{ github.event.inputs.bump_type }} release' + body: | + ## Manual Rust Release Request + + This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** crates.io release. + + ### Release Details + - Type: ${{ github.event.inputs.bump_type }} + - Description: ${{ github.event.inputs.description || 'Manual Rust release' }} + - Triggered by: @${{ github.actor }} diff --git a/docs/CI-CD.md b/docs/CI-CD.md new file mode 100644 index 00000000..aa39acd9 --- /dev/null +++ b/docs/CI-CD.md @@ -0,0 +1,174 @@ +# CI/CD + +Seven workflows guard this repository. Everything below is enforced by +`js/tests/workflow-hygiene.test.mjs` and `js/tests/repository-layout.test.mjs`, +which parse the workflow files themselves — if a job drifts from what this +document describes, those tests fail. + +## Workflows + +| Workflow | Runs on | Jobs | +| --------------- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `js.yml` | push to `main`, pull request, dispatch | changeset check, lint and format, test (bun + node 20/22/24 × ubuntu/macos/windows), release, instant release, changeset PR | +| `rust.yml` | push to `main`, pull request, dispatch | changelog fragment check, lint and format, test (ubuntu/macos/windows), release scripts, build, release, instant release, changelog PR | +| `parity.yml` | pull request | JS/Rust source parity | +| `workflows.yml` | push to `main`, pull request, dispatch | actionlint, zizmor | +| `security.yml` | push to `main`, pull request, **weekly**, dispatch | CodeQL, dependency review, npm/bun/cargo audit, secret scan | +| `quality.yml` | push to `main`, pull request, dispatch | formatting of every tracked file, documentation validation, workflow invariants | +| `links.yml` | **weekly**, dispatch | external link check (lychee) | + +Every workflow except `quality.yml` and `links.yml` is scoped by a `paths:` +filter. The union of those filters is not the repository, which is why +`quality.yml` has no filter at all: a pull request that only touches `docs/**` +still runs the three checks that read the whole tree. + +## Invariants + +- **Warnings are errors.** `eslint --max-warnings 0`; `RUSTFLAGS`/`RUSTDOCFLAGS` + are `-Dwarnings` and clippy runs with `-- -D warnings`, because `RUSTFLAGS` + does not reach clippy's own lints. `cargo doc --no-deps` catches the + rustdoc-only lints that neither clippy nor `cargo test --doc` reports. +- **actionlint runs from `docker://rhysd/actionlint`,** not from a bare binary. + The Docker image bundles shellcheck and pyflakes; a binary without shellcheck + on `PATH` silently skips every `run:` block and still exits 0. +- **zizmor runs at `--min-confidence low`, not `medium`.** `artipacked` — a + checkout that leaves the job token in `.git/config` — is a Low-confidence + audit, so `medium` hides every one of them. At `low` a checkout either sets + `persist-credentials: false` or is one of the six release jobs that pushes + with that credential and says so inline; the hygiene test asserts both, and + the suppression count, so a seventh cannot appear by copy-paste. +- **zizmor audits `.github/workflows` only.** Its default input is `.`, which + also collects the archived copies of other repositories' workflows under + `docs/case-studies/**/templates/**`. Those never run here. +- **No secret is declared in a workflow-level `env:`.** That block is inherited + by every job in the file, including the ones that compile pull-request code. + Publishing credentials belong on the publishing job. +- **Only jobs that write hold a non-cancellable concurrency group.** Read-only + checks use a cancellable `check-*` group; jobs with `contents: write` share + `main-writer-${{ github.repository }}-main` and are never cancelled halfway. +- **Release jobs gate on `!cancelled()`, not `always()`.** A job with `needs:` + is skipped when a dependency is skipped, and _any_ non-`success()` condition + lifts that — so `always()` adds nothing except the risk of running after a + real failure. +- **A pull-request job that reads the tree merges the base branch first.** + A pull-request run checks out `refs/pull/N/merge`, computed when the branch + was last synchronised; if `main` moved since, the checks pass on a combination + that will not exist after the merge. + `.github/scripts/simulate-fresh-merge.sh` closes that window and turns a merge + conflict into a clear failure. Five jobs are exempt, each with the reason + recorded next to it and in the hygiene test: `changeset-check`, the Rust + changelog checks and `parity` are diff-based, `dependency-review` compares two + SHAs through the API, and CodeQL uploads results keyed to the checked-out + commit — GitHub rejects a merge commit created on the runner. +- **A workflow's `push:` and `pull_request:` path filters are identical.** Two + lists that drift mean a check runs on the pull request and then not on the + merge to `main`, or the reverse, so a green pull request stops predicting a + green `main`. +- **Every quality gate that ships is invoked.** `rust/scripts/` held a + file-size, a crate-size and a version-modification check that no workflow ran; + the hygiene test now fails when a script under `rust/scripts/` is neither + referenced by a workflow nor listed as deliberately unwired. +- **The tree is scanned for committed credentials.** secretlint runs over every + file on each pull request; `.secretlintrc.json` holds the rule set and + `.secretlintignore` only the generated trees. Neither CodeQL nor the audit + jobs look for secrets. +- **Documentation is validated like code.** `js/tests/docs-validation.test.mjs` + enforces a 2500-line ceiling, resolves every relative link, and checks that + the documents other automation points readers at still carry their sections. + External links are deliberately out of scope for that test; they are checked + separately, below. +- **External links are checked weekly, not on pull requests.** Both pipeline + templates run lychee as a pull-request gate. The same job here reports 20 + errors on an unmodified tree, and all 20 are links that are correct in the + document and unreachable from a runner: npmjs.com answers `403` to any + non-browser client, and GitHub serves the stargazers list and the `/settings/` + pages only to a signed-in session. So the check is split by who can break the + link. Relative links — the only ones a change here can break — are resolved + offline on every pull request; the network is fetched by `links.yml` on a + schedule, where a failure means a link that used to work has stopped working + and blocks no merge. `.lycheeignore` carries the known-unreachable URLs, one + commented entry each, and the hygiene test rejects an uncommented one. +- **The duplication gate has a threshold just above the current measurement.** + jscpd's `format` is the list of _languages_ to analyse, not the reporter; it + read `"console"`, matched no file and passed in under a millisecond. With the + language list corrected the tree measures 4.84 % of lines and 5.55 % of tokens + duplicated, so the threshold is `6`: high enough not to fail on code that was + already there, low enough that adding duplication fails the job. + `js/tests/duplication-check.test.mjs` pins both the language list and the + threshold range. +- **Every network dependency of the release scripts loads with a deadline and a + retry.** The scripts carry no `package.json` dependencies: they fetch `use-m` + from `https://unpkg.com/use-m/use.js` and eval it. Done inline, at module + scope, that load has no deadline, no retry and no diagnostics, so a CDN blip + killed the script during module initialisation — before its first log line and + before anything reached `GITHUB_OUTPUT` — and the job reported a publish + defect. `js/scripts/use-m-loader.mjs` is the single loader: a 15 s deadline per + attempt, three attempts with exponential backoff, the HTTP status checked + before the eval (an error page is HTML, and eval-ing HTML blames this + repository for `Unexpected token '<'`), and a final error naming the URL, the + attempts and the cause. Every caller uses it — the eleven release scripts and + the `claude-profiles.mjs` CLI — and `js/tests/use-m-loader.test.mjs` scans the + whole tree to assert that nothing fetches use.js inline again. +- **Lint and format configuration lives at the repository root.** eslint and + prettier treat the directory holding their config as the project base path; + while these files lived in `js/`, root-level JavaScript was outside that path + and silently unlintable. `js/eslint.config.js` remains the rule set the root + copies re-export, and `js.yml`'s trigger lists the root-level files eslint + reaches (`eslint.config.js`, `claude-profiles.mjs`, `experiments/**`) so a + lint error in them cannot first surface on an unrelated pull request. + +## Required repository settings + +Two things a workflow cannot configure. Both are open: + +### Branch protection on `main` + +`GET /repos/link-foundation/command-stream/branches/main/protection` returns +`404 Branch not protected` and the ruleset list is empty, so a pull request with +red checks can still be merged. Protect `main` and mark the lint, test and +security jobs as required. + +### Dependency graph + +`actions/dependency-review-action` needs the dependency graph, which is off for +this repository — the API reports no `dependency_graph` key and a `PATCH` to +enable it has no effect, so it is controlled at the organisation level. Enable +it at +. + +Until then the job probes the compare endpoint and skips with a warning rather +than failing on every pull request; it starts reviewing on its own once the +graph is on. Any other API status still fails the job. The npm, bun and cargo +audit jobs cover the committed lockfiles in the meantime. + +## Debugging a failed run + +The release scripts keep their tracing in the code with the default state +switched off, so a run that failed can be re-run with the tracing on and no code +change. Any of these enables it: + +- `CI_SCRIPTS_DEBUG=1` — the local switch, e.g. + `CI_SCRIPTS_DEBUG=1 bun js/scripts/check-release-needed.mjs`; +- `RUNNER_DEBUG=1` — set by GitHub's **Re-run all jobs with debug logging**; +- `ACTIONS_STEP_DEBUG=true` — the secret-gated workflow debug switch. + +Every line is prefixed with `::debug::`, so Actions renders it in the +collapsible debug stream and the main log stays readable. What it reports: the +publish, registry-verification and resolved-package decisions in +`publish-to-npm.mjs`, and one line per use-m load attempt including the URL and +the failure that caused a retry. `js/scripts/debug-print.mjs` is the +helper; it never throws, because tracing must not be the reason a script fails +(Deno denies `process.env` without `--allow-env`, and the denial surfaces on the +property read itself). + +## Releasing + +JavaScript uses changesets: add a `js/.changeset/*.md` entry describing the +change and its bump type, or the `Check for JavaScript changesets` job fails. +Rust uses changelog fragments: add `rust/changelog.d/YYYYMMDD_HHMMSS_*.md` with +`bump:` frontmatter, or `Rust changelog fragment check` fails. + +`js/scripts/publish-retry.mjs` treats an "already published" registry error as +success, including npm's E409 `Cannot publish over previously staged version` — +a slow-propagating publish used to be reported as a failed release even though +the version was live. diff --git a/docs/case-studies/issue-162/README.md b/docs/case-studies/issue-162/README.md index d4afff06..68e72858 100644 --- a/docs/case-studies/issue-162/README.md +++ b/docs/case-studies/issue-162/README.md @@ -114,8 +114,11 @@ passes with the fix. Patch release markers were added for both packages so the next merge to `main` has release input to consume: -- [release-job-skipped-by-gate.md](../../../js/.changeset/release-job-skipped-by-gate.md) -- [20260608_issue_162_release_job_skip.md](../../../rust/changelog.d/20260608_issue_162_release_job_skip.md) +- `js/.changeset/release-job-skipped-by-gate.md` +- `rust/changelog.d/20260608_issue_162_release_job_skip.md` + +Both files are gone from the tree: a release consumes its markers and folds them +into the changelogs, so they are named here rather than linked. ## Template Comparison diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..0e9d2eb6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,10 @@ +// ESLint resolves its configuration by walking up from the working directory, +// and treats the directory holding that file as the root of the linted project. +// Keeping the file here is what puts repository-root JavaScript — the +// experiments/ reproductions and claude-profiles.mjs — inside the lint scope; +// while the configuration lived in js/, those files could not be linted at all +// ("the file is ignored because it is located outside of the base path"). +// +// The rules themselves stay next to the code and the node_modules they import +// from, so this file only re-exports them. +export { default } from './js/eslint.config.js'; diff --git a/experiments/cd-edge.mjs b/experiments/cd-edge.mjs index 51fa7cbf..22888a19 100644 --- a/experiments/cd-edge.mjs +++ b/experiments/cd-edge.mjs @@ -1,6 +1,13 @@ import { $ } from '../js/src/$.mjs'; process.chdir('/tmp'); -async function t(cmd){ try{const r=await $`${{raw:cmd}}`; return `code=${r.code} out=${JSON.stringify((r.stdout||'').toString().trim())} err=${JSON.stringify((r.stderr||'').toString().trim())}`;}catch(e){return 'THROW '+e.message;}} +async function t(cmd) { + try { + const r = await $`${{ raw: cmd }}`; + return `code=${r.code} out=${JSON.stringify((r.stdout || '').toString().trim())} err=${JSON.stringify((r.stderr || '').toString().trim())}`; + } catch (e) { + return 'THROW ' + e.message; + } +} console.log('cd ~ ->', await t('cd ~'), 'cwd:', process.cwd()); process.chdir('/tmp'); console.log('cd ->', await t('cd'), 'cwd:', process.cwd()); diff --git a/experiments/cd-sh-comparison.mjs b/experiments/cd-sh-comparison.mjs index 64f3a302..30276692 100644 --- a/experiments/cd-sh-comparison.mjs +++ b/experiments/cd-sh-comparison.mjs @@ -10,13 +10,27 @@ try { await $`cd sub`; console.log('after cd sub, cwd=', process.cwd()); const r = await $`cd -`; - console.log('cd - stdout:', JSON.stringify(r.stdout), 'code:', r.code, 'cwd now:', process.cwd()); -} catch(e) { console.log('cd - error:', e.message); } + console.log( + 'cd - stdout:', + JSON.stringify(r.stdout), + 'code:', + r.code, + 'cwd now:', + process.cwd() + ); +} catch (e) { + console.log('cd - error:', e.message); +} console.log('=== Test 2: $PWD env var ==='); process.chdir(tmp); const pwdEnv = await $`echo $PWD`; -console.log('echo $PWD ->', JSON.stringify(pwdEnv.stdout), ' actual process.cwd:', process.cwd()); +console.log( + 'echo $PWD ->', + JSON.stringify(pwdEnv.stdout), + ' actual process.cwd:', + process.cwd() +); console.log('=== Test 3: subshell isolation (cd x); pwd ==='); process.chdir(tmp); @@ -25,7 +39,12 @@ console.log('subshell stdout:', JSON.stringify(sub.stdout)); console.log('process.cwd after subshell:', process.cwd()); console.log('=== Test 4: cwd option with cd ==='); -const r4 = await $({cwd: tmp})`cd sub && pwd`; -console.log('cwd-option cd sub && pwd ->', JSON.stringify(r4.stdout), 'code', r4.code); +const r4 = await $({ cwd: tmp })`cd sub && pwd`; +console.log( + 'cwd-option cd sub && pwd ->', + JSON.stringify(r4.stdout), + 'code', + r4.code +); process.chdir('/tmp'); diff --git a/experiments/env-expand.mjs b/experiments/env-expand.mjs index acc3cf76..a3d3dd4e 100644 --- a/experiments/env-expand.mjs +++ b/experiments/env-expand.mjs @@ -1,6 +1,8 @@ import { $ } from '../js/src/$.mjs'; process.chdir('/tmp'); for (const cmd of ['echo $HOME', 'echo $PWD', 'echo $OLDPWD', 'echo ~']) { - const r = await $`${{raw: cmd}}`.catch(e=>({stdout:'ERR '+e.message})); - console.log(cmd, '->', JSON.stringify((r.stdout||'').toString().trim())); + const r = await $`${{ raw: cmd }}`.catch((e) => ({ + stdout: 'ERR ' + e.message, + })); + console.log(cmd, '->', JSON.stringify((r.stdout || '').toString().trim())); } diff --git a/experiments/env2.mjs b/experiments/env2.mjs index ce4ba0c8..889dd915 100644 --- a/experiments/env2.mjs +++ b/experiments/env2.mjs @@ -1,8 +1,14 @@ import { $ } from '../js/src/$.mjs'; process.chdir('/tmp'); let r = await $`echo $HOME`; -console.log('virtual echo $HOME ->', JSON.stringify(r.stdout.toString().trim())); +console.log( + 'virtual echo $HOME ->', + JSON.stringify(r.stdout.toString().trim()) +); r = await $`/bin/echo $HOME`; console.log('/bin/echo $HOME ->', JSON.stringify(r.stdout.toString().trim())); r = await $`/bin/echo hi && /bin/echo $HOME`; -console.log('chained /bin/echo $HOME ->', JSON.stringify(r.stdout.toString().trim())); +console.log( + 'chained /bin/echo $HOME ->', + JSON.stringify(r.stdout.toString().trim()) +); diff --git a/experiments/fresh-merge-simulation.sh b/experiments/fresh-merge-simulation.sh new file mode 100644 index 00000000..8aaf76df --- /dev/null +++ b/experiments/fresh-merge-simulation.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# +# Exercises .github/scripts/simulate-fresh-merge.sh in a throwaway repository. +# +# Three cases, each asserted: +# 1. the branch already contains every commit on the base -> no-op, exit 0 +# 2. the base moved ahead with a compatible change -> merged, exit 0, +# base file present +# 3. the base moved ahead with a conflicting change -> exit 1 and an +# ::error:: annotation +# +# Usage: bash experiments/fresh-merge-simulation.sh +set -uo pipefail + +script="$(cd "$(dirname "$0")/.." && pwd)/.github/scripts/simulate-fresh-merge.sh" +root="$(mktemp -d)" +trap 'rm -rf "$root"' EXIT +failures=0 + +check() { # check + if [ "$2" = "$3" ]; then + echo "ok - $1" + else + echo "FAIL - $1 (expected exit $2, got $3)" + failures=$((failures + 1)) + fi +} + +setup() { # setup ; prints the clone path + local name="$1" + local origin="$root/$name-origin" clone="$root/$name" + git init -q --bare -b main "$origin" + git init -q -b main "$root/$name-seed" + ( + cd "$root/$name-seed" + git config user.email seed@example.com + git config user.name Seed + echo base > shared.txt + git add shared.txt + git commit -qm 'initial' + git branch -M main + git remote add origin "$origin" + git push -q origin main + ) + git clone -q "$origin" "$clone" + ( + cd "$clone" + git config user.email dev@example.com + git config user.name Dev + git checkout -qb feature + echo feature > feature.txt + git add feature.txt + git commit -qm 'feature work' + ) + echo "$clone" +} + +advance_base() { # advance_base + local seed="$root/$1-seed" + ( + cd "$seed" + git checkout -q main + echo "$3" > "$2" + git add "$2" + git commit -qm "base moves" + git push -q origin main + ) +} + +# 1. nothing to merge +clone="$(setup uptodate)" +out="$(cd "$clone" && BASE_REF=main bash "$script" 2>&1)"; status=$? +check 'up-to-date branch is a no-op' 0 "$status" +grep -q 'nothing to simulate' <<<"$out" || { echo "FAIL - expected the no-op message"; failures=$((failures + 1)); } + +# 2. base moved ahead, no conflict +clone="$(setup clean)" +advance_base clean other.txt 'added on main' +out="$(cd "$clone" && BASE_REF=main bash "$script" 2>&1)"; status=$? +check 'a base commit is merged in' 0 "$status" +[ -f "$clone/other.txt" ] || { echo "FAIL - the base file is missing after the merge"; failures=$((failures + 1)); } + +# 3. base moved ahead with a conflict +clone="$(setup conflict)" +(cd "$clone" && echo 'branch version' > shared.txt && git commit -qam 'branch edits shared.txt') +advance_base conflict shared.txt 'main version' +out="$(cd "$clone" && BASE_REF=main bash "$script" 2>&1)"; status=$? +check 'a conflicting base commit fails the job' 1 "$status" +grep -q '::error::Merge conflict' <<<"$out" || { echo "FAIL - expected an ::error:: annotation"; failures=$((failures + 1)); } + +echo +if [ "$failures" -eq 0 ]; then + echo 'All fresh-merge simulation cases behaved as expected.' +else + echo "$failures case(s) failed." +fi +exit $((failures > 0)) diff --git a/experiments/git-ls-files-quoting.mjs b/experiments/git-ls-files-quoting.mjs new file mode 100644 index 00000000..e502f51f --- /dev/null +++ b/experiments/git-ls-files-quoting.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +// Why js/tests/*.test.mjs list tracked files with execFileSync. +// +// `execSync("git ls-files '*.md'")` runs through the platform shell: +// /bin/sh on POSIX, which strips the single quotes, and cmd.exe on Windows, +// which does not. git then looks for a path literally named `'*.md'`, matches +// nothing and exits 0, so the caller sees an empty file list instead of an +// error -- js/tests/docs-validation.test.mjs validated zero documents on the +// Windows leg of the matrix until this was fixed. +// +// This script reproduces the Windows behaviour on any platform by quoting the +// pattern twice, and shows that execFileSync is immune because no shell is +// involved. +import { execSync, execFileSync } from 'child_process'; + +const count = (out) => out.trim().split('\n').filter(Boolean).length; +const repoRoot = new URL('..', import.meta.url).pathname; +const run = (label, fn) => { + try { + console.log(`${label}: ${count(fn())} file(s)`); + } catch (error) { + console.log(`${label}: failed with ${error.message.split('\n')[0]}`); + } +}; + +run('execSync, shell strips the quotes (POSIX)', () => + execSync("git ls-files '*.md'", { cwd: repoRoot, encoding: 'utf8' }) +); +run('execSync, quotes reach git (what cmd.exe does)', () => + execSync(`git ls-files "'*.md'"`, { cwd: repoRoot, encoding: 'utf8' }) +); +run('execFileSync, no shell at all', () => + execFileSync('git', ['ls-files', '*.md'], { cwd: repoRoot, encoding: 'utf8' }) +); diff --git a/experiments/issue-199-publish-false-positive.mjs b/experiments/issue-199-publish-false-positive.mjs new file mode 100644 index 00000000..8e887bec --- /dev/null +++ b/experiments/issue-199-publish-false-positive.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env bun + +/** + * Issue #199 — reproduce the npm publish false positive, then show the fix. + * + * Run: bun experiments/issue-199-publish-false-positive.mjs + * + * Simulates the exact sequence observed in run 33914574283 + * (dev/log/issues/199/pulls/200/ci-logs/run-33914574283.log): + * + * 1. `changeset publish` succeeds -> command-stream@0.20.1 is on npm + * 2. verification 2s later misses -> registry replica still answers 404 + * 3. the old loop republishes -> npm E409 "Cannot publish over + * previously staged version" + * 4. E409 matches 'npm error code e' -> reported as a hard failure + * + * The OLD strategy is reimplemented here verbatim so the regression is + * observable; the NEW strategy is imported from the real module. + */ + +import { publishWithRetry } from '../js/scripts/publish-retry.mjs'; + +const E409_STAGED_OUTPUT = [ + 'npm error code E409', + 'npm error 409 Conflict - PUT https://registry.npmjs.org/command-stream - Cannot publish over previously staged version "0.20.1"', +].join('\n'); + +const FAILURE_PATTERNS = [ + 'packages failed to publish', + 'error occurred while publishing', + 'npm error code e', + 'npm error 404', + 'npm error 401', + 'npm error 403', + 'access token expired', + 'eneedauth', + 'exited with code 1', +]; + +/** + * A registry that has the version but only reveals it from the Nth read on, + * mimicking npm's read-replica propagation lag. + * @param {number} visibleFromRead + */ +function makeLaggingRegistry(visibleFromRead) { + let reads = 0; + return { + get reads() { + return reads; + }, + async isPublished() { + return ++reads >= visibleFromRead; + }, + }; +} + +/** + * `changeset publish`: succeeds once, then answers E409 because the tarball is + * already staged. + */ +function makePublisher() { + let calls = 0; + return { + get calls() { + return calls; + }, + async run() { + calls++; + if (calls === 1) { + return { + code: 0, + output: '🦋 success packages published successfully', + }; + } + return { code: 1, output: E409_STAGED_OUTPUT }; + }, + }; +} + +/** The pre-#199 algorithm: one verification sample, republish on a miss. */ +async function oldStrategy({ publisher, registry, maxRetries = 3 }) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const { code, output } = await publisher.run(); + const lower = output.toLowerCase(); + const matched = FAILURE_PATTERNS.find((p) => lower.includes(p)); + if (matched) { + console.log( + ` attempt ${attempt}: failed — detected "${matched}" in output` + ); + continue; + } + if (code !== 0) { + console.log(` attempt ${attempt}: failed — exit code ${code}`); + continue; + } + // Single-shot verification, no polling. + if (await registry.isPublished()) { + return { success: true, attempts: attempt }; + } + console.log( + ` attempt ${attempt}: verification missed, republishing (this is the bug)` + ); + } + return { success: false, attempts: maxRetries }; +} + +/** The post-#199 algorithm, imported from the shipped module. */ +async function newStrategy({ publisher, registry }) { + return publishWithRetry({ + publish: async () => { + const { code, output } = await publisher.run(); + return { + success: code === 0, + error: code === 0 ? null : new Error(`exit code ${code}`), + output, + }; + }, + verify: () => registry.isPublished(), + maxRetries: 3, + retryDelay: 0, + sleepFn: async () => {}, + log: (message) => console.log(` ${message}`), + verifyOptions: { attempts: 7, initialDelay: 0, maxDelay: 0 }, + }); +} + +// The version becomes visible on the 2nd registry read — i.e. the very first +// sample misses, exactly as in the failing CI run. +const VISIBLE_FROM_READ = 2; + +console.log('OLD strategy (single-shot verification, republish on a miss):'); +const oldPublisher = makePublisher(); +const oldResult = await oldStrategy({ + publisher: oldPublisher, + registry: makeLaggingRegistry(VISIBLE_FROM_READ), +}); +console.log( + ` => success=${oldResult.success}, publish invocations=${oldPublisher.calls}\n` +); + +console.log('NEW strategy (bounded verification polling, no republish):'); +const newPublisher = makePublisher(); +const newResult = await newStrategy({ + publisher: newPublisher, + registry: makeLaggingRegistry(VISIBLE_FROM_READ), +}); +console.log( + ` => success=${newResult.success}, publish invocations=${newPublisher.calls}\n` +); + +const reproduced = oldResult.success === false && newResult.success === true; +console.log( + reproduced + ? '✅ Reproduced: the old strategy fails a successful release, the new one does not.' + : '❌ Not reproduced.' +); +process.exit(reproduced ? 0 : 1); diff --git a/experiments/issue-49/escape-handling.mjs b/experiments/issue-49/escape-handling.mjs index f88b5812..be8ccc27 100644 --- a/experiments/issue-49/escape-handling.mjs +++ b/experiments/issue-49/escape-handling.mjs @@ -1,6 +1,6 @@ -import { $ } from "../../js/src/$.mjs"; +import { $ } from '../../js/src/$.mjs'; const v = 'price is $5 and "q" and `tick` and back\\slash'; -for (const cmd of ["echo", "/bin/echo", 'printf "%s\\n"']) { +for (const cmd of ['echo', '/bin/echo', 'printf "%s\\n"']) { const strings = Object.assign([`${cmd} "`, '"'], { raw: [`${cmd} "`, '"'] }); const r = await $(strings, v).run({ capture: true, mirror: false }); console.log(cmd.padEnd(16), JSON.stringify(r.stdout)); diff --git a/experiments/issue-49/printf-check.mjs b/experiments/issue-49/printf-check.mjs index ef9df3db..bc24a63f 100644 --- a/experiments/issue-49/printf-check.mjs +++ b/experiments/issue-49/printf-check.mjs @@ -1,4 +1,4 @@ -import { $ } from "../../js/src/$.mjs"; +import { $ } from '../../js/src/$.mjs'; const v = 'price is $5 and "q"'; const strings = Object.assign(['printf "%s\\n" "', '"'], { raw: ['printf "%s\\n" "', '"'], diff --git a/experiments/issue-49/repro.mjs b/experiments/issue-49/repro.mjs index bc95be2e..baf2e2f9 100644 --- a/experiments/issue-49/repro.mjs +++ b/experiments/issue-49/repro.mjs @@ -1,7 +1,7 @@ -import { $ } from "../../js/src/$.mjs"; +import { $ } from '../../js/src/$.mjs'; const cmd = 'for file in a.js b.js; do echo "Processing: $file"; done'; const r = await $`bash -c "${cmd}"`.run({ capture: true, mirror: false }); -console.log("exit:", r.code); -console.log("stdout:", JSON.stringify(r.stdout)); -console.log("stderr:", JSON.stringify(r.stderr)); +console.log('exit:', r.code); +console.log('stdout:', JSON.stringify(r.stdout)); +console.log('stderr:', JSON.stringify(r.stderr)); diff --git a/experiments/issue-49/sh-parity.mjs b/experiments/issue-49/sh-parity.mjs index 1d68c92b..674d63f5 100644 --- a/experiments/issue-49/sh-parity.mjs +++ b/experiments/issue-49/sh-parity.mjs @@ -3,134 +3,137 @@ // $V in the same position. The reference is a real shell: the value is passed // through the environment (so it is never re-parsed) and the same script text // is run by sh. -import { spawnSync } from "node:child_process"; -import { $ } from "../../js/src/$.mjs"; +import { spawnSync } from 'node:child_process'; +import { $ } from '../../js/src/$.mjs'; // Each case: sh script text using $V, plus the value of V. const cases = [ { - name: "bash -c double quotes (issue #49)", + name: 'bash -c double quotes (issue #49)', sh: 'bash -c "$V"', value: 'for f in a.js b.js; do echo "Processing: $f"; done', }, - { name: "echo double quotes", sh: 'echo "$V"', value: "hello world" }, + { name: 'echo double quotes', sh: 'echo "$V"', value: 'hello world' }, { - name: "echo double quotes with $", + name: 'echo double quotes with $', sh: 'echo "$V"', - value: "price is $5 and $USER", + value: 'price is $5 and $USER', }, { - name: "echo double quotes with backticks", + name: 'echo double quotes with backticks', sh: 'echo "$V"', - value: "a `date` b", + value: 'a `date` b', }, { - name: "echo double quotes with quotes", + name: 'echo double quotes with quotes', sh: 'echo "$V"', value: 'she said "hi" and it\'s fine', }, { - name: "echo double quotes with backslash", + name: 'echo double quotes with backslash', sh: 'echo "$V"', - value: "a\\b\\\\c", + value: 'a\\b\\\\c', }, // Inside '...' a real shell would not expand anything; command-stream splices // the value in as literal text, so the expected output is the value itself. { - name: "echo single quotes", + name: 'echo single quotes', sh: "echo '$V'", value: "literal $V and it's fine", expect: "literal $V and it's fine\n", }, - { name: "echo unquoted", sh: "echo $V", value: "hello world" }, + { name: 'echo unquoted', sh: 'echo $V', value: 'hello world' }, { - name: "sh -c nested quoting", + name: 'sh -c nested quoting', sh: 'sh -c "$V"', value: `echo 'single' && echo "double"`, }, { - name: "double quotes with newline", + name: 'double quotes with newline', sh: 'echo "$V"', - value: "line1\nline2", + value: 'line1\nline2', }, - { name: "double quotes with glob", sh: 'echo "$V"', value: "*.js" }, + { name: 'double quotes with glob', sh: 'echo "$V"', value: '*.js' }, { - name: "double quotes with semicolons", + name: 'double quotes with semicolons', sh: 'echo "$V"', - value: "a; echo pwned; b", + value: 'a; echo pwned; b', }, { - name: "double quotes command substitution text", + name: 'double quotes command substitution text', sh: 'echo "$V"', - value: "$(echo injected)", + value: '$(echo injected)', }, { - name: "bash -c with here-doc", + name: 'bash -c with here-doc', sh: 'bash -c "$V"', - value: "cat < - spawnSync("/bin/sh", ["-c", script], { + spawnSync('/bin/sh', ['-c', script], { env: { ...process.env, V: value }, - encoding: "utf8", + encoding: 'utf8', }); let failures = 0; for (const c of cases) { - const parts = c.sh.split("$V"); - if (parts.length !== 2) + const parts = c.sh.split('$V'); + if (parts.length !== 2) { throw new Error(`case ${c.name} must contain exactly one $V`); + } const strings = Object.assign([parts[0], parts[1]], { raw: [parts[0], parts[1]], }); const ref = c.expect === undefined ? runShell(c.sh, c.value) - : { stdout: c.expect, status: 0, stderr: "" }; + : { stdout: c.expect, status: 0, stderr: '' }; const got = await $(strings, c.value).run({ capture: true, mirror: false }); const ok = got.stdout === ref.stdout && got.code === ref.status; - if (!ok) failures++; - console.log(`${ok ? "PASS" : "FAIL"} ${c.name}`); + if (!ok) { + failures++; + } + console.log(`${ok ? 'PASS' : 'FAIL'} ${c.name}`); if (!ok) { console.log( - ` sh : code=${ref.status} stdout=${JSON.stringify(ref.stdout)} stderr=${JSON.stringify(ref.stderr)}`, + ` sh : code=${ref.status} stdout=${JSON.stringify(ref.stdout)} stderr=${JSON.stringify(ref.stderr)}` ); console.log( - ` cs : code=${got.code} stdout=${JSON.stringify(got.stdout)} stderr=${JSON.stringify(got.stderr)}`, + ` cs : code=${got.code} stdout=${JSON.stringify(got.stdout)} stderr=${JSON.stringify(got.stderr)}` ); } } diff --git a/experiments/jscpd-format/README.md b/experiments/jscpd-format/README.md new file mode 100644 index 00000000..016bf0f0 --- /dev/null +++ b/experiments/jscpd-format/README.md @@ -0,0 +1,30 @@ +# `"format": "console"` makes jscpd analyse zero files + +`jscpd`'s `format` option is the list of **languages** to analyse, not the list +of reporters — reporters are configured separately, under `reporters`. A +configuration that sets `"format": "console"` therefore asks jscpd to analyse a +language called `console`, which no file is written in, so the run finds nothing +and exits 0 no matter how much duplication the tree contains. + +`@jscpd/finder` selects files with `options.format.includes(format)`, where +`format` is the detected language of the file. `'console'.includes('javascript')` +is `false` for every real language, so every file is filtered out. + +## Reproduce + +``` +node run.mjs +``` + +The script writes two byte-identical JavaScript files into a temporary +directory and runs jscpd over them twice, with `threshold: 0`: + +- `"format": "console"` — no table is printed, no clone is found, exit code 0. +- `"format": ["javascript"]` — 1 clone, 43.75% duplication, exit code 1. + +## Where this mattered + +`js/.jscpd.json` in this repository carried the broken value, so +`bun run check:duplication` passed over a tree it never read (issue #199). The +same value is still present in +`link-foundation/js-ai-driven-development-pipeline-template`. diff --git a/experiments/jscpd-format/run.mjs b/experiments/jscpd-format/run.mjs new file mode 100644 index 00000000..0d08b077 --- /dev/null +++ b/experiments/jscpd-format/run.mjs @@ -0,0 +1,57 @@ +// Demonstrates that jscpd's `format` option names languages, not reporters: +// `"format": "console"` silently narrows the analysis to zero files. +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const jscpd = resolve(here, '..', '..', 'js', 'node_modules', '.bin', 'jscpd'); + +const work = mkdtempSync(join(tmpdir(), 'jscpd-format-')); +const duplicated = `export function alpha(list) { + const out = []; + for (const item of list) { + if (item == null) continue; + out.push(String(item).trim().toLowerCase()); + } + return out; +} +`; +writeFileSync(join(work, 'a.mjs'), duplicated); +writeFileSync(join(work, 'b.mjs'), duplicated); + +const base = { + threshold: 0, + minTokens: 30, + minLines: 5, + reporters: ['console'], +}; + +for (const [label, format] of [ + ['"format": "console"', 'console'], + ['"format": ["javascript"]', ['javascript']], +]) { + const configPath = join( + work, + `config-${Array.isArray(format) ? 'lang' : 'reporter'}.json` + ); + writeFileSync(configPath, JSON.stringify({ ...base, format })); + + const result = spawnSync(jscpd, ['-c', configPath, work], { + encoding: 'utf8', + }); + const found = /Found (\d+) clones/.exec(result.stdout ?? ''); + + console.log(`${label}`); + console.log(` exit code: ${result.status}`); + console.log(` clones found: ${found ? found[1] : 0}`); + console.log( + ` files analysed: ${/│ javascript/.test(result.stdout ?? '') ? 2 : 0}` + ); +} + +console.log( + '\nWith threshold 0 and two identical files, only the second configuration fails.' +); diff --git a/experiments/publish-cdn-unreachable.mjs b/experiments/publish-cdn-unreachable.mjs new file mode 100644 index 00000000..2a7e8e6f --- /dev/null +++ b/experiments/publish-cdn-unreachable.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// Why js/tests/publish-to-npm.test.mjs probes unpkg as well as npm, and what +// js/scripts/use-m-loader.mjs changed about it. +// +// Every release script needs `use-m`, which is fetched from +// https://unpkg.com/use-m/use.js and eval-ed. Eleven scripts did that inline, +// at module scope: +// +// const { use } = eval(await (await fetch('https://unpkg.com/use-m/use.js')).text()); +// +// That await sits outside main()'s try/catch, has no deadline and no retry, so +// an unreachable CDN killed the script during module initialisation: nothing +// was written to GITHUB_OUTPUT and not even the first log line was printed. The +// suite then failed with +// +// Expected to contain: "published=true" +// Received: "" +// +// which names neither the CDN nor the network. Observed intermittently while +// investigating issue #199: two runs of the same unchanged suite differed only +// in whether unpkg answered. +// +// The old offline guard probed `npm view` only. npm and unpkg fail +// independently, so a reachable registry said "we are online" while the +// dependency the script actually needs at startup was not. +// +// This script forces the failure without waiting for a real outage, by pointing +// the fetch at a blackholed address, and runs both shapes side by side: the +// legacy inline fetch and the shared loader the scripts use now. +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +// 203.0.113.0/24 is TEST-NET-3 (RFC 5737): guaranteed never routable. Standing +// in for unpkg, it fails the way a real outage makes the CDN fail -- a connect +// that never completes -- without depending on proxy settings a given runtime +// may or may not honour. +const BLACKHOLE_URL = 'https://203.0.113.1/use-m/use.js'; + +// Capped so an unroutable proxy stalls the run for a bounded time instead of +// hanging until the connect attempt gives up on its own. +const TIMEOUT_MS = 30000; + +const repoRoot = resolve(new URL('..', import.meta.url).pathname); +const LOADER = join(repoRoot, 'js/scripts/use-m-loader.mjs'); +const dir = mkdtempSync(join(tmpdir(), 'publish-cdn-')); + +/** + * Run one .mjs source in a scratch directory and report what a CI log would + * show: whether the script reached its own code, and what it said when it did + * not. + * @param {string} label + * @param {string} source + * @returns {void} + */ +function run(label, source) { + const script = join(dir, `${label.replace(/\W+/g, '-')}.mjs`); + writeFileSync(script, source); + const outputFile = join(dir, 'gh-output.txt'); + writeFileSync(outputFile, ''); + + const started = Date.now(); + const res = spawnSync('node', [script], { + cwd: dir, + encoding: 'utf8', + timeout: TIMEOUT_MS, + env: { ...process.env, GITHUB_OUTPUT: outputFile }, + }); + + // A null status means spawnSync hit its own timeout and killed the child: the + // script was still blocked in a fetch with no deadline of its own. + const status = + res.status === null ? `null (killed after ${TIMEOUT_MS}ms)` : res.status; + // Node prints the offending source line before the error itself; the message + // a reader of the CI log actually sees is the first line naming an Error type. + const message = (res.stderr || '') + .split('\n') + .find((line) => /^[A-Za-z]*Error(:| \[)/.test(line.trim())); + console.log(`${label}:`); + console.log(` exit status ${status}`); + console.log(` elapsed ${Date.now() - started}ms`); + console.log(` stdout ${JSON.stringify(res.stdout || '')}`); + console.log(` failure reported ${JSON.stringify(message ?? '')}`); + console.log(''); +} + +// Before: the module-scope fetch the eleven release scripts used to open with. +run( + 'legacy inline fetch', + `const { use } = eval( + await (await fetch(${JSON.stringify(BLACKHOLE_URL)})).text() + ); + console.log('reached the script body');` +); + +// After: the same failure through js/scripts/use-m-loader.mjs. attempts and +// timeoutMs are shortened here only so the experiment finishes quickly; the +// code path, the retry and the message are the production ones. Set +// CI_SCRIPTS_DEBUG=1 to see one ::debug:: line per attempt. +run( + 'shared loader', + `import { loadUseM } from ${JSON.stringify(LOADER)}; + const use = await loadUseM({ + url: ${JSON.stringify(BLACKHOLE_URL)}, + attempts: 2, + timeoutMs: 3000, + retryDelayMs: 200, + }); + console.log('reached the script body');` +); diff --git a/experiments/repro-issue-170-awaited.mjs b/experiments/repro-issue-170-awaited.mjs index bae53474..78aa7b40 100644 --- a/experiments/repro-issue-170-awaited.mjs +++ b/experiments/repro-issue-170-awaited.mjs @@ -19,7 +19,8 @@ async function run() { let code, stderr, threw; try { - const result = await $`sh -c "echo stdout-marker; echo stderr-marker >&2; sleep 0.3; exit 5"`; + const result = + await $`sh -c "echo stdout-marker; echo stderr-marker >&2; sleep 0.3; exit 5"`; code = result.code; stderr = result.stderr; } catch (error) { @@ -28,7 +29,9 @@ async function run() { stderr = error.stderr; } - console.error(`RESULT threw=${threw} code=${code} stderr=${JSON.stringify(stderr)}`); + console.error( + `RESULT threw=${threw} code=${code} stderr=${JSON.stringify(stderr)}` + ); if (code === 5) { console.error('PASS: real exit code 5 preserved'); process.exit(0); diff --git a/experiments/repro-issue-170-parentclose.mjs b/experiments/repro-issue-170-parentclose.mjs index 9beed359..083ea469 100644 --- a/experiments/repro-issue-170-parentclose.mjs +++ b/experiments/repro-issue-170-parentclose.mjs @@ -6,14 +6,14 @@ // monitorParentStreams' listener -> _handleParentStreamClosure() on every active // runner, which aborts/kills the live command. If that replaces the real exit // code (5) with a synthetic SIGTERM (143), the errexit error is wrong. -import { $, shell } from "../js/src/$.mjs"; +import { $, shell } from '../js/src/$.mjs'; shell.errexit(true); // Fire a spurious 'close' on process.stdout shortly after the command starts. setTimeout(() => { - console.error(">>> emitting spurious close on process.stdout"); - process.stdout.emit("close"); + console.error('>>> emitting spurious close on process.stdout'); + process.stdout.emit('close'); }, 60); let code, stderr, stdout, threw; @@ -31,14 +31,14 @@ try { } console.error( - `RESULT threw=${threw} code=${code} stdout=${JSON.stringify(stdout)} stderr=${JSON.stringify(stderr)}`, + `RESULT threw=${threw} code=${code} stdout=${JSON.stringify(stdout)} stderr=${JSON.stringify(stderr)}` ); if (code === 5) { - console.error("PASS: real exit code 5 preserved"); + console.error('PASS: real exit code 5 preserved'); process.exit(0); } else { console.error( - `FAIL: expected code 5, got ${code} (synthetic SIGTERM means the bug reproduced)`, + `FAIL: expected code 5, got ${code} (synthetic SIGTERM means the bug reproduced)` ); process.exit(1); } diff --git a/experiments/secretlint-scope.sh b/experiments/secretlint-scope.sh new file mode 100644 index 00000000..a49381b9 --- /dev/null +++ b/experiments/secretlint-scope.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Does `secretlint "**/*"` reach dot-directories, and is .secretlintignore honoured? +# +# Both questions decide whether the secret-scan job in .github/workflows/security.yml +# is worth anything. A scan that silently skips .github/ or .changeset/ is a false +# negative, and one that cannot be scoped is too slow to keep. +# +# The probe is a *fake* GitHub token: `ghp_` followed by 36 random characters, which +# matches @secretlint/secretlint-rule-github's pattern without being a credential. +# (The AWS example key from the AWS documentation is deliberately not flagged by the +# recommended preset, so it cannot be used as a probe.) +# +# Result on 2026-09-04, secretlint 13.0.5: +# plain file -> reported +# .github/ file -> reported (the glob does descend into dot-directories) +# ignored directory -> not reported (.secretlintignore is picked up from the cwd) +# +# Usage: bash experiments/secretlint-scope.sh +set -uo pipefail +cd "$(dirname "$0")/.." + +probes=('secretlint-probe.txt' '.github/secretlint-probe.txt' 'node_modules/secretlint-probe.txt') +cleanup() { rm -f "${probes[@]}"; } +trap cleanup EXIT + +token="ghp_$(head -c 40 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 36)" +mkdir -p node_modules +for probe in "${probes[@]}"; do + printf 'token = %s\n' "$token" > "$probe" +done + +report="$(npx --yes -p secretlint@13.0.5 \ + -p @secretlint/secretlint-rule-preset-recommend@13.0.5 \ + secretlint '**/*' 2>&1 || true)" + +for probe in "${probes[@]}"; do + if grep -qF "$probe" <<<"$report"; then + echo "reported: $probe" + else + echo "not reported: $probe" + fi +done diff --git a/js/.changeset/ci-audit-and-lint-coverage.md b/js/.changeset/ci-audit-and-lint-coverage.md new file mode 100644 index 00000000..8612d5f3 --- /dev/null +++ b/js/.changeset/ci-audit-and-lint-coverage.md @@ -0,0 +1,5 @@ +--- +'command-stream': patch +--- + +Update the development toolchain (eslint, prettier, jscpd, lint-staged, changesets, node-pty, subset-font) to versions with no outstanding npm or bun audit findings, and reformat `terminal-artifacts.mjs` for prettier 3.9. No runtime behaviour changes. diff --git a/js/.jscpd.json b/js/.jscpd.json index 3f1351b1..eaf0307a 100644 --- a/js/.jscpd.json +++ b/js/.jscpd.json @@ -1,14 +1,16 @@ { - "threshold": 0, + "threshold": 6, "minTokens": 30, "minLines": 5, "skipComments": true, + "format": ["javascript"], "ignore": [ "**/node_modules/**", "**/build/**", "**/dist/**", "**/*.min.js", "**/coverage/**", + "**/reports/**", "**/.changeset/**", "**/docs/case-studies/**/data/**", "**/docs/case-studies/**/log-excerpts/**", @@ -17,7 +19,6 @@ "**/pnpm-lock.yaml", "**/yarn.lock" ], - "format": "console", "reporters": ["console", "html"], "output": "./reports/jscpd" } diff --git a/js/.prettierignore b/js/.prettierignore deleted file mode 100644 index a2fd810e..00000000 --- a/js/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -node_modules -coverage -dist -*.min.js -package-lock.json -.eslintcache -CLAUDE.md -docs/case-studies/**/data/** -docs/case-studies/**/log-excerpts/** -docs/case-studies/**/templates/** diff --git a/js/bun.lock b/js/bun.lock index 5d933490..5df44b9c 100644 --- a/js/bun.lock +++ b/js/bun.lock @@ -1,6 +1,6 @@ { "lockfileVersion": 1, - "configVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "command-stream", @@ -8,116 +8,120 @@ "@resvg/resvg-js": "^2.6.2", "@xterm/headless": "^6.0.0", "gifenc": "^1.0.3", - "node-pty": "^1.2.0-beta.14", + "node-pty": "^1.2.0-beta.15", }, "devDependencies": { - "@changesets/cli": "^2.29.7", + "@changesets/cli": "^2.31.1", "dejavu-fonts-ttf": "^2.37.3", - "eslint": "^9.38.0", + "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-prettier": "^5.5.6", "husky": "^9.1.7", - "jscpd": "^4.0.5", - "lint-staged": "^16.2.6", - "prettier": "^3.6.2", - "subset-font": "^2.5.0", + "jscpd": "^4.3.0", + "lint-staged": "^16.4.0", + "prettier": "^3.9.6", + "subset-font": "^2.7.0", }, }, }, "packages": { - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, ""], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, ""], - "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], + "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" } }, ""], - "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, ""], - "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, ""], - "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.0.14", "", { "dependencies": { "@changesets/config": "^3.1.2", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA=="], + "@changesets/apply-release-plan": ["@changesets/apply-release-plan@7.1.1", "", { "dependencies": { "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", "lodash.startcase": "^4.4.0", "outdent": "^0.5.0", "prettier": "^2.7.1", "resolve-from": "^5.0.0", "semver": "^7.5.3" } }, "sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA=="], - "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.9", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ=="], + "@changesets/assemble-release-plan": ["@changesets/assemble-release-plan@6.0.10", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A=="], - "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, "sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q=="], + "@changesets/changelog-git": ["@changesets/changelog-git@0.2.1", "", { "dependencies": { "@changesets/types": "^6.1.0" } }, ""], - "@changesets/cli": ["@changesets/cli@2.29.8", "", { "dependencies": { "@changesets/apply-release-plan": "^7.0.14", "@changesets/assemble-release-plan": "^6.0.9", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.2", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/get-release-plan": "^4.0.14", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.6", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "ci-info": "^3.7.0", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA=="], + "@changesets/cli": ["@changesets/cli@2.31.1", "", { "dependencies": { "@changesets/apply-release-plan": "^7.1.1", "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", "semver": "^7.5.3", "spawndamnit": "^3.0.1", "term-size": "^2.1.0" }, "bin": { "changeset": "bin.js" } }, "sha512-uO05WTcRBwuVOJVSW8Cmpqw6q0WDL53ajGCMyszutvOe5toOnunbpM4jZzf+qxBOz7i0AzopZ8diBuewjmF40w=="], - "@changesets/config": ["@changesets/config@3.1.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.3", "@changesets/logger": "^0.1.1", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog=="], + "@changesets/config": ["@changesets/config@3.1.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.8" } }, "sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q=="], - "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow=="], + "@changesets/errors": ["@changesets/errors@0.2.0", "", { "dependencies": { "extendable-error": "^0.1.5" } }, ""], - "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ=="], + "@changesets/get-dependents-graph": ["@changesets/get-dependents-graph@2.1.4", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "picocolors": "^1.1.0", "semver": "^7.5.3" } }, "sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg=="], - "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.14", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.9", "@changesets/config": "^3.1.2", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.6", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g=="], + "@changesets/get-release-plan": ["@changesets/get-release-plan@4.0.16", "", { "dependencies": { "@changesets/assemble-release-plan": "^6.0.10", "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g=="], - "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ=="], + "@changesets/get-version-range-type": ["@changesets/get-version-range-type@0.4.0", "", {}, ""], - "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, "sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw=="], + "@changesets/git": ["@changesets/git@3.0.4", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.8", "spawndamnit": "^3.0.1" } }, ""], - "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, "sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg=="], + "@changesets/logger": ["@changesets/logger@0.1.1", "", { "dependencies": { "picocolors": "^1.1.0" } }, ""], - "@changesets/parse": ["@changesets/parse@0.4.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA=="], + "@changesets/parse": ["@changesets/parse@0.4.3", "", { "dependencies": { "@changesets/types": "^6.1.0", "js-yaml": "^4.1.1" } }, "sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A=="], - "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug=="], + "@changesets/pre": ["@changesets/pre@2.0.2", "", { "dependencies": { "@changesets/errors": "^0.2.0", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, ""], - "@changesets/read": ["@changesets/read@0.6.6", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.2", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg=="], + "@changesets/read": ["@changesets/read@0.6.7", "", { "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", "picocolors": "^1.1.0" } }, "sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA=="], - "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, "sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw=="], + "@changesets/should-skip-package": ["@changesets/should-skip-package@0.1.2", "", { "dependencies": { "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } }, ""], - "@changesets/types": ["@changesets/types@6.1.0", "", {}, "sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA=="], + "@changesets/types": ["@changesets/types@6.1.0", "", {}, ""], - "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, "sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q=="], + "@changesets/write": ["@changesets/write@0.4.0", "", { "dependencies": { "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "human-id": "^4.1.1", "prettier": "^2.7.1" } }, ""], - "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@colors/colors": ["@colors/colors@1.5.0", "", {}, ""], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, ""], - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, ""], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, ""], - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, ""], - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.7", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.3.2", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw=="], - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + "@eslint/js": ["@eslint/js@9.39.5", "", {}, "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, ""], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, ""], - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, ""], - "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA=="], + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, ""], - "@jscpd/core": ["@jscpd/core@4.0.1", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-6Migc68Z8p7q5xqW1wbF3SfIbYHPQoiLHPbJb1A1Z1H9DwImwopFkYflqRDpuamLd0Jfg2jx3ZBmHQt21NbD1g=="], + "@inquirer/external-editor": ["@inquirer/external-editor@1.0.3", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, ""], - "@jscpd/finder": ["@jscpd/finder@4.0.1", "", { "dependencies": { "@jscpd/core": "4.0.1", "@jscpd/tokenizer": "4.0.1", "blamer": "^1.0.6", "bytes": "^3.1.2", "cli-table3": "^0.6.5", "colors": "^1.4.0", "fast-glob": "^3.3.2", "fs-extra": "^11.2.0", "markdown-table": "^2.0.0", "pug": "^3.0.3" } }, "sha512-TcCT28686GeLl87EUmrBXYmuOFELVMDwyjKkcId+qjNS1zVWRd53Xd5xKwEDzkCEgen/vCs+lorLLToolXp5oQ=="], + "@jscpd/badge-reporter": ["@jscpd/badge-reporter@4.2.5", "", { "dependencies": { "badgen": "^3.2.3", "colors": "^1.4.0", "fs-extra": "^11.2.0" } }, "sha512-ktXrjPeRaRyUDktxTroSA2/w5sshXpQplWkUuq/e6XqEpKBSbGEnwZLIaegSijOrMwIcCXPQ9k4feXIz5eVJNA=="], - "@jscpd/html-reporter": ["@jscpd/html-reporter@4.0.1", "", { "dependencies": { "colors": "1.4.0", "fs-extra": "^11.2.0", "pug": "^3.0.3" } }, "sha512-M9fFETNvXXuy4fWv0M2oMluxwrQUBtubxCHaWw21lb2G8A6SE19moe3dUkluZ/3V4BccywfeF9lSEUg84heLww=="], + "@jscpd/core": ["@jscpd/core@4.2.5", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-Esf2deHxaoNEjePwf2jqP6Urzj+BAOsJVPFLbnnSsV+q7rLNMcn0UEEoKBXIOOt4qMkrkhl9DfwpMyPPOr6GkQ=="], - "@jscpd/tokenizer": ["@jscpd/tokenizer@4.0.1", "", { "dependencies": { "@jscpd/core": "4.0.1", "reprism": "^0.0.11", "spark-md5": "^3.0.2" } }, "sha512-l/CPeEigadYcQUsUxf1wdCBfNjyAxYcQU04KciFNmSZAMY+ykJ8fZsiuyfjb+oOuDgsIPZZ9YvbvsCr6NBXueg=="], + "@jscpd/finder": ["@jscpd/finder@4.3.0", "", { "dependencies": { "@jscpd/core": "4.2.5", "@jscpd/tokenizer": "4.2.6", "blamer": "^1.0.6", "bytes": "^3.1.2", "cli-table3": "^0.6.5", "colors": "^1.4.0", "fast-glob": "^3.3.2", "fs-extra": "^11.3.6", "markdown-table": "^2.0.0", "pug": "^3.0.4" } }, "sha512-MnEUyier0D6P9zRIhAlBoyJUV8BYT6d5FDZuhR7X23FdZAgdMV8yaRRO1Q1EveK9/ReA6WVHT1HE8F15rij71A=="], - "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, "sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA=="], + "@jscpd/html-reporter": ["@jscpd/html-reporter@4.2.5", "", { "dependencies": { "colors": "1.4.0", "fs-extra": "^11.2.0", "pug": "^3.0.4" } }, "sha512-zMMIKbvi43dMgeNeHXlHQy1ovf+KJrzNlUubaBvCAVatqP23ksW8d3fmsevIQG9mMMTH0D1xOz+SxUn1FREOPg=="], - "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, "sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A=="], + "@jscpd/tokenizer": ["@jscpd/tokenizer@4.2.6", "", { "dependencies": { "@jscpd/core": "4.2.5", "spark-md5": "^3.0.2" } }, "sha512-/eyFjINWLs2mrBTU4H681bs855r5oRyl1O3mZxcd7TpL1JIG85a7pps0RkRPAe9c/2KcjVCc82HbhOz86M0n5g=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@manypkg/find-root": ["@manypkg/find-root@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@types/node": "^12.7.1", "find-up": "^4.1.0", "fs-extra": "^8.1.0" } }, ""], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@manypkg/get-packages": ["@manypkg/get-packages@1.1.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "@changesets/types": "^4.0.1", "@manypkg/find-root": "^1.1.0", "fs-extra": "^8.1.0", "globby": "^11.0.0", "read-yaml-file": "^1.1.0" } }, ""], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, ""], - "@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="], + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, ""], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, ""], + + "@pkgr/core": ["@pkgr/core@0.3.6", "", {}, "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA=="], "@resvg/resvg-js": ["@resvg/resvg-js@2.6.2", "", { "optionalDependencies": { "@resvg/resvg-js-android-arm-eabi": "2.6.2", "@resvg/resvg-js-android-arm64": "2.6.2", "@resvg/resvg-js-darwin-arm64": "2.6.2", "@resvg/resvg-js-darwin-x64": "2.6.2", "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", "@resvg/resvg-js-linux-arm64-musl": "2.6.2", "@resvg/resvg-js-linux-x64-gnu": "2.6.2", "@resvg/resvg-js-linux-x64-musl": "2.6.2", "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", "@resvg/resvg-js-win32-x64-msvc": "2.6.2" } }, "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q=="], @@ -145,574 +149,572 @@ "@resvg/resvg-js-win32-x64-msvc": ["@resvg/resvg-js-win32-x64-msvc@2.6.2", "", { "os": "win32", "cpu": "x64" }, "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, ""], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, ""], "@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], + "@types/sarif": ["@types/sarif@2.1.7", "", {}, ""], "@xterm/headless": ["@xterm/headless@6.0.0", "", {}, "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw=="], - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + "acorn": ["acorn@8.15.0", "", { "bin": "bin/acorn" }, ""], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], + "ansi-colors": ["ansi-colors@4.1.3", "", {}, ""], - "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw=="], + "ansi-escapes": ["ansi-escapes@7.2.0", "", { "dependencies": { "environment": "^1.0.0" } }, ""], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, ""], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "argparse": ["argparse@2.0.1", "", {}, ""], - "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "array-union": ["array-union@2.1.0", "", {}, ""], - "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "asap": ["asap@2.0.6", "", {}, ""], - "assert-never": ["assert-never@1.4.0", "", {}, "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA=="], + "assert-never": ["assert-never@1.4.0", "", {}, ""], - "babel-walk": ["babel-walk@3.0.0-canary-5", "", { "dependencies": { "@babel/types": "^7.9.6" } }, "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw=="], + "babel-walk": ["babel-walk@3.0.0-canary-5", "", { "dependencies": { "@babel/types": "^7.9.6" } }, ""], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "badgen": ["badgen@3.3.2", "", {}, "sha512-fbQwK9norfdzbdsoPwbLIAmgBXDGEme3jeIyqPAH7o6vp9lmuLHS7uXULvOiQ6XnMLkYNG4gDjILf74hgtTAug=="], - "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, ""], - "blamer": ["blamer@1.0.7", "", { "dependencies": { "execa": "^4.0.0", "which": "^2.0.2" } }, "sha512-GbBStl/EVlSWkiJQBZps3H1iARBrC7vt++Jb/TTmCNu/jZ04VW7tSN1nScbFXBUy1AN+jzeL7Zep9sbQxLhXKA=="], + "better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, ""], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "blamer": ["blamer@1.0.7", "", { "dependencies": { "execa": "^4.0.0", "which": "^2.0.2" } }, ""], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + "bytes": ["bytes@3.1.2", "", {}, ""], - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""], - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, ""], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "callsites": ["callsites@3.1.0", "", {}, ""], - "character-parser": ["character-parser@2.2.0", "", { "dependencies": { "is-regex": "^1.0.3" } }, "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""], - "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="], + "character-parser": ["character-parser@2.2.0", "", { "dependencies": { "is-regex": "^1.0.3" } }, ""], - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "chardet": ["chardet@2.1.1", "", {}, ""], - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, ""], - "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, ""], - "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="], + "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, ""], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-name": ["color-name@1.1.4", "", {}, ""], - "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "colorette": ["colorette@2.0.20", "", {}, ""], - "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], + "colors": ["colors@1.4.0", "", {}, ""], - "commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "concat-map": ["concat-map@0.0.1", "", {}, ""], - "constantinople": ["constantinople@4.0.1", "", { "dependencies": { "@babel/parser": "^7.6.0", "@babel/types": "^7.6.1" } }, "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw=="], + "constantinople": ["constantinople@4.0.1", "", { "dependencies": { "@babel/parser": "^7.6.0", "@babel/types": "^7.6.1" } }, ""], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "deep-is": ["deep-is@0.1.4", "", {}, ""], "dejavu-fonts-ttf": ["dejavu-fonts-ttf@2.37.3", "", {}, "sha512-f1hd7jJbeQa1VWcw+K2KrTXS50zTMaHpVC4XIKJpNcDeYR5ajMtj/iLlQDYNvLOKamUB3ARVVCf79lNwNVztSQ=="], - "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + "detect-indent": ["detect-indent@6.1.0", "", {}, ""], - "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, ""], - "doctypes": ["doctypes@1.1.0", "", {}, "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ=="], + "doctypes": ["doctypes@1.1.0", "", {}, ""], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, ""], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, ""], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, ""], - "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], + "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, ""], - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "environment": ["environment@1.1.0", "", {}, ""], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, ""], - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-errors": ["es-errors@1.3.0", "", {}, ""], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, ""], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, ""], - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + "eslint": ["eslint@9.39.5", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.6", "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw=="], - "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": "bin/cli.js" }, ""], - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.4", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.6", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.13" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, ""], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, ""], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, ""], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" } }, ""], - "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, ""], - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, ""], - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estraverse": ["estraverse@5.3.0", "", {}, ""], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "esutils": ["esutils@2.0.3", "", {}, ""], - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "eventemitter3": ["eventemitter3@5.0.1", "", {}, ""], - "execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="], + "execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, ""], - "extendable-error": ["extendable-error@0.1.7", "", {}, "sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg=="], + "extendable-error": ["extendable-error@0.1.7", "", {}, ""], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""], - "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], + "fast-diff": ["fast-diff@1.3.0", "", {}, ""], - "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-glob": ["fast-glob@3.3.3", "", { "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" } }, ""], - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, ""], - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, ""], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, ""], - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, ""], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""], - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""], - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, ""], - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], "fontverter": ["fontverter@2.0.0", "", { "dependencies": { "wawoff2": "^2.0.0", "woff2sfnt-sfnt2woff": "^1.0.0" } }, "sha512-DFVX5hvXuhi1Jven1tbpebYTCT9XYnvx6/Z+HFUPb7ZRMCW+pj2clU9VMhoTPgWKPhAs7JJDSk3CW1jNUvKCZQ=="], - "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, ""], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "function-bind": ["function-bind@1.1.2", "", {}, ""], - "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, ""], - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, ""], - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, ""], - "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, ""], "gifenc": ["gifenc@1.0.3", "", {}, "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw=="], - "gitignore-to-glob": ["gitignore-to-glob@0.3.0", "", {}, "sha512-mk74BdnK7lIwDHnotHddx1wsjMOFIThpLY3cPNniJ/2fA/tlLzHnFxIdR+4sLOu5KGgQJdij4kjJ2RoUNnCNMA=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""], - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "globals": ["globals@14.0.0", "", {}, ""], - "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, ""], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "gopd": ["gopd@1.2.0", "", {}, ""], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, ""], "harfbuzzjs": ["harfbuzzjs@0.10.3", "", {}, "sha512-GJnLUrgLMadlMYrBGEXwYEimObbysy3prWT4HyPpFQERvgTU/OZ+ReUlEPOum6w4RBtFXzXiCCmECOr4sz3qwQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "has-flag": ["has-flag@4.0.0", "", {}, ""], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, ""], - "human-id": ["human-id@4.1.3", "", { "bin": { "human-id": "dist/cli.js" } }, "sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, ""], - "human-signals": ["human-signals@1.1.1", "", {}, "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""], - "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "human-id": ["human-id@4.1.3", "", { "bin": "dist/cli.js" }, ""], - "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="], + "human-signals": ["human-signals@1.1.1", "", {}, ""], - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "husky": ["husky@9.1.7", "", { "bin": "bin.js" }, ""], - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, ""], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "ignore": ["ignore@5.3.2", "", {}, ""], - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""], - "is-expression": ["is-expression@4.0.0", "", { "dependencies": { "acorn": "^7.1.1", "object-assign": "^4.1.1" } }, "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, ""], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, ""], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-expression": ["is-expression@4.0.0", "", { "dependencies": { "acorn": "^7.1.1", "object-assign": "^4.1.1" } }, ""], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, ""], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, ""], - "is-promise": ["is-promise@2.2.2", "", {}, "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="], + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""], - "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + "is-number": ["is-number@7.0.0", "", {}, ""], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "is-promise": ["is-promise@2.2.2", "", {}, ""], - "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, "sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, ""], - "is-windows": ["is-windows@1.0.2", "", {}, "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="], + "is-stream": ["is-stream@2.0.1", "", {}, ""], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "is-subdir": ["is-subdir@1.2.0", "", { "dependencies": { "better-path-resolve": "1.0.0" } }, ""], - "js-stringify": ["js-stringify@1.0.2", "", {}, "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g=="], + "is-windows": ["is-windows@1.0.2", "", {}, ""], - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "isexe": ["isexe@2.0.0", "", {}, ""], - "jscpd": ["jscpd@4.0.5", "", { "dependencies": { "@jscpd/core": "4.0.1", "@jscpd/finder": "4.0.1", "@jscpd/html-reporter": "4.0.1", "@jscpd/tokenizer": "4.0.1", "colors": "^1.4.0", "commander": "^5.0.0", "fs-extra": "^11.2.0", "gitignore-to-glob": "^0.3.0", "jscpd-sarif-reporter": "4.0.3" }, "bin": { "jscpd": "bin/jscpd" } }, "sha512-AzJlSLvKtXYkQm93DKE1cRN3rf6pkpv3fm5TVuvECwoqljQlCM/56ujHn9xPcE7wyUnH5+yHr7tcTiveIoMBoQ=="], + "js-stringify": ["js-stringify@1.0.2", "", {}, ""], - "jscpd-sarif-reporter": ["jscpd-sarif-reporter@4.0.3", "", { "dependencies": { "colors": "^1.4.0", "fs-extra": "^11.2.0", "node-sarif-builder": "^2.0.3" } }, "sha512-0T7KiWiDIVArvlBkvCorn2NFwQe7p7DJ37o4YFRuPLDpcr1jNHQlEfbFPw8hDdgJ4hpfby6A5YwyHqASKJ7drA=="], + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "jscpd": ["jscpd@4.3.0", "", { "dependencies": { "@jscpd/badge-reporter": "4.2.5", "@jscpd/core": "4.2.5", "@jscpd/finder": "4.3.0", "@jscpd/html-reporter": "4.2.5", "@jscpd/tokenizer": "4.2.6", "colors": "^1.4.0", "commander": "^15.0.0", "fs-extra": "^11.3.6", "jscpd-sarif-reporter": "4.2.5" }, "bin": { "jscpd": "bin/jscpd" } }, "sha512-yUqcHy/USHvzFamS6Loo49MCSW0Dc+4RL6ELH+Un9o2/jRSQbs9+a5hjfFceV2d3Zgv1opv5PXJ0eZ3fEQdjkg=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "jscpd-sarif-reporter": ["jscpd-sarif-reporter@4.2.5", "", { "dependencies": { "colors": "^1.4.0", "fs-extra": "^11.2.0", "node-sarif-builder": "^4.1.0" } }, "sha512-O8LcM9grAS5yO5x1Q0yegYaYcUX//IEBEyvzGFSYCeo1YzHbMnAI6EK7oTrwD+7Csjvfg9m8B8G7OOxzcSlr9w=="], - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, ""], - "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""], - "jstransformer": ["jstransformer@1.0.0", "", { "dependencies": { "is-promise": "^2.0.0", "promise": "^7.0.1" } }, "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, ""], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "jstransformer": ["jstransformer@1.0.0", "", { "dependencies": { "is-promise": "^2.0.0", "promise": "^7.0.1" } }, ""], - "lint-staged": ["lint-staged@16.2.7", "", { "dependencies": { "commander": "^14.0.2", "listr2": "^9.0.5", "micromatch": "^4.0.8", "nano-spawn": "^2.0.0", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.8.1" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, ""], - "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="], + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, ""], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lint-staged": ["lint-staged@16.4.0", "", { "dependencies": { "commander": "^14.0.3", "listr2": "^9.0.5", "picomatch": "^4.0.3", "string-argv": "^0.3.2", "tinyexec": "^1.0.4", "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw=="], - "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + "listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, ""], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, ""], - "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + "lodash.merge": ["lodash.merge@4.6.2", "", {}, ""], - "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, ""], - "markdown-table": ["markdown-table@2.0.0", "", { "dependencies": { "repeat-string": "^1.0.0" } }, "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A=="], + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, ""], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "markdown-table": ["markdown-table@2.0.0", "", { "dependencies": { "repeat-string": "^1.0.0" } }, ""], - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, ""], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + "merge2": ["merge2@1.4.1", "", {}, ""], - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, ""], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "mimic-function": ["mimic-function@5.0.1", "", {}, ""], - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mri": ["mri@1.2.0", "", {}, ""], - "nano-spawn": ["nano-spawn@2.0.0", "", {}, "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw=="], + "ms": ["ms@2.1.3", "", {}, ""], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, ""], "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], - "node-pty": ["node-pty@1.2.0-beta.14", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-XORU9BQgpxVgqr7WivjJ17mLenOHUgKKWzuZZNaw3NDYgHc/wPJQMSaoLDrpEgqV6aU1nNwil1o/OqYj6lWmUA=="], + "node-pty": ["node-pty@1.2.0-beta.15", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-vORSzHXi4Ofl7HemVWpuudLqCPdaQb4LfpRCUpE5HPxhp4JYscl8zZwxh11p26v2wvW24WMwnMfLjhRLixrfxA=="], - "node-sarif-builder": ["node-sarif-builder@2.0.3", "", { "dependencies": { "@types/sarif": "^2.1.4", "fs-extra": "^10.0.0" } }, "sha512-Pzr3rol8fvhG/oJjIq2NTVB0vmdNNlz22FENhhPojYRZ4/ee08CfK4YuKmuL54V9MLhI1kpzxfOJ/63LzmZzDg=="], + "node-sarif-builder": ["node-sarif-builder@4.1.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-IWqZF6u0EI/07HTBm+zZ+MgXgWl09dnSJRGaDCPBSlOqilDcx6pj3Mpb3HvPN8V2Gr+ISw7ZrMsL7STWs1F++w=="], - "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, ""], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-assign": ["object-assign@4.1.1", "", {}, ""], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, ""], - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, ""], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, ""], - "outdent": ["outdent@0.5.0", "", {}, "sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q=="], + "outdent": ["outdent@0.5.0", "", {}, ""], - "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, "sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw=="], + "p-filter": ["p-filter@2.1.0", "", { "dependencies": { "p-map": "^2.0.0" } }, ""], - "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, ""], - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, ""], - "p-map": ["p-map@2.1.0", "", {}, "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw=="], + "p-map": ["p-map@2.1.0", "", {}, ""], - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "p-try": ["p-try@2.2.0", "", {}, ""], - "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, "sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ=="], + "package-manager-detector": ["package-manager-detector@0.2.11", "", { "dependencies": { "quansync": "^0.2.7" } }, ""], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, ""], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-exists": ["path-exists@4.0.0", "", {}, ""], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-key": ["path-key@3.1.1", "", {}, ""], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-parse": ["path-parse@1.0.7", "", {}, ""], - "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "path-type": ["path-type@4.0.0", "", {}, ""], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + "picocolors": ["picocolors@1.1.1", "", {}, ""], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + "pify": ["pify@4.0.1", "", {}, ""], - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, ""], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], - "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + "prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="], - "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, ""], - "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], + "pug": ["pug@3.0.4", "", { "dependencies": { "pug-code-gen": "^3.0.4", "pug-filters": "^4.0.0", "pug-lexer": "^5.0.1", "pug-linker": "^4.0.0", "pug-load": "^3.0.0", "pug-parser": "^6.0.0", "pug-runtime": "^3.0.1", "pug-strip-comments": "^2.0.0" } }, "sha512-kFfq5mMzrS7+wrl5pLJzZEzemx34OQ0w4SARfhy/3yxTlhbstsudDwJzhf1hP02yHzbjoVMSXUj/Sz6RNfMyXg=="], - "pug": ["pug@3.0.3", "", { "dependencies": { "pug-code-gen": "^3.0.3", "pug-filters": "^4.0.0", "pug-lexer": "^5.0.1", "pug-linker": "^4.0.0", "pug-load": "^3.0.0", "pug-parser": "^6.0.0", "pug-runtime": "^3.0.1", "pug-strip-comments": "^2.0.0" } }, "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g=="], + "pug-attrs": ["pug-attrs@3.0.0", "", { "dependencies": { "constantinople": "^4.0.1", "js-stringify": "^1.0.2", "pug-runtime": "^3.0.0" } }, ""], - "pug-attrs": ["pug-attrs@3.0.0", "", { "dependencies": { "constantinople": "^4.0.1", "js-stringify": "^1.0.2", "pug-runtime": "^3.0.0" } }, "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA=="], + "pug-code-gen": ["pug-code-gen@3.0.4", "", { "dependencies": { "constantinople": "^4.0.1", "doctypes": "^1.1.0", "js-stringify": "^1.0.2", "pug-attrs": "^3.0.0", "pug-error": "^2.1.0", "pug-runtime": "^3.0.1", "void-elements": "^3.1.0", "with": "^7.0.0" } }, "sha512-6okWYIKdasTyXICyEtvobmTZAVX57JkzgzIi4iRJlin8kmhG+Xry2dsus+Mun/nGCn6F2U49haHI5mkELXB14g=="], - "pug-code-gen": ["pug-code-gen@3.0.3", "", { "dependencies": { "constantinople": "^4.0.1", "doctypes": "^1.1.0", "js-stringify": "^1.0.2", "pug-attrs": "^3.0.0", "pug-error": "^2.1.0", "pug-runtime": "^3.0.1", "void-elements": "^3.1.0", "with": "^7.0.0" } }, "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw=="], + "pug-error": ["pug-error@2.1.0", "", {}, ""], - "pug-error": ["pug-error@2.1.0", "", {}, "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg=="], + "pug-filters": ["pug-filters@4.0.0", "", { "dependencies": { "constantinople": "^4.0.1", "jstransformer": "1.0.0", "pug-error": "^2.0.0", "pug-walk": "^2.0.0", "resolve": "^1.15.1" } }, ""], - "pug-filters": ["pug-filters@4.0.0", "", { "dependencies": { "constantinople": "^4.0.1", "jstransformer": "1.0.0", "pug-error": "^2.0.0", "pug-walk": "^2.0.0", "resolve": "^1.15.1" } }, "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A=="], + "pug-lexer": ["pug-lexer@5.0.1", "", { "dependencies": { "character-parser": "^2.2.0", "is-expression": "^4.0.0", "pug-error": "^2.0.0" } }, ""], - "pug-lexer": ["pug-lexer@5.0.1", "", { "dependencies": { "character-parser": "^2.2.0", "is-expression": "^4.0.0", "pug-error": "^2.0.0" } }, "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w=="], + "pug-linker": ["pug-linker@4.0.0", "", { "dependencies": { "pug-error": "^2.0.0", "pug-walk": "^2.0.0" } }, ""], - "pug-linker": ["pug-linker@4.0.0", "", { "dependencies": { "pug-error": "^2.0.0", "pug-walk": "^2.0.0" } }, "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw=="], + "pug-load": ["pug-load@3.0.0", "", { "dependencies": { "object-assign": "^4.1.1", "pug-walk": "^2.0.0" } }, ""], - "pug-load": ["pug-load@3.0.0", "", { "dependencies": { "object-assign": "^4.1.1", "pug-walk": "^2.0.0" } }, "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ=="], + "pug-parser": ["pug-parser@6.0.0", "", { "dependencies": { "pug-error": "^2.0.0", "token-stream": "1.0.0" } }, ""], - "pug-parser": ["pug-parser@6.0.0", "", { "dependencies": { "pug-error": "^2.0.0", "token-stream": "1.0.0" } }, "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw=="], + "pug-runtime": ["pug-runtime@3.0.1", "", {}, ""], - "pug-runtime": ["pug-runtime@3.0.1", "", {}, "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg=="], + "pug-strip-comments": ["pug-strip-comments@2.0.0", "", { "dependencies": { "pug-error": "^2.0.0" } }, ""], - "pug-strip-comments": ["pug-strip-comments@2.0.0", "", { "dependencies": { "pug-error": "^2.0.0" } }, "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ=="], + "pug-walk": ["pug-walk@2.0.0", "", {}, ""], - "pug-walk": ["pug-walk@2.0.0", "", {}, "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, ""], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, ""], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "quansync": ["quansync@0.2.11", "", {}, ""], - "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, ""], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, ""], - "read-yaml-file": ["read-yaml-file@1.1.0", "", { "dependencies": { "graceful-fs": "^4.1.5", "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" } }, "sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA=="], + "repeat-string": ["repeat-string@1.6.1", "", {}, ""], - "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""], - "reprism": ["reprism@0.0.11", "", {}, "sha512-VsxDR5QxZo08M/3nRypNlScw5r3rKeSOPdU/QhDmu3Ai3BJxHn/qgfXGWQp/tAxUtzwYNo9W6997JZR0tPLZsA=="], + "resolve-from": ["resolve-from@5.0.0", "", {}, ""], - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, ""], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "reusify": ["reusify@1.1.0", "", {}, ""], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + "rfdc": ["rfdc@1.4.1", "", {}, ""], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, ""], - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, ""], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "semver": ["semver@7.7.3", "", { "bin": "bin/semver.js" }, ""], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "shebang-regex": ["shebang-regex@3.0.0", "", {}, ""], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, ""], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "slash": ["slash@3.0.0", "", {}, ""], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, ""], - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + "spark-md5": ["spark-md5@3.0.2", "", {}, ""], - "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, ""], - "spark-md5": ["spark-md5@3.0.2", "", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], + "sprintf-js": ["sprintf-js@1.0.3", "", {}, ""], - "spawndamnit": ["spawndamnit@3.0.1", "", { "dependencies": { "cross-spawn": "^7.0.5", "signal-exit": "^4.0.1" } }, "sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg=="], + "string-argv": ["string-argv@0.3.2", "", {}, ""], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, ""], - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, ""], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, ""], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, ""], - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""], - "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + "subset-font": ["subset-font@2.7.0", "", { "dependencies": { "fontverter": "^2.0.0", "harfbuzzjs": "^0.10.3", "p-limit": "^3.1.0" } }, "sha512-KoBEshkhodCspvR8DweQFkxRB0OxLmXiWV49y8O5fksD9gf/qBK8gcLwUOlGP4ktm55t2Ot0gLRsVQLT4391QQ=="], - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""], - "subset-font": ["subset-font@2.5.0", "", { "dependencies": { "fontverter": "^2.0.0", "harfbuzzjs": "^0.10.3", "lodash": "^4.17.21", "p-limit": "^3.1.0" } }, "sha512-Vsa8ngQ/ohhUj0an7on49y9jLZ2rK5U+T1FzPM4/ZQY0xUy5mLis6BfFtPGzecTjFgYXQlvY7FlsJF4t3R/6Ug=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, ""], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "synckit": ["synckit@0.11.13", "", { "dependencies": { "@pkgr/core": "^0.3.6" } }, "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "term-size": ["term-size@2.2.1", "", {}, ""], - "synckit": ["synckit@0.11.11", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw=="], + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], - "term-size": ["term-size@2.2.1", "", {}, "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "token-stream": ["token-stream@1.0.0", "", {}, ""], - "token-stream": ["token-stream@1.0.0", "", {}, "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "universalify": ["universalify@0.1.2", "", {}, ""], - "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "void-elements": ["void-elements@3.1.0", "", {}, ""], "wawoff2": ["wawoff2@2.0.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "woff2_compress.js": "bin/woff2_compress.js", "woff2_decompress.js": "bin/woff2_decompress.js" } }, "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""], - "with": ["with@7.0.2", "", { "dependencies": { "@babel/parser": "^7.9.6", "@babel/types": "^7.9.6", "assert-never": "^1.2.1", "babel-walk": "3.0.0-canary-5" } }, "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w=="], + "with": ["with@7.0.2", "", { "dependencies": { "@babel/parser": "^7.9.6", "@babel/types": "^7.9.6", "assert-never": "^1.2.1", "babel-walk": "3.0.0-canary-5" } }, ""], "woff2sfnt-sfnt2woff": ["woff2sfnt-sfnt2woff@1.0.0", "", { "dependencies": { "pako": "^1.0.7" } }, "sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, ""], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, ""], + + "wrappy": ["wrappy@1.0.2", "", {}, ""], + + "yaml": ["yaml@2.9.0", "", { "bin": "bin.mjs" }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, ""], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": "bin-prettier.js" }, ""], - "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": "bin-prettier.js" }, ""], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, ""], - "@changesets/apply-release-plan/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "@jscpd/badge-reporter/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - "@changesets/write/prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="], + "@jscpd/finder/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@jscpd/html-reporter/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - "@jscpd/finder/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, ""], - "@jscpd/html-reporter/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, ""], - "@manypkg/find-root/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, ""], - "@manypkg/find-root/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, ""], - "@manypkg/get-packages/@changesets/types": ["@changesets/types@4.1.0", "", {}, "sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw=="], + "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, ""], - "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "execa/signal-exit": ["signal-exit@3.0.7", "", {}, ""], - "cli-truncate/string-width": ["string-width@8.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" } }, "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""], - "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, ""], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "is-expression/acorn": ["acorn@7.4.1", "", { "bin": "bin/acorn" }, ""], - "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "jscpd/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - "is-expression/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + "jscpd-sarif-reporter/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - "jscpd/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "lint-staged/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "jscpd-sarif-reporter/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""], - "lint-staged/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "log-update/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "node-sarif-builder/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - "node-sarif-builder/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "read-yaml-file/js-yaml": ["js-yaml@3.15.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w=="], - "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, ""], - "read-yaml-file/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="], + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, ""], - "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, ""], - "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, ""], - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, ""], - "subset-font/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + "@jscpd/badge-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "@jscpd/badge-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, ""], - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "@jscpd/finder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "@jscpd/finder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "@jscpd/finder/fs-extra/universalify": ["universalify@2.0.1", "", {}, ""], - "@jscpd/finder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "@jscpd/html-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "@jscpd/html-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "@jscpd/html-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, ""], - "@jscpd/html-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, ""], - "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, ""], - "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + "jscpd-sarif-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "jscpd-sarif-reporter/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jscpd-sarif-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, ""], - "jscpd-sarif-reporter/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "jscpd/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "jscpd/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "jscpd/fs-extra/universalify": ["universalify@2.0.1", "", {}, ""], - "jscpd/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, ""], - "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "node-sarif-builder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, ""], - "node-sarif-builder/fs-extra/jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "node-sarif-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, ""], - "node-sarif-builder/fs-extra/universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, ""], - "read-yaml-file/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, ""], - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, ""], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, ""], - "@manypkg/find-root/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, ""], - "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "@manypkg/find-root/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, ""], } } diff --git a/js/docs/case-studies/issue-153/README.md b/js/docs/case-studies/issue-153/README.md index 81219165..40dbc8f1 100644 --- a/js/docs/case-studies/issue-153/README.md +++ b/js/docs/case-studies/issue-153/README.md @@ -156,7 +156,7 @@ When you see errors like these, suspect the Array.join() pitfall: ## Related Documentation -- [BEST-PRACTICES.md](../../BEST-PRACTICES.md) - Best practices for command-stream usage +- [BEST-PRACTICES.md](../../../BEST-PRACTICES.md) - Best practices for command-stream usage - [README.md](../../../../README.md) - Common Pitfalls section - [$.quote.mjs](../../../src/$.quote.mjs) - Quote function implementation diff --git a/js/eslint.config.js b/js/eslint.config.js index c878573f..c2973e11 100644 --- a/js/eslint.config.js +++ b/js/eslint.config.js @@ -1,3 +1,9 @@ +// ESLint runs with `js/` as the working directory (`bun run lint`), while +// lint-staged and editors may invoke it from the repository root. Flat-config +// globs resolve against the working directory, so every override below is +// anchored with `**/` and matches under either one. Anchoring the globs to one +// directory is how `js/tests/**` and `tests/**` ended up duplicated here, with +// one of the two always dead. import js from '@eslint/js'; import prettierConfig from 'eslint-config-prettier'; import prettierPlugin from 'eslint-plugin-prettier'; @@ -147,14 +153,7 @@ export default [ }, { // Test files have different requirements - files: [ - 'tests/**/*.js', - 'tests/**/*.mjs', - 'js/tests/**/*.js', - 'js/tests/**/*.mjs', - '**/*.test.js', - '**/*.test.mjs', - ], + files: ['**/tests/**/*.{js,mjs}', '**/*.test.{js,mjs}'], rules: { 'no-unused-vars': 'off', // Tests often have unused vars for demonstration or intentional non-use 'require-await': 'off', // Async functions without await are common in tests @@ -173,13 +172,9 @@ export default [ { // Example, experiment, and debug files are more lenient files: [ - 'examples/**/*.js', - 'examples/**/*.mjs', - 'js/examples/**/*.js', - 'js/examples/**/*.mjs', - 'experiments/**/*.js', - 'experiments/**/*.mjs', - 'claude-profiles.mjs', + '**/examples/**/*.{js,mjs}', + '**/experiments/**/*.{js,mjs}', + '**/claude-profiles.mjs', ], rules: { 'no-unused-vars': 'off', // Examples often have unused vars for demonstration @@ -202,7 +197,7 @@ export default [ }, { // Virtual command implementations have specific interface requirements - files: ['src/commands/**/*.mjs', 'js/src/commands/**/*.mjs'], + files: ['**/src/commands/**/*.mjs'], rules: { 'require-await': 'off', // Commands must be async to match interface even if they don't await complexity: 'off', // Commands can be complex due to argument parsing and validation @@ -228,10 +223,8 @@ export default [ // These wrapper functions are larger than typical functions but contain // method definitions themselves, not complex logic. files: [ - 'js/src/$.process-runner-execution.mjs', - 'js/src/$.process-runner-pipeline.mjs', - 'src/$.process-runner-execution.mjs', - 'src/$.process-runner-pipeline.mjs', + '**/src/$.process-runner-execution.mjs', + '**/src/$.process-runner-pipeline.mjs', ], rules: { 'max-lines-per-function': [ @@ -246,14 +239,21 @@ export default [ }, { ignores: [ - 'node_modules/**', - 'coverage/**', - 'dist/**', - '*.min.js', - '.eslintcache', - 'docs/case-studies/**/data/**', - 'docs/case-studies/**/log-excerpts/**', - 'docs/case-studies/**/templates/**', + '**/node_modules/**', + '**/coverage/**', + '**/dist/**', + // Generated by `bun run check:duplication`; running it before `bun run + // lint` used to make the lint step fail on jscpd's bundled prism.js. + '**/reports/**', + '**/*.min.js', + '**/.eslintcache', + // Build output: rustdoc ships minified JavaScript that is not ours. + '**/rust/target/**', + '**/docs/case-studies/**/data/**', + '**/docs/case-studies/**/log-excerpts/**', + '**/docs/case-studies/**/templates/**', + // Collected CI evidence, kept verbatim. + '**/dev/log/**', ], }, ]; diff --git a/js/package-lock.json b/js/package-lock.json index ab186772..ce1349b1 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -12,19 +12,19 @@ "@resvg/resvg-js": "^2.6.2", "@xterm/headless": "^6.0.0", "gifenc": "^1.0.3", - "node-pty": "^1.2.0-beta.14" + "node-pty": "^1.2.0-beta.15" }, "devDependencies": { - "@changesets/cli": "^2.29.7", + "@changesets/cli": "^2.31.1", "dejavu-fonts-ttf": "^2.37.3", - "eslint": "^9.38.0", + "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-prettier": "^5.5.6", "husky": "^9.1.7", - "jscpd": "^4.0.5", - "lint-staged": "^16.2.6", - "prettier": "^3.6.2", - "subset-font": "^2.5.0" + "jscpd": "^4.3.0", + "lint-staged": "^16.4.0", + "prettier": "^3.9.6", + "subset-font": "^2.7.0" }, "engines": { "bun": ">=1.0.0", @@ -82,11 +82,11 @@ } }, "node_modules/@changesets/apply-release-plan": { - "version": "7.0.14", + "version": "7.1.1", "dev": true, "license": "MIT", "dependencies": { - "@changesets/config": "^3.1.2", + "@changesets/config": "^3.1.4", "@changesets/get-version-range-type": "^0.4.0", "@changesets/git": "^3.0.4", "@changesets/should-skip-package": "^0.1.2", @@ -116,12 +116,12 @@ } }, "node_modules/@changesets/assemble-release-plan": { - "version": "6.0.9", + "version": "6.0.10", "dev": true, "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-dependents-graph": "^2.1.4", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", @@ -137,32 +137,30 @@ } }, "node_modules/@changesets/cli": { - "version": "2.29.8", + "version": "2.31.1", "dev": true, "license": "MIT", "dependencies": { - "@changesets/apply-release-plan": "^7.0.14", - "@changesets/assemble-release-plan": "^6.0.9", + "@changesets/apply-release-plan": "^7.1.1", + "@changesets/assemble-release-plan": "^6.0.10", "@changesets/changelog-git": "^0.2.1", - "@changesets/config": "^3.1.2", + "@changesets/config": "^3.1.4", "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", - "@changesets/get-release-plan": "^4.0.14", + "@changesets/get-dependents-graph": "^2.1.4", + "@changesets/get-release-plan": "^4.0.16", "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.6", + "@changesets/read": "^0.6.7", "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@changesets/write": "^0.4.0", "@inquirer/external-editor": "^1.0.2", "@manypkg/get-packages": "^1.1.3", "ansi-colors": "^4.1.3", - "ci-info": "^3.7.0", "enquirer": "^2.4.1", "fs-extra": "^7.0.1", "mri": "^1.2.0", - "p-limit": "^2.2.0", "package-manager-detector": "^0.2.0", "picocolors": "^1.1.0", "resolve-from": "^5.0.0", @@ -175,13 +173,14 @@ } }, "node_modules/@changesets/config": { - "version": "3.1.2", + "version": "3.1.4", "dev": true, "license": "MIT", "dependencies": { "@changesets/errors": "^0.2.0", - "@changesets/get-dependents-graph": "^2.1.3", + "@changesets/get-dependents-graph": "^2.1.4", "@changesets/logger": "^0.1.1", + "@changesets/should-skip-package": "^0.1.2", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", @@ -197,7 +196,7 @@ } }, "node_modules/@changesets/get-dependents-graph": { - "version": "2.1.3", + "version": "2.1.4", "dev": true, "license": "MIT", "dependencies": { @@ -208,14 +207,14 @@ } }, "node_modules/@changesets/get-release-plan": { - "version": "4.0.14", + "version": "4.0.16", "dev": true, "license": "MIT", "dependencies": { - "@changesets/assemble-release-plan": "^6.0.9", - "@changesets/config": "^3.1.2", + "@changesets/assemble-release-plan": "^6.0.10", + "@changesets/config": "^3.1.4", "@changesets/pre": "^2.0.2", - "@changesets/read": "^0.6.6", + "@changesets/read": "^0.6.7", "@changesets/types": "^6.1.0", "@manypkg/get-packages": "^1.1.3" } @@ -246,7 +245,7 @@ } }, "node_modules/@changesets/parse": { - "version": "0.4.2", + "version": "0.4.3", "dev": true, "license": "MIT", "dependencies": { @@ -266,13 +265,13 @@ } }, "node_modules/@changesets/read": { - "version": "0.6.6", + "version": "0.6.7", "dev": true, "license": "MIT", "dependencies": { "@changesets/git": "^3.0.4", "@changesets/logger": "^0.1.1", - "@changesets/parse": "^0.4.2", + "@changesets/parse": "^0.4.3", "@changesets/types": "^6.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0", @@ -364,13 +363,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", + "version": "0.21.2", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -399,18 +398,18 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", + "version": "3.3.7", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -421,7 +420,7 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", + "version": "9.39.5", "dev": true, "license": "MIT", "engines": { @@ -452,25 +451,37 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", + "version": "0.19.2", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", + "version": "0.16.8", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "dev": true, @@ -515,8 +526,50 @@ } } }, + "node_modules/@jscpd/badge-reporter": { + "version": "4.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "badgen": "^3.2.3", + "colors": "^1.4.0", + "fs-extra": "^11.2.0" + } + }, + "node_modules/@jscpd/badge-reporter/node_modules/fs-extra": { + "version": "11.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@jscpd/badge-reporter/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@jscpd/badge-reporter/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@jscpd/core": { - "version": "4.0.1", + "version": "4.2.5", "dev": true, "license": "MIT", "dependencies": { @@ -524,24 +577,24 @@ } }, "node_modules/@jscpd/finder": { - "version": "4.0.1", + "version": "4.3.0", "dev": true, "license": "MIT", "dependencies": { - "@jscpd/core": "4.0.1", - "@jscpd/tokenizer": "4.0.1", + "@jscpd/core": "4.2.5", + "@jscpd/tokenizer": "4.2.6", "blamer": "^1.0.6", "bytes": "^3.1.2", "cli-table3": "^0.6.5", "colors": "^1.4.0", "fast-glob": "^3.3.2", - "fs-extra": "^11.2.0", + "fs-extra": "^11.3.6", "markdown-table": "^2.0.0", - "pug": "^3.0.3" + "pug": "^3.0.4" } }, "node_modules/@jscpd/finder/node_modules/fs-extra": { - "version": "11.3.3", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -573,17 +626,17 @@ } }, "node_modules/@jscpd/html-reporter": { - "version": "4.0.1", + "version": "4.2.5", "dev": true, "license": "MIT", "dependencies": { "colors": "1.4.0", "fs-extra": "^11.2.0", - "pug": "^3.0.3" + "pug": "^3.0.4" } }, "node_modules/@jscpd/html-reporter/node_modules/fs-extra": { - "version": "11.3.3", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -615,12 +668,11 @@ } }, "node_modules/@jscpd/tokenizer": { - "version": "4.0.1", + "version": "4.2.6", "dev": true, "license": "MIT", "dependencies": { - "@jscpd/core": "4.0.1", - "reprism": "^0.0.11", + "@jscpd/core": "4.2.5", "spark-md5": "^3.0.2" } }, @@ -676,6 +728,20 @@ "node": ">=8" } }, + "node_modules/@manypkg/find-root/node_modules/find-up/node_modules/locate-path/node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@manypkg/find-root/node_modules/fs-extra": { "version": "8.1.0", "dev": true, @@ -753,11 +819,11 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.9", + "version": "0.3.6", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/pkgr" @@ -765,8 +831,6 @@ }, "node_modules/@resvg/resvg-js": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", - "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", "license": "MPL-2.0", "engines": { "node": ">= 10" @@ -906,8 +970,6 @@ }, "node_modules/@resvg/resvg-js-linux-x64-gnu": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", - "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", "cpu": [ "x64" ], @@ -925,8 +987,6 @@ }, "node_modules/@resvg/resvg-js-linux-x64-musl": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", - "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", "cpu": [ "x64" ], @@ -1007,8 +1067,6 @@ }, "node_modules/@xterm/headless": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.0.0.tgz", - "integrity": "sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==", "license": "MIT", "workspaces": [ "addons/*" @@ -1034,7 +1092,7 @@ } }, "node_modules/ajv": { - "version": "6.12.6", + "version": "6.15.0", "dev": true, "license": "MIT", "dependencies": { @@ -1126,6 +1184,11 @@ "node": ">= 10.0.0" } }, + "node_modules/badgen": { + "version": "3.3.2", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, @@ -1155,7 +1218,7 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.18", "dev": true, "license": "MIT", "dependencies": { @@ -1245,20 +1308,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ci-info": { - "version": "3.9.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cli-cursor": { "version": "5.0.0", "dev": true, @@ -1372,11 +1421,11 @@ } }, "node_modules/commander": { - "version": "5.1.0", + "version": "15.0.0", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=22.12.0" } }, "node_modules/concat-map": { @@ -1429,8 +1478,6 @@ }, "node_modules/dejavu-fonts-ttf": { "version": "2.37.3", - "resolved": "https://registry.npmjs.org/dejavu-fonts-ttf/-/dejavu-fonts-ttf-2.37.3.tgz", - "integrity": "sha512-f1hd7jJbeQa1VWcw+K2KrTXS50zTMaHpVC4XIKJpNcDeYR5ajMtj/iLlQDYNvLOKamUB3ARVVCf79lNwNVztSQ==", "dev": true, "license": "SEE LICENSE IN README.md AND LICENSE" }, @@ -1546,23 +1593,23 @@ } }, "node_modules/eslint": { - "version": "9.39.2", + "version": "9.39.5", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -1581,7 +1628,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -1618,12 +1665,12 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", + "version": "5.5.6", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -1879,14 +1926,14 @@ } }, "node_modules/flatted": { - "version": "3.3.3", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, "node_modules/fontverter": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fontverter/-/fontverter-2.0.0.tgz", - "integrity": "sha512-DFVX5hvXuhi1Jven1tbpebYTCT9XYnvx6/Z+HFUPb7ZRMCW+pj2clU9VMhoTPgWKPhAs7JJDSk3CW1jNUvKCZQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1977,18 +2024,8 @@ }, "node_modules/gifenc": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/gifenc/-/gifenc-1.0.3.tgz", - "integrity": "sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==", "license": "MIT" }, - "node_modules/gitignore-to-glob": { - "version": "0.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.4 <5 || >=6.9" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "dev": true, @@ -2048,8 +2085,6 @@ }, "node_modules/harfbuzzjs": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.10.3.tgz", - "integrity": "sha512-GJnLUrgLMadlMYrBGEXwYEimObbysy3prWT4HyPpFQERvgTU/OZ+ReUlEPOum6w4RBtFXzXiCCmECOr4sz3qwQ==", "dev": true, "license": "MIT" }, @@ -2313,8 +2348,18 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", + "version": "4.3.2", "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" @@ -2324,36 +2369,36 @@ } }, "node_modules/jscpd": { - "version": "4.0.5", + "version": "4.3.0", "dev": true, "license": "MIT", "dependencies": { - "@jscpd/core": "4.0.1", - "@jscpd/finder": "4.0.1", - "@jscpd/html-reporter": "4.0.1", - "@jscpd/tokenizer": "4.0.1", + "@jscpd/badge-reporter": "4.2.5", + "@jscpd/core": "4.2.5", + "@jscpd/finder": "4.3.0", + "@jscpd/html-reporter": "4.2.5", + "@jscpd/tokenizer": "4.2.6", "colors": "^1.4.0", - "commander": "^5.0.0", - "fs-extra": "^11.2.0", - "gitignore-to-glob": "^0.3.0", - "jscpd-sarif-reporter": "4.0.3" + "commander": "^15.0.0", + "fs-extra": "^11.3.6", + "jscpd-sarif-reporter": "4.2.5" }, "bin": { "jscpd": "bin/jscpd" } }, "node_modules/jscpd-sarif-reporter": { - "version": "4.0.3", + "version": "4.2.5", "dev": true, "license": "MIT", "dependencies": { "colors": "^1.4.0", "fs-extra": "^11.2.0", - "node-sarif-builder": "^2.0.3" + "node-sarif-builder": "^4.1.0" } }, "node_modules/jscpd-sarif-reporter/node_modules/fs-extra": { - "version": "11.3.3", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -2385,7 +2430,7 @@ } }, "node_modules/jscpd/node_modules/fs-extra": { - "version": "11.3.3", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -2469,17 +2514,16 @@ } }, "node_modules/lint-staged": { - "version": "16.2.7", + "version": "16.4.0", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.2", + "commander": "^14.0.3", "listr2": "^9.0.5", - "micromatch": "^4.0.8", - "nano-spawn": "^2.0.0", - "pidtree": "^0.6.0", + "picomatch": "^4.0.3", "string-argv": "^0.3.2", - "yaml": "^2.8.1" + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -2492,7 +2536,7 @@ } }, "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.2", + "version": "14.0.3", "dev": true, "license": "MIT", "engines": { @@ -2529,13 +2573,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, @@ -2634,6 +2671,17 @@ "node": ">=8.6" } }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "dev": true, @@ -2654,7 +2702,7 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", "dev": true, "license": "ISC", "dependencies": { @@ -2677,17 +2725,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -2695,14 +2732,10 @@ }, "node_modules/node-addon-api": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, "node_modules/node-pty": { - "version": "1.2.0-beta.14", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.14.tgz", - "integrity": "sha512-XORU9BQgpxVgqr7WivjJ17mLenOHUgKKWzuZZNaw3NDYgHc/wPJQMSaoLDrpEgqV6aU1nNwil1o/OqYj6lWmUA==", + "version": "1.2.0-beta.15", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2710,19 +2743,19 @@ } }, "node_modules/node-sarif-builder": { - "version": "2.0.3", + "version": "4.1.0", "dev": true, "license": "MIT", "dependencies": { - "@types/sarif": "^2.1.4", - "fs-extra": "^10.0.0" + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" }, "engines": { - "node": ">=14" + "node": ">=20" } }, "node_modules/node-sarif-builder/node_modules/fs-extra": { - "version": "10.1.0", + "version": "11.4.0", "dev": true, "license": "MIT", "dependencies": { @@ -2731,7 +2764,7 @@ "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/node-sarif-builder/node_modules/fs-extra/node_modules/jsonfile": { @@ -2827,14 +2860,14 @@ } }, "node_modules/p-limit": { - "version": "2.3.0", + "version": "3.1.0", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2894,8 +2927,6 @@ }, "node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true, "license": "(MIT AND Zlib)" }, @@ -2945,27 +2976,16 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "4.0.7", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/pify": { "version": "4.0.1", "dev": true, @@ -2983,7 +3003,7 @@ } }, "node_modules/prettier": { - "version": "3.7.4", + "version": "3.9.6", "dev": true, "license": "MIT", "bin": { @@ -2997,7 +3017,7 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", + "version": "1.0.1", "dev": true, "license": "MIT", "dependencies": { @@ -3016,11 +3036,11 @@ } }, "node_modules/pug": { - "version": "3.0.3", + "version": "3.0.4", "dev": true, "license": "MIT", "dependencies": { - "pug-code-gen": "^3.0.3", + "pug-code-gen": "^3.0.4", "pug-filters": "^4.0.0", "pug-lexer": "^5.0.1", "pug-linker": "^4.0.0", @@ -3041,7 +3061,7 @@ } }, "node_modules/pug-code-gen": { - "version": "3.0.3", + "version": "3.0.4", "dev": true, "license": "MIT", "dependencies": { @@ -3192,8 +3212,18 @@ "node": ">=6" } }, + "node_modules/read-yaml-file/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/read-yaml-file/node_modules/js-yaml": { - "version": "3.14.2", + "version": "3.15.2", "dev": true, "license": "MIT", "dependencies": { @@ -3204,14 +3234,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/read-yaml-file/node_modules/js-yaml/node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/repeat-string": { "version": "1.6.1", "dev": true, @@ -3220,11 +3242,6 @@ "node": ">=0.10" } }, - "node_modules/reprism": { - "version": "0.0.11", - "dev": true, - "license": "MIT" - }, "node_modules/resolve": { "version": "1.22.11", "dev": true, @@ -3490,34 +3507,15 @@ } }, "node_modules/subset-font": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/subset-font/-/subset-font-2.5.0.tgz", - "integrity": "sha512-Vsa8ngQ/ohhUj0an7on49y9jLZ2rK5U+T1FzPM4/ZQY0xUy5mLis6BfFtPGzecTjFgYXQlvY7FlsJF4t3R/6Ug==", + "version": "2.7.0", "dev": true, "license": "BSD-3-Clause", "dependencies": { "fontverter": "^2.0.0", "harfbuzzjs": "^0.10.3", - "lodash": "^4.17.21", "p-limit": "^3.1.0" } }, - "node_modules/subset-font/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -3541,11 +3539,11 @@ } }, "node_modules/synckit": { - "version": "0.11.11", + "version": "0.11.13", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.9" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3565,6 +3563,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinyexec": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "dev": true, @@ -3618,8 +3624,6 @@ }, "node_modules/wawoff2": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wawoff2/-/wawoff2-2.0.1.tgz", - "integrity": "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==", "dev": true, "license": "MIT", "dependencies": { @@ -3660,8 +3664,6 @@ }, "node_modules/woff2sfnt-sfnt2woff": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/woff2sfnt-sfnt2woff/-/woff2sfnt-sfnt2woff-1.0.0.tgz", - "integrity": "sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3755,7 +3757,7 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", + "version": "2.9.0", "dev": true, "license": "ISC", "bin": { diff --git a/js/package.json b/js/package.json index 748186a2..c86c606b 100644 --- a/js/package.json +++ b/js/package.json @@ -32,11 +32,11 @@ "test:sync": "cd .. && bun test js/tests/sync.test.mjs --timeout 10000", "test:builtin": "cd .. && bun test js/tests/builtin-commands.test.mjs --timeout 10000", "test:pipe": "cd .. && bun test js/tests/pipe.test.mjs --timeout 10000", - "lint": "eslint .", - "lint:fix": "eslint . --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "check:duplication": "jscpd .", + "lint": "cd .. && js/node_modules/.bin/eslint . --max-warnings 0", + "lint:fix": "cd .. && js/node_modules/.bin/eslint . --fix --max-warnings 0", + "format": "cd .. && js/node_modules/.bin/prettier --write .", + "format:check": "cd .. && js/node_modules/.bin/prettier --check .", + "check:duplication": "jscpd src scripts", "check": "bun run lint && bun run format:check && bun run check:duplication", "build:terminal-font": "node scripts/build-terminal-font.mjs", "prepare": "cd .. && js/node_modules/.bin/husky || true", @@ -70,32 +70,21 @@ "examples/" ], "devDependencies": { - "@changesets/cli": "^2.29.7", + "@changesets/cli": "^2.31.1", "dejavu-fonts-ttf": "^2.37.3", - "eslint": "^9.38.0", + "eslint": "^9.39.5", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-prettier": "^5.5.6", "husky": "^9.1.7", - "jscpd": "^4.0.5", - "lint-staged": "^16.2.6", - "prettier": "^3.6.2", - "subset-font": "^2.5.0" - }, - "lint-staged": { - "*.{js,mjs,cjs}": [ - "eslint --fix --max-warnings 0 --no-warn-ignored", - "prettier --write", - "prettier --check" - ], - "*.md": [ - "prettier --write", - "prettier --check" - ] + "jscpd": "^4.3.0", + "lint-staged": "^16.4.0", + "prettier": "^3.9.6", + "subset-font": "^2.7.0" }, "dependencies": { "@resvg/resvg-js": "^2.6.2", "@xterm/headless": "^6.0.0", "gifenc": "^1.0.3", - "node-pty": "^1.2.0-beta.14" + "node-pty": "^1.2.0-beta.15" } } diff --git a/js/scripts/changeset-version.mjs b/js/scripts/changeset-version.mjs index 88133448..7ce2dd2c 100644 --- a/js/scripts/changeset-version.mjs +++ b/js/scripts/changeset-version.mjs @@ -13,10 +13,10 @@ * - command-stream: Modern shell command execution with streaming support */ -// Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +import { loadUseM } from './use-m-loader.mjs'; + +// Load use-m dynamically, retrying a CDN blip instead of dying at module load. +const use = await loadUseM(); // Import command-stream for shell command execution const { $ } = await use('command-stream'); diff --git a/js/scripts/check-release-needed.mjs b/js/scripts/check-release-needed.mjs index 86f8e9db..a97bb5fe 100644 --- a/js/scripts/check-release-needed.mjs +++ b/js/scripts/check-release-needed.mjs @@ -53,11 +53,10 @@ */ import { readFileSync, appendFileSync } from 'fs'; +import { loadUseM } from './use-m-loader.mjs'; // Load use-m dynamically (matches the other release scripts in this folder). -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); const { $ } = await use('command-stream'); diff --git a/js/scripts/create-github-release.mjs b/js/scripts/create-github-release.mjs index 430b9378..c94cd586 100644 --- a/js/scripts/create-github-release.mjs +++ b/js/scripts/create-github-release.mjs @@ -13,11 +13,10 @@ */ import { readFileSync } from 'fs'; +import { loadUseM } from './use-m-loader.mjs'; // Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); diff --git a/js/scripts/create-manual-changeset.mjs b/js/scripts/create-manual-changeset.mjs index 60bfac68..4fce398c 100644 --- a/js/scripts/create-manual-changeset.mjs +++ b/js/scripts/create-manual-changeset.mjs @@ -14,13 +14,12 @@ import { writeFileSync } from 'fs'; import { randomBytes } from 'crypto'; +import { loadUseM } from './use-m-loader.mjs'; const PACKAGE_NAME = 'command-stream'; // Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); diff --git a/js/scripts/debug-print.mjs b/js/scripts/debug-print.mjs new file mode 100644 index 00000000..53b2764c --- /dev/null +++ b/js/scripts/debug-print.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +/** + * Lightweight debug logger for the pipeline scripts. + * + * When a dependency fails to load, a bare `$ is not a function` says nothing + * about the module shape that produced it. This helper lets the scripts keep + * such tracing in the code with the default state switched off, so an interop + * regression stays readable from the CI log alone. + * + * Activation: + * - CI_SCRIPTS_DEBUG=1 (preferred local toggle), or + * - RUNNER_DEBUG=1 (GitHub "Re-run all jobs with debug logging"), or + * - ACTIONS_STEP_DEBUG=true (the secret-gated workflow debug switch). + * + * Every line is prefixed with `::debug::` so GitHub Actions renders it in the + * collapsible debug stream, keeping the main log clean. + * + * Usage: + * import { debug } from './debug-print.mjs'; + * debug('loaded command-stream', { keys }); + */ + +/** + * Read one environment variable without ever throwing. + * + * Deno denies `process.env` access unless the run was granted `--allow-env` + * (the Deno test job only passes `--allow-read`), and the denial surfaces as a + * `NotCapable` error on the property read itself. Tracing must never be the + * reason a script or a test fails, so an unreadable variable counts as unset. + * + * @param {string} name variable to read + * @param {Record} [env] explicit source, for tests + * @returns {string | undefined} + */ +export function readEnvVar(name, env) { + const source = + env ?? (typeof process === 'undefined' ? undefined : process.env); + if (!source) { + return undefined; + } + try { + return source[name]; + } catch { + return undefined; + } +} + +/** + * @param {Record} [env] explicit source, for tests + * @returns {boolean} true when debug output is enabled for this process + */ +export function isDebugEnabled(env) { + const flag = readEnvVar('CI_SCRIPTS_DEBUG', env); + return ( + flag === '1' || + flag === 'true' || + readEnvVar('RUNNER_DEBUG', env) === '1' || + readEnvVar('ACTIONS_STEP_DEBUG', env) === 'true' + ); +} + +function format(value) { + if (typeof value === 'string') { + return value; + } + try { + return JSON.stringify(value, null, 2); + } catch { + return String(value); + } +} + +/** + * Render the lines `debug()` would print, without printing them. + * @param {unknown[]} parts values to join into one message + * @returns {string[]} `::debug::`-prefixed lines + */ +export function formatDebugLines(parts) { + return parts + .map(format) + .join(' ') + .split('\n') + .map((chunk) => `::debug::${chunk}`); +} + +/** + * Print a debug line through injected collaborators, but only when debug + * output is enabled. `debug()` is the production binding of this function. + * + * @param {{env?: Record, log?: (line: string) => void}} options + * @param {...unknown} parts values to join into one message + * @returns {string[]} the lines printed, empty when debug output is off + */ +export function debugWith(options, ...parts) { + const { env, log = console.log } = options ?? {}; + if (!isDebugEnabled(env)) { + return []; + } + const lines = formatDebugLines(parts); + for (const line of lines) { + log(line); + } + return lines; +} + +/** + * Print a debug line, but only when debug output is enabled. + * @param {...unknown} parts values to join into one message + */ +export function debug(...parts) { + return debugWith({}, ...parts); +} diff --git a/js/scripts/format-github-release.mjs b/js/scripts/format-github-release.mjs index 8ab39b1a..c60c09c0 100644 --- a/js/scripts/format-github-release.mjs +++ b/js/scripts/format-github-release.mjs @@ -13,10 +13,10 @@ * - lino-arguments: Unified configuration from CLI args, env vars, and .lenv files */ -// Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +import { loadUseM } from './use-m-loader.mjs'; + +// Load use-m dynamically, retrying a CDN blip instead of dying at module load. +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); diff --git a/js/scripts/format-release-notes.mjs b/js/scripts/format-release-notes.mjs index 1e5a15e9..c7340a04 100644 --- a/js/scripts/format-release-notes.mjs +++ b/js/scripts/format-release-notes.mjs @@ -23,12 +23,12 @@ * Note: Uses --release-version instead of --version to avoid conflict with yargs' built-in --version flag. */ +import { loadUseM } from './use-m-loader.mjs'; + const PACKAGE_NAME = 'command-stream'; // Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); diff --git a/js/scripts/instant-version-bump.mjs b/js/scripts/instant-version-bump.mjs index 8d15132c..f78fee05 100644 --- a/js/scripts/instant-version-bump.mjs +++ b/js/scripts/instant-version-bump.mjs @@ -13,11 +13,10 @@ */ import { readFileSync, writeFileSync } from 'fs'; +import { loadUseM } from './use-m-loader.mjs'; // Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); diff --git a/js/scripts/npm-registry.mjs b/js/scripts/npm-registry.mjs new file mode 100644 index 00000000..f1472d7b --- /dev/null +++ b/js/scripts/npm-registry.mjs @@ -0,0 +1,100 @@ +export const DEFAULT_NPM_REGISTRY_URL = 'https://registry.npmjs.org'; + +function getNpmRegistryFromEnv() { + try { + // npm itself reads the lowercase `npm_config_registry` form, so honor both. + return ( + process.env.NPM_CONFIG_REGISTRY || process.env.npm_config_registry || '' + ); + } catch { + return ''; + } +} + +/** + * Normalize an npm registry URL so package metadata paths can be appended. + * @param {string} registryUrl + * @returns {string} + */ +export function normalizeRegistryUrl( + registryUrl = getNpmRegistryFromEnv() || DEFAULT_NPM_REGISTRY_URL +) { + return String(registryUrl || DEFAULT_NPM_REGISTRY_URL).replace(/\/+$/, ''); +} + +/** + * Encode a package name for npm registry metadata URLs. + * @param {string} packageName + * @returns {string} + */ +export function encodePackageName(packageName) { + if (typeof packageName !== 'string' || packageName.trim() === '') { + throw new Error('Package name is required'); + } + + if (packageName.startsWith('@')) { + const [scope, name] = packageName.split('/'); + if (!scope || !name) { + throw new Error(`Invalid scoped package name: ${packageName}`); + } + return `${scope}%2F${encodeURIComponent(name)}`; + } + + return encodeURIComponent(packageName); +} + +/** + * Build the npm registry package metadata URL. + * @param {string} packageName + * @param {string} registryUrl + * @returns {string} + */ +export function buildPackageMetadataUrl( + packageName, + registryUrl = getNpmRegistryFromEnv() || DEFAULT_NPM_REGISTRY_URL +) { + return `${normalizeRegistryUrl(registryUrl)}/${encodePackageName(packageName)}`; +} + +/** + * Check whether a package version exists in npm registry metadata. + * HTTP 404 means the package has not been published yet and is not an error. + * @param {string} packageName + * @param {string} version + * @param {object} options + * @param {Function} [options.fetchFn] + * @param {string} [options.registryUrl] + * @returns {Promise} + */ +export async function isPackageVersionPublished( + packageName, + version, + { + fetchFn = fetch, + registryUrl = getNpmRegistryFromEnv() || DEFAULT_NPM_REGISTRY_URL, + } = {} +) { + if (typeof version !== 'string' || version.trim() === '') { + throw new Error('Package version is required'); + } + + const metadataUrl = buildPackageMetadataUrl(packageName, registryUrl); + const response = await fetchFn(metadataUrl, { + headers: { + accept: 'application/json', + }, + }); + + if (response.status === 404) { + return false; + } + + if (!response.ok) { + throw new Error( + `Failed to fetch npm package metadata for ${packageName}: ${response.status} ${response.statusText}` + ); + } + + const metadata = await response.json(); + return Object.hasOwn(metadata?.versions || {}, version); +} diff --git a/js/scripts/publish-failure-classifier.mjs b/js/scripts/publish-failure-classifier.mjs new file mode 100644 index 00000000..cbd818a7 --- /dev/null +++ b/js/scripts/publish-failure-classifier.mjs @@ -0,0 +1,77 @@ +/** + * Classify npm publish failures and build actionable guidance. + * + * Some publish failures are permanent: retrying a 404/401/403 (or any auth / + * registry-configuration error) produces the same error every time and only + * delays a clear, actionable message. The most common case is the FIRST publish + * of a brand-new package via npm OIDC trusted publishing, which returns E404 + * because npm cannot bootstrap a new package with trusted publishing alone — a + * trusted publisher can only be configured for a package that already exists. + * + * Addresses issue: + * - link-foundation/js-ai-driven-development-pipeline-template#77 + */ + +// Failures caused by authentication / registry configuration. Retrying these is +// pointless and only hides the real cause behind a generic +// "Failed to publish after N attempts" message. +export const NON_RETRYABLE_PATTERNS = [ + 'npm error 404', + 'npm error 401', + 'npm error 403', + 'e404', + 'e401', + 'e403', + 'access token expired', + 'eneedauth', + 'you must be logged in', + 'unable to authenticate', +]; + +/** + * Determine whether a detected failure is caused by authentication / registry + * configuration (and therefore should not be retried). + * @param {string} output - Combined stdout and stderr (and/or error message) + * @returns {boolean} + */ +export function isNonRetryableFailure(output) { + const lowerOutput = String(output || '').toLowerCase(); + return NON_RETRYABLE_PATTERNS.some((pattern) => + lowerOutput.includes(pattern) + ); +} + +/** + * Build an actionable, human-readable explanation for an authentication / + * registry-configuration publish failure (most commonly an E404 on the very + * first publish of a brand-new package via OIDC trusted publishing). + * @param {string} packageName - The package that failed to publish + * @returns {string} + */ +export function buildAuthFailureGuidance(packageName) { + return [ + '', + '=== NPM PUBLISH AUTHENTICATION / REGISTRY FAILURE ===', + '', + `Failed to publish ${packageName}. This is an authentication or registry`, + 'configuration error, not a transient one, so it was not retried.', + '', + 'Most common cause: the FIRST publish of a brand-new package via npm OIDC', + 'trusted publishing returns "E404 Not Found - PUT". npm cannot bootstrap a', + 'new package with trusted publishing alone, because a trusted publisher can', + 'only be configured for a package that already exists on the registry.', + '', + 'SOLUTION (choose one):', + ' 1. Bootstrap the first release with a classic automation token:', + ' - Create a granular/automation token on npmjs.com with publish access.', + ' - Add it as the repository secret NPM_TOKEN.', + ' - The release workflow passes it as NODE_AUTH_TOKEN automatically, so', + ' the next run will publish the initial version.', + ' 2. After the package exists, configure OIDC trusted publishing on', + ' npmjs.com (Package settings -> Trusted publishing) so future releases', + ' need no token at all. The NPM_TOKEN secret then becomes optional.', + '', + 'See: https://docs.npmjs.com/trusted-publishers', + '', + ].join('\n'); +} diff --git a/js/scripts/publish-retry.mjs b/js/scripts/publish-retry.mjs new file mode 100644 index 00000000..544bba78 --- /dev/null +++ b/js/scripts/publish-retry.mjs @@ -0,0 +1,232 @@ +/** + * Publish orchestration helpers that keep the two failure domains separate: + * + * - the publish command itself failing (retryable: run `changeset publish` again) + * - post-publish verification missing because the npm registry has not + * propagated yet (NOT retryable by republishing: the only correct response is + * to look again) + * + * A single verification check a couple of seconds after a successful publish + * samples a race. Issue #199 is exactly that race: on 2026-09-04 the release job + * for command-stream@0.20.1 logged + * + * 🦋 success packages published successfully: + * 🦋 command-stream@0.20.1 + * + * and then, 2 seconds later, `npm view command-stream@0.20.1 version` answered + * `E404 No match found for version 0.20.1` because the registry read replica had + * not caught up. The old loop treated that miss as "publish failed" and + * republished, which npm rejected with + * + * npm error code E409 + * npm error 409 Conflict - PUT https://registry.npmjs.org/command-stream - + * Cannot publish over previously staged version "0.20.1" + * + * The release was reported red even though 0.20.1 was live on npm. + * + * Ported from link-foundation/js-ai-driven-development-pipeline-template + * (scripts/publish-retry.mjs) and extended with the E409 "previously staged + * version" patterns that the template is still missing. + */ + +export const DEFAULT_VERIFY_ATTEMPTS = 7; +export const DEFAULT_VERIFY_INITIAL_DELAY = 2000; +export const DEFAULT_VERIFY_MAX_DELAY = 30000; + +/** + * Default sleep implementation. + * @param {number} ms + * @returns {Promise} + */ +export function sleep(ms) { + return new Promise((resolve) => globalThis.setTimeout(resolve, ms)); +} + +/** + * Patterns that mean "this exact version is already on the registry". + * Such an error is a cue to verify, not to fail. + * + * npm distinguishes *published* from *staged*: a version whose tarball reached + * the registry but whose metadata write is still settling is reported as + * `Cannot publish over previously staged version`. Both wordings prove the + * version exists, so both must be treated the same way. + */ +export const ALREADY_PUBLISHED_PATTERNS = [ + 'epublishconflict', + 'cannot publish over the previously published version', + 'cannot publish over previously published version', + 'you cannot publish over the previously published versions', + 'cannot publish over the previously staged version', + 'cannot publish over previously staged version', + 'previously staged version', + 'already published', + 'npm error code e409', + 'npm error 409', + '409 conflict', +]; + +/** + * Check whether publish output indicates the version is already published. + * @param {string} output + * @returns {boolean} + */ +export function isAlreadyPublishedError(output) { + const lowerOutput = String(output || '').toLowerCase(); + return ALREADY_PUBLISHED_PATTERNS.some((pattern) => + lowerOutput.includes(pattern) + ); +} + +/** + * Poll the registry until the version becomes visible, using exponential + * backoff. Returns true as soon as the version is found. + * @param {object} options + * @param {Function} options.verify - async () => boolean + * @param {number} [options.attempts] + * @param {number} [options.initialDelay] + * @param {number} [options.maxDelay] + * @param {Function} [options.sleepFn] + * @param {Function} [options.log] + * @returns {Promise} + */ +export async function waitForVersionOnRegistry({ + verify, + attempts = DEFAULT_VERIFY_ATTEMPTS, + initialDelay = DEFAULT_VERIFY_INITIAL_DELAY, + maxDelay = DEFAULT_VERIFY_MAX_DELAY, + sleepFn = sleep, + log = () => {}, +}) { + let delay = initialDelay; + for (let attempt = 1; attempt <= attempts; attempt++) { + await sleepFn(delay); + let found = false; + try { + found = await verify(); + } catch (error) { + // A transient registry/network error is indistinguishable from a miss + // here, so polling continues and the release is not failed at this point. + log(`Verification attempt ${attempt} errored: ${error.message}`); + } + if (found) { + log(`Verification succeeded on attempt ${attempt}`); + return true; + } + log( + `Verification attempt ${attempt} of ${attempts}: version not visible yet` + ); + delay = Math.min(delay * 2, maxDelay); + } + return false; +} + +/** + * Decide whether a publish invocation should move on to verification. + * @param {object} outcome + * @param {boolean} outcome.success + * @param {Error} [outcome.error] + * @param {string} [outcome.output] + * @param {Function} outcome.log + * @returns {boolean} + */ +export function shouldVerify({ success, error, output, log = () => {} }) { + if (success) { + return true; + } + if (!isAlreadyPublishedError(output || error?.message || '')) { + return false; + } + log( + 'Publish reported the version is already published or staged, verifying registry.' + ); + return true; +} + +/** + * Build the result of the verification stage. A verification miss is terminal: + * the publish path must not be re-entered, because the package may already be + * live and republishing would fail with a conflict. + * @param {boolean} verified + * @returns {{success: boolean, error: Error|null}} + */ +function verificationOutcome(verified) { + if (verified) { + return { success: true, error: null }; + } + const error = new Error( + 'Package not found on npm after publish; verification polling exhausted' + ); + error.nonRetryable = true; + error.verificationFailed = true; + return { success: false, error }; +} + +/** + * Run the publish command with retries, then verify with bounded polling. + * + * The publish command is retried only when the publish itself failed. Once a + * publish reports success (or reports an "already published"/"already staged" + * conflict), the flow moves to verification and never re-enters the publish + * path. + * + * Verification is still required: a publish that falsely claims success still + * fails the release (issue #166). + * + * @param {object} options + * @param {Function} options.publish - async () => ({ success, error, output }) + * @param {Function} options.verify - async () => boolean + * @param {number} [options.maxRetries] + * @param {number} [options.retryDelay] + * @param {Function} [options.sleepFn] + * @param {Function} [options.log] + * @param {object} [options.verifyOptions] + * @returns {Promise<{success: boolean, error: Error|null, publishAttempts: number}>} + */ +export async function publishWithRetry({ + publish, + verify, + maxRetries = 3, + retryDelay = 10000, + sleepFn = sleep, + log = () => {}, + verifyOptions = {}, +}) { + let publishAttempts = 0; + let lastError = null; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + log(`Publish attempt ${attempt} of ${maxRetries}...`); + publishAttempts++; + const { success, error, output } = await publish(); + + if (shouldVerify({ success, error, output, log })) { + const verified = await waitForVersionOnRegistry({ + verify, + sleepFn, + log, + ...verifyOptions, + }); + return { ...verificationOutcome(verified), publishAttempts }; + } + + lastError = error; + + if (error?.nonRetryable) { + return { success: false, error, publishAttempts }; + } + + if (attempt < maxRetries) { + log( + `Publish failed: ${error?.message}, waiting ${retryDelay / 1000}s before retry...` + ); + await sleepFn(retryDelay); + } + } + + return { + success: false, + error: + lastError || new Error(`Failed to publish after ${maxRetries} attempts`), + publishAttempts, + }; +} diff --git a/js/scripts/publish-to-npm.mjs b/js/scripts/publish-to-npm.mjs index f4052990..5fefff04 100644 --- a/js/scripts/publish-to-npm.mjs +++ b/js/scripts/publish-to-npm.mjs @@ -5,23 +5,34 @@ * Usage: bun scripts/publish-to-npm.mjs [--should-pull] * should_pull: Optional flag to pull latest changes before publishing (for release job) * - * IMPORTANT: Update the PACKAGE_NAME constant below to match your package.json - * * Reliable success detection (prevents false-positive releases): * command-stream's `$` does NOT throw on a non-zero exit code (errexit is * off by default — see issue #156). A bare `await $`cmd`` therefore never - * rejects, so a try/catch around it can never observe a failure. The previous - * version of this script relied on that catch, so a failed `changeset publish` - * (e.g. npm E404) was silently reported as a success — which created a - * GitHub release (`js-v0.10.1`) for a version that never reached npm (#166). + * rejects, so a try/catch around it can never observe a failure. An early + * version of this script relied on that catch, so a failed + * `changeset publish` (e.g. npm E404) was silently reported as a success — + * which created a GitHub release (`js-v0.10.1`) for a version that never + * reached npm (#166). Output scanning, the exit code and a registry check are + * therefore all required. + * + * Reliable failure detection (prevents false-negative releases): + * The registry check must be *bounded polling*, not a single sample. npm + * serves package metadata from read replicas, so a version can be published + * and still answer 404 for several seconds. Issue #199: command-stream@0.20.1 + * published successfully, failed a single verification 2s later, was + * republished, and npm answered + * `E409 ... Cannot publish over previously staged version "0.20.1"` — which + * the old code counted as a hard failure. The release was red while the + * package was live on npm. + * + * Both concerns now live in scripts/publish-retry.mjs: the publish command is + * retried only when the publish itself failed, and an "already published" or + * "already staged" conflict is a cue to verify rather than to fail. * - * This version mirrors the multi-layer detection used by the pipeline - * template (link-foundation/js-ai-driven-development-pipeline-template, - * originally link-assistant/agent PR #116): - * 1. scan the captured output for known failure patterns, - * 2. check the captured exit code, and - * 3. verify the version is actually visible on npm with `npm view`. - * A publish is only reported when all three layers pass. + * Verbose tracing: + * Set CI_SCRIPTS_DEBUG=1 (or re-run the job with GitHub's debug logging, which + * sets RUNNER_DEBUG=1) to emit `::debug::` lines describing every publish and + * verification decision. Off by default. * * Uses link-foundation libraries: * - use-m: Dynamic package loading without package.json dependencies @@ -30,13 +41,26 @@ */ import { readFileSync, appendFileSync } from 'fs'; +import { debug } from './debug-print.mjs'; +import { isPackageVersionPublished } from './npm-registry.mjs'; +import { + buildAuthFailureGuidance, + isNonRetryableFailure, +} from './publish-failure-classifier.mjs'; +import { + DEFAULT_VERIFY_ATTEMPTS, + DEFAULT_VERIFY_INITIAL_DELAY, + DEFAULT_VERIFY_MAX_DELAY, + isAlreadyPublishedError, + publishWithRetry, + sleep, +} from './publish-retry.mjs'; +import { loadUseM } from './use-m-loader.mjs'; -const PACKAGE_NAME = 'command-stream'; +const FALLBACK_PACKAGE_NAME = 'command-stream'; // Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); @@ -55,10 +79,23 @@ const config = makeConfig({ const { shouldPull } = config; const MAX_RETRIES = 3; // Configurable so tests can run the retry loop without waiting (see -// tests/publish-to-npm.test.mjs). Defaults to 10s for real CI runs. +// tests/publish-to-npm.test.mjs). Defaults are tuned for real CI runs. const RETRY_DELAY = Number(process.env.PUBLISH_RETRY_DELAY ?? 10000); // ms -// Wait for the npm registry to propagate before verifying a fresh publish. -const VERIFY_DELAY = Number(process.env.PUBLISH_VERIFY_DELAY ?? 2000); // ms +const VERIFY_DELAY = Number( + process.env.PUBLISH_VERIFY_DELAY ?? DEFAULT_VERIFY_INITIAL_DELAY +); // ms +const VERIFY_ATTEMPTS = Number( + process.env.PUBLISH_VERIFY_ATTEMPTS ?? DEFAULT_VERIFY_ATTEMPTS +); +const VERIFY_MAX_DELAY = Number( + process.env.PUBLISH_VERIFY_MAX_DELAY ?? DEFAULT_VERIFY_MAX_DELAY +); +// Registry used for the publication check only. Unset in production, where +// npm-registry.mjs falls back to NPM_CONFIG_REGISTRY and then to +// https://registry.npmjs.org. Tests point it at a stub registry; overriding +// NPM_CONFIG_REGISTRY instead would also redirect use-m's own module +// installation, which must keep talking to the real registry. +const REGISTRY_URL = process.env.PUBLISH_REGISTRY_URL || undefined; // Patterns that indicate a publish failure in the changeset/npm output. // `changeset publish` can print these and still exit 0 in some npm versions, @@ -76,14 +113,6 @@ const FAILURE_PATTERNS = [ 'exited with code 1', ]; -/** - * Sleep for specified milliseconds - * @param {number} ms - */ -function sleep(ms) { - return new Promise((resolve) => globalThis.setTimeout(resolve, ms)); -} - /** * Append to GitHub Actions output file * @param {string} key @@ -113,62 +142,71 @@ function detectPublishFailure(output) { /** * Verify a package version is actually published on npm. + * + * Reads the registry metadata document directly instead of shelling out to + * `npm view`: `npm view` mixes registry state with local cache/auth + * configuration, and its E404 is indistinguishable from a network hiccup. + * * @param {string} packageName * @param {string} version * @returns {Promise} */ async function verifyPublished(packageName, version) { - const result = await $`npm view "${packageName}@${version}" version`.run({ - capture: true, + const published = await isPackageVersionPublished(packageName, version, { + registryUrl: REGISTRY_URL, }); - return result.code === 0 && result.stdout.trim().includes(version); + debug('registry verification', { packageName, version, published }); + return published; } /** - * Run `changeset:publish` once and decide whether it really succeeded. + * Run `changeset:publish` once and report what happened. * - * command-stream does not throw on non-zero exits, so we capture the output - * and apply three independent checks before trusting the result. + * command-stream does not throw on non-zero exits, so the output and exit code + * are captured and classified here. Registry verification is *not* done here: + * it belongs to publishWithRetry, which must never republish just because a + * verification sample missed. * - * @param {string} packageName - * @param {string} version - * @returns {Promise<{success: boolean, error: Error|null}>} + * @returns {Promise<{success: boolean, error: Error|null, output: string}>} */ -async function attemptPublish(packageName, version) { +async function attemptPublish() { // IMPORTANT: capture:true mirrors output to the console *and* returns it, // so CI logs stay readable while we still get the text and exit code. const result = await $`bun run changeset:publish`.run({ capture: true }); const combinedOutput = `${result.stdout || ''}\n${result.stderr || ''}`; + debug('changeset publish exit code', result.code); - // Layer 1: scan output for known failure signatures. - const failurePattern = detectPublishFailure(combinedOutput); - if (failurePattern) { + // An "already published"/"already staged" conflict proves the version exists. + // Report it verbatim so publishWithRetry moves to verification instead of + // republishing (issue #199). + if (isAlreadyPublishedError(combinedOutput)) { return { success: false, - error: new Error(`detected "${failurePattern}" in publish output`), + error: new Error('npm reports this version is already on the registry'), + output: combinedOutput, }; } - // Layer 2: trust the exit code when it is non-zero. - if (result.code !== 0) { - return { - success: false, - error: new Error(`changeset publish exited with code ${result.code}`), - }; + // Layer 1: scan output for known failure signatures. + const failurePattern = detectPublishFailure(combinedOutput); + if (failurePattern) { + const error = new Error(`detected "${failurePattern}" in publish output`); + // Auth / registry-configuration failures repeat identically on every retry. + error.nonRetryable = isNonRetryableFailure(combinedOutput); + return { success: false, error, output: combinedOutput }; } - // Layer 3: confirm the version is really on npm (the ultimate check). - console.log('Verifying package was published to npm...'); - await sleep(VERIFY_DELAY); - if (await verifyPublished(packageName, version)) { - return { success: true, error: null }; + // Layer 2: trust the exit code when it is non-zero. + if (result.code !== 0) { + const error = new Error( + `changeset publish exited with code ${result.code}` + ); + error.nonRetryable = isNonRetryableFailure(combinedOutput); + return { success: false, error, output: combinedOutput }; } - return { - success: false, - error: new Error('version not found on npm after publish attempt'), - }; + return { success: true, error: null, output: combinedOutput }; } async function main() { @@ -181,66 +219,65 @@ async function main() { // Get current version const packageJson = JSON.parse(readFileSync('./package.json', 'utf8')); const currentVersion = packageJson.version; + const packageName = packageJson.name || FALLBACK_PACKAGE_NAME; console.log(`Current version to publish: ${currentVersion}`); + debug('resolved package', { packageName, currentVersion }); // Check if this version is already published on npm console.log( `Checking if version ${currentVersion} is already published...` ); - const checkResult = - await $`npm view "${PACKAGE_NAME}@${currentVersion}" version`.run({ - capture: true, - }); - - // command-stream returns { code: 0 } on success, { code: 1 } on failure (e.g., E404) - // Exit code 0 means version exists, non-zero means version not found - if (checkResult.code === 0) { + if (await verifyPublished(packageName, currentVersion)) { console.log(`Version ${currentVersion} is already published to npm`); setOutput('published', 'true'); setOutput('published_version', currentVersion); setOutput('already_published', 'true'); return; - } else { - // Version not found on npm (E404), proceed with publish - console.log( - `Version ${currentVersion} not found on npm, proceeding with publish...` - ); } - // Publish to npm using OIDC trusted publishing with retry logic. - // Multi-layer failure detection prevents false-positive releases (#166). - for (let i = 1; i <= MAX_RETRIES; i++) { - console.log(`Publish attempt ${i} of ${MAX_RETRIES}...`); - const { success, error } = await attemptPublish( - PACKAGE_NAME, - currentVersion - ); + console.log( + `Version ${currentVersion} not found on npm, proceeding with publish...` + ); + + // Publish to npm using OIDC trusted publishing. + // - the publish command is retried only when the publish itself failed + // (#166: a silent failure must never be reported as a release), and + // - verification polls the registry with exponential backoff instead of + // sampling it once (#199: a propagation lag must never be reported as a + // failure). + const { success, error } = await publishWithRetry({ + publish: attemptPublish, + verify: () => verifyPublished(packageName, currentVersion), + maxRetries: MAX_RETRIES, + retryDelay: RETRY_DELAY, + sleepFn: sleep, + log: console.log, + verifyOptions: { + attempts: VERIFY_ATTEMPTS, + initialDelay: VERIFY_DELAY, + maxDelay: VERIFY_MAX_DELAY, + }, + }); - if (success) { - setOutput('published', 'true'); - setOutput('published_version', currentVersion); - console.log(`✅ Published ${PACKAGE_NAME}@${currentVersion} to npm`); - return; - } - - if (i < MAX_RETRIES) { - console.log( - `Publish failed: ${error.message}, waiting ${RETRY_DELAY / 1000}s before retry...` - ); - await sleep(RETRY_DELAY); - } else { - console.error(`Publish attempt ${i} failed: ${error.message}`); - } + if (success) { + setOutput('published', 'true'); + setOutput('published_version', currentVersion); + console.log(`✅ Published ${packageName}@${currentVersion} to npm`); + return; } - console.error(`❌ Failed to publish after ${MAX_RETRIES} attempts`); - console.error( - 'Hint: an npm E404 on PUT usually means OIDC trusted publishing is not ' + - 'configured for this workflow file. npm allows only one workflow file ' + - 'as a trusted publisher; if the release workflow was renamed (e.g. ' + - 'release.yml -> js.yml), update the trusted publisher on npmjs.com. ' + - 'See docs/case-studies/issue-166/README.md.' - ); + console.error(`❌ Failed to publish ${packageName}@${currentVersion}`); + console.error(`Reason: ${error?.message}`); + if (error?.nonRetryable && !error?.verificationFailed) { + console.error(buildAuthFailureGuidance(packageName)); + console.error( + 'Hint: an npm E404 on PUT usually means OIDC trusted publishing is not ' + + 'configured for this workflow file. npm allows only one workflow file ' + + 'as a trusted publisher; if the release workflow was renamed (e.g. ' + + 'release.yml -> js.yml), update the trusted publisher on npmjs.com. ' + + 'See docs/case-studies/issue-166/README.md.' + ); + } // Ensure no false-positive output leaks to the release job. setOutput('published', 'false'); process.exit(1); diff --git a/js/scripts/setup-npm.mjs b/js/scripts/setup-npm.mjs index 3ef1307d..4e29a0b7 100644 --- a/js/scripts/setup-npm.mjs +++ b/js/scripts/setup-npm.mjs @@ -26,6 +26,7 @@ import { resolve } from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; +import { loadUseM } from './use-m-loader.mjs'; export const NPM_MIN_VERSION = '11.5.1'; export const NODE_MIN_VERSION = '22.14.0'; @@ -300,9 +301,7 @@ if (isMainModule()) { // Load use-m dynamically only for CLI execution, so tests can import the // pure version helpers without fetching dependencies or mutating npm. - const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() - ); + const use = await loadUseM(); const { $ } = await use('command-stream'); await setupNpm($); diff --git a/js/scripts/use-m-loader.mjs b/js/scripts/use-m-loader.mjs new file mode 100644 index 00000000..8e3aa3e2 --- /dev/null +++ b/js/scripts/use-m-loader.mjs @@ -0,0 +1,202 @@ +#!/usr/bin/env node + +/** + * Load `use-m` from its CDN, with a timeout, bounded retries and an error that + * names what failed. + * + * Every release script in this folder starts by fetching + * https://unpkg.com/use-m/use.js and eval-ing it. Eleven scripts did that + * inline, at module scope, with no timeout and no retry: + * + * const { use } = eval(await (await fetch(USE_M_URL)).text()); + * + * Three failure modes follow from that shape, and all three were observed + * while investigating issue #199: + * + * - A network-level failure (DNS, connect, reset) rejects with a bare + * `TypeError: fetch failed`, thrown during module initialisation. The + * script dies before its first `console.log` and before it writes anything + * to GITHUB_OUTPUT, so the job's log names neither the CDN nor the URL. In + * the publish tests this surfaced as `Expected to contain: "published=true" + * / Received: ""` -- a red release caused by a third-party outage, reported + * as a publish defect. + * - A CDN error page is HTML, and eval-ing HTML raises `SyntaxError: + * Unexpected token '<'`, which points at this repository's code for a + * response it never inspected. The status is checked before the eval here. + * - A stalled connection has no deadline of its own, so the job burned its + * whole `timeout-minutes` on a socket that was never going to answer. + * + * A CDN blip is transient by nature, so the fetch is retried with exponential + * backoff before it is allowed to fail the run at all. + * + * Verbose tracing: set CI_SCRIPTS_DEBUG=1 (or re-run the job with GitHub's + * debug logging) to emit one `::debug::` line per attempt. Off by default. + * + * Usage: + * import { loadUseM } from './use-m-loader.mjs'; + * const use = await loadUseM(); + * const { $ } = await use('command-stream'); + */ + +import { debug } from './debug-print.mjs'; + +/** CDN entry point for use-m, kept in one place. */ +export const USE_M_URL = 'https://unpkg.com/use-m/use.js'; + +/** Total attempts, including the first one. */ +export const DEFAULT_ATTEMPTS = 3; + +/** Per-attempt deadline: a stalled connect must not consume the job's budget. */ +export const DEFAULT_TIMEOUT_MS = 15000; + +/** Delay before the second attempt; doubled for each attempt after it. */ +export const DEFAULT_RETRY_DELAY_MS = 2000; + +/** Cached `use`, so a process fetches use.js at most once. */ +let cachedUse = null; + +/** + * @param {number} ms + * @returns {Promise} + */ +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** + * Fetch use.js once and evaluate it. + * + * @param {{fetchImpl: typeof fetch, url: string, timeoutMs: number}} options + * @returns {Promise<(name: string) => Promise>} use-m's `use` + */ +async function fetchUse({ fetchImpl, url, timeoutMs }) { + // AbortSignal.timeout is the deadline the bare fetch never had. Node 18+ and + // Bun both ship it; the scripts here run on nothing older. + const response = await fetchImpl(url, { + signal: AbortSignal.timeout(timeoutMs), + }); + if (!response.ok) { + throw new Error( + `HTTP ${response.status} ${response.statusText || ''}`.trim() + ); + } + const source = await response.text(); + // use-m ships as an eval-able bundle; this is its documented entry point. + const evaluated = await eval(source); + const use = evaluated?.use ?? evaluated?.default?.use; + if (typeof use !== 'function') { + const keys = + evaluated && typeof evaluated === 'object' + ? `[${Object.keys(evaluated).join(', ')}]` + : String(evaluated); + throw new Error(`the bundle did not export a callable "use"; got ${keys}`); + } + return use; +} + +/** + * Apply the defaults. Kept apart from `loadUseM` so the retry loop stays under + * the complexity limit eslint enforces for this repository. + * + * @param {Record} options + * @returns {{ + * fetchImpl: typeof fetch, + * url: string, + * attempts: number, + * timeoutMs: number, + * retryDelayMs: number, + * sleep: (ms: number) => Promise, + * cacheable: boolean, + * }} + */ +function resolveOptions(options) { + const url = options.url ?? USE_M_URL; + return { + fetchImpl: options.fetchImpl ?? fetch, + url, + attempts: options.attempts ?? DEFAULT_ATTEMPTS, + timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS, + retryDelayMs: options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS, + sleep: options.sleep ?? wait, + // Tests inject their own fetch, so a cached `use` from a previous call must + // not answer for them -- and must not be overwritten by their stub either. + cacheable: !options.fetchImpl && url === USE_M_URL, + }; +} + +/** + * Readable text for a thrown value, for logs and for the final message. + * @param {unknown} error + * @returns {string} + */ +const describeError = (error) => error?.message ?? String(error); + +/** + * Load use-m, retrying a transient CDN failure before failing the run. + * + * @param {{ + * fetchImpl?: typeof fetch, + * url?: string, + * attempts?: number, + * timeoutMs?: number, + * retryDelayMs?: number, + * sleep?: (ms: number) => Promise, + * }} [options] injection seams for tests; production passes nothing + * @returns {Promise<(name: string) => Promise>} use-m's `use` + * @throws {Error} naming the URL, the attempt count and the last cause + */ +export async function loadUseM(options = {}) { + const { + fetchImpl, + url, + attempts, + timeoutMs, + retryDelayMs, + sleep, + cacheable, + } = resolveOptions(options); + + if (cacheable && cachedUse) { + return cachedUse; + } + + let lastError; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + const use = await fetchUse({ fetchImpl, url, timeoutMs }); + debug('loaded use-m', { url, attempt }); + if (cacheable) { + cachedUse = use; + } + return use; + } catch (error) { + lastError = error; + debug('use-m load attempt failed', { + url, + attempt, + attempts, + error: describeError(error), + }); + if (attempt < attempts) { + await sleep(retryDelayMs * 2 ** (attempt - 1)); + } + } + } + + // The message is the whole point of this module: it has to be readable from + // the CI log alone, and it has to say that the failure is the CDN's rather + // than this repository's. + throw new Error( + `Failed to load use-m from ${url} after ${attempts} attempt(s): ` + + `${describeError(lastError)}. ` + + 'This is a network dependency of the release scripts, not a defect in ' + + 'the published package; re-run the job when the CDN answers again.', + { cause: lastError } + ); +} + +/** + * Drop the cached `use`. Only tests need this. + * @returns {void} + */ +export function resetUseMCache() { + cachedUse = null; +} diff --git a/js/scripts/version-and-commit.mjs b/js/scripts/version-and-commit.mjs index a474b7a3..47e92e11 100644 --- a/js/scripts/version-and-commit.mjs +++ b/js/scripts/version-and-commit.mjs @@ -13,11 +13,10 @@ */ import { readFileSync, appendFileSync, readdirSync } from 'fs'; +import { loadUseM } from './use-m-loader.mjs'; // Load use-m dynamically -const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() -); +const use = await loadUseM(); // Import link-foundation libraries const { $ } = await use('command-stream'); diff --git a/js/scripts/wait-for-npm.mjs b/js/scripts/wait-for-npm.mjs index ca6033f3..4f5a5c23 100644 --- a/js/scripts/wait-for-npm.mjs +++ b/js/scripts/wait-for-npm.mjs @@ -28,6 +28,7 @@ import { appendFileSync, readFileSync } from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; +import { loadUseM } from './use-m-loader.mjs'; export const DEFAULT_MAX_ATTEMPTS = 30; export const DEFAULT_SLEEP_SECONDS = 10; @@ -98,9 +99,7 @@ function isCliEntryPoint() { async function runCli() { // Load use-m dynamically (matches the other release scripts in this folder). - const { use } = eval( - await (await fetch('https://unpkg.com/use-m/use.js')).text() - ); + const use = await loadUseM(); const { $ } = await use('command-stream'); const { makeConfig } = await use('lino-arguments'); diff --git a/js/src/terminal-artifacts.mjs b/js/src/terminal-artifacts.mjs index e29d7d88..a0558b7a 100644 --- a/js/src/terminal-artifacts.mjs +++ b/js/src/terminal-artifacts.mjs @@ -157,7 +157,7 @@ const isBlankCell = (cell) => (cell.chars || ' ').trim() === ''; const coalesceRow = (cells, options) => { const runs = []; - for (let column = 0; column < cells.length; ) { + for (let column = 0; column < cells.length;) { const cell = cells[column]; if (cell.width === 0) { column += 1; @@ -257,7 +257,7 @@ const renderFrame = (frame, options) => { const backgrounds = []; const foregrounds = []; rows.forEach((cells, row) => { - for (let column = 0; column < cells.length; ) { + for (let column = 0; column < cells.length;) { const first = cells[column]; const background = cellStyle(first, options).bg; let width = Math.max(first.width, 1); diff --git a/js/tests/debug-print.test.mjs b/js/tests/debug-print.test.mjs new file mode 100644 index 00000000..ebcf0259 --- /dev/null +++ b/js/tests/debug-print.test.mjs @@ -0,0 +1,70 @@ +// Unit tests for js/scripts/debug-print.mjs +// +// The release scripts keep verbose tracing in the code with the default state +// switched off, so a failing run can be re-run with debug logging and produce +// the evidence needed to find a root cause without a code change. + +import { test, expect } from 'bun:test'; +import { + debugWith, + formatDebugLines, + isDebugEnabled, + readEnvVar, +} from '../scripts/debug-print.mjs'; + +test('debug output is off by default', () => { + expect(isDebugEnabled({})).toBe(false); +}); + +test('every documented switch enables debug output', () => { + expect(isDebugEnabled({ CI_SCRIPTS_DEBUG: '1' })).toBe(true); + expect(isDebugEnabled({ CI_SCRIPTS_DEBUG: 'true' })).toBe(true); + expect(isDebugEnabled({ RUNNER_DEBUG: '1' })).toBe(true); + expect(isDebugEnabled({ ACTIONS_STEP_DEBUG: 'true' })).toBe(true); +}); + +test('an unreadable environment counts as unset', () => { + const hostile = new Proxy( + {}, + { + get() { + throw new Error('NotCapable'); + }, + } + ); + expect(readEnvVar('CI_SCRIPTS_DEBUG', hostile)).toBeUndefined(); + expect(isDebugEnabled(hostile)).toBe(false); +}); + +test('lines are prefixed for the GitHub Actions debug stream', () => { + expect(formatDebugLines(['first\nsecond'])).toEqual([ + '::debug::first', + '::debug::second', + ]); +}); + +test('objects are serialized', () => { + expect(formatDebugLines([{ published: true }])).toEqual([ + '::debug::{', + '::debug:: "published": true', + '::debug::}', + ]); +}); + +test('nothing is printed while debug output is off', () => { + const lines = []; + expect(debugWith({ env: {}, log: (l) => lines.push(l) }, 'hidden')).toEqual( + [] + ); + expect(lines).toEqual([]); +}); + +test('lines are printed once debug output is on', () => { + const lines = []; + debugWith( + { env: { CI_SCRIPTS_DEBUG: '1' }, log: (l) => lines.push(l) }, + 'visible', + { code: 0 } + ); + expect(lines[0]).toBe('::debug::visible {'); +}); diff --git a/js/tests/docs-validation.test.mjs b/js/tests/docs-validation.test.mjs new file mode 100644 index 00000000..e67a0d19 --- /dev/null +++ b/js/tests/docs-validation.test.mjs @@ -0,0 +1,134 @@ +// Documentation is validated in CI like code: principle #12 of the hive-mind +// CI/CD best practices, which nothing in this repository implemented before +// issue #199. Three failure modes are covered here, all of them found in the +// tree when this file was written or fixed in the same commit: +// +// - a relative link that points at a file which does not exist (a case study +// linked to two release markers that the release process had consumed, and +// another one was one directory level off), +// - a document that has outgrown any reasonable review size, +// - a key document losing the section a reader is sent to it for. +// +// External links are deliberately out of scope here: a link checker that +// reaches the network turns unrelated pull requests red when a third-party site +// rots, which is the class of false positive issue #199 is about. They are +// fetched weekly instead, by .github/workflows/links.yml, which blocks nothing. +import { describe, test, expect } from 'bun:test'; +import { readFileSync, existsSync } from 'fs'; +import { join, dirname, resolve, relative, sep } from 'path'; +import { execFileSync } from 'child_process'; + +const repoRoot = join(dirname(Bun.fileURLToPath(import.meta.url)), '..', '..'); + +// execFileSync, not execSync: `git ls-files '*.md'` goes through cmd.exe on +// Windows, which does not strip single quotes, so git looked for a file named +// `'*.md'`, matched nothing, and this file validated an empty list -- green on +// Windows because it checked nothing at all (caught by the Windows leg of the +// test matrix). Without a shell, git expands the pattern itself everywhere. +const markdownFiles = execFileSync('git', ['ls-files', '*.md'], { + cwd: repoRoot, + encoding: 'utf8', +}) + .trim() + .split('\n') + .filter(Boolean); + +/** + * Verbatim copies of other repositories' files, kept as evidence and never + * edited: their links point into the tree they came from, and their size is + * not this repository's to control. + */ +const isArchived = (file) => + file.startsWith('dev/log/') || + /docs\/case-studies\/[^/]+\/(templates|data|template-data)\//.test(file); + +const authored = markdownFiles.filter((file) => !isArchived(file)); + +// Best practice #12 suggests 2500 lines for documentation, above the 1500-line +// limit eslint enforces for source. The longest file in the tree is js/README.md. +const MAX_DOC_LINES = 2500; + +describe('documentation validation', () => { + test('the file list is not empty and skips archived copies', () => { + expect(authored.length).toBeGreaterThan(20); + expect(authored.some(isArchived)).toBe(false); + }); + + test.each(authored.map((file) => [file]))( + '%s stays within the documentation line limit', + (file) => { + const lines = readFileSync(join(repoRoot, file), 'utf8').split( + '\n' + ).length; + expect(`${file}: ${lines <= MAX_DOC_LINES}`).toBe(`${file}: true`); + } + ); + + test.each(authored.map((file) => [file]))( + '%s has no broken relative links', + (file) => { + const text = readFileSync(join(repoRoot, file), 'utf8'); + const targets = [ + ...text.matchAll(/\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g), + ].map((match) => match[1]); + + const broken = []; + for (const raw of targets) { + // Anchors, external schemes and inline data are out of scope. + if (/^(https?:|mailto:|tel:|data:|#)/.test(raw)) { + continue; + } + const target = decodeURI(raw.split('#')[0]); + if (!target) { + continue; + } + const absolute = target.startsWith('/') + ? join(repoRoot, target) + : resolve(join(repoRoot, dirname(file)), target); + if (!existsSync(absolute)) { + broken.push(`${raw} (-> ${relative(repoRoot, absolute) || sep})`); + } + } + expect(`${file}: ${broken.join(', ')}`).toBe(`${file}: `); + } + ); + + // The evidence root holds copies that are read, never run. A tracked + // executable bit on an inert copy is state the checkout environment can flip + // on its own -- it did, twice, and each time it surfaced as a working tree + // that was dirty without a single byte of content having changed. + test('evidence files under dev/log are stored non-executable', () => { + const executable = execFileSync('git', ['ls-files', '-s', 'dev/log'], { + cwd: repoRoot, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean) + .filter((line) => line.startsWith('100755')) + .map((line) => line.split('\t')[1]); + + expect(executable).toEqual([]); + }); + + // A reader following a cross-reference lands on a heading. These are the + // headings other documents and the workflows point at. + test.each([ + ['README.md', ['## Repository Layout', '## Releases', '## Development']], + [ + 'docs/CI-CD.md', + ['## Workflows', '## Invariants', '## Required repository settings'], + ], + ['js/README.md', ['## Installation', '## API Reference']], + ['rust/README.md', ['## Installation', '## Library Usage']], + ['rust/changelog.d/README.md', ['bump:']], + ['js/.changeset/README.md', ['changeset']], + ])('%s keeps its required sections', (file, sections) => { + const text = readFileSync(join(repoRoot, file), 'utf8'); + for (const section of sections) { + expect(`${file}: ${section}: ${text.includes(section)}`).toBe( + `${file}: ${section}: true` + ); + } + }); +}); diff --git a/js/tests/duplication-check.test.mjs b/js/tests/duplication-check.test.mjs new file mode 100644 index 00000000..125fd581 --- /dev/null +++ b/js/tests/duplication-check.test.mjs @@ -0,0 +1,139 @@ +// Regression tests for the jscpd duplication check (issue #199). +// +// `bun run check:duplication` was a no-op that always passed. jscpd's `format` +// option is the list of *languages* to analyse, but .jscpd.json set it to the +// string "console" — a reporter name. The finder filters files with +// +// options.format.includes(format) // @jscpd/finder +// +// and `"console".includes("javascript")` is false, so every file was skipped: +// zero sources, zero clones, exit 0, in under a millisecond. The check reported +// success without ever looking at the code (a false negative). +// +// These tests run the real binary against a fixture that contains one obvious +// clone, so they fail if the configuration ever stops analysing JavaScript. + +import { test, expect, beforeAll, afterAll } from 'bun:test'; +import { spawnSync } from 'child_process'; +import { + mkdtempSync, + mkdirSync, + writeFileSync, + rmSync, + readFileSync, + existsSync, +} from 'fs'; +import { tmpdir } from 'os'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const jsDir = join(dirname(fileURLToPath(import.meta.url)), '..'); +const jscpdBin = join(jsDir, 'node_modules', '.bin', 'jscpd'); +const repoConfig = JSON.parse(readFileSync(join(jsDir, '.jscpd.json'), 'utf8')); + +// Two files sharing an identical block, comfortably above minTokens/minLines. +const DUPLICATED_BLOCK = [ + 'export function normalize(input) {', + " const trimmed = String(input ?? '').trim();", + ' if (trimmed.length === 0) {', + " return { ok: false, reason: 'empty' };", + ' }', + " const parts = trimmed.split(',').map((part) => part.trim());", + ' const unique = Array.from(new Set(parts));', + " return { ok: true, value: unique.join('|') };", + '}', +].join('\n'); + +let workDir; + +beforeAll(() => { + workDir = mkdtempSync(join(tmpdir(), 'jscpd-check-')); + mkdirSync(join(workDir, 'src')); + writeFileSync(join(workDir, 'src', 'first.mjs'), `${DUPLICATED_BLOCK}\n`); + writeFileSync(join(workDir, 'src', 'second.mjs'), `${DUPLICATED_BLOCK}\n`); +}); + +afterAll(() => { + if (workDir) { + rmSync(workDir, { recursive: true, force: true }); + } +}); + +/** + * Run jscpd over the fixture with the given `format` value and return the + * statistics it produced. `threshold` is high so the run always exits 0 and the + * assertions are about what jscpd *saw*, not about its verdict. + */ +function runJscpd(format, outputName) { + const output = join(workDir, outputName); + const configPath = join(workDir, `${outputName}.json`); + writeFileSync( + configPath, + JSON.stringify({ + minTokens: repoConfig.minTokens, + minLines: repoConfig.minLines, + threshold: 100, + format, + reporters: ['json'], + output, + }) + ); + + const result = spawnSync(jscpdBin, ['-c', configPath, 'src'], { + cwd: workDir, + encoding: 'utf8', + }); + // jscpd writes no report at all when it analysed nothing, which is itself + // the symptom of the bug; report that as zero sources rather than crashing. + const reportPath = join(output, 'jscpd-report.json'); + const total = existsSync(reportPath) + ? JSON.parse(readFileSync(reportPath, 'utf8')).statistics.total + : { sources: 0, clones: 0 }; + return { exitCode: result.status, total }; +} + +test('the repository config analyses JavaScript, not a reporter name', () => { + // The exact shape of the bug: a bare string here silently disables the check. + expect(Array.isArray(repoConfig.format)).toBe(true); + expect(repoConfig.format).toContain('javascript'); +}); + +test('the repository config detects a real clone', () => { + const { exitCode, total } = runJscpd(repoConfig.format, 'out-repo-config'); + expect(exitCode).toBe(0); + expect(total.sources).toBe(2); + expect(total.clones).toBeGreaterThan(0); +}); + +test('the old "console" format skipped every file', () => { + // Documents the false negative so nobody reintroduces it as a "fix". + const { total } = runJscpd('console', 'out-old-config'); + expect(total.sources).toBe(0); + expect(total.clones).toBe(0); +}); + +test('the duplication script points at directories that exist', () => { + const pkg = JSON.parse(readFileSync(join(jsDir, 'package.json'), 'utf8')); + const script = pkg.scripts['check:duplication']; + expect(script.startsWith('jscpd ')).toBe(true); + + // A path typo would make jscpd scan nothing and pass, exactly like the + // format bug did, so the targets are checked against the working tree. + const targets = script.split(/\s+/).slice(1); + expect(targets).toContain('src'); + expect(targets).toContain('scripts'); + for (const target of targets) { + expect(existsSync(join(jsDir, target))).toBe(true); + } +}); + +test('the threshold is a real gate, not a way to switch the check off', () => { + // The check had never run, so the tree already contained 5.55% duplicated + // tokens when it was switched on: a threshold of 0 would have failed on + // existing code instead of on a regression. 6 sits just above today's + // measurement. Anything much higher passes whatever is added, which is the + // same false negative in a different disguise. + expect(typeof repoConfig.threshold).toBe('number'); + expect(repoConfig.threshold).toBeGreaterThan(0); + expect(repoConfig.threshold).toBeLessThanOrEqual(10); +}); diff --git a/js/tests/npm-registry.test.mjs b/js/tests/npm-registry.test.mjs new file mode 100644 index 00000000..93fa664a --- /dev/null +++ b/js/tests/npm-registry.test.mjs @@ -0,0 +1,105 @@ +// Unit tests for js/scripts/npm-registry.mjs +// +// The publish verification in scripts/publish-to-npm.mjs reads registry +// metadata directly rather than shelling out to `npm view`, because `npm view` +// mixes registry state with local cache/auth configuration and its E404 is +// indistinguishable from a network hiccup (issue #199). + +import { test, expect } from 'bun:test'; +import { + buildPackageMetadataUrl, + encodePackageName, + isPackageVersionPublished, + normalizeRegistryUrl, +} from '../scripts/npm-registry.mjs'; + +function jsonResponse(status, body, statusText = 'OK') { + return { + ok: status >= 200 && status < 300, + status, + statusText, + async json() { + return body; + }, + }; +} + +test('normalizes registry URLs by stripping trailing slashes', () => { + expect(normalizeRegistryUrl('https://registry.npmjs.org///')).toBe( + 'https://registry.npmjs.org' + ); + expect(normalizeRegistryUrl('')).toBe('https://registry.npmjs.org'); +}); + +test('encodes unscoped and scoped package names', () => { + expect(encodePackageName('command-stream')).toBe('command-stream'); + expect(encodePackageName('@scope/pkg')).toBe('@scope%2Fpkg'); +}); + +test('rejects an empty package name', () => { + expect(() => encodePackageName('')).toThrow('Package name is required'); + expect(() => encodePackageName(' ')).toThrow('Package name is required'); +}); + +test('builds metadata URLs', () => { + expect(buildPackageMetadataUrl('command-stream')).toBe( + 'https://registry.npmjs.org/command-stream' + ); + expect(buildPackageMetadataUrl('@scope/pkg')).toBe( + 'https://registry.npmjs.org/@scope%2Fpkg' + ); +}); + +test('reports a published version as published', async () => { + const published = await isPackageVersionPublished( + 'command-stream', + '0.20.1', + { + fetchFn: async () => + jsonResponse(200, { versions: { '0.20.0': {}, '0.20.1': {} } }), + } + ); + expect(published).toBe(true); +}); + +test('reports a missing version as not published', async () => { + const published = await isPackageVersionPublished( + 'command-stream', + '99.99.99', + { fetchFn: async () => jsonResponse(200, { versions: { '0.20.1': {} } }) } + ); + expect(published).toBe(false); +}); + +test('treats a 404 as "not published", not an error', async () => { + const published = await isPackageVersionPublished('brand-new-pkg', '1.0.0', { + fetchFn: async () => jsonResponse(404, {}, 'Not Found'), + }); + expect(published).toBe(false); +}); + +test('surfaces other HTTP failures so polling can retry them', async () => { + await expect( + isPackageVersionPublished('command-stream', '0.20.1', { + fetchFn: async () => jsonResponse(503, {}, 'Service Unavailable'), + }) + ).rejects.toThrow('503 Service Unavailable'); +}); + +test('requires a version', async () => { + await expect( + isPackageVersionPublished('command-stream', '', { fetchFn: async () => {} }) + ).rejects.toThrow('Package version is required'); +}); + +test('honours a custom registry URL', async () => { + let requestedUrl = ''; + await isPackageVersionPublished('command-stream', '1.0.0', { + registryUrl: 'https://registry.example.com/', + fetchFn: async (url) => { + requestedUrl = url; + return jsonResponse(200, { versions: { '1.0.0': {} } }); + }, + }); + expect(requestedUrl).toBe('https://registry.example.com/command-stream'); +}); diff --git a/js/tests/publish-failure-classifier.test.mjs b/js/tests/publish-failure-classifier.test.mjs new file mode 100644 index 00000000..569d943f --- /dev/null +++ b/js/tests/publish-failure-classifier.test.mjs @@ -0,0 +1,42 @@ +// Unit tests for js/scripts/publish-failure-classifier.mjs +// +// Auth / registry-configuration failures repeat identically on every retry. +// Retrying them only hides the real cause behind a generic +// "Failed to publish after N attempts" message. + +import { test, expect } from 'bun:test'; +import { + NON_RETRYABLE_PATTERNS, + buildAuthFailureGuidance, + isNonRetryableFailure, +} from '../scripts/publish-failure-classifier.mjs'; + +test('classifies auth and registry-configuration failures as non-retryable', () => { + for (const pattern of NON_RETRYABLE_PATTERNS) { + expect( + isNonRetryableFailure(`prefix ${pattern.toUpperCase()} suffix`) + ).toBe(true); + } +}); + +test('classifies the OIDC bootstrap E404 as non-retryable', () => { + expect( + isNonRetryableFailure( + 'npm error code E404\nnpm error 404 Not Found - PUT https://registry.npmjs.org/command-stream' + ) + ).toBe(true); +}); + +test('does not classify a transient publish failure as non-retryable', () => { + expect(isNonRetryableFailure('npm error code E500')).toBe(false); + expect(isNonRetryableFailure('packages failed to publish')).toBe(false); + expect(isNonRetryableFailure('')).toBe(false); + expect(isNonRetryableFailure(undefined)).toBe(false); +}); + +test('guidance names the package and points at trusted publishing', () => { + const guidance = buildAuthFailureGuidance('command-stream'); + expect(guidance).toContain('command-stream'); + expect(guidance).toContain('https://docs.npmjs.com/trusted-publishers'); + expect(guidance).toContain('NPM_TOKEN'); +}); diff --git a/js/tests/publish-retry.test.mjs b/js/tests/publish-retry.test.mjs new file mode 100644 index 00000000..eac6d904 --- /dev/null +++ b/js/tests/publish-retry.test.mjs @@ -0,0 +1,254 @@ +// Regression tests for js/scripts/publish-retry.mjs +// +// Issue #199: the release job for command-stream@0.20.1 published successfully +// (`🦋 success packages published successfully: command-stream@0.20.1`), then +// verified once 2 seconds later, got a registry-replica E404, republished, and +// npm answered: +// +// npm error code E409 +// npm error 409 Conflict - PUT https://registry.npmjs.org/command-stream - +// Cannot publish over previously staged version "0.20.1" +// +// The old loop counted that as a hard failure and turned the release red even +// though 0.20.1 was live on npm. These tests pin both halves of the fix: +// +// 1. an "already published"/"already staged" conflict is a cue to VERIFY, +// never to fail, and +// 2. verification polls with backoff, so a slow registry is not a failure. +// +// The complementary #166 guarantee (a publish that never reached npm must never +// be reported as a release) is asserted too, so the fix cannot regress into a +// false positive. + +import { test, expect } from 'bun:test'; +import { + ALREADY_PUBLISHED_PATTERNS, + isAlreadyPublishedError, + publishWithRetry, + shouldVerify, + waitForVersionOnRegistry, +} from '../scripts/publish-retry.mjs'; + +const noSleep = async () => {}; + +// The verbatim npm output from the failed run, trimmed to the relevant lines. +// Source: dev/log/issues/199/pulls/200/ci-logs/run-33914574283.log +const E409_STAGED_OUTPUT = [ + 'npm error code E409', + 'npm error 409 Conflict - PUT https://registry.npmjs.org/command-stream - Cannot publish over previously staged version "0.20.1"', + '🦋 error an error occurred while publishing command-stream: E409 Conflict', +].join('\n'); + +test('recognises npm E409 "previously staged version" as already published', () => { + expect(isAlreadyPublishedError(E409_STAGED_OUTPUT)).toBe(true); +}); + +test('recognises the classic "previously published version" conflict too', () => { + expect( + isAlreadyPublishedError( + 'npm error You cannot publish over the previously published versions: 0.20.1.' + ) + ).toBe(true); +}); + +test('does not treat an ordinary publish error as already published', () => { + expect( + isAlreadyPublishedError( + 'npm error code E404\nnpm error 404 Not Found - PUT https://registry.npmjs.org/command-stream' + ) + ).toBe(false); + expect(isAlreadyPublishedError('')).toBe(false); + expect(isAlreadyPublishedError(undefined)).toBe(false); +}); + +test('every already-published pattern is matched case-insensitively', () => { + for (const pattern of ALREADY_PUBLISHED_PATTERNS) { + expect(isAlreadyPublishedError(pattern.toUpperCase())).toBe(true); + } +}); + +test('shouldVerify routes an E409 conflict to verification', () => { + expect( + shouldVerify({ + success: false, + error: new Error('changeset publish exited with code 1'), + output: E409_STAGED_OUTPUT, + }) + ).toBe(true); +}); + +test('shouldVerify does not route an unrelated failure to verification', () => { + expect( + shouldVerify({ + success: false, + error: new Error('changeset publish exited with code 1'), + output: 'npm error code E404', + }) + ).toBe(false); +}); + +test('waitForVersionOnRegistry returns true as soon as the version appears', async () => { + let calls = 0; + const found = await waitForVersionOnRegistry({ + verify: async () => ++calls >= 3, + attempts: 7, + initialDelay: 0, + maxDelay: 0, + sleepFn: noSleep, + }); + + expect(found).toBe(true); + expect(calls).toBe(3); +}); + +test('waitForVersionOnRegistry keeps polling through transient errors', async () => { + let calls = 0; + const found = await waitForVersionOnRegistry({ + verify: async () => { + calls++; + if (calls < 3) { + throw new Error('ECONNRESET'); + } + return true; + }, + attempts: 5, + initialDelay: 0, + maxDelay: 0, + sleepFn: noSleep, + }); + + expect(found).toBe(true); + expect(calls).toBe(3); +}); + +test('waitForVersionOnRegistry gives up after the configured attempts', async () => { + let calls = 0; + const found = await waitForVersionOnRegistry({ + verify: async () => { + calls++; + return false; + }, + attempts: 4, + initialDelay: 0, + maxDelay: 0, + sleepFn: noSleep, + }); + + expect(found).toBe(false); + expect(calls).toBe(4); +}); + +test('issue #199: a slow registry after a successful publish is not a failure', async () => { + // The exact shape of the failed run: publish succeeds, the first verification + // sample misses, the next one finds the version. + let publishCalls = 0; + let verifyCalls = 0; + + const result = await publishWithRetry({ + publish: async () => { + publishCalls++; + return { + success: true, + error: null, + output: '🦋 success packages published successfully', + }; + }, + verify: async () => ++verifyCalls >= 2, + maxRetries: 3, + retryDelay: 0, + sleepFn: noSleep, + verifyOptions: { attempts: 7, initialDelay: 0, maxDelay: 0 }, + }); + + expect(result.success).toBe(true); + // The publish command must run exactly once: republishing is what produced + // the E409 that turned the 0.20.1 release red. + expect(publishCalls).toBe(1); + expect(verifyCalls).toBe(2); +}); + +test('issue #199: an E409 staged conflict resolves to success once verified', async () => { + let publishCalls = 0; + + const result = await publishWithRetry({ + publish: async () => { + publishCalls++; + return { + success: false, + error: new Error('detected "npm error code e" in publish output'), + output: E409_STAGED_OUTPUT, + }; + }, + verify: async () => true, + maxRetries: 3, + retryDelay: 0, + sleepFn: noSleep, + verifyOptions: { attempts: 7, initialDelay: 0, maxDelay: 0 }, + }); + + expect(result.success).toBe(true); + expect(publishCalls).toBe(1); +}); + +test('issue #166: a publish that never reaches npm is still a failure', async () => { + const result = await publishWithRetry({ + publish: async () => ({ + success: true, + error: null, + output: 'no projects to publish', + }), + verify: async () => false, + maxRetries: 3, + retryDelay: 0, + sleepFn: noSleep, + verifyOptions: { attempts: 3, initialDelay: 0, maxDelay: 0 }, + }); + + expect(result.success).toBe(false); + expect(result.error.verificationFailed).toBe(true); + expect(result.error.nonRetryable).toBe(true); +}); + +test('a genuinely failing publish is retried up to maxRetries', async () => { + let publishCalls = 0; + + const result = await publishWithRetry({ + publish: async () => { + publishCalls++; + return { + success: false, + error: new Error('changeset publish exited with code 1'), + output: 'packages failed to publish', + }; + }, + verify: async () => false, + maxRetries: 3, + retryDelay: 0, + sleepFn: noSleep, + verifyOptions: { attempts: 1, initialDelay: 0, maxDelay: 0 }, + }); + + expect(result.success).toBe(false); + expect(publishCalls).toBe(3); +}); + +test('a non-retryable auth failure is not retried', async () => { + let publishCalls = 0; + const error = new Error('detected "npm error 404" in publish output'); + error.nonRetryable = true; + + const result = await publishWithRetry({ + publish: async () => { + publishCalls++; + return { success: false, error, output: 'npm error 404 Not Found - PUT' }; + }, + verify: async () => false, + maxRetries: 3, + retryDelay: 0, + sleepFn: noSleep, + verifyOptions: { attempts: 1, initialDelay: 0, maxDelay: 0 }, + }); + + expect(result.success).toBe(false); + expect(publishCalls).toBe(1); +}); diff --git a/js/tests/publish-to-npm.test.mjs b/js/tests/publish-to-npm.test.mjs index db055c28..138d2d5a 100644 --- a/js/tests/publish-to-npm.test.mjs +++ b/js/tests/publish-to-npm.test.mjs @@ -37,15 +37,29 @@ const isWindows = process.platform === 'win32'; let networkAvailable = !isWindows; -beforeAll(() => { +// The very first thing publish-to-npm.mjs does is load use-m from +// https://unpkg.com/use-m/use.js, at module scope — outside main()'s +// try/catch. When the CDN is unreachable the script dies during module +// initialisation: it never writes a line to GITHUB_OUTPUT and never prints its +// first log. Probing only `npm view` misses this — the npm registry and unpkg +// fail independently — and the suite then reports +// `Expected to contain: "published=true" / Received: ""`, which names neither +// the CDN nor the network as the cause. Both endpoints are probed. +// +// scripts/use-m-loader.mjs now gives that load a deadline, retries and an error +// naming the URL, so an outage is reported honestly instead of opaquely. The +// probe stays: it is cheaper to skip the suite once than to let every spawned +// case spend the loader's whole retry budget on a CDN that is down. +const USE_M_URL = 'https://unpkg.com/use-m/use.js'; + +beforeAll(async () => { // Skip the probe entirely on Windows so this hook can never exceed the suite's // global test timeout (the per-test timeout does not apply to hooks). if (isWindows) { return; } - // The script loads use-m + command-stream from unpkg/npm at runtime and the - // npm-view checks hit the registry. Skip gracefully when offline. Keep the - // probe timeout below the suite's global --timeout so the hook never trips it. + // Skip gracefully when offline. Keep both probe timeouts below the suite's + // global --timeout so the hook never trips it. try { const probe = spawnSync('npm', ['view', 'command-stream', 'version'], { encoding: 'utf8', @@ -55,8 +69,47 @@ beforeAll(() => { } catch { networkAvailable = false; } + if (!networkAvailable) { + return; + } + try { + const response = await fetch(USE_M_URL, { + signal: AbortSignal.timeout(8000), + }); + networkAvailable = response.ok; + } catch { + networkAvailable = false; + } }); +/** + * Fail with the child's own diagnostics when the script never started. + * + * `publish-to-npm.mjs` prints "Current version to publish: ..." before it does + * anything else, so stdout without that line means module initialisation threw + * (almost always the use-m load above) and every later assertion would compare + * against an empty string. Raising here puts the child's exit status and stderr + * in the failure message instead — since the loader landed, that stderr names + * the CDN and the URL. + * + * @param {{status:number|null, stdout:string, stderr:string, output:string}} result + * @returns {{status:number|null, stdout:string, stderr:string, output:string}} the same result + */ +function assertScriptStarted(result) { + if (result.stdout.includes('Current version to publish:')) { + return result; + } + throw new Error( + [ + 'publish-to-npm.mjs exited before it produced any output, so it never ran.', + `exit status: ${result.status}`, + `GITHUB_OUTPUT: ${JSON.stringify(result.output)}`, + `stdout: ${JSON.stringify(result.stdout)}`, + `stderr: ${JSON.stringify(result.stderr)}`, + ].join('\n') + ); +} + /** * Run publish-to-npm.mjs in an isolated temp package. * @param {object} opts @@ -94,12 +147,12 @@ function runPublish({ version, publishScript }) { }); const output = existsSync(outputFile) ? readFileSync(outputFile, 'utf8') : ''; - return { + return assertScriptStarted({ status: res.status, stdout: res.stdout || '', stderr: res.stderr || '', output, - }; + }); } test('does NOT report published when changeset:publish fails (exit 1)', () => { @@ -151,3 +204,206 @@ test('reports published for a version already on npm (legit success path)', () = expect(output).toContain('already_published=true'); expect(status).toBe(0); }, 130000); + +// --------------------------------------------------------------------------- +// Issue #199 — a slow registry after a successful publish must not fail the +// release. +// +// The three tests above run against the real npm registry, which cannot be made +// to lag on demand. These run the same real script against a stub registry +// served over HTTP (PUBLISH_REGISTRY_URL redirects the publication check only), +// so the exact production sequence from run 33914574283 is reproducible: +// +// pre-check -> 404 (version not published yet) +// publish -> npm E409 "Cannot publish over previously staged version" +// verification -> 404 on the first poll, then the version appears +// +// Before the fix this ended in "❌ Failed to publish after 3 attempts" while +// the version was live on npm. + +/** + * Serve npm package metadata that reveals `version` only from the Nth read on. + * @param {object} options + * @param {string} options.packageName + * @param {string} options.version + * @param {number} options.visibleFromRead - 1-based read index + * @returns {Promise<{url: string, reads: () => number, stop: () => void}>} + */ +async function startLaggingRegistry({ packageName, version, visibleFromRead }) { + let reads = 0; + const server = Bun.serve({ + port: 0, + fetch(request) { + const wanted = `/${encodeURIComponent(packageName)}`; + if (new URL(request.url).pathname !== wanted) { + return new Response('not found', { status: 404 }); + } + reads++; + if (reads < visibleFromRead) { + return new Response('{}', { status: 404 }); + } + return Response.json({ name: packageName, versions: { [version]: {} } }); + }, + }); + + return { + url: `http://127.0.0.1:${server.port}`, + reads: () => reads, + stop: () => server.stop(true), + }; +} + +/** + * Run publish-to-npm.mjs against a stub registry. + * @param {object} opts + * @param {string} opts.version + * @param {string} opts.publishScript + * @param {string} opts.registryUrl + * @returns {Promise<{status:number, stdout:string, stderr:string, output:string}>} + */ +async function runPublishAgainstRegistry({ + version, + publishScript, + registryUrl, +}) { + const dir = mkdtempSync(join(tmpdir(), 'issue199-publish-')); + writeFileSync( + join(dir, 'package.json'), + JSON.stringify( + { + name: 'command-stream', + version, + scripts: { 'changeset:publish': publishScript }, + }, + null, + 2 + ) + ); + const outputFile = join(dir, 'gh-output.txt'); + writeFileSync(outputFile, ''); + + // Must be asynchronous: the stub registry runs in this process, so a + // synchronous spawn would block the event loop and never answer a request. + const child = Bun.spawn(['bun', SCRIPT], { + cwd: dir, + stdout: 'pipe', + stderr: 'pipe', + env: { + ...process.env, + GITHUB_OUTPUT: outputFile, + // Only the publication check is redirected. Overriding NPM_CONFIG_REGISTRY + // would also redirect use-m's module installation, which must keep + // talking to the real registry. + PUBLISH_REGISTRY_URL: registryUrl, + PUBLISH_RETRY_DELAY: '0', + PUBLISH_VERIFY_DELAY: '0', + PUBLISH_VERIFY_MAX_DELAY: '0', + }, + }); + + const [stdout, stderr, status] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]); + + return assertScriptStarted({ + status, + stdout, + stderr, + output: existsSync(outputFile) ? readFileSync(outputFile, 'utf8') : '', + }); +} + +// The verbatim npm output from the failed run, escaped for `node -e`. +const E409_STAGED_OUTPUT = + 'npm error code E409\\nnpm error 409 Conflict - PUT https://registry.npmjs.org/command-stream - Cannot publish over previously staged version "0.20.1"'; + +test('issue #199: an E409 "previously staged version" resolves to a successful release', async () => { + if (!networkAvailable) { + return; + } // offline: skip (the script still fetches use-m from unpkg) + + const registry = await startLaggingRegistry({ + packageName: 'command-stream', + version: '0.20.1', + // read 1 = the pre-check (404), read 2 = first verification poll (404), + // read 3 = the version becomes visible. + visibleFromRead: 3, + }); + + try { + const { status, output, stdout } = await runPublishAgainstRegistry({ + version: '0.20.1', + publishScript: `node -e "console.error('${E409_STAGED_OUTPUT}'); process.exit(1)"`, + registryUrl: registry.url, + }); + + expect(output).toContain('published=true'); + expect(output).toContain('published_version=0.20.1'); + expect(output).not.toContain('published=false'); + expect(status).toBe(0); + // The publish command must not be re-run: republishing is what produced the + // E409 in the first place. + expect(stdout).toContain('Publish attempt 1 of 3'); + expect(stdout).not.toContain('Publish attempt 2 of 3'); + } finally { + registry.stop(); + } +}, 130000); + +test('issue #199: registry propagation lag after a clean publish is not a failure', async () => { + if (!networkAvailable) { + return; + } // offline: skip + + const registry = await startLaggingRegistry({ + packageName: 'command-stream', + version: '0.20.1', + visibleFromRead: 4, + }); + + try { + const { status, output, stdout } = await runPublishAgainstRegistry({ + version: '0.20.1', + publishScript: + 'node -e "console.log(\'🦋 success packages published successfully\'); process.exit(0)"', + registryUrl: registry.url, + }); + + expect(output).toContain('published=true'); + expect(status).toBe(0); + expect(stdout).not.toContain('Publish attempt 2 of 3'); + // Polling, not a single sample, is what makes this pass. + expect(registry.reads()).toBeGreaterThan(2); + } finally { + registry.stop(); + } +}, 130000); + +test('issue #166 stays fixed: verification exhaustion still fails the release', async () => { + if (!networkAvailable) { + return; + } // offline: skip + + const registry = await startLaggingRegistry({ + packageName: 'command-stream', + version: '0.20.1', + visibleFromRead: Number.MAX_SAFE_INTEGER, // never becomes visible + }); + + try { + const { status, output } = await runPublishAgainstRegistry({ + version: '0.20.1', + publishScript: + 'node -e "console.log(\'no projects to publish\'); process.exit(0)"', + registryUrl: registry.url, + }); + + expect(output).toContain('published=false'); + expect(output).not.toContain('published=true'); + expect(status).not.toBe(0); + } finally { + registry.stop(); + } +}, 130000); diff --git a/js/tests/repository-layout.test.mjs b/js/tests/repository-layout.test.mjs index 4d3fa8a7..1e3620b9 100644 --- a/js/tests/repository-layout.test.mjs +++ b/js/tests/repository-layout.test.mjs @@ -40,7 +40,6 @@ describe('repository language layout', () => { expect(existsFromRepo('package-lock.json')).toBe(false); expect(existsFromRepo('bun.lock')).toBe(false); expect(existsFromRepo('bunfig.toml')).toBe(false); - expect(existsFromRepo('eslint.config.js')).toBe(false); expect(existsFromRepo('.changeset/config.json')).toBe(false); expect(existsFromRepo('js/package.json')).toBe(true); @@ -52,6 +51,30 @@ describe('repository language layout', () => { expect(existsFromRepo('js/scripts/publish-to-npm.mjs')).toBe(true); }); + test('lints and formats from the repository root, not from js/', () => { + // eslint and prettier both treat the directory holding their configuration + // as the base path of the linted project. While these files lived in js/, + // repository-root JavaScript (claude-profiles.mjs, experiments/) was outside + // that base path and silently unlintable, and js/.prettierignore's + // docs/case-studies/**/{data,templates}/** patterns resolved against js/, + // where no such directories exist -- the archived evidence they were meant + // to protect is under the repository-root docs/. The root copies are what + // put the whole tree in scope; js/eslint.config.js stays as the rule set + // they re-export. + expect(existsFromRepo('eslint.config.js')).toBe(true); + expect(existsFromRepo('.prettierrc')).toBe(true); + expect(existsFromRepo('.prettierignore')).toBe(true); + expect(existsFromRepo('.lintstagedrc.json')).toBe(true); + + expect(existsFromRepo('js/.prettierrc')).toBe(false); + expect(existsFromRepo('js/.prettierignore')).toBe(false); + expect(existsFromRepo('js/.lintstagedrc.json')).toBe(false); + + expect(readFromRepo('eslint.config.js')).toContain( + "export { default } from './js/eslint.config.js'" + ); + }); + test('does not keep language release scripts at the repository root', () => { expect(existsFromRepo('scripts')).toBe(false); expect(existsFromRepo('scripts/publish-to-npm.mjs')).toBe(false); @@ -88,6 +111,13 @@ describe('repository language layout', () => { }); test('release jobs evaluate after PR-only gate jobs are skipped on push', () => { + // A release job needs an `if:` that is not `success()`, or GitHub skips it + // whenever a dependency was skipped -- which is exactly what happens to the + // PR-only gate jobs on a push to main. `!cancelled()` lifts that implicit + // requirement on its own; `always() && !cancelled()` reads as if it did + // something more, but `always()` is subsumed by the operand next to it and + // only invites `always()` being kept when the `!cancelled()` half is edited + // away, at which point a cancelled run would still publish. const jsWorkflow = readFromRepo('.github/workflows/js.yml'); const rustWorkflow = readFromRepo('.github/workflows/rust.yml'); @@ -102,14 +132,16 @@ describe('repository language layout', () => { ); expect(jsReleaseJob).toContain('needs: [lint, test]'); - expect(jsReleaseJob).toContain('always() && !cancelled()'); + expect(jsReleaseJob).toContain('!cancelled()'); + expect(jsReleaseJob).not.toContain('always()'); expect(jsReleaseJob).toContain("github.ref == 'refs/heads/main'"); expect(jsReleaseJob).toContain("github.event_name == 'push'"); expect(jsReleaseJob).toContain("needs.lint.result == 'success'"); expect(jsReleaseJob).toContain("needs.test.result == 'success'"); expect(rustReleaseJob).toContain('needs: [lint, test, scripts, build]'); - expect(rustReleaseJob).toContain('always() && !cancelled()'); + expect(rustReleaseJob).toContain('!cancelled()'); + expect(rustReleaseJob).not.toContain('always()'); expect(rustReleaseJob).toContain("github.ref == 'refs/heads/main'"); expect(rustReleaseJob).toContain("github.event_name == 'push'"); expect(rustReleaseJob).toContain("needs.lint.result == 'success'"); diff --git a/js/tests/use-m-loader.test.mjs b/js/tests/use-m-loader.test.mjs new file mode 100644 index 00000000..860b599c --- /dev/null +++ b/js/tests/use-m-loader.test.mjs @@ -0,0 +1,259 @@ +// Unit tests for js/scripts/use-m-loader.mjs +// +// Every release script starts by fetching https://unpkg.com/use-m/use.js and +// eval-ing it. Eleven of them did that inline, at module scope, with no +// timeout and no retry, so a CDN blip killed the script during module +// initialisation: no log line, nothing written to GITHUB_OUTPUT, and a bare +// `TypeError: fetch failed` on stderr. That is a third-party outage reported as +// a publish defect -- the class of false positive issue #199 is about. +// +// These tests pin the four properties that make the failure honest: a deadline, +// bounded retries, a status check before the eval, and an error naming the URL +// and the cause. The network is never touched: `fetchImpl` is injected. +import { test, expect } from 'bun:test'; +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { execFileSync } from 'child_process'; +import { + loadUseM, + resetUseMCache, + USE_M_URL, + DEFAULT_ATTEMPTS, + DEFAULT_TIMEOUT_MS, +} from '../scripts/use-m-loader.mjs'; + +const repoRoot = join(dirname(Bun.fileURLToPath(import.meta.url)), '..', '..'); + +/** A use.js bundle whose evaluation yields `{ use }`, like the real one. */ +const BUNDLE = '({ use: async (name) => ({ loaded: name }) })'; + +/** + * @param {string} body response body + * @param {{status?: number, statusText?: string}} [init] + * @returns {Response} + */ +const respond = (body, init = {}) => + new Response(body, { + status: init.status ?? 200, + statusText: init.statusText ?? 'OK', + }); + +/** Records the delays a retry would have slept, without sleeping. */ +const recordingSleep = (delays) => async (ms) => { + delays.push(ms); +}; + +test('a healthy CDN yields a callable use', async () => { + const seen = []; + const use = await loadUseM({ + fetchImpl: async (url, options) => { + seen.push({ url, hasSignal: Boolean(options?.signal) }); + return respond(BUNDLE); + }, + }); + + expect(typeof use).toBe('function'); + expect(await use('command-stream')).toEqual({ loaded: 'command-stream' }); + expect(seen).toEqual([{ url: USE_M_URL, hasSignal: true }]); +}); + +// The bare fetch had no deadline of its own, so a stalled connect burned the +// job's whole timeout-minutes on a socket that was never going to answer. +test('every attempt carries an abort deadline', async () => { + const signals = []; + await loadUseM({ + fetchImpl: async (_url, options) => { + signals.push(options.signal); + return respond(BUNDLE); + }, + }); + + expect(signals).toHaveLength(1); + expect(signals[0]).toBeInstanceOf(AbortSignal); + expect(signals[0].aborted).toBe(false); +}); + +test('an aborted fetch is reported as a load failure, not a hang', async () => { + await expect( + loadUseM({ + attempts: 1, + timeoutMs: 5, + fetchImpl: (_url, options) => + new Promise((_resolve, reject) => { + options.signal.addEventListener('abort', () => + reject(options.signal.reason) + ); + }), + }) + ).rejects.toThrow(/Failed to load use-m from https:\/\/unpkg\.com/); +}); + +// A CDN blip is transient by nature: retrying is the difference between a red +// release and a run that is a couple of seconds slower. +test('a transient failure is retried and then succeeds', async () => { + const delays = []; + let calls = 0; + const use = await loadUseM({ + sleep: recordingSleep(delays), + fetchImpl: async () => { + calls += 1; + if (calls < 3) { + throw new TypeError('fetch failed'); + } + return respond(BUNDLE); + }, + }); + + expect(typeof use).toBe('function'); + expect(calls).toBe(3); + // Exponential backoff: 2000, then 4000. + expect(delays).toEqual([2000, 4000]); +}); + +test('attempts are bounded and the last cause is preserved', async () => { + const delays = []; + let calls = 0; + const failure = new TypeError('fetch failed'); + + const error = await loadUseM({ + sleep: recordingSleep(delays), + fetchImpl: async () => { + calls += 1; + throw failure; + }, + }).then( + () => null, + (thrown) => thrown + ); + + expect(calls).toBe(DEFAULT_ATTEMPTS); + expect(delays).toHaveLength(DEFAULT_ATTEMPTS - 1); + expect(error.message).toContain(USE_M_URL); + expect(error.message).toContain(`after ${DEFAULT_ATTEMPTS} attempt(s)`); + expect(error.message).toContain('fetch failed'); + // The message has to say whose failure this is, so the run is re-run rather + // than investigated as a defect in the published package. + expect(error.message).toContain('network dependency'); + expect(error.cause).toBe(failure); +}); + +// eval-ing a CDN error page raises `SyntaxError: Unexpected token '<'`, which +// points at this repository's code for a response it never inspected. +test('an error page is reported by status, never evaluated', async () => { + let calls = 0; + const error = await loadUseM({ + attempts: 1, + fetchImpl: async () => { + calls += 1; + return respond('503 Service Unavailable', { + status: 503, + statusText: 'Service Unavailable', + }); + }, + }).then( + () => null, + (thrown) => thrown + ); + + expect(calls).toBe(1); + expect(error.message).toContain('HTTP 503 Service Unavailable'); + expect(error.message).not.toContain('Unexpected token'); +}); + +test('a bundle without a callable use names what it did export', async () => { + const error = await loadUseM({ + attempts: 1, + fetchImpl: async () => respond('({ notUse: 1, alsoNotUse: 2 })'), + }).then( + () => null, + (thrown) => thrown + ); + + expect(error.message).toContain('notUse'); + expect(error.message).toContain('alsoNotUse'); +}); + +test('a bundle nested under default is unwrapped', async () => { + const use = await loadUseM({ + fetchImpl: async () => + respond('({ default: { use: async (name) => ({ nested: name }) } })'), + }); + + expect(await use('lino-arguments')).toEqual({ nested: 'lino-arguments' }); +}); + +// An injected fetch must neither read nor write the process-wide cache, or one +// test would answer for the next. +test('injected fetches bypass the cache in both directions', async () => { + resetUseMCache(); + let calls = 0; + const fetchImpl = async () => { + calls += 1; + return respond(BUNDLE); + }; + + await loadUseM({ fetchImpl }); + await loadUseM({ fetchImpl }); + + expect(calls).toBe(2); +}); + +test('the defaults keep a stalled CDN inside a job timeout', () => { + expect(DEFAULT_ATTEMPTS).toBeGreaterThan(1); + // Worst case: attempts * timeout + backoff, well under the 10-minute + // timeout-minutes the release jobs declare. + const worstCaseMs = DEFAULT_ATTEMPTS * DEFAULT_TIMEOUT_MS + 6000; + expect(worstCaseMs).toBeLessThan(10 * 60 * 1000); +}); + +// The point of the shared loader is that the hardening applies everywhere. A +// file that fetches use.js inline gets none of it back, so the whole tree is +// checked, not only js/scripts/. +test('no script fetches use-m inline any more', () => { + // Excluded on purpose: the loader itself, the tests and the experiment, which + // quote the old statement as the thing they are about, and the archived + // copies of other repositories' code under dev/log and docs/case-studies. + const isExcluded = (file) => + file === 'js/scripts/use-m-loader.mjs' || + file.startsWith('js/tests/') || + file.startsWith('experiments/') || + file.startsWith('dev/log/') || + file.startsWith('docs/case-studies/'); + + const files = execFileSync('git', ['ls-files', '*.mjs', '*.js', '*.cjs'], { + cwd: repoRoot, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean) + .filter((file) => !isExcluded(file)); + + const offenders = files.filter((file) => + readFileSync(join(repoRoot, file), 'utf8').includes('unpkg.com/use-m') + ); + + expect(offenders).toEqual([]); +}); + +// Every script that needs use-m goes through the loader: eleven release +// scripts plus the profile CLI at the repository root. +test('the release scripts load use-m through the loader', () => { + const files = execFileSync('git', ['ls-files', 'js/scripts/*.mjs'], { + cwd: repoRoot, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean) + .filter((file) => !file.endsWith('use-m-loader.mjs')); + + const viaLoader = files.filter((file) => + readFileSync(join(repoRoot, file), 'utf8').includes('loadUseM(') + ); + + expect(viaLoader.length).toBeGreaterThanOrEqual(11); + expect(readFileSync(join(repoRoot, 'claude-profiles.mjs'), 'utf8')).toContain( + 'loadUseM(' + ); +}); diff --git a/js/tests/workflow-hygiene.test.mjs b/js/tests/workflow-hygiene.test.mjs new file mode 100644 index 00000000..4fb4b924 --- /dev/null +++ b/js/tests/workflow-hygiene.test.mjs @@ -0,0 +1,715 @@ +// Guards the CI/CD invariants fixed for issue #199. actionlint and zizmor run in +// .github/workflows/workflows.yml and cover syntax and security, but neither one +// knows about the repository-specific rules below: how concurrency has to be +// shaped so a release is never cancelled mid-publish, that `always()` must not +// be used where `!cancelled()` is meant, and that matrix job names must stay +// distinguishable. +import { describe, test, expect } from 'bun:test'; +import { readFileSync, readdirSync } from 'fs'; +import { join, dirname, basename } from 'path'; +import { fileURLToPath } from 'url'; +import { execFileSync } from 'child_process'; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const workflowDir = join(repoRoot, '.github', 'workflows'); + +const workflowFiles = readdirSync(workflowDir) + .filter((name) => name.endsWith('.yml') || name.endsWith('.yaml')) + .sort(); + +const workflows = workflowFiles.map((name) => { + const text = readFileSync(join(workflowDir, name), 'utf8'); + return { name, text, doc: Bun.YAML.parse(text) }; +}); + +/** + * Jobs that mutate the repository: push a commit or a tag to main, publish a + * package, or open a release pull request. These are the ones that must never + * be cancelled halfway. + * + * `contents: write` is the test, not `pull-requests: write`. A job can hold the + * latter alone and still change nothing that outlives the run -- the security + * workflow's dependency-review only uses it to leave a review comment -- and + * putting such a job in the shared non-cancellable group would serialise every + * pull request behind main's releases for no benefit. + */ +const isWriterJob = (job) => (job.permissions ?? {})['contents'] === 'write'; + +const WRITER_GROUP = 'main-writer-${{ github.repository }}-main'; + +describe('workflow files', () => { + test('at least the four known workflows are present', () => { + expect(workflowFiles).toContain('js.yml'); + expect(workflowFiles).toContain('rust.yml'); + expect(workflowFiles).toContain('parity.yml'); + // Added for #199: nothing linted the workflows themselves before, and + // nothing audited the dependency trees or analysed the sources. + expect(workflowFiles).toContain('workflows.yml'); + expect(workflowFiles).toContain('security.yml'); + }); + + test.each(workflows.map((w) => [w.name, w]))( + '%s declares a least-privilege top-level permissions block', + (_name, workflow) => { + // Without an explicit block the job token keeps whatever the repository + // default is, which is `write-all` on older repositories. + expect(workflow.doc.permissions).toEqual({ contents: 'read' }); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s keeps secrets out of the workflow-level env block', + (_name, workflow) => { + // A workflow-level `env:` is inherited by every job, so declaring the + // crates.io token there handed it to `cargo test` and `cargo clippy` on + // pull requests -- both of which compile and run code from the branch + // under review, via build.rs, proc macros or the tests themselves. + // Publishing credentials belong on the jobs that publish. + for (const [key, value] of Object.entries(workflow.doc.env ?? {})) { + expect(`${key}: ${String(value).includes('secrets.')}`).toBe( + `${key}: false` + ); + } + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s uses !cancelled() rather than always()', + (_name, workflow) => { + // `always()` keeps a job running after the run is cancelled, so a + // cancelled prerequisite still lets its dependents start. + const conditions = Object.values(workflow.doc.jobs) + .map((job) => String(job.if ?? '')) + .filter((cond) => cond.includes('always()')); + expect(conditions).toEqual([]); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s has no expression interpolation inside run: blocks', + (_name, workflow) => { + // `${{ }}` is pasted into the shell before it runs; an attacker-controlled + // value (a branch name on a fork PR) becomes shell code. Pass values + // through `env:` instead. + const offenders = []; + for (const [jobId, job] of Object.entries(workflow.doc.jobs)) { + for (const step of job.steps ?? []) { + if (typeof step.run === 'string' && step.run.includes('${{')) { + offenders.push(`${jobId}: ${step.name ?? step.run.slice(0, 40)}`); + } + } + } + expect(offenders).toEqual([]); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s pins every third-party action to a full commit hash', + (_name, workflow) => { + // Matches .github/zizmor.yml: these publishers are trusted at tag + // granularity, everything else must be hash-pinned. + const refPinnedOwners = [ + 'actions', + 'github', + 'docker', + 'astral-sh', + 'lycheeverse', + 'zizmorcore', + 'changesets', + ]; + const offenders = []; + for (const job of Object.values(workflow.doc.jobs)) { + for (const step of job.steps ?? []) { + const uses = step.uses; + if (typeof uses !== 'string' || uses.startsWith('docker://')) { + continue; + } + const [path, ref] = uses.split('@'); + if (refPinnedOwners.includes(path.split('/')[0])) { + continue; + } + if (!/^[0-9a-f]{40}$/.test(ref ?? '')) { + offenders.push(uses); + } + } + } + expect(offenders).toEqual([]); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s scopes concurrency per job instead of per workflow when it can write', + (_name, workflow) => { + const jobs = Object.values(workflow.doc.jobs); + if (!jobs.some(isWriterJob)) { + return; + } + // A workflow-level cancellable group cancels the whole run, including a + // release that has already started publishing. + expect(workflow.doc.concurrency).toBeUndefined(); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s gives every job a concurrency group', + (_name, workflow) => { + const missing = Object.entries(workflow.doc.jobs) + .filter(([, job]) => !job.concurrency?.group) + .map(([jobId]) => jobId); + expect(missing).toEqual([]); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s puts writer jobs in the shared non-cancellable group', + (_name, workflow) => { + for (const [jobId, job] of Object.entries(workflow.doc.jobs)) { + if (!isWriterJob(job)) { + continue; + } + expect(`${jobId}: ${job.concurrency.group}`).toBe( + `${jobId}: ${WRITER_GROUP}` + ); + expect(`${jobId}: ${job.concurrency['cancel-in-progress']}`).toBe( + `${jobId}: false` + ); + } + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s keeps check jobs cancellable and matrix entries independent', + (_name, workflow) => { + for (const [jobId, job] of Object.entries(workflow.doc.jobs)) { + if (isWriterJob(job)) { + continue; + } + const group = job.concurrency.group; + expect(`${jobId}: ${group.startsWith('check-')}`).toBe( + `${jobId}: true` + ); + expect(`${jobId}: ${job.concurrency['cancel-in-progress']}`).toBe( + `${jobId}: true` + ); + // Every matrix dimension must appear in the group, otherwise the matrix + // entries share one group and cancel each other. + for (const key of Object.keys(job.strategy?.matrix ?? {})) { + if (key === 'include' || key === 'exclude') { + continue; + } + expect(`${jobId}/${key}: ${group.includes(`matrix.${key}`)}`).toBe( + `${jobId}/${key}: true` + ); + } + } + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s gives every matrix job a name that distinguishes its entries', + (_name, workflow) => { + // Three Node entries in js.yml differed only by `node-version`, which was + // missing from the name, so all three reported as "Test JavaScript (node + // on ubuntu-latest)" and no branch rule could require a specific one. + for (const [jobId, job] of Object.entries(workflow.doc.jobs)) { + const matrix = job.strategy?.matrix; + if (!matrix) { + continue; + } + const keys = new Set( + Object.keys(matrix).filter((k) => k !== 'include') + ); + for (const entry of matrix.include ?? []) { + Object.keys(entry).forEach((k) => keys.add(k)); + } + const name = job.name ?? jobId; + for (const key of keys) { + expect(`${jobId}/${key}: ${name.includes(`matrix.${key}`)}`).toBe( + `${jobId}/${key}: true` + ); + } + } + } + ); +}); + +describe('workflow linting is itself wired into CI', () => { + const lintWorkflow = workflows.find((w) => w.name === 'workflows.yml'); + + test('actionlint runs from the Docker image that bundles shellcheck', () => { + // A native actionlint binary without shellcheck on PATH skips every `run:` + // check and still exits 0. + const uses = Object.values(lintWorkflow.doc.jobs) + .flatMap((job) => job.steps ?? []) + .map((step) => step.uses) + .filter(Boolean); + expect(uses.some((u) => u.startsWith('docker://rhysd/actionlint:'))).toBe( + true + ); + }); + + test('zizmor runs with the repository policy at low confidence', () => { + const step = Object.values(lintWorkflow.doc.jobs) + .flatMap((job) => job.steps ?? []) + .find((s) => (s.uses ?? '').startsWith('zizmorcore/zizmor-action@')); + expect(step.with.config).toBe('.github/zizmor.yml'); + // `artipacked` -- a checkout that leaves the job token in .git/config -- is + // a Low-confidence audit, so `medium` hides every one of them. That blind + // spot is what made the JavaScript template ship 25 credential-persisting + // checkouts while its audit reported three findings + // (js-ai-driven-development-pipeline-template#160). + expect(String(step.with['min-confidence'])).toBe('low'); + // The action's default input is `.`, which walks the whole tree and picks up + // docs/case-studies/**/templates/**: verbatim archived copies of other + // repositories' workflows, kept as evidence. Auditing those reported 30 + // findings in files that never run here and that a fix would falsify. + expect(step.with.inputs).toBe('.github/workflows'); + }); + + test.each(workflows.map((w) => [w.name, w]))( + 'every checkout in %s drops the token or says why it keeps it', + (_name, workflow) => { + // The counterpart of running zizmor at low confidence: a checkout either + // sets persist-credentials: false, or is one of the writer jobs that + // pushes with that credential and carries the suppression inline. A new + // checkout that does neither fails here and in the audit. + const lines = workflow.text.split('\n'); + lines.forEach((line, index) => { + if (!/^\s*-\s+uses:\s*actions\/checkout@/.test(line)) { + return; + } + if (line.includes('zizmor: ignore[artipacked]')) { + return; + } + // The `with:` block of this step: everything indented deeper, up to the + // next step or the end of the file. + const indent = line.search(/\S/); + const block = []; + for (let i = index + 1; i < lines.length; i += 1) { + const next = lines[i]; + if (next.trim() !== '' && next.search(/\S/) <= indent) { + break; + } + block.push(next); + } + expect(`${workflow.name}:${index + 1} ${block.join('\n')}`).toContain( + 'persist-credentials: false' + ); + }); + } + ); + + test('only the release jobs suppress artipacked', () => { + // Six suppressions today, all in jobs that push to main or publish. The + // count is asserted so adding one is a deliberate edit here rather than a + // quiet copy-paste. + const suppressions = workflows.flatMap((workflow) => + workflow.text + .split('\n') + .filter((line) => line.includes('zizmor: ignore[artipacked]')) + .map(() => workflow.name) + ); + expect(suppressions.length).toBe(6); + expect(new Set(suppressions)).toEqual(new Set(['js.yml', 'rust.yml'])); + }); + + test('the zizmor policy requires hash pins by default', () => { + const policy = Bun.YAML.parse( + readFileSync(join(repoRoot, '.github', 'zizmor.yml'), 'utf8') + ); + expect(policy.rules['unpinned-uses'].config.policies['*']).toBe('hash-pin'); + }); + + test('the lint workflow triggers on changes to .github', () => { + const paths = lintWorkflow.doc.on.pull_request.paths; + expect(paths.some((p) => p.startsWith('.github'))).toBe(true); + }); + + test('every workflow file is covered by these checks', () => { + expect(workflows.map((w) => basename(w.name)).length).toBe( + workflowFiles.length + ); + expect(workflowFiles.length).toBeGreaterThanOrEqual(4); + }); +}); + +describe('every shipped ecosystem is audited', () => { + const security = workflows.find((w) => w.name === 'security.yml'); + const runs = Object.values(security.doc.jobs) + .flatMap((job) => job.steps ?? []) + .map((step) => step.run) + .filter(Boolean) + .join('\n'); + + test('both JavaScript lockfiles are audited, not just one', () => { + // package-lock.json and bun.lock resolve transitive versions + // independently, so one can be clean while the other is not: they differed + // by 8 high-severity advisories when this workflow was written. + expect(runs).toContain('npm audit --package-lock-only --audit-level=high'); + expect(runs).toContain('bun audit --audit-level=high'); + }); + + test('the Rust lockfile is audited', () => { + expect(runs).toContain('cargo audit --file Cargo.lock'); + }); + + test('CodeQL covers both languages and the workflows', () => { + const languages = security.doc.jobs.codeql.strategy.matrix.language; + expect(languages).toContain('javascript-typescript'); + expect(languages).toContain('rust'); + expect(languages).toContain('actions'); + }); + + test('the working tree is scanned for committed credentials', () => { + // Nothing looked for credentials in the tree: CodeQL does not, and the + // audit jobs only read lockfiles (issue #199, best practice #11). + expect(runs).toContain('secretlint'); + const policy = JSON.parse( + readFileSync(join(repoRoot, '.secretlintrc.json'), 'utf8') + ); + expect(policy.rules.map((rule) => rule.id)).toContain( + '@secretlint/secretlint-rule-preset-recommend' + ); + // The ignore list may exclude generated trees, never authored source. + const ignored = readFileSync(join(repoRoot, '.secretlintignore'), 'utf8') + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')); + for (const pattern of ignored) { + expect( + `${pattern}: ${/^(node_modules|rust\/target|js\/(reports|coverage))\//.test(pattern)}` + ).toBe(`${pattern}: true`); + } + }); + + test('the security workflow is not narrowed by a paths filter', () => { + // js.yml and rust.yml only run for their own language's files. The audits + // and the secret scan have to see every change, so this workflow must stay + // unfiltered -- a `paths:` here would let a credential in, say, a case + // study reach main unscanned. + expect(security.doc.on.pull_request?.paths).toBeUndefined(); + }); + + test('the audits also run on a schedule', () => { + // A new advisory lands against code that has not changed, so the + // push/pull_request triggers alone would leave it unreported until the + // next commit. + expect(security.doc.on.schedule?.length).toBeGreaterThan(0); + }); +}); + +describe('the shipped quality gates are actually invoked', () => { + const rust = workflows.find((w) => w.name === 'rust.yml'); + const rustRuns = Object.values(rust.doc.jobs) + .flatMap((job) => job.steps ?? []) + .map((step) => step.run) + .filter(Boolean) + .join('\n'); + + // rust/scripts/ shipped four `check-*.rs` guards, but only the changelog one + // was ever executed: the other three were referenced by no workflow, no + // script and no document (issue #199). A gate nobody runs is a silent false + // negative -- the pipeline reports "all checks passed" while the check does + // not exist. + test.each([ + ['check-changelog-fragment.rs'], + ['check-version-modification.rs'], + ['check-file-size.rs'], + ['check-crate-size.rs'], + ])('rust.yml runs %s', (script) => { + expect(rustRuns).toContain(`rust-script rust/scripts/${script}`); + }); + + test('every rust/scripts entry is invoked or a documented exception', () => { + // Standalone entry points that this repository deliberately does not wire + // up. They come from the Rust pipeline template, where separate workflow + // steps call them; here the same work happens elsewhere. + const unwired = new Map([ + // version-and-commit.rs does its own bumping (`Version::bump`) and its + // own fragment collection (`collect_changelog`), so these two standalone + // entry points would be a second implementation of the same steps. + ['bump-version.rs', 'version-and-commit.rs does both steps itself'], + ['collect-changelog.rs', 'version-and-commit.rs does both steps itself'], + // The workflows select what runs with `on: paths:` filters instead of + // computing a change matrix in a first job. + ['detect-code-changes.rs', 'replaced by on: paths: filters'], + // The release jobs set the bot identity inline, next to the commit they + // are about to make. + ['git-config.rs', 'release jobs configure git inline'], + ]); + + const scriptDir = join(repoRoot, 'rust', 'scripts'); + const scripts = readdirSync(scriptDir).filter((n) => n.endsWith('.rs')); + const sources = scripts.map((n) => + readFileSync(join(scriptDir, n), 'utf8') + ); + const workflowText = workflows.map((w) => w.text).join('\n'); + + for (const script of scripts) { + const referenced = + workflowText.includes(script) || + sources.some( + (text, i) => scripts[i] !== script && text.includes(script) + ); + expect(`${script}: ${referenced || unwired.has(script)}`).toBe( + `${script}: true` + ); + } + }); + + test('both languages enforce a maximum file length', () => { + // Principle #2 of the hive-mind CI/CD best practices. JavaScript gets this + // from eslint; Rust had the script but no caller. + const eslint = readFileSync( + join(repoRoot, 'js', 'eslint.config.js'), + 'utf8' + ); + expect(eslint).toContain("'max-lines': ['error', 1500]"); + expect(rustRuns).toContain('rust-script rust/scripts/check-file-size.rs'); + }); +}); + +describe('checks validate the merge result, not a stale preview', () => { + const simulation = '.github/scripts/simulate-fresh-merge.sh'; + + // Best practice #7. A pull-request run checks out refs/pull/N/merge, computed + // when the pull request was last synchronised; if main moved since, the checks + // pass on a combination that will not exist after the merge. + const simulationStep = (job) => + (job.steps ?? []).find((step) => (step.run ?? '').includes(simulation)); + + /** + * Jobs that run on pull requests and deliberately do not merge the base + * branch first, with the reason. Anything not listed here has to simulate the + * merge, so a new job cannot quietly go back to checking a stale preview. + */ + const exempt = new Map([ + [ + 'js.yml/changeset-check', + 'diffs base against head; a local merge changes neither side of that diff', + ], + ['rust.yml/changelog', 'same: the two guards it runs are diff-based'], + ['parity.yml/parity', 'same: it diffs the merge base against HEAD'], + [ + 'security.yml/codeql', + 'uploads results keyed to the checked-out commit, and GitHub rejects a commit it has never seen', + ], + [ + 'security.yml/dependency-review', + 'compares two commit SHAs through the API and never reads the tree', + ], + ]); + + const pullRequestJobs = workflows + .filter((workflow) => 'pull_request' in (workflow.doc.on ?? {})) + .flatMap((workflow) => + Object.entries(workflow.doc.jobs) + // Writers run on push and workflow_dispatch, never on a pull request. + .filter(([, job]) => !isWriterJob(job)) + .map(([jobId, job]) => [`${workflow.name}/${jobId}`, job]) + ); + + test('the exemption list has no stale entries', () => { + const known = new Set(pullRequestJobs.map(([key]) => key)); + expect([...exempt.keys()].filter((key) => !known.has(key))).toEqual([]); + }); + + test.each(pullRequestJobs)( + '%s merges the base branch before it checks anything', + (key, job) => { + if (exempt.has(key)) { + return; + } + const steps = job.steps; + const index = steps.indexOf(simulationStep(job)); + expect(`${key}: ${index !== -1}`).toBe(`${key}: true`); + // Everything that inspects the tree has to come after the merge. + const checkout = steps.findIndex((step) => + (step.uses ?? '').startsWith('actions/checkout@') + ); + expect(checkout).toBeLessThan(index); + expect(index).toBeLessThan(steps.length - 1); + } + ); + + test.each(workflows.map((w) => [w.name, w]))( + '%s only simulates a merge where it can work', + (_name, workflow) => { + for (const [jobId, job] of Object.entries(workflow.doc.jobs)) { + const step = simulationStep(job); + if (!step) { + continue; + } + // `github.base_ref` is empty outside a pull request, and the merge + // needs history a shallow checkout does not have. + expect(`${jobId}: ${step.if}`).toBe( + `${jobId}: github.event_name == 'pull_request'` + ); + const checkout = job.steps.find((s) => + (s.uses ?? '').startsWith('actions/checkout@') + ); + expect(`${jobId}: ${checkout.with['fetch-depth']}`).toBe(`${jobId}: 0`); + } + } + ); +}); + +describe('repository-wide checks are not hidden behind a paths filter', () => { + // Every other workflow is scoped: js.yml to `js/**`, rust.yml to `rust/**`, + // workflows.yml to `.github/**`. A pull request touching only `docs/**` used + // to match none of them and ran nothing at all, and the formatter -- which + // reads every tracked file -- only ran behind the `js/**` filter, so a + // violation introduced in a workflow or a markdown file first turned red on + // an unrelated JavaScript pull request. + const quality = workflows.find((w) => w.name === 'quality.yml'); + + test('the quality workflow exists and runs on every pull request', () => { + expect(quality).toBeDefined(); + expect(quality.doc.on.pull_request?.paths).toBeUndefined(); + expect(quality.doc.on.push.paths).toBeUndefined(); + }); + + test.each([ + ['format', 'bun run format:check'], + ['docs', 'bun test js/tests/docs-validation.test.mjs'], + ['hygiene', 'bun test js/tests/workflow-hygiene.test.mjs'], + ])('the %s job runs %s', (jobId, command) => { + const runs = (quality.doc.jobs[jobId].steps ?? []) + .map((step) => step.run ?? '') + .join('\n'); + expect(`${jobId}: ${runs.includes(command)}`).toBe(`${jobId}: true`); + }); + + test.each(workflows.map((w) => [w.name, w]))( + '%s filters push and pull_request identically', + (_name, workflow) => { + // Two lists that drift apart mean a check runs on the pull request and + // then not on the merge to main, or the other way round. Keeping them + // equal is what makes a green pull request predict a green main. + const on = workflow.doc.on ?? {}; + if (!on.push?.paths || !on.pull_request?.paths) { + return; + } + expect(on.push.paths).toEqual(on.pull_request.paths); + } + ); + + test('every file eslint lints outside js/ triggers the lint job', () => { + // ESLint's configuration sits at the repository root so that experiments/ + // and claude-profiles.mjs are inside the lint scope. js.yml's `paths:` + // filter has to list them too, otherwise the lint job that would catch an + // error in them does not start. + const js = workflows.find((w) => w.name === 'js.yml'); + const patterns = js.doc.on.pull_request.paths; + // execFileSync: with a shell, cmd.exe passes the quotes through to git on + // Windows, the pattern matches nothing, and the empty line that leaves + // behind is reported as an uncovered file. + const linted = execFileSync('git', ['ls-files', '*.mjs', '*.js', '*.cjs'], { + cwd: repoRoot, + encoding: 'utf8', + }) + .trim() + .split('\n') + .filter(Boolean) + .filter( + (file) => + !file.startsWith('js/') && + !file.startsWith('dev/log/') && + // Mirrors the ignores in js/eslint.config.js: archived evidence. + !/docs\/case-studies\/[^/]+\/(templates|data|log-excerpts)\//.test( + file + ) + ); + const uncovered = linted.filter( + (file) => + !patterns.some((pattern) => + pattern.endsWith('/**') + ? file.startsWith(pattern.slice(0, -2)) + : file === pattern + ) + ); + expect(uncovered).toEqual([]); + }); + + test('the formatter is not confined to one language directory', () => { + // js/package.json's format:check steps out of js/ on purpose; the ignore + // rules that matter live in the repository-root .prettierignore. + const scripts = JSON.parse( + readFileSync(join(repoRoot, 'js', 'package.json'), 'utf8') + ).scripts; + expect(scripts['format:check']).toContain('cd ..'); + }); +}); + +describe('external links are checked without gating pull requests', () => { + // Best practice #12 names lychee. Both pipeline templates run it as a + // pull-request gate; here that would have turned unrelated pull requests red, + // because a run over this tree reports 20 errors and every one of them is a + // link that is correct in the document and unreachable from a runner + // (npmjs.com answers 403 to non-browser clients, GitHub serves the stargazers + // list and /settings/ only to a signed-in session). So the check is split: + // relative links -- the only ones a change here can break -- are resolved + // offline on every pull request by docs-validation.test.mjs, and the network + // is fetched on a schedule instead. + const links = workflows.find((w) => w.name === 'links.yml'); + + const lycheeStep = () => + Object.values(links.doc.jobs) + .flatMap((job) => job.steps ?? []) + .find((step) => + (step.uses ?? '').startsWith('lycheeverse/lychee-action') + ); + + test('the link workflow exists and runs lychee', () => { + expect(links).toBeDefined(); + expect(lycheeStep()).toBeDefined(); + }); + + test('it runs on a schedule and on demand, never on a pull request', () => { + const on = links.doc.on ?? {}; + expect(on.schedule?.length).toBeGreaterThan(0); + expect('workflow_dispatch' in on).toBe(true); + // A third-party site going down is not a reason to block someone's merge, + // and a red check nobody can fix from the branch is how a pipeline teaches + // people to ignore red checks. + expect('pull_request' in on).toBe(false); + expect('push' in on).toBe(false); + }); + + test('a broken link fails the scheduled run', () => { + // `fail: false` is what the templates use, because a later step decides; + // there is no later step here, so the action itself has to fail the job or + // the schedule reports success no matter what it found. + expect(lycheeStep().with.fail).toBe(true); + }); + + test('archived copies of other repositories are excluded', () => { + // Same trees docs-validation.test.mjs treats as archived: their links point + // into the repository they were copied from. The archived hive-mind + // best-practices document alone contributes five unfixable errors. + const args = String(lycheeStep().with.args); + expect(args).toContain('--exclude-path dev/log'); + expect(args).toContain('--exclude-path docs/case-studies'); + }); + + test('every .lycheeignore entry is a valid regex with a stated reason', () => { + // An ignore list is where a link checker goes to die: one uncommented line + // and the next reader cannot tell a bot-walled host from a link somebody + // gave up on. + const lines = readFileSync(join(repoRoot, '.lycheeignore'), 'utf8').split( + '\n' + ); + const patterns = []; + lines.forEach((line, index) => { + const value = line.trim(); + if (!value || value.startsWith('#')) { + return; + } + patterns.push(value); + const previous = (lines[index - 1] ?? '').trim(); + expect(`${value}: ${previous.startsWith('#')}`).toBe(`${value}: true`); + expect(() => new RegExp(value)).not.toThrow(); + }); + expect(patterns.length).toBeGreaterThan(0); + }); +}); diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2fc246c5..4f78b2ff 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -123,9 +123,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 83bdb004..108cb422 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -44,6 +44,18 @@ assert_cmd = "2.0" default = [] json = [] +# Lint policy. `cargo clippy` and `cargo build` only *report* warnings, so the +# `-D warnings` in .github/workflows/rust.yml is what turns these into a failing +# build. Both halves are needed: this section makes the policy visible to local +# builds and to `cargo clippy --fix`, the workflow makes it enforceable. +[lints.rust] +# The crate contains no `unsafe` block today (nix/libc are used through their +# safe wrappers); `forbid` keeps it that way and cannot be locally overridden. +unsafe_code = "forbid" + +[lints.clippy] +all = { level = "warn", priority = -1 } + [profile.release] opt-level = 3 lto = true diff --git a/rust/changelog.d/20260904_223000_ci-warnings-and-audit.md b/rust/changelog.d/20260904_223000_ci-warnings-and-audit.md new file mode 100644 index 00000000..c4b95691 --- /dev/null +++ b/rust/changelog.d/20260904_223000_ci-warnings-and-audit.md @@ -0,0 +1,13 @@ +--- +bump: patch +--- + +### Fixed + +- `bytes` bumped to 1.12.1, clearing RUSTSEC-2026-0007 (integer overflow in `BytesMut::reserve`). The advisory went unnoticed because nothing in the pipeline audited the lockfile; `cargo audit` now runs on every push, pull request and weekly. +- `ls` no longer computes a file type character it never used, and iterates directory entries with `.flatten()` instead of matching on each `Result`. + +### Changed + +- The Rust pipeline denies warnings: `RUSTFLAGS`/`RUSTDOCFLAGS` are `-Dwarnings`, clippy runs with `-- -D warnings`, and `cargo doc --no-deps` gates the rustdoc-only lints. Clippy previously printed 15 warnings and exited 0. `Cargo.toml` forbids `unsafe_code` and warns on `clippy::all`. +- `CommandContext::cwd` is covered by tests: `pwd` honours it, and `ls` resolves both a relative path and its default argument against it. diff --git a/rust/changelog.d/README.md b/rust/changelog.d/README.md index b3437e32..38d4a027 100644 --- a/rust/changelog.d/README.md +++ b/rust/changelog.d/README.md @@ -23,6 +23,7 @@ bump: patch --- ### Fixed + - Description of bug fix ``` @@ -44,21 +45,27 @@ bump: minor --- ### Added + - Description of new feature ### Changed + - Description of change to existing functionality ### Fixed + - Description of bug fix ### Removed + - Description of removed feature ### Deprecated + - Description of deprecated feature ### Security + - Description of security fix ``` @@ -72,6 +79,7 @@ bump: minor --- ### Added + - New async processing mode for batch operations ``` @@ -83,6 +91,7 @@ bump: patch --- ### Fixed + - Fixed memory leak in connection pool handling ``` @@ -94,9 +103,11 @@ bump: major --- ### Changed + - Renamed `process()` to `process_async()` - this is a breaking change ### Removed + - Removed deprecated `legacy_mode` option ``` diff --git a/rust/scripts/version-and-commit.rs b/rust/scripts/version-and-commit.rs index d3e0822e..c01f39f4 100644 --- a/rust/scripts/version-and-commit.rs +++ b/rust/scripts/version-and-commit.rs @@ -23,6 +23,13 @@ //! serde_json = "1" //! ``` +// `rust-script --test` builds this file as a test harness, where `main` is not +// the entry point. Everything reachable only from `main` -- most of the file -- +// is therefore unreferenced, and the pipeline's RUSTFLAGS=-Dwarnings turns that +// into a build failure. The imports below were already gated on `not(test)` for +// the same reason. The real, non-test build still denies dead code. +#![cfg_attr(test, allow(dead_code))] + #[cfg(not(test))] use chrono::Utc; use regex::Regex; diff --git a/rust/src/commands/cd.rs b/rust/src/commands/cd.rs index b52e6fd5..75556441 100644 --- a/rust/src/commands/cd.rs +++ b/rust/src/commands/cd.rs @@ -43,7 +43,7 @@ pub(crate) struct CdContext { /// - `cd` -> change to $HOME (or $USERPROFILE on Windows) /// - `cd ~`/`cd ~/x` -> tilde expands to $HOME /// - `cd -` -> change to $OLDPWD and print the new directory (like sh) -/// - `cd ` -> change to (relative paths resolve against the +/// - `cd ` -> change to `` (relative paths resolve against the /// current working directory, or the `cwd` option) /// /// This low-level command API retains its original process-mutating behavior. diff --git a/rust/src/commands/ls.rs b/rust/src/commands/ls.rs index 160f01f4..65230fa0 100644 --- a/rust/src/commands/ls.rs +++ b/rust/src/commands/ls.rs @@ -67,20 +67,20 @@ pub async fn ls(ctx: CommandContext) -> CommandResult { Ok(entries) => { let mut entry_strs = Vec::new(); - for entry in entries { - if let Ok(entry) = entry { - let name = entry.file_name().to_string_lossy().to_string(); - - // Skip hidden files unless -a is specified - if !show_all && name.starts_with('.') { - continue; - } - - if long_format { - entry_strs.push(format_entry(&entry.path(), true)); - } else { - entry_strs.push(name); - } + // `flatten()` drops unreadable entries, matching the + // behaviour of `ls`, which lists what it can read. + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + + // Skip hidden files unless -a is specified + if !show_all && name.starts_with('.') { + continue; + } + + if long_format { + entry_strs.push(format_entry(&entry.path(), true)); + } else { + entry_strs.push(name); } } @@ -121,7 +121,6 @@ fn format_entry(path: &Path, long_format: bool) -> String { Err(_) => return name, }; - let file_type = if metadata.is_dir() { "d" } else { "-" }; let size = metadata.len(); // Simplified permissions diff --git a/rust/src/commands/mod.rs b/rust/src/commands/mod.rs index 48dcbea4..fc90fcfc 100644 --- a/rust/src/commands/mod.rs +++ b/rust/src/commands/mod.rs @@ -50,7 +50,6 @@ pub use yes::yes; use crate::utils::CommandResult; use std::collections::HashMap; -use std::path::Path; use tokio::sync::mpsc; /// Context for virtual command execution diff --git a/rust/src/commands/sleep.rs b/rust/src/commands/sleep.rs index 81058c13..1ec012b4 100644 --- a/rust/src/commands/sleep.rs +++ b/rust/src/commands/sleep.rs @@ -47,7 +47,7 @@ pub async fn sleep(ctx: CommandContext) -> CommandResult { } } => { trace_lazy("VirtualCommand", || { - format!("sleep: cancelled after partial sleep") + "sleep: cancelled after partial sleep".to_string() }); CommandResult::error_with_code("", 130) // SIGINT exit code } diff --git a/rust/src/commands/touch.rs b/rust/src/commands/touch.rs index c6a89bcb..61e61b78 100644 --- a/rust/src/commands/touch.rs +++ b/rust/src/commands/touch.rs @@ -30,8 +30,8 @@ pub async fn touch(ctx: CommandContext) -> CommandResult { if resolved_path.exists() { // Update modification time let now = SystemTime::now(); - if let Err(e) = - filetime::set_file_mtime(&resolved_path, filetime::FileTime::from_system_time(now)) + if filetime::set_file_mtime(&resolved_path, filetime::FileTime::from_system_time(now)) + .is_err() { // Fallback: try to just open and close the file if let Err(e2) = OpenOptions::new().write(true).open(&resolved_path) { diff --git a/rust/src/lib.rs b/rust/src/lib.rs index b03c44a2..849e8269 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -229,6 +229,11 @@ pub struct ProcessRunner { finished: bool, cancelled: bool, output_tx: Option>, + // Held, never read: dropping the receiver would close the channel, and + // streaming virtual commands treat a closed channel as "stop now" (see + // `commands::yes`, which loops until `output_tx.send` fails). Keeping it + // alive is what gives those commands their run-until-cancelled behaviour. + #[allow(dead_code)] output_rx: Option>, } @@ -345,12 +350,10 @@ impl ProcessRunner { return Ok(result.clone()); } - let mut child = self.child.take().ok_or_else(|| { - Error::Io(std::io::Error::new( - std::io::ErrorKind::Other, - "Process not started", - )) - })?; + let mut child = self + .child + .take() + .ok_or_else(|| Error::Io(std::io::Error::other("Process not started")))?; // Handle stdin content if provided if let StdinOption::Content(ref content) = self.options.stdin { diff --git a/rust/src/main.rs b/rust/src/main.rs index d5ff0097..01c93ea7 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -2,7 +2,7 @@ //! //! A simple CLI wrapper for the command-stream library. -use command_stream::{run, ProcessRunner, RunOptions}; +use command_stream::run; use std::env; #[tokio::main] diff --git a/rust/src/pipeline.rs b/rust/src/pipeline.rs index ee9d19a4..ef5a4c20 100644 --- a/rust/src/pipeline.rs +++ b/rust/src/pipeline.rs @@ -77,6 +77,10 @@ impl Pipeline { } /// Add a command to the pipeline + // `std::ops::Add` is not a fit for a consuming builder step, and renaming + // this method would break the published 0.x API and diverge from the + // JavaScript `.add()` it mirrors. + #[allow(clippy::should_implement_trait)] pub fn add(mut self, command: impl Into) -> Self { self.commands.push(command.into()); self diff --git a/rust/tests/builtin_commands.rs b/rust/tests/builtin_commands.rs index 0b923220..1c77cddc 100644 --- a/rust/tests/builtin_commands.rs +++ b/rust/tests/builtin_commands.rs @@ -3,10 +3,9 @@ //! These tests mirror the JavaScript tests in js/tests/builtin-commands.test.mjs use command_stream::commands::{ - basename, cat, cd, cp, dirname, echo, env, exit, ls, mkdir, mv, pwd, rm, seq, sleep, test, - touch, which, yes, CommandContext, + basename, cat, cp, dirname, echo, env, exit, ls, mkdir, mv, pwd, rm, seq, sleep, test, touch, + which, yes, CommandContext, }; -use command_stream::utils::CommandResult; use std::fs; use std::path::PathBuf; use tempfile::TempDir; @@ -167,6 +166,37 @@ async fn test_ls_with_a_flag() { assert!(result.stdout.contains("visible.txt")); } +#[tokio::test] +async fn test_pwd_honors_context_cwd() { + let dir = TempDir::new().unwrap(); + let expected = fs::canonicalize(dir.path()).unwrap(); + + let result = pwd(ctx_with_cwd(vec![], expected.clone())).await; + assert!(result.is_success()); + assert_eq!(result.stdout.trim(), expected.to_string_lossy()); +} + +#[tokio::test] +async fn test_ls_resolves_relative_path_against_context_cwd() { + let dir = TempDir::new().unwrap(); + fs::create_dir(dir.path().join("nested")).unwrap(); + fs::write(dir.path().join("nested/inside.txt"), "content").unwrap(); + + let result = ls(ctx_with_cwd(vec!["nested"], dir.path().to_path_buf())).await; + assert!(result.is_success()); + assert!(result.stdout.contains("inside.txt")); +} + +#[tokio::test] +async fn test_ls_without_path_lists_context_cwd() { + let dir = TempDir::new().unwrap(); + fs::write(dir.path().join("listed.txt"), "content").unwrap(); + + let result = ls(ctx_with_cwd(vec![], dir.path().to_path_buf())).await; + assert!(result.is_success()); + assert!(result.stdout.contains("listed.txt")); +} + #[tokio::test] async fn test_ls_with_l_flag() { let dir = TempDir::new().unwrap(); diff --git a/rust/tests/cd_invocation_isolation.rs b/rust/tests/cd_invocation_isolation.rs index c24da8ac..c44d8594 100644 --- a/rust/tests/cd_invocation_isolation.rs +++ b/rust/tests/cd_invocation_isolation.rs @@ -7,6 +7,11 @@ fn canonical(path: impl AsRef) -> std::path::PathBuf { std::fs::canonicalize(path.as_ref()).unwrap_or_else(|_| path.as_ref().to_path_buf()) } +// Only the `#[cfg(unix)]` assertions read this, because the environment dump +// they parse comes from `/usr/bin/env`, which the Windows runner does not have. +// Without the same cfg on the helper, the Windows build sees an unused function +// and `-D warnings` turns that into a hard error. +#[cfg(unix)] fn output_env<'a>(output: &'a str, name: &str) -> Option<&'a str> { output.lines().find_map(|line| { line.strip_prefix(name) diff --git a/rust/tests/pipeline.rs b/rust/tests/pipeline.rs index fd85400f..7e5cf92d 100644 --- a/rust/tests/pipeline.rs +++ b/rust/tests/pipeline.rs @@ -1,6 +1,6 @@ //! Tests for the Pipeline module -use command_stream::{Pipeline, PipelineExt, ProcessRunner, RunOptions}; +use command_stream::Pipeline; #[tokio::test] async fn test_pipeline_simple() { diff --git a/rust/tests/process_runner.rs b/rust/tests/process_runner.rs index e041f1f0..7349914f 100644 --- a/rust/tests/process_runner.rs +++ b/rust/tests/process_runner.rs @@ -4,7 +4,6 @@ use command_stream::{create, exec, run, ProcessRunner, RunOptions, StdinOption}; use std::collections::HashMap; -use std::path::PathBuf; use tempfile::TempDir; // ============================================================================ diff --git a/rust/tests/stream.rs b/rust/tests/stream.rs index c0a99852..5b639079 100644 --- a/rust/tests/stream.rs +++ b/rust/tests/stream.rs @@ -1,6 +1,6 @@ //! Tests for the streaming module -use command_stream::{AsyncIterator, OutputChunk, StreamingRunner}; +use command_stream::{OutputChunk, StreamingRunner}; #[tokio::test] async fn test_streaming_runner_basic() {