Skip to content

Point SDK canary at the copilot-canary feed #8

Point SDK canary at the copilot-canary feed

Point SDK canary at the copilot-canary feed #8

Workflow file for this run

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
# 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
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"
;;
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
;;
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
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"
- 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.
# - 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
# 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' ||
github.event_name == 'push')
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
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"
# 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}"