Files
Jens GeyerandClaude Opus 5.5 16768cfed4 THRIFT-6400: Package the voted compiler in the .NET tool
Client: build

The .NET tool workflow built thrift.exe from the release tag and packed
that build. A new build of the same source is not the same bytes, so
Apache.Thrift.Compiler did not carry the thrift-<version>.exe the
release vote covered: the compiler in the 0.25.0 package has SHA-256
3d3f2e8a..., the voted one 9e1cb466.... THRIFT-6397 made the Windows
installer package the voted executable, and the .NET tool now does the
same.

When the GitHub release is published, a new voted-compiler job runs the
tests of get-voted-compiler.ps1 and then uses it to fetch
thrift-<version>.exe from dist/release, checking its checksums and its
signature against KEYS. The package carries that file and the LICENSE
and NOTICE of the release tag. If the fetch or a check fails, nothing is
packed or published: a release does not fall back to building its own
compiler. A manual run with the new compiler_url input packs the
executable at that URL, for example a release candidate's, and never
publishes. Pull requests, and manual runs without compiler_url, still
build the compiler from source.

test-dotnet-tool.ps1 gets -ExpectedSha256, which the workflow uses to
check that the thrift.exe in the package is the file it packed. The run
summary names the compiler's source and hash.

The README shown on nuget.org, build/windows/README.md and
doc/ReleaseManagement.md describe it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 00:44:49 +02:00

357 lines
14 KiB
YAML

