diff --git a/.changeset/release-hardening.md b/.changeset/release-hardening.md new file mode 100644 index 00000000..491f41b2 --- /dev/null +++ b/.changeset/release-hardening.md @@ -0,0 +1,6 @@ +--- +--- + +Release-process hardening: CI guardrails (conventional PR titles, required +changesets), corepack-aware git hooks, documented Node policy, and review +rules. No package changes — intentionally empty (no release). diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml new file mode 100644 index 00000000..c9d59ec9 --- /dev/null +++ b/.github/workflows/changeset.yml @@ -0,0 +1,45 @@ +name: Changeset + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + require-changeset: + name: Require Changeset + runs-on: ubuntu-latest + # The Changesets "version packages" PR (branch changeset-release/main) + # consumes changesets and legitimately contains none — exempt it. + if: github.head_ref != 'changeset-release/main' + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + # Fail the PR unless it adds a changeset. Every PR needs one; for a genuine + # no-release change (CI, docs, tooling) add an intentional empty changeset + # with `pnpm changeset --empty`, which still writes a .changeset/*.md file + # and satisfies this gate. See docs/release-hardening-decisions.md (Decision 4). + # + # We check for an added changeset file directly rather than using + # `changeset status --since`: in the pinned @changesets/cli that command + # exits 0 even when no changeset exists, so it cannot gate. + - name: Require a changeset + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + git fetch --no-tags origin main:refs/remotes/origin/main + base=$(git merge-base origin/main "$HEAD_SHA") + changed=$(git diff --name-only --diff-filter=d "$base".."$HEAD_SHA" -- .changeset \ + | grep -E '\.changeset/.+\.md$' \ + | grep -vE '\.changeset/README\.md$' || true) + if [ -z "$changed" ]; then + echo "::error::No changeset found on this PR. Run 'pnpm changeset' to describe the release, or 'pnpm changeset --empty' for a no-release change (CI/docs/tooling)." + exit 1 + fi + echo "Changeset(s) detected:" + echo "$changed" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cbee587..40642a35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,11 @@ name: CI +# Node policy (see docs/release-hardening-decisions.md, Decision 3): +# CI runs Node 24 LTS across the board. The consumer floor is engines.node +# >=22.13 (pnpm 11 requires it). New dev-deps MUST support that floor, or it +# gets raised as a deliberate decision — never lowered silently to escape a +# dependency constraint. + on: push: branches: diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 00000000..0367a41b --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,52 @@ +name: Commitlint + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + commitlint: + name: Lint Commit Messages + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + # Node 24 LTS, matching the rest of CI (pnpm 11 floor is Node 22.13). + # See docs/release-hardening-decisions.md (Decision 3). + node-version: 24 + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + # Lint only the commits this branch adds, using the merge-base with the + # live origin/main as the lower bound (not the PR's recorded base SHA, + # which goes stale as main moves forward). The merge-base is robust even + # when the branch is behind main: it always resolves to the point where + # this branch diverged, so the range is exactly the new commits. + # + # main MUST be fetched with full history (no --depth). A shallow fetch + # hides the merge-base, so `git log
..HEAD` can no longer exclude + # shared history and the range balloons to include unrelated historical + # commits. See docs/release-hardening-plan.md (AC6). + - name: Resolve lint range + id: range + env: + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + set -euo pipefail + git fetch --no-tags origin main:refs/remotes/origin/main + base=$(git merge-base origin/main "$HEAD_SHA") + echo "base=${base}" >> "$GITHUB_OUTPUT" + echo "Linting ${base}..${HEAD_SHA}" + + - name: Lint commits + run: pnpm exec commitlint --from ${{ steps.range.outputs.base }} --to ${{ github.event.pull_request.head.sha }} --verbose diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 00000000..ecb68ee6 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,22 @@ +name: PR Title + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + lint-pr-title: + name: Lint PR Title + runs-on: ubuntu-latest + steps: + # We squash-merge, so the PR title becomes the landing commit on main and + # must be Conventional Commits. Ticket references (YPE-1234) belong in the + # branch name / PR body, not the title. This is the real commit-format + # gate; the per-commit husky/commitlint hook is only a local dev aid. + # See docs/release-hardening-decisions.md (Decision 1). + - uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fece7288..0a29fefc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,6 +30,8 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v6 with: + # Node 24 LTS, matching CI. Consumer floor is engines.node >=22.13 + # (pnpm 11). See docs/release-hardening-decisions.md (Decision 3). node-version: 24 cache: 'pnpm' diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..2e08b399 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,9 @@ +# Prefer corepack so the repo-pinned pnpm (packageManager in package.json) runs +# regardless of what's on PATH. Fall back to pnpm directly where corepack isn't +# available — Node 25+ no longer bundles it, and nvm/global-pnpm setups may lack +# it. See docs/release-hardening-decisions.md (Decision 2). +if command -v corepack >/dev/null 2>&1; then + corepack pnpm exec commitlint --edit "$1" +else + pnpm exec commitlint --edit "$1" +fi diff --git a/.husky/pre-commit b/.husky/pre-commit index e02c24e2..8c606562 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1,9 @@ -pnpm lint-staged \ No newline at end of file +# Prefer corepack so the repo-pinned pnpm (packageManager in package.json) runs +# regardless of what's on PATH. Fall back to pnpm directly where corepack isn't +# available — Node 25+ no longer bundles it, and nvm/global-pnpm setups may lack +# it. See docs/release-hardening-decisions.md (Decision 2). +if command -v corepack >/dev/null 2>&1; then + corepack pnpm lint-staged +else + pnpm lint-staged +fi diff --git a/AGENTS.md b/AGENTS.md index a1994561..d4c0c09a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,11 +117,16 @@ pnpm --filter @youversion/platform-react-ui build - Changesets required for ALL version bumps (even patches) - **Unified versioning**: All packages must share exact same version - never version packages independently - Pre-commit hooks fail if typecheck or lint fails +- **Every PR must include a changeset** — CI (`.github/workflows/changeset.yml`) fails a PR that adds none. For a genuine no-release change (CI/docs/tooling), add an intentional empty changeset: `pnpm changeset --empty`. A missing changeset is what caused the 2026-07-17 release failure. + +### Commits & PRs +- **PR titles must be Conventional Commits** — the PR title becomes the squash-merge commit on `main` and is linted by `.github/workflows/pr-title.yml`. Ticket refs (e.g. `YPE-1234`) go in the **branch name** and PR body, not the title. +- The per-commit husky/commitlint hook is an optional local dev aid; the PR title is the real gate. ### Environment -- **Node.js requirement**: Minimum version 22.13.0 required (pnpm 11 requires Node >= 22.13); we develop and test on Node 24 LTS, which is what CI runs +- **Node.js requirement**: Minimum version 22.13.0 required (pnpm 11 requires Node >= 22.13); we develop and test on Node 24 LTS, which is what CI runs. New dev-deps must support `engines.node >=22.13`; don't lower the floor to escape a dependency constraint without a deliberate decision (see `docs/release-hardening-decisions.md`, Decision 3). - **React version**: Do not change React dependencies; pnpm overrides (in `pnpm-workspace.yaml`) enforce 19.1.2 -- **Package manager**: Do not use npm/yarn; only pnpm supported +- **Package manager**: Do not use npm/yarn; only pnpm supported. Git hooks prefer `corepack pnpm ...` (repo-pinned pnpm regardless of PATH) and fall back to `pnpm` where corepack isn't available (Node 25+ no longer bundles corepack). Keep the corepack-preferred/pnpm-fallback shape; don't hard-code bare `pnpm` only. - **Supply-chain protection**: `minimumReleaseAge: 4320` (3-day cooldown) in `pnpm-workspace.yaml` — `pnpm install` will reject packages published < 3 days ago. Override with `--force` if needed urgently. Workspace packages (`workspace:*`) are inherently excluded as they aren't fetched from the registry. - **pnpm 11 breaking changes**: Overrides moved from `package.json` → `pnpm-workspace.yaml`; build scripts require `allowBuilds` approval; `@internal/eslint-config` and `eslint-plugin-storybook` must be root devDependencies for resolution diff --git a/PUBLISHING.md b/PUBLISHING.md index 6e46964d..83d1abd0 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -2,6 +2,8 @@ This guide is for project maintainers who need to set up publishing infrastructure or troubleshoot release issues. +> **Hit something not covered here?** [`RELEASE-RUNBOOK.md`](./RELEASE-RUNBOOK.md) catalogues specific failure modes (EPUBLISHCONFLICT-after-success, transient registry 5xx, provenance attestation failure, expired `NPM_TOKEN`, OTP/2FA, `workspace:*` not rewritten, peer-dep skew, dist-tag drift) with concrete state-check and recovery commands. + ## How Publishing Works The repository uses [Changesets](https://github.com/changesets/changesets) with GitHub Actions for automated publishing. @@ -53,6 +55,10 @@ Required packages: - Automatic provenance generation - Audit trail of all publishes +### `NPM_TOKEN` fallback (token type matters) + +The workflow keeps `NPM_TOKEN` as a fallback for any package where Trusted Publishing isn't configured yet. If you set one, generate it as an **Automation token** — not a Publish or personal-user token. Automation tokens explicitly bypass npm's 2FA-on-publish, which CI cannot satisfy. A Publish token will fail every publish with `EOTP` / "need a one-time password" (see [`RELEASE-RUNBOOK.md` §5](./RELEASE-RUNBOOK.md#5-otp--2fa-error-class-wrong-token-type)). Remove `NPM_TOKEN` once all three packages are on Trusted Publishing. + ## Troubleshooting ### "Version Packages" PR Not Created diff --git a/README.md b/README.md index 2721eeeb..991f6055 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,20 @@ console.log(passage.content); > [!NOTE] > We are not yet accepting pull requests from external contributors, though we intend to do so in the future. In the meantime, we welcome you to use the SDK, report bugs via [GitHub Issues](https://github.com/youversion/platform-sdk-react/issues), and share feedback. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. +### Package manager + +This is a pnpm workspace — the `workspace:*` dependencies between packages are a pnpm feature, so npm and yarn are not supported. The required pnpm version is pinned in `package.json` via `packageManager` (and `engines.pnpm`). + +The Git hooks prefer [Corepack](https://nodejs.org/api/corepack.html) (`corepack pnpm ...`) so a newer global pnpm on your PATH can't change how commits are linted or staged files are formatted, and fall back to plain `pnpm` where corepack isn't present. + +To get the pinned-pnpm guarantee, enable Corepack once: + +```bash +corepack enable +``` + +Note: **Node 25+ no longer bundles Corepack** — on newer Node, install it first (`npm install -g corepack`) or just rely on the `pnpm` fallback with a locally-installed pnpm that satisfies `engines.pnpm`. See [docs/release-hardening-decisions.md](./docs/release-hardening-decisions.md) (Decision 2) for the rationale and the plan to revisit once Corepack's successor settles. + ## License This SDK is licensed under [Apache 2.0](./LICENSE). diff --git a/RELEASE-RUNBOOK.md b/RELEASE-RUNBOOK.md new file mode 100644 index 00000000..9d6bdf80 --- /dev/null +++ b/RELEASE-RUNBOOK.md @@ -0,0 +1,275 @@ +# Release Runbook (Web-SDK-specific) + +When a release encounters something the generic Changesets / npm flow doesn't cover, this runbook is the place to look. For the normal happy-path flow and generic publish troubleshooting (auth, "Version Packages" PR not appearing, deprecate / unpublish), see [PUBLISHING.md](./PUBLISHING.md). + +This runbook covers failure modes that survive the [`changesets/action`](https://github.com/changesets/action) abstraction — either because they happen below it (npm registry, OIDC provider) or because the operator needs to understand them to diagnose what they're seeing in workflow logs. + +For the equivalent runbook on the React Native side, see [`platform-sdk-reactnative-expo`'s RELEASE-RUNBOOK.md](https://github.com/youversion/platform-sdk-reactnative-expo/blob/main/RELEASE-RUNBOOK.md). The two repos share the same Changesets flow, so most procedures here apply there too — diverging only on platform-specific concerns (peer-dep skew on RN, React/React-DOM compatibility on Web). + +--- + +## 1. `EPUBLISHCONFLICT` after success + +**Symptom.** `pnpm publish` (invoked by `changeset publish`) exits non-zero with `EPUBLISHCONFLICT` or `You cannot publish over the previously published versions`, but `npm view @youversion/platform-react-ui@$VERSION version` returns `$VERSION`. The registry accepted the publish; the response came back as a conflict because npm's deduplication fired before the response was sent — or because a previous retry attempt already landed. + +**Why it happens.** npm publish is not idempotent: re-publishing the same version-tarball pair returns `EPUBLISHCONFLICT` even when the registry already has exactly what you tried to publish. Changesets-action treats this as a hard fail by default. + +**State check.** + +```bash +for p in @youversion/platform-core @youversion/platform-react-hooks @youversion/platform-react-ui; do + echo "== $p @ $VERSION ==" + npm view "${p}@${VERSION}" version 2>/dev/null || echo " NOT on npm" +done +``` + +If `$VERSION` is on the registry for all packages that should have shipped, the release is **complete** — the workflow's non-zero exit is a false negative. + +**Recovery.** + +- **All packages on registry, but workflow failed.** No re-publish needed. Changesets tags and releases **per package** — `@youversion/platform-core@$VERSION`, `@youversion/platform-react-hooks@$VERSION`, `@youversion/platform-react-ui@$VERSION` — not a single bare `$VERSION` tag. Verify each tag was pushed and each GitHub release exists, and create any that are missing: + ```bash + for p in @youversion/platform-core @youversion/platform-react-hooks @youversion/platform-react-ui; do + tag="${p}@${VERSION}" + git ls-remote --exit-code origin "refs/tags/${tag}" >/dev/null 2>&1 \ + || { git tag "$tag" && git push origin "$tag"; } + gh release view "$tag" >/dev/null 2>&1 \ + || gh release create "$tag" --notes-file CHANGELOG-entry.md # changesets-generated body for that package + done + ``` +- **Some packages on registry, others not.** Re-run the workflow. Changesets-action will skip the published packages (it reads the registry before publishing each) and retry the rest. If a specific package keeps failing, see #3 or #4. + +**Expected end state.** All three packages at `$VERSION` on npm, with a matching `@$VERSION` tag and GitHub release each. + +--- + +## 2. Registry transient failure (timeout / 5xx / network) + +**Symptom.** `pnpm publish` exits non-zero with one of: `ETIMEDOUT`, `ECONNRESET`, `EAI_AGAIN`, `503 Service Unavailable`, `502 Bad Gateway`, `504 Gateway Time-out`, `fetch failed`, `Could not resolve host`. + +**State check.** Same as #1 — `npm view` each package. Transient failures may or may not have recorded the publish before the error surfaced. + +**Recovery.** Re-run the release workflow from the Actions tab. Changesets-action re-reads the registry per package before publishing; anything that landed during the failed run is skipped automatically. If the registry is genuinely down (status page red), wait and re-run later. + +If retries are wanted *automatically*, that's a Changesets-action upstream feature request — the project doesn't expose retry config today. Watch the workflow and re-trigger manually if needed. + +**Expected end state.** All three packages at `$VERSION` on npm. + +--- + +## 3. Provenance attestation failure (publish succeeded, attestation missing) + +**Symptom.** `pnpm publish` exits non-zero with text mentioning `attestation`, `provenance`, `OIDC`, or `Sigstore`. The package itself may or may not have published — provenance generation is a separate step from the publish itself and either can fail independently. + +**Possible underlying causes.** + +- `id-token: write` permission missing from the workflow (it is present in [`release.yml`](.github/workflows/release.yml)). +- The npm-side trust policy for this repo / workflow isn't configured for one of the three packages. +- Sigstore / npm provenance service has a transient outage (rare; treat as transient). + +**State check.** `npm view @` — if the version is there, the publish succeeded. Check the provenance badge on the package page (`https://www.npmjs.com/package//v/`): + +```bash +npm view "@youversion/platform-react-ui@${VERSION}" --json | jq '.dist.attestations' +``` + +If `.dist.attestations` is `null` or absent, the publish landed without provenance. + +**Recovery.** + +- **Version is on registry without provenance.** Release is usable; consumers will install fine but lose the supply-chain attestation for this version. To attest after the fact: not currently supported by npm — you'd have to bump the version and re-publish. + 1. Verify `id-token: write` is in `release.yml`'s `permissions:` block. + 2. Verify the npm trust policy (`https://www.npmjs.com/package//access` → Trusted publishers) lists this workflow. + 3. If both are correct, treat as transient and accept the missing attestation for this version. Next release should attest cleanly. +- **Version not on registry.** Re-run the workflow. Changesets-action publishes attestation alongside; both succeed or fail per-package. + +**Expected end state.** All three packages at `$VERSION` on npm, ideally with provenance attestations. + +--- + +## 4. Expired / invalid `NPM_TOKEN` (fallback auth path) + +**Symptom.** `pnpm publish` exits non-zero with `E401`, `EAUTH`, `EUNAUTHORIZED`, `E403`, or `EFORBIDDEN`. The workflow is using the `NODE_AUTH_TOKEN` (sourced from `NPM_TOKEN` secret) fallback path because Trusted Publishing handed off — usually because OIDC isn't configured for that specific package, or the OIDC token was rejected. + +**State check.** On npm, verify the token at `https://www.npmjs.com/settings//tokens` — is it expired? Was it revoked? Is the package in scope for that token? + +**Recovery.** + +1. Generate a new **Automation** token (not Publish, not Read-only) on npm with publish rights for `@youversion/*`. **The token type matters** — see #5. +2. Update the `NPM_TOKEN` repo secret at `https://github.com/youversion/platform-sdk-react/settings/secrets/actions`. +3. Re-run the workflow. + +> **Better long-term fix:** configure Trusted Publishing on the failing package(s) so this repo doesn't need `NPM_TOKEN` at all. See [PUBLISHING.md](./PUBLISHING.md#setting-up-trusted-publishing). + +**Expected end state.** All three packages at `$VERSION` on npm. + +--- + +## 5. OTP / 2FA error class (wrong token type) + +**Symptom.** `pnpm publish` exits non-zero with `EOTP`, `need a one-time password`, `OTP required`, or similar. The token in use is a **Publish** token or a personal user token, not an **Automation** token — npm is requiring a 2FA OTP on every publish call. + +**Why it happens.** Automation tokens explicitly bypass 2FA-on-publish for CI use. Publish tokens require 2FA on every publish. Personal user tokens require 2FA if the npm account has 2FA enabled (which it should). CI cannot prompt for an OTP, so the publish fails immediately. + +**State check.** Error message is unambiguous — no further checks needed. + +**Recovery.** + +1. On `https://www.npmjs.com/settings//tokens`, generate an **Automation** token with publish rights for `@youversion/*`. The token type is the picker at the top of the new-token form — pick **Automation**, not **Publish**, not **Read and publish**. +2. Replace the `NPM_TOKEN` repo secret. +3. Re-run the workflow. + +> **Token-type cheat sheet:** Automation = bypass 2FA, CI-safe. Publish = require 2FA, manual use only. Read-only = read public packages, no publish. + +**Expected end state.** All three packages at `$VERSION` on npm. + +--- + +## 6. `workspace:*` not rewritten in published tarballs + +**Symptom.** Consumers install `@youversion/platform-react-ui@$VERSION` (or `…-react-hooks@$VERSION`) and see `Cannot resolve workspace:* outside a workspace` or `Unsupported URL Type "workspace:": workspace:*` in install logs. + +**Why it happens.** `pnpm publish` (invoked by `changeset publish`) rewrites `workspace:*` to a real version range at pack time. If a publish ran through a non-pnpm tool, or `publishConfig` was edited in a way pnpm doesn't recognize, the literal `workspace:*` reference can ship. + +**State check.** The dependency graph: `…-react-ui` depends on `…-core` AND `…-react-hooks`; `…-react-hooks` depends on `…-core`. Inspect any published tarball: + +```bash +mkdir -p /tmp/inspect-ui && cd /tmp/inspect-ui +npm pack "@youversion/platform-react-ui@$VERSION" +tar -xzf youversion-platform-react-ui-${VERSION}.tgz +node -p "require('./package/package.json').dependencies" +``` + +The `…-core` and `…-react-hooks` deps should be `^$VERSION` (or `~$VERSION`), not `workspace:*`. + +**Recovery.** If a published tarball is broken: + +1. Deprecate it: `npm deprecate @youversion/platform-react-ui@$VERSION "broken workspace:* — use ${NEXT_VERSION}"`. +2. Land a fix on `main` (usually a `publishConfig` cleanup), add a Changeset, and let the normal release flow ship a patch. + +**Why not unpublish?** npm's 72-hour unpublish window has historically been narrowed; prefer deprecation + a patch release. + +--- + +## 7. Peer-dep version skew with consumer projects + +**Symptom.** Consumers report install errors like `incorrect peer dependency` or runtime errors after upgrading. Their `react` / `react-dom` doesn't satisfy the SDK's `peerDependencies` range. Typical signatures: hooks fail with "Invalid hook call" when React major versions mismatch; SSR output diverges when `react-dom` is on a different minor than what the SDK was built against. + +**State check.** + +```bash +npm view "@youversion/platform-react-ui@${VERSION}" peerDependencies +npm view "@youversion/platform-react-hooks@${VERSION}" peerDependencies +npm view "@youversion/platform-core@${VERSION}" peerDependencies +``` + +Cross-reference with the consumer's `package.json`. + +**Recovery — two options:** + +- **Consumer upgrades.** Document required peer ranges in the Changeset body / GitHub release notes. Consumers update `react` / `react-dom` to a satisfying version. +- **SDK widens the range.** If a swath of consumers is affected and the SDK *would actually work* with the older peer, ship a patch widening `peerDependencies`. Be conservative — widening too far papers over a real incompatibility (e.g. an actual React 19-only API the SDK depends on) and produces opaque runtime errors instead of install errors. + +**Prevention.** When bumping a major peer range (`react >=18` → `react >=19`), describe the consumer impact in the Changeset body. The Changeset becomes part of the release notes consumers read before upgrading. + +--- + +## 8. Dist-tag drift (next / beta channels) + +**Symptom.** A pre-release shipped to `latest` instead of `next` / `beta`, breaking consumers on the stable channel. Or: a stable release landed on `next` and the pre-release channel now points at a stable version. + +**Why it happens.** Changesets-action publishes to `latest` by default. Pre-release mode is controlled by `pnpm changeset pre enter ` / `pnpm changeset pre exit`, which writes `.changeset/pre.json`. If the pre-mode state file is missing or stale when a release runs, the dist-tag is wrong. + +**State check.** + +```bash +npm view "@youversion/platform-react-ui" dist-tags +cat .changeset/pre.json 2>/dev/null || echo "not in pre-release mode" +``` + +**Recovery.** + +```bash +# Move latest off the bad version +npm dist-tag add @youversion/platform-react-ui@ latest + +# Point next at the right pre-release +npm dist-tag add @youversion/platform-react-ui@ next +``` + +Repeat for `…-react-hooks` and `…-core`. + +**Prevention.** Before opening a release that should hit `next`, confirm `.changeset/pre.json` exists and references the right tag. After the pre-release window, run `pnpm changeset pre exit` and commit the deletion in the same PR that ships the first stable. + +--- + +## 9. Rogue tag (tag on origin without a matching release / publish) + +**Symptom.** A Changesets tag (`@$VERSION`, e.g. `@youversion/platform-core@$VERSION`) exists on origin, but one or more of: the GitHub release is missing, the npm packages were never published, or the tag points at the wrong commit. Usually the tail of a run that died after the tag push but before (or during) publish — the inverse of #1, where the registry landed but the tags didn't. + +**Why it happens.** Changesets-action pushes the version-bump commit and the per-package tags as part of merging the "Version Packages" PR, then publishes. If the job is killed between those steps, or a tag was pushed by hand during a manual recovery, a tag can outlive the work it was supposed to mark. Note the tags are **per package** — not a single bare `$VERSION` — so a partial failure can leave some package tags present and others missing. + +**State check.** + +```bash +# What does origin have, where does each tag point, and is each version on npm? +for p in @youversion/platform-core @youversion/platform-react-hooks @youversion/platform-react-ui; do + tag="${p}@${VERSION}" + echo "== ${tag} ==" + git ls-remote origin "refs/tags/${tag}" || echo " no tag on origin" + git rev-parse "${tag}^{commit}" 2>/dev/null || echo " not fetched locally" + gh release view "${tag}" >/dev/null 2>&1 && echo " release exists" || echo " no release" + npm view "${tag}" version 2>/dev/null || echo " NOT on npm" +done +``` + +**Recovery.** + +- **Tag is correct, but release / publish are missing.** Leave the tag. Re-run the release workflow — `changesets/action` will publish the packages that aren't on the registry yet. If a GitHub release is still missing afterward, create it from the tag: `gh release create "@$VERSION" --notes-file `. +- **A tag points at the wrong commit, or was pushed for a version that should never have shipped.** Delete that package's tag on both sides, then let the normal flow re-create it: + ```bash + tag="@youversion/platform-core@${VERSION}" # repeat per affected package + git push origin ":refs/tags/${tag}" # delete remote tag + git tag -d "${tag}" # delete local tag + ``` + Only delete a tag if that package's version is **not** on npm. If npm already has `@$VERSION`, deleting the tag desyncs git from the registry — instead, re-point the tag at the published commit and keep it. + +**Expected end state.** For every package: either the tag is deleted (because that version never shipped) or it points at the published commit and has a matching GitHub release + npm version. + +--- + +## 10. Wrong version input (bad bump in the "Version Packages" PR) + +**Symptom.** The "Version Packages" PR proposes a version that's wrong — a major bump for a patch-only change, a skipped number, or a downgrade. There is no manual `VERSION` field in this repo (unlike the Swift orchestrator); the version is computed by Changesets from the `.changeset/*.md` files' declared bump levels. + +**Why it happens.** A changeset declared the wrong bump level (`major` where `patch` was meant, or vice versa), or multiple changesets stacked to a larger bump than intended. + +**State check.** Read the proposed bump before merging: + +```bash +gh pr view --json title,files | jq . # the "chore: version packages" PR +cat .changeset/*.md # the bump levels feeding the computed version +``` + +The version in the PR's `package.json` diffs is the version that will publish the moment you merge. + +**Recovery (before merge — the safe path).** + +1. **Do not merge the "Version Packages" PR.** Close it, or leave it open. +2. Fix the bump at the source on `main`: edit or delete the offending `.changeset/*.md` entry (the front-matter line like `'@youversion/platform-react-ui': minor`). +3. Push to `main`. Changesets-action regenerates the "Version Packages" PR with the corrected version. + +**Recovery (after merge — already published).** npm publish is not reversible (see #1 / #6). If a wrong version already shipped: + +- **Too-high version (e.g. an accidental major).** You can't reclaim the number. Accept it, deprecate if it's confusing (`npm deprecate …`), and continue from there — SemVer only requires monotonic increase, not contiguity. +- **Broken release.** Deprecate it and ship a corrected patch via the normal flow (see #6). + +**Prevention.** Treat the "Version Packages" PR as the version approval gate — review the computed version in its diff before merging. This is also the **breaking-change approval point**: a `major` bump only ships when a human merges that PR, so a reviewer can block a `major` there. See the breaking-change note in [`docs/release-hardening-plan.md`](./docs/release-hardening-plan.md#breaking-change-approval). + +**Expected end state.** The intended version (and no other) is on npm with a matching tag and GitHub release. + +--- + +## When something isn't here + +These docs are deliberately limited to failure modes that aren't already handled by Changesets-action's normal behavior. Generic publish issues (missing changeset, "Version Packages" PR didn't appear, npm scope misconfigured) are covered in [PUBLISHING.md](./PUBLISHING.md). If you hit something not in either place, capture the symptom + recovery in a PR adding a new section here so the next operator doesn't start cold. diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..0ac5ba1b --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,27 @@ +// Commit message linting for the React SDK. +// +// We squash-merge, so the landing commit on main is the PR title. The real +// gate is the PR-title lint job (.github/workflows/pr-title.yml); this +// per-commit config stays as an optional local dev aid via the husky +// commit-msg hook. See docs/release-hardening-decisions.md (Decision 1). +// +// Titles are plain Conventional Commits — no ticket prefix. Ticket references +// (YPE-1234) live in the branch name and PR body, not the title. +// +// The Changesets release commit subject is "chore: version packages" (see +// .github/workflows/release.yml); we ignore it so the bot's own commit never +// fails the lint range on a workflow re-run. +// +// This repo is ESM ("type": "module"), so the config uses `export default`. +export default { + extends: ['@commitlint/config-conventional'], + ignores: [ + (message) => /^chore: version packages(?:\r?\n|$)/i.test(message), + // Merge commits are auto-generated and not authored conventional commits. + (message) => message.startsWith('Merge '), + ], + rules: { + 'body-max-line-length': [0, 'always'], + 'footer-max-line-length': [0, 'always'], + }, +}; diff --git a/docs/release-hardening-decisions.md b/docs/release-hardening-decisions.md new file mode 100644 index 00000000..a6cf2ffd --- /dev/null +++ b/docs/release-hardening-decisions.md @@ -0,0 +1,88 @@ +# Release Hardening — Open Decisions (YPE-2486) + +**Status:** 🟢 All decisions settled; guardrails implemented on [PR #268](https://github.com/youversion/platform-sdk-react/pull/268) — remaining work is merging current `main` and aligning to its pnpm 11 / Node 24 toolchain. Decisions 1 & 2 ruled by jhampton (2026-07-01), concurred by davidfedor (2026-07-14). Decision 3 was resolved by main's pnpm 11 upgrade — floor recorded at `>=22.13` (2026-07-20), superseding David's earlier keep-at-`>=20` ruling. Decision 4 (changeset-per-PR gate) added 2026-07-20 after a live release failure. + +Review feedback (jhampton, `CHANGES_REQUESTED`, 2026-06-29) raised three items that are **decisions, not bugs**. Each gets pinned two ways once decided: + +- **Deterministic** — code/config (`commitlint.config.js`, workflows, `package.json`, hooks). +- **Semi-deterministic** — [`greptile.json`](https://www.greptile.com/docs/code-review/custom-standards) (`customContext.rules`), `AGENTS.md`. + +--- + +## Decision 1 — Commit-lint subject model 🟢 Decided (jhampton, 2026-07-01; davidfedor concurred 2026-07-14) + +**Ruling.** Enforce conventional commits. Ticket refs live in **branch names** (Jira parses those) and the PR body — not the commit title. We squash-merge, so the **PR title is the landing commit** and must be conventional. + +**Rationale — checked per jhampton's "check my math".** In the Swift SDK, conventional commits drive changelog + version math. **This repo does not work that way:** +- Changelog = `@changesets/cli/changelog`, generated from `.changeset/*.md` summaries. +- Version bump = the level declared in each changeset (`patch`/`minor`/`major`) + the fixed group. +- No semantic-release / conventional-changelog tooling. + +So commit titles **don't** feed the changelog or version here — Changesets does. Enforcing conventional commits is still correct, but for **consistency, cross-SDK parity, and clean history** — not changelog/version correctness. Document it this way so no one later assumes commits drive the changelog. + +**Implementation.** +- Deterministic: plain `@commitlint/config-conventional`; **remove** the `YPE-####` prefix parser from [`commitlint.config.js`](../commitlint.config.js); lint the **PR title** (becomes the squash commit, e.g. `amannn/action-semantic-pull-request`); keep the per-commit husky hook as an optional dev aid. +- Semi-deterministic: `greptile.json` + `AGENTS.md` — PR titles must be conventional; tickets belong in the branch name / PR body. + +--- + +## Decision 2 — Package-manager lane 🟢 Decided (jhampton, 2026-07-01; davidfedor concurred 2026-07-14, deferring to the daily release dev) + +**Ruling.** Option 1 — pnpm + Corepack. The lane is already pnpm (`workspace:*` is pnpm-only); enforce the pinned version and document why. + +**Deferred knock-on.** Corepack is deprecated but supported **through Node 24**. The "what replaces Corepack" call is deferred until the ecosystem settles (Corepack team, Node/Bun/Deno); revisit before moving off Node 24 — file a follow-up ticket. + +**Implementation.** +- Deterministic: `corepack pnpm exec` in [`.husky/`](../.husky) hooks; optional CI "verify package manager" step; keep `packageManager`/`engines` pins. +- Semi-deterministic: `README.md` "Package Manager" section (the rationale); `greptile.json` flag for bare `pnpm exec` in hooks. + +--- + +## Decision 3 — Node-version policy 🟢 Resolved by main (pnpm 11 upgrade; floor recorded at 22.13, 2026-07-20) + +**Problem (original).** Versions were inconsistent: CI/commitlint on Node 20, release on Node 24, `engines.node >=20`, with no stated policy. + +**What changed.** While this branch was open, `main` upgraded to **pnpm 11**, which requires **Node >=22.13**. As part of that upgrade main raised the published `engines.node` from `>=20` to `>=22.13`, moved the React overrides into `pnpm-workspace.yaml`, added a supply-chain cooldown (`minimumReleaseAge`), and standardized **CI on Node 24** across the board. The Node-20-vs-24 split this decision was originally about no longer exists. + +**Outcome (recorded 2026-07-20).** Accept main's direction: **consumer floor is `engines.node >=22.13`**, toolchain is pnpm 11, CI is Node 24. This supersedes davidfedor's 2026-07-14 audience-split ruling ("keep the consumer floor at `>=20`") — the pnpm 11 upgrade is exactly the "good reason to require something new" he left the door open for, and reverting to `>=20` would mean giving up pnpm 11. We are not moving backwards for the sake of a pre-upgrade decision. + +**Guardrail (retained, re-based to the new floor).** New dev-deps must support `engines.node >=22.13`, or the floor gets bumped **deliberately**, not silently. (The old `@commitlint/*@21`-needs-Node-≥22.12 break that motivated this is now moot on Node 24.) + +**Implementation.** +- Deterministic: align the release-hardening branch to main — `engines.node >=22.13`, pnpm 11, Node 24 in CI; correct the earlier Node-20 workflow comments. +- Semi-deterministic: `AGENTS.md` + `greptile.json` — "new dev-deps must support `engines.node` (>=22.13); don't lower the floor to escape a dep constraint without a decision." + +--- + +## Decision 4 — Enforce a changeset per PR 🟠 New (surfaced by the 2026-07-17 release failure) + +**Incident.** [#282](https://github.com/youversion/platform-sdk-react/pull/282) (`chore: localization protection`, merged `833aa47`) landed with **no changeset**. With nothing to version, the [Release run](https://github.com/youversion/platform-sdk-react/actions/runs/29616967518) took the publish path and tried to re-publish the already-live 2.3.0 → `E403 — cannot publish over previously published versions: 2.3.0`. No release was lost and nothing on npm was corrupted, but the pipeline stays red until a fresh version ships. Fixed forward by [#292](https://github.com/youversion/platform-sdk-react/pull/292) (adds the missing changeset → 2.3.1). + +**Root cause.** The changesets action branches on whether unconsumed `.changeset/*.md` files exist: present → open a "version packages" PR; absent → run publish. A PR merged without a changeset silently routes main into a no-op publish that collides on the current version. Note the trap: #282 was a `chore:` that still touched publishable `packages/ui` source, so it genuinely needed a release — "chore" is not a safe signal for "no changeset needed." This reinforces Decision 1's finding that **changesets are load-bearing** here (they drive changelog + version), so a missing one is a release break, not a style nit. + +**Options.** +1. **CI hard gate** — a PR job running `pnpm changeset status --since=origin/main` that fails when no changeset is present; no-release PRs opt out deliberately with an empty changeset (`pnpm changeset --empty`). +2. **Changesets bot** — the GitHub app posts a comment + status check when a PR has no changeset (better UX, softer gate). +3. **Both** — bot for contributor UX, CI check as the enforced gate. + +**Recommend:** Option 1 (deterministic hard gate); add the bot later for UX if wanted. **Decides:** team. +**Pin:** a "changeset required" job in the PR workflow; `greptile.json` + `AGENTS.md` note "every PR needs a changeset — use `pnpm changeset --empty` for genuine no-release changes (CI/docs/tooling)." + +--- + +## Already fixed (no decision) + +| Item | Fix | +| --- | --- | +| commitlint `@21` needs Node ≥22.12 | Pinned to `19.8.1` — `98a3a86` | +| Runbook used bare `$VERSION` tag; repo tags per-package | §1/§9 loop over `@$VERSION` — `5f56a0d` | +| Release-ignore regex too broad | Anchored to `(?:\r?\n\|$)` — `5f56a0d` | +| Commit-lint range too wide (shallow fetch) | `git merge-base origin/main HEAD` + full fetch — `6ab6118` | + +## Next + +1. Implement deterministic guardrails for all three decisions (all decided — ready now). +2. Add the "changeset required" CI gate (Decision 4). +3. Add `greptile.json` + `AGENTS.md` rules. +4. File the Corepack-successor follow-up ticket (Decision 2). +5. Update [`release-hardening-plan.md`](./release-hardening-plan.md); PR #268 is unblocked — push the guardrail commits and take it out of draft. diff --git a/docs/release-hardening-plan.md b/docs/release-hardening-plan.md new file mode 100644 index 00000000..1bea44e9 --- /dev/null +++ b/docs/release-hardening-plan.md @@ -0,0 +1,84 @@ +# Release Hardening Plan (YPE-2486) + +## Why this doc exists + +YPE-2486 was opened to port [`platform-sdk-swift`'s YPE-2684 release hardening](https://github.com/youversion/platform-sdk-swift) to the React SDK. The Swift hardening was built around a custom bash orchestrator (`scripts/release.sh`) with explicit version inputs, tag-state resume mode, and bounded retry on `pod trunk push`. + +The React SDK does not use that shape — it uses [Changesets](https://github.com/changesets/changesets) with `changesets/action@v1`, triggered on push to `main`. Most of YPE-2486's "Constant across SDKs" scope assumes the bash-orchestrator model and is structurally inapplicable here; the one registry-agnostic item that *does* apply — commit-lint (AC6) — was ported from the Kotlin SDK. This document records what the team decided to do instead. + +The sibling RN-Expo SDK ([`platform-sdk-reactnative-expo`](https://github.com/youversion/platform-sdk-reactnative-expo)) made the same call — see its [`docs/release-hardening-plan.md`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/main/docs/release-hardening-plan.md) for the parallel pivot. + +## Decision: keep Changesets, harden around the edges + +Three options were on the table: + +1. **Adopt the Swift model.** Replace Changesets with a `workflow_dispatch`-triggered bash orchestrator that takes a `version` input and implements resume mode, bounded retry, and per-package idempotency. Faithful to the Swift hardening, but rips out a working release model and introduces ~1000 LoC of bash to maintain. +2. **Keep Changesets, layer Swift-style hardening on top.** Add `npm view` pre-publish checks, `EPUBLISHCONFLICT` handling, and retry logic inside the workflow alongside `changesets/action`. Higher complexity than option 3 with marginal upside — Changesets already handles most of these cases. +3. **Keep Changesets, document the failure surface, and accept what's already idempotent.** Land the failure-mode runbook and engineering plan called for by YPE-2486 ACs 4 / 5 / 7, port the registry-agnostic items that *do* apply (commit-lint, AC6), document the breaking-change gate, drop the AC items that conflict with Changesets, and update the ticket scope to match. + +**Option 3 was chosen.** Changesets-action already: + +- Re-reads the registry per package before publishing, so a retry skips packages that landed. +- Treats publishing as atomic per package (one tarball uploads or none does — no partial-package state). +- Re-runs cleanly on workflow retry: if the "Version Packages" PR didn't merge yet, the next run regenerates it; if the publish failed mid-way, the next run picks up the missing packages. + +The hardening Swift needed (tag-resume, bounded retry, per-package idempotency) is mostly built in for free here. What's actually missing is **documentation** — operators need to know how to map an npm error message to a recovery, what failure classes are transient vs hard, and what an Automation token vs Publish token means for CI. + +## What landed under YPE-2486 + +| AC | Deliverable | Status | +| --- | --- | --- | +| 4 | `RELEASE-RUNBOOK.md` with 10 failure modes: EPUBLISHCONFLICT-after-success, transient 5xx, provenance attestation failure, expired NPM_TOKEN, OTP/2FA, workspace:* rewrite, peer-dep skew, dist-tag drift, rogue tag, wrong version input | ✅ Landed (all AC4-named modes covered: registry transient, EPUBLISHCONFLICT-after-success, provenance, NPM_TOKEN, OTP/2FA, rogue tag, wrong VERSION input) | +| 5 | Operator guide: `PUBLISHING.md` retained, cross-links runbook | ✅ Landed (cross-link added) | +| 6 | Commit-lint workflow anchored at `origin/main` tip, ignores the release commit | ✅ Landed (ported from the Kotlin SDK) | +| 7 | This engineering plan | ✅ This document | +| BREAKING CHANGE requires manual approval | The "Version Packages" PR merge is the approval gate | ✅ Documented below | + +## What was dropped from scope (and why) + +| Original AC item | Why dropped | +| --- | --- | +| 1. Manual `workflow_dispatch` with explicit version input | Conflicts with Changesets' auto-on-merge model. Versions come from `.changeset/*.md` files, not workflow inputs. Operator override is done by hand-editing the "Version Packages" PR before merging. | +| 2. Resume-on-re-dispatch in a custom script | `changesets/action` is idempotent by design — re-running it from the Actions tab is the recovery path. No custom resume logic needed. | +| 3. Custom `npm view` idempotency check + EPUBLISHCONFLICT handling | Changesets-action does per-package registry checks before publishing. EPUBLISHCONFLICT-after-success is documented in the runbook so operators recognize the symptom; no in-band classifier needed. | +| Bounded retry on registry-publish | Not supported by `changesets/action` upstream. Workflow re-trigger from Actions UI fills the gap. | +| Dist-tag preservation in resume mode | Changesets handles dist-tag via `.changeset/pre.json`; #8 in the runbook covers the manual recovery if it drifts. | + +These items aren't impossible to add; they just don't pull their weight against the Changesets baseline. If the release model ever pivots (e.g. to manual-dispatch for slow-rolling enterprise channels), they should be re-evaluated. + +## Commit linting (AC6) + +Ported from the Kotlin SDK's working setup ([`commitlint.yml`](https://github.com/youversion/platform-sdk-kotlin/blob/main/.github/workflows/commitlint.yml) + [`commitlint.config.js`](https://github.com/youversion/platform-sdk-kotlin/blob/main/commitlint.config.js)), which is the reference this AC was modeled on: + +- **CI** — [`.github/workflows/commitlint.yml`](../.github/workflows/commitlint.yml) runs on PRs and anchors the lint range at the **merge-base with the live `origin/main`** (`git fetch origin main` full history → `base=$(git merge-base origin/main )` → `commitlint --from --to `), not the PR's recorded base SHA. The recorded base goes stale as `main` advances and would re-lint already-merged commits; the merge-base lints only commits new to the branch, and stays correct even when the branch is behind main. Note: `main` must be fetched with full history — a shallow (`--depth=1`) fetch hides the merge-base and balloons the range to include unrelated historical commits. +- **Local** — a husky `commit-msg` hook ([`.husky/commit-msg`](../.husky/commit-msg)) runs `commitlint --edit` so authors catch a bad message at commit time, before pushing. +- **Config** — [`commitlint.config.js`](../commitlint.config.js) extends `@commitlint/config-conventional`. + +Two React-specific deltas from the Kotlin original: + +- **Release-commit ignore.** Kotlin (semantic-release) ignores `chore(release):`. This repo (Changesets) emits `chore: version packages` — the config ignores that subject instead, so the release bot's own commit never fails the lint range on a workflow re-run. +- **Ticket prefix allowed.** ~Half this repo's history leads with a ticket id (`YPE-1234: feat(ui): …` / `YPE-1234 - feat(ui): …`). A custom `headerPattern` permits an optional leading ticket reference before the conventional `type(scope): subject`, so the team's existing convention keeps passing. Note: `config-conventional`'s `subject-case` rule still applies, so capitalized subjects (`feat(ui): Move …`) are flagged — a minor habit change, consistent with the Kotlin reference. + +## BREAKING CHANGE approval + +The ticket requires that a breaking change can't ship without manual approval. In the Changesets model this gate already exists and is structural — there is no separate workflow to add: + +- A breaking change is declared as a `major` bump in its `.changeset/*.md` entry. +- That bump flows into the **"Version Packages" PR**, where the new `major` version is visible in the `package.json` diffs. +- Nothing publishes until a human **merges that PR**. The merge is the manual approval — a reviewer can block a `major` bump there, or send it back to correct the bump level before anything reaches npm. + +So the approval point for breaking changes is the same review-and-merge step every release passes through; no auto-publish path bypasses it (the workflow only publishes *after* the version PR merges to `main`). Recovery for a wrong bump that slips through is in [`RELEASE-RUNBOOK.md` §10](../RELEASE-RUNBOOK.md#10-wrong-version-input-bad-bump-in-the-version-packages-pr). + +## What's *not* in this PR but worth knowing + +- **Trusted Publishing** is configured for all three packages (`@youversion/platform-core`, `…-react-hooks`, `…-react-ui`) — see [`PUBLISHING.md`](../PUBLISHING.md). `NPM_TOKEN` is kept as a fallback only. +- **Provenance** is on by default (`publishConfig.provenance: true` in each package + `NPM_CONFIG_PROVENANCE: true` in [`release.yml`](../.github/workflows/release.yml)). +- **Three-package fixed group** in [`.changeset/config.json`](../.changeset/config.json) ensures every release ships all three packages at the same version. +- **The RN SDK matches this same shape** so contributors and maintainers don't have to relearn the release flow per repo. + +## Related work / cross-references + +- [`platform-sdk-reactnative-expo`'s YPE-2790](https://github.com/youversion/platform-sdk-reactnative-expo) — parallel pivot on the RN repo, same Changesets model, slim runbook with RN-specific failure modes. +- [`platform-sdk-swift`'s YPE-2684](https://github.com/youversion/platform-sdk-swift) — original Swift hardening; the React SDK does not adopt its orchestrator shape but borrows its failure-class taxonomy. +- [`changesets/action`](https://github.com/changesets/action) — upstream action behavior. +- [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) — the OIDC auth path used here. diff --git a/greptile.json b/greptile.json index 00522367..2f1f1349 100644 --- a/greptile.json +++ b/greptile.json @@ -38,6 +38,22 @@ { "scope": ["packages/ui/src/i18n/locales/**"], "rule": "Locale JSON is upstream-owned (platform-localization). CI locale-ownership job fails feature PRs that edit packages/ui/src/i18n/locales/. Add English in sources/common/en.json (react.*); fr/es arrive via Crowdin sync PRs (chore/localization-sync-react-*)." + }, + { + "scope": ["*"], + "rule": "Conventional PR titles: the PR title must follow Conventional Commits because it becomes the squash-merge commit on main. Ticket references like YPE-1234 belong in the branch name and PR body, not the title. See docs/release-hardening-decisions.md (Decision 1)." + }, + { + "scope": [".husky/**"], + "rule": "Git hooks should invoke the package manager via `corepack pnpm ...` when corepack is available (so the repo-pinned pnpm runs regardless of PATH), falling back to `pnpm` where corepack isn't present (Node 25+ no longer bundles it). See docs/release-hardening-decisions.md (Decision 2)." + }, + { + "scope": ["package.json", "packages/**/package.json"], + "rule": "New devDependencies must support the consumer engines.node floor (>=22.13). Do not lower the floor to escape a dependency constraint without a deliberate decision. See docs/release-hardening-decisions.md (Decision 3)." + }, + { + "scope": ["*"], + "rule": "Every PR must include a changeset. For a genuine no-release change (CI, docs, tooling) add an intentional empty changeset via `pnpm changeset --empty`. See docs/release-hardening-decisions.md (Decision 4)." } ], "files": [ diff --git a/package.json b/package.json index 04eb97a5..0122cbe0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ }, "devDependencies": { "@changesets/cli": "2.29.7", + "@commitlint/cli": "19.8.1", + "@commitlint/config-conventional": "19.8.1", "@internal/eslint-config": "workspace:*", "@eslint/eslintrc": "3.3.1", "@microsoft/api-extractor": "7.53.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad206bd5..1952421e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,12 @@ importers: '@changesets/cli': specifier: 2.29.7 version: 2.29.7(@types/node@24.9.1) + '@commitlint/cli': + specifier: 19.8.1 + version: 19.8.1(@types/node@24.9.1)(typescript@5.9.3) + '@commitlint/config-conventional': + specifier: 19.8.1 + version: 19.8.1 '@eslint/eslintrc': specifier: 3.3.1 version: 3.3.1 @@ -727,6 +733,75 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@commitlint/cli@19.8.1': + resolution: {integrity: sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.8.1': + resolution: {integrity: sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.8.1': + resolution: {integrity: sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.8.1': + resolution: {integrity: sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.8.1': + resolution: {integrity: sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==} + engines: {node: '>=v18'} + + '@commitlint/format@19.8.1': + resolution: {integrity: sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.8.1': + resolution: {integrity: sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.8.1': + resolution: {integrity: sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==} + engines: {node: '>=v18'} + + '@commitlint/load@19.8.1': + resolution: {integrity: sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==} + engines: {node: '>=v18'} + + '@commitlint/message@19.8.1': + resolution: {integrity: sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.8.1': + resolution: {integrity: sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.8.1': + resolution: {integrity: sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.8.1': + resolution: {integrity: sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.8.1': + resolution: {integrity: sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.8.1': + resolution: {integrity: sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.8.1': + resolution: {integrity: sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==} + engines: {node: '>=v18'} + + '@commitlint/types@19.8.1': + resolution: {integrity: sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==} + engines: {node: '>=v18'} + '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -2903,6 +2978,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/conventional-commits-parser@5.0.2': + resolution: {integrity: sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -3100,6 +3178,10 @@ packages: xstate: optional: true + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -3207,6 +3289,9 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + array-includes@3.1.9: resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} engines: {node: '>= 0.4'} @@ -3450,6 +3535,9 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3473,6 +3561,19 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -3499,6 +3600,14 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} @@ -3545,6 +3654,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -3692,6 +3805,10 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dotenv-cli@7.4.2: resolution: {integrity: sha512-SbUj8l61zIbzyhIbg0FwPJq6+wjbzdn9oEtozQpZ6kW2ihCcapKVZj49oCT3oPM+mgQm+itgvUQcG5szxVrZTA==} hasBin: true @@ -4052,6 +4169,10 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -4176,6 +4297,12 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. + hasBin: true + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -4203,6 +4330,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4354,6 +4485,9 @@ packages: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -4369,6 +4503,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -4477,6 +4615,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + is-obj@3.0.0: resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} engines: {node: '>=12'} @@ -4527,6 +4669,10 @@ packages: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -4684,6 +4830,10 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -4900,15 +5050,40 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4996,6 +5171,10 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -5254,6 +5433,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -5262,6 +5445,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -5308,6 +5495,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -5845,6 +6036,10 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -6011,6 +6206,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -6018,6 +6217,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -6234,6 +6436,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -6578,6 +6784,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yoctocolors-cjs@2.1.3: resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} engines: {node: '>=18'} @@ -7150,6 +7360,116 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 + '@commitlint/cli@19.8.1(@types/node@24.9.1)(typescript@5.9.3)': + dependencies: + '@commitlint/format': 19.8.1 + '@commitlint/lint': 19.8.1 + '@commitlint/load': 19.8.1(@types/node@24.9.1)(typescript@5.9.3) + '@commitlint/read': 19.8.1 + '@commitlint/types': 19.8.1 + tinyexec: 1.0.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + ajv: 8.18.0 + + '@commitlint/ensure@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.8.1': {} + + '@commitlint/format@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + + '@commitlint/is-ignored@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + semver: 7.7.3 + + '@commitlint/lint@19.8.1': + dependencies: + '@commitlint/is-ignored': 19.8.1 + '@commitlint/parse': 19.8.1 + '@commitlint/rules': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/load@19.8.1(@types/node@24.9.1)(typescript@5.9.3)': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/execute-rule': 19.8.1 + '@commitlint/resolve-extends': 19.8.1 + '@commitlint/types': 19.8.1 + chalk: 5.6.2 + cosmiconfig: 9.0.1(typescript@5.9.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@24.9.1)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.8.1': {} + + '@commitlint/parse@19.8.1': + dependencies: + '@commitlint/types': 19.8.1 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.8.1': + dependencies: + '@commitlint/top-level': 19.8.1 + '@commitlint/types': 19.8.1 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 1.0.2 + + '@commitlint/resolve-extends@19.8.1': + dependencies: + '@commitlint/config-validator': 19.8.1 + '@commitlint/types': 19.8.1 + global-directory: 4.0.1 + import-meta-resolve: 4.2.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.8.1': + dependencies: + '@commitlint/ensure': 19.8.1 + '@commitlint/message': 19.8.1 + '@commitlint/to-lines': 19.8.1 + '@commitlint/types': 19.8.1 + + '@commitlint/to-lines@19.8.1': {} + + '@commitlint/top-level@19.8.1': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.8.1': + dependencies: + '@types/conventional-commits-parser': 5.0.2 + chalk: 5.6.2 + '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -9203,6 +9523,10 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/conventional-commits-parser@5.0.2': + dependencies: + '@types/node': 24.11.0 + '@types/deep-eql@4.0.2': {} '@types/doctrine@0.0.9': {} @@ -9604,6 +9928,11 @@ snapshots: transitivePeerDependencies: - '@types/react' + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -9704,6 +10033,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-ify@1.0.0: {} + array-includes@3.1.9: dependencies: call-bind: 1.0.8 @@ -9960,6 +10291,11 @@ snapshots: commander@4.1.1: {} + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + concat-map@0.0.1: {} concurrently@9.2.1: @@ -9979,6 +10315,21 @@ snapshots: content-type@1.0.5: {} + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + convert-source-map@1.9.0: {} convert-source-map@2.0.0: {} @@ -9996,6 +10347,13 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig-typescript-loader@6.3.0(@types/node@24.9.1)(cosmiconfig@9.0.1(typescript@5.9.3))(typescript@5.9.3): + dependencies: + '@types/node': 24.9.1 + cosmiconfig: 9.0.1(typescript@5.9.3) + jiti: 2.6.1 + typescript: 5.9.3 + cosmiconfig@9.0.1(typescript@5.9.3): dependencies: env-paths: 2.2.1 @@ -10047,6 +10405,8 @@ snapshots: csstype@3.1.3: {} + dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} data-urls@5.0.0: @@ -10166,6 +10526,10 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + dotenv-cli@7.4.2: dependencies: cross-spawn: 7.0.6 @@ -10753,6 +11117,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.19 @@ -10881,6 +11251,12 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -10922,6 +11298,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@14.0.0: {} globals@16.5.0: {} @@ -11059,6 +11439,8 @@ snapshots: import-lazy@4.0.0: {} + import-meta-resolve@4.2.0: {} + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -11070,6 +11452,8 @@ snapshots: inherits@2.0.4: {} + ini@4.1.1: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -11168,6 +11552,8 @@ snapshots: is-number@7.0.0: {} + is-obj@2.0.0: {} + is-obj@3.0.0: {} is-plain-obj@4.1.0: {} @@ -11210,6 +11596,10 @@ snapshots: has-symbols: 1.1.0 safe-regex-test: 1.1.0 + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.19 @@ -11403,6 +11793,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonparse@1.3.1: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -11585,12 +11977,30 @@ snapshots: dependencies: p-locate: 5.0.0 + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.camelcase@4.3.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + lodash.sortby@4.7.0: {} lodash.startcase@4.4.0: {} + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + lodash@4.17.21: {} log-symbols@6.0.0: @@ -11660,6 +12070,8 @@ snapshots: media-typer@1.1.0: {} + meow@12.1.1: {} + merge-descriptors@2.0.0: {} merge-source-map@1.1.0: @@ -11965,6 +12377,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@4.0.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -11973,6 +12389,10 @@ snapshots: dependencies: p-limit: 3.1.0 + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + p-map@2.1.0: {} p-try@2.2.0: {} @@ -12012,6 +12432,8 @@ snapshots: path-exists@4.0.0: {} + path-exists@5.0.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -12651,6 +13073,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + split2@4.2.0: {} + sprintf-js@1.0.3: {} stackback@0.0.2: {} @@ -12841,6 +13265,8 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + text-extensions@2.4.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -12849,6 +13275,8 @@ snapshots: dependencies: any-promise: 1.3.0 + through@2.3.8: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -13109,6 +13537,8 @@ snapshots: undici-types@7.16.0: {} + unicorn-magic@0.1.0: {} + unicorn-magic@0.3.0: {} universalify@0.1.2: {} @@ -13458,6 +13888,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yoctocolors-cjs@2.1.3: {} yoctocolors@2.1.2: {}