From 74ec56cab4ec27adfacbd59ebf95e89c2e91164f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=9C=E5=86=A0=E9=AD=81?= Date: Sun, 6 Sep 2026 09:02:56 +0900 Subject: [PATCH] fix(angular): keep the GitHub Packages token out of the committed .npmrc angular/.npmrc referenced ${PACKAGES_READ_TOKEN}, which broke local development outright: any ${...} placeholder in a committed .npmrc makes every yarn invocation in that directory fail with "Failed to replace env in config" the moment the variable is unset - including yarn start, yarn build and yarn test, none of which touch that registry. Reported from a plain ./start.ps1. The file now carries only the two scope mappings. Both workflows write the credential to ~/.npmrc in an "Authenticate to GitHub Packages" step, and the file documents the one-line equivalent for developers. That also drops the job-level env from both workflows, and with it the whole "must be job-level because setup-node's cache probe reads .npmrc first" complication - with no placeholder to expand, that probe has nothing to fail on. Note the token needs the read:packages scope; a plain repo-scoped one (which is what tends to be sitting in $GITHUB_TOKEN) is not enough, and would have failed at download time instead. Also drops //npm.pkg.github.com/:always-auth=true. With both scopes mapped yarn already attaches the token to the tarball download without it, and npm 11 warns on every invocation that the per-registry form is unknown config. Verified by cold-cache `yarn install --frozen-lockfile` with and without it, and by re-running the reported ./start.ps1 path with no token variable set. --- .github/workflows/ci.yml | 39 +++++++++++++----------- .github/workflows/release.yml | 57 ++++++++++++++++++----------------- CHANGELOG.md | 14 +++++++++ angular/.npmrc | 20 ++++++++++-- 4 files changed, 83 insertions(+), 47 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6eb953..a3ac99f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,20 +39,10 @@ jobs: permissions: contents: read - # angular/.npmrc maps the @dignite and @dignite-projects scopes to GitHub Packages and - # references ${PACKAGES_READ_TOKEN} for it. Yarn Classic substitutes every env-var placeholder - # in its resolved config on every invocation and throws when one is unset - including the - # `yarn cache dir` probe setup-node runs for `cache: yarn`, which happens before any run: step - # of this job. So it has to be job-level, not set on the install step - exactly as release.yml - # already does. - # - # @dignite/ng.flex-fields (and its -ckeditor/-file-explorer adapters, and @dignite/ng.file- - # explorer) started resolving through those scopes as of 10.0.0-rc.16 - release.yml's copy of - # this comment carries the full account (why, the alias mechanism, and what to revert once it's - # no longer needed); angular/.npmrc carries the reason BOTH scopes have to be mapped, which is - # the non-obvious half and the one this job actually died on twice. - env: - PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} + # No job-level token env here on purpose: angular/.npmrc deliberately carries no ${...} + # placeholder, because one would make every yarn invocation in that directory fail hard when the + # variable is unset (see that file's own comment). The credential is written to ~/.npmrc by the + # step below instead. steps: - name: Checkout site @@ -116,6 +106,20 @@ jobs: cache: yarn cache-dependency-path: site/angular/yarn.lock + # @dignite/* resolves from abp-modules' private GitHub Packages as of 10.0.0-rc.16, so the + # install below needs a credential for it. It goes in ~/.npmrc rather than angular/.npmrc, and + # as its own step rather than a job-level env, for the reason that file's comment gives: a + # ${...} placeholder in the committed .npmrc breaks every yarn command run in that directory + # whenever the variable is missing, which is most of the time on a developer's machine. + # + # PACKAGES_READ_TOKEN (a read:packages PAT) rather than secrets.GITHUB_TOKEN, matching what + # release.yml's NuGet verification already does - GITHUB_TOKEN's package access is documented + # as scoped to the workflow's own repository, and these packages belong to abp-modules. + - name: Authenticate to GitHub Packages + env: + PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} + run: printf '//npm.pkg.github.com/:_authToken=%s\n' "$PACKAGES_READ_TOKEN" >> ~/.npmrc + # Yarn with the committed lockfile, matching release.yml and every developer machine, so the # tree verified here is the one the lockfile actually describes rather than a fresh resolution # that ignores it. --frozen-lockfile additionally fails if a package.json change was committed @@ -125,9 +129,10 @@ jobs: # lockfile entry carried a GitHub Packages "resolved" URL through a code path that never # attached registry auth. That was fixed and both workflows ran the same yarn command against # public-npmjs-only @dignite/* packages for a while - but as of 10.0.0-rc.16, @dignite/* - # aliases back onto GitHub Packages (see the job-level env: block's comment above), so this - # repeats the original gotcha's shape if that auth ever regresses. release.yml's step carries - # the longer account. + # aliases back onto GitHub Packages, and the alias reopened that same wound from a new angle: + # yarn picks a tarball's registry (hence its token) by the ALIAS's scope, so angular/.npmrc has + # to map @dignite as well as @dignite-projects or yarn sends no credential at all. That one + # cost two red CI runs; angular/.npmrc carries the full account. # # It matters for the duplicate check below specifically: npm dedupes graphs that Yarn Classic # splits into nested copies, so running that check after an npm install would pass vacuously. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85acdd7..3cc343d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,35 +40,30 @@ jobs: contents: write # Required by softprops/action-gh-release to create a draft Release packages: write # Required to push pre-release NuGet/npm packages to GitHub Packages - # angular/.npmrc references ${PACKAGES_READ_TOKEN} to authenticate the @dignite-projects scope - # against GitHub Packages. As of 10.0.0-rc.16, angular/package.json aliases @dignite/ng.flex-fields - # (and its -ckeditor/-file-explorer adapters, and @dignite/ng.file-explorer) to - # "npm:@dignite-projects/@" (see the "Install Angular dependencies" step below for - # why), which is what actually routes those four through this scope/token - GitHub Packages' - # npm registry requires the scope to equal the owning org login, so @dignite itself has no - # registry entry there at all. setup-node's `cache: yarn` reads angular/.npmrc (to prime/verify - # the cache) before any later step gets a chance to export the token itself, so it has to be a - # job-level env, not just set on that step. (ci.yml carries the same env: block for the same - # reason; this comment is the one canonical copy of the reasoning - keep them in sync.) + # As of 10.0.0-rc.16, angular/package.json aliases @dignite/ng.flex-fields (and its + # -ckeditor/-file-explorer adapters, and @dignite/ng.file-explorer) to + # "npm:@dignite-projects/@" - GitHub Packages' npm registry requires the scope to + # equal the owning org login, so @dignite has no registry entry there at all. angular/.npmrc maps + # both scopes (the @dignite one is needed because yarn picks a tarball's registry, and hence its + # auth token, by the ALIAS's scope) and deliberately holds no token placeholder; the credential + # is written to ~/.npmrc by the "Authenticate to GitHub Packages" step below, in this workflow + # and in ci.yml alike. angular/.npmrc carries the full account of both decisions. # # PACKAGES_READ_TOKEN (a PAT scoped to read:packages only), not secrets.GITHUB_TOKEN - the same # choice "Verify packed NuGet packages restore cleanly" below already made, for the same # documented reason: GITHUB_TOKEN's package access is scoped to the workflow's own repository, # and these packages are published by a different one (abp-modules). Whether GITHUB_TOKEN would - # in fact be refused here has never been isolated - the CI 401s that led to this token were a - # different bug entirely (yarn was sending no token at all; see angular/.npmrc's own comment) - - # so this is the documented-behavior choice plus the NuGet side's precedent, not a measurement. - # If someone wants the ephemeral token back, that experiment is one commit and one CI run. + # in fact be refused has never been isolated - the CI 401s that led here were a different bug + # entirely (yarn was sending no token at all) - so this is the documented-behavior choice plus + # the NuGet side's precedent, not a measurement. # # To revert once abp-modules tags a release that mirrors flex-fields (rc.16 or a successor) to # public npmjs again: drop the four npm: aliases in angular/package.json's dependencies and - # resolutions blocks back to plain semver ranges; change check-angular-package-duplicates.mjs's - # target list in both this file and ci.yml from @dignite-projects back to @dignite. Leave - # PACKAGES_READ_TOKEN itself and angular/.npmrc's @dignite-projects mapping in place either way - - # they're what "Verify packed NuGet packages restore cleanly" already depends on permanently, and - # cost nothing unused. - env: - PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} + # resolutions blocks back to plain semver ranges; drop the @dignite mapping from angular/.npmrc + # (the @dignite-projects one stays - this workflow publishes under it); change + # check-angular-package-duplicates.mjs's target list in both this file and ci.yml from + # @dignite-projects back to @dignite. That also gives developers their tokenless local + # `yarn install` back, which is the part with the ongoing cost. steps: - name: Checkout site @@ -240,18 +235,24 @@ jobs: cache: yarn cache-dependency-path: site/angular/yarn.lock - # angular/.npmrc maps the @dignite-projects scope to GitHub Packages; ${GITHUB_TOKEN} in that - # file is a literal env-var placeholder npm/yarn substitute at read time, so it has to be - # present in the environment here (it is, job-level - see the env: block above). - # + # Same credential handling as ci.yml, for the reason angular/.npmrc explains: no token + # placeholder in the committed file, so it goes to ~/.npmrc here instead. This has to run + # before the install below, and before "Configure npm registry for publishing" further down + # redirects npm's user config elsewhere. + - name: Authenticate to GitHub Packages + env: + PACKAGES_READ_TOKEN: ${{ secrets.PACKAGES_READ_TOKEN }} + run: printf '//npm.pkg.github.com/:_authToken=%s\n' "$PACKAGES_READ_TOKEN" >> ~/.npmrc + # Yarn with the committed lockfile, matching ci.yml and every developer machine. This step # used to be `npm install --no-package-lock` because Yarn Classic fetches a dependency whose # lockfile entry carries a GitHub Packages "resolved" URL through a code path that never # attaches registry auth. That was fixed, and for a while every @dignite/* package resolved # from public npmjs instead - but as of 10.0.0-rc.16, @dignite/* aliases back onto GitHub - # Packages (see the job-level env: block's comment), so this step is exercising that auth - # path again. --legacy-peer-deps is gone with npm: Yarn Classic does not enforce peer ranges, - # which is what that flag was working around (@swimlane/ngx-datatable@22 wants + # Packages, and the alias reopened the same wound from a new angle (yarn keys a tarball's + # registry, hence its token, off the ALIAS's scope), which is why angular/.npmrc now maps + # @dignite too. --legacy-peer-deps is gone with npm: Yarn Classic does not enforce peer + # ranges, which is what that flag was working around (@swimlane/ngx-datatable@22 wants # @angular/common 18-20.x, workspace is on 21.2.x). # # It also matters for the duplicate check below: npm dedupes graphs that Yarn Classic splits diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0edde..8b8407b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `ci.yml`'s briefly-added `packages: read` permission is gone again, and no "Manage Actions access" grant turned out to be required either. + **The credential itself is no longer named in the committed `angular/.npmrc`.** It briefly was, as + `${PACKAGES_READ_TOKEN}`, and that broke local development outright: any `${...}` placeholder in a + committed `.npmrc` makes *every* yarn invocation in that directory fail with `Failed to replace env + in config` when the variable is unset - including `yarn start`, `yarn build` and `yarn test`, none + of which touch this registry. The file now carries only the two scope mappings, and both CI + workflows write the token to `~/.npmrc` in an "Authenticate to GitHub Packages" step instead. + Developers do the same thing once, by hand - the file documents the command. Note a plain + `repo`-scoped token is not sufficient; this needs `read:packages`. + + The `//npm.pkg.github.com/:always-auth=true` line is gone too: with both scopes mapped, yarn + already attaches the token to the tarball download without it, and npm 11 warns on every + invocation that the per-registry form is unknown config. Verified by cold-cache installs with and + without it - the fix was always the `@dignite` scope mapping, never `always-auth`. + **`release.yml`'s "Verify packed npm package installs and bundles cleanly" (`packed` mode) step would otherwise have failed on the next actual release attempt** - a real, verified break, not a hypothetical one: it installs the *raw* packed `dist/site/package.json`, which still names its diff --git a/angular/.npmrc b/angular/.npmrc index e0916ae..f2181c8 100644 --- a/angular/.npmrc +++ b/angular/.npmrc @@ -13,5 +13,21 @@ # a package under the @dignite scope, which would 404.) @dignite:registry=https://npm.pkg.github.com -//npm.pkg.github.com/:_authToken=${PACKAGES_READ_TOKEN} -//npm.pkg.github.com/:always-auth=true +# Deliberately NO _authToken line here. Any ${ENV_VAR} placeholder in a committed .npmrc makes +# EVERY yarn invocation in this directory die with "Failed to replace env in config" the moment that +# variable is unset - including `yarn start`, `yarn build` and `yarn test`, which never touch this +# registry at all. Credentials belong in the user-level ~/.npmrc instead, where they also stay out +# of git. +# +# One-time local setup - needs a token with the read:packages scope (a plain `repo`-scoped one is +# NOT enough; `gh auth token` is usually the easy source): +# +# printf '//npm.pkg.github.com/:_authToken=%s\n' "$(gh auth token)" >> ~/.npmrc +# +# CI writes the same line from the PACKAGES_READ_TOKEN secret - see the "Authenticate to GitHub +# Packages" step in ci.yml and release.yml. +# +# There is deliberately no always-auth line either: with both scopes mapped above, yarn already +# attaches the token to the tarball download, and the per-registry form makes npm 11 warn on every +# invocation that it is unknown config. Verified by a cold-cache `yarn install --frozen-lockfile` +# with and without it - the fix is the @dignite mapping, not always-auth.