Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,25 @@ target frameworks, generates an SBOM and a provenance attestation, and pushes
to NuGet through trusted publishing. Renaming that workflow file breaks the
trusted-publishing policy registered on nuget.org, so change the policy first.

### The psmux preview gates

Publishing the preview needs the accepted Windows x64 executable, which is a
maintainer validation build rather than a published psmux release asset.
[`docs/psmux.md`](../docs/psmux.md) states the trust boundary it has to meet.

`release.yml` reads the repository variables `PSMUX_ARTIFACT_URL`,
`PSMUX_SOURCE_PROVENANCE_URL`, `PSMUX_LICENSE_URL`, `PSMUX_WSL_DISTRIBUTION`
and `PSMUX_WSL_DOTNET_PATH`, and needs a self-hosted `Windows`, `X64`, `psmux`
runner for the native and WSL gates. `PSMUX_WSL_DOTNET_PATH` is the absolute
Linux `dotnet` path for that checkout, which the runner reports:

```console
$ mise exec -- which dotnet
```

Those inputs make the gates runnable; they do not by themselves complete the
artifact or the runtime evidence.

### Recorded evidence is a release artifact

A capability row is `pending` until a matrix run records evidence for it, and
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/dotnet-tmux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
name: dotnet-tmux

on:
workflow_call:
push:
branches: [master]
pull_request:
Expand Down
22 changes: 9 additions & 13 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
name: dotnet

on:
workflow_call:
push:
branches: [master]
pull_request:
Expand Down Expand Up @@ -109,8 +110,8 @@ jobs:
done

- name: Examples
# An example that stopped working should fail the build, not the
# reader who copied it.
# An ordinary-tmux example that stopped working should fail here,
# before a reader copies it.
run: >
dotnet run
--project examples/LibTmux.Examples/LibTmux.Examples.csproj
Expand All @@ -119,10 +120,8 @@ jobs:
--no-build

- name: Example tests
# The same examples again, one test each, so a broken example is named
# in a report rather than being an exit code. This reaches them through
# the examples project and never through a project reference of its
# own, which is what makes it a reading of the surface a caller has.
# The ordinary-tmux examples run again, one test each, through the
# same compiled surface a caller reads.
run: >
dotnet test
--project tests/LibTmux.ExampleTests/LibTmux.ExampleTests.csproj
Expand Down Expand Up @@ -153,8 +152,10 @@ jobs:
# code that runs, and this is what says so out loud.
run: |
uv run python eng/parity/verify_public_api.py
uv run python eng/parity/render_public_api.py --check
uv run python eng/parity/verify_capabilities.py
uv run python eng/parity/verify_workflows.py
uv run python eng/docs/render_api_reference.py --check
uv run python eng/docs/sync_snippets.py --check
uv run eng/mcp/dump_tools.py --check

Expand All @@ -181,13 +182,8 @@ jobs:
path: artifacts/packages
if-no-files-found: warn

# The compatibility claim names macOS, and until now only Linux was ever run.
# The first run of this lane failed 15 of 854 integration tests, which is the
# answer it was added to get: the macOS support was asserted, not proven.
#
# It is advisory until those are fixed. Requiring it would block every commit
# on a platform difference nobody has diagnosed yet, and deleting it would go
# back to not knowing. Reporting it is the honest middle.
# macOS exercises the advertised platform but stays advisory; failures remain
# visible without making the primary Linux gate depend on hosted macOS.
macos:
name: macos arm64 (advisory)
runs-on: macos-latest
Expand Down
233 changes: 167 additions & 66 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Publishing. A version on nuget.org can never be deleted, only unlisted, so
# everything that can be proven about a package is proven here before the push
# rather than after it: the gate's build and tests, the package inspection, and
# a consumer that restores the built package from a folder feed and runs it.
# Publishing waits for same-commit gates and native/WSL psmux proof before
# nuget.org grants a token for its immutable feed.
#
# The name of this file is part of the trusted publishing policy on nuget.org.
# Renaming it means editing that policy, and until then no publish is possible.
Expand All @@ -25,97 +23,203 @@ env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'

jobs:
publish:
name: publish to nuget.org
validate:
name: validate release ref
runs-on: ubuntu-latest

# The policy on nuget.org names this environment, so a workflow that ran
# outside it cannot exchange a token. It is also where a required reviewer
# and a tag restriction belong.
environment: nuget

permissions:
contents: read
# Lets the job ask GitHub for the short-lived token nuget.org trades for
# a one-hour API key. Nothing else here needs a credential.
id-token: write
attestations: write

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# SourceLink points a debugger at the commit that produced the
# assembly, which a shallow checkout does not have.
fetch-depth: 0

- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

- name: Install tmux
# The consumer proof drives a real server rather than a stub.
run: |
sudo apt-get update
sudo apt-get install --yes tmux

- name: Check the tag matches the version
# A tag that disagrees with the version publishes something permanently
# mislabelled, and the mislabelling is what nobody can take back.
if: github.event_name == 'push'
env:
REF_TYPE: ${{ github.ref_type }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ "${REF_TYPE}" != tag ] || [[ "${TAG}" != v* ]]; then
echo "release must run at a v* tag, not ${REF_TYPE} ${TAG}" >&2
exit 1
fi
version="$(dotnet msbuild src/LibTmux/LibTmux.csproj \
-getProperty:Version -verbosity:quiet | tr -d '[:space:]')"
if [ "${TAG}" != "v${version}" ]; then
echo "tag ${TAG} does not match version ${version}" >&2
exit 1
fi
echo "publishing ${version} as ${TAG}"
echo "validated ${TAG} at ${GITHUB_SHA}"

- name: Restore
run: dotnet restore LibTmux.slnx --locked-mode
dotnet:
name: full dotnet gate
needs: validate
uses: ./.github/workflows/dotnet.yml

- name: Build
run: dotnet build LibTmux.slnx --configuration Release --no-restore --warnaserror
compatibility:
name: supported tmux matrix
needs: validate
uses: ./.github/workflows/dotnet-tmux.yml