#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
#
# Publishes the Windows Thrift compiler to NuGet as a .NET tool, so that it can
# be installed with "dotnet tool install --global Apache.Thrift.Compiler".
#
# The package of a release has to carry the thrift.exe the vote covered, not a
# new build of the same source: two builds are not the same bytes. So where
# there is such an executable, this workflow packs that one, as
# windows-packages.yml does for the installer:
#
# - for a GitHub release, the thrift-<version>.exe in the release's
# directory on dist.apache.org;
# - for a manual run with compiler_url, the executable at that URL, such as
# the one a release candidate put on dist/dev. A manual run never
# publishes; it shows that the package works on a Windows runner.
#
# It is used only once its checksum and its signature against KEYS check out.
# Pull requests, and manual runs without compiler_url, build the compiler from
# source instead. That build mirrors the compiler-windows job in cmake.yml,
# which is what actually tests it on every push.
name: ".NET tool"
on:
release:
types: [published]
pull_request:
branches:
- master
paths:
- ".github/workflows/dotnet-tool.yml"
- "build/windows/dotnet-tool/**"
- "build/windows/build-dotnet-tool.ps1"
- "build/windows/check-compiler-imports.ps1"
- "doc/ReleaseManagement.md"
workflow_dispatch:
inputs:
compiler_url:
description: "A signed thrift-X.Y.Z.exe on dist.apache.org to pack, for example https://dist.apache.org/repos/dist/release/thrift/0.25.0/thrift-0.25.0.exe. Empty builds the compiler from the branch instead. A manual run never publishes."
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
voted-compiler:
name: Voted compiler
if: ${{ github.event_name == 'release' || inputs.compiler_url != '' }}
# gpg is at hand on Linux.
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
version: ${{ steps.fetch.outputs.version }}
url: ${{ steps.fetch.outputs.url }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# windows-packages.yml runs these on every pull request that touches
# them. Here they run before the download they guard, so that a release
# does not rely on checks that no longer work.
- name: Test the download checks
shell: pwsh
run: ./build/windows/get-voted-compiler-tests.ps1
- name: Download and check the compiler
id: fetch
shell: pwsh
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
INPUT_URL: ${{ inputs.compiler_url }}
run: |
$url = $env:INPUT_URL
if (-not $url) {
# A release is promoted to dist/release before its GitHub release
# is published, so the executable is already there.
$version = $env:RELEASE_TAG -replace '^v', ''
$url = "https://dist.apache.org/repos/dist/release/thrift/$version/thrift-$version.exe"
}
"url=$url" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
./build/windows/get-voted-compiler.ps1 -Url $url -OutputDir voted
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Collect LICENSE and NOTICE
env:
VERSION: ${{ steps.fetch.outputs.version }}
run: |
# The package carries the LICENSE and NOTICE of the release it
# packs. A release has a tag to take them from. A release
# candidate has none yet, so its files come from the branch this
# runs on, which then has to be at that version.
if git fetch --quiet --depth=1 origin "refs/tags/v${VERSION}:refs/tags/v${VERSION}" 2>/dev/null; then
git show "v${VERSION}:LICENSE" > voted/LICENSE
git show "v${VERSION}:NOTICE" > voted/NOTICE
echo "LICENSE and NOTICE from tag v${VERSION}"
else
branch_version=$(sed -n 's/^ *set(thrift_VERSION "\([0-9.]*\)").*/\1/p' CMakeLists.txt)
if [ "$branch_version" != "$VERSION" ]; then
echo "::error::There is no tag v${VERSION}, and this branch is at ${branch_version}. Run this from the branch the release candidate was made from."
exit 1
fi
cp LICENSE NOTICE voted/
echo "LICENSE and NOTICE from ${GITHUB_REF_NAME}"
fi
- name: Archive the compiler
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: voted-compiler
if-no-files-found: error
path: voted/
retention-days: 3
pack:
name: Pack
needs: voted-compiler
# Runs when the voted compiler is there, and when there was none to fetch
# and voted-compiler was skipped. Not when fetching or checking it failed:
# a release must not fall back to building its own compiler.
if: ${{ !cancelled() && needs.voted-compiler.result != 'failure' }}
runs-on: windows-2025
timeout-minutes: 60
permissions:
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Determine the version
id: version
shell: pwsh
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
VOTED_VERSION: ${{ needs.voted-compiler.outputs.version }}
run: |
$sourceVersion = ''
foreach ($line in Get-Content CMakeLists.txt) {
if ($line -match '^\s*set\(thrift_VERSION\s+"(\d+\.\d+\.\d+)"\)') {
$sourceVersion = $Matches[1]
break
}
}
if (-not $sourceVersion) {
Write-Error 'Could not read thrift_VERSION from CMakeLists.txt.'
exit 1
}
$version = $sourceVersion
if ($env:RELEASE_TAG) {
# A NuGet version can never be replaced, so refuse to publish a
# package whose version disagrees with the release it comes from.
# release_rust.yml does the same for the crate.
$tagVersion = $env:RELEASE_TAG -replace '^v', ''
if ($tagVersion -ne $sourceVersion) {
Write-Error "Version mismatch: CMakeLists.txt=$sourceVersion, tag=$env:RELEASE_TAG"
exit 1
}
$version = $tagVersion
}
if ($env:VOTED_VERSION) {
# On a release the voted executable was fetched for the tag's
# version. A manual run packs whatever release compiler_url names,
# which need not be the one this branch is at.
if ($env:RELEASE_TAG -and $env:VOTED_VERSION -ne $version) {
Write-Error "Version mismatch: voted compiler=$env:VOTED_VERSION, tag=$env:RELEASE_TAG"
exit 1
}
$version = $env:VOTED_VERSION
}
Write-Host "Version: $version"
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Download the voted compiler
if: ${{ needs.voted-compiler.result == 'success' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: voted-compiler
path: voted
- name: Install dependencies
if: ${{ needs.voted-compiler.result != 'success' }}
shell: pwsh
timeout-minutes: 10
# Pinned to the version build/docker/msvc/Dockerfile uses.
run: choco install winflexbison3 --version=2.5.24.20210105 -y --no-progress
- name: Build the compiler
if: ${{ needs.voted-compiler.result != 'success' }}
shell: pwsh
timeout-minutes: 30
run: |
# No -DWITH_MT: the released compiler links the shared runtime. See THRIFT-6320.
cmake -S . -B cmake_build -DBUILD_LIBRARIES=OFF -DBUILD_TESTING=OFF
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --build cmake_build --config Release
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
cmake --install cmake_build --config Release --prefix cmake_install
- name: Choose the compiler to pack
id: compiler
shell: pwsh
env:
VERSION: ${{ steps.version.outputs.version }}
VOTED: ${{ needs.voted-compiler.result == 'success' }}
run: |
if ($env:VOTED -eq 'true') {
$compiler = "voted/thrift-$env:VERSION.exe"
# The LICENSE and NOTICE of that release, collected with it.
$sourceRoot = 'voted'
}
else {
$compiler = 'cmake_install/bin/thrift.exe'
$sourceRoot = ''
}
$sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $compiler).Hash.ToLowerInvariant()
Write-Host "Packing $compiler, sha256 $sha256"
"path=$compiler" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"source_root=$sourceRoot" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"sha256=$sha256" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Check what the compiler imports
shell: pwsh
env:
COMPILER: ${{ steps.compiler.outputs.path }}
# A published NuGet version cannot be replaced, so this has to hold
# before the package is built, not only in cmake.yml afterwards.
run: ./build/windows/check-compiler-imports.ps1 -Path $env:COMPILER
- name: Pack the tool
shell: pwsh
env:
VERSION: ${{ steps.version.outputs.version }}
COMPILER: ${{ steps.compiler.outputs.path }}
SOURCE_ROOT: ${{ steps.compiler.outputs.source_root }}
run: |
$toolArgs = @{ Version = $env:VERSION; Compiler = $env:COMPILER; OutputDir = 'nupkg' }
if ($env:SOURCE_ROOT) {
$toolArgs['SourceRoot'] = $env:SOURCE_ROOT
}
./build/windows/build-dotnet-tool.ps1 @toolArgs
- name: Test the package
shell: pwsh
timeout-minutes: 15
env:
VERSION: ${{ steps.version.outputs.version }}
COMPILER_SHA256: ${{ steps.compiler.outputs.sha256 }}
run: |
# The hash shows that the package carries the very file it was
# given - for a release, the one the vote covered.
./build/windows/dotnet-tool/test-dotnet-tool.ps1 `
-Package "nupkg/Apache.Thrift.Compiler.$env:VERSION.nupkg" `
-Version $env:VERSION `
-ExpectedCompilerOutput "Thrift version $env:VERSION" `
-ExpectedSha256 $env:COMPILER_SHA256
- name: Report the compiler
shell: pwsh
env:
VERSION: ${{ steps.version.outputs.version }}
COMPILER_SHA256: ${{ steps.compiler.outputs.sha256 }}
COMPILER_URL: ${{ needs.voted-compiler.outputs.url }}
run: |
$package = "nupkg/Apache.Thrift.Compiler.$env:VERSION.nupkg"
$hash = (Get-FileHash -Algorithm SHA256 $package).Hash.ToLowerInvariant()
@(
'### .NET tool',
'',
"| | |",
"|---|---|",
"| package | ``$([System.IO.Path]::GetFileName($package))`` |",
"| package sha256 | ``$hash`` |",
"| compiler | $(if ($env:COMPILER_URL) { $env:COMPILER_URL } else { 'built from source' }) |",
"| compiler sha256 | ``$env:COMPILER_SHA256`` |"
) | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append
- name: Archive the package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dotnet-tool
if-no-files-found: error
path: nupkg/*.nupkg
retention-days: 3
publish:
name: Publish to NuGet
needs: pack
if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release
permissions:
contents: read
# Mandatory for NuGet trusted publishing.
id-token: write
steps:
- name: Download the package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dotnet-tool
path: nupkg
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "8.0"
# Trusted publishing, as for PyPI in pypi.yml and crates.io in
# release_rust.yml: nuget.org hands back a short lived key in exchange
# for this run's OIDC token, so no long lived secret is stored here.
# It needs a trusted publishing policy on nuget.org naming this
# repository, this workflow and the release environment.
- name: Authenticate to NuGet
id: nuget-auth
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
with:
user: ${{ vars.NUGET_USER }}
- name: Publish
env:
NUGET_API_KEY: ${{ steps.nuget-auth.outputs.NUGET_API_KEY }}
VERSION: ${{ needs.pack.outputs.version }}
run: |
dotnet nuget push "nupkg/Apache.Thrift.Compiler.${VERSION}.nupkg" \
--source https://api.nuget.org/v3/index.json \
--api-key "$NUGET_API_KEY" \
--skip-duplicate