-
Notifications
You must be signed in to change notification settings - Fork 1.4k
391 lines (360 loc) · 17.3 KB
/
Copy pathsdk-canary.yml
File metadata and controls
391 lines (360 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
name: "SDK Canary Test/Publish"
# Nightly-style canary pipeline. First installs an explicit version of the
# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite
# against it to prove runtime <-> SDK compatibility. When that gate passes (and
# mode allows), publishes an SDK canary pinned to the tested runtime to the
# internal Azure Artifacts feed only (never public npm).
env:
HUSKY: 0
# Internal org-scoped Azure Artifacts feed — single source of truth so the
# feed name isn't repeated across steps. The SDK canary publishes here and
# (when runtime_source=internal) installs the runtime from here; it must NEVER
# reach public npm (@github/copilot-sdk is a live public package).
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/
# Azure DevOps resource ID used to mint an ADO access token for the feed.
ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798
on:
workflow_dispatch:
inputs:
runtime_version:
description: "Exact @github/copilot version to test (e.g. 1.0.69 or 1.0.70-canary.<sha>)"
required: true
type: string
runtime_source:
description: "Where to install the runtime from"
required: true
type: choice
options:
- public
- internal
default: public
mode:
description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)"
required: false
type: choice
default: publish
options:
- publish
- publish-force
- tests-only
repository_dispatch:
types: [runtime-canary]
permissions:
contents: read
id-token: write
# Serialize runs per ref so two overlapping canary runs can't race the feed
# publish. cancel-in-progress: false — never kill an in-flight publish.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
resolve:
name: "Resolve runtime inputs"
if: github.event.repository.fork == false
runs-on: ubuntu-latest
permissions: {}
outputs:
RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }}
RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }}
PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }}
steps:
# Normalize whichever trigger fired into a single (RUNTIME_VERSION,
# RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step
# references. workflow_dispatch reads the human-supplied inputs;
# repository_dispatch reads client_payload and forces source=internal
# (a runtime canary only exists on the feed), defaulting mode to publish.
- name: Normalize inputs
id: normalize
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.runtime_version }}
INPUT_SOURCE: ${{ inputs.runtime_source }}
INPUT_MODE: ${{ inputs.mode }}
PAYLOAD_VERSION: ${{ github.event.client_payload.runtime_version }}
PAYLOAD_SOURCE: ${{ github.event.client_payload.runtime_source }}
PAYLOAD_MODE: ${{ github.event.client_payload.mode }}
run: |
set -euo pipefail
case "$EVENT_NAME" in
workflow_dispatch)
VERSION="$INPUT_VERSION"
SOURCE="$INPUT_SOURCE"
MODE="$INPUT_MODE"
;;
repository_dispatch)
VERSION="$PAYLOAD_VERSION"
# A runtime canary only ever exists on the internal feed.
SOURCE="${PAYLOAD_SOURCE:-internal}"
MODE="${PAYLOAD_MODE:-publish}"
;;
*)
echo "::error::Unsupported event '$EVENT_NAME'."
exit 1
;;
esac
if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi
if [ -z "$SOURCE" ]; then SOURCE="public"; fi
case "$SOURCE" in
public|internal) ;;
*) echo "::error::Invalid runtime source '$SOURCE'. Expected one of: public, internal."; exit 1 ;;
esac
if [ -z "$MODE" ]; then MODE="publish"; fi
case "$MODE" in
publish|publish-force|tests-only) ;;
*) echo "::error::Invalid publish mode '$MODE'. Expected one of: publish, publish-force, tests-only."; exit 1 ;;
esac
echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE PUBLISH_MODE=$MODE"
echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT"
echo "PUBLISH_MODE=$MODE" >> "$GITHUB_OUTPUT"
- name: Validate runtime version (semver)
env:
RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }}
run: |
if [[ ! "$RUNTIME_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "::error::Invalid runtime version '$RUNTIME_VERSION'. Expected semver (e.g. 1.0.69 or 1.0.70-canary.abc123)."
exit 1
fi
test:
name: "E2E tests (${{ matrix.os }})"
needs: resolve
if: github.event.repository.fork == false
environment: cicd
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
POWERSHELL_UPDATECHECK: Off
RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }}
RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }}
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6
with:
cache: "npm"
cache-dependency-path: "./nodejs/package-lock.json"
node-version: 22
- name: Install SDK dependencies
run: npm ci --ignore-scripts
- name: Install test harness dependencies
working-directory: ./test/harness
run: npm ci --ignore-scripts
- name: Azure Login (OIDC -> id-cpd-ci)
if: env.RUNTIME_SOURCE == 'internal'
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci
tenant-id: "${{ vars.CPD_ID_TENANT_ID }}"
allow-no-subscriptions: true
# Route ONLY @github/* (the runtime + its 8 platform packages) to the
# internal feed via a scoped registry. All other deps (e.g. detect-libc)
# still resolve from public npm. A global --registry would break because
# detect-libc is not on the feed.
- name: Configure canary feed (.npmrc)
if: env.RUNTIME_SOURCE == 'internal'
run: |
set -euo pipefail
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
# Derive the protocol-relative auth scopes from FEED_URL so the feed
# name lives in exactly one place (the workflow-level env).
FEED_AUTH_REGISTRY="${FEED_URL#https:}"
FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}"
NPMRC="$(printf '%s\n' \
"@github:registry=${FEED_URL}" \
"${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \
"${FEED_AUTH_BASE}:_authToken=${TOKEN}")"
printf '%s\n' "$NPMRC" > .npmrc
echo "Wrote scoped @github registry .npmrc to ./nodejs"
- name: Override runtime version
run: |
set -euo pipefail
echo "Installing @github/copilot@${RUNTIME_VERSION} (source: ${RUNTIME_SOURCE})"
npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts
- name: Verify installed runtime
run: |
set -euo pipefail
node -e '
const fs = require("fs");
const expected = process.env.RUNTIME_VERSION;
const pkg = require("./node_modules/@github/copilot/package.json");
if (pkg.version !== expected) {
console.error(`::error::Installed @github/copilot version ${pkg.version} does not match requested ${expected}`);
process.exit(1);
}
const dir = "./node_modules/@github";
const entries = fs.readdirSync(dir).filter((d) => d.startsWith("copilot-"));
const plat = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux";
const arch = process.arch;
const match = entries.find((d) => d.includes(plat) && d.includes(arch));
if (!match) {
console.error(`::error::No @github/copilot platform optional dep for ${plat}-${arch}. Present: ${entries.join(", ") || "(none)"}`);
process.exit(1);
}
const platPkg = require(`${dir}/${match}/package.json`);
if (platPkg.version !== expected) {
console.error(`::error::Platform package @github/${match} version ${platPkg.version} does not match requested ${expected}`);
process.exit(1);
}
console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}@${platPkg.version}`);
'
- name: Build SDK
run: npm run build
- name: Warm up PowerShell
if: runner.os == 'Windows'
run: pwsh.exe -Command "Write-Host 'PowerShell ready'"
- name: Run Node.js SDK e2e tests
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: npm test
publish:
name: "Publish SDK canary (internal feed)"
needs: [resolve, test]
# Publish runs only when the gate permits it. Mode governs behavior:
# - tests-only: never publish (skips this job entirely).
# - publish: publish only when the e2e gate is green (the default for both
# the human and automated triggers).
# - publish-force: publish even on a non-green gate — a human-acknowledged
# flake override, audited via the ::warning:: step below and the run actor.
# publish-force only skips the e2e *signal* — the publish job still runs the
# build (so a broken build can't publish) and enforces the feed-only guards.
if: >
!cancelled() &&
github.event.repository.fork == false &&
needs.resolve.result == 'success' &&
needs.resolve.outputs.PUBLISH_MODE != 'tests-only' &&
(needs.test.result == 'success' ||
needs.resolve.outputs.PUBLISH_MODE == 'publish-force')
environment: cicd
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }}
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- name: Warn — publishing despite failed e2e gate (publish-force)
# always() so this audit is never skipped by prior-step status; it fires
# specifically when publish proceeded on a non-green gate via publish-force.
# Runs at the workspace root because it executes before checkout, so the
# job's default working-directory (./nodejs) does not exist yet.
if: always() && needs.test.result != 'success' && needs.resolve.outputs.PUBLISH_MODE == 'publish-force'
working-directory: ${{ github.workspace }}
run: |
echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}) via publish-force. Triggered by '${{ github.actor }}' through '${{ github.event_name }}'. The e2e signal was bypassed; build + feed-only guards still apply."
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6
with:
node-version: 22
# Default public registry: installs build deps and the currently pinned
# runtime. Do NOT write any feed .npmrc or scoped @github:registry line
# here, or npm ci would try to fetch the runtime from the upstream-less
# feed and 404.
- name: Install SDK dependencies
run: npm ci --ignore-scripts
- name: Compute SDK canary version
id: sdkver
env:
RUN_NUMBER: ${{ github.run_number }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
SHORT_SHA="${SHA:0:7}"
# Base the canary on the NEXT patch of the public SDK latest so canaries
# correlate with public releases: they sort ABOVE the current public
# latest and BELOW the eventual real release of that next patch (a
# prerelease of X.Y.Z always sorts below X.Y.Z), so a canary can never
# shadow the real release when it ships.
# Reuse the repo's own version helper (scripts/get-version.js) so this
# stays consistent with publish.yml: `current` returns the latest public
# dist-tag version, read-only from public npm (never the feed), then
# we bump the patch ourselves to keep strict patch+1 semantics.
PUBLIC_LATEST="$(node scripts/get-version.js current || true)"
BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}"
if [[ "$BASE" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$(( BASH_REMATCH[3] + 1 ))"
else
echo "::error::Could not resolve public SDK latest version (got '$PUBLIC_LATEST'); refusing to publish a canary with an unknown base."
exit 1
fi
SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}"
if [[ ! "$SDK_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver."
exit 1
fi
echo "SDK canary version: $SDK_VERSION"
echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_OUTPUT"
- name: Set package version and pin runtime dependency
env:
SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }}
run: |
set -euo pipefail
npm version "$SDK_VERSION" --no-git-tag-version --allow-same-version
# Exact pin (no caret) so the published SDK canary depends on precisely
# the runtime version that was just tested by the e2e gate.
npm pkg set "dependencies.@github/copilot=$RUNTIME_VERSION"
echo "Pinned @github/copilot to $(npm pkg get dependencies.@github/copilot)"
- name: Build SDK
run: npm run build
- name: Azure Login (OIDC -> id-cpd-ci)
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci
tenant-id: "${{ vars.CPD_ID_TENANT_ID }}"
allow-no-subscriptions: true
# Auth-only .npmrc: just the two token lines, NO scoped registry line.
# The publish target is supplied explicitly via publishConfig + --registry.
- name: Configure feed auth (.npmrc)
run: |
set -euo pipefail
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
# Derive the protocol-relative auth scopes from FEED_URL (single source
# of truth). NO scoped @github:registry line here — publish target is
# supplied explicitly via publishConfig + --registry.
FEED_AUTH_REGISTRY="${FEED_URL#https:}"
FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}"
printf '%s\n' \
"${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \
"${FEED_AUTH_BASE}:_authToken=${TOKEN}" > .npmrc
echo "Wrote auth-only .npmrc to ./nodejs"
# Belt and suspenders (2 of 3): pin the publish target in the package too.
- name: Set publishConfig registry
run: npm pkg set "publishConfig.registry=$FEED_URL"
# Belt and suspenders (3 of 3): fail loudly unless the effective publish
# target is the internal feed. Guards against ever reaching public npm.
- name: Assert publish target is the internal feed
run: |
set -euo pipefail
EFFECTIVE="$(npm pkg get publishConfig.registry | tr -d '"')"
echo "Effective publishConfig.registry: $EFFECTIVE"
if [ "$EFFECTIVE" != "$FEED_URL" ]; then
echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish."
exit 1
fi
- name: Publish SDK canary to internal feed
run: npm publish --registry "$FEED_URL"
- name: Summarize published canary
env:
SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }}
run: |
set -euo pipefail
{
echo "## SDK canary published"
echo ""
echo "| | |"
echo "| --- | --- |"
echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |"
echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |"
echo "| Feed | ${FEED_URL} |"
} >> "$GITHUB_STEP_SUMMARY"