diff --git a/docs/packkit-core.js b/docs/packkit-core.js index 3bce1ef..dcbe91c 100644 --- a/docs/packkit-core.js +++ b/docs/packkit-core.js @@ -538,6 +538,22 @@ function readme(cfg) { if (cfg.hasCli) { lines.push("## CLI", "", "```sh", `npx ${cfg.name} --help`, "```", ""); } + if (cfg.workflows?.includes("npm-publish")) { + const changesets = cfg.release === "changesets"; + lines.push( + "## Releasing", + "", + changesets ? "Releases are handled by the `Release` workflow: push a changeset (`npx changeset`) and it opens a version PR, then publishes when that PR merges." : "Pushing a `v*` tag triggers the `Publish` workflow, which publishes to npm with provenance.", + "", + "**Publishing needs npm credentials, which a new repository does not have.** Either:", + "", + "- Set up [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC) \u2014 no secret to store or rotate, and the recommended option; or", + "- Add an [npm automation token](https://docs.npmjs.com/creating-and-viewing-access-tokens) as an `NPM_TOKEN` repository secret.", + "", + changesets ? "Until one of those is in place the `Release` workflow will fail with `ENEEDAUTH` on every push. That is expected on a brand-new repo." : "Until one of those is in place, tag pushes will fail with `ENEEDAUTH`.", + "" + ); + } lines.push("## License", "", cfg.license === "none" ? "Unlicensed." : `${cfg.license}${cfg.author ? " \xA9 " + cfg.author : ""}`, ""); return lines.join("\n"); } diff --git a/src/cli/index.js b/src/cli/index.js index 0be3bd8..acd48fb 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -247,6 +247,11 @@ export async function run(argv = process.argv.slice(2)) { skipped.length ? `Kept ${skipped.length} existing file${skipped.length > 1 ? 's' : ''} — Packkit's version was not written: ${skipped.join(', ')}` : null, + // Said here too, not just in the README: this is the moment a red X appears + // on a repo that is ten seconds old, and the cause is not obvious. + pushedTo && config.workflows?.includes('npm-publish') && config.release === 'changesets' + ? `The Release workflow will fail until npm credentials exist — set up npm Trusted Publishing, or add an NPM_TOKEN secret. See "Releasing" in the README.` + : null, ].filter(Boolean); const done = diff --git a/src/core/features/meta.js b/src/core/features/meta.js index 891680c..a062c8a 100644 --- a/src/core/features/meta.js +++ b/src/core/features/meta.js @@ -144,6 +144,31 @@ function readme(cfg) { lines.push('## CLI', '', '```sh', `npx ${cfg.name} --help`, '```', ''); } + // Publishing needs a credential the repo doesn't have yet. The changesets + // workflow runs on every push, so without this note the first thing a new + // repo does is fail a job for a reason that's only explained in a YAML + // comment. Say it where someone will actually read it. + if (cfg.workflows?.includes('npm-publish')) { + const changesets = cfg.release === 'changesets'; + lines.push( + '## Releasing', + '', + changesets + ? 'Releases are handled by the `Release` workflow: push a changeset (`npx changeset`) and it opens a version PR, then publishes when that PR merges.' + : 'Pushing a `v*` tag triggers the `Publish` workflow, which publishes to npm with provenance.', + '', + '**Publishing needs npm credentials, which a new repository does not have.** Either:', + '', + '- Set up [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC) — no secret to store or rotate, and the recommended option; or', + '- Add an [npm automation token](https://docs.npmjs.com/creating-and-viewing-access-tokens) as an `NPM_TOKEN` repository secret.', + '', + changesets + ? 'Until one of those is in place the `Release` workflow will fail with `ENEEDAUTH` on every push. That is expected on a brand-new repo.' + : 'Until one of those is in place, tag pushes will fail with `ENEEDAUTH`.', + '', + ); + } + lines.push('## License', '', cfg.license === 'none' ? 'Unlicensed.' : `${cfg.license}${cfg.author ? ' © ' + cfg.author : ''}`, ''); return lines.join('\n'); } diff --git a/test/scaffold.test.js b/test/scaffold.test.js index c2b7779..b41cf9d 100644 --- a/test/scaffold.test.js +++ b/test/scaffold.test.js @@ -4,6 +4,7 @@ import { mkdtemp, mkdir, writeFile, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { writeProject, existingEntries, dirIsEmptyOrMissing, writeLockfile } from '../src/cli/write.js'; +import { generate, fromPreset } from '../src/core/index.js'; const tmp = () => mkdtemp(join(tmpdir(), 'packkit-')); @@ -61,3 +62,16 @@ test('without merge every file is written, nested dirs created', async () => { assert.equal(written.length, 2); assert.equal(await readFile(join(dir, 'a/b/c.txt'), 'utf8'), 'hi'); }); + +test('README documents the npm credential requirement when a publish workflow is included', () => { + const out = generate(fromPreset('ts-lib', { name: 'r1' })); + const readme = out.files['README.md']; + assert.match(readme, /## Releasing/); + assert.match(readme, /Trusted Publishing/); + assert.match(readme, /ENEEDAUTH/, 'names the exact error the user will see'); +}); + +test('README omits the releasing section when nothing publishes', () => { + const out = generate(fromPreset('react-app', { name: 'r2' })); + assert.doesNotMatch(out.files['README.md'], /## Releasing/); +});