From 6741c2324d4034574cedcc2d1724823fab6f25d1 Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sat, 9 Nov 2024 16:38:07 +0000 Subject: [PATCH 1/2] feat: allow manual supplying pat --- README.md | 6 +- src/bin/index.ts | 94 ++++++++++++----------------- src/createPat.ts | 35 ++++++----- src/createUserNpmrc.ts | 2 +- src/parseProjectNpmrc.ts | 15 +++-- src/shared/options/args.ts | 14 ++++- src/shared/options/optionsSchema.ts | 1 + src/writeNpmrc.ts | 10 +-- 8 files changed, 91 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 477470a..25b6516 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ With the above `preinstall` script in place, when the user performs `npm i` or s ## Prerequisites -`ado-npm-auth-lite` requires that you are authenticated with Azure to acquire an Azure DevOps Personal Access Token. To authenticate, run `az login`. [If you need to install the Azure CLI, follow these instructions](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli). It is not necessary to run `az login` if you are already authenticated with Azure. +If you would like `ado-npm-auth-lite` to acquire a token on your behalf, then it requires that your [Azure DevOps organisation is connected with your Azure account / Microsoft Entra ID](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/connect-organization-to-azure-ad?view=azure-devops). Then, assuming you are authenticated with Azure, it can acquire an Azure DevOps Personal Access Token on your behalf. To authenticate, run `az login`. [If you need to install the Azure CLI, follow these instructions](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli). It is not necessary to run `az login` if you are already authenticated with Azure. You might be worried about `ado-npm-auth-lite` trying to create user `.npmrc` files when running CI builds. Happily this does not happen; it detects whether it is running in a CI environment and does **not** create a user `.npmrc` file in that case. @@ -84,7 +84,9 @@ There is an official package named [`ado-npm-auth`](https://github.com/microsoft `-c` | `--config` (`string`): The location of the .npmrc file. Defaults to current directory -`-e` | `--email` (`string`): Allows users to supply an explicit email - if not supplied, will be inferred from git user.config +`-e` | `--email` (`string`): Allows users to supply an explicit email - if not supplied, the example ADO value will be used + +`-p` | `--pat` (`string`): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI `-h` | `--help`: Show help diff --git a/src/bin/index.ts b/src/bin/index.ts index d8b2afb..f7545c3 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -49,6 +49,7 @@ export async function bin(args: string[]) { logLine(); const mappedOptions = { + pat: values.pat, config: values.config, email: values.email, }; @@ -71,7 +72,7 @@ export async function bin(args: string[]) { return StatusCodes.Failure; } - const { config, email } = optionsParseResult.data; + const { config, email, pat } = optionsParseResult.data; // TODO: this will prevent this file from running tests on the server after this - create an override parameter if (ci.isCI) { @@ -84,6 +85,7 @@ export async function bin(args: string[]) { } prompts.log.info(`options: +- pat: ${pat ? "supplied" : "[NONE SUPPLIED - WILL ACQUIRE FROM AZURE API]"} - config: ${config ?? "[NONE SUPPLIED - WILL USE DEFAULT]"} - email: ${email ?? "[NONE SUPPLIED - WILL USE DEFAULT]"}`); @@ -92,70 +94,52 @@ export async function bin(args: string[]) { error: prompts.log.error, }; - const parsedProjectNpmrc = await withSpinner(`Parsing project .npmrc`, () => - parseProjectNpmrc({ - config, - logger, - }), - ); - - if (!parsedProjectNpmrc) { - prompts.cancel(operationMessage("failed")); - prompts.outro(outroPrompts); - - return StatusCodes.Failure; - } - - const pat = await withSpinner( - `Creating Personal Access Token with vso.packaging scope`, - () => createPat({ logger, organisation: parsedProjectNpmrc.organisation }), - ); - // const pat = { - // patToken: { - // token: "123456", - // }, - // }; - - if (!pat) { - prompts.cancel(operationMessage("failed")); - prompts.outro(outroPrompts); + try { + const parsedProjectNpmrc = await withSpinner(`Parsing project .npmrc`, () => + parseProjectNpmrc({ + config, + logger, + }), + ); - return StatusCodes.Failure; - } + const personalAccessToken = pat + ? { + patToken: { + token: pat, + }, + } + : await withSpinner(`Creating Personal Access Token`, () => + createPat({ logger, organisation: parsedProjectNpmrc.organisation }), + ); + + const npmrc = await withSpinner(`Constructing user .npmrc`, () => + Promise.resolve( + createUserNpmrc({ + parsedProjectNpmrc, + email, + logger, + pat: personalAccessToken.patToken.token, + }), + ), + ); - const npmrc = await withSpinner(`Constructing user .npmrc`, () => - Promise.resolve( - createUserNpmrc({ - parsedProjectNpmrc, - email, + await withSpinner(`Writing user .npmrc`, () => + writeNpmrc({ + npmrc, logger, - pat: pat.patToken.token, }), - ), - ); + ); - if (!npmrc) { - prompts.cancel(operationMessage("failed")); prompts.outro(outroPrompts); - return StatusCodes.Failure; - } - - const succeeded = await withSpinner(`Writing user .npmrc`, () => - writeNpmrc({ - npmrc, - logger, - }), - ); - - if (!succeeded) { + return StatusCodes.Success; + } catch (error) { + prompts.log.error( + `Error: ${error instanceof Error && error.cause instanceof Error ? error.cause.message : ""}`, + ); prompts.cancel(operationMessage("failed")); prompts.outro(outroPrompts); return StatusCodes.Failure; } - - prompts.outro(outroPrompts); - - return StatusCodes.Success; } diff --git a/src/createPat.ts b/src/createPat.ts index 94d6779..c5f0d8a 100644 --- a/src/createPat.ts +++ b/src/createPat.ts @@ -1,5 +1,4 @@ import { AzureCliCredential } from "@azure/identity"; -import chalk from "chalk"; import { fromZodError } from "zod-validation-error"; import type { TokenResult } from "./types.js"; @@ -13,8 +12,10 @@ export async function createPat({ }: { logger?: Logger; organisation: string; -}): Promise { +}): Promise { // const credential = new InteractiveBrowserCredential({}); + logger.info(`Creating Azure CLI Token`); + const credential = new AzureCliCredential(); // get a token that can be used to authenticate to Azure DevOps @@ -24,6 +25,8 @@ export async function createPat({ "499b84ac-1321-427f-aa17-267ca6975798", ]); + logger.info(`Created Azure CLI Token`); + // Get the current date const currentDate = new Date(); @@ -32,6 +35,8 @@ export async function createPat({ futureDate.setDate(currentDate.getDate() + 30); try { + logger.info(`Creating Personal Access Token with scope: vso.packaging`); + // https://learn.microsoft.com/en-us/rest/api/azure/devops/tokens/pats/create?view=azure-devops-rest-7.1&tabs=HTTP const url = `https://vssps.dev.azure.com/${organisation}/_apis/tokens/pats?api-version=7.1-preview.1`; const data = { @@ -53,28 +58,30 @@ export async function createPat({ }); if (!response.ok) { - logger.error(`HTTP error! status: ${response.status.toString()}`); - return; + const responseText = await response.text(); + const errorMessage = `HTTP error! status: ${response.status.toString()} - ${responseText}`; + throw new Error(errorMessage); } const tokenParseResult = tokenResultSchema.safeParse(await response.json()); if (!tokenParseResult.success) { - logger.error( - chalk.red( - fromZodError(tokenParseResult.error, { - issueSeparator: "\n - ", - }), - ), - ); + const errorMessage = `Error parsing the token result: ${fromZodError(tokenParseResult.error).message}`; + throw new Error(errorMessage); } logger.info(`Created Personal Access Token`); return tokenParseResult.data; } catch (error) { - logger.error( - `Error creating Personal Access Token: ${error instanceof Error ? error.message : ""}`, - ); + const errorMessage = `Error creating Personal Access Token: +${error instanceof Error ? error.message : JSON.stringify(error)} + +Please ensure that: +1. Your Azure DevOps organisation is connected with your Azure account / Microsoft Entra ID +2. You are logged into the Azure CLI (use \`az login\` to log in) + +If you continue to have issues, consider creating a Personal Access Token with the Packaging read and write scopes manually in Azure DevOps and providing it to \`ado-npm-auth-lite\` using the \`--pat\` option.`; + throw new Error(errorMessage); } } diff --git a/src/createUserNpmrc.ts b/src/createUserNpmrc.ts index ba15c4e..3629a4b 100644 --- a/src/createUserNpmrc.ts +++ b/src/createUserNpmrc.ts @@ -23,7 +23,7 @@ export function createUserNpmrc({ parsedProjectNpmrc: ParsedProjectNpmrc; logger?: Logger; pat: string; -}): string | undefined { +}): string { const base64EncodedPAT = Buffer.from(pat).toString("base64"); const { urlWithoutRegistryAtEnd, urlWithoutRegistryAtStart, organisation } = diff --git a/src/parseProjectNpmrc.ts b/src/parseProjectNpmrc.ts index 1804e7f..c7e9abe 100644 --- a/src/parseProjectNpmrc.ts +++ b/src/parseProjectNpmrc.ts @@ -18,7 +18,7 @@ export async function parseProjectNpmrc({ }: { config?: string | undefined; logger?: Logger; -}): Promise { +}): Promise { const npmrcPath = config ? path.resolve(config) : path.resolve(process.cwd(), ".npmrc"); @@ -28,23 +28,21 @@ export async function parseProjectNpmrc({ const npmrcContents = await readFileSafe(npmrcPath, ""); if (!npmrcContents) { - logger.error(`No .npmrc found at: ${npmrcPath}`); - return; + throw new Error(`No .npmrc found at: ${npmrcPath}`); } const regex = /^registry=.*$/gm; const match = npmrcContents.match(regex); if (!match || match.length === 0) { - logger.error(`Unable to extract information from project .npmrc`); - return; + throw new Error(`Unable to extract information from project .npmrc`); } const urlWithoutRegistryAtStart = match[0] .replace("registry=https:", "") .trim(); const urlWithoutRegistryAtEnd = urlWithoutRegistryAtStart.replace( - /\/registry\/$/, + /registry\/$/, "", ); // extract the organisation which we will use as the username @@ -52,5 +50,10 @@ export async function parseProjectNpmrc({ // defined in ADO const organisation = urlWithoutRegistryAtEnd.split("/")[3]; + logger.info(`Parsed: +- organisation: ${organisation} +- urlWithoutRegistryAtStart: ${urlWithoutRegistryAtStart} +- urlWithoutRegistryAtEnd: ${urlWithoutRegistryAtEnd}`); + return { urlWithoutRegistryAtStart, urlWithoutRegistryAtEnd, organisation }; } diff --git a/src/shared/options/args.ts b/src/shared/options/args.ts index 715012d..98f58d6 100644 --- a/src/shared/options/args.ts +++ b/src/shared/options/args.ts @@ -9,6 +9,11 @@ export const options = { type: "string", }, + pat: { + short: "p", + type: "string", + }, + help: { short: "h", type: "boolean", @@ -43,7 +48,14 @@ export const allArgOptions: Record = { email: { ...options.email, description: - "Allows users to supply an explicit email - if not supplied, will be inferred from git user.config", + "Allows users to supply an explicit email - if not supplied, the example ADO value will be used", + docsSection: "optional", + }, + + pat: { + ...options.pat, + description: + "Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI", docsSection: "optional", }, diff --git a/src/shared/options/optionsSchema.ts b/src/shared/options/optionsSchema.ts index 16f793d..68b1607 100644 --- a/src/shared/options/optionsSchema.ts +++ b/src/shared/options/optionsSchema.ts @@ -1,6 +1,7 @@ import { z } from "zod"; export const optionsSchema = z.object({ + pat: z.string().optional(), config: z.string().optional(), email: z.string().optional(), }); diff --git a/src/writeNpmrc.ts b/src/writeNpmrc.ts index 230b74c..d640bb5 100644 --- a/src/writeNpmrc.ts +++ b/src/writeNpmrc.ts @@ -10,7 +10,7 @@ export async function writeNpmrc({ }: { npmrc: string; logger?: Logger; -}): Promise { +}): Promise { // Get the home directory const homeDirectory = os.homedir(); @@ -23,11 +23,7 @@ export async function writeNpmrc({ // Write the content to the .npmrc file await fs.writeFile(userNpmrcPath, npmrc); } catch (error) { - logger.error( - `Error writing users .npmrc to ${userNpmrcPath}: ${error instanceof Error ? error.message : ""}`, - ); - return false; + const errorMessage = `Error writing users .npmrc to ${userNpmrcPath}: ${error instanceof Error ? error.message : ""}`; + throw new Error(errorMessage); } - - return true; } From f8dbd1af6acbc22610193470e91af605b372333b Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sat, 9 Nov 2024 16:40:08 +0000 Subject: [PATCH 2/2] fix: test --- src/bin/help.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/bin/help.test.ts b/src/bin/help.test.ts index 055a3ef..4bf14c9 100644 --- a/src/bin/help.test.ts +++ b/src/bin/help.test.ts @@ -56,7 +56,11 @@ describe("logHelpText", () => { ], [ " - -e | --email (string): Allows users to supply an explicit email - if not supplied, will be inferred from git user.config", + -e | --email (string): Allows users to supply an explicit email - if not supplied, the example ADO value will be used", + ], + [ + " + -p | --pat (string): Allows users to supply an explicit Personal Access Token (which must include the Packaging read and write scopes) - if not supplied, will be acquired from the Azure CLI", ], [], ]