Skip to content
Open
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0336508
Add SDK canary runtime compatibility gate workflow
MackinnonBuck Jul 8, 2026
f3dd868
Add guarded publish job to SDK canary workflow
MackinnonBuck Jul 8, 2026
578c247
Add force_publish bypass for flaky e2e gate in SDK canary
MackinnonBuck Jul 8, 2026
05e0213
Harden canary bypass audit and mark temp branch-validation lines
MackinnonBuck Jul 8, 2026
79519e4
Fix canary bypass audit step failing before checkout
MackinnonBuck Jul 8, 2026
b289233
Finalize SDK canary: remove temporary branch-validation bypasses
MackinnonBuck Jul 8, 2026
20b7003
Base SDK canary version on next patch of public SDK latest
MackinnonBuck Jul 8, 2026
925844e
Revert "Finalize SDK canary: remove temporary branch-validation bypas…
MackinnonBuck Jul 8, 2026
4380414
Reapply "Finalize SDK canary: remove temporary branch-validation bypa…
MackinnonBuck Jul 8, 2026
58a4767
Revert "Reapply "Finalize SDK canary: remove temporary branch-validat…
MackinnonBuck Jul 8, 2026
a1adab7
Replace force_publish boolean with a mode enum
MackinnonBuck Jul 8, 2026
20f1280
Address PR review feedback on sdk-canary workflow
MackinnonBuck Jul 9, 2026
b6a6667
Point SDK canary at the copilot-canary feed
MackinnonBuck Jul 9, 2026
7078f04
Finalize SDK canary: remove temporary branch-validation bypasses
MackinnonBuck Jul 9, 2026
147d9f7
Harden semver validation and .npmrc writes in sdk-canary
MackinnonBuck Jul 9, 2026
0edff0c
Add publish run summary; remove read-back verify step
MackinnonBuck Jul 9, 2026
9c2f498
Fix stale/misleading comments; drop unused resolve checkout
MackinnonBuck Jul 9, 2026
27f0e2b
Drop unused test-harness runtime override and feed .npmrc
MackinnonBuck Jul 9, 2026
78b727c
Harden version resolution, validate source, tighten resolve perms
MackinnonBuck Jul 9, 2026
5fd0c68
TEMP: re-add branch-push validation trigger
MackinnonBuck Jul 9, 2026
f6625c7
Revert "TEMP: re-add branch-push validation trigger"
MackinnonBuck Jul 9, 2026
e0c8e24
Reject leading-zero version cores in semver validation
MackinnonBuck Jul 9, 2026
4609e5f
Add repository_dispatch receiver for runtime canary chaining
MackinnonBuck Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 390 additions & 0 deletions .github/workflows/sdk-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,390 @@
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
# against it. This proves runtime <-> SDK compatibility. No publishing happens
# here.

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
# (a well-known public constant, not a secret).
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

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
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, 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.
- 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"
;;
*)
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
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-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$ ]]; then
Comment thread
MackinnonBuck marked this conversation as resolved.
Outdated
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="$(cat <<EOF
@github:registry=${FEED_URL}
${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}
${FEED_AUTH_BASE}:_authToken=${TOKEN}
EOF
)"
printf '%s\n' "$NPMRC" > .npmrc
printf '%s\n' "$NPMRC" > ../test/harness/.npmrc
echo "Wrote scoped @github registry .npmrc to ./nodejs and ./test/harness"
Comment thread
MackinnonBuck marked this conversation as resolved.
Outdated

- 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);
}
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 (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.
# 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() &&
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 + read-back 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

- 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). Public latest is
# currently 1.0.6, so canaries look like 1.0.7-canary.<run>.g<sha> and
# can never shadow the real 1.0.7 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
# 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 ))"
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
Comment thread
MackinnonBuck marked this conversation as resolved.
Outdated
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/}"
cat > .npmrc <<EOF
${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}
${FEED_AUTH_BASE}:_authToken=${TOKEN}
EOF
echo "Wrote auth-only .npmrc to ./nodejs"
Comment thread
MackinnonBuck marked this conversation as resolved.
Outdated

# 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: Read-back verify 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}"
Loading