Skip to content

runtime-canary

runtime-canary #10

Workflow file for this run

name: "SDK Canary Test/Publish"
# Nightly-style canary pipeline. First installs an explicit version of the
# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite
# against it to prove runtime <-> SDK compatibility. When that gate passes (and
# mode allows), publishes an SDK canary pinned to the tested runtime to the
# internal Azure Artifacts feed only (never public npm).
env:
HUSKY: 0
# Internal org-scoped Azure Artifacts feed — single source of truth so the
# feed name isn't repeated across steps. The SDK canary publishes here and
# (when runtime_source=internal) installs the runtime from here; it must NEVER
# reach public npm (@github/copilot-sdk is a live public package).
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/
# Azure DevOps resource ID used to mint an ADO access token for the feed.
ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798
on:
workflow_dispatch:
inputs:
runtime_version:
description: "Exact @github/copilot version to test (e.g. 1.0.69 or 1.0.70-canary.<sha>)"
required: true
type: string
runtime_source:
description: "Where to install the runtime from"
required: true
type: choice
options:
- public
- internal
default: public
mode:
description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)"
required: false
type: choice
default: publish
options:
- publish
- publish-force
- tests-only
repository_dispatch:
types: [runtime-canary]
permissions:
contents: read
id-token: write
# Serialize runs per ref so two overlapping canary runs can't race the feed
# publish. cancel-in-progress: false — never kill an in-flight publish.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
resolve:
name: "Resolve runtime inputs"
if: github.event.repository.fork == false
runs-on: ubuntu-latest
permissions: {}
outputs:
RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }}
RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }}
PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }}
steps:
# Normalize whichever trigger fired into a single (RUNTIME_VERSION,
# RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step
# references. workflow_dispatch reads the human-supplied inputs;
# repository_dispatch reads client_payload and forces source=internal
# (a runtime canary only exists on the feed), defaulting mode to publish.
- name: Normalize inputs
id: normalize
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ inputs.runtime_version }}
INPUT_SOURCE: ${{ inputs.runtime_source }}
INPUT_MODE: ${{ inputs.mode }}
PAYLOAD_VERSION: ${{ github.event.client_payload.runtime_version }}
PAYLOAD_SOURCE: ${{ github.event.client_payload.runtime_source }}
PAYLOAD_MODE: ${{ github.event.client_payload.mode }}
run: |
set -euo pipefail
case "$EVENT_NAME" in
workflow_dispatch)
VERSION="$INPUT_VERSION"
SOURCE="$INPUT_SOURCE"
MODE="$INPUT_MODE"
;;
repository_dispatch)
VERSION="$PAYLOAD_VERSION"
# A runtime canary only ever exists on the internal feed.
SOURCE="${PAYLOAD_SOURCE:-internal}"
MODE="${PAYLOAD_MODE:-publish}"
;;
*)
echo "::error::Unsupported event '$EVENT_NAME'."
exit 1
;;
esac
if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi
if [ -z "$SOURCE" ]; then SOURCE="public"; fi
case "$SOURCE" in
public|internal) ;;
*) echo "::error::Invalid runtime source '$SOURCE'. Expected one of: public, internal."; exit 1 ;;
esac
if [ -z "$MODE" ]; then MODE="publish"; fi
case "$MODE" in
publish|publish-force|tests-only) ;;
*) echo "::error::Invalid publish mode '$MODE'. Expected one of: publish, publish-force, tests-only."; exit 1 ;;
esac
echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE PUBLISH_MODE=$MODE"
echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT"
echo "PUBLISH_MODE=$MODE" >> "$GITHUB_OUTPUT"
- name: Validate runtime version (semver)
env:
RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }}
run: |
if [[ ! "$RUNTIME_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "::error::Invalid runtime version '$RUNTIME_VERSION'. Expected semver (e.g. 1.0.69 or 1.0.70-canary.abc123)."
exit 1
fi
test:
name: "E2E tests (${{ matrix.os }})"
needs: resolve
if: github.event.repository.fork == false
environment: cicd
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
env:
POWERSHELL_UPDATECHECK: Off
RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }}
RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }}
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6
with:
cache: "npm"
cache-dependency-path: "./nodejs/package-lock.json"
node-version: 22
- name: Install SDK dependencies
run: npm ci --ignore-scripts
- name: Install test harness dependencies
working-directory: ./test/harness
run: npm ci --ignore-scripts
- name: Azure Login (OIDC -> id-cpd-ci)
if: env.RUNTIME_SOURCE == 'internal'
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci
tenant-id: "${{ vars.CPD_ID_TENANT_ID }}"
allow-no-subscriptions: true
# Route ONLY @github/* (the runtime + its 8 platform packages) to the
# internal feed via a scoped registry. All other deps (e.g. detect-libc)
# still resolve from public npm. A global --registry would break because
# detect-libc is not on the feed.
- name: Configure canary feed (.npmrc)
if: env.RUNTIME_SOURCE == 'internal'
run: |
set -euo pipefail
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
# Derive the protocol-relative auth scopes from FEED_URL so the feed
# name lives in exactly one place (the workflow-level env).
FEED_AUTH_REGISTRY="${FEED_URL#https:}"
FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}"
NPMRC="$(printf '%s\n' \
"@github:registry=${FEED_URL}" \
"${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \
"${FEED_AUTH_BASE}:_authToken=${TOKEN}")"
printf '%s\n' "$NPMRC" > .npmrc
echo "Wrote scoped @github registry .npmrc to ./nodejs"
- name: Override runtime version
run: |
set -euo pipefail
echo "Installing @github/copilot@${RUNTIME_VERSION} (source: ${RUNTIME_SOURCE})"
npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts
- name: Verify installed runtime
run: |
set -euo pipefail
node -e '
const fs = require("fs");
const expected = process.env.RUNTIME_VERSION;
const pkg = require("./node_modules/@github/copilot/package.json");
if (pkg.version !== expected) {
console.error(`::error::Installed @github/copilot version ${pkg.version} does not match requested ${expected}`);
process.exit(1);
}
const dir = "./node_modules/@github";
const entries = fs.readdirSync(dir).filter((d) => d.startsWith("copilot-"));
const plat = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux";
const arch = process.arch;
const match = entries.find((d) => d.includes(plat) && d.includes(arch));
if (!match) {
console.error(`::error::No @github/copilot platform optional dep for ${plat}-${arch}. Present: ${entries.join(", ") || "(none)"}`);
process.exit(1);
}
const platPkg = require(`${dir}/${match}/package.json`);
if (platPkg.version !== expected) {
console.error(`::error::Platform package @github/${match} version ${platPkg.version} does not match requested ${expected}`);
process.exit(1);
}
console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}@${platPkg.version}`);
'
- name: Build SDK
run: npm run build
- name: Warm up PowerShell
if: runner.os == 'Windows'
run: pwsh.exe -Command "Write-Host 'PowerShell ready'"
- name: Run Node.js SDK e2e tests
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: npm test
publish:
name: "Publish SDK canary (internal feed)"
needs: [resolve, test]
# Publish runs only when the gate permits it. Mode governs behavior:
# - tests-only: never publish (skips this job entirely).
# - publish: publish only when the e2e gate is green (the default for both
# the human and automated triggers).
# - publish-force: publish even on a non-green gate — a human-acknowledged
# flake override, audited via the ::warning:: step below and the run actor.
# publish-force only skips the e2e *signal* — the publish job still runs the
# build (so a broken build can't publish) and enforces the feed-only guards.
if: >
!cancelled() &&
github.event.repository.fork == false &&
needs.resolve.result == 'success' &&
needs.resolve.outputs.PUBLISH_MODE != 'tests-only' &&
(needs.test.result == 'success' ||
needs.resolve.outputs.PUBLISH_MODE == 'publish-force')
environment: cicd
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
env:
RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }}
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- name: Warn — publishing despite failed e2e gate (publish-force)
# always() so this audit is never skipped by prior-step status; it fires
# specifically when publish proceeded on a non-green gate via publish-force.
# Runs at the workspace root because it executes before checkout, so the
# job's default working-directory (./nodejs) does not exist yet.
if: always() && needs.test.result != 'success' && needs.resolve.outputs.PUBLISH_MODE == 'publish-force'
working-directory: ${{ github.workspace }}
run: |
echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}) via publish-force. Triggered by '${{ github.actor }}' through '${{ github.event_name }}'. The e2e signal was bypassed; build + feed-only guards still apply."
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6
with:
node-version: 22
# Default public registry: installs build deps and the currently pinned
# runtime. Do NOT write any feed .npmrc or scoped @github:registry line
# here, or npm ci would try to fetch the runtime from the upstream-less
# feed and 404.
- name: Install SDK dependencies
run: npm ci --ignore-scripts
- name: Compute SDK canary version
id: sdkver
env:
RUN_NUMBER: ${{ github.run_number }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
SHORT_SHA="${SHA:0:7}"
# Base the canary on the NEXT patch of the public SDK latest so canaries
# correlate with public releases: they sort ABOVE the current public
# latest and BELOW the eventual real release of that next patch (a
# prerelease of X.Y.Z always sorts below X.Y.Z), so a canary can never
# shadow the real release when it ships.
# Reuse the repo's own version helper (scripts/get-version.js) so this
# stays consistent with publish.yml: `current` returns the latest public
# dist-tag version, read-only from public npm (never the feed), then
# we bump the patch ourselves to keep strict patch+1 semantics.
PUBLIC_LATEST="$(node scripts/get-version.js current || true)"
BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}"
if [[ "$BASE" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$(( BASH_REMATCH[3] + 1 ))"
else
echo "::error::Could not resolve public SDK latest version (got '$PUBLIC_LATEST'); refusing to publish a canary with an unknown base."
exit 1
fi
SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}"
if [[ ! "$SDK_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver."
exit 1
fi
echo "SDK canary version: $SDK_VERSION"
echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_OUTPUT"
- name: Set package version and pin runtime dependency
env:
SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }}
run: |
set -euo pipefail
npm version "$SDK_VERSION" --no-git-tag-version --allow-same-version
# Exact pin (no caret) so the published SDK canary depends on precisely
# the runtime version that was just tested by the e2e gate.
npm pkg set "dependencies.@github/copilot=$RUNTIME_VERSION"
echo "Pinned @github/copilot to $(npm pkg get dependencies.@github/copilot)"
- name: Build SDK
run: npm run build
- name: Azure Login (OIDC -> id-cpd-ci)
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci
tenant-id: "${{ vars.CPD_ID_TENANT_ID }}"
allow-no-subscriptions: true
# Auth-only .npmrc: just the two token lines, NO scoped registry line.
# The publish target is supplied explicitly via publishConfig + --registry.
- name: Configure feed auth (.npmrc)
run: |
set -euo pipefail
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
# Derive the protocol-relative auth scopes from FEED_URL (single source
# of truth). NO scoped @github:registry line here — publish target is
# supplied explicitly via publishConfig + --registry.
FEED_AUTH_REGISTRY="${FEED_URL#https:}"
FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}"
printf '%s\n' \
"${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \
"${FEED_AUTH_BASE}:_authToken=${TOKEN}" > .npmrc
echo "Wrote auth-only .npmrc to ./nodejs"
# Belt and suspenders (2 of 3): pin the publish target in the package too.
- name: Set publishConfig registry
run: npm pkg set "publishConfig.registry=$FEED_URL"
# Belt and suspenders (3 of 3): fail loudly unless the effective publish
# target is the internal feed. Guards against ever reaching public npm.
- name: Assert publish target is the internal feed
run: |
set -euo pipefail
EFFECTIVE="$(npm pkg get publishConfig.registry | tr -d '"')"
echo "Effective publishConfig.registry: $EFFECTIVE"
if [ "$EFFECTIVE" != "$FEED_URL" ]; then
echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish."
exit 1
fi
- name: Publish SDK canary to internal feed
run: npm publish --registry "$FEED_URL"
- name: Summarize published canary
env:
SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }}
run: |
set -euo pipefail
{
echo "## SDK canary published"
echo ""
echo "| | |"
echo "| --- | --- |"
echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |"
echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |"
echo "| Feed | ${FEED_URL} |"
} >> "$GITHUB_STEP_SUMMARY"