Skip to content

Commit 2cd9009

Browse files
Mackinnon BuckCopilot
andcommitted
Add Azure Artifacts npm auth refresh
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent a67956b commit 2cd9009

6 files changed

Lines changed: 379 additions & 0 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
docs/.validation/
44
.DS_Store
55

6+
# Generated by `npm run auth:refresh` for local Azure Artifacts routing.
7+
/nodejs/.npmrc
8+
/test/harness/.npmrc
9+
/java/scripts/codegen/.npmrc
10+
611

712
# Visual Studio
813
.vs/

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ We are generally **not** looking for:
3333
- Additional documentation
3434
- **SDKs for other languages** — if you want to create a Copilot SDK for another language, we'd love to hear from you and may offer to link to your SDK from our repo. However we do not plan to add further language-specific SDKs to this repo in the short term, since we need to retain our maintenance capacity for moving forwards quickly with the existing language set. For other languages, please consider running your own external project.
3535

36+
## Microsoft Contributor Setup
37+
38+
Microsoft contributors who need recent builds of `@github`-scoped packages from the internal Azure Artifacts feed should run this command from `nodejs`:
39+
40+
```bash
41+
npm run auth:refresh
42+
```
43+
44+
The command generates scoped registry configurations at `nodejs/.npmrc`, `test/harness/.npmrc`, and `java/scripts/codegen/.npmrc`. Each configuration routes only the `@github` scope through the `copilot-canary` feed's `@Local` view, so you can then use the normal dependency installation commands. Credentials remain in your user-level npm configuration rather than in project files. On Windows, the command uses `vsts-npm-auth`; on Linux and macOS, it uses the Microsoft Azure Artifacts npm credential provider.
45+
46+
Run `npm run auth:refresh` again after an Azure Artifacts 401 or 403 response. To return to public registry behavior, delete the three generated `.npmrc` files. Public contributors do not need this setup and are unaffected.
47+
3648
## Developing an SDK
3749

3850
Setup, build, and test instructions are maintained with each SDK:

