Skip to content

Commit 6822afb

Browse files
Mirror Node SDK releases to internal feed (#2025)
* Mirror Node SDK releases internally Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6e76edd-7acc-4681-a884-56d16967c048 * Simplify release retry handling Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6e76edd-7acc-4681-a884-56d16967c048 * Handle Azure feed conflict prefix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6e76edd-7acc-4681-a884-56d16967c048 * Fix Windows release helper parsing Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b6e76edd-7acc-4681-a884-56d16967c048 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3cbf4e7 commit 6822afb

3 files changed

Lines changed: 321 additions & 13 deletions

File tree

.github/workflows/publish.yml

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ on:
2121
required: false
2222

2323
permissions:
24-
contents: write
25-
id-token: write # Required for OIDC
26-
actions: write # Required to trigger changelog workflow
24+
contents: read
2725

2826
concurrency:
2927
group: publish
@@ -78,11 +76,21 @@ jobs:
7876
echo "Auto-incremented version: $VERSION" >> $GITHUB_STEP_SUMMARY
7977
fi
8078
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
79+
- name: Verify version is available on public npm
80+
env:
81+
VERSION: ${{ steps.version.outputs.VERSION }}
82+
run: |
83+
node scripts/npm-release.js preflight \
84+
@github/copilot-sdk \
85+
"$VERSION" \
86+
https://registry.npmjs.org
8187
82-
publish-nodejs:
83-
name: Publish Node.js SDK
88+
package-nodejs:
89+
name: Package Node.js SDK
8490
needs: version
8591
runs-on: ubuntu-latest
92+
permissions:
93+
contents: read
8694
defaults:
8795
run:
8896
working-directory: ./nodejs
@@ -91,8 +99,6 @@ jobs:
9199
- uses: actions/setup-node@v6
92100
with:
93101
node-version: "22.x"
94-
- name: Update npm for OIDC support
95-
run: npm i -g "npm@11.6.3"
96102
- run: npm ci --ignore-scripts
97103
- name: Set version
98104
run: node scripts/set-version.js
@@ -101,21 +107,126 @@ jobs:
101107
- name: Build
102108
run: npm run build
103109
- name: Pack
104-
run: npm pack
110+
id: pack
111+
run: |
112+
TARBALL="$(npm pack . --json | jq -r '.[0].filename')"
113+
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
114+
echo "::error::npm pack did not produce a tarball."
115+
exit 1
116+
fi
117+
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
105118
- name: Upload artifact
106119
uses: actions/upload-artifact@v7.0.0
107120
with:
108121
name: nodejs-package
109-
path: nodejs/*.tgz
110-
- name: Publish to npm
111-
if: github.ref == 'refs/heads/main' || github.event.inputs.dist-tag == 'unstable'
112-
run: npm publish --tag ${{ github.event.inputs.dist-tag }} --access public --registry https://registry.npmjs.org
122+
path: nodejs/${{ steps.pack.outputs.tarball }}
123+
if-no-files-found: error
124+
125+
publish-nodejs:
126+
name: Publish Node.js SDK
127+
needs: package-nodejs
128+
if: github.ref == 'refs/heads/main' || github.event.inputs.dist-tag == 'unstable'
129+
runs-on: ubuntu-latest
130+
permissions:
131+
actions: read
132+
contents: read
133+
id-token: write
134+
steps:
135+
- uses: actions/checkout@v6.0.2
136+
- uses: actions/setup-node@v6
137+
with:
138+
node-version: "22.x"
139+
- name: Update npm for OIDC support
140+
run: npm i -g "npm@11.6.3"
141+
- name: Download Node.js package
142+
uses: actions/download-artifact@v8.0.0
143+
with:
144+
name: nodejs-package
145+
path: ./dist
146+
- name: Publish tarball to public npm
147+
env:
148+
DIST_TAG: ${{ github.event.inputs.dist-tag }}
149+
run: |
150+
set -euo pipefail
151+
shopt -s nullglob
152+
TARBALLS=(./dist/*.tgz)
153+
if [ "${#TARBALLS[@]}" -ne 1 ]; then
154+
echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}."
155+
exit 1
156+
fi
157+
node nodejs/scripts/npm-release.js publish \
158+
"${TARBALLS[0]}" \
159+
"$DIST_TAG" \
160+
https://registry.npmjs.org \
161+
public
162+
163+
publish-nodejs-internal:
164+
name: Publish Node.js SDK to internal feed
165+
needs: publish-nodejs
166+
environment: cicd
167+
runs-on: ubuntu-latest
168+
permissions:
169+
actions: read
170+
contents: read
171+
id-token: write
172+
env:
173+
ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798
174+
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/
175+
steps:
176+
- uses: actions/checkout@v6.0.2
177+
- uses: actions/setup-node@v6
178+
with:
179+
node-version: "22.x"
180+
- name: Download Node.js package
181+
uses: actions/download-artifact@v8.0.0
182+
with:
183+
name: nodejs-package
184+
path: ./dist
185+
- name: Azure Login (OIDC -> id-cpd-ci)
186+
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
187+
with:
188+
client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci
189+
tenant-id: "${{ vars.CPD_ID_TENANT_ID }}"
190+
allow-no-subscriptions: true
191+
- name: Configure feed auth
192+
run: |
193+
set -euo pipefail
194+
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
195+
echo "::add-mask::$TOKEN"
196+
FEED_AUTH_REGISTRY="${FEED_URL#https:}"
197+
FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}"
198+
printf '%s\n' \
199+
"${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \
200+
"${FEED_AUTH_BASE}:_authToken=${TOKEN}" > "$HOME/.npmrc"
201+
- name: Publish tarball to internal feed
202+
env:
203+
DIST_TAG: ${{ github.event.inputs.dist-tag }}
204+
run: |
205+
set -euo pipefail
206+
if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/" ]; then
207+
echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish."
208+
exit 1
209+
fi
210+
shopt -s nullglob
211+
TARBALLS=(./dist/*.tgz)
212+
if [ "${#TARBALLS[@]}" -ne 1 ]; then
213+
echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}."
214+
exit 1
215+
fi
216+
node nodejs/scripts/npm-release.js publish \
217+
"${TARBALLS[0]}" \
218+
"$DIST_TAG" \
219+
"$FEED_URL" \
220+
azure
113221
114222
publish-dotnet:
115223
name: Publish .NET SDK
116224
if: github.event.inputs.dist-tag != 'unstable'
117225
needs: version
118226
runs-on: ubuntu-latest
227+
permissions:
228+
contents: read
229+
id-token: write
119230
defaults:
120231
run:
121232
working-directory: ./dotnet
@@ -200,6 +311,9 @@ jobs:
200311
if: github.event.inputs.dist-tag != 'unstable'
201312
needs: version
202313
runs-on: ubuntu-latest
314+
permissions:
315+
contents: read
316+
id-token: write
203317
defaults:
204318
run:
205319
working-directory: ./python
@@ -237,6 +351,9 @@ jobs:
237351
name: Publish Java SDK
238352
if: github.event.inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main'
239353
needs: version
354+
permissions:
355+
contents: write
356+
id-token: write
240357
uses: ./.github/workflows/java-publish-maven.yml
241358
with:
242359
releaseVersion: ${{ needs.version.outputs.version }}
@@ -245,7 +362,15 @@ jobs:
245362

246363
github-release:
247364
name: Create GitHub Release
248-
needs: [version, publish-nodejs, publish-dotnet, publish-python, publish-rust, publish-java]
365+
needs:
366+
[
367+
version,
368+
publish-nodejs,
369+
publish-dotnet,
370+
publish-python,
371+
publish-rust,
372+
publish-java,
373+
]
249374
if: |
250375
always() &&
251376
github.ref == 'refs/heads/main' &&
@@ -256,6 +381,9 @@ jobs:
256381
needs.publish-python.result == 'success' &&
257382
needs.publish-rust.result == 'success'
258383
runs-on: ubuntu-latest
384+
permissions:
385+
actions: write
386+
contents: write
259387
steps:
260388
- uses: actions/checkout@v6.0.2
261389
- name: Create GitHub Release

nodejs/scripts/npm-release.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { spawn } from "node:child_process";
2+
import { pathToFileURL } from "node:url";
3+
4+
const PUBLIC_CONFLICT =
5+
/^(?:npm (?:error|ERR!) code EPUBLISHCONFLICT|npm (?:error|ERR!) (?:403 [^\r\n]* - )?(?:You )?cannot publish over (?:the )?previously published versions(?:: [^\r\n]+)?\.?)\r?$/im;
6+
const AZURE_CONFLICT =
7+
/^npm (?:error|ERR!) (?:403 [^\r\n]* - )?(?:The feed '[^'\r\n]+' )?already contains file '[^'\r\n]+\.tgz' in package '[^'\r\n]+'\.?\r?$/im;
8+
9+
export function runCommand(command, args, { stream = false } = {}) {
10+
return new Promise((resolve, reject) => {
11+
const child = spawn(command, args, { shell: false });
12+
let stdout = "";
13+
let stderr = "";
14+
15+
child.stdout.on("data", (chunk) => {
16+
stdout += chunk;
17+
if (stream) process.stdout.write(chunk);
18+
});
19+
child.stderr.on("data", (chunk) => {
20+
stderr += chunk;
21+
if (stream) process.stderr.write(chunk);
22+
});
23+
child.on("error", reject);
24+
child.on("close", (status) => resolve({ status: status ?? 1, stdout, stderr }));
25+
});
26+
}
27+
28+
export async function assertVersionAbsent(packageName, version, registry, runner = runCommand) {
29+
const result = await runner("npm", [
30+
"view",
31+
`${packageName}@${version}`,
32+
"version",
33+
"--json",
34+
"--registry",
35+
registry,
36+
]);
37+
38+
if (result.status === 0) {
39+
throw new Error(`${packageName}@${version} already exists on public npm.`);
40+
}
41+
42+
try {
43+
if (JSON.parse(result.stdout)?.error?.code === "E404") return;
44+
} catch {
45+
// The failure below includes npm's output for diagnosis.
46+
}
47+
48+
const output = `${result.stdout}\n${result.stderr}`.trim();
49+
throw new Error(
50+
`Could not confirm that ${packageName}@${version} is absent from public npm (npm exited ${result.status}).${output ? `\n${output}` : ""}`
51+
);
52+
}
53+
54+
export async function publishTarball(tarball, tag, registry, mode, runner = runCommand) {
55+
const args = ["publish", tarball, "--tag", tag, "--registry", registry];
56+
if (mode === "public") args.push("--access", "public");
57+
if (mode !== "public" && mode !== "azure") throw new Error(`Unknown publish mode: ${mode}`);
58+
59+
const result = await runner("npm", args, { stream: true });
60+
if (result.status === 0) return;
61+
62+
const output = `${result.stdout}\n${result.stderr}`;
63+
if (PUBLIC_CONFLICT.test(output) || (mode === "azure" && AZURE_CONFLICT.test(output))) {
64+
console.log(
65+
"Version already published; treating the immutable-version conflict as success."
66+
);
67+
return;
68+
}
69+
70+
throw new Error(`npm publish failed with exit code ${result.status}.`);
71+
}
72+
73+
async function main() {
74+
const [command, ...args] = process.argv.slice(2);
75+
if (command === "preflight" && args.length === 3) {
76+
await assertVersionAbsent(...args);
77+
console.log(`${args[0]}@${args[1]} is available on public npm.`);
78+
} else if (command === "publish" && args.length === 4) {
79+
await publishTarball(...args);
80+
} else {
81+
throw new Error(
82+
"Usage: npm-release.js preflight <package> <version> <registry> | publish <tarball> <tag> <registry> <public|azure>"
83+
);
84+
}
85+
}
86+
87+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
88+
main().catch((error) => {
89+
console.error(`::error::${error.message}`);
90+
process.exitCode = 1;
91+
});
92+
}

0 commit comments

Comments
 (0)