Skip to content

Publish SDK packages #89

Publish SDK packages

Publish SDK packages #89

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
version:
description: "Version override (optional, e.g., 1.0.0). If empty, auto-increments."
type: string
required: false
internal-only:
description: "Test only: publish the Node.js SDK exclusively to the internal feed"
type: boolean
required: false
default: false
permissions:
contents: read
concurrency:
group: publish
cancel-in-progress: false
jobs:
# Shared job to calculate version once for all publish jobs
version:
name: Calculate Version
runs-on: ubuntu-latest
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@v6.0.2
- uses: actions/setup-node@v6
with:
node-version: "22.x"
- run: npm ci --ignore-scripts
- name: Get version
id: version
env:
INTERNAL_ONLY: ${{ github.event.inputs.internal-only }}
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 [ "$INTERNAL_ONLY" = "true" ]; then
VERSION="0.0.0-${GITHUB_RUN_ID}"
echo "Using internal-only test version: $VERSION" >> $GITHUB_STEP_SUMMARY
elif [ -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/unstable"
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
env:
VERSION: ${{ steps.version.outputs.VERSION }}
run: |
node scripts/npm-release.js preflight \
@github/copilot-sdk \
"$VERSION" \
https://registry.npmjs.org
- name: Summarize internal-only validation
if: github.event.inputs.internal-only == 'true'
env:
FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/
VERSION: ${{ steps.version.outputs.VERSION }}
run: |
{
echo "## Internal-only SDK publish validation"
echo ""
echo "| | |"
echo "| --- | --- |"
echo "| Mode | \`internal-only\` |"
echo "| SDK version | \`${VERSION}\` |"
echo "| Dist-tag | \`internal-test\` |"
echo "| Feed | ${FEED_URL} |"
} >> "$GITHUB_STEP_SUMMARY"
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@v6.0.2
- uses: actions/setup-node@v6
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
id: pack
run: |
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 "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
- name: Record prepared package checksum
if: github.event.inputs.internal-only == 'true'
env:
TARBALL: ${{ steps.pack.outputs.tarball }}
run: sha512sum "$TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v7.0.0
with:
name: nodejs-package
path: nodejs/${{ steps.pack.outputs.tarball }}
if-no-files-found: error
publish-nodejs:
name: Publish Node.js SDK
needs: package-nodejs
if: |
github.event.inputs.internal-only != 'true' &&
(github.ref == 'refs/heads/main' || github.event.inputs.dist-tag == 'unstable')
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
id-token: write
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Update npm for OIDC support
run: npm i -g "npm@11.6.3"
- name: Download Node.js package
uses: actions/download-artifact@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
shopt -s nullglob
TARBALLS=(./dist/*.tgz)
if [ "${#TARBALLS[@]}" -ne 1 ]; then
echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}."
exit 1
fi
node nodejs/scripts/npm-release.js publish \
"${TARBALLS[0]}" \
"$DIST_TAG" \
https://registry.npmjs.org \
public
publish-nodejs-internal:
name: Publish Node.js SDK to internal feed
needs: [package-nodejs, publish-nodejs]
if: |
always() &&
needs.package-nodejs.result == 'success' &&
(
(github.event.inputs.internal-only == 'true' && needs.publish-nodejs.result == 'skipped') ||
(github.event.inputs.internal-only != 'true' && needs.publish-nodejs.result == 'success')
)
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/npm/registry/
steps:
- uses: actions/checkout@v6.0.2
- uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Download Node.js package
uses: actions/download-artifact@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: Record downloaded package checksum
if: github.event.inputs.internal-only == 'true'
run: sha512sum ./dist/*.tgz
- name: Publish tarball to internal feed
env:
DIST_TAG: ${{ github.event.inputs.internal-only == 'true' && 'internal-test' || 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
shopt -s nullglob
TARBALLS=(./dist/*.tgz)
if [ "${#TARBALLS[@]}" -ne 1 ]; then
echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}."
exit 1
fi
node nodejs/scripts/npm-release.js publish \
"${TARBALLS[0]}" \
"$DIST_TAG" \
"$FEED_URL" \
azure
- name: Intentionally fail first internal-only attempt
if: github.event.inputs.internal-only == 'true' && github.run_attempt == 1
env:
RUN_ID: ${{ github.run_id }}
run: |
{
echo "## Intentional first-attempt failure"
echo ""
echo "The SDK was published to the internal feed. Re-run only failed jobs to exercise immutable Azure conflict handling:"
echo ""
echo "\`gh run rerun ${RUN_ID} --failed --repo ${GITHUB_REPOSITORY}\`"
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Intentional internal-only failure after the internal publish side effect."
exit 1
publish-dotnet:
name: Publish .NET SDK
if: github.event.inputs.internal-only != 'true' && github.event.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@v6.0.2
- uses: actions/setup-dotnet@v5
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@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@v1
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-rust:
name: Publish Rust SDK
if: github.event.inputs.internal-only != 'true' && github.event.inputs.dist-tag != 'unstable'
needs: version
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./rust
steps:
- uses: actions/checkout@v6.0.2
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94.0"
- uses: Swatinem/rust-cache@v2
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@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: github.event.inputs.internal-only != 'true' && github.event.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@v6.0.2
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- uses: actions/setup-node@v6
with:
node-version: "22.x"
- name: Set up uv
uses: astral-sh/setup-uv@v7
- name: Install Node.js dependencies (for CLI version)
working-directory: ./nodejs
run: npm ci --ignore-scripts
- 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@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@release/v1
with:
packages-dir: python/dist/
publish-java:
name: Publish Java SDK
if: github.event.inputs.internal-only != 'true' && github.event.inputs.dist-tag != 'unstable' && github.ref == 'refs/heads/main'
needs: version
permissions:
contents: write
id-token: write
uses: ./.github/workflows/java-publish-maven.yml
with:
releaseVersion: ${{ needs.version.outputs.version }}
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.event.inputs.internal-only != 'true' &&
github.ref == 'refs/heads/main' &&
github.event.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'
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
steps:
- uses: actions/checkout@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 and create Rust GitHub Release
# Rust gets its own version-scoped GitHub Release with notes
# derived from PR titles since the previous Rust tag. The
# cross-language `vX.Y.Z` release above still exists; this one
# is the canonical reference for Rust users.
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
# Find the previous Rust tag for note generation. Prefer rust/v*,
# fall back to the historical rust-v* tags from the release-plz era.
PREV_TAG=$(git tag --list 'rust/v*' --sort=-v:refname | grep -vFx "$TAG_NAME" | head -n1)
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git tag --list 'rust-v*' --sort=-v:refname | head -n1)
fi
NOTES_FLAG=""
if [ -n "$PREV_TAG" ]; then
NOTES_FLAG="--notes-start-tag $PREV_TAG"
echo "Generating notes from $PREV_TAG..$TAG_NAME"
else
echo "No previous Rust tag found; generating notes from full history"
fi
PRERELEASE_FLAG=""
if [ "${{ github.event.inputs.dist-tag }}" = "prerelease" ]; then
PRERELEASE_FLAG="--prerelease"
fi
gh release create "$TAG_NAME" \
--title "$TAG_NAME" \
--generate-notes $NOTES_FLAG $PRERELEASE_FLAG \
--target ${{ github.sha }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}