Skip to content
Open
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
17 changes: 17 additions & 0 deletions packages/ado-npm-auth/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,31 @@ export interface Args {
doValidCheck: boolean;
skipAuth: boolean;
configFile?: string;
help: boolean;
deviceCode: boolean;
}

export function printHelp() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm working on a change #31 to add a config file support.
I'm planning to leverage yargs to do the argument parsing.
Just sharing we'll likely hit a merge conflict. If you happen to be the one you'll know what PR to look at :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the heads up, i have no problem with rebasing and reworking this to rely on yargs.

console.log(`
Usage:

-h --help Show this
--skip-auth Don't authenticate
--skip-check Don't check whether auth is still valid
--device-code Use device code flow for authentication
`)
}

export function parseArgs(args: string[]): Args {
const doValidCheck = !args.includes("--skip-check");
const skipAuth = args.includes("--skip-auth");
const help = args.includes('--help') || args.includes('-h');
const deviceCode = args.includes('--device-code');

return {
doValidCheck,
skipAuth,
help,
deviceCode,
};
}
35 changes: 23 additions & 12 deletions packages/ado-npm-auth/src/azureauth/ado.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test, vi, beforeEach } from "vitest";
import { AdoPatResponse, adoPat } from "./ado.js";
import { exec } from "../utils/exec.js";
import { spawn } from "../utils/exec.js";
import { spawnSync } from "child_process";
import * as utils from "../utils/is-wsl.js";

