Skip to content
Draft
Changes from all commits
Commits
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
163 changes: 142 additions & 21 deletions .github/workflows/sdk-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ name: "SDK Canary Test/Publish"
# @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).
# internal Azure Artifacts feed, and optionally (opt-in) to 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 name isn't repeated across steps. The SDK canary always publishes here
# and (when runtime_source=internal) installs the runtime from here. Public
# npm is a separate, opt-in target handled by its own job.
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
Expand Down Expand Up @@ -40,6 +40,11 @@ on:
- publish
- publish-force
- tests-only
publish_npm:
description: "ALSO publish the canary to public npm (opt-in). Only honored on workflow_dispatch; does not affect internal-feed publish eligibility."
required: false
type: boolean
default: false
repository_dispatch:
types: [runtime-canary]

Expand Down Expand Up @@ -252,6 +257,9 @@ jobs:
needs.resolve.outputs.PUBLISH_MODE == 'publish-force')
environment: cicd
runs-on: ubuntu-latest
outputs:
SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }}
TARBALL: ${{ steps.pack.outputs.TARBALL }}
permissions:
contents: read
id-token: write
Expand Down Expand Up @@ -289,6 +297,7 @@ jobs:
id: sdkver
env:
RUN_NUMBER: ${{ github.run_number }}
RUN_ATTEMPT: ${{ github.run_attempt }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
Expand All @@ -310,7 +319,10 @@ jobs:
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}"
# run_attempt disambiguates re-run attempts: a re-run rebuilds the
# tarball, so it must publish under a distinct version to avoid
# colliding with the prior attempt's immutable published version.
SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.${RUN_ATTEMPT}.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
Expand All @@ -332,48 +344,79 @@ jobs:
- name: Build SDK
run: npm run build

# Pack ONCE into a neutral tarball (no registry-specific publishConfig) so
# the exact same bytes — and therefore the same sha512 integrity — can be
# published to BOTH the internal feed and (opt-in) public npm. A VS Code
# lockfile can then resolve the identical pinned canary from either
# registry. Publishing a pre-built tarball also skips lifecycle scripts, so
# neither publish step rebuilds or mutates the artifact.
- name: Pack SDK canary tarball
id: pack
run: |
set -euo pipefail
TARBALL="$(npm pack --json | jq -r '.[0].filename')"
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
echo "::error::npm pack did not produce a tarball."
exit 1
fi
echo "Packed $TARBALL"
echo "TARBALL=$TARBALL" >> "$GITHUB_OUTPUT"

- name: Upload canary tarball artifact
uses: actions/upload-artifact@v7.0.0
with:
name: sdk-canary-tarball
path: nodejs/${{ steps.pack.outputs.TARBALL }}
if-no-files-found: error
retention-days: 7

- 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)
# Registry + auth .npmrc for the feed. We pin the DEFAULT registry to the
# feed here (single source of truth: FEED_URL) so the effective-registry
# assertion below is meaningful for a pre-packed tarball publish, then also
# pass --registry explicitly on publish (belt and suspenders). No scoped
# @github:registry line is needed since this job only publishes.
- name: Configure feed registry + 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.
# of truth).
FEED_AUTH_REGISTRY="${FEED_URL#https:}"
FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}"
printf '%s\n' \
"registry=${FEED_URL}" \
"${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"
echo "Wrote feed registry + auth .npmrc to ./nodejs"

# Belt and suspenders (3 of 3): fail loudly unless the effective publish
# target is the internal feed. Guards against ever reaching public npm.
# Fail loudly unless the effective npm registry is the internal feed.
# Guards against ever reaching public npm from this job. The npm publish
# path lives in a separate job with its own opposite assertion so the two
# registry/auth contexts never contaminate each other.
- 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"
EFFECTIVE="$(npm config get registry)"
echo "Effective registry: $EFFECTIVE"
if [ "$EFFECTIVE" != "$FEED_URL" ]; then
echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish."
echo "::error::Effective npm registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish."
exit 1
fi

# Publish the pre-packed neutral tarball (see the Pack step) so the exact
# same bytes go to the feed and, opt-in, to public npm.
- name: Publish SDK canary to internal feed
run: npm publish --registry "$FEED_URL"
env:
TARBALL: ${{ steps.pack.outputs.TARBALL }}
run: npm publish "$TARBALL" --tag canary --registry "$FEED_URL"

- name: Summarize published canary
env:
Expand All @@ -389,3 +432,81 @@ jobs:
echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |"
echo "| Feed | ${FEED_URL} |"
} >> "$GITHUB_STEP_SUMMARY"

publish-npm:
name: "Publish SDK canary (public npm)"
needs: [resolve, publish]
# OPT-IN, workflow_dispatch-only public npm publish. It NEVER runs for
# repository_dispatch (or any non-dispatch event such as schedule), and only
# when a human explicitly set publish_npm=true. It publishes the EXACT same
# tarball the feed publish produced (same canary version, byte-identical
# sha512), under the `canary` dist-tag, via npm OIDC trusted publishing.
# Deliberately NOT bound to a protected `environment:` so the automated flow
# isn't gated on a manual approval once a trusted publisher is registered.
if: >
!cancelled() &&
github.event.repository.fork == false &&
github.event_name == 'workflow_dispatch' &&
inputs.publish_npm == true &&
needs.publish.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for npm OIDC trusted publishing
env:
RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }}
SDK_VERSION: ${{ needs.publish.outputs.SDK_VERSION }}
TARBALL: ${{ needs.publish.outputs.TARBALL }}
defaults:
run:
shell: bash
steps:
- uses: actions/setup-node@v6
with:
node-version: 22

# npm >= 11.5 is required for OIDC trusted publishing (mirrors publish.yml).
# No token is used — trust is granted per owner/repo/workflow-file.
- name: Update npm for OIDC support
run: npm i -g "npm@11.6.3"

- name: Download canary tarball
uses: actions/download-artifact@v8.0.0
with:
name: sdk-canary-tarball
path: ./dist

# This job never touches the internal feed context (no Azure login, no feed
# .npmrc), so the default registry stays public npm. Assert it before we
# publish so a misconfiguration can never retarget the tarball, and confirm
# the shared tarball arrived.
- name: Assert publish target is public npm
run: |
set -euo pipefail
if [ -z "${TARBALL:-}" ] || [ ! -f "./dist/${TARBALL}" ]; then
echo "::error::Canary tarball '${TARBALL}' not found in ./dist."
exit 1
fi
EFFECTIVE="$(npm config get registry)"
echo "Effective registry: $EFFECTIVE"
if [ "$EFFECTIVE" != "https://registry.npmjs.org/" ]; then
echo "::error::Effective npm registry ('$EFFECTIVE') is not public npm. Refusing to publish."
exit 1
fi

- name: Publish SDK canary to public npm
run: npm publish "./dist/${TARBALL}" --tag canary --access public --registry https://registry.npmjs.org

- name: Summarize published canary
run: |
set -euo pipefail
{
echo "## SDK canary published to public npm"
echo ""
echo "| | |"
echo "| --- | --- |"
echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |"
echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |"
echo "| dist-tag | \`canary\` |"
echo "| Registry | https://registry.npmjs.org/ |"
} >> "$GITHUB_STEP_SUMMARY"
Loading