Files
2026-09-11 09:07:01 -07:00

855 lines
29 KiB
YAML

name: Release All Components
# Unified release workflow for all ExcelMcp components:
# - MCP Server (standalone exe ZIP — primary) + NuGet (.NET tool — secondary)
# - CLI (standalone exe ZIP — primary) + NuGet (.NET tool — secondary)
# - VS Code Extension (VSIX + Marketplace)
# - MCPB (Claude Desktop bundle)
# - Agent Skills (ZIP package)
#
# All components are released with the same version.
# Trigger: workflow_dispatch with version bump (major/minor/patch) or custom version.
# The workflow auto-calculates the next version from the latest git tag,
# creates the git tag after successful builds, then creates the GitHub release.
#
# Required GitHub Secrets:
# - NUGET_USER: Your NuGet.org username (profile name, NOT email)
# - VSCE_TOKEN: VS Code Marketplace PAT
# - APPINSIGHTS_CONNECTION_STRING: Application Insights connection string
#
on:
workflow_dispatch:
inputs:
version_bump:
description: 'Version bump type'
required: true
type: choice
default: 'patch'
options:
- major
- minor
- patch
custom_version:
description: 'Custom version (e.g., 1.2.3, overrides version_bump)'
required: false
type: string
env:
DOTNET_VERSION: '10.0.x'
NODE_VERSION: '22'
concurrency:
group: release
cancel-in-progress: false
jobs:
# =============================================================================
# Job 0: Calculate Version
# =============================================================================
version:
name: Calculate Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.calculate_version.outputs.version }}
tag: ${{ steps.calculate_version.outputs.tag }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history and tags
- name: Calculate version
id: calculate_version
run: |
# If custom version is provided, use it
if [ -n "${{ inputs.custom_version }}" ]; then
VERSION="${{ inputs.custom_version }}"
echo "Using custom version: ${VERSION}"
else
# Get the latest tag
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "Latest tag: ${LATEST_TAG}"
# Remove 'v' prefix if present
CURRENT_VERSION="${LATEST_TAG#v}"
echo "Current version: ${CURRENT_VERSION}"
# Parse version components
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"
# Increment based on bump type
case "${{ inputs.version_bump }}" in
major)
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
;;
minor)
MINOR=$((MINOR + 1))
PATCH=0
;;
patch)
PATCH=$((PATCH + 1))
;;
esac
VERSION="${MAJOR}.${MINOR}.${PATCH}"
echo "New version: ${VERSION}"
fi
TAG="v${VERSION}"
# Check if tag already exists
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# =============================================================================
# Job 1: Prepare the changelog before packaging
# =============================================================================
prepare-release:
name: Prepare Release Changelog
needs: [version]
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install changesets CLI
run: npm ci
- name: Compile changesets into CHANGELOG.md
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$releaseDate = Get-Date -AsUTC -Format 'yyyy-MM-dd'
Set-Content -Path release_date.txt -Value $releaseDate -NoNewline
./scripts/Build-Changelog.ps1 -Version "${{ needs.version.outputs.version }}" -Date $releaseDate
- name: Upload Prepared Changelog
uses: actions/upload-artifact@v7
with:
name: release-changelog
path: |
CHANGELOG.md
release_notes_body.md
release_date.txt
# =============================================================================
# Job 2: Build CLI (standalone exe — primary + NuGet pack — secondary)
# =============================================================================
build-cli:
name: CLI
needs: [version]
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
shell: pwsh
- name: Restore
run: dotnet restore src/ExcelMcp.CLI/ExcelMcp.CLI.csproj
- name: Build CLI
run: dotnet build src/ExcelMcp.CLI/ExcelMcp.CLI.csproj --configuration Release --no-restore -p:Version=${{ env.VERSION }} -p:AssemblyVersion=${{ env.VERSION }}.0 -p:FileVersion=${{ env.VERSION }}.0
- name: Pack CLI NuGet (secondary distribution)
run: dotnet pack src/ExcelMcp.CLI/ExcelMcp.CLI.csproj --configuration Release --no-build --output ./cli-nupkg -p:Version=${{ env.VERSION }}
- name: Publish CLI (standalone exe — primary distribution)
run: |
dotnet publish src/ExcelMcp.CLI/ExcelMcp.CLI.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:PublishTrimmed=false `
-p:PublishReadyToRun=false `
-p:Version=${{ env.VERSION }} `
-p:AssemblyVersion=${{ env.VERSION }}.0 `
-p:FileVersion=${{ env.VERSION }}.0 `
-p:NuGetAudit=false `
--output ./cli-publish
shell: pwsh
- name: Create CLI ZIP
run: |
$version = $env:VERSION
New-Item -ItemType Directory -Path "cli-release" -Force
Copy-Item "cli-publish/excelcli.exe" "cli-release/"
Copy-Item "README.md" "cli-release/"
Copy-Item "LICENSE" "cli-release/"
Compress-Archive -Path "cli-release/*" -DestinationPath "ExcelMcp-CLI-$version-windows.zip"
shell: pwsh
- name: Upload CLI ZIP Artifact
uses: actions/upload-artifact@v7
with:
name: cli-zip
path: ExcelMcp-CLI-*-windows.zip
- name: Upload CLI NuGet Artifact
uses: actions/upload-artifact@v7
with:
name: cli-nupkg
path: cli-nupkg/*.nupkg
- name: Upload CLI Build Output (for other jobs)
uses: actions/upload-artifact@v7
with:
name: cli-build
path: cli-publish/
# =============================================================================
# Job 2: Build MCP Server (standalone exe — primary + NuGet pack — secondary)
# =============================================================================
build-mcp-server:
name: MCP Server
needs: [version]
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
Write-Output "Version: $version"
shell: pwsh
- name: Update Project Versions
run: |
.\scripts\Update-McpRegistryMetadata.ps1 `
-ServerJsonPath "src\ExcelMcp.McpServer\.mcp\server.json" `
-Version $env:VERSION
shell: pwsh
- name: Restore
run: dotnet restore src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj
- name: Build
run: dotnet build src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj --configuration Release --no-restore -p:Version=${{ env.VERSION }} -p:AssemblyVersion=${{ env.VERSION }}.0 -p:FileVersion=${{ env.VERSION }}.0
env:
APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
- name: Pack NuGet (secondary distribution)
run: dotnet pack src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj --configuration Release --no-build --output ./nupkg -p:Version=${{ env.VERSION }}
- name: Publish MCP Server (standalone exe — primary distribution)
run: |
dotnet publish src/ExcelMcp.McpServer/ExcelMcp.McpServer.csproj `
--configuration Release `
--runtime win-x64 `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
-p:PublishTrimmed=false `
-p:PublishReadyToRun=false `
-p:Version=${{ env.VERSION }} `
-p:AssemblyVersion=${{ env.VERSION }}.0 `
-p:FileVersion=${{ env.VERSION }}.0 `
-p:NuGetAudit=false `
--output ./mcp-server-publish
shell: pwsh
env:
APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
- name: Rename exe to mcp-excel.exe
run: |
$expectedExe = "mcp-server-publish/Sbroenne.ExcelMcp.McpServer.exe"
if (Test-Path $expectedExe) {
Rename-Item $expectedExe "mcp-excel.exe"
} else {
Write-Error "Expected executable not found at: $expectedExe"
Write-Host "Contents of mcp-server-publish/:"
Get-ChildItem "mcp-server-publish/" | Select-Object Name
exit 1
}
shell: pwsh
- name: Create Release Package
run: |
$version = $env:VERSION
New-Item -ItemType Directory -Path "mcp-server-release" -Force
Copy-Item "mcp-server-publish/mcp-excel.exe" "mcp-server-release/"
Copy-Item "README.md" "mcp-server-release/"
Copy-Item "LICENSE" "mcp-server-release/"
Compress-Archive -Path "mcp-server-release/*" -DestinationPath "ExcelMcp-MCP-Server-$version-windows.zip"
shell: pwsh
- name: Upload ZIP Artifact
uses: actions/upload-artifact@v7
with:
name: mcp-server-zip
path: ExcelMcp-MCP-Server-*-windows.zip
- name: Upload NuGet Artifact
uses: actions/upload-artifact@v7
with:
name: mcp-server-nupkg
path: nupkg/*.nupkg
# =============================================================================
# Job 3: Build and Publish VS Code Extension
# =============================================================================
build-vscode:
name: VS Code Extension
needs: [version, prepare-release]
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
shell: pwsh
- name: Synchronize Release Metadata
run: |
.\scripts\Update-ReleaseVersionMetadata.ps1 -Version $env:VERSION
shell: pwsh
- name: Download Prepared Changelog
uses: actions/download-artifact@v8
with:
name: release-changelog
path: release-changelog
- name: Sync Prepared Changelog
run: |
Copy-Item "release-changelog/CHANGELOG.md" "CHANGELOG.md" -Force
shell: pwsh
- name: Install Dependencies
run: |
cd vscode-extension
npm install
- name: Build and Package
run: |
cd vscode-extension
npm run package
env:
APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
- name: Rename VSIX
run: |
$version = $env:VERSION
cd vscode-extension
$vsix = Get-ChildItem -Filter "*.vsix" | Select-Object -First 1
$targetName = "excelmcp-$version.vsix"
if ($vsix.Name -ne $targetName) {
Rename-Item $vsix.FullName -NewName $targetName
}
shell: pwsh
- name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: vscode-vsix
path: vscode-extension/excelmcp-*.vsix
# =============================================================================
# Job 4: Build MCPB (Claude Desktop Bundle)
# =============================================================================
build-mcpb:
name: MCPB Bundle
needs: [version, prepare-release]
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
shell: pwsh
- name: Download Prepared Changelog
uses: actions/download-artifact@v8
with:
name: release-changelog
path: release-changelog
- name: Sync Prepared Changelog
run: Copy-Item "release-changelog/CHANGELOG.md" "CHANGELOG.md" -Force
shell: pwsh
- name: Build MCPB Bundle
run: |
cd mcpb
.\Build-McpBundle.ps1 -Version $env:VERSION
shell: pwsh
env:
APPINSIGHTS_CONNECTION_STRING: ${{ secrets.APPINSIGHTS_CONNECTION_STRING }}
- name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: mcpb-bundle
path: mcpb/artifacts/excel-mcp-*.mcpb
# =============================================================================
# Job 5: Build Agent Skills Packages
# =============================================================================
build-agent-skills:
name: Agent Skills
needs: [version]
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
shell: pwsh
- name: Build Solution (generates SKILL.md and copies references)
run: dotnet build -c Release
shell: pwsh
- name: Create Skills Package
run: |
& .\scripts\Build-AgentSkills.ps1 -OutputDir "artifacts/skills" -Version $env:VERSION
shell: pwsh
- name: Upload Skills Artifact
uses: actions/upload-artifact@v7
with:
name: agent-skills
path: artifacts/skills/excel-skills-*.zip
# =============================================================================
# Job 6: Wait for NuGet Propagation + MCP Registry
# =============================================================================
publish-mcp-registry:
name: MCP Registry
needs: [version, create-tag, build-mcp-server]
runs-on: windows-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v7
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
shell: pwsh
- name: Update server.json Version
run: |
.\scripts\Update-McpRegistryMetadata.ps1 `
-ServerJsonPath "src\ExcelMcp.McpServer\.mcp\server.json" `
-Version $env:VERSION
shell: pwsh
- name: Wait for NuGet Propagation
run: |
$version = $env:VERSION
$packageId = "sbroenne.excelmcp.mcpserver"
$readmeUrl = "https://api.nuget.org/v3-flatcontainer/$packageId/$version/readme"
Write-Output "Waiting for NuGet CDN propagation..."
$maxAttempts = 3
$pollInterval = 600 # 10 minutes
# First check immediately; subsequent checks wait pollInterval seconds
for ($i = 1; $i -le $maxAttempts; $i++) {
Write-Output "Attempt $i of $maxAttempts..."
try {
$response = Invoke-WebRequest -Uri $readmeUrl -UseBasicParsing -ErrorAction Stop
if ($response.StatusCode -eq 200) {
$content = [System.Text.Encoding]::UTF8.GetString($response.Content)
if ($content -match "mcp-name:\s+io\.github\.sbroenne/mcp-server-excel") {
Write-Output "README propagated successfully"
break
}
}
} catch {
Write-Output "README not yet available"
}
if ($i -lt $maxAttempts) {
Write-Output "Waiting $pollInterval seconds before retry..."
Start-Sleep -Seconds $pollInterval
}
}
shell: pwsh
- name: Install MCP Publisher
run: |
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq "Arm64") { "arm64" } else { "amd64" }
Invoke-WebRequest -Uri "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_windows_$arch.tar.gz" -OutFile "mcp-publisher.tar.gz"
tar xf mcp-publisher.tar.gz mcp-publisher.exe
shell: pwsh
- name: Publish to MCP Registry
run: |
./mcp-publisher.exe login github-oidc
Set-Location src/ExcelMcp.McpServer/.mcp
../../../mcp-publisher.exe publish --verbose
shell: pwsh
# =============================================================================
# Job 7: Create Git Tag
# =============================================================================
create-tag:
name: Create Git Tag
needs: [version, prepare-release, build-cli, build-mcp-server, build-vscode, build-mcpb, build-agent-skills]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install changesets CLI
run: npm ci
- name: Download Prepared Changelog
uses: actions/download-artifact@v8
with:
name: release-changelog
path: prepared-release
- name: Compile and Verify Release Metadata
shell: pwsh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$releaseDate = Get-Content -Raw prepared-release/release_date.txt
./scripts/Build-Changelog.ps1 -Version "${{ needs.version.outputs.version }}" -Date $releaseDate
$generatedChangelog = (Get-FileHash CHANGELOG.md -Algorithm SHA256).Hash
$preparedChangelog = (Get-FileHash prepared-release/CHANGELOG.md -Algorithm SHA256).Hash
if ($generatedChangelog -ne $preparedChangelog) {
throw 'Generated CHANGELOG.md differs from the changelog used to build release artifacts.'
}
$generatedNotes = (Get-FileHash release_notes_body.md -Algorithm SHA256).Hash
$preparedNotes = (Get-FileHash prepared-release/release_notes_body.md -Algorithm SHA256).Hash
if ($generatedNotes -ne $preparedNotes) {
throw 'Generated release notes differ from the notes prepared before artifact builds.'
}
- name: Commit Release Metadata Update
id: release_commit
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
set -euo pipefail
VERSION=${{ needs.version.outputs.version }}
REPO="${{ github.repository }}"
if [ -z "${GH_TOKEN:-}" ]; then
echo "::error::RELEASE_PAT secret is not configured. Add an admin Personal Access Token with 'contents: write' as the repo secret RELEASE_PAT so release metadata can be committed back to main."
exit 1
fi
# Stage exactly the files Build-Changelog.ps1 touches.
git add -A CHANGELOG.md package.json package-lock.json Directory.Build.props \
mcpb/manifest.json src/ExcelMcp.McpServer/.mcp/server.json \
vscode-extension/package.json vscode-extension/package-lock.json .changeset
BASE_SHA=$(gh api "repos/$REPO/git/ref/heads/main" --jq '.object.sha')
if git diff --cached --quiet; then
echo "No release metadata changes to commit; tagging main@$BASE_SHA."
echo "sha=$BASE_SHA" >> "$GITHUB_OUTPUT"
exit 0
fi
# Base the metadata commit on the current main tip so the update is a
# fast-forward even when main advanced while release artifacts built.
BASE_TREE=$(gh api "repos/$REPO/git/commits/$BASE_SHA" --jq '.tree.sha')
echo "Basing release metadata commit on main@$BASE_SHA"
tree_entries='[]'
while IFS=$'\t' read -r status path; do
[ -z "$path" ] && continue
if [ "$status" = "D" ]; then
echo " delete $path"
entry=$(jq -n --arg path "$path" '{path:$path, mode:"100644", type:"blob", sha:null}')
else
echo " write $path"
git show ":$path" | base64 -w0 > blob.b64
blob_sha=$(gh api "repos/$REPO/git/blobs" --method POST \
-f encoding=base64 -F content=@blob.b64 --jq '.sha')
rm -f blob.b64
staged_sha=$(git rev-parse ":$path")
if [ "$blob_sha" != "$staged_sha" ]; then
echo "::error::Uploaded blob for $path does not match the staged content."
exit 1
fi
entry=$(jq -n --arg path "$path" --arg sha "$blob_sha" \
'{path:$path, mode:"100644", type:"blob", sha:$sha}')
fi
tree_entries=$(jq -c --argjson e "$entry" '. + [$e]' <<<"$tree_entries")
done < <(git diff --cached --name-status)
new_tree=$(jq -n --arg base "$BASE_TREE" --argjson tree "$tree_entries" \
'{base_tree:$base, tree:$tree}' \
| gh api "repos/$REPO/git/trees" --method POST --input - --jq '.sha')
new_commit=$(jq -n \
--arg msg "chore: update release metadata for v${VERSION} [skip ci]" \
--arg tree "$new_tree" --arg parent "$BASE_SHA" \
'{message:$msg, tree:$tree, parents:[$parent]}' \
| gh api "repos/$REPO/git/commits" --method POST --input - --jq '.sha')
gh api "repos/$REPO/git/refs/heads/main" --method PATCH \
-f sha="$new_commit" -F force=false >/dev/null
echo "sha=$new_commit" >> "$GITHUB_OUTPUT"
echo "Committed release metadata for v${VERSION} to main@$new_commit."
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Create and push tag
run: |
TAG="${{ needs.version.outputs.tag }}"
RELEASE_COMMIT="${{ steps.release_commit.outputs.sha }}"
git fetch origin main
echo "Creating tag $TAG at release metadata commit $RELEASE_COMMIT"
git tag -a "$TAG" "$RELEASE_COMMIT" -m "Release $TAG"
git push origin "$TAG"
# =============================================================================
# Job 8: Publish to External Registries (after all builds succeed)
# =============================================================================
publish:
name: Publish to Registries
needs: [version, create-tag]
runs-on: windows-latest
permissions:
contents: read
id-token: write # Required for NuGet OIDC
steps:
- uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set Version
run: |
$version = "${{ needs.version.outputs.version }}"
echo "VERSION=$version" >> $env:GITHUB_ENV
shell: pwsh
- name: Download NuGet Package
uses: actions/download-artifact@v8
with:
name: mcp-server-nupkg
path: nupkg
- name: Download CLI NuGet Package
uses: actions/download-artifact@v8
with:
name: cli-nupkg
path: cli-nupkg
- name: Download VS Code VSIX
uses: actions/download-artifact@v8
with:
name: vscode-vsix
path: vscode-vsix
- name: NuGet Login (OIDC)
uses: NuGet/login@v1
id: nuget-login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish to NuGet.org (secondary distribution)
run: |
$version = $env:VERSION
dotnet nuget push "nupkg/Sbroenne.ExcelMcp.McpServer.$version.nupkg" `
--api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} `
--source https://api.nuget.org/v3/index.json `
--skip-duplicate
Write-Output "Published Sbroenne.ExcelMcp.McpServer.$version to NuGet.org"
dotnet nuget push "cli-nupkg/Sbroenne.ExcelMcp.CLI.$version.nupkg" `
--api-key ${{ steps.nuget-login.outputs.NUGET_API_KEY }} `
--source https://api.nuget.org/v3/index.json `
--skip-duplicate
Write-Output "Published Sbroenne.ExcelMcp.CLI.$version to NuGet.org"
shell: pwsh
- name: Publish to VS Code Marketplace
uses: HaaLeo/publish-vscode-extension@v2
with:
pat: ${{ secrets.VSCE_TOKEN }}
registryUrl: https://marketplace.visualstudio.com
extensionFile: vscode-vsix/excelmcp-${{ env.VERSION }}.vsix
continue-on-error: true
# =============================================================================
# Job 9: Create Unified GitHub Release
# =============================================================================
create-release:
name: Create GitHub Release
needs: [version, prepare-release, create-tag, publish]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- name: Set Version
run: |
echo "VERSION=${{ needs.version.outputs.version }}" >> $GITHUB_ENV
- name: Download All Artifacts
uses: actions/download-artifact@v8
with:
path: artifacts
- name: List Artifacts
run: |
echo "Downloaded artifacts:"
find artifacts -type f -name "*" | head -20
- name: Render Release Notes
shell: pwsh
run: |
$template = Get-Content -Raw '.github/release-notes-template.md'
$changelogBody = Get-Content -Raw artifacts/release-changelog/release_notes_body.md
$notes = $template.Replace('{{VERSION}}', $env:VERSION).Replace('{{CHANGELOG}}', $changelogBody.Trim())
Set-Content -Path release_notes.md -Value $notes -NoNewline
- name: Generate SHA-256 checksums
shell: pwsh
run: |
$expectedArchives = @(
"ExcelMcp-MCP-Server-${{ env.VERSION }}-windows.zip"
"ExcelMcp-CLI-${{ env.VERSION }}-windows.zip"
)
$archivePaths = @(
Get-ChildItem -Path artifacts/mcp-server-zip -Filter *.zip -File
Get-ChildItem -Path artifacts/cli-zip -Filter *.zip -File
)
$actualNames = @($archivePaths | ForEach-Object Name)
foreach ($expectedArchive in $expectedArchives) {
if ($expectedArchive -notin $actualNames) {
throw "Cannot publish checksums: expected runtime archive '$expectedArchive' was not downloaded."
}
}
if ($archivePaths.Count -ne $expectedArchives.Count) {
throw "Cannot publish checksums: expected exactly $($expectedArchives.Count) runtime archives, found $($archivePaths.Count)."
}
$checksumLines = foreach ($archivePath in ($archivePaths | Sort-Object Name)) {
$hash = (Get-FileHash -Path $archivePath.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $($archivePath.Name)"
}
[System.IO.File]::WriteAllLines(
(Join-Path $PWD "SHA256SUMS"),
$checksumLines,
[System.Text.UTF8Encoding]::new($false))
- name: Create Release
run: |
VERSION=${{ env.VERSION }}
TAG=${{ needs.version.outputs.tag }}
# Collect all artifacts
ARTIFACTS=""
for f in artifacts/mcp-server-zip/*.zip; do [ -f "$f" ] && ARTIFACTS="$ARTIFACTS $f"; done
for f in artifacts/cli-zip/*.zip; do [ -f "$f" ] && ARTIFACTS="$ARTIFACTS $f"; done
for f in artifacts/vscode-vsix/*.vsix; do [ -f "$f" ] && ARTIFACTS="$ARTIFACTS $f"; done
for f in artifacts/mcpb-bundle/*.mcpb; do [ -f "$f" ] && ARTIFACTS="$ARTIFACTS $f"; done
for f in artifacts/agent-skills/*.zip; do [ -f "$f" ] && ARTIFACTS="$ARTIFACTS $f"; done
ARTIFACTS="$ARTIFACTS SHA256SUMS"
echo "Creating release with artifacts:$ARTIFACTS"
gh release create "$TAG" $ARTIFACTS \
--title "ExcelMcp $VERSION" \
--notes-file release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}