Expand All @@ -21,7 +21,7 @@ vi.mock("./is-azureauth-installed.js", async () => {

vi.mock("../utils/exec.js", async () => {
return {
exec: vi.fn(),
spawn: vi.fn(),
};
});

Expand All @@ -43,7 +43,7 @@ beforeEach(() => {

test("it should spawn azureauth", async () => {
vi.mocked(utils.isWsl).mockReturnValue(false);
vi.mocked(exec).mockReturnValue(
vi.mocked(spawn).mockReturnValue(
Promise.resolve({
stdout: '{ "token": "foobarabc123" }',
stderr: "",
Expand All @@ -60,8 +60,24 @@ test("it should spawn azureauth", async () => {
timeout: "200",
})) as AdoPatResponse;

expect(exec).toHaveBeenCalledWith(
'npm exec --silent --yes azureauth -- ado pat --prompt-hint "hint" --organization org --display-name test display --scope foobar --output json --domain baz.com --timeout 200',
expect(spawn).toHaveBeenCalledWith(
"npm",
[
"exec",
"--silent",
"--yes",
"azureauth",
"--",
"ado",
"pat",
'--prompt-hint "hint"',
"--organization org",
"--display-name test display",
"--scope foobar",
"--output json",
"--domain baz.com",
"--timeout 200",
],
expect.anything(),
);
expect(results.token).toBe("foobarabc123");
Expand Down Expand Up @@ -108,7 +124,7 @@ test("it should spawnSync azureauth on wsl", async () => {

test("it should handle json errors", async () => {
vi.mocked(utils.isWsl).mockReturnValue(false);
vi.mocked(exec).mockReturnValue(
vi.mocked(spawn).mockReturnValue(
Promise.resolve({
stdout: "an error",
stderr: "",
Expand All @@ -130,12 +146,7 @@ test("it should handle json errors", async () => {

test("it should handle errors from azureauth-cli", async () => {
vi.mocked(utils.isWsl).mockReturnValue(false);
vi.mocked(exec).mockReturnValue(
Promise.resolve({
stdout: "",
stderr: "an error",
}) as any,
);
vi.mocked(spawn).mockReturnValue(Promise.reject(new Error("an error")));

await expect(
adoPat({
Expand Down
14 changes: 7 additions & 7 deletions packages/ado-npm-auth/src/azureauth/ado.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { arch, platform } from "os";
import { exec } from "../utils/exec.js";
import { spawn } from "../utils/exec.js";
import { isSupportedPlatformAndArchitecture } from "./is-supported-platform-and-architecture.js";
import { azureAuthCommand } from "./azureauth-command.js";
import { isWsl } from "../utils/is-wsl.js";
Expand Down Expand Up @@ -47,7 +47,7 @@ export const adoPat = async (
...authCommand,
`ado`,
`pat`,
`--prompt-hint ${isWsl() ? options.promptHint : `"${options.promptHint}"`}`, // We only use spawn for WSL. spawn does not does not require prompt hint to be wrapped in quotes. exec does.
`--prompt-hint ${isWsl() ? options.promptHint : `"${options.promptHint}"`}`, // Prompt hint may contain spaces so wrap in quotes.
`--organization ${options.organization}`,
`--display-name ${options.displayName}`,
...options.scope.map((scope) => `--scope ${scope}`),
Expand Down Expand Up @@ -85,11 +85,11 @@ export const adoPat = async (
}
} else {
try {
result = await exec(command.join(" "), { env });

if (result.stderr) {
throw new Error(result.stderr);
}
result = await spawn(command[0], command.slice(1), {
shell: false,
env,
stdio: ["ignore", "pipe", "inherit"],
});
} catch (error: any) {
throw new Error(
`Failed to get Ado Pat from npx AzureAuth: ${error.message}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { isWsl } from "../utils/is-wsl.js";
export const isSupportedPlatformAndArchitecture = (): boolean => {
const supportedPlatformsAndArchitectures: Record<string, string[]> = {
win32: ["x64"],
linux: ["x64", "arm64"],
darwin: ["x64", "arm64"],
};

Expand Down
8 changes: 6 additions & 2 deletions packages/ado-npm-auth/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { isSupportedPlatformAndArchitecture } from "./azureauth/is-supported-pla
import { isCodespaces } from "./utils/is-codespaces.js";
import { logTelemetry } from "./telemetry/index.js";
import { arch, platform } from "os";
import { Args, parseArgs } from "./args.js";
import { Args, parseArgs, printHelp } from "./args.js";
import { NpmrcFileProvider } from "./npmrc/npmrcFileProvider.js";
import { defaultEmail, defaultUser, ValidatedFeed } from "./fileProvider.js";
import { generateNpmrcPat } from "./npmrc/generate-npmrc-pat.js";
Expand Down Expand Up @@ -54,7 +54,7 @@ export const run = async (args: Args): Promise<null | boolean> => {
// get a token for each feed
const organizationPatMap: Record<string, string> = {};
for (const adoOrg of adoOrgs) {
organizationPatMap[adoOrg] = await generateNpmrcPat(adoOrg, false);
organizationPatMap[adoOrg] = await generateNpmrcPat(adoOrg, false, args.deviceCode);
}

// Update the pat in the invalid feeds.
Expand Down Expand Up @@ -115,6 +115,10 @@ if (!isSupportedPlatformAndArchitecture()) {
}

const args = parseArgs(process.argv);
if (args.help) {
printHelp();
process.exit(0)
}

const result = await run(args);

Expand Down
3 changes: 3 additions & 0 deletions packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ import { toBase64 } from "../utils/encoding.js";
export const generateNpmrcPat = async (
organization: string,
encode = false,
devicecode = false,
): Promise<string> => {
const name = `${hostname()}-${organization}`;
const mode = devicecode ? "devicecode" : "";
const pat = await adoPat({
promptHint: `${name} .npmrc PAT`,
organization,
displayName: `${name}-npmrc-pat`,
scope: ["vso.packaging"],
timeout: "30",
output: "json",
mode,
});

const rawToken = (pat as AdoPatResponse).token;
Expand Down
25 changes: 24 additions & 1 deletion packages/ado-npm-auth/src/utils/exec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
import { exec as _exec } from "node:child_process";
import { exec as _exec, spawn as _spawn } from "node:child_process";
import { promisify } from "node:util";

export const exec = promisify(_exec);
export const spawn = (
cmd: string,
args: ReadonlyArray<string>,
opts: Object,
): Promise<{ stdout: string }> => {
let result = _spawn(cmd, args, opts);
return new Promise((resolve, reject) => {
let stdout = "";
result.stdout.on("data", (data) => {
stdout += data;
});
result.on("close", (code) => {
if (code == 0) {
resolve({ stdout });
} else {
reject(new Error(`process exited with error code: ${code}`));
}
});
result.on("error", (err) => {
reject(err);
});
});
};
25 changes: 19 additions & 6 deletions packages/node-azureauth/scripts/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import fs from "fs";
import { DownloaderHelper } from "node-downloader-helper";
import decompress from "decompress";
import { fileURLToPath } from "url";
import { promisify } from "node:util";
import { execFile as _execFile } from "node:child_process";
const execFile = promisify(_execFile);

const __dirname = fileURLToPath(new URL(".", import.meta.url));
const AZURE_AUTH_VERSION = "0.8.4";
Expand Down Expand Up @@ -38,7 +41,7 @@ const AZUREAUTH_INFO = {
const AZUREAUTH_NAME_MAP = {
def: "azureauth",
win32: "azureauth.exe",
linux: "azureauth.exe",
linux: "azureauth",
};

export const AZUREAUTH_NAME =
Expand Down Expand Up @@ -75,10 +78,10 @@ export const install = async () => {
arm64: `azureauth-${AZUREAUTH_INFO.version}-osx-arm64.tar.gz`,
},
// TODO: support linux when the binaries are available
// linux: {
// def: "azureauth.exe",
// x64: "azureauth-${AZUREAUTH_INFO.version}-win10-x64.zip",
// },
linux: {
x64: `azureauth-${AZUREAUTH_INFO.version}-linux-x64.deb`,
arm64: `azureauth-${AZUREAUTH_INFO.version}-linux-arm64.deb`,
},
};
if (platform in DOWNLOAD_MAP) {
// download the executable
Expand Down Expand Up @@ -107,7 +110,17 @@ export const install = async () => {

const binaryPath = path.join(distPath, AZUREAUTH_NAME);

await decompress(archivePath, distPath);
if (platform == "linux") {
try {
await execFile("dpkg-deb", ["--extract", archivePath, distPath])
} catch (err) {
fs.unlinkSync(archivePath);
throw err;
}
fs.symlinkSync("usr/lib/azureauth/azureauth", binaryPath)
} else {
await decompress(archivePath, distPath);
}

if (fileExist(binaryPath)) {
fs.chmodSync(binaryPath, fs.constants.S_IXUSR || 0o100);
Expand Down