Skip to content

Establish and enforce a formal C# coding style guide #944

Establish and enforce a formal C# coding style guide

Establish and enforce a formal C# coding style guide #944

Workflow file for this run

name: CI
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Restore
run: dotnet restore
- name: Determine version
id: version
shell: bash
run: |
# The git tag is the source of truth for the released version. On a
# tag build (refs/tags/v*) derive the version from the tag name; on
# branch/PR builds fall back to the dev placeholder in props.
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [[ "${GITHUB_REF_NAME}" == v* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
VERSION="$(grep -oE '<Version>[^<]+' src/Directory.Build.props | sed 's/<Version>//')"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build
run: dotnet build --no-restore --configuration Release -p:Version=${{ steps.version.outputs.version }}
- name: Test
run: dotnet test --no-build --configuration Release --logger trx --results-directory TestResults
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: TestResults/*.trx
- name: Upload visual regression diffs
if: failure()
uses: actions/upload-artifact@v4
with:
name: rendering-snapshots
path: |
tests/EncDotNet.S100.VisualRegression.Tests/Snapshots/**/*.received.png
tests/EncDotNet.S100.VisualRegression.Tests/Snapshots/**/*.diff.png
if-no-files-found: ignore
- name: Pack NuGet packages
env:
VERSION: ${{ steps.version.outputs.version }}
# Pack every packable project in the solution. Projects opt out via
# <IsPackable>false</IsPackable> (hosts, tooling, tests), so new
# Datasets.SXXX libraries are published automatically with no edit here.
run: dotnet pack EncDotNet.S100.slnx --no-build --configuration Release --output nupkgs -p:Version=$VERSION
- name: Upload NuGet packages
uses: actions/upload-artifact@v4
with:
name: nupkgs
path: nupkgs/*.nupkg
format:
name: Format check
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Restore
run: dotnet restore
- name: Verify formatting (whitespace)
# Enforces the whitespace / formatting rules encoded in .editorconfig
# (see docs/coding-style.md). Fix locally with:
# dotnet format whitespace EncDotNet.S100.slnx
run: dotnet format whitespace EncDotNet.S100.slnx --no-restore --verify-no-changes
- name: Verify usings (ordering + unused)
# Enforces the using-directive rules from .editorconfig: System-first
# ordering, placement outside the file-scoped namespace, and removal of
# unnecessary directives (IDE0005). Fix locally with:
# dotnet format style EncDotNet.S100.slnx --diagnostics IDE0005
#
# Scope note: this is intentionally scoped to imports only. Broader
# style and naming rules (e.g. IDE1006) are NOT gated because dotnet
# format cannot Fix-All naming, and its solution-wide style fix-all is
# unreliable on the multi-targeted (net8.0;net10.0) projects. Imports
# are safe to gate: IDE0005 is identical across both target frameworks
# here, and the scoped verify does not false-positive on the handful of
# multi-target files that the full fix-all mis-handles.
run: dotnet format style EncDotNet.S100.slnx --diagnostics IDE0005 --severity warn --no-restore --verify-no-changes
test-arm64:
strategy:
matrix:
include:
- os: windows-11-arm
name: win-arm64
- os: ubuntu-22.04-arm
name: linux-arm64
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --configuration Release --logger trx --results-directory TestResults
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.name }}
path: TestResults/*.trx
- name: Upload visual regression diffs
if: failure()
uses: actions/upload-artifact@v4
with:
name: rendering-snapshots-${{ matrix.name }}
path: |
tests/EncDotNet.S100.VisualRegression.Tests/Snapshots/**/*.received.png
tests/EncDotNet.S100.VisualRegression.Tests/Snapshots/**/*.diff.png
if-no-files-found: ignore
publish:
needs: build
strategy:
matrix:
include:
- os: macos-latest
rid: osx-arm64
artifact: Viewer-osx-arm64
- os: windows-latest
rid: win-x64
artifact: Viewer-win-x64
# win-arm64 is cross-published on the x64 runner (not windows-11-arm)
# because azure/trusted-signing-action does not support ARM64 runners
# — its x64 signing dlib fails under emulation with "SignTool failed
# with exit code 3" (Azure/trusted-signing-action#92). signtool on x64
# signs arm64 PE binaries fine, and a RID-targeted dotnet publish
# produces identical win-arm64 output regardless of host arch. Native
# arm64 build/test coverage is retained by the separate test-arm64 job.
- os: windows-latest
rid: win-arm64
artifact: Viewer-win-arm64
- os: ubuntu-latest
rid: linux-x64
artifact: Viewer-linux-x64
- os: ubuntu-22.04-arm
rid: linux-arm64
artifact: Viewer-linux-arm64
runs-on: ${{ matrix.os }}
permissions:
contents: read
env:
# macOS signing/notarization is expensive (notary upload + polling) and is
# not needed for PR validation — developers can clear the quarantine
# attribute on unsigned PR artifacts if they need to run them. Gate it to
# main CI runs (pushes to main and v* tags), i.e. skip on pull_request.
MACOS_SIGNING_ENABLED: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12 != '' && github.event_name != 'pull_request' }}
# Windows Authenticode signing via Azure Trusted Signing. Gated the same
# way as macOS: only when the signing service principal secret is present
# and the run is not a pull_request, so PRs produce unsigned win-* zips.
WINDOWS_SIGNING_ENABLED: ${{ secrets.AZURE_CLIENT_ID != '' && github.event_name != 'pull_request' }}
steps:
- uses: actions/checkout@v5
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Determine version
id: version
shell: bash
run: |
# The git tag is the source of truth for the released version. On a
# tag build (refs/tags/v*) derive the version from the tag name; on
# branch/PR builds fall back to the dev placeholder in props.
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [[ "${GITHUB_REF_NAME}" == v* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
VERSION="$(grep -oE '<Version>[^<]+' src/Directory.Build.props | sed 's/<Version>//')"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Publish Viewer
run: >
dotnet publish src/EncDotNet.S100.Viewer/EncDotNet.S100.Viewer.csproj
--configuration Release
--runtime ${{ matrix.rid }}
-p:Version=${{ steps.version.outputs.version }}
${{ matrix.rid == 'osx-arm64' && '-p:PublishSingleFile=false' || '' }}
- name: Publish s100 CLI
# Publish the `s100` command-line tool self-contained into a `cli/`
# subfolder of the viewer's publish output. This single location flows
# into every platform archive and, for macOS, into the .app bundle's
# Contents/MacOS/cli/ where the recursive signing loop below signs it.
# Must run after "Publish Viewer" because dotnet publish cleans its
# own output directory; the cli/ subfolder is unaffected by that.
run: >
dotnet publish tools/EncDotNet.S100.Cli/EncDotNet.S100.Cli.csproj
--configuration Release
--runtime ${{ matrix.rid }}
--self-contained true
-p:Version=${{ steps.version.outputs.version }}
--output src/EncDotNet.S100.Viewer/bin/Release/net10.0/${{ matrix.rid }}/publish/cli
- name: Disable NuGet source mapping for signing
# azure/trusted-signing-action installs its tooling (the `sign` CLI,
# Microsoft.Trusted.Signing.Client, SDK build tools) with
# `dotnet tool install --add-source`, which NuGet refuses to combine
# with package source mapping (our repo nuget.config maps all packages
# to nuget.org). Every project restore is already complete by this
# point, so move the config aside for the signing tool install; the
# "Restore NuGet config" step below puts it back.
if: startsWith(matrix.rid, 'win-') && env.WINDOWS_SIGNING_ENABLED == 'true'
shell: pwsh
run: Move-Item nuget.config nuget.config.bak
- name: Sign Windows binaries (Azure Trusted Signing)
# Authenticode-sign the published Windows executables before they are
# archived, so both the viewer zip and the standalone s100 CLI zip
# contain signed binaries. Signing the viewer publish folder recursively
# also covers the bundled cli/s100.exe under it. Account-specific (but
# non-secret) endpoint/account/profile values come from repo variables;
# the service principal credentials come from secrets. Must run before
# the "Archive ..." steps below.
if: startsWith(matrix.rid, 'win-') && env.WINDOWS_SIGNING_ENABLED == 'true'
uses: azure/trusted-signing-action@v0
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }}
trusted-signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_SIGNING_PROFILE_NAME }}
files-folder: ${{ github.workspace }}\src\EncDotNet.S100.Viewer\bin\Release\net10.0\${{ matrix.rid }}\publish
files-folder-filter: exe
files-folder-recurse: true
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Restore NuGet config
# Counterpart to "Disable NuGet source mapping for signing". Runs even
# if signing failed so the workspace is left in a consistent state.
if: always() && startsWith(matrix.rid, 'win-') && env.WINDOWS_SIGNING_ENABLED == 'true'
shell: pwsh
run: if (Test-Path nuget.config.bak) { Move-Item -Force nuget.config.bak nuget.config }
- name: Verify Windows signatures
if: startsWith(matrix.rid, 'win-') && env.WINDOWS_SIGNING_ENABLED == 'true'
shell: pwsh
run: |
$publish = "src/EncDotNet.S100.Viewer/bin/Release/net10.0/${{ matrix.rid }}/publish"
$targets = @(
"$publish/EncDotNet.S100.Viewer.exe",
"$publish/cli/s100.exe"
)
foreach ($exe in $targets) {
$sig = Get-AuthenticodeSignature $exe
Write-Host "$exe => $($sig.Status)"
if ($sig.Status -ne 'Valid') {
Write-Error "Signature for $exe is $($sig.Status), expected Valid."
exit 1
}
}
- name: Archive standalone s100 CLI (Linux)
# Package the self-contained `s100` CLI as its own per-RID release
# asset so users can download just the command-line tool without the
# viewer. The osx-arm64 archive is produced later, after the CLI is
# code-signed and notarized.
if: startsWith(matrix.rid, 'linux-')
shell: bash
run: >
tar -czf "s100-${{ steps.version.outputs.version }}-${{ matrix.rid }}.tar.gz"
-C "src/EncDotNet.S100.Viewer/bin/Release/net10.0/${{ matrix.rid }}/publish/cli" .
- name: Archive standalone s100 CLI (Windows)
if: startsWith(matrix.rid, 'win-')
shell: pwsh
run: |
$cli = "src/EncDotNet.S100.Viewer/bin/Release/net10.0/${{ matrix.rid }}/publish/cli"
Compress-Archive -Path "$cli/*" -DestinationPath "s100-${{ steps.version.outputs.version }}-${{ matrix.rid }}.zip"
- name: Create macOS .app bundle
if: matrix.rid == 'osx-arm64'
run: |
PUBLISH_DIR="src/EncDotNet.S100.Viewer/bin/Release/net10.0/osx-arm64/publish"
APP="EncDotNet.S100.Viewer.app"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp src/EncDotNet.S100.Viewer/Info.plist "$APP/Contents/"
# AppIcon.icns is referenced from Info.plist via CFBundleIconFile.
cp src/EncDotNet.S100.Viewer/Branding/AppIcon.icns "$APP/Contents/Resources/"
cp -a "$PUBLISH_DIR"/. "$APP/Contents/MacOS/"
# Remove debug symbols; they are not needed in the release bundle
# and codesign treats them as unsigned code objects.
find "$APP" -name '*.pdb' -delete
- name: Import signing certificate
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
P12_BASE64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12 }}
P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }}
run: |
echo "$P12_BASE64" | base64 --decode > cert.p12
security create-keychain -p "" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "" build.keychain
security import cert.p12 -k build.keychain -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
- name: Sign macOS .app bundle
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
APP="EncDotNet.S100.Viewer.app"
ENTITLEMENTS="src/EncDotNet.S100.Viewer/entitlements.plist"
MAIN_EXE="EncDotNet.S100.Viewer"
# Sign all subcomponents individually before sealing the bundle.
# Exclude the main executable — it will be signed when we sign
# the bundle itself, which also validates all subcomponents.
find "$APP/Contents/MacOS" -type f ! -name "$MAIN_EXE" | while read -r f; do
if file "$f" | grep -q "Mach-O"; then
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" "$f"
else
codesign --force --timestamp \
--sign "$APPLE_SIGNING_IDENTITY" "$f"
fi
done
# Sign the bundle (signs main executable and seals everything)
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" \
"$APP"
# Verify the seal recursively. This fails the build before
# notarization if any nested Mach-O (e.g. the bundled s100 CLI
# or its native dylibs) was left unsigned.
codesign --verify --deep --strict --verbose=2 "$APP"
codesign --verify --strict --verbose=2 "$APP/Contents/MacOS/cli/s100"
- name: Notarize macOS .app bundle
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
ditto -c -k --keepParent \
EncDotNet.S100.Viewer.app \
Viewer.zip
xcrun notarytool submit Viewer.zip \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait \
--output-format json | tee notarization-result.json
STATUS=$(python3 -c "import json,sys; print(json.load(sys.stdin)['status'])" < notarization-result.json)
ID=$(python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" < notarization-result.json)
if [ "$STATUS" != "Accepted" ]; then
echo "::error::Notarization failed with status: $STATUS"
xcrun notarytool log "$ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD"
exit 1
fi
- name: Staple notarization ticket
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
continue-on-error: true
run: |
# The notarization ticket may not be immediately available in
# CloudKit. Stapling is optional — Gatekeeper will verify the
# notarization online on first launch if the ticket is absent.
for i in 1 2 3 4 5; do
if xcrun stapler staple EncDotNet.S100.Viewer.app; then
exit 0
fi
echo "Staple attempt $i failed, waiting 30s..."
sleep 30
done
echo "::warning::Stapling failed after 5 attempts. The app is still notarized; Gatekeeper will verify online."
exit 1
- name: Sign standalone macOS s100 CLI
# The bundled CLI inside the .app is signed by the recursive loop above,
# but the standalone copy at .../publish/cli is a separate set of files
# and must be signed independently for its own release archive.
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
CLI="src/EncDotNet.S100.Viewer/bin/Release/net10.0/osx-arm64/publish/cli"
ENTITLEMENTS="src/EncDotNet.S100.Viewer/entitlements.plist"
MAIN_EXE="s100"
# Remove debug symbols; codesign treats .pdb as unsigned code objects.
find "$CLI" -name '*.pdb' -delete
# Sign every nested Mach-O (native dylibs such as libSkiaSharp.dylib)
# before the host executable.
find "$CLI" -type f ! -name "$MAIN_EXE" | while read -r f; do
if file "$f" | grep -q "Mach-O"; then
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" "$f"
fi
done
# Sign the host executable last with hardened runtime + entitlements.
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--sign "$APPLE_SIGNING_IDENTITY" "$CLI/s100"
codesign --verify --strict --verbose=2 "$CLI/s100"
- name: Notarize standalone macOS s100 CLI
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
CLI="src/EncDotNet.S100.Viewer/bin/Release/net10.0/osx-arm64/publish/cli"
# notarytool accepts .zip/.dmg/.pkg but not .tar.gz, so submit a
# throwaway .zip. The ticket is registered server-side against each
# binary's cdhash, so the distributed .tar.gz (identical signed
# binaries) passes Gatekeeper's online check on first run. A bare CLI
# cannot be stapled, so we rely on that online verification.
ditto -c -k --keepParent "$CLI" s100-cli-notarize.zip
xcrun notarytool submit s100-cli-notarize.zip \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait \
--output-format json | tee cli-notarization-result.json
STATUS=$(python3 -c "import json,sys; print(json.load(sys.stdin)['status'])" < cli-notarization-result.json)
ID=$(python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" < cli-notarization-result.json)
if [ "$STATUS" != "Accepted" ]; then
echo "::error::CLI notarization failed with status: $STATUS"
xcrun notarytool log "$ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD"
exit 1
fi
- name: Archive standalone macOS s100 CLI
if: matrix.rid == 'osx-arm64'
run: >
tar -czf "s100-${{ steps.version.outputs.version }}-osx-arm64.tar.gz"
-C "src/EncDotNet.S100.Viewer/bin/Release/net10.0/osx-arm64/publish/cli" .
- name: Upload standalone s100 CLI
uses: actions/upload-artifact@v4
with:
name: s100-cli-${{ matrix.rid }}
path: s100-*-${{ matrix.rid }}.*
- name: Build macOS DMG
if: matrix.rid == 'osx-arm64'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
# Build a styled DMG with the branding background image, icon
# positions and Applications drop link. We delegate to
# `create-dmg` (https://github.com/create-dmg/create-dmg)
# because it bundles a battle-tested workaround for the
# `Finder got an error: Can't get disk (-1728)` failure that
# bare `osascript` hits on GitHub-hosted macOS runners.
# Geometry must match src/EncDotNet.S100.Viewer/Branding/dmg-background.svg.
DMG_NAME="EncDotNet.S100.Viewer-${VERSION}.dmg"
STAGING="$(mktemp -d)/dmg"
BG="src/EncDotNet.S100.Viewer/Branding/dmg-background.png"
VOLICON="src/EncDotNet.S100.Viewer/Branding/AppIcon.icns"
VOLNAME="S-100 Viewer"
mkdir -p "$STAGING"
cp -R EncDotNet.S100.Viewer.app "$STAGING/"
brew install create-dmg
create-dmg \
--volname "$VOLNAME" \
--volicon "$VOLICON" \
--background "$BG" \
--window-pos 200 120 \
--window-size 540 380 \
--icon-size 96 \
--icon "EncDotNet.S100.Viewer.app" 140 200 \
--app-drop-link 400 200 \
--hdiutil-quiet \
"$DMG_NAME" \
"$STAGING"
echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV"
- name: Sign macOS DMG
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
run: |
codesign --force --sign "$APPLE_SIGNING_IDENTITY" --timestamp "$DMG_NAME"
- name: Notarize macOS DMG
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
xcrun notarytool submit "$DMG_NAME" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD" \
--wait \
--output-format json | tee dmg-notarization-result.json
STATUS=$(python3 -c "import json,sys; print(json.load(sys.stdin)['status'])" < dmg-notarization-result.json)
ID=$(python3 -c "import json,sys; print(json.load(sys.stdin)['id'])" < dmg-notarization-result.json)
if [ "$STATUS" != "Accepted" ]; then
echo "::error::DMG notarization failed with status: $STATUS"
xcrun notarytool log "$ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APPLE_APP_PASSWORD"
exit 1
fi
- name: Staple notarization ticket to DMG
if: matrix.rid == 'osx-arm64' && env.MACOS_SIGNING_ENABLED == 'true'
continue-on-error: true
run: |
# Same retry shape as the .app stapler step: the notarization
# ticket may not yet be available in CloudKit. The DMG is
# still notarized — Gatekeeper will verify online if the
# ticket is absent.
for i in 1 2 3 4 5; do
if xcrun stapler staple "$DMG_NAME"; then
exit 0
fi
echo "DMG staple attempt $i failed, waiting 30s..."
sleep 30
done
echo "::warning::DMG stapling failed after 5 attempts. The DMG is still notarized; Gatekeeper will verify online."
exit 1
- name: Archive macOS .app bundle
if: matrix.rid == 'osx-arm64'
run: tar -czf "${{ matrix.artifact }}.tar.gz" EncDotNet.S100.Viewer.app
- name: Archive published app (Linux)
if: startsWith(matrix.rid, 'linux-')
shell: bash
run: tar -czf "${{ matrix.artifact }}.tar.gz" -C "src/EncDotNet.S100.Viewer/bin/Release/net10.0/${{ matrix.rid }}/publish" .
- name: Archive published app (Windows)
if: startsWith(matrix.rid, 'win-')
shell: pwsh
run: Compress-Archive -Path "src/EncDotNet.S100.Viewer/bin/Release/net10.0/${{ matrix.rid }}/publish/*" -DestinationPath "${{ matrix.artifact }}.zip"
- name: Upload published app
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}.*
- name: Upload macOS DMG
if: matrix.rid == 'osx-arm64'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-dmg
path: ${{ env.DMG_NAME }}
publish-nuget:
if: startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Download NuGet packages
uses: actions/download-artifact@v4
with:
name: nupkgs
path: nupkgs
- name: NuGet login
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Push to NuGet.org
run: dotnet nuget push "nupkgs/*.nupkg" --api-key ${{steps.login.outputs.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json
create-release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [build, publish]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: |
artifacts/nupkgs/*.nupkg
artifacts/Viewer-*/*.tar.gz
artifacts/Viewer-*/*.zip
artifacts/s100-cli-*/*.tar.gz
artifacts/s100-cli-*/*.zip
artifacts/Viewer-*-dmg/*.dmg