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 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: "22.x" - run: npm ci --ignore-scripts - name: Get version 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/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 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: Upload artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: nodejs-package path: nodejs/github-copilot-sdk-*.tgz if-no-files-found: error publish-nodejs: name: Publish Node.js SDK needs: package-nodejs if: 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 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@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 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 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: 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 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: 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@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-rust: name: Publish Rust SDK if: github.event.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: 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@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: 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.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' && 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 }}