Skip to content

chore(deps): take Dependabot to zero — every alert resolved, none introduced - #60

Merged
abbaseya merged 2 commits into
mainfrom
chore/security-dependabot-critical-high
Aug 10, 2026
Merged

chore(deps): take Dependabot to zero — every alert resolved, none introduced#60
abbaseya merged 2 commits into
mainfrom
chore/security-dependabot-critical-high

Conversation

@abbaseya

@abbaseya abbaseya commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Takes https://github.com/convertcom/php-sdk/security/dependabot to zero. Every alert on the repo is resolved — not just the critical and high ones — and nothing this PR adds carries an advisory. Two commits, all checks green.

Zero, measured three ways

1. Every alert clears. 88 alerts exist on main: 79 open and 9 auto-dismissed. Each was evaluated against this branch by matching the installed version against the advisory's own vulnerable_version_range (not just "is it newer than the fix", which gets the two separate picomatch lines wrong).

critical high medium low total left after this PR
Open 1 20 56 2 79 0
Auto-dismissed 1 5 3 0 9 0

The auto-dismissed nine are easy to miss: GitHub had filtered them off the Security tab, but the vulnerable versions were still in the tree. One is a criticaltar ≤ 7.5.18, GHSA-23hp-3jrh-7fpw — which never appeared as an open alert at all. So this clears two criticals: that one and handlebars GHSA-2w6w-674q-4c4q.

2. Nothing new is introduced. The branch adds 189 dependencies and removes 261, so "did we trade one set of advisories for another" is a fair question. GitHub's dependency-review API answers it against the same database Dependabot uses:

GET /repos/convertcom/php-sdk/dependency-graph/compare/main...chore/security-dependabot-critical-high

dependency changes: 450  (added 189, removed 261)
ADDED deps carrying advisories: 0
advisories carried by REMOVED deps: 90  {critical: 2, high: 27, moderate: 59, low: 2}

3. Both native auditors agree. composer audit on each lock and yarn npm audit --recursive --all report no advisories.

What changed, per manifest

composer.lock — guzzle 7.10.0 → 7.15.3, psr7 2.9.0 → 2.13.0, symfony/http-kernel 8.0.8 → 8.0.15, symfony/http-foundation 8.0.8 → 8.1.4, plus transitives. No composer.json constraint moved, so nothing changes for Packagist consumers — they resolve against their own tree.

demo/laravel/composer.lock — laravel/framework 13.1.1 → 13.24.0, league/commonmark 2.8.2 → 2.9.0, guzzle/psr7 as above, and symfony {http-kernel, mime, routing, http-foundation, cache, polyfill-intl-idn} to their patched releases.

yarn.lock — handlebars → 4.7.9, lodash-es → 4.18.1, js-yaml → 4.3.1, ip-address → 10.4.0, tar → 7.5.22, picomatch → 2.3.2 and 4.0.5, brace-expansion → 5.0.9, sigstore → 4.1.1.

packages/Utils/composer.lock — removed from version control.

Three things that need real eyes

