Repository navigation
Fix installer fixture path literal #47
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
| name: Release | |
| on: | |
| push: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| env: | |
| DOTNET_VERSION: '10.0.x' | |
| NODE_VERSION: '22' | |
| jobs: | |
| verify-ci: | |
| name: Verify exact-SHA CI before release | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' | |
| timeout-minutes: 31 | |
| permissions: | |
| actions: read | |
| contents: read | |
| steps: | |
| - name: Wait for successful CI on this commit | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| deadline=$((SECONDS + 1800)) | |
| ci_workflow_id=$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows" \ | |
| --jq '[.workflows[] | select(.path == ".github/workflows/ci.yml")][0].id // empty') | |
| if [[ -z "$ci_workflow_id" ]]; then | |
| echo "Could not resolve this repository's CI workflow; release publication is blocked." | |
| exit 1 | |
| fi | |
| while (( SECONDS < deadline )); do | |
| run=$(gh api --method GET "repos/${GITHUB_REPOSITORY}/actions/runs" \ | |
| -f head_sha="$GITHUB_SHA" \ | |
| -F per_page=100 \ | |
| --jq "[.workflow_runs[] | select(.workflow_id == $ci_workflow_id and .head_sha == \"$GITHUB_SHA\" and .head_branch == \"main\" and .event == \"push\")] | sort_by(.created_at) | last // empty") | |
| if [[ -z "$run" ]]; then | |
| echo "Waiting for the CI run for this exact commit to appear." | |
| else | |
| status=$(jq -r '.status' <<<"$run") | |
| conclusion=$(jq -r '.conclusion // ""' <<<"$run") | |
| echo "Exact-SHA CI status: $status; conclusion: ${conclusion:-pending}." | |
| if [[ "$status" == "completed" ]]; then | |
| if [[ "$conclusion" == "success" ]]; then | |
| exit 0 | |
| fi | |
| echo "Exact-SHA CI did not succeed; release publication is blocked." | |
| exit 1 | |
| fi | |
| fi | |
| sleep 15 | |
| done | |
| echo "Timed out waiting for successful exact-SHA CI; release publication is blocked." | |
| exit 1 | |
| build: | |
| name: Build and Test | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| with: | |
| submodules: recursive | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install Claude Code CLI | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target_version="$(sed -nE 's/^[[:space:]]*public const string TargetVersion = "([0-9]+\.[0-9]+\.[0-9]+)";$/\1/p' ClaudeCodeSharpSDK/Models/ClaudeCliCompatibility.cs)" | |
| [[ -n "$target_version" && "$target_version" != *$'\n'* ]] || { echo "Expected exactly one three-part Claude CLI TargetVersion."; exit 1; } | |
| npm install --global "@anthropic-ai/claude-code@$target_version" | |
| version="$(claude --version)" | |
| printf '%s\n' "$version" | |
| actual_version="$(printf '%s\n' "$version" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | sed -n '1p')" | |
| printf 'Installed Claude CLI version: %s\n' "$actual_version" | |
| [[ "$actual_version" == "$target_version" ]] | |
| - name: Verify Claude Code CLI | |
| run: claude --version | |
| - name: Verify unauthenticated Claude Code behavior | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| sandbox="$RUNNER_TEMP/claude-empty-home" | |
| output_file="$RUNNER_TEMP/claude-unauth-output.txt" | |
| rm -rf "$sandbox" | |
| mkdir -p "$sandbox/.config" "$sandbox/AppData/Roaming" "$sandbox/AppData/Local" | |
| set +e | |
| HOME="$sandbox" \ | |
| USERPROFILE="$sandbox" \ | |
| XDG_CONFIG_HOME="$sandbox/.config" \ | |
| APPDATA="$sandbox/AppData/Roaming" \ | |
| LOCALAPPDATA="$sandbox/AppData/Local" \ | |
| claude -p "health check" >"$output_file" 2>&1 | |
| exit_code=$? | |
| set -e | |
| cat "$output_file" | |
| if [ "$exit_code" -eq 0 ]; then | |
| echo "Expected unauthenticated Claude CLI invocation to fail in isolated profile." | |
| exit 1 | |
| fi | |
| grep -Eiq "Please run /login|Not logged in|Not authenticated|Invalid API key|claude login" "$output_file" | |
| - name: Extract version from Directory.Build.props | |
| id: version | |
| run: | | |
| VERSION=$(grep -oPm1 "(?<=<Version>)[^<]+" Directory.Build.props) | |
| if [ -z "$VERSION" ]; then | |
| echo "Failed to resolve package version from Directory.Build.props" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "Version from Directory.Build.props: $VERSION" | |
| - name: Validate semantic version | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then | |
| echo "Version '$VERSION' is not a valid semantic version." | |
| exit 1 | |
| fi | |
| - name: Restore dependencies | |
| run: dotnet restore ManagedCode.ClaudeCodeSharpSDK.slnx | |
| - name: Build | |
| run: dotnet build ManagedCode.ClaudeCodeSharpSDK.slnx --configuration Release --warnaserror --no-restore | |
| - name: Test (full solution) | |
| run: dotnet test --solution ManagedCode.ClaudeCodeSharpSDK.slnx --configuration Release --no-build -- --treenode-filter "/*/*/*/*[RequiresClaudeAuth!=true]" | |
| - name: Install Native AOT prerequisites | |
| run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev libssl-dev libbrotli-dev | |
| - name: Publish and run Native AOT smoke | |
| run: | | |
| dotnet publish AotSmoke/ManagedCode.ClaudeCodeSharpSDK.AotSmoke.csproj --configuration Release --runtime linux-x64 --output ./AotSmoke/artifacts | |
| ./AotSmoke/artifacts/ManagedCode.ClaudeCodeSharpSDK.AotSmoke | |
| - name: Claude Code CLI smoke tests | |
| run: dotnet test --project ClaudeCodeSharpSDK.Tests/ClaudeCodeSharpSDK.Tests.csproj --configuration Release --no-build -- --treenode-filter "/*/*/*/ClaudeCli_Smoke_*" | |
| - name: Pack NuGet packages | |
| run: | | |
| dotnet pack ClaudeCodeSharpSDK/ClaudeCodeSharpSDK.csproj --configuration Release --no-build -p:IncludeSymbols=false -p:SymbolPackageFormat=snupkg --output ./artifacts | |
| dotnet pack ClaudeCodeSharpSDK.Extensions.AI/ClaudeCodeSharpSDK.Extensions.AI.csproj --configuration Release --no-build -p:IncludeSymbols=false -p:SymbolPackageFormat=snupkg --output ./artifacts | |
| dotnet pack ClaudeCodeSharpSDK.Extensions.AgentFramework/ClaudeCodeSharpSDK.Extensions.AgentFramework.csproj --configuration Release --no-build -p:IncludeSymbols=false -p:SymbolPackageFormat=snupkg --output ./artifacts | |
| - name: Validate packaged version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ steps.version.outputs.version }}" | |
| mapfile -t packages < <(find ./artifacts -maxdepth 1 -type f -name '*.nupkg' | sort) | |
| if [ "${#packages[@]}" -eq 0 ]; then | |
| echo "No NuGet packages were created." | |
| exit 1 | |
| fi | |
| for package in "${packages[@]}"; do | |
| file_name=$(basename "$package") | |
| echo "Validating $file_name" | |
| expected_suffix=".${VERSION}.nupkg" | |
| if [[ "$file_name" != *"${expected_suffix}" ]]; then | |
| echo "Package version mismatch: expected filename to end with ${expected_suffix}" | |
| exit 1 | |
| fi | |
| done | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: ./artifacts/*.nupkg | |
| retention-days: 5 | |
| publish-nuget: | |
| name: Publish to NuGet | |
| needs: [build, verify-ci] | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' | |
| outputs: | |
| published: ${{ steps.publish.outputs.published }} | |
| version: ${{ needs.build.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v5 | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v5 | |
| with: | |
| name: nuget-packages | |
| path: ./artifacts | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Publish to NuGet | |
| id: publish | |
| run: | | |
| set +e | |
| OUTPUT="" | |
| PUBLISHED=false | |
| for package in ./artifacts/*.nupkg; do | |
| echo "Publishing $package..." | |
| RESULT=$(dotnet nuget push "$package" \ | |
| --api-key ${{ secrets.NUGET_API_KEY }} \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --skip-duplicate 2>&1) | |
| EXIT_CODE=$? | |
| echo "$RESULT" | |
| OUTPUT="$OUTPUT$RESULT" | |
| if [ $EXIT_CODE -eq 0 ]; then | |
| echo "Successfully published $package" | |
| PUBLISHED=true | |
| elif echo "$RESULT" | grep -qi "already exists"; then | |
| echo "Package already exists, skipping..." | |
| else | |
| echo "Failed to publish $package" | |
| exit 1 | |
| fi | |
| done | |
| if [ "$PUBLISHED" = true ] || echo "$OUTPUT" | grep -q "Your package was pushed"; then | |
| echo "published=true" >> "$GITHUB_OUTPUT" | |
| echo "At least one package was successfully published" | |
| else | |
| echo "published=false" >> "$GITHUB_OUTPUT" | |
| echo "No new packages were published (all already exist)" | |
| fi | |
| create-release: | |
| name: Create GitHub Release and Tag | |
| needs: publish-nuget | |
| runs-on: ubuntu-latest | |
| if: github.ref == 'refs/heads/main' && needs.publish-nuget.result == 'success' | |
| permissions: | |
| actions: read | |
| contents: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.publish-nuget.outputs.version }} | |
| TAG: v${{ needs.publish-nuget.outputs.version }} | |
| PACKAGES_PUBLISHED: ${{ needs.publish-nuget.outputs.published }} | |
| steps: | |
| - name: Verify GitHub CLI | |
| run: gh --version | |
| - name: Resolve release state | |
| id: state | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| RELEASE_EXISTS=true | |
| else | |
| RELEASE_EXISTS=false | |
| fi | |
| SHOULD_SYNC=false | |
| if [ "$PACKAGES_PUBLISHED" = "true" ] || [ "$RELEASE_EXISTS" != "true" ]; then | |
| SHOULD_SYNC=true | |
| fi | |
| echo "release_exists=$RELEASE_EXISTS" >> "$GITHUB_OUTPUT" | |
| echo "should_sync=$SHOULD_SYNC" >> "$GITHUB_OUTPUT" | |
| echo "Release exists: $RELEASE_EXISTS" | |
| echo "Should sync release: $SHOULD_SYNC" | |
| - name: Download package artifacts | |
| if: steps.state.outputs.should_sync == 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p artifacts | |
| gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" --name nuget-packages --dir artifacts | |
| find artifacts -maxdepth 2 -type f -name '*.nupkg' -print | |
| - name: Create or update GitHub Release | |
| if: steps.state.outputs.should_sync == 'true' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mapfile -t packages < <(find artifacts -maxdepth 2 -type f -name '*.nupkg' | sort) | |
| if [ "${#packages[@]}" -eq 0 ]; then | |
| echo "No NuGet package artifacts were downloaded." | |
| exit 1 | |
| fi | |
| NOTES=$(cat <<EOF | |
| ## NuGet Packages | |
| - [ManagedCode.ClaudeCodeSharpSDK v$VERSION](https://www.nuget.org/packages/ManagedCode.ClaudeCodeSharpSDK/$VERSION) | |
| - [ManagedCode.ClaudeCodeSharpSDK.Extensions.AI v$VERSION](https://www.nuget.org/packages/ManagedCode.ClaudeCodeSharpSDK.Extensions.AI/$VERSION) | |
| - [ManagedCode.ClaudeCodeSharpSDK.Extensions.AgentFramework v$VERSION](https://www.nuget.org/packages/ManagedCode.ClaudeCodeSharpSDK.Extensions.AgentFramework/$VERSION) | |
| EOF | |
| ) | |
| if [ "${{ steps.state.outputs.release_exists }}" = "true" ]; then | |
| mapfile -t existing_assets < <(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name') | |
| for asset in "${existing_assets[@]}"; do | |
| case "$asset" in | |
| ManagedCode.ClaudeCodeSharpSDK*.nupkg) | |
| keep_asset=false | |
| for package in "${packages[@]}"; do | |
| if [ "$asset" = "$(basename "$package")" ]; then | |
| keep_asset=true | |
| break | |
| fi | |
| done | |
| if [ "$keep_asset" != "true" ]; then | |
| gh release delete-asset "$TAG" "$asset" --repo "$GITHUB_REPOSITORY" --yes | |
| fi | |
| ;; | |
| esac | |
| done | |
| gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --notes "$NOTES" | |
| gh release upload "$TAG" "${packages[@]}" --repo "$GITHUB_REPOSITORY" --clobber | |
| else | |
| gh release create "$TAG" "${packages[@]}" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --target "$GITHUB_SHA" \ | |
| --title "$TAG" \ | |
| --generate-notes \ | |
| --notes "$NOTES" | |
| fi | |
| - name: Skip release sync | |
| if: steps.state.outputs.should_sync != 'true' | |
| run: echo "Release already exists and no new NuGet packages were published." |