- name: Unit tests
run: >
dotnet test
--project tests/LibTmux.UnitTests/LibTmux.UnitTests.csproj
--configuration Release
--framework net10.0
--no-build
--minimum-expected-tests 1
psmux-metadata:
name: published psmux artifact
needs: validate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify the published artifact and provenance links
env:
ARTIFACT_URL: ${{ vars.PSMUX_ARTIFACT_URL }}
LICENSE_URL: ${{ vars.PSMUX_LICENSE_URL }}
SOURCE_URL: ${{ vars.PSMUX_SOURCE_PROVENANCE_URL }}
run: |
set -euo pipefail
for variable in ARTIFACT_URL LICENSE_URL SOURCE_URL; do
value="${!variable:-}"
if [[ "${value}" != https://* ]]; then
echo "${variable} must name a published HTTPS resource" >&2
exit 1
fi
done
if [[ "${SOURCE_URL}" != *aa26cd39edcfab03e718f94ea21bb47e8c5b85e8* ]]; then
echo "SOURCE_URL must attest the exact audited psmux commit" >&2
exit 1
fi
curl --fail --location --proto '=https' --tlsv1.2 \
--connect-timeout 15 --max-time 120 \
"${ARTIFACT_URL}" --output "${RUNNER_TEMP}/psmux.exe"
echo "1abd0eaa3de1ed5491a4f744c8b3db492ae9ac94e9e9a8fea9da217c744ba94e ${RUNNER_TEMP}/psmux.exe" \
| sha256sum --check --strict
curl --fail --location --proto '=https' --tlsv1.2 \
--connect-timeout 15 --max-time 120 \
"${SOURCE_URL}" --output "${RUNNER_TEMP}/psmux-source-provenance"
curl --fail --location --proto '=https' --tlsv1.2 \
--connect-timeout 15 --max-time 120 \
"${LICENSE_URL}" --output "${RUNNER_TEMP}/psmux-license"
test -s "${RUNNER_TEMP}/psmux-source-provenance"
test -s "${RUNNER_TEMP}/psmux-license"

- name: Pack
run: dotnet pack LibTmux.slnx --configuration Release --no-build --output artifacts/packages
psmux:
name: psmux native Windows and WSL
needs: [validate, psmux-metadata]
runs-on: [self-hosted, Windows, X64, psmux]
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Inspect the packages
# Metadata, assemblies, dependencies and symbols, per package. What
# this catches is only fixable before the version exists.
run: uv run python eng/parity/inspect_packages.py
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json

- name: Prove the package installs and runs
# Reaching the library through the built package rather than a project
# reference is the last thing that can fail while it is still private.
- name: Download the audited psmux artifact
shell: pwsh
env:
ARTIFACT_URL: ${{ vars.PSMUX_ARTIFACT_URL }}
run: |
$ErrorActionPreference = 'Stop'
Invoke-WebRequest `
-Uri $env:ARTIFACT_URL `
-OutFile (Join-Path $env:RUNNER_TEMP 'psmux.exe')

- name: Build the native and packed consumers
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$env:NUGET_PACKAGES = Join-Path `
$env:RUNNER_TEMP `
"libtmux-psmux-nuget-$([Guid]::NewGuid().ToString('N'))"
dotnet restore LibTmux.slnx --locked-mode
dotnet build LibTmux.slnx `
--configuration Release `
--no-restore `
--warnaserror
dotnet pack LibTmux.slnx `
--configuration Release `
--no-build `
--output artifacts/packages
dotnet restore tests/LibTmux.PackageConsumer/LibTmux.PackageConsumer.csproj
for framework in net8.0 net10.0; do
dotnet run \
--project tests/LibTmux.PackageConsumer/LibTmux.PackageConsumer.csproj \
--configuration Release \
--framework "${framework}" \
--no-restore
done
foreach ($framework in @('net8.0', 'net10.0')) {
dotnet build tests/LibTmux.PackageConsumer/LibTmux.PackageConsumer.csproj `
--configuration Release `
--framework $framework `
--no-restore `
--warnaserror
}

- name: Run the native and WSL psmux harness
shell: pwsh
env:
WSL_DISTRIBUTION: ${{ vars.PSMUX_WSL_DISTRIBUTION }}
WSL_DOTNET_PATH: ${{ vars.PSMUX_WSL_DOTNET_PATH }}
run: |
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($env:WSL_DISTRIBUTION)) {
throw 'PSMUX_WSL_DISTRIBUTION must name the release runner distribution.'
}
if ([string]::IsNullOrWhiteSpace($env:WSL_DOTNET_PATH)) {
throw 'PSMUX_WSL_DOTNET_PATH must name the WSL dotnet executable.'
}
$dotnet = (Get-Command dotnet.exe -ErrorAction Stop).Source
foreach ($framework in @('net8.0', 'net10.0')) {
$nonce = [Guid]::NewGuid().ToString('N')
& .\eng\psmux\Invoke-PsmuxSmoke.ps1 `
-PsmuxPath (Join-Path $env:RUNNER_TEMP 'psmux.exe') `
-ExpectedSha256 '1abd0eaa3de1ed5491a4f744c8b3db492ae9ac94e9e9a8fea9da217c744ba94e' `
-DataDirectory (Join-Path $env:RUNNER_TEMP "libtmux-psmux-$nonce") `
-NamespaceName "libtmux_smoke_$($nonce.Substring(0, 16))" `
-DotnetPath $dotnet `
-TestAssembly (Join-Path $env:GITHUB_WORKSPACE "tests\LibTmux.UnitTests\bin\Release\$framework\LibTmux.UnitTests.dll") `
-ExampleAssembly (Join-Path $env:GITHUB_WORKSPACE "examples\LibTmux.Examples\bin\Release\$framework\LibTmux.Examples.dll") `
-PackageConsumerAssembly (Join-Path $env:GITHUB_WORKSPACE "tests\LibTmux.PackageConsumer\bin\Release\$framework\LibTmux.PackageConsumer.dll") `
-TargetFramework $framework `
-RunWslSmoke `
-WslDistribution $env:WSL_DISTRIBUTION `
-WslRepository $env:GITHUB_WORKSPACE `
-WslDotnetPath $env:WSL_DOTNET_PATH
}

publish:
name: publish to nuget.org
needs: [dotnet, compatibility, psmux]
runs-on: ubuntu-latest

# The policy on nuget.org names this environment, so a workflow that ran
# outside it cannot exchange a token. It is also where a required reviewer
# and a tag restriction belong.
environment: nuget

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
permissions:
contents: read
# Lets the job ask GitHub for the short-lived token nuget.org trades for
# a one-hour API key. Nothing else here needs a credential.
id-token: write
attestations: write

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# SourceLink points a debugger at the commit that produced the
# assembly, which a shallow checkout does not have.
fetch-depth: 0

- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json

- uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1

- name: Download the packages proved by the gate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: packages
path: artifacts/packages
if-no-files-found: error

- name: Inspect the packages
run: uv run python eng/parity/inspect_packages.py

- name: NuGet login
# The key this returns lasts an hour, so it is asked for immediately
Expand Down Expand Up @@ -145,12 +249,9 @@ jobs:
subject-path: 'artifacts/packages/*.nupkg'

- name: Push
# The matching .snupkg is pushed alongside each package automatically,
# which is what puts the symbols on the NuGet symbol server.
env:
NUGET_API_KEY: ${{ steps.login.outputs.NUGET_API_KEY }}
run: >
dotnet nuget push "artifacts/packages/*.nupkg"
--api-key "${NUGET_API_KEY}"
--source https://api.nuget.org/v3/index.json
--skip-duplicate
Loading
Loading