Removing packages/Utils/composer.lock fixes rather than hides. Deleting a manifest makes its alerts disappear, so this deserves proof rather than assertion. .gitignore already lists /packages/*/composer.lock; this file predates the rule and stayed tracked, so it kept reporting a dependency set the package no longer has — stale enough that composer validate fails on it (phpunit locked at 10.5.45 against a ^11.0 constraint, and convertcom/php-sdk-types missing from the lock entirely). Resolving the package fresh from its composer.json, which is what a clean CI checkout does:

$ cd packages/Utils && composer install
phpunit/phpunit    11.5.56
guzzlehttp/guzzle  7.15.3
guzzlehttp/psr7    2.13.0
$ composer audit   → No security vulnerability advisories found.
$ composer test    → OK (148 tests, 176 assertions)

No vulnerable version exists in the real tree — the lockfile was the only thing claiming otherwise. The other eleven packages carry no lockfile, and libraries should not.

semantic-release 24 → 25, and @semantic-release/github 11 → 12 with it. This is the release path for the SDK, so it is the riskiest line in the diff. It was not optional: sigstore needs 4.1.1 but sits behind a ^3.0.0 range, so no in-range bump reaches it. It arrives via semantic-release → @semantic-release/npm → npm → libnpmpublish, and only the major bump pulls libnpmpublish 11 and sigstore ^4. The custom scripts/rollover-version-plugin.mjs was exercised directly across all six of its bump cases — patch below 9, patch rollover to minor, 2.9.9 rollover to major, refactor, breaking change, chore-only — with identical results to 24, and semantic-release --dry-run loads every configured plugin.

CI was never installing what yarn.lock says. The release job runs yarn install --immutable, but nothing pinned the package manager, so the runner's preinstalled Yarn 1.22.22 serviced it. Classic Yarn cannot read a Yarn 4 lockfile and does not recognise --immutable (it spells it --frozen-lockfile), so it re-resolved every dependency from the semver ranges and rewrote the lockfile on every run. The release log says it outright:

yarn install v1.22.22
[1/4] Resolving packages...
success Saved lockfile.

The lockfile was decorative. Fixed by committing the Yarn 4.18.0 binary under .yarn/releases and pointing yarnPath at it — see the review round below for why this rather than Corepack. Without this, the yarn.lock half of this PR would not have taken effect in CI.

Review round — what changed after @DmytroConvert's review

All three findings were reproduced and all three are fixed in 00e00ee.

1. The Corepack cliff — confirmed, and the date is exact. Node stopped distributing Corepack as of 25.0.0 (nodejs/node doc/contributing/distribution.md, History: "corepack … is no longer distributed as of Node.js 25.0.0"), and nodejs/Release schedule.json puts v26 at lts: 2026-10-28. So node-version: 'lts/*' plus corepack enable was a release job with about ten weeks of life left. (One correction to the review: nodejs/node#57617 is closed, not merged — the doc is the citation that holds.)

Fixed by removing the Corepack dependency rather than reinstalling it. yarn set version 4.18.0 --yarn-path commits the binary under .yarn/releases and sets yarnPath in .yarnrc.yml. Every yarn honours yarnPath, including the Yarn 1.22.x runners preinstall, so yarn resolves to 4.18.0 with no reference to Corepack and no coupling to which Node lts/* picks. The corepack enable step is deleted.

Measured in this repo, driving the runner's actual classic Yarn 1.22.22:

yarn --version                 → 4.18.0      (1.22.22 with yarnPath removed)
install --immutable, cold      → exit 0
install --immutable, drifted   → exit 1, YN0028 "The lockfile would have been
                                 modified by this install, which is explicitly forbidden"

So --immutable is now genuinely enforced through the same entry point that used to silently rewrite the lockfile.

Chosen over the two alternatives in the review: npm install -g corepack@latest fetches an unpinned global on every release run, which is new floating surface in a PR whose point is removing exactly that; and pinning node-version: '24' reschedules the failure to 2028-04-30 rather than removing it.

The vendored binary is byte-identical to upstream 4.18.0 from three independent sources — this file, Corepack's npm-sourced cache, and a fresh fetch of repo.yarnpkg.com/4.18.0/packages/yarnpkg-cli/bin/yarn.js:

sha256  fb8b1d20be72a0b544a35bcec4c7ed0ff55a9b173c01f191b02ba164b2051db5

Two details worth flagging. .gitignore needed .yarn/* rather than .yarn/git cannot re-include a path whose parent directory is excluded, so !.yarn/releases was inert under the old pattern and the binary would have been silently left out of the commit. And a new .gitattributes marks .yarn/ export-ignore so the 3.6 MB never reaches consumers through Composer's dist archive; it is scoped to .yarn/ only, since excluding anything else would change what consumers receive.

2. The .yarnrc.yml comment overstated the gate — correct. npmMinimalAgeGate is enforced at resolution, not install. Reworded to "refuse to resolve", and to spell out what it does not cover: a frozen yarn install --immutable never consults it, and a Dependabot PR arrives with the lockfile already resolved. It guards the act of choosing a version, not CI.

3. @semantic-release/release-notes-generator was locked twice — correct. package.json asked for ^14.0.0 (→ 14.1.0) while semantic-release 25 wants ^14.1.0 (→ 14.1.1). Bumped the devDependency to ^14.1.0; the lock now carries one copy, at 14.1.1.

Also restored two things yarn set version silently changed: it rewrote packageManager without the +sha512 integrity hash, and stripped the comment from .yarnrc.yml. Both are back.

Adjacent, not fixed here: the same Corepack cliff exists in convertcom/python-sdk release.yml (two jobs, lts/* + corepack enable) and convertcom/javascript-sdk qa.yml (corepack enable + corepack prepare yarn@stable). Both will break the same way on 2026-10-28 and want the same yarnPath treatment.

Also in here

npmMinimalAgeGate: 4320 (3 days) in .yarnrc.yml, matching convertcom/javascript-sdk#421 — php-sdk never got that hardening. No npmPreapprovedPackages entry is needed: this project has no internal npm dependencies (the PHP packages ship via Packagist), so nothing in our own release chain can stall on a freshly-published package. Every version in the lockfile is at least 3.5 days old, and re-resolving under the gate shifts nothing.

Worth knowing about the gate: Yarn's own lockfile migration writes hardening opt-outs — on first install it rewrote .yarnrc.yml with npmMinimalAgeGate: 0, enableScripts: true and approvedGitRepositories: ["**"], against Yarn 4.18 defaults of 1d, false and []. Those writes were discarded; the committed value is the deliberate 3-day one.

Verification

Locally on this branch: composer validate, composer build:validate (monorepo-builder), composer cs-check (0 of 130 files), composer analyze (phpstan, no errors), 648 unit tests, 474 cross-SDK parity tests, composer audit on both locks, yarn npm audit, semantic-release --dry-run loading every plugin, the packages/Utils fresh-resolve above, and install --immutable driven by classic Yarn exactly as CI drives it — passing cold and failing on drift.

On CI: the full matrix across PHP 8.2 / 8.3 / 8.4 in both prefer-lowest and prefer-stable, plus Code Style, Monorepo Validation, Generated Types Guard, PHPStan and CodeQL.

All 88 alerts were re-checked against the post-review lockfile. Still zero.

Supersedes — both can be closed, nothing is lost

#40handlebars, picomatch. Fully covered: this PR takes handlebars to the same 4.7.9 and picomatch further (2.3.2 / 4.0.5), plus the other npm advisories #40 does not touch.

#36phpunit in packages/Utils. Its alert (GHSA-vvj3-c3rp-c85p, phpunit < 10.5.62) is resolved here, and the fresh-resolve above shows the package landing on phpunit 11.5.56.

Its changes are deliberately not folded in, and that is worth stating rather than leaving it to look like an oversight. #36 is not only a lockfile bump — it also widens packages/Utils/composer.json:

     "require-dev": {
-        "phpunit/phpunit": "^11.0"
+        "phpunit/phpunit": "^11.0 || ^10.0"
     },

That is Dependabot bending the constraint backwards so the stale locked phpunit 10.5.45 becomes legal, rather than updating the lock to 11. It has two problems: it re-admits the phpunit 10 range the advisory is about, and it breaks monorepo validation. Applying that exact diff on this branch and running composer build:validate:

 packages/Utils/composer.json        ^11.0 || ^10.0
 composer.json                       ^11.0
 [ERROR] Found conflicting package versions, fix them first.

which is presumably why it has sat open since April. The root cause is that the lock was generated against an older composer.json and never regenerated, so the lock and the constraint disagree. Untracking the lock removes the disagreement instead of legalising it — and the repo's own .gitignore already asked for that.

Neither PR cuts a release, and neither does this one: all three are chore, and scripts/rollover-version-plugin.mjs returns null for chore.

🤖 Generated with Claude Code

Clears every open critical/high advisory on the repo (1 critical, 20 high)
plus the mediums and lows that ride along on the same package bumps —
composer audit and yarn npm audit are both clean afterwards.

composer.lock (root)
  guzzlehttp/guzzle 7.10.0 -> 7.15.3, guzzlehttp/psr7 2.9.0 -> 2.13.0,
  symfony/http-kernel 8.0.8 -> 8.0.15, symfony/http-foundation 8.0.8 -> 8.1.4
  and their transitive deps. No composer.json constraint changed, so nothing
  changes for Packagist consumers — they resolve against their own tree.

demo/laravel/composer.lock
  laravel/framework 13.1.1 -> 13.24.0, league/commonmark 2.8.2 -> 2.9.0,
  guzzle/psr7 as above, symfony/{http-kernel,mime,routing,http-foundation,
  cache,polyfill-intl-idn} to their patched releases.

packages/Utils/composer.lock — untracked
  .gitignore already lists /packages/*/composer.lock; this one predates the
  rule and stayed tracked, so it kept reporting a dependency set the package
  no longer has. It was stale enough that `composer validate` failed on it:
  phpunit locked at 10.5.45 against a "^11.0" constraint, and
  convertcom/php-sdk-types missing from the lock entirely. Eleven sibling
  packages carry no lockfile; libraries should not.

