-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add SDK canary pipeline #1950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MackinnonBuck
wants to merge
23
commits into
main
Choose a base branch
from
mackinnonbuck-sdk-canary-compat-gate
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+391
−0
Open
Add SDK canary pipeline #1950
Changes from 7 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 f3dd868
Add guarded publish job to SDK canary workflow
MackinnonBuck 578c247
Add force_publish bypass for flaky e2e gate in SDK canary
MackinnonBuck 05e0213
Harden canary bypass audit and mark temp branch-validation lines
MackinnonBuck 79519e4
Fix canary bypass audit step failing before checkout
MackinnonBuck b289233
Finalize SDK canary: remove temporary branch-validation bypasses
MackinnonBuck 20b7003
Base SDK canary version on next patch of public SDK latest
MackinnonBuck 925844e
Revert "Finalize SDK canary: remove temporary branch-validation bypas…
MackinnonBuck 4380414
Reapply "Finalize SDK canary: remove temporary branch-validation bypa…
MackinnonBuck 58a4767
Revert "Reapply "Finalize SDK canary: remove temporary branch-validat…
MackinnonBuck a1adab7
Replace force_publish boolean with a mode enum
MackinnonBuck 20f1280
Address PR review feedback on sdk-canary workflow
MackinnonBuck b6a6667
Point SDK canary at the copilot-canary feed
MackinnonBuck 7078f04
Finalize SDK canary: remove temporary branch-validation bypasses
MackinnonBuck 147d9f7
Harden semver validation and .npmrc writes in sdk-canary
MackinnonBuck 0edff0c
Add publish run summary; remove read-back verify step
MackinnonBuck 9c2f498
Fix stale/misleading comments; drop unused resolve checkout
MackinnonBuck 27f0e2b
Drop unused test-harness runtime override and feed .npmrc
MackinnonBuck 78b727c
Harden version resolution, validate source, tighten resolve perms
MackinnonBuck 5fd0c68
TEMP: re-add branch-push validation trigger
MackinnonBuck f6625c7
Revert "TEMP: re-add branch-push validation trigger"
MackinnonBuck e0c8e24
Reject leading-zero version cores in semver validation
MackinnonBuck 4609e5f
Add repository_dispatch receiver for runtime canary chaining
MackinnonBuck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,344 @@ | ||
| 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. | ||
|
|
||
| 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.<sha>)" | ||
| required: true | ||
| type: string | ||
| runtime_source: | ||
| description: "Where to install the runtime from" | ||
| required: true | ||
| type: choice | ||
| options: | ||
| - 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 | ||
|
|
||
| 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 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 | ||
| # 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" | ||
| ;; | ||
| *) | ||
| 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 <<EOF | ||
| @github:registry=https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/registry/ | ||
| //pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/registry/:_authToken=${TOKEN} | ||
| //pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/:_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); | ||
| } | ||
| console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}`); | ||
|
MackinnonBuck marked this conversation as resolved.
Outdated
|
||
| ' | ||
|
|
||
| - 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] | ||
| # 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: > | ||
| !cancelled() && | ||
| github.event.repository.fork == false && | ||
| needs.resolve.result == 'success' && | ||
| (needs.test.result == 'success' || | ||
| (github.event_name == 'workflow_dispatch' && inputs.force_publish)) | ||
| 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: | ||
| - 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. 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." | ||
|
|
||
| - 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. | ||
| # 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 | ||
|
MackinnonBuck marked this conversation as resolved.
Outdated
|
||
| 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 <<EOF | ||
| //pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/registry/:_authToken=${TOKEN} | ||
| //pkgs.dev.azure.com/devdiv/_packaging/copilot-canary-test/npm/:_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}" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.