Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow manual supplying pat #9

Merged
merged 2 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
6 changes: 5 additions & 1 deletion src/bin/help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
[],
]
Expand Down
94 changes: 39 additions & 55 deletions src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function bin(args: string[]) {
logLine();

const mappedOptions = {
pat: values.pat,
config: values.config,
email: values.email,
};
Expand All @@ -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) {
Expand All @@ -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]"}`);

Expand All @@ -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;
}
35 changes: 21 additions & 14 deletions src/createPat.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,8 +12,10 @@ export async function createPat({
}: {
logger?: Logger;
organisation: string;
}): Promise<TokenResult | undefined> {
}): Promise<TokenResult> {
// 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
Expand All @@ -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();

Expand All @@ -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 = {
Expand All @@ -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);
}
}
2 changes: 1 addition & 1 deletion src/createUserNpmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand Down
15 changes: 9 additions & 6 deletions src/parseProjectNpmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function parseProjectNpmrc({
}: {
config?: string | undefined;
logger?: Logger;
}): Promise<ParsedProjectNpmrc | undefined> {
}): Promise<ParsedProjectNpmrc> {
const npmrcPath = config
? path.resolve(config)
: path.resolve(process.cwd(), ".npmrc");
Expand All @@ -28,29 +28,32 @@ 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
// not sure why this is the case, but this is the behaviour
// defined in ADO
const organisation = urlWithoutRegistryAtEnd.split("/")[3];

logger.info(`Parsed:
- organisation: ${organisation}
- urlWithoutRegistryAtStart: ${urlWithoutRegistryAtStart}
- urlWithoutRegistryAtEnd: ${urlWithoutRegistryAtEnd}`);

return { urlWithoutRegistryAtStart, urlWithoutRegistryAtEnd, organisation };
}
14 changes: 13 additions & 1 deletion src/shared/options/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export const options = {
type: "string",
},

pat: {
short: "p",
type: "string",
},

help: {
short: "h",
type: "boolean",
Expand Down Expand Up @@ -43,7 +48,14 @@ export const allArgOptions: Record<ValidOption, DocOption> = {
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",
},

Expand Down
1 change: 1 addition & 0 deletions src/shared/options/optionsSchema.ts
Original file line number Diff line number Diff line change
@@ -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(),
});
10 changes: 3 additions & 7 deletions src/writeNpmrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function writeNpmrc({
}: {
npmrc: string;
logger?: Logger;
}): Promise<boolean> {
}): Promise<void> {
// Get the home directory
const homeDirectory = os.homedir();

Expand All @@ -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;
}
Loading