yarn.lock
  handlebars 4.7.8 -> 4.7.9 (the one critical), lodash-es -> 4.18.1,
  js-yaml -> 4.3.1, ip-address -> 10.4.0, tar -> 7.5.22,
  picomatch -> 2.3.2/4.0.5, brace-expansion -> 5.0.9.

  sigstore needed 4.1.1 but sat behind a "^3.0.0" range, so no in-range bump
  could reach it. It arrives via semantic-release -> @semantic-release/npm ->
  npm -> libnpmpublish, so semantic-release moves 24 -> 25 (and its peer
  @semantic-release/github 11 -> 12), which pulls libnpmpublish 11 and
  sigstore ^4.0.0. Verified: the custom rollover-version plugin loads and all
  six of its bump cases behave identically under 25, and `semantic-release
  --dry-run` loads every configured plugin.

CI was not installing what yarn.lock says
  The release job ran `yarn install --immutable` against the runner's
  preinstalled Yarn 1.22.22. Classic Yarn cannot read a Yarn 4 lockfile and
  does not recognise `--immutable` (it spells it `--frozen-lockfile`), so it
  re-resolved every dependency from the package.json ranges and rewrote the
  lockfile on each run — "success Saved lockfile" in the job log. The lockfile
  was decorative. Pinning packageManager to yarn@4.18.0 and enabling Corepack
  makes the install reproducible and makes lockfile drift fail the job.

