Skip to content

Publish SDK packages #141

Publish SDK packages

Publish SDK packages #141

Workflow file for this run

name: Publish SDK packages
env:
HUSKY: 0
on:
workflow_dispatch:
inputs:
dist-tag:
description: "Tag to publish under"
type: choice
required: true
default: "prerelease"
options:
- latest
- prerelease
- unstable
- canary
version:
description: "Version override (optional, e.g., 1.0.0). If empty, auto-increments. Unstable overrides must use <core>-unstable."
type: string
required: false
mode:
description: "Publish or validate without registry and release mutations"
type: choice
required: true
default: publish
options:
- publish
- dry-run
runtime:
description: "Runtime metadata (automation only)"
type: string
required: false
test-policy:
description: "Runtime E2E test policy"
type: choice
required: true
default: required
options:
- required
- advisory
- skipped
permissions:
contents: read
concurrency:
group: ${{ inputs.mode == 'dry-run' && format('publish-dry-run-{0}', github.run_id) || inputs.runtime != '' && format('publish-runtime-{0}', github.run_id) || inputs.dist-tag == 'unstable' && 'sdk-runtime-public-unstable' || 'publish' }}
cancel-in-progress: false
jobs:
validate-dispatch:
name: Validate dispatch
runs-on: ubuntu-latest
outputs:
kind: ${{ steps.validate.outputs.kind }}
runtime_run_id: ${{ steps.validate.outputs.runtime_run_id }}
runtime_sha: ${{ steps.validate.outputs.runtime_sha }}
runtime_version: ${{ steps.validate.outputs.runtime_version }}
test_policy: ${{ steps.validate.outputs.test_policy }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22.x"
- run: npm ci --ignore-scripts
working-directory: ./nodejs
- name: Validate release dispatch
id: validate
working-directory: ./nodejs
env:
DIST_TAG: ${{ inputs.dist-tag }}
MODE: ${{ inputs.mode }}
RUNTIME_JSON: ${{ inputs.runtime }}
TEST_POLICY: ${{ inputs.test-policy }}
VERSION_OVERRIDE: ${{ inputs.version }}
run: npx tsx scripts/runtime-release-identity.ts
# Shared job to calculate version once for all publish jobs
version:
name: Calculate Version
needs: validate-dispatch
if: needs.validate-dispatch.outputs.kind == 'direct'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
outputs:
version: ${{ steps.unstable_version.outputs.VERSION || steps.version.outputs.VERSION }}
current: ${{ steps.version.outputs.CURRENT }}
current-prerelease: ${{ steps.version.outputs.CURRENT_PRERELEASE }}
defaults:
run:
working-directory: ./nodejs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: ${{ inputs.dist-tag == 'unstable' && '0' || '1' }}
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22.x"
- run: npm ci --ignore-scripts
- name: Plan unstable version
if: inputs.dist-tag == 'unstable'
id: unstable_version
env:
GH_TOKEN: ${{ github.token }}
SDK_CHANNEL: unstable
SDK_SHA: ${{ github.sha }}
SDK_VERSION_OVERRIDE: ${{ inputs.version }}
WORKFLOW_RUN_ID: ${{ github.run_id }}
run: |
set -euo pipefail
WORKFLOW_CREATED_AT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .created_at)"
gh api --paginate "/repos/$GITHUB_REPOSITORY/releases?per_page=100" |
jq -s 'add' > "$RUNNER_TEMP/sdk-releases.json"
export SDK_RELEASES_FILE="$RUNNER_TEMP/sdk-releases.json"
export WORKFLOW_CREATED_AT
VERSION="$(npx tsx scripts/unstable-version.ts)"
npm exec -- semver "$VERSION" >/dev/null
echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT"
echo "Planned unstable version: $VERSION" >> "$GITHUB_STEP_SUMMARY"
- name: Get version
if: inputs.dist-tag != 'unstable'
id: version
run: |
CURRENT="$(node scripts/get-version.js current)"
echo "CURRENT=$CURRENT" >> $GITHUB_OUTPUT
echo "Current latest version: $CURRENT" >> $GITHUB_STEP_SUMMARY
CURRENT_PRERELEASE="$(node scripts/get-version.js current-prerelease)"
echo "CURRENT_PRERELEASE=$CURRENT_PRERELEASE" >> $GITHUB_OUTPUT
echo "Current prerelease version: $CURRENT_PRERELEASE" >> $GITHUB_STEP_SUMMARY
if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
# Validate version format matches dist-tag
if [ "${{ github.event.inputs.dist-tag }}" = "latest" ]; then
if [[ "$VERSION" == *-* ]]; then
echo "❌ Error: Version '$VERSION' has a prerelease suffix but dist-tag is 'latest'" >> $GITHUB_STEP_SUMMARY
echo "Use a version without suffix (e.g., '1.0.0') for latest releases"
exit 1
fi
else
if [[ "$VERSION" != *-* ]]; then
echo "❌ Error: Version '$VERSION' has no prerelease suffix but dist-tag is '${{ github.event.inputs.dist-tag }}'" >> $GITHUB_STEP_SUMMARY
echo "Use a version with suffix (e.g., '1.0.0-preview.0') for prerelease"
exit 1
fi
fi
echo "Using manual version override: $VERSION" >> $GITHUB_STEP_SUMMARY
else
VERSION="$(node scripts/get-version.js ${{ github.event.inputs.dist-tag }})"
echo "Auto-incremented version: $VERSION" >> $GITHUB_STEP_SUMMARY
fi
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
- name: Verify version is available on public npm
if: inputs.dist-tag != 'unstable'
env:
VERSION: ${{ steps.version.outputs.VERSION }}
run: |
node scripts/npm-release.js preflight \
@github/copilot-sdk \
"$VERSION" \
https://registry.npmjs.org
package-nodejs:
name: Package Node.js SDK
needs: version
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: ./nodejs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22.x"
- run: npm ci --ignore-scripts
- name: Set version
run: node scripts/set-version.js
env:
VERSION: ${{ needs.version.outputs.version }}
- name: Build
run: npm run build
- name: Pack
run: |
npm run pack:release
TARBALL_COUNT="$(find . -maxdepth 1 -name 'github-copilot-sdk-*.tgz' | wc -l | tr -d ' ')"
if [ "$TARBALL_COUNT" -ne 9 ]; then
echo "::error::Expected nine Node.js package tarballs, found $TARBALL_COUNT."
exit 1
fi
npm run verify:release-packages
- name: Create unstable package manifest
if: inputs.dist-tag == 'unstable'
env:
SDK_VERSION: ${{ needs.version.outputs.version }}
run: npm run release:manifest -- create-package-set package-set-manifest.json .
- name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: nodejs-package
path: |
nodejs/github-copilot-sdk-*.tgz
nodejs/package-set-manifest.json
if-no-files-found: error
publish-nodejs:
name: Publish Node.js SDK
needs: package-nodejs
if: inputs.mode == 'publish' && (github.ref == 'refs/heads/main' || inputs.dist-tag == 'unstable')
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22.x"
- name: Install release dependencies
if: inputs.dist-tag == 'unstable'
working-directory: ./nodejs
run: npm ci --ignore-scripts
- name: Update npm for OIDC support
run: npm i -g "npm@11.6.3"
- name: Download Node.js package
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: nodejs-package
path: ./dist
- name: Publish tarball to public npm
env:
DIST_TAG: ${{ github.event.inputs.dist-tag }}
run: |
set -euo pipefail
if [ "$DIST_TAG" = "unstable" ]; then
node nodejs/scripts/npm-release.js publish-manifest \
dist/package-set-manifest.json dist unstable https://registry.npmjs.org public
exit 0
fi
shopt -s nullglob
TARBALLS=(./dist/*.tgz)
if [ "${#TARBALLS[@]}" -ne 9 ]; then
echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}."
exit 1
fi
MAIN_TARBALL=""
for TARBALL in "${TARBALLS[@]}"; do
PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)"
if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then
MAIN_TARBALL="$TARBALL"
continue
fi
node nodejs/scripts/npm-release.js publish \
"$TARBALL" \
"$DIST_TAG" \
https://registry.npmjs.org \
public
done
if [ -z "$MAIN_TARBALL" ]; then
echo "::error::Main @github/copilot-sdk tarball not found."
exit 1
fi
node nodejs/scripts/npm-release.js publish \
"$MAIN_TARBALL" \
"$DIST_TAG" \
https://registry.npmjs.org \
public
publish-nodejs-internal:
name: Publish Node.js SDK to internal feed
needs: publish-nodejs
environment: cicd
runs-on: ubuntu-latest
concurrency:
group: sdk-runtime-internal-${{ inputs.dist-tag }}
cancel-in-progress: false
queue: max
permissions:
actions: read
contents: read
id-token: write
env:
ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22.x"
- name: Install release dependencies
if: inputs.dist-tag == 'unstable'
working-directory: ./nodejs
run: npm ci --ignore-scripts
- name: Download Node.js package
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: nodejs-package
path: ./dist
- 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
- name: Configure feed auth
run: |
set -euo pipefail
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
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}" > "$HOME/.npmrc"
- name: Publish tarball to internal feed
env:
DIST_TAG: ${{ github.event.inputs.dist-tag }}
run: |
set -euo pipefail
if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/" ]; then
echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish."
exit 1
fi
if [ "$DIST_TAG" = "unstable" ]; then
node nodejs/scripts/npm-release.js publish-manifest \
dist/package-set-manifest.json dist unstable "$FEED_URL" azure
exit 0
fi
shopt -s nullglob
TARBALLS=(./dist/*.tgz)
if [ "${#TARBALLS[@]}" -ne 9 ]; then
echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}."
exit 1
fi
MAIN_TARBALL=""
for TARBALL in "${TARBALLS[@]}"; do
PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)"
if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then
MAIN_TARBALL="$TARBALL"
continue
fi
node nodejs/scripts/npm-release.js publish \
"$TARBALL" \
"$DIST_TAG" \
"$FEED_URL" \
azure
done
if [ -z "$MAIN_TARBALL" ]; then
echo "::error::Main @github/copilot-sdk tarball not found."
exit 1
fi
node nodejs/scripts/npm-release.js publish \
"$MAIN_TARBALL" \
"$DIST_TAG" \
"$FEED_URL" \
azure
publish-dotnet:
name: Publish .NET SDK
if: inputs.dist-tag != 'unstable'
needs: version
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./dotnet
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: "10.0.x"
- name: Restore dependencies
run: dotnet restore
- name: Build and pack
run: dotnet pack src/GitHub.Copilot.SDK.csproj -c Release -p:Version=${{ needs.version.outputs.version }} -o ./artifacts
- name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: dotnet-package
path: |
dotnet/artifacts/*.nupkg
dotnet/artifacts/*.snupkg
- name: NuGet login (OIDC)
if: github.ref == 'refs/heads/main'
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
id: nuget-login
with:
# The following must be a username, not an organization name, and that user must have configured Trusted Publishing
# for this owner/repo/workflow combination in their NuGet.org account settings. We could set up a dedicated user for
# this purpose if needed, but then we'd have to manage that account separately. Other GitHub-owned packages on NuGet
# are associated with individual maintainers' accounts too.
user: stevesanderson
- name: Publish to NuGet
if: github.ref == 'refs/heads/main'
run: |
dotnet nuget push ./artifacts/*.nupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate --no-symbols
dotnet nuget push ./artifacts/*.snupkg --api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
publish-dotnet-internal:
name: Publish .NET SDK to internal feed
needs: publish-dotnet
if: github.ref == 'refs/heads/main'
environment: cicd
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
id-token: write
env:
ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/nuget/v3/index.json
steps:
- uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0
with:
dotnet-version: "10.0.x"
- name: Download .NET package
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: dotnet-package
path: ./dist
- 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
- name: Publish package to internal feed
run: |
set -euo pipefail
if [ "$FEED_URL" != "https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/nuget/v3/index.json" ]; then
echo "::error::FEED_URL ('$FEED_URL') is not the expected internal feed. Refusing to publish."
exit 1
fi
shopt -s nullglob
PACKAGES=(./dist/*.nupkg)
if [ "${#PACKAGES[@]}" -ne 1 ]; then
echo "::error::Expected one .NET package, found ${#PACKAGES[@]}."
exit 1
fi
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
dotnet nuget add source "$FEED_URL" --name CopilotInternal
# Keep the short-lived token out of NuGet.Config and command-line arguments.
export NuGetPackageSourceCredentials_CopilotInternal="Username=azure;Password=$TOKEN;ValidAuthenticationTypes=Basic"
# Azure Artifacts does not support .snupkg symbol packages.
dotnet nuget push "${PACKAGES[0]}" \
--api-key AzureArtifacts \
--source CopilotInternal \
--skip-duplicate \
--no-symbols
publish-rust:
name: Publish Rust SDK
if: inputs.dist-tag != 'unstable'
needs: version
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./rust
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable
with:
toolchain: "1.94.0"
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: "rust"
- name: Set version
run: sed -i -E 's/^version = ".*"$/version = "${{ needs.version.outputs.version }}"/' Cargo.toml
- name: Snapshot CLI version + hashes for build.rs
run: |
bash scripts/snapshot-bundled-cli-version.sh
bash scripts/snapshot-bundled-in-process-version.sh
- name: Verify CLI version snapshots exist
run: |
for snapshot in cli-version.txt cli-version-in-process.txt; do
if [[ ! -f "${snapshot}" ]]; then
echo "::error::${snapshot} was not generated. The Snapshot step must run before packaging."
exit 1
fi
done
- name: Package (dry run)
run: cargo publish --dry-run --allow-dirty
- name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: rust-package
path: rust/target/package/*.crate
- name: Publish to crates.io
if: github.ref == 'refs/heads/main'
run: cargo publish --allow-dirty
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
publish-python:
name: Publish Python SDK
if: inputs.dist-tag != 'unstable'
needs: version
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: ./python
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: "22.x"
- name: Set up uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Set version
run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml
- name: Inject CLI version
run: node scripts/inject-cli-version.mjs
- name: Build wheel
run: uv build --wheel --out-dir dist
- name: Upload artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-package
path: python/dist/*
- name: Publish to PyPI
if: github.ref == 'refs/heads/main'
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: python/dist/
publish-java:
name: Publish Java SDK
if: inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main'
needs: version
permissions:
contents: read
uses: ./.github/workflows/java-publish-maven.yml
with:
releaseVersion: ${{ needs.version.outputs.version }}
sourceSha: ${{ github.sha }}
prerelease: ${{ github.event.inputs.dist-tag == 'prerelease' }}
secrets: inherit
github-release:
name: Create GitHub Release
needs:
[
version,
publish-nodejs,
publish-dotnet,
publish-python,
publish-rust,
publish-java,
]
if: |
always() &&
github.ref == 'refs/heads/main' &&
inputs.dist-tag != 'unstable' &&
needs.version.result == 'success' &&
needs.publish-nodejs.result == 'success' &&
needs.publish-dotnet.result == 'success' &&
needs.publish-python.result == 'success' &&
needs.publish-rust.result == 'success' &&
needs.publish-java.outputs.mavenPublished == 'true'
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Create GitHub Release
if: github.event.inputs.dist-tag == 'latest'
run: |
NOTES_FLAG=""
if git rev-parse "v${{ needs.version.outputs.current }}" >/dev/null 2>&1; then
NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current }}"
fi
gh release create "v${{ needs.version.outputs.version }}" \
--title "v${{ needs.version.outputs.version }}" \
--generate-notes $NOTES_FLAG \
--target ${{ github.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub Pre-Release
if: github.event.inputs.dist-tag == 'prerelease'
run: |
NOTES_FLAG=""
if git rev-parse "v${{ needs.version.outputs.current-prerelease }}" >/dev/null 2>&1; then
NOTES_FLAG="--notes-start-tag v${{ needs.version.outputs.current-prerelease }}"
fi
gh release create "v${{ needs.version.outputs.version }}" \
--prerelease \
--title "v${{ needs.version.outputs.version }}" \
--generate-notes $NOTES_FLAG \
--target ${{ github.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger changelog generation
run: gh workflow run release-changelog.lock.yml -f tag="v${{ needs.version.outputs.version }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Tag Go SDK submodule
if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease'
run: |
set -e
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch --tags
TAG_NAME="go/v${{ needs.version.outputs.version }}"
# Try to create the tag - will fail if it already exists
if git tag "$TAG_NAME" ${{ github.sha }} 2>/dev/null; then
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git "$TAG_NAME"
echo "Created and pushed tag $TAG_NAME"
else
echo "Tag $TAG_NAME already exists, skipping"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Tag Rust SDK
# Keep a language-scoped source tag for traceability. Rust is
# included in the cross-language `vX.Y.Z` GitHub Release.
if: github.event.inputs.dist-tag == 'latest' || github.event.inputs.dist-tag == 'prerelease'
run: |
set -e
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch --tags
VERSION="${{ needs.version.outputs.version }}"
TAG_NAME="rust/v${VERSION}"
if git tag "$TAG_NAME" ${{ github.sha }} 2>/dev/null; then
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git "$TAG_NAME"
echo "Created and pushed tag $TAG_NAME"
else
echo "Tag $TAG_NAME already exists, skipping tag push"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
tag-java:
name: Tag Java SDK
needs: [version, publish-java, github-release]
if: |
success() &&
(github.event.inputs.dist-tag == 'latest' ||
github.event.inputs.dist-tag == 'prerelease')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.publish-java.outputs.sourceSha }}
fetch-depth: 0
- name: Tag Java SDK
# Reuse a tag only when it identifies the source that was published.
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git fetch --tags origin
TAG_NAME="java/v${VERSION}"
if git show-ref --verify --quiet "refs/tags/$TAG_NAME"; then
TAG_COMMIT=$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}")
if [ "$TAG_COMMIT" != "$SOURCE_SHA" ]; then
echo "::error::Tag $TAG_NAME points to $TAG_COMMIT, expected $SOURCE_SHA. Refusing to overwrite it."
exit 1
fi
echo "Tag $TAG_NAME already points to the release source, skipping tag push"
else
STATUS=$?
if [ "$STATUS" -ne 1 ]; then
echo "::error::Could not inspect tag $TAG_NAME."
exit "$STATUS"
fi
git tag "$TAG_NAME" "$SOURCE_SHA"
git push origin "refs/tags/$TAG_NAME"
echo "Created and pushed tag $TAG_NAME"
fi
env:
VERSION: ${{ needs.version.outputs.version }}
SOURCE_SHA: ${{ needs.publish-java.outputs.sourceSha }}
deploy-java-site:
name: Deploy Java documentation site
needs: [version, tag-java]
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Trigger Java documentation site deploy
# A failed dispatch can be retried without recreating the GitHub release.
run: |
set -euo pipefail
TAG="java/v${VERSION}"
PUBLISH_AS_LATEST=true
if [ "$DIST_TAG" = "prerelease" ]; then
PUBLISH_AS_LATEST=false
fi
echo "Triggering site deployment for version ${VERSION} (tag: ${TAG})"
gh workflow run deploy-site.yml \
--repo github/copilot-sdk-java \
-f version="${VERSION}" \
-f publish_as_latest="${PUBLISH_AS_LATEST}" \
-f monorepo_tag="${TAG}"
env:
VERSION: ${{ needs.version.outputs.version }}
DIST_TAG: ${{ github.event.inputs.dist-tag }}
GITHUB_TOKEN: ${{ secrets.JAVA_RELEASE_GITHUB_TOKEN }}
runtime-plan:
name: Plan runtime release
needs: validate-dispatch
if: needs.validate-dispatch.outputs.kind == 'runtime'
runs-on: ubuntu-latest
environment: cicd
permissions:
actions: read
contents: read
outputs:
artifact_name: ${{ steps.plan.outputs.artifact_name }}
sdk_version: ${{ steps.plan.outputs.sdk_version }}
workflow_created_at: ${{ steps.plan.outputs.workflow_created_at }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: npm
cache-dependency-path: ./nodejs/package-lock.json
node-version: 22
- run: npm ci --ignore-scripts
working-directory: ./nodejs
- name: Calculate the release identity
id: plan
working-directory: ./nodejs
env:
CHANNEL: ${{ inputs.dist-tag }}
GH_TOKEN: ${{ github.token }}
SDK_CHANNEL: ${{ inputs.dist-tag }}
SDK_SHA: ${{ github.sha }}
WORKFLOW_RUN_ID: ${{ github.run_id }}
WORKFLOW_RUN_NUMBER: ${{ github.run_number }}
run: |
set -euo pipefail
WORKFLOW_CREATED_AT="$(gh api "/repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .created_at)"
gh api --paginate "/repos/$GITHUB_REPOSITORY/releases?per_page=100" |
jq -s 'add' > "$RUNNER_TEMP/sdk-releases.json"
export SDK_RELEASES_FILE="$RUNNER_TEMP/sdk-releases.json"
export WORKFLOW_CREATED_AT
SDK_VERSION="$(npx tsx scripts/unstable-version.ts)"
npm exec -- semver "$SDK_VERSION" >/dev/null
ARTIFACT_NAME="nodejs-${CHANNEL}-${SDK_VERSION}"
echo "Runtime E2E test policy: ${{ needs.validate-dispatch.outputs.test_policy }}" >> "$GITHUB_STEP_SUMMARY"
{
echo "artifact_name=$ARTIFACT_NAME"
echo "sdk_version=$SDK_VERSION"
echo "workflow_created_at=$WORKFLOW_CREATED_AT"
} >> "$GITHUB_OUTPUT"
runtime-acquire:
name: Acquire runtime
needs: [validate-dispatch, runtime-plan]
runs-on: ubuntu-latest
environment: cicd
permissions:
contents: read
packages: read
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: npm
cache-dependency-path: ./nodejs/package-lock.json
node-version: 22
- run: npm ci --ignore-scripts
- name: Configure authentication-only GitHub Packages access
env:
NODE_AUTH_TOKEN: ${{ github.token }}
run: echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > "$HOME/.npmrc"
- name: Download and validate all runtime platforms
env:
NODE_AUTH_TOKEN: ${{ github.token }}
RUNTIME_SHA: ${{ needs.validate-dispatch.outputs.runtime_sha }}
RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }}
run: |
npm run acquire:runtime-packages -- \
--version "$RUNTIME_VERSION" \
--sha "$RUNTIME_SHA" \
--output "$RUNNER_TEMP/runtime-packages"
- name: Archive validated runtime packages
run: tar -czf "$RUNNER_TEMP/runtime-packages.tar.gz" --exclude "runtime-packages/tarballs" -C "$RUNNER_TEMP" runtime-packages
- name: Upload validated runtime packages
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }}
path: ${{ runner.temp }}/runtime-packages.tar.gz
if-no-files-found: error
retention-days: 7
runtime-test:
name: Test runtime (${{ matrix.os }}, ${{ matrix.transport }})
needs: [validate-dispatch, runtime-plan, runtime-acquire]
if: needs.validate-dispatch.outputs.test_policy != 'skipped'
permissions:
contents: read
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
transport: ["default", "inprocess"]
runs-on: ${{ matrix.os }}
environment: cicd
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: npm
cache-dependency-path: ./nodejs/package-lock.json
node-version: 22
- run: npm ci --ignore-scripts
- name: Install test harness dependencies
working-directory: ./test/harness
run: npm ci --ignore-scripts
- name: Download validated runtime packages
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }}
path: ${{ runner.temp }}/runtime-package-artifact
- name: Extract validated runtime packages
run: |
runner_temp="$RUNNER_TEMP"
if command -v cygpath >/dev/null 2>&1; then
runner_temp="$(cygpath -u "$runner_temp")"
fi
rm -rf "$runner_temp/runtime-packages"
tar -xzf "$runner_temp/runtime-package-artifact/runtime-packages.tar.gz" -C "$runner_temp"
- name: Select the acquired runtime
env:
COPILOT_SDK_RUNTIME_PACKAGE_DIR: ${{ runner.temp }}/runtime-packages
RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }}
run: |
node scripts/set-cli-version.js "$RUNTIME_VERSION" --local-package
runtime_path="$(npm run --silent prepare:runtime -- --print-path)"
echo "COPILOT_SDK_RUNTIME_PACKAGE_DIR=$COPILOT_SDK_RUNTIME_PACKAGE_DIR" >> "$GITHUB_ENV"
echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV"
- run: npm run build
- name: Warm up PowerShell
if: runner.os == 'Windows'
run: pwsh.exe -Command "Write-Host 'PowerShell ready'"
- name: Select inprocess transport
if: matrix.transport == 'inprocess'
run: echo "COPILOT_SDK_DEFAULT_CONNECTION=inprocess" >> "$GITHUB_ENV"
- name: Run Node SDK tests
id: e2e
continue-on-error: ${{ needs.validate-dispatch.outputs.test_policy == 'advisory' }}
env:
COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }}
run: npm test
- name: Report advisory E2E failure
if: needs.validate-dispatch.outputs.test_policy == 'advisory' && steps.e2e.outcome == 'failure'
env:
RUNNER_OS: ${{ runner.os }}
run: |
echo "::warning::Runtime-backed Node SDK E2E tests failed on ${RUNNER_OS}; continuing because test-policy is advisory."
{
echo "### Advisory runtime E2E failure"
echo
echo "Runtime-backed Node SDK E2E tests failed on **${RUNNER_OS}**. Publication remains eligible because \`test-policy\` is \`advisory\`."
} >> "$GITHUB_STEP_SUMMARY"
runtime-package:
name: Build SDK packages
needs: [validate-dispatch, runtime-plan, runtime-acquire, runtime-test]
if: |
always() &&
!cancelled() &&
needs.validate-dispatch.result == 'success' &&
needs.runtime-plan.result == 'success' &&
needs.runtime-acquire.result == 'success' &&
(
needs.runtime-test.result == 'success' ||
(needs.validate-dispatch.outputs.test_policy == 'skipped' && needs.runtime-test.result == 'skipped')
)
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
shell: bash
working-directory: ./nodejs
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
cache: npm
cache-dependency-path: ./nodejs/package-lock.json
node-version: 22
- run: npm ci --ignore-scripts
- name: Download validated runtime packages
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: runtime-${{ inputs.dist-tag }}-${{ needs.validate-dispatch.outputs.runtime_version }}-${{ needs.validate-dispatch.outputs.runtime_sha }}
path: ${{ runner.temp }}/runtime-package-artifact
- name: Extract validated runtime packages
run: |
runner_temp="$RUNNER_TEMP"
if command -v cygpath >/dev/null 2>&1; then
runner_temp="$(cygpath -u "$runner_temp")"
fi
rm -rf "$runner_temp/runtime-packages"
tar -xzf "$runner_temp/runtime-package-artifact/runtime-packages.tar.gz" -C "$runner_temp"
- name: Build and verify exact package set
env:
COPILOT_SDK_RUNTIME_PACKAGE_DIR: ${{ runner.temp }}/runtime-packages
RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }}
SDK_VERSION: ${{ needs.runtime-plan.outputs.sdk_version }}
run: |
VERSION="$SDK_VERSION" node scripts/set-version.js
node scripts/set-cli-version.js "$RUNTIME_VERSION" --local-package
grep -F "COPILOT_CLI_USE_NPM_PACKAGE = false" src/cliVersion.ts
npm run build
npm run pack:release
npm run verify:release-packages
- name: Create immutable release manifest
env:
RELEASE_CHANNEL: ${{ inputs.dist-tag }}
RUNTIME_RUN_ID: ${{ needs.validate-dispatch.outputs.runtime_run_id }}
RUNTIME_SHA: ${{ needs.validate-dispatch.outputs.runtime_sha }}
RUNTIME_VERSION: ${{ needs.validate-dispatch.outputs.runtime_version }}
SDK_REF: ${{ github.ref }}
SDK_SHA: ${{ github.sha }}
SDK_VERSION: ${{ needs.runtime-plan.outputs.sdk_version }}
TEST_POLICY: ${{ needs.validate-dispatch.outputs.test_policy }}
WORKFLOW_CREATED_AT: ${{ needs.runtime-plan.outputs.workflow_created_at }}
WORKFLOW_RUN_ID: ${{ github.run_id }}
WORKFLOW_RUN_NUMBER: ${{ github.run_number }}
run: |
npm run release:manifest -- create release-manifest.json .
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ needs.runtime-plan.outputs.artifact_name }}
path: |
nodejs/release-manifest.json
nodejs/github-copilot-sdk-*.tgz
if-no-files-found: error
retention-days: 30
runtime-publish-internal:
name: Publish SDK internally
if: |
always() &&
!cancelled() &&
inputs.mode == 'publish' &&
needs.runtime-plan.result == 'success' &&
needs.runtime-package.result == 'success'
needs: [validate-dispatch, runtime-plan, runtime-package]
runs-on: ubuntu-latest
concurrency:
group: sdk-runtime-internal-${{ inputs.dist-tag }}
cancel-in-progress: false
queue: max
environment: cicd
permissions:
actions: read
contents: read
id-token: write
env:
ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
- run: npm ci --ignore-scripts
working-directory: ./nodejs
- name: Download retained release
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: ${{ needs.runtime-plan.outputs.artifact_name }}
path: ./dist
- name: Validate retained release
run: |
node nodejs/node_modules/.bin/tsx nodejs/scripts/release-manifest.ts verify dist/release-manifest.json dist
[ "$(jq -r .workflow.runId dist/release-manifest.json)" = "${{ github.run_id }}" ] ||
{ echo "::error::Retained release belongs to a different workflow run."; exit 1; }
[ "$(jq -r .channel dist/release-manifest.json)" = "${{ inputs.dist-tag }}" ] ||
{ echo "::error::Retained release channel does not match the requested channel."; exit 1; }
- name: Azure login
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
allow-no-subscriptions: true
client-id: ${{ vars.CPD_ID_CLIENT_ID }}
tenant-id: ${{ vars.CPD_ID_TENANT_ID }}
- name: Configure authentication-only Azure npm access
run: |
TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)"
echo "::add-mask::$TOKEN"
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}" > "$HOME/.npmrc"
- name: Publish exact tarballs internally
run: |
node nodejs/scripts/npm-release.js publish-manifest \
dist/release-manifest.json dist "${{ inputs.dist-tag }}" "$FEED_URL" azure
runtime-publish-public:
name: Publish SDK publicly
if: |
always() &&
!cancelled() &&
inputs.dist-tag == 'unstable' &&
inputs.mode == 'publish' &&
needs.runtime-plan.result == 'success' &&
needs.runtime-publish-internal.result == 'success'
needs: [runtime-plan, runtime-publish-internal]
runs-on: ubuntu-latest
concurrency:
group: sdk-runtime-public-unstable
cancel-in-progress: false
queue: max
permissions:
actions: read
contents: read
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 22
- run: npm ci --ignore-scripts
working-directory: ./nodejs
- name: Update npm for trusted publishing
run: npm install -g npm@11.6.3
- name: Download retained release
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: ${{ needs.runtime-plan.outputs.artifact_name }}
path: ./dist
- name: Validate retained release
run: |
node nodejs/node_modules/.bin/tsx nodejs/scripts/release-manifest.ts verify \
dist/release-manifest.json dist
- name: Publish the same tarballs to public npm
run: |
node nodejs/scripts/npm-release.js publish-manifest \
dist/release-manifest.json dist unstable https://registry.npmjs.org public