diff --git a/README.md b/README.md index de6b9045..5a01de45 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ As an added bonus, this plugin will also publish some simple monorepo patterns. - [`package.json` file](#packagejson-file) - [Plugin options](#plugin-options) - [Examples](#examples) + - [Only create package tarball](#only-create-package-tarball) - [Plugin steps](#plugin-steps) - [Development](#development) - [Roadmap](#roadmap) @@ -59,14 +60,25 @@ for example: ## NPM registry authentication -The NPM authentication configuration is **required** and can be set either via -[environment variables](#environment-variables) or the +Providing a NPM access token in your configuration is **required** and can be +set either via [environment variables](#environment-variables) or the [`.yarnrc.yml`](#yarnrcyml-file) file. -> **Note**: when -> [two-factor authentication](https://docs.npmjs.com/configuring-two-factor-authentication) -> is enabled on your NPM account and enabled for writes (default setting), the -> token needs to be of type **Automation**. +Make sure your access token has write access to the package you want to publish: + +- **When using a + [classic/legacy token](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-legacy-tokens-on-the-website)**, + it must be either: + - A "**Publish**" token if you're not using 2FA or if 2FA is disabled for + write operations (The "Require two-factor authentication for write actions" + is unchecked in your 2FA settings) + - An "**Automation**" token if 2FA is enabled for write operations (The + "Require two-factor authentication for write actions" is checked in your 2FA + settings) +- **When using a + [granular access token](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-granular-access-tokens-on-the-website)** + make sure it has "**Read and write**" permissions on the package you want to + publish. > **Note**: only the > [`npmAuthToken`](https://yarnpkg.com/configuration/yarnrc/#npmAuthToken) is @@ -83,7 +95,7 @@ release is due, all workspaces will be published to the NPM registry. Monorepos are detected by the presence of a [`workspaces`](https://yarnpkg.com/configuration/manifest#workspaces) option in -the root `package.json` file: +the root `package.json` file, for example: ```json { @@ -97,10 +109,10 @@ See [our roadmap](#roadmap) for further implementation status. ### Environment variables -| Variable | Description | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `YARN_NPM_AUTH_TOKEN` | [NPM token](https://docs.npmjs.com/creating-and-viewing-access-tokens). Translates to the [npmAuthToken](https://yarnpkg.com/configuration/yarnrc#npmAuthToken) `.yarnrc.yml` option. | -| `YARN_NPM_PUBLISH_REGISTRY` | NPM registry to use. Translates to the [npmPublishRegistry](https://yarnpkg.com/configuration/yarnrc#npmPublishRegistry) `.yarnrc.yml` option. | +| Variable | Description | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `YARN_NPM_AUTH_TOKEN` | [NPM access token](https://docs.npmjs.com/creating-and-viewing-access-tokens). Translates to the [npmAuthToken](https://yarnpkg.com/configuration/yarnrc#npmAuthToken) `.yarnrc.yml` option. | +| `YARN_NPM_PUBLISH_REGISTRY` | NPM registry to use. Translates to the [npmPublishRegistry](https://yarnpkg.com/configuration/yarnrc#npmPublishRegistry) `.yarnrc.yml` option. | Most other Yarn options could be specified as environment variables as well. Just prefix the names and write them in snake case. Refer to the @@ -121,7 +133,7 @@ of option. The [`registry`](https://yarnpkg.com/configuration/manifest#publishConfig.registry) can be configured in the `package.json` and will take precedence over the -configuration in environment variables and the `.yarnrc.yml` file. +configuration in environment variables and the `.yarnrc.yml` file: ```json { @@ -169,6 +181,8 @@ for example: ## Examples +### Only create package tarball + The `npmPublish` and `tarballDir` option can be used to skip the publishing to the NPM registry and instead release the package tarball with another plugin. For example with the diff --git a/src/definitions/constants.ts b/src/definitions/constants.ts index b786fd7d..d103a8bc 100644 --- a/src/definitions/constants.ts +++ b/src/definitions/constants.ts @@ -6,6 +6,8 @@ export const PLUGIN_HOMEPAGE = "https://github.com/hongaar/semantic-release-yarn"; export const PLUGIN_GIT_BRANCH = "main"; +export const YARNRC_FILENAME = ".yarnrc.yml"; + export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org"; export const DEFAULT_YARN_REGISTRY = "https://registry.yarnpkg.com"; diff --git a/src/definitions/errors.ts b/src/definitions/errors.ts index b23ed248..04d6868e 100644 --- a/src/definitions/errors.ts +++ b/src/definitions/errors.ts @@ -6,77 +6,85 @@ function linkify(file: string) { export function EINVALIDNPMPUBLISH({ npmPublish }: { npmPublish: unknown }) { return { - message: "Invalid `npmPublish` option.", + message: 'Invalid "npmPublish" option.', details: `The [npmPublish option](${linkify( - "README.md#npmpublish" - )}) option, if defined, must be a \`Boolean\`. + "README.md#plugin-options" + )}) option, if defined, must be a "Boolean". -Your configuration for the \`npmPublish\` option is \`${npmPublish}\`.`, +Your configuration for the "npmPublish" option is "${npmPublish}".`, }; } export function EINVALIDTARBALLDIR({ tarballDir }: { tarballDir: unknown }) { return { - message: "Invalid `tarballDir` option.", + message: 'Invalid "tarballDir" option.', details: `The [tarballDir option](${linkify( - "README.md#tarballdir" - )}) option, if defined, must be a \`String\`. + "README.md#plugin-options" + )}) option, if defined, must be a "String". -Your configuration for the \`tarballDir\` option is \`${tarballDir}\`.`, +Your configuration for the "tarballDir" option is "${tarballDir}".`, }; } export function EINVALIDPKGROOT({ pkgRoot }: { pkgRoot: unknown }) { return { - message: "Invalid `pkgRoot` option.", + message: 'Invalid "pkgRoot" option.', details: `The [pkgRoot option](${linkify( - "README.md#pkgroot" - )}) option, if defined, must be a \`String\`. + "README.md#plugin-options" + )}) option, if defined, must be a "String". -Your configuration for the \`pkgRoot\` option is \`${pkgRoot}\`.`, +Your configuration for the "pkgRoot" option is "${pkgRoot}".`, }; } export function ENONPMTOKEN({ registry }: { registry: string }) { return { - message: "No npm token specified.", - details: `An [npm token](${linkify( + message: "No NPM access token specified.", + details: `An [NPM access token](${linkify( "README.md#npm-registry-authentication" - )}) must be created and set in the \`YARN_NPM_AUTH_TOKEN\` environment variable on your CI environment. - -Please make sure to create an [npm token](https://docs.npmjs.com/getting-started/working_with_tokens#how-to-create-new-tokens) and to set it in the \`YARN_NPM_AUTH_TOKEN\` environment variable on your CI environment. The token must allow to publish to the registry \`${registry}\`.`, + )}) must be provided in your configuration. The token must allow to publish to the registry "${registry}". + +Please refer to the [npm registry authentication](${linkify( + "README.md#npm-registry-authentication" + )}) section of the README to learn how to configure the NPM registry access token.`, }; } export function EINVALIDNPMTOKEN({ registry }: { registry: string }) { return { - message: "Invalid npm token.", - details: `The [npm token](${linkify( + message: "Invalid NPM access token.", + details: `The [NPM access token](${linkify( "README.md#npm-registry-authentication" - )}) configured in the \`YARN_NPM_AUTH_TOKEN\` environment variable must be a valid [token](https://docs.npmjs.com/getting-started/working_with_tokens) allowing to publish to the registry \`${registry}\`. - -If you are using Two Factor Authentication for your account, set its level to ["Authorization only"](https://docs.npmjs.com/getting-started/using-two-factor-authentication#levels-of-authentication) in your account settings. **semantic-release** cannot publish with the default " -Authorization and writes" level. - -Please make sure to set the \`YARN_NPM_AUTH_TOKEN\` environment variable in your CI with the exact value of the npm token.`, + )}) configured must be a valid [access token](https://docs.npmjs.com/getting-started/working_with_tokens) allowing to publish to the registry "${registry}". + +Please refer to the [npm registry authentication](${linkify( + "README.md#npm-registry-authentication" + )}) section of the README to learn how to configure the NPM registry access token.`, }; } export function ENOPKGNAME() { return { - message: "Missing `name` property in `package.json`.", - details: `The \`package.json\`'s [name](https://docs.npmjs.com/files/package.json#name) property is required in order to publish a package to the npm registry. + message: 'Missing "name" property in "package.json".', + details: `The "package.json"'s [name](https://docs.npmjs.com/files/package.json#name) property is required in order to publish a package to the registry. -Please make sure to add a valid \`name\` for your package in your \`package.json\`.`, +Please make sure to add a valid "name" for your package in your "package.json".`, }; } export function ENOPKG() { return { - message: "Missing `package.json` file.", - details: `A [package.json file](https://docs.npmjs.com/files/package.json) at the root of your project is required to release on npm. + message: 'Missing "package.json" file.', + details: `A [package.json file](https://docs.npmjs.com/files/package.json) at the root of your project is required to release on NPM. -Please follow the [npm guideline](https://docs.npmjs.com/getting-started/creating-node-modules) to create a valid \`package.json\` file.`, +Please follow the [npm guideline](https://docs.npmjs.com/getting-started/creating-node-modules) to create a valid "package.json" file.`, + }; +} + +export function ENOYARN() { + return { + message: "Yarn not found.", + details: `The Yarn CLI could not be found in your PATH. Make sure Yarn is installed and try again.`, }; } @@ -87,6 +95,6 @@ export function EINVALIDYARN({ version }: { version: string }) { "README.md#install" )}) to review which versions of Yarn are currently supported -Your version of Yarn is \`${version}\`.`, +Your version of Yarn is "${version}".`, }; } diff --git a/src/get-error.ts b/src/get-error.ts index f35377ce..891cea1f 100644 --- a/src/get-error.ts +++ b/src/get-error.ts @@ -8,10 +8,11 @@ export type ErrorDefinition = Error & { semanticRelease: boolean; }; -export function getError( - code: keyof typeof ERROR_DEFINITIONS, - ctx: any = {} +export function getError( + code: T, + ctx: Parameters[0] ): ErrorDefinition { - const { message, details } = ERROR_DEFINITIONS[code](ctx); + const { message, details } = ERROR_DEFINITIONS[code](ctx as any); + return new SemanticReleaseError(message, code, details); } diff --git a/src/get-pkg.ts b/src/get-pkg.ts index fe268b6f..531f3a10 100644 --- a/src/get-pkg.ts +++ b/src/get-pkg.ts @@ -16,13 +16,13 @@ export async function getPkg( }); if (!pkg.name) { - throw getError("ENOPKGNAME"); + throw getError("ENOPKGNAME", undefined); } return pkg; } catch (error: any) { if (error.code === "ENOENT") { - throw getError("ENOPKG"); + throw getError("ENOPKG", undefined); } throw error; diff --git a/src/get-token.ts b/src/get-token.ts index ec86f6a2..3360d590 100644 --- a/src/get-token.ts +++ b/src/get-token.ts @@ -1,27 +1,49 @@ // @ts-ignore import toNerfDart from "nerf-dart"; +import { YARNRC_FILENAME } from "./definitions/constants.js"; import type { CommonContext } from "./definitions/context.js"; import type { Yarnrc } from "./definitions/yarnrc.js"; export function getToken( registry: string, { npmRegistries, npmAuthToken }: Yarnrc, - { env }: { env?: CommonContext["env"] } + { + env, + logger, + }: { env: CommonContext["env"]; logger: CommonContext["logger"] } ) { - // @todo implement yarnrc.npmScopes - const registryId = toNerfDart(registry); + // @todo implement yarnrc.npmScopes + // Lookup in yarnrc.npmRegistries + // @todo - Verify this does in fact override an auth token set in env var const entry = npmRegistries && Object.entries(npmRegistries).find(([id, { npmAuthToken }]) => { return toNerfDart(id) === registryId && npmAuthToken; }); - if (entry) { + logger.log( + `Using token from "${YARNRC_FILENAME}: npmRegistries["${entry[0]}"].npmAuthToken"` + ); + return npmRegistries[entry[0]]!.npmAuthToken!; } - return env?.["YARN_NPM_AUTH_TOKEN"] || npmAuthToken; + // Return env var if set + if (env["YARN_NPM_AUTH_TOKEN"]) { + logger.log(`Using token from environment variable YARN_NPM_AUTH_TOKEN`); + + return env["YARN_NPM_AUTH_TOKEN"]; + } + + // Return yarnrc.npmAuthToken if set + if (npmAuthToken) { + logger.log(`Using token from "${YARNRC_FILENAME}: npmAuthToken"`); + + return npmAuthToken; + } + + return; } diff --git a/src/get-yarn-config.ts b/src/get-yarn-config.ts index f0ac8bd5..e51cb757 100644 --- a/src/get-yarn-config.ts +++ b/src/get-yarn-config.ts @@ -1,6 +1,7 @@ import { cosmiconfig } from "cosmiconfig"; import _ from "lodash"; import { dirname, resolve } from "node:path"; +import { YARNRC_FILENAME } from "./definitions/constants.js"; import type { CommonContext } from "./definitions/context.js"; import type { Yarnrc } from "./definitions/yarnrc.js"; @@ -13,7 +14,7 @@ export async function getYarnConfig({ }): Promise { const result = await cosmiconfigSearchRecursive( cosmiconfig("yarn", { - searchPlaces: [".yarnrc.yml"], + searchPlaces: [YARNRC_FILENAME], }), cwd ); diff --git a/src/get-yarn-version.ts b/src/get-yarn-version.ts index 412a591b..d173532c 100644 --- a/src/get-yarn-version.ts +++ b/src/get-yarn-version.ts @@ -1,5 +1,6 @@ import { getImplementation } from "./container.js"; import type { CommonContext } from "./definitions/context.js"; +import { getError } from "./get-error.js"; export async function getYarnVersion({ cwd }: { cwd: CommonContext["cwd"] }) { const execa = await getImplementation("execa"); @@ -7,7 +8,7 @@ export async function getYarnVersion({ cwd }: { cwd: CommonContext["cwd"] }) { try { return (await execa("yarn", ["--version"], { cwd })).stdout; } catch { - throw new Error("Could not determine Yarn version. Is Yarn installed?"); + throw getError("ENOYARN", undefined); } } diff --git a/src/prepare.ts b/src/prepare.ts index f5bfd6ca..1096ba99 100644 --- a/src/prepare.ts +++ b/src/prepare.ts @@ -4,55 +4,41 @@ import type { PackageJson } from "read-pkg"; import { getImplementation } from "./container.js"; import type { PrepareContext } from "./definitions/context.js"; import type { PluginConfig } from "./definitions/pluginConfig.js"; +import { installYarnPluginIfNeeded } from "./yarn-plugins.js"; const TARBALL_FILENAME = "%s-%v.tgz"; export async function prepare( { tarballDir, pkgRoot }: PluginConfig, pkg: PackageJson, - { cwd, env, stdout, stderr, nextRelease: { version }, logger }: PrepareContext + context: PrepareContext ) { - const basePath = pkgRoot ? resolve(cwd, String(pkgRoot)) : cwd; + const { + cwd, + env, + stdout, + stderr, + nextRelease: { version }, + logger, + } = context; const execa = await getImplementation("execa"); + + const basePath = pkgRoot ? resolve(cwd, String(pkgRoot)) : cwd; const isMonorepo = typeof pkg.workspaces !== "undefined"; const workspacesPrefix = isMonorepo ? ["workspaces", "foreach", "--topological", "--verbose", "--no-private"] : []; if (isMonorepo) { - logger.log("Installing Yarn workspace-tools plugin in %s", basePath); - const pluginImportResult = execa( - "yarn", - ["plugin", "import", "workspace-tools"], - { - cwd: basePath, - env, - } - ); - pluginImportResult.stdout!.pipe(stdout, { end: false }); - pluginImportResult.stderr!.pipe(stderr, { end: false }); - await pluginImportResult; - - logger.log("Running `yarn install` in %s", basePath); - const yarnInstallResult = execa("yarn", ["install", "--no-immutable"], { + await installYarnPluginIfNeeded("workspace-tools", { + ...context, cwd: basePath, - env, }); - yarnInstallResult.stdout!.pipe(stdout, { end: false }); - yarnInstallResult.stderr!.pipe(stderr, { end: false }); - await yarnInstallResult; } - logger.log("Installing Yarn version plugin in %s", basePath); - const pluginImportResult = execa("yarn", ["plugin", "import", "version"], { - cwd: basePath, - env, - }); - pluginImportResult.stdout!.pipe(stdout, { end: false }); - pluginImportResult.stderr!.pipe(stderr, { end: false }); - await pluginImportResult; + await installYarnPluginIfNeeded("version", { ...context, cwd: basePath }); - logger.log("Write version %s to package.json in %s", version, basePath); + logger.log('Write version "%s" to package.json in "%s"', version, basePath); const versionResult = execa( "yarn", [...workspacesPrefix, "version", version, "--immediate"], @@ -66,7 +52,7 @@ export async function prepare( await versionResult; if (tarballDir) { - logger.log("Creating package tarball in %s", tarballDir); + logger.log('Creating package tarball in "%s"', tarballDir); await fs.ensureDir(resolve(cwd, tarballDir.trim())); const packResult = execa( "yarn", diff --git a/src/publish.ts b/src/publish.ts index 4bc5cdbf..e68dd8ab 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -35,30 +35,6 @@ export async function publish( ? ["workspaces", "foreach", "--topological", "--verbose", "--no-private"] : []; - if (isMonorepo) { - logger.log("Installing Yarn workspace-tools plugin in %s", basePath); - const pluginImportResult = execa( - "yarn", - ["plugin", "import", "workspace-tools"], - { - cwd: basePath, - env, - } - ); - pluginImportResult.stdout!.pipe(stdout, { end: false }); - pluginImportResult.stderr!.pipe(stderr, { end: false }); - await pluginImportResult; - - logger.log("Running `yarn install` in %s", basePath); - const yarnInstallResult = execa("yarn", ["install", "--no-immutable"], { - cwd: basePath, - env, - }); - yarnInstallResult.stdout!.pipe(stdout, { end: false }); - yarnInstallResult.stderr!.pipe(stderr, { end: false }); - await yarnInstallResult; - } - logger.log( `Publishing version ${version} to npm registry ${registry} on dist-tag ${distTag}` ); @@ -75,7 +51,7 @@ export async function publish( await result; logger.log( - `Published ${pkg.name}@${version} to dist-tag @${distTag} on ${registry}` + `Published ${pkg.name}@${version} on ${registry} (with tag @${distTag})` ); return getReleaseInfo(pkg, context, distTag, registry); diff --git a/src/verify-auth.ts b/src/verify-auth.ts index 19564cd5..abfc9c4d 100644 --- a/src/verify-auth.ts +++ b/src/verify-auth.ts @@ -19,8 +19,6 @@ export async function verifyAuth( const execa = await getImplementation("execa"); const AggregateError = await getImplementation("AggregateError"); - logger.log("Verify authentication"); - const yarnrc = await getYarnConfig(context); const registry = getRegistry(pkg, yarnrc, context); const token = getToken(registry, yarnrc, context); @@ -30,10 +28,18 @@ export async function verifyAuth( } if (!isDefaultRegistry(registry)) { + logger.log( + `Skipping authentication verification for non-default registry "${registry}"` + ); + return; } try { + logger.log( + `Running "yarn npm whoami --publish" to verify authentication on registry "${registry}"` + ); + // @todo deal with npm npm whoami --scope if pkg is scoped const whoamiResult = execa("yarn", ["npm", "whoami", "--publish"], { cwd: basePath, diff --git a/src/verify-yarn.ts b/src/verify-yarn.ts index 139a68ca..fd3abfaf 100644 --- a/src/verify-yarn.ts +++ b/src/verify-yarn.ts @@ -9,13 +9,13 @@ export async function verifyYarn(context: CommonContext) { const { logger } = context; const AggregateError = await getImplementation("AggregateError"); - logger.log("Verify yarn version"); + logger.log(`Verify yarn version is >= ${MIN_YARN_VERSION}`); const yarnMajorVersion = await getYarnMajorVersion(context); if (yarnMajorVersion < MIN_YARN_VERSION) { throw new AggregateError([ - getError("EINVALIDYARN", { version: yarnMajorVersion }), + getError("EINVALIDYARN", { version: String(yarnMajorVersion) }), ]); } } diff --git a/src/yarn-plugins.ts b/src/yarn-plugins.ts new file mode 100644 index 00000000..79072a33 --- /dev/null +++ b/src/yarn-plugins.ts @@ -0,0 +1,48 @@ +import { getImplementation } from "./container.js"; +import type { CommonContext } from "./definitions/context.js"; + +export async function installYarnPluginIfNeeded( + name: string, + { cwd, env, logger, stdout, stderr }: CommonContext +) { + const execa = await getImplementation("execa"); + + const plugins = await getYarnPlugins({ cwd }); + + if (!plugins.includes(name)) { + logger.log('Installing Yarn "%s" plugin in "%s"', name, cwd); + + const pluginImportResult = execa("yarn", ["plugin", "import", name], { + cwd, + env, + }); + pluginImportResult.stdout!.pipe(stdout, { end: false }); + pluginImportResult.stderr!.pipe(stderr, { end: false }); + await pluginImportResult; + + return true; + } + + return false; +} +export async function getYarnPlugins({ cwd }: { cwd: CommonContext["cwd"] }) { + const execa = await getImplementation("execa"); + + const { stdout } = await execa("yarn", ["plugin", "runtime", "--json"], { + cwd, + }); + + return stdout.split("\n").reduce((acc, line) => { + try { + const { name, builtin } = JSON.parse(line); + + if (!builtin && name !== "@@core") { + return [...acc, name.replace("@yarnpkg/plugin-", "")]; + } + } catch { + // ignore + } + + return acc; + }, [] as string[]); +} diff --git a/test/get-token.test.ts b/test/get-token.test.ts index 74e6ada6..c4a6f89a 100644 --- a/test/get-token.test.ts +++ b/test/get-token.test.ts @@ -1,19 +1,25 @@ import test from "ava"; import { getToken } from "../src/get-token.js"; +import { createContext } from "./helpers/create-context.js"; test("Get token from npmAuthToken", (t) => { + const context = createContext(); + t.is( - getToken("https://registry.npmjs.org", { npmAuthToken: "token" }, {}), + getToken("https://registry.npmjs.org", { npmAuthToken: "token" }, context), "token" ); }); test("Get token from environment variable", (t) => { + const context = createContext(); + t.is( getToken( "https://registry.npmjs.org", {}, { + ...context, env: { YARN_NPM_AUTH_TOKEN: "token", }, @@ -24,6 +30,8 @@ test("Get token from environment variable", (t) => { }); test("Get token from registries list", (t) => { + const context = createContext(); + t.is( getToken( "https://registry.npmjs.org", @@ -34,13 +42,15 @@ test("Get token from registries list", (t) => { }, }, }, - {} + context ), "token" ); }); test("Precedence: registries list > environment variable > npmAuthToken", (t) => { + const context = createContext(); + t.is( getToken( "https://registry.npmjs.org", @@ -53,6 +63,7 @@ test("Precedence: registries list > environment variable > npmAuthToken", (t) => }, }, { + ...context, env: { YARN_NPM_AUTH_TOKEN: "token3", }, @@ -67,6 +78,7 @@ test("Precedence: registries list > environment variable > npmAuthToken", (t) => npmAuthToken: "token1", }, { + ...context, env: { YARN_NPM_AUTH_TOKEN: "token3", }, diff --git a/test/get-yarn-version.test.ts b/test/get-yarn-version.test.ts index b9f0ecee..0db1d4c6 100644 --- a/test/get-yarn-version.test.ts +++ b/test/get-yarn-version.test.ts @@ -24,8 +24,8 @@ test.serial("Yarn not installed", async (t) => { const error = await t.throwsAsync(getYarnVersion(context)); - t.is(error.name, "Error"); - t.is(error.message, "Could not determine Yarn version. Is Yarn installed?"); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "ENOYARN"); }); test.serial("Yarn with empty output", async (t) => { diff --git a/test/integration.test.ts b/test/integration.test.ts index 36d58035..fc7d6708 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -1,4 +1,5 @@ import test from "ava"; +import { execa } from "execa"; import fs from "fs-extra"; import { resolve } from "node:path"; import { defaultRegistries } from "../src/definitions/constants.js"; @@ -106,7 +107,6 @@ test("Throws error if NPM token is invalid", async (t) => { t.is(error.name, "SemanticReleaseError"); t.is(error.code, "EINVALIDNPMTOKEN"); - t.is(error.message, "Invalid npm token."); }); test("Skip auth validation if the registry configured is not the default one", async (t) => { @@ -894,6 +894,7 @@ test("Publish monorepo packages", async (t) => { version: "0.0.0-dev", publishConfig: { registry: url }, }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); const result = await mod.publish( {}, @@ -953,6 +954,7 @@ test("Publish non-private monorepo packages", async (t) => { version: "0.0.0-dev", publishConfig: { registry: url }, }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); const result = await mod.publish( {}, @@ -1029,6 +1031,7 @@ test("Publish monorepo packages on a dist-tag", async (t) => { version: "0.0.0-dev", publishConfig: { registry: url }, }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); const result = await mod.publish( {}, @@ -1104,6 +1107,7 @@ test.failing( version: "0.0.0-dev", publishConfig: { registry: url }, }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); await mod.publish( {}, @@ -1175,6 +1179,7 @@ test.failing("Publish monorepo packages and add to lts dist-tag", async (t) => { version: "0.0.0-dev", publishConfig: { registry: url }, }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); await mod.publish( {}, diff --git a/test/prepare.test.ts b/test/prepare.test.ts index 557fd342..0befc057 100644 --- a/test/prepare.test.ts +++ b/test/prepare.test.ts @@ -1,4 +1,5 @@ import test from "ava"; +import { execa } from "execa"; import fs from "fs-extra"; import { resolve } from "node:path"; import { prepare } from "../src/prepare.js"; @@ -24,13 +25,14 @@ test("Update package.json", async (t) => { // Verify the logger has been called with the version plugin call t.deepEqual(context.logger.log.args[0], [ - "Installing Yarn version plugin in %s", + 'Installing Yarn "%s" plugin in "%s"', + "version", cwd, ]); // Verify the logger has been called with the version updated t.deepEqual(context.logger.log.args[1], [ - "Write version %s to package.json in %s", + 'Write version "%s" to package.json in "%s"', "1.0.0", cwd, ]); @@ -58,7 +60,7 @@ test("Update package.json in a sub-directory", async (t) => { // Verify the logger has been called with the version updated t.deepEqual(context.logger.log.args[1], [ - "Write version %s to package.json in %s", + 'Write version "%s" to package.json in "%s"', "1.0.0", resolve(cwd, pkgRoot), ]); @@ -85,7 +87,7 @@ test("Create the package tarball", async (t) => { // Verify the logger has been called with the version updated t.deepEqual(context.logger.log.args[1], [ - "Write version %s to package.json in %s", + 'Write version "%s" to package.json in "%s"', "1.0.0", cwd, ]); @@ -95,7 +97,7 @@ test("Create the package tarball", async (t) => { // Verify the logger has been called with the version updated t.deepEqual(context.logger.log.args[2], [ - "Creating package tarball in %s", + 'Creating package tarball in "%s"', "tarball", ]); }); @@ -120,7 +122,7 @@ test("Create the package tarball in the current directory", async (t) => { // Verify the logger has been called with the version updated t.deepEqual(context.logger.log.args[2], [ - "Creating package tarball in %s", + 'Creating package tarball in "%s"', ".", ]); }); @@ -164,6 +166,7 @@ test("Update monorepo package.json files", async (t) => { name: "workspace-b", version: "0.0.0-dev", }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); await prepare({}, rootPkg, { ...context, @@ -182,25 +185,22 @@ test("Update monorepo package.json files", async (t) => { // Verify the logger has been called with the workspace-tools plugin call t.deepEqual(context.logger.log.args[0], [ - "Installing Yarn workspace-tools plugin in %s", - cwd, - ]); - - // Verify the logger has been called with the yarn install call - t.deepEqual(context.logger.log.args[1], [ - "Running `yarn install` in %s", + 'Installing Yarn "%s" plugin in "%s"', + "workspace-tools", cwd, ]); // Verify the logger has been called with the version plugin call - t.deepEqual(context.logger.log.args[2], [ - "Installing Yarn version plugin in %s", + // Verify the logger has been called with the workspace-tools plugin call + t.deepEqual(context.logger.log.args[1], [ + 'Installing Yarn "%s" plugin in "%s"', + "version", cwd, ]); // Verify the logger has been called with the version updated - t.deepEqual(context.logger.log.args[3], [ - "Write version %s to package.json in %s", + t.deepEqual(context.logger.log.args[2], [ + 'Write version "%s" to package.json in "%s"', "1.0.0", cwd, ]); @@ -225,6 +225,7 @@ test("Create the monorepo package tarballs", async (t) => { name: "workspace-b", version: "0.0.0-dev", }); + await execa("yarn", ["install", "--no-immutable"], { cwd }); await prepare({ tarballDir: "." }, rootPkg, { ...context, @@ -239,8 +240,8 @@ test("Create the monorepo package tarballs", async (t) => { t.is(await fs.pathExists(resolve(cwd, "workspace-b-1.0.0.tgz")), true); // Verify the logger has been called with the version updated - t.deepEqual(context.logger.log.args[4], [ - "Creating package tarball in %s", + t.deepEqual(context.logger.log.args[3], [ + 'Creating package tarball in "%s"', ".", ]); }); diff --git a/test/yarn-plugins.test.ts b/test/yarn-plugins.test.ts new file mode 100644 index 00000000..61cef6dd --- /dev/null +++ b/test/yarn-plugins.test.ts @@ -0,0 +1,42 @@ +import test from "ava"; +import { + getYarnPlugins, + installYarnPluginIfNeeded, +} from "../src/yarn-plugins.js"; +import { createContext } from "./helpers/create-context.js"; +import { mockExeca } from "./helpers/create-execa-implementation.js"; + +test.serial("getYarnPlugins", async (t) => { + const context = createContext(); + + mockExeca({ + stdout: `{"name":"@@core","builtin":false} +{"name":"@yarnpkg/plugin-essentials","builtin":true} +{"name":"@yarnpkg/plugin-compat","builtin":true} +{"name":"@yarnpkg/plugin-pnp","builtin":true} +{"name":"@yarnpkg/plugin-pnpm","builtin":true} +{"name":"@yarnpkg/plugin-interactive-tools","builtin":false} +{"name":"@yarnpkg/plugin-typescript","builtin":false} +{"name":"@yarnpkg/plugin-version","builtin":false}`, + }); + + const plugins = await getYarnPlugins(context); + + t.deepEqual(plugins, ["interactive-tools", "typescript", "version"]); +}); + +test.serial("installYarnPluginIfNeeded when needed", async (t) => { + const context = createContext(); + + mockExeca({ stdout: "" }); + + t.true(await installYarnPluginIfNeeded("version", context)); +}); + +test.serial("installYarnPluginIfNeeded when not needed", async (t) => { + const context = createContext(); + + mockExeca({ stdout: '{"name":"@yarnpkg/plugin-version","builtin":false}' }); + + t.false(await installYarnPluginIfNeeded("version", context)); +});