Supply-chain minimum release age
  Adds npmMinimalAgeGate: 4320 (3 days), matching convertcom/javascript-sdk.
  php-sdk never got that hardening. No npmPreapprovedPackages entry is needed
  here — this project has no internal npm dependencies, so nothing in our own
  release chain can stall on a freshly-published package. Every version in the
  lockfile is at least 3.5 days old, and re-resolving under the gate shifts
  nothing.

Supersedes the open Dependabot PRs #40 (handlebars, picomatch) and #36
(phpunit in packages/Utils).

Verified: composer validate, monorepo-builder validate, php-cs-fixer,
phpstan, 648 unit tests, 474 cross-SDK parity tests, composer audit on both
locks, yarn npm audit, and a clean `yarn install --immutable` under Yarn 4.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abbaseya abbaseya self-assigned this Aug 8, 2026
@abbaseya
abbaseya requested a review from DmytroConvert August 8, 2026 16:34
abbaseya added a commit to convertcom/python-sdk that referenced this pull request Aug 8, 2026
Same fix as convertcom/php-sdk#60, applied to this repo.

CI has never run Yarn 4. `release.yml` already ran `corepack enable` — which
is not enough on its own. With no `packageManager` field in `package.json`,
corepack has nothing to resolve and falls through to its own default, Yarn
1.22.22, which it then downloads. From the last real release run
(30637880488, 2026-07-31):

    ! Corepack is about to download .../yarn-1.22.22.tgz
    yarn install v1.22.22
    [1/4] Resolving packages...
    warning Workspaces can only be enabled in private projects.
    success Saved lockfile.

Three consequences, all live until now:

  1. The committed `yarn.lock` was never used. Classic Yarn cannot read a
     `__metadata: version: 10` lockfile, so it re-resolved every release from
     the `package.json` semver ranges against whatever the registry served at
     that moment. "success Saved lockfile." is Yarn 1 writing its own.
  2. `--immutable` was a silent no-op — classic spells it
     `--frozen-lockfile`, does not error on the unknown flag, and saved a
     lockfile anyway.
  3. `.yarnrc.yml` was never read. `nodeLinker: node-modules` did nothing,
     and an age gate added there would have done nothing either.

So the `packageManager` pin is what makes the gate exist, not a nicety
alongside it. Written with `corepack use yarn@4.18.0`, never by hand; 4.18.0
is the version that wrote the lockfile in the previous commit, and Yarn
4.10.3 rejects that lockfile as needing modification — the pin has to match.

`npmMinimalAgeGate: 4320` (3 days) matches javascript-sdk and php-sdk. No
`npmPreapprovedPackages`: unlike javascript-sdk, this project has zero
`@convertcom/*` npm packages in `package.json` or `yarn.lock` (the SDK ships
via PyPI; the only Node packages here are release tooling), so an exemption
list would be dead config. Yarn 4.18 already defaults the gate to 1440, so
this raises it to the house value rather than introducing it.