nodejs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"type": "module",
3434
"scripts": {
35+
"auth:refresh": "node ../scripts/npm-auth-refresh.mjs --run",
3536
"clean": "rimraf --glob dist *.tgz",
3637
"build": "tsx esbuild-copilotsdk-nodejs.ts",
3738
"test": "vitest run",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { spawnSync } from "node:child_process";
2+
import { mkdtemp, mkdir, readFile, rm } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import path from "node:path";
5+
import { fileURLToPath, pathToFileURL } from "node:url";
6+
7+
import { afterEach, describe, expect, it, vi } from "vitest";
8+
9+
import {
10+
azureFeedLocalRegistry,
11+
buildProjectNpmConfig,
12+
cfsRegistry,
13+
credentialProviderRegistry,
14+
getAuthCommands,
15+
getCommandInvocation,
16+
getProjectNpmrcPaths,
17+
main,
18+
refreshNpmAuthentication,
19+
runCommand,
20+
writeProjectNpmConfigs,
21+
} from "../../scripts/npm-auth-refresh.mjs";
22+
23+
const scriptPath = fileURLToPath(new URL("../../scripts/npm-auth-refresh.mjs", import.meta.url));
24+
const temporaryDirectories: string[] = [];
25+
26+
async function createTemporaryNpmrcPaths(): Promise<string[]> {
27+
const repositoryRoot = await mkdtemp(path.join(tmpdir(), "copilot-sdk-npm-auth-"));
28+
temporaryDirectories.push(repositoryRoot);
29+
30+
const directories = [
31+
path.join(repositoryRoot, "nodejs"),
32+
path.join(repositoryRoot, "test", "harness"),
33+
path.join(repositoryRoot, "java", "scripts", "codegen"),
34+
];
35+
await Promise.all(directories.map((directory) => mkdir(directory, { recursive: true })));
36+
return directories.map((directory) => path.join(directory, ".npmrc"));
37+
}
38+
39+
afterEach(async () => {
40+
await Promise.all(
41+
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
42+
);
43+
});
44+
45+
describe("local npm authentication refresh", () => {
46+
it.each(["--help", "-h"])("prints help successfully for %s", (flag) => {
47+
const result = spawnSync(process.execPath, [scriptPath, flag], {
48+
encoding: "utf8",
49+
});
50+
51+
expect(result.status).toBe(0);
52+
expect(result.stdout).toContain("Usage: npm run auth:refresh");
53+
});
54+
55+
it.each([[[]], [["--refresh"]], [["--run", "unexpected"]]])(
56+
"requires the explicit --run argument for %j",
57+
(args) => {
58+
const result = spawnSync(process.execPath, [scriptPath, ...args], {
59+
encoding: "utf8",
60+
});
61+
62+
expect(result.status).toBe(1);
63+
expect(result.stdout).toContain("Usage: npm run auth:refresh");
64+
}
65+
);
66+
67+
it("runs authentication only for --run", () => {
68+
const refresh = vi.fn();
69+
70+
expect(main(["--run"], refresh)).toBe(0);
71+
expect(refresh).toHaveBeenCalledOnce();
72+
});
73+
74+
it("resolves all project configs from the script URL", () => {
75+
const repositoryRoot = path.resolve(path.dirname(scriptPath), "..");
76+
expect(getProjectNpmrcPaths(pathToFileURL(scriptPath).href)).toEqual([
77+
path.join(repositoryRoot, "nodejs", ".npmrc"),
78+
path.join(repositoryRoot, "test", "harness", ".npmrc"),
79+
path.join(repositoryRoot, "java", "scripts", "codegen", ".npmrc"),
80+
]);
81+
});
82+
83+
it("writes only the scoped registry to all three project configs", async () => {
84+
const npmrcPaths = await createTemporaryNpmrcPaths();
85+
86+
writeProjectNpmConfigs(npmrcPaths);
87+
88+
const expected = `@github:registry=${azureFeedLocalRegistry}\n`;
89+
await Promise.all(
90+
npmrcPaths.map(async (npmrcPath) => {
91+
await expect(readFile(npmrcPath, "utf8")).resolves.toBe(expected);
92+
})
93+
);
94+
expect(buildProjectNpmConfig()).not.toMatch(/^registry=/m);
95+
expect(buildProjectNpmConfig()).not.toMatch(/(?:_auth|token|password)/i);
96+
});
97+
98+
it("authenticates once using the nodejs config", () => {
99+
const npmrcPaths = [
100+
"C:\\repo\\nodejs\\.npmrc",
101+
"C:\\repo\\test\\harness\\.npmrc",
102+
"C:\\repo\\java\\scripts\\codegen\\.npmrc",
103+
];
104+
const writer = vi.fn();
105+
const runner = vi.fn();
106+
107+
refreshNpmAuthentication("win32", npmrcPaths, writer, runner);
108+
109+
expect(writer).toHaveBeenCalledOnce();
110+
expect(writer).toHaveBeenCalledWith(npmrcPaths);
111+
expect(runner).toHaveBeenCalledTimes(2);
112+
expect(runner).toHaveBeenLastCalledWith(
113+
"vsts-npm-auth.cmd",
114+
["-config", npmrcPaths[0], "-Force", "-ReadOnly"],
115+
"win32"
116+
);
117+
});
118+
119+
it("uses vsts-npm-auth on Windows", () => {
120+
expect(getAuthCommands("win32", "C:\\repo\\nodejs\\.npmrc")).toEqual([
121+
{
122+
command: "npm.cmd",
123+
args: ["install", "--global", "vsts-npm-auth@0.43.0", `--registry=${cfsRegistry}`],
124+
},
125+
{
126+
command: "vsts-npm-auth.cmd",
127+
args: ["-config", "C:\\repo\\nodejs\\.npmrc", "-Force", "-ReadOnly"],
128+
},
129+
]);
130+
});
131+
132+
it("launches Windows command shims through the command interpreter", () => {
133+
expect(
134+
getCommandInvocation(
135+
"win32",
136+
"npm.cmd",
137+
["--version"],
138+
"C:\\Windows\\System32\\cmd.exe"
139+
)
140+
).toEqual({
141+
command: "C:\\Windows\\System32\\cmd.exe",
142+
args: ["/d", "/s", "/c", "npm.cmd", "--version"],
143+
});
144+
});
145+
146+
it("surfaces command spawn errors", () => {
147+
expect(() =>
148+
runCommand(path.join(tmpdir(), "copilot-sdk-command-does-not-exist"), [], "linux")
149+
).toThrow();
150+
});
151+
152+
it("surfaces nonzero command exit statuses", () => {
153+
expect(() => runCommand(process.execPath, ["-e", "process.exit(7)"], "linux")).toThrow(
154+
"exited with code 7"
155+
);
156+
});
157+
158+
it.each(["linux", "darwin"])("uses the Azure credential provider on %s", (platform) => {
159+
expect(getAuthCommands(platform, "/repo/nodejs/.npmrc")).toEqual([
160+
{
161+
command: "npm",
162+
args: [
163+
"install",
164+
"--global",
165+
"@microsoft/artifacts-npm-credprovider@1.1.3",
166+
`--registry=${credentialProviderRegistry}`,
167+
`--@microsoft:registry=${credentialProviderRegistry}`,
168+
],
169+
},
170+
{
171+
command: "artifacts-npm-credprovider",
172+
args: ["-c", "/repo/nodejs/.npmrc"],
173+
},
174+
]);
175+
expect(getCommandInvocation(platform, "npm", ["--version"])).toEqual({
176+
command: "npm",
177+
args: ["--version"],
178+
});
179+
});
180+
});

scripts/npm-auth-refresh.d.mts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
export interface AuthCommand {
6+
command: string;
7+
args: string[];
8+
}
9+
10+
export type ConfigWriter = (npmrcPaths: string[]) => void;
11+
export type CommandRunner = (command: string, args: string[], platform: string) => void;
12+
export type AuthRefresher = () => void;
13+
14+
export const azureFeedLocalRegistry: string;
15+
export const cfsRegistry: string;
16+
export const credentialProviderRegistry: string;
17+
export function getProjectNpmrcPaths(scriptUrl?: string): string[];
18+
export function buildProjectNpmConfig(): string;
19+
export function writeProjectNpmConfigs(npmrcPaths: string[]): void;
20+
export function getAuthCommands(platform: string, npmrcPath: string): AuthCommand[];
21+
export function getCommandInvocation(
22+
platform: string,
23+
command: string,
24+
args: string[],
25+
commandInterpreter?: string
26+
): AuthCommand;
27+
export function runCommand(
28+
command: string,
29+
args: string[],
30+
platform?: string,
31+
commandInterpreter?: string
32+
): void;
33+
export function refreshNpmAuthentication(
34+
platform?: string,
35+
npmrcPaths?: string[],
36+
writer?: ConfigWriter,
37+
runner?: CommandRunner
38+
): void;
39+
export function main(args?: string[], refresh?: AuthRefresher): number;

0 commit comments

Comments
 (0)