From 03365082cf2ad89227118825aed93a68e3151821 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 14:59:05 -0700 Subject: [PATCH 01/23] Add SDK canary runtime compatibility gate workflow Installs an explicit @github/copilot runtime version, builds the Node SDK, and runs the Node e2e suite against it as a runtime<->SDK compat gate. No publishing. A temporary branch-push trigger validates the public path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 195 +++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 .github/workflows/sdk-canary.yml diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml new file mode 100644 index 000000000..63282b200 --- /dev/null +++ b/.github/workflows/sdk-canary.yml @@ -0,0 +1,195 @@ +name: "SDK Canary (runtime compat gate)" + +# Nightly-style canary compatibility gate: installs an explicit version of the +# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite +# against it. This proves runtime <-> SDK compatibility. No publishing happens +# here. + +env: + HUSKY: 0 + +on: + workflow_dispatch: + inputs: + runtime_version: + description: "Exact @github/copilot version to test (e.g. 1.0.69 or 1.0.70-canary.)" + required: true + type: string + runtime_source: + description: "Where to install the runtime from" + required: true + type: choice + options: + - public + - feed + default: public + # TEMPORARY: branch-push trigger so we can validate the pipeline before the + # workflow_dispatch entrypoint exists on main. Push events carry no dispatch + # inputs, so the resolve job falls back to the currently pinned runtime from + # public npm. Remove this trigger once the workflow lands on main. + push: + branches: [mackinnonbuck-sdk-canary-compat-gate] + +permissions: + contents: read + id-token: write + +jobs: + resolve: + name: "Resolve runtime inputs" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + outputs: + RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} + steps: + - uses: actions/checkout@v6.0.2 + + # Normalize whichever trigger fired into a single (RUNTIME_VERSION, + # RUNTIME_SOURCE) pair that every downstream step references. Adding a + # `repository_dispatch: types: [runtime-canary]` trigger later is purely + # additive: add one more case that reads client_payload and defaults + # source to 'feed'. No downstream rework required. + - name: Normalize inputs + id: normalize + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ inputs.runtime_version }} + INPUT_SOURCE: ${{ inputs.runtime_source }} + run: | + set -euo pipefail + case "$EVENT_NAME" in + workflow_dispatch) + VERSION="$INPUT_VERSION" + SOURCE="$INPUT_SOURCE" + ;; + push) + # No dispatch inputs on push: fall back to the currently pinned + # runtime from public npm so the run is self-consistent and a green + # result proves the pipeline mechanics. + SOURCE="public" + VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" + ;; + *) + 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 + echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE" + echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT" + + - name: Validate runtime version (semver) + env: + RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} + run: | + if [[ ! "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]; 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 (${{ 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 == 'feed' + 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 == 'feed' + run: | + set -euo pipefail + TOKEN="$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)" + echo "::add-mask::$TOKEN" + NPMRC="$(cat < .npmrc + printf '%s\n' "$NPMRC" > ../test/harness/.npmrc + echo "Wrote scoped @github registry .npmrc to ./nodejs and ./test/harness" + + - 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 + ( cd ../test/harness && 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); + } + console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}`); + ' + + - 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 From f3dd8680590355384b9b97908c423aa54e67afd0 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 15:33:23 -0700 Subject: [PATCH 02/23] Add guarded publish job to SDK canary workflow Publishes an SDK canary to the internal copilot-canary-test feed, pinned to the exact runtime version just tested. Feed-only via publishConfig.registry, explicit --registry, and a pre-publish assertion. Runs only when all e2e matrix jobs are green (needs: [resolve, test]). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 120 +++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 63282b200..64646dcdb 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -193,3 +193,123 @@ jobs: env: COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} run: npm test + + publish: + name: "Publish SDK canary (internal feed)" + needs: [resolve, test] + if: github.event.repository.fork == false + environment: cicd + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + env: + RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} + # Org-scoped internal TEST feed. The SDK canary MUST only ever go here, + # never to public npm (@github/copilot-sdk is a live public package). + FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/registry/ + defaults: + run: + shell: bash + working-directory: ./nodejs + steps: + - 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 + + - 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}" + SDK_VERSION="0.0.0-canary.${RUN_NUMBER}.g${SHORT_SHA}" + if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]; 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 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)" + echo "::add-mask::$TOKEN" + cat > .npmrc < Date: Wed, 8 Jul 2026 15:48:33 -0700 Subject: [PATCH 03/23] Add force_publish bypass for flaky e2e gate in SDK canary Adds a workflow_dispatch force_publish input that lets a human publish the SDK canary despite a non-passing e2e gate (for a known flake). A loud ::warning:: is logged whenever publish proceeds without a green gate. The bypass only skips the e2e signal; build + feed-only + read-back guards still apply. A temporary push-event bypass pairs with the temporary push trigger for branch validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 64646dcdb..8afe65046 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -23,6 +23,11 @@ on: - public - feed default: public + force_publish: + description: "Publish the SDK canary even if the e2e gate fails (use ONLY for a known flake). Bypasses the e2e signal; the build + feed-only guards still apply." + required: false + type: boolean + default: false # TEMPORARY: branch-push trigger so we can validate the pipeline before the # workflow_dispatch entrypoint exists on main. Push events carry no dispatch # inputs, so the resolve job falls back to the currently pinned runtime from @@ -197,7 +202,22 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - if: github.event.repository.fork == false + # Normally publish only runs when the e2e gate is green. Two bypasses: + # - workflow_dispatch with force_publish=true: a human-acknowledged flake + # override (audited via the actor on the run). + # - github.event_name == 'push': TEMPORARY branch-validation bypass that + # pairs with the temporary push trigger so we can exercise the publish + + # bypass paths on this branch. Remove alongside the push trigger at + # finalize; after that, force is honored only on real dispatches. + # The bypass only skips the e2e *signal* — the publish job still runs the + # build (so a broken build can't publish) and enforces feed-only + read-back. + if: > + !cancelled() && + github.event.repository.fork == false && + needs.resolve.result == 'success' && + (needs.test.result == 'success' || + (github.event_name == 'workflow_dispatch' && inputs.force_publish) || + github.event_name == 'push') environment: cicd runs-on: ubuntu-latest permissions: @@ -213,6 +233,11 @@ jobs: shell: bash working-directory: ./nodejs steps: + - name: Warn — publishing despite failed e2e gate (bypass) + if: needs.test.result != 'success' + run: | + echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}). Triggered by '${{ github.actor }}' via '${{ github.event_name }}'${{ (github.event_name == 'workflow_dispatch' && inputs.force_publish) && ' with force_publish=true' || '' }}. The e2e signal was bypassed; build + feed-only + read-back guards still apply." + - uses: actions/checkout@v6.0.2 - uses: actions/setup-node@v6 From 05e0213927e1f64dc264a7953f66952e2216da0e Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 15:59:59 -0700 Subject: [PATCH 04/23] Harden canary bypass audit and mark temp branch-validation lines - Warn step now uses always() so the bypass audit is never skipped by prior-step status; it fires whenever publish proceeds with test != success. - Add greppable 'TEMP: remove at finalize' markers on the temporary push trigger and the coupled publish.if event==push bypass clause. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 8afe65046..578611698 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -32,7 +32,7 @@ on: # workflow_dispatch entrypoint exists on main. Push events carry no dispatch # inputs, so the resolve job falls back to the currently pinned runtime from # public npm. Remove this trigger once the workflow lands on main. - push: + push: # TEMP: remove at finalize (coupled with the publish.if `event==push` clause) branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: @@ -205,10 +205,11 @@ jobs: # Normally publish only runs when the e2e gate is green. Two bypasses: # - workflow_dispatch with force_publish=true: a human-acknowledged flake # override (audited via the actor on the run). - # - github.event_name == 'push': TEMPORARY branch-validation bypass that - # pairs with the temporary push trigger so we can exercise the publish + - # bypass paths on this branch. Remove alongside the push trigger at - # finalize; after that, force is honored only on real dispatches. + # - github.event_name == 'push': TEMP: remove at finalize — branch-validation + # bypass coupled with the temporary push trigger so we can exercise the + # publish + bypass paths on this branch. Drop the `github.event_name == + # 'push'` clause from the `if:` below together with the push trigger; after + # that, force is honored only on real dispatches. # The bypass only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > @@ -234,7 +235,9 @@ jobs: working-directory: ./nodejs steps: - name: Warn — publishing despite failed e2e gate (bypass) - if: needs.test.result != 'success' + # always() so this audit is never skipped by prior-step status; it fires + # specifically when publish proceeded without a green e2e gate. + if: always() && needs.test.result != 'success' run: | echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}). Triggered by '${{ github.actor }}' via '${{ github.event_name }}'${{ (github.event_name == 'workflow_dispatch' && inputs.force_publish) && ' with force_publish=true' || '' }}. The e2e signal was bypassed; build + feed-only + read-back guards still apply." From 79519e43f695c1a7eae4aaa327a802caecd47ef5 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:08:34 -0700 Subject: [PATCH 05/23] Fix canary bypass audit step failing before checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Warn step runs first (before checkout), but the job default working-directory ./nodejs does not exist yet, so bash failed to cd into it and the step errored — which skipped the whole publish job the one time the bypass actually fired. Pin the step to the workspace root. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 578611698..7827c322e 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -236,8 +236,11 @@ jobs: steps: - name: Warn — publishing despite failed e2e gate (bypass) # always() so this audit is never skipped by prior-step status; it fires - # specifically when publish proceeded without a green e2e gate. + # specifically when publish proceeded without a green e2e gate. 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' + 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 }}). Triggered by '${{ github.actor }}' via '${{ github.event_name }}'${{ (github.event_name == 'workflow_dispatch' && inputs.force_publish) && ' with force_publish=true' || '' }}. The e2e signal was bypassed; build + feed-only + read-back guards still apply." From b2892338f90d05ee2f5443f5bcb3dde4c0ffd3b4 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:18:52 -0700 Subject: [PATCH 06/23] Finalize SDK canary: remove temporary branch-validation bypasses - Remove the temporary push: trigger (workflow_dispatch is now the only trigger). - Remove the event==push clause from publish.if; force_publish (workflow_dispatch only) is the sole bypass. - Drop the now-dead push-fallback normalization from the resolve job. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 38 +++++++++----------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 7827c322e..712875a78 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,9 +1,10 @@ name: "SDK Canary (runtime compat gate)" -# Nightly-style canary compatibility gate: installs an explicit version of the -# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite -# against it. This proves runtime <-> SDK compatibility. No publishing happens -# here. +# Canary compatibility gate: 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. On a green gate it publishes an SDK canary +# (pinned to the exact tested runtime) to the internal copilot-canary-test feed. +# Triggered manually via workflow_dispatch. env: HUSKY: 0 @@ -28,12 +29,6 @@ on: required: false type: boolean default: false - # TEMPORARY: branch-push trigger so we can validate the pipeline before the - # workflow_dispatch entrypoint exists on main. Push events carry no dispatch - # inputs, so the resolve job falls back to the currently pinned runtime from - # public npm. Remove this trigger once the workflow lands on main. - push: # TEMP: remove at finalize (coupled with the publish.if `event==push` clause) - branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -50,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v6.0.2 - # Normalize whichever trigger fired into a single (RUNTIME_VERSION, + # Normalize the dispatch inputs into a single (RUNTIME_VERSION, # RUNTIME_SOURCE) pair that every downstream step references. Adding a # `repository_dispatch: types: [runtime-canary]` trigger later is purely # additive: add one more case that reads client_payload and defaults @@ -68,13 +63,6 @@ jobs: VERSION="$INPUT_VERSION" SOURCE="$INPUT_SOURCE" ;; - push) - # No dispatch inputs on push: fall back to the currently pinned - # runtime from public npm so the run is self-consistent and a green - # result proves the pipeline mechanics. - SOURCE="public" - VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" - ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -202,14 +190,9 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - # Normally publish only runs when the e2e gate is green. Two bypasses: - # - workflow_dispatch with force_publish=true: a human-acknowledged flake - # override (audited via the actor on the run). - # - github.event_name == 'push': TEMP: remove at finalize — branch-validation - # bypass coupled with the temporary push trigger so we can exercise the - # publish + bypass paths on this branch. Drop the `github.event_name == - # 'push'` clause from the `if:` below together with the push trigger; after - # that, force is honored only on real dispatches. + # Normally publish only runs when the e2e gate is green. One bypass: + # workflow_dispatch with force_publish=true — a human-acknowledged flake + # override, audited via the ::warning:: step below and the run's actor. # The bypass only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > @@ -217,8 +200,7 @@ jobs: github.event.repository.fork == false && needs.resolve.result == 'success' && (needs.test.result == 'success' || - (github.event_name == 'workflow_dispatch' && inputs.force_publish) || - github.event_name == 'push') + (github.event_name == 'workflow_dispatch' && inputs.force_publish)) environment: cicd runs-on: ubuntu-latest permissions: From 20b7003054ca33ca1c719b0bdb8c5123e5d383ce Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:21:25 -0700 Subject: [PATCH 07/23] Base SDK canary version on next patch of public SDK latest Compute the canary base as public latest + patch bump (e.g. 1.0.7-canary..g) so canaries correlate with public releases: they sort above current public latest and below the eventual real release of that next patch. Public latest is read read-only from public npm (never the feed) with a resilient 0.0.0 fallback + warning when it can't be resolved. Runtime correlation stays carried by the pinned dependencies.@github/copilot. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 712875a78..7033aad0f 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -247,7 +247,23 @@ jobs: run: | set -euo pipefail SHORT_SHA="${SHA:0:7}" - SDK_VERSION="0.0.0-canary.${RUN_NUMBER}.g${SHORT_SHA}" + # 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). Public latest is + # currently 1.0.6, so canaries look like 1.0.7-canary..g and + # can never shadow the real 1.0.7 when it ships. + # Read public latest EXPLICITLY from public npm (never the feed: the + # feed has no public upstream and now holds our own canaries). Read-only. + PUBLIC_LATEST="$(npm view @github/copilot-sdk version --registry https://registry.npmjs.org/ 2>/dev/null || 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 + NEXT="0.0.0" + echo "::warning::Could not resolve public SDK latest; falling back to 0.0.0 canary base" + fi + SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]; then echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver." exit 1 From 925844ea76356f27bb214ff497a4f77aa293f6e5 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:23:18 -0700 Subject: [PATCH 08/23] Revert "Finalize SDK canary: remove temporary branch-validation bypasses" This reverts commit b2892338f90d05ee2f5443f5bcb3dde4c0ffd3b4. --- .github/workflows/sdk-canary.yml | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 7033aad0f..53ce452d2 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,10 +1,9 @@ name: "SDK Canary (runtime compat gate)" -# Canary compatibility gate: 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. On a green gate it publishes an SDK canary -# (pinned to the exact tested runtime) to the internal copilot-canary-test feed. -# Triggered manually via workflow_dispatch. +# Nightly-style canary compatibility gate: installs an explicit version of the +# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite +# against it. This proves runtime <-> SDK compatibility. No publishing happens +# here. env: HUSKY: 0 @@ -29,6 +28,12 @@ on: required: false type: boolean default: false + # TEMPORARY: branch-push trigger so we can validate the pipeline before the + # workflow_dispatch entrypoint exists on main. Push events carry no dispatch + # inputs, so the resolve job falls back to the currently pinned runtime from + # public npm. Remove this trigger once the workflow lands on main. + push: # TEMP: remove at finalize (coupled with the publish.if `event==push` clause) + branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -45,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v6.0.2 - # Normalize the dispatch inputs into a single (RUNTIME_VERSION, + # Normalize whichever trigger fired into a single (RUNTIME_VERSION, # RUNTIME_SOURCE) pair that every downstream step references. Adding a # `repository_dispatch: types: [runtime-canary]` trigger later is purely # additive: add one more case that reads client_payload and defaults @@ -63,6 +68,13 @@ jobs: VERSION="$INPUT_VERSION" SOURCE="$INPUT_SOURCE" ;; + push) + # No dispatch inputs on push: fall back to the currently pinned + # runtime from public npm so the run is self-consistent and a green + # result proves the pipeline mechanics. + SOURCE="public" + VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" + ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -190,9 +202,14 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - # Normally publish only runs when the e2e gate is green. One bypass: - # workflow_dispatch with force_publish=true — a human-acknowledged flake - # override, audited via the ::warning:: step below and the run's actor. + # Normally publish only runs when the e2e gate is green. Two bypasses: + # - workflow_dispatch with force_publish=true: a human-acknowledged flake + # override (audited via the actor on the run). + # - github.event_name == 'push': TEMP: remove at finalize — branch-validation + # bypass coupled with the temporary push trigger so we can exercise the + # publish + bypass paths on this branch. Drop the `github.event_name == + # 'push'` clause from the `if:` below together with the push trigger; after + # that, force is honored only on real dispatches. # The bypass only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > @@ -200,7 +217,8 @@ jobs: github.event.repository.fork == false && needs.resolve.result == 'success' && (needs.test.result == 'success' || - (github.event_name == 'workflow_dispatch' && inputs.force_publish)) + (github.event_name == 'workflow_dispatch' && inputs.force_publish) || + github.event_name == 'push') environment: cicd runs-on: ubuntu-latest permissions: From 4380414bc1bdc5915dc2e5cd80e5401f06295c40 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:33:08 -0700 Subject: [PATCH 09/23] Reapply "Finalize SDK canary: remove temporary branch-validation bypasses" This reverts commit 925844ea76356f27bb214ff497a4f77aa293f6e5. --- .github/workflows/sdk-canary.yml | 38 +++++++++----------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 53ce452d2..7033aad0f 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,9 +1,10 @@ name: "SDK Canary (runtime compat gate)" -# Nightly-style canary compatibility gate: installs an explicit version of the -# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite -# against it. This proves runtime <-> SDK compatibility. No publishing happens -# here. +# Canary compatibility gate: 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. On a green gate it publishes an SDK canary +# (pinned to the exact tested runtime) to the internal copilot-canary-test feed. +# Triggered manually via workflow_dispatch. env: HUSKY: 0 @@ -28,12 +29,6 @@ on: required: false type: boolean default: false - # TEMPORARY: branch-push trigger so we can validate the pipeline before the - # workflow_dispatch entrypoint exists on main. Push events carry no dispatch - # inputs, so the resolve job falls back to the currently pinned runtime from - # public npm. Remove this trigger once the workflow lands on main. - push: # TEMP: remove at finalize (coupled with the publish.if `event==push` clause) - branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -50,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v6.0.2 - # Normalize whichever trigger fired into a single (RUNTIME_VERSION, + # Normalize the dispatch inputs into a single (RUNTIME_VERSION, # RUNTIME_SOURCE) pair that every downstream step references. Adding a # `repository_dispatch: types: [runtime-canary]` trigger later is purely # additive: add one more case that reads client_payload and defaults @@ -68,13 +63,6 @@ jobs: VERSION="$INPUT_VERSION" SOURCE="$INPUT_SOURCE" ;; - push) - # No dispatch inputs on push: fall back to the currently pinned - # runtime from public npm so the run is self-consistent and a green - # result proves the pipeline mechanics. - SOURCE="public" - VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" - ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -202,14 +190,9 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - # Normally publish only runs when the e2e gate is green. Two bypasses: - # - workflow_dispatch with force_publish=true: a human-acknowledged flake - # override (audited via the actor on the run). - # - github.event_name == 'push': TEMP: remove at finalize — branch-validation - # bypass coupled with the temporary push trigger so we can exercise the - # publish + bypass paths on this branch. Drop the `github.event_name == - # 'push'` clause from the `if:` below together with the push trigger; after - # that, force is honored only on real dispatches. + # Normally publish only runs when the e2e gate is green. One bypass: + # workflow_dispatch with force_publish=true — a human-acknowledged flake + # override, audited via the ::warning:: step below and the run's actor. # The bypass only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > @@ -217,8 +200,7 @@ jobs: github.event.repository.fork == false && needs.resolve.result == 'success' && (needs.test.result == 'success' || - (github.event_name == 'workflow_dispatch' && inputs.force_publish) || - github.event_name == 'push') + (github.event_name == 'workflow_dispatch' && inputs.force_publish)) environment: cicd runs-on: ubuntu-latest permissions: From 58a4767415492605de72ba617f5b2e89f6a1b413 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:35:52 -0700 Subject: [PATCH 10/23] Revert "Reapply "Finalize SDK canary: remove temporary branch-validation bypasses"" This reverts commit 4380414bc1bdc5915dc2e5cd80e5401f06295c40. --- .github/workflows/sdk-canary.yml | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 7033aad0f..53ce452d2 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,10 +1,9 @@ name: "SDK Canary (runtime compat gate)" -# Canary compatibility gate: 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. On a green gate it publishes an SDK canary -# (pinned to the exact tested runtime) to the internal copilot-canary-test feed. -# Triggered manually via workflow_dispatch. +# Nightly-style canary compatibility gate: installs an explicit version of the +# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite +# against it. This proves runtime <-> SDK compatibility. No publishing happens +# here. env: HUSKY: 0 @@ -29,6 +28,12 @@ on: required: false type: boolean default: false + # TEMPORARY: branch-push trigger so we can validate the pipeline before the + # workflow_dispatch entrypoint exists on main. Push events carry no dispatch + # inputs, so the resolve job falls back to the currently pinned runtime from + # public npm. Remove this trigger once the workflow lands on main. + push: # TEMP: remove at finalize (coupled with the publish.if `event==push` clause) + branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -45,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v6.0.2 - # Normalize the dispatch inputs into a single (RUNTIME_VERSION, + # Normalize whichever trigger fired into a single (RUNTIME_VERSION, # RUNTIME_SOURCE) pair that every downstream step references. Adding a # `repository_dispatch: types: [runtime-canary]` trigger later is purely # additive: add one more case that reads client_payload and defaults @@ -63,6 +68,13 @@ jobs: VERSION="$INPUT_VERSION" SOURCE="$INPUT_SOURCE" ;; + push) + # No dispatch inputs on push: fall back to the currently pinned + # runtime from public npm so the run is self-consistent and a green + # result proves the pipeline mechanics. + SOURCE="public" + VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" + ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -190,9 +202,14 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - # Normally publish only runs when the e2e gate is green. One bypass: - # workflow_dispatch with force_publish=true — a human-acknowledged flake - # override, audited via the ::warning:: step below and the run's actor. + # Normally publish only runs when the e2e gate is green. Two bypasses: + # - workflow_dispatch with force_publish=true: a human-acknowledged flake + # override (audited via the actor on the run). + # - github.event_name == 'push': TEMP: remove at finalize — branch-validation + # bypass coupled with the temporary push trigger so we can exercise the + # publish + bypass paths on this branch. Drop the `github.event_name == + # 'push'` clause from the `if:` below together with the push trigger; after + # that, force is honored only on real dispatches. # The bypass only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > @@ -200,7 +217,8 @@ jobs: github.event.repository.fork == false && needs.resolve.result == 'success' && (needs.test.result == 'success' || - (github.event_name == 'workflow_dispatch' && inputs.force_publish)) + (github.event_name == 'workflow_dispatch' && inputs.force_publish) || + github.event_name == 'push') environment: cicd runs-on: ubuntu-latest permissions: From a1adab7c1bc59716acfa458cea8da46cefcfd84f Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 16:37:51 -0700 Subject: [PATCH 11/23] Replace force_publish boolean with a mode enum workflow_dispatch now takes mode: publish (default, tests must pass), publish-force (publish even on a failed gate), or tests-only (run the gate, never publish). The resolve job normalizes a PUBLISH_MODE output (only a human dispatch can pick a non-default mode; automated triggers are always 'publish'), publish.if gates on it, and the audit warning fires specifically for publish-force on a non-green gate. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 68 +++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 53ce452d2..c8a6889a1 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -23,11 +23,15 @@ on: - public - feed default: public - force_publish: - description: "Publish the SDK canary even if the e2e gate fails (use ONLY for a known flake). Bypasses the e2e signal; the build + feed-only guards still apply." + mode: + description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)" required: false - type: boolean - default: false + type: choice + default: publish + options: + - publish + - publish-force + - tests-only # TEMPORARY: branch-push trigger so we can validate the pipeline before the # workflow_dispatch entrypoint exists on main. Push events carry no dispatch # inputs, so the resolve job falls back to the currently pinned runtime from @@ -47,33 +51,40 @@ jobs: outputs: RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} + PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} steps: - uses: actions/checkout@v6.0.2 # Normalize whichever trigger fired into a single (RUNTIME_VERSION, - # RUNTIME_SOURCE) pair that every downstream step references. Adding a - # `repository_dispatch: types: [runtime-canary]` trigger later is purely - # additive: add one more case that reads client_payload and defaults - # source to 'feed'. No downstream rework required. + # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step + # references. Adding a `repository_dispatch: types: [runtime-canary]` + # trigger later is purely additive: add one more case that reads + # client_payload, defaults source to 'feed', and mode to 'publish'. No + # downstream rework required. - 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 }} run: | set -euo pipefail case "$EVENT_NAME" in workflow_dispatch) VERSION="$INPUT_VERSION" SOURCE="$INPUT_SOURCE" + # Only a human dispatch may pick a non-default publish mode. + MODE="$INPUT_MODE" ;; push) # No dispatch inputs on push: fall back to the currently pinned # runtime from public npm so the run is self-consistent and a green - # result proves the pipeline mechanics. + # result proves the pipeline mechanics. Automated triggers always + # publish (green-gated) — they can never force or skip. SOURCE="public" VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" + MODE="publish" ;; *) echo "::error::Unsupported event '$EVENT_NAME'." @@ -82,9 +93,15 @@ jobs: esac if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi if [ -z "$SOURCE" ]; then SOURCE="public"; fi - echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE" + 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: @@ -202,22 +219,27 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - # Normally publish only runs when the e2e gate is green. Two bypasses: - # - workflow_dispatch with force_publish=true: a human-acknowledged flake - # override (audited via the actor on the run). + # Publish runs only when the gate permits it. Mode (human dispatch only) + # governs behavior: + # - tests-only: never publish (skips this job entirely). + # - publish: publish only when the e2e gate is green (the default; the only + # mode automated triggers ever get). + # - publish-force: publish even on a non-green gate — a human-acknowledged + # flake override, audited via the ::warning:: step below and the run actor. # - github.event_name == 'push': TEMP: remove at finalize — branch-validation # bypass coupled with the temporary push trigger so we can exercise the - # publish + bypass paths on this branch. Drop the `github.event_name == - # 'push'` clause from the `if:` below together with the push trigger; after - # that, force is honored only on real dispatches. + # publish path on this branch. Drop the `github.event_name == 'push'` + # clause from the `if:` below together with the push trigger; after that, + # the only publish-on-red path is publish-force on a real dispatch. # The bypass only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > !cancelled() && github.event.repository.fork == false && needs.resolve.result == 'success' && + needs.resolve.outputs.PUBLISH_MODE != 'tests-only' && (needs.test.result == 'success' || - (github.event_name == 'workflow_dispatch' && inputs.force_publish) || + needs.resolve.outputs.PUBLISH_MODE == 'publish-force' || github.event_name == 'push') environment: cicd runs-on: ubuntu-latest @@ -234,15 +256,15 @@ jobs: shell: bash working-directory: ./nodejs steps: - - name: Warn — publishing despite failed e2e gate (bypass) + - 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 without a green e2e gate. 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' + # 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 }}). Triggered by '${{ github.actor }}' via '${{ github.event_name }}'${{ (github.event_name == 'workflow_dispatch' && inputs.force_publish) && ' with force_publish=true' || '' }}. The e2e signal was bypassed; build + feed-only + read-back guards still apply." + 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 + read-back guards still apply." - uses: actions/checkout@v6.0.2 From 20f1280921b8093c3d991ded1407821dc267b122 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 17:00:15 -0700 Subject: [PATCH 12/23] Address PR review feedback on sdk-canary workflow Batch of review-response changes to .github/workflows/sdk-canary.yml: - Rename workflow to "SDK Canary Test/Publish". - Rename runtime_source value "feed" -> "internal" (option, guards, comment). - Rename test job to "E2E tests ()". - Hoist the feed URL and ADO resource id into workflow-level env (FEED_URL, ADO_RESOURCE) as single sources of truth; derive .npmrc auth scopes from FEED_URL. - Add a per-ref concurrency group (cancel-in-progress: false) to avoid overlapping runs racing the feed publish. - Reuse scripts/get-version.js (current) for the public-latest lookup to stay consistent with publish.yml, keeping strict patch+1 semantics. - Tighten the semver validation regex to disallow underscores in the prerelease segment (invalid per SemVer/npm) in both the runtime-version and computed-SDK-version checks. - Verify the matched platform optional dependency version also matches the requested runtime version (not just that the package exists). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 71 ++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index c8a6889a1..61eb87760 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,4 +1,4 @@ -name: "SDK Canary (runtime compat gate)" +name: "SDK Canary Test/Publish" # Nightly-style canary compatibility gate: installs an explicit version of the # @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite @@ -7,6 +7,14 @@ name: "SDK Canary (runtime compat gate)" 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-test/npm/registry/ + # Azure DevOps resource ID used to mint an ADO access token for the feed + # (a well-known public constant, not a secret). + ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 on: workflow_dispatch: @@ -21,7 +29,7 @@ on: type: choice options: - public - - feed + - internal default: public mode: description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)" @@ -43,6 +51,12 @@ 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" @@ -59,7 +73,7 @@ jobs: # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step # references. Adding a `repository_dispatch: types: [runtime-canary]` # trigger later is purely additive: add one more case that reads - # client_payload, defaults source to 'feed', and mode to 'publish'. No + # client_payload, defaults source to 'internal', and mode to 'publish'. No # downstream rework required. - name: Normalize inputs id: normalize @@ -107,13 +121,13 @@ jobs: env: RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} run: | - if [[ ! "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]; then + if [[ ! "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; 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 (${{ matrix.os }})" + name: "E2E tests (${{ matrix.os }})" needs: resolve if: github.event.repository.fork == false environment: cicd @@ -147,7 +161,7 @@ jobs: run: npm ci --ignore-scripts - name: Azure Login (OIDC -> id-cpd-ci) - if: env.RUNTIME_SOURCE == 'feed' + if: env.RUNTIME_SOURCE == 'internal' uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 with: client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci @@ -159,15 +173,19 @@ jobs: # 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 == 'feed' + if: env.RUNTIME_SOURCE == 'internal' run: | set -euo pipefail - TOKEN="$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)" + 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="$(cat < .npmrc @@ -201,7 +219,12 @@ jobs: console.error(`::error::No @github/copilot platform optional dep for ${plat}-${arch}. Present: ${entries.join(", ") || "(none)"}`); process.exit(1); } - console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}`); + 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 @@ -248,9 +271,6 @@ jobs: id-token: write env: RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} - # Org-scoped internal TEST feed. The SDK canary MUST only ever go here, - # never to public npm (@github/copilot-sdk is a live public package). - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/registry/ defaults: run: shell: bash @@ -293,9 +313,11 @@ jobs: # prerelease of X.Y.Z always sorts below X.Y.Z). Public latest is # currently 1.0.6, so canaries look like 1.0.7-canary..g and # can never shadow the real 1.0.7 when it ships. - # Read public latest EXPLICITLY from public npm (never the feed: the - # feed has no public upstream and now holds our own canaries). Read-only. - PUBLIC_LATEST="$(npm view @github/copilot-sdk version --registry https://registry.npmjs.org/ 2>/dev/null || true)" + # 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 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 2>/dev/null || 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 ))" @@ -304,7 +326,7 @@ jobs: echo "::warning::Could not resolve public SDK latest; falling back to 0.0.0 canary base" fi SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" - if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]; then + if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver." exit 1 fi @@ -337,11 +359,16 @@ jobs: - name: Configure feed auth (.npmrc) run: | set -euo pipefail - TOKEN="$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv)" + 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/}" cat > .npmrc < Date: Wed, 8 Jul 2026 17:10:24 -0700 Subject: [PATCH 13/23] Point SDK canary at the copilot-canary feed Switch the canary feed from copilot-canary-test to the final org-scoped copilot-canary feed. The feed URL lives in a single place (env.FEED_URL); the test-job .npmrc, publish-job auth .npmrc, publishConfig assertion, and read-back all derive from it, so this one change repoints every step. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 61eb87760..16225001d 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -11,7 +11,7 @@ env: # 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-test/npm/registry/ + 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 # (a well-known public constant, not a secret). ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 From 7078f042648b7a612b55d774a4b46bac1c061001 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 17:20:43 -0700 Subject: [PATCH 14/23] Finalize SDK canary: remove temporary branch-validation bypasses Now that the pipeline is proven green against the copilot-canary feed, strip the on-branch validation scaffolding so the workflow is workflow_dispatch-only: - Remove the temporary push: trigger scoped to the feature branch. - Remove the push-event fallback case in the resolve/normalize step. - Remove the github.event_name == 'push' clause from publish.if. Version scheme, mode enum, and the copilot-canary feed are all retained. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 16225001d..6c27bfe9f 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -40,12 +40,6 @@ on: - publish - publish-force - tests-only - # TEMPORARY: branch-push trigger so we can validate the pipeline before the - # workflow_dispatch entrypoint exists on main. Push events carry no dispatch - # inputs, so the resolve job falls back to the currently pinned runtime from - # public npm. Remove this trigger once the workflow lands on main. - push: # TEMP: remove at finalize (coupled with the publish.if `event==push` clause) - branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -91,15 +85,6 @@ jobs: # Only a human dispatch may pick a non-default publish mode. MODE="$INPUT_MODE" ;; - push) - # No dispatch inputs on push: fall back to the currently pinned - # runtime from public npm so the run is self-consistent and a green - # result proves the pipeline mechanics. Automated triggers always - # publish (green-gated) — they can never force or skip. - SOURCE="public" - VERSION="$(node -e "const v=require('./nodejs/package.json').dependencies['@github/copilot']; process.stdout.write(String(v).replace(/^[\^~]/, ''))")" - MODE="publish" - ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -249,12 +234,7 @@ jobs: # mode automated triggers ever get). # - publish-force: publish even on a non-green gate — a human-acknowledged # flake override, audited via the ::warning:: step below and the run actor. - # - github.event_name == 'push': TEMP: remove at finalize — branch-validation - # bypass coupled with the temporary push trigger so we can exercise the - # publish path on this branch. Drop the `github.event_name == 'push'` - # clause from the `if:` below together with the push trigger; after that, - # the only publish-on-red path is publish-force on a real dispatch. - # The bypass only skips the e2e *signal* — the publish job still runs the + # publish-force only skips the e2e *signal* — the publish job still runs the # build (so a broken build can't publish) and enforces feed-only + read-back. if: > !cancelled() && @@ -262,8 +242,7 @@ jobs: needs.resolve.result == 'success' && needs.resolve.outputs.PUBLISH_MODE != 'tests-only' && (needs.test.result == 'success' || - needs.resolve.outputs.PUBLISH_MODE == 'publish-force' || - github.event_name == 'push') + needs.resolve.outputs.PUBLISH_MODE == 'publish-force') environment: cicd runs-on: ubuntu-latest permissions: From 147d9f77c5d4345dcd5f47baae2183d755f7d874 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 19:41:40 -0700 Subject: [PATCH 15/23] Harden semver validation and .npmrc writes in sdk-canary Address PR review feedback: - Tighten the SemVer prerelease regex to require non-empty dot-separated identifiers ((-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?), so malformed values like 1.0.7-canary..gsha are rejected at validation instead of later at npm. Applied to both the runtime-version and computed-SDK-version checks. - Write both .npmrc files with printf instead of an indented heredoc. The heredoc already produced correct column-0 keys (the YAML run block dedents to column 0, proven by the successful live publish + read-back), but printf makes the intended bytes unambiguous and avoids recurring reviewer confusion. Output is byte-identical. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 6c27bfe9f..19a470d3b 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -106,7 +106,7 @@ jobs: env: RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} run: | - if [[ ! "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + if [[ ! "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-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 @@ -167,12 +167,10 @@ jobs: # name lives in exactly one place (the workflow-level env). FEED_AUTH_REGISTRY="${FEED_URL#https:}" FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - NPMRC="$(cat < .npmrc printf '%s\n' "$NPMRC" > ../test/harness/.npmrc echo "Wrote scoped @github registry .npmrc to ./nodejs and ./test/harness" @@ -305,7 +303,7 @@ jobs: echo "::warning::Could not resolve public SDK latest; falling back to 0.0.0 canary base" fi SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" - if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then + if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-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 @@ -345,10 +343,9 @@ jobs: # supplied explicitly via publishConfig + --registry. FEED_AUTH_REGISTRY="${FEED_URL#https:}" FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - cat > .npmrc < .npmrc echo "Wrote auth-only .npmrc to ./nodejs" # Belt and suspenders (2 of 3): pin the publish target in the package too. From 0edff0c09443bc7909c9dff28b7b4740fd1d77c8 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 19:58:14 -0700 Subject: [PATCH 16/23] Add publish run summary; remove read-back verify step Replace the post-publish read-back (npm view against the feed) with a GITHUB_STEP_SUMMARY panel showing the runtime consumed and canary SDK produced. The read-back was not a repo standard (publish.yml trusts the npm publish exit code), was exposed to Azure Artifacts feed-propagation lag (risking false failures), and was redundant: the version is guaranteed by the publish exit code and the runtime pin is set deterministically in the same job under set -euo pipefail. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 19a470d3b..a6633724b 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -233,7 +233,7 @@ jobs: # - 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 feed-only + read-back. + # build (so a broken build can't publish) and enforces the feed-only guards. if: > !cancelled() && github.event.repository.fork == false && @@ -261,7 +261,7 @@ jobs: 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 + read-back guards still apply." + 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 @@ -367,21 +367,17 @@ jobs: - name: Publish SDK canary to internal feed run: npm publish --registry "$FEED_URL" - - name: Read-back verify published canary + - name: Summarize published canary env: SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} run: | set -euo pipefail - PUBLISHED="$(npm view "@github/copilot-sdk@${SDK_VERSION}" version --registry "$FEED_URL")" - PINNED="$(npm view "@github/copilot-sdk@${SDK_VERSION}" dependencies.@github/copilot --registry "$FEED_URL")" - echo "Published version: $PUBLISHED" - echo "Pinned @github/copilot: $PINNED" - if [ "$PUBLISHED" != "$SDK_VERSION" ]; then - echo "::error::Read-back version '$PUBLISHED' != published '$SDK_VERSION'." - exit 1 - fi - if [ "$PINNED" != "$RUNTIME_VERSION" ]; then - echo "::error::Read-back runtime pin '$PINNED' != tested '$RUNTIME_VERSION'." - exit 1 - fi - echo "Read-back OK: @github/copilot-sdk@${SDK_VERSION} exact-depends on @github/copilot@${RUNTIME_VERSION}" + { + 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" From 9c2f498750d1842a60fbaf285a3d38874234806a Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 20:56:19 -0700 Subject: [PATCH 17/23] Fix stale/misleading comments; drop unused resolve checkout - Rewrite the file header: it claimed 'No publishing happens here', but the workflow tests AND publishes an SDK canary to the internal feed. - Remove actions/checkout from the resolve job; it only normalizes dispatch inputs and validates the version, never touching repo files. - Reword the canary version-base comment to describe the mechanism (next patch of public latest) instead of a hardcoded version that rots. - Fix a doubled-word typo (read read-only -> read-only). - Note that the runtime verify targets the ./nodejs copy (the one the SDK actually spawns during e2e; the harness is a mock CAPI proxy). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index a6633724b..01139c776 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,9 +1,10 @@ name: "SDK Canary Test/Publish" -# Nightly-style canary compatibility gate: installs an explicit version of the +# 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. This proves runtime <-> SDK compatibility. No publishing happens -# here. +# 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 @@ -61,8 +62,9 @@ jobs: RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} steps: - - uses: actions/checkout@v6.0.2 - + # No checkout: this job only normalizes dispatch inputs and validates the + # version string — it never touches repo files. + # # Normalize whichever trigger fired into a single (RUNTIME_VERSION, # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step # references. Adding a `repository_dispatch: types: [runtime-canary]` @@ -182,6 +184,8 @@ jobs: npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts ( cd ../test/harness && npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts ) + # Verify the ./nodejs copy, which is the one the SDK actually spawns during + # e2e (the harness is a mock CAPI proxy and never launches the runtime). - name: Verify installed runtime run: | set -euo pipefail @@ -287,12 +291,11 @@ jobs: # 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). Public latest is - # currently 1.0.6, so canaries look like 1.0.7-canary..g and - # can never shadow the real 1.0.7 when it ships. + # 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 read-only from public npm (never the feed), then + # 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 2>/dev/null || true)" BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}" From 27f0e2bffb4352aa78b419db1ae9b4e40b4c2922 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 21:05:27 -0700 Subject: [PATCH 18/23] Drop unused test-harness runtime override and feed .npmrc The harness is the mock CAPI replay proxy: it declares @github/copilot as a devDependency but never imports or spawns it (verified: zero source imports). During e2e the SDK resolves and launches the runtime from ./nodejs's own node_modules, so overriding the runtime inside ./test/harness had no effect on what the compat gate exercises. Removing the harness override also removes the only reason to write the feed .npmrc there, shrinking the internal-source path and eliminating one place the ADO token was written to disk. Also folds in minor comment tidy-ups. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 01139c776..e5d25dc35 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -13,8 +13,7 @@ env: # (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 - # (a well-known public constant, not a secret). + # Azure DevOps resource ID used to mint an ADO access token for the feed. ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 on: @@ -62,9 +61,6 @@ jobs: RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} steps: - # No checkout: this job only normalizes dispatch inputs and validates the - # version string — it never touches repo files. - # # Normalize whichever trigger fired into a single (RUNTIME_VERSION, # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step # references. Adding a `repository_dispatch: types: [runtime-canary]` @@ -174,18 +170,18 @@ jobs: "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ "${FEED_AUTH_BASE}:_authToken=${TOKEN}")" printf '%s\n' "$NPMRC" > .npmrc - printf '%s\n' "$NPMRC" > ../test/harness/.npmrc - echo "Wrote scoped @github registry .npmrc to ./nodejs and ./test/harness" + echo "Wrote scoped @github registry .npmrc to ./nodejs" + # Only ./nodejs needs the runtime override: the SDK resolves and spawns the + # runtime from its own node_modules during e2e. The harness is a mock CAPI + # proxy that never imports or launches @github/copilot, so its pinned copy + # is left as-is (and needs no feed .npmrc). - 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 - ( cd ../test/harness && npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts ) - # Verify the ./nodejs copy, which is the one the SDK actually spawns during - # e2e (the harness is a mock CAPI proxy and never launches the runtime). - name: Verify installed runtime run: | set -euo pipefail From 78b727cb26bb58545e83623a626e77cbd8e78734 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 21:26:24 -0700 Subject: [PATCH 19/23] Harden version resolution, validate source, tighten resolve perms - Fail the publish job (instead of falling back to a 0.0.0 canary base) when the public SDK latest version can't be resolved, so a transient npm failure can't publish a misleading canary for a live public package. Also stop swallowing get-version.js stderr so failures surface in logs. - Validate runtime_source (public|internal) in the resolve normalize step, mirroring the existing mode validation, so a future non-dispatch trigger can't pass an invalid source that silently behaves like public. - Use 'npm ci --ignore-scripts' in the publish job for consistency with the test-job installs and the canonical publish.yml. - Give the resolve job 'permissions: {}'; it only normalizes inputs and needs neither repo contents nor an OIDC token. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index e5d25dc35..a7b491d8a 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -56,6 +56,7 @@ jobs: 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 }} @@ -90,6 +91,10 @@ jobs: 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) ;; @@ -172,10 +177,6 @@ jobs: printf '%s\n' "$NPMRC" > .npmrc echo "Wrote scoped @github registry .npmrc to ./nodejs" - # Only ./nodejs needs the runtime override: the SDK resolves and spawns the - # runtime from its own node_modules during e2e. The harness is a mock CAPI - # proxy that never imports or launches @github/copilot, so its pinned copy - # is left as-is (and needs no feed .npmrc). - name: Override runtime version run: | set -euo pipefail @@ -274,7 +275,7 @@ jobs: # here, or npm ci would try to fetch the runtime from the upstream-less # feed and 404. - name: Install SDK dependencies - run: npm ci + run: npm ci --ignore-scripts - name: Compute SDK canary version id: sdkver @@ -293,13 +294,13 @@ jobs: # 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 2>/dev/null || true)" + 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 - NEXT="0.0.0" - echo "::warning::Could not resolve public SDK latest; falling back to 0.0.0 canary base" + 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-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then From 5fd0c68e8de5261c545df62411467f461d8655b7 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 21:28:12 -0700 Subject: [PATCH 20/23] TEMP: re-add branch-push validation trigger Temporarily re-adds the on-branch validation scaffolding (push trigger + resolve push case + publish.if event==push clause) to exercise the full pipeline after the recent augmentations. To be removed after the run. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index a7b491d8a..57b82e551 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -40,6 +40,13 @@ on: - publish - publish-force - tests-only + # TEMPORARY: branch-push trigger so we can validate the pipeline before the + # workflow_dispatch entrypoint exists on main. Push events carry no dispatch + # inputs, so the resolve job falls back to the currently pinned runtime from + # public npm. Remove this trigger once validation is done (coupled with the + # resolve `push)` case and the publish.if `event==push` clause). + push: # TEMP: remove after on-branch validation + branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -84,6 +91,15 @@ jobs: # Only a human dispatch may pick a non-default publish mode. MODE="$INPUT_MODE" ;; + push) + # TEMP (remove with the push trigger): no dispatch inputs on push, + # so use the currently pinned public runtime. A green run proves the + # pipeline mechanics end to end on-branch. Hardcoded (not read from + # package.json) because the resolve job has no checkout. + SOURCE="public" + VERSION="1.0.69" + MODE="publish" + ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -235,13 +251,16 @@ jobs: # 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. + # TEMP: the `github.event_name == 'push'` clause below is a branch-validation + # bypass coupled with the temporary push trigger; remove all three together. 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') + needs.resolve.outputs.PUBLISH_MODE == 'publish-force' || + github.event_name == 'push') environment: cicd runs-on: ubuntu-latest permissions: From f6625c79f9d22b14c6f13bc8623d37cf6a96872a Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 21:43:41 -0700 Subject: [PATCH 21/23] Revert "TEMP: re-add branch-push validation trigger" This reverts commit 5fd0c68e8de5261c545df62411467f461d8655b7. --- .github/workflows/sdk-canary.yml | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 57b82e551..a7b491d8a 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -40,13 +40,6 @@ on: - publish - publish-force - tests-only - # TEMPORARY: branch-push trigger so we can validate the pipeline before the - # workflow_dispatch entrypoint exists on main. Push events carry no dispatch - # inputs, so the resolve job falls back to the currently pinned runtime from - # public npm. Remove this trigger once validation is done (coupled with the - # resolve `push)` case and the publish.if `event==push` clause). - push: # TEMP: remove after on-branch validation - branches: [mackinnonbuck-sdk-canary-compat-gate] permissions: contents: read @@ -91,15 +84,6 @@ jobs: # Only a human dispatch may pick a non-default publish mode. MODE="$INPUT_MODE" ;; - push) - # TEMP (remove with the push trigger): no dispatch inputs on push, - # so use the currently pinned public runtime. A green run proves the - # pipeline mechanics end to end on-branch. Hardcoded (not read from - # package.json) because the resolve job has no checkout. - SOURCE="public" - VERSION="1.0.69" - MODE="publish" - ;; *) echo "::error::Unsupported event '$EVENT_NAME'." exit 1 @@ -251,16 +235,13 @@ jobs: # 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. - # TEMP: the `github.event_name == 'push'` clause below is a branch-validation - # bypass coupled with the temporary push trigger; remove all three together. 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' || - github.event_name == 'push') + needs.resolve.outputs.PUBLISH_MODE == 'publish-force') environment: cicd runs-on: ubuntu-latest permissions: From e0c8e246875c45a42defc6f9d809a4d41b805a72 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 8 Jul 2026 22:05:44 -0700 Subject: [PATCH 22/23] Reject leading-zero version cores in semver validation Tighten both semver checks (runtime version and computed SDK canary version) to disallow leading zeros in major/minor/patch, e.g. 01.0.0, which are invalid SemVer and would be rejected later by npm. Uses the standard (0|[1-9][0-9]*) core pattern; the prerelease portion is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index a7b491d8a..302b9bb59 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -109,7 +109,7 @@ jobs: env: RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} run: | - if [[ ! "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + 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 @@ -303,7 +303,7 @@ jobs: exit 1 fi SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" - if [[ ! "$SDK_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + 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 From 4609e5f2f3c206554fc9822b5d4cba9c17ed1cbc Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 9 Jul 2026 10:43:21 -0700 Subject: [PATCH 23/23] Add repository_dispatch receiver for runtime canary chaining Add a repository_dispatch trigger (type runtime-canary) so the runtime's nightly canary pipeline can chain into this workflow: it re-tests and publishes the SDK against the freshly published runtime canary. The resolve job's Normalize step gains a repository_dispatch case that reads runtime_version (required), runtime_source (optional, forced to internal since a runtime canary only lives on the feed), and mode (optional, defaults publish) from client_payload via env. The runtime version is semver-validated as before, and the existing fork guard still applies. The runtime sender is deferred; only the receiver is added here. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sdk-canary.yml | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 302b9bb59..95f8b1c92 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -40,6 +40,8 @@ on: - publish - publish-force - tests-only + repository_dispatch: + types: [runtime-canary] permissions: contents: read @@ -64,10 +66,9 @@ jobs: steps: # Normalize whichever trigger fired into a single (RUNTIME_VERSION, # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step - # references. Adding a `repository_dispatch: types: [runtime-canary]` - # trigger later is purely additive: add one more case that reads - # client_payload, defaults source to 'internal', and mode to 'publish'. No - # downstream rework required. + # 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: @@ -75,15 +76,23 @@ jobs: 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" - # Only a human dispatch may pick a non-default publish mode. 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 @@ -226,11 +235,10 @@ jobs: publish: name: "Publish SDK canary (internal feed)" needs: [resolve, test] - # Publish runs only when the gate permits it. Mode (human dispatch only) - # governs behavior: + # 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; the only - # mode automated triggers ever get). + # - 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