Verified after the change, per the known migration trap: `yarn config --json`
shows `npmMinimalAgeGate` = 4320 sourced from `.yarnrc.yml`, and
`enableScripts` / `approvedGitRepositories` still at their secure defaults —
no `YN0087` migration fired and no hardening opt-out was written. Cold-cache
`yarn install --immutable` (no node_modules, empty YARN_GLOBAL_FOLDER, all
418 packages fetched fresh with the gate active) passes, and
`yarn release:dry-run` loads every plugin and correctly declines to publish
off main. All 122 resolutions the previous commit adds were separately
checked against the 3-day floor; the youngest, semantic-release@25.0.9, is
3.5 days old.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@DmytroConvert

DmytroConvert commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Checked this out into a clean worktree and re-ran every claim in the description — all of it reproduces:
composer validate --strict, build:validate, composer audit on both locks, yarn install --immutable
under Yarn 4.18.0 with the gate active, yarn npm audit, 648 unit + 474 cross-SDK tests, phpstan clean. The
packageManager hash is byte-identical to what corepack use yarn@4.18.0 emits.

Two things worth adding to the record:

Coverage is better than the description claims. I pulled all 79 open alerts and compared each
first_patched_version / vulnerable_version_range against the resolved versions on this branch. All 79 close
— 1 critical, 20 high, 56 medium, 2 low — not just the crit/high plus stragglers. The only package resolving to
two versions is picomatch (2.3.2 / 4.0.5), and both sit above their respective advisory lines.

Provenance checks out. Since this is 95% lockfile and checksums only verify against the lock itself, I looked
at where the entries point: every yarn.lock resolution is @npm:<version> (no patch:, no git/https tarballs,
no aliases), no npmRegistryServer override, and the added composer URLs are 61 × api.github.com, 17 ×
github.com, 6 funding links.

Also confirmed the #36 reasoning rather than taking it on faith — applying that exact "^11.0 || ^10.0" widening
here reproduces the monorepo-builder conflict verbatim. Agreed on closing both #36 and #40.


One change I'd want before merge

corepack enable has a ~2-month shelf life under node-version: 'lts/*'.

Node stopped distributing Corepack in 25.0.0nodejs/node#57617, (SEMVER-MAJOR) build: stop distributing Corepack — and doc/contributing/distribution.md states it outright: "corepack … is no longer distributed as of
Node.js 25.0.0."
lts/* resolves to 24.19.0 today so the step works, but Node 26 is already cut (v26.7.0) and
enters Active LTS around October. The moment lts/* flips, this step fails with command-not-found and the release
job dies.

It's the same shape as the bug this PR is fixing — the release job inheriting whatever toolchain the runner happens
to ship. Failing loudly beats Yarn 1 silently rewriting the lockfile, but it still stops releases. Suggest making
the step version-independent so it survives the flip:

      - name: Enable Corepack
        run: |
          npm install -g corepack@latest
          corepack enable

Pinning node-version: '24' also works, but that just moves the deadline.

Two non-blocking notes

1. The .yarnrc.yml comment overstates the gate. It says "refuse to install any dependency version published
less than 3 days ago" — the gate is enforced at resolution, not install. Verified both directions by cranking
npmMinimalAgeGate to 10 years on this branch:

  • yarn install --immutable with the committed lockfile → still passes, gate never consulted
  • same config with yarn.lock deleted → fails: YN0016: @semantic-release/github@npm:^12.0.0: All versions satisfying "^12.0.0" are quarantined

So it covers yarn add / yarn up on a developer machine, but not CI, and not versions arriving through a
Dependabot lockfile PR (Dependabot writes the resolved lock itself). Still worth having and consistent with
javascript-sdk — just worth wording as "refuse to resolve" so the next reader doesn't assume CI is behind it.

2. @semantic-release/release-notes-generator is duplicated in the lock. package.json pins ^14.0.0 → 14.1.0
while semantic-release 25 wants ^14.1.0 → 14.1.1. Both v14, both declaring semantic-release >=20.1.0 as a peer,
so nothing breaks — but bumping the devDependency to ^14.1.0 dedupes it.

FYI, not asks

  • The lock bumps barely intersect what CI tests: qa.yml's matrix runs composer update --prefer-lowest|--prefer-stable
    and ignores composer.lock, so the root lock is only consumed by lint, validate, and release, and
    demo/laravel/composer.lock by no job at all. Low merge risk (the matrix on main was already resolving to these
    guzzle/psr7 versions), but the demo lock is Dependabot-signal-only and nothing will notice if it rots.

  • Untracking packages/Utils/composer.lock is the right call — .gitignore already asked for it, the eleven siblings
    carry none, build:validate passes without it, and split.yml propagates the deletion to php-sdk-utils, which is
    what you want for a library.

    SO:
    One real problem:

    corepack enable in release.yml will fail on 2026-10-28. Node 25 dropped Corepack from its distribution. node-version:
    'lts/*' flips to Node 26 that day. Release job breaks.

    Fix: npm install -g corepack@latest before corepack enable.

    Two cosmetic ones:

    • .yarnrc.yml comment says the age gate blocks installs. It blocks resolution. CI and Dependabot bypass it.
    • @semantic-release/release-notes-generator is in the lock twice (14.1.0 + 14.1.1). Pin ^14.1.0 to dedupe.

    Everything else is correct — all 79 alerts close, locks are clean, all tests and checks pass.

@abbaseya abbaseya changed the title chore(deps): resolve all critical and high Dependabot alerts chore(deps): take Dependabot to zero — every alert resolved, none introduced Aug 10, 2026
Addresses all three findings from @DmytroConvert's review on #60.

1. The Corepack cliff (blocking). Confirmed and the date is exact: Node
   stopped distributing Corepack as of 25.0.0 — nodejs/node
   doc/contributing/distribution.md, "History": "corepack ... is no longer
   distributed as of Node.js 25.0.0" — and nodejs/Release schedule.json puts
   v26 at lts: 2026-10-28. So `node-version: 'lts/*'` plus `corepack enable`
   was a release job with ~2.5 months of life left. (One correction to the
   review: nodejs/node#57617 is closed, not merged; the doc is the citation
   that holds.)

   Fixed by removing the Corepack dependency rather than reinstalling it.
   `yarn set version 4.18.0 --yarn-path` commits the 4.18.0 binary under
   .yarn/releases and points yarnPath at it in .yarnrc.yml. Every yarn honours
   yarnPath, including the Yarn 1.22.x GitHub runners preinstall, so `yarn`
   resolves to 4.18.0 with no reference to Corepack and no coupling to which
   Node `lts/*` picks. The `corepack enable` step is deleted.

   Measured against the runner's actual classic Yarn 1.22.22, in this repo:
     yarn --version                  -> 4.18.0   (1.22.22 without yarnPath)
     install --immutable, cold       -> exit 0
     install --immutable, drifted    -> exit 1, YN0028 lockfile would have
                                        been modified
   So --immutable is now genuinely enforced through the same entry point that
   used to silently rewrite the lockfile.

   Preferred over `npm install -g corepack@latest` because that fetches an
   unpinned global on every release run — new floating surface in a PR whose
   point is removing exactly that. Preferred over pinning node-version: '24'
   because that reschedules the failure to 2028-04-30 rather than removing it.

   The vendored binary is byte-identical to upstream 4.18.0 from three
   independent sources — this file, Corepack's npm-sourced cache, and a fresh
   fetch of repo.yarnpkg.com/4.18.0/packages/yarnpkg-cli/bin/yarn.js:
   sha256 fb8b1d20be72a0b544a35bcec4c7ed0ff55a9b173c01f191b02ba164b2051db5

   .gitignore needed `.yarn/*` rather than `.yarn/` — git cannot re-include a
   path whose parent directory is excluded, so `!.yarn/releases` was inert
   under the old pattern and the binary would have been silently left out of
   the commit. .yarn/cache and friends stay ignored. A new .gitattributes
   marks `.yarn/ export-ignore` so the 3.6 MB does not reach consumers through
   Composer's dist archive; it is scoped to .yarn/ only, since excluding
   anything else would change what consumers receive.

2. The .yarnrc.yml comment overstated the gate. Correct — npmMinimalAgeGate is
   enforced at resolution, not install, which the review verified in both
   directions. Reworded to say "refuse to RESOLVE", and to spell out what it
   does not cover: a frozen `yarn install --immutable` never consults it, and a
   Dependabot PR arrives with the lockfile already resolved. It guards the act
   of choosing a version, not CI.

3. @semantic-release/release-notes-generator was locked twice. Correct —
   package.json asked for ^14.0.0 (14.1.0) while semantic-release 25 wants
   ^14.1.0 (14.1.1). Bumped the devDependency to ^14.1.0; the lock now carries
   one copy at 14.1.1.

Also restores two things `yarn set version` silently changed: it rewrote
packageManager without the +sha512 integrity hash, and stripped the comment
from .yarnrc.yml. Both are back.

Still zero vulnerabilities: all 88 alerts (79 open + 9 auto-dismissed)
re-checked against the new lockfile, none remain.

Verified: composer validate, monorepo-builder validate, php-cs-fixer, phpstan,
648 unit tests, 474 cross-SDK parity tests, composer audit on both locks,
yarn npm audit, semantic-release --dry-run loading every plugin, and
`install --immutable` driven by classic Yarn as CI will drive it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abbaseya

Copy link
Copy Markdown
Contributor Author

Thanks @DmytroConvert — all three reproduced, all three fixed in 00e00ee. Description updated.

The Corepack cliff is real and your date is exact. Confirmed independently: nodejs/node doc/contributing/distribution.md History says "corepack … is no longer distributed as of Node.js 25.0.0", and nodejs/Release schedule.json has v26 at lts: 2026-10-28. One correction: nodejs/node#57617 is closed, not merged — the removal landed elsewhere, so the doc is the citation that holds. Doesn't change the conclusion.

I went with a third option rather than either you named, because both leave the release job depending on something it shouldn't. yarn set version 4.18.0 --yarn-path commits the binary under .yarn/releases and sets yarnPath — and every yarn honours yarnPath, including the 1.22.x the runners preinstall. So Corepack leaves the picture entirely and there is no Node coupling left to expire. The corepack enable step is deleted.

Measured in the repo, driving the runner's actual classic Yarn 1.22.22:

yarn --version                 → 4.18.0      (1.22.22 with yarnPath removed)
install --immutable, cold      → exit 0
install --immutable, drifted   → exit 1, YN0028 "The lockfile would have been
                                 modified by this install, which is explicitly forbidden"

Which also means --immutable is now enforced through the exact entry point that used to silently rewrite the lockfile — the delegation would have prevented the original bug on its own.

Why not your two: npm install -g corepack@latest pulls an unpinned global on every release run, which is new floating surface in a PR whose whole point is removing that; and node-version: '24' reschedules to 2028-04-30 rather than removing the class of failure. You were right that pinning "just moves the deadline" — that reasoning is what pushed me past it.

Since it's a 3.6 MB binary going into the repo, provenance: byte-identical to upstream 4.18.0 from three independent sources — the vendored file, Corepack's npm-sourced cache, and a fresh fetch of repo.yarnpkg.com/4.18.0/packages/yarnpkg-cli/bin/yarn.jssha256 fb8b1d20be72a0b544a35bcec4c7ed0ff55a9b173c01f191b02ba164b2051db5. .gitattributes marks .yarn/ export-ignore so it never reaches consumers through Composer's dist archive, scoped to .yarn/ only so nothing else consumers receive changes.

One trap worth recording: .gitignore needed .yarn/*, not .yarn/. Git cannot re-include a path whose parent directory is excluded, so !.yarn/releases was inert and the binary would have been silently left out of the commit — CI would then have fallen straight back to Yarn 1 with a yarnPath pointing at a file that doesn't exist.

Both cosmetic ones fixed as described. The gate comment now says "refuse to resolve" and spells out that a frozen install never consults it and Dependabot arrives with the lockfile already resolved. @semantic-release/release-notes-generator is bumped to ^14.1.0 and the lock carries one copy at 14.1.1.

Also worth flagging: yarn set version silently rewrote packageManager without the +sha512 hash you'd verified, and stripped the .yarnrc.yml comment. Both restored.

Two things from your review I want to pick up separately rather than here:

  • Your point that the root lock is only consumed by lint, validate and release, and demo/laravel/composer.lock by no job at all, is the more interesting finding in your review. A lock nothing installs will rot silently. Worth its own issue.
  • The same cliff exists in two sibling repospython-sdk release.yml (both jobs, lts/* + corepack enable) and javascript-sdk qa.yml (corepack enable + corepack prepare yarn@stable). Both break on 2026-10-28 and want the same yarnPath treatment.

Still zero: all 88 alerts (79 open + 9 auto-dismissed) re-checked against the post-review lockfile, dependency-review still reports 0 added dependencies carrying an advisory, and all 11 checks are green. Back to you.

@abbaseya
abbaseya merged commit c739d46 into main Aug 10, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants