From b613331f8f7cd7bce9c4a74a0d606a269033553c Mon Sep 17 00:00:00 2001 From: DarkPro1337 Date: Tue, 28 Jul 2026 03:16:30 +0400 Subject: [PATCH] Improve CI and release validation - run CI checks for pull requests from dev to main - verify formatting, build with warnings as errors, and run tests - trigger release builds when a GitHub Release is published - preserve manual release workflow dispatch for build verification - package the compatible x64 FFmpeg backend for Windows ARM64 - add a transparent VP9 WebM encoding smoke test - verify the packaged codec and alpha metadata - update release packaging and third-party documentation - fix FfmpegEncoder formatting --- .github/workflows/ci.yml | 39 +++++++ .github/workflows/release.yml | 107 ++++++++++++++++--- README.md | 15 ++- THIRD_PARTY_NOTICES.md | 10 +- src/Telemorph.Core/Pipeline/FfmpegEncoder.cs | 2 +- 5 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3bf8430 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: Pull Request CI + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: pr-ci-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + name: Validate dev to main + if: github.event.pull_request.head.ref == 'dev' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Restore + run: dotnet restore src/Telemorph.slnx + + - name: Verify formatting + run: dotnet format src/Telemorph.slnx --verify-no-changes --no-restore + + - name: Build + run: dotnet build src/Telemorph.slnx -c Release --no-restore -warnaserror + + - name: Test + run: dotnet test src/Telemorph.slnx -c Release --no-build --no-restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a92f2f..d7f5492 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,8 @@ name: Build and Release on: - push: - branches: [main] - tags: - - "v*" + release: + types: [published] workflow_dispatch: permissions: @@ -17,6 +15,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.ref }} - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -53,6 +53,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.ref }} - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -93,22 +95,24 @@ jobs: Copy-Item -LiteralPath $ffprobe -Destination "$toolDir/ffprobe.exe" Copy-Item LICENSE, THIRD_PARTY_NOTICES.md -Destination "publish/${{ matrix.rid }}" - - name: Bundle pinned FFmpeg (Windows ARM64) + - name: Bundle compatible FFmpeg (Windows ARM64) if: matrix.rid == 'win-arm64' shell: pwsh run: | - $url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2025-10-31-13-40/ffmpeg-n8.0-30-g71007e6c12-winarm64-gpl-8.0.zip" - $expectedSha256 = "5276af84b736b279a70514c5b7e236f03961ab5517d911a88371838459b94c40" - Invoke-WebRequest -Uri $url -OutFile ffmpeg-win-arm64.zip - $actualSha256 = (Get-FileHash ffmpeg-win-arm64.zip -Algorithm SHA256).Hash.ToLowerInvariant() + # The native BtbN Windows ARM64 build does not include libvpx-vp9. + # Windows 11 ARM can run this verified x64 media backend through emulation. + $url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2025-10-31-13-40/ffmpeg-n8.0-30-g71007e6c12-win64-gpl-8.0.zip" + $expectedSha256 = "05ecc01bb03ef1f4d908c3c982512f07f888848429be6e9662a5a7c558c60b4f" + Invoke-WebRequest -Uri $url -OutFile ffmpeg-win-x64.zip + $actualSha256 = (Get-FileHash ffmpeg-win-x64.zip -Algorithm SHA256).Hash.ToLowerInvariant() if ($actualSha256 -ne $expectedSha256) { throw "FFmpeg archive checksum mismatch. Expected $expectedSha256, got $actualSha256." } - Expand-Archive ffmpeg-win-arm64.zip -DestinationPath ffmpeg-win-arm64 + Expand-Archive ffmpeg-win-x64.zip -DestinationPath ffmpeg-win-x64 $toolDir = "publish/${{ matrix.rid }}/tools/${{ matrix.rid }}" New-Item -ItemType Directory -Force -Path $toolDir | Out-Null - $ffmpeg = Get-ChildItem ffmpeg-win-arm64 -Recurse -Filter ffmpeg.exe | Select-Object -First 1 - $ffprobe = Get-ChildItem ffmpeg-win-arm64 -Recurse -Filter ffprobe.exe | Select-Object -First 1 + $ffmpeg = Get-ChildItem ffmpeg-win-x64 -Recurse -Filter ffmpeg.exe | Select-Object -First 1 + $ffprobe = Get-ChildItem ffmpeg-win-x64 -Recurse -Filter ffprobe.exe | Select-Object -First 1 if (-not $ffmpeg -or -not $ffprobe) { throw "The verified FFmpeg archive does not contain ffmpeg.exe and ffprobe.exe." } @@ -132,8 +136,77 @@ jobs: - name: Verify packaged toolchain shell: pwsh run: | + $publishDir = "publish/${{ matrix.rid }}" $executable = if ($IsWindows) { "telemorph.exe" } else { "telemorph" } - & "publish/${{ matrix.rid }}/$executable" --doctor + $toolSuffix = if ($IsWindows) { ".exe" } else { "" } + $app = Join-Path $publishDir $executable + $toolDir = Join-Path $publishDir "tools/${{ matrix.rid }}" + $ffmpeg = Join-Path $toolDir "ffmpeg$toolSuffix" + $ffprobe = Join-Path $toolDir "ffprobe$toolSuffix" + + & $app --doctor + if ($LASTEXITCODE -ne 0) { + throw "Packaged toolchain diagnostics failed with exit code $LASTEXITCODE." + } + + $smokeDir = Join-Path $env:RUNNER_TEMP "telemorph-smoke-${{ matrix.rid }}" + New-Item -ItemType Directory -Force -Path $smokeDir | Out-Null + $smokeInput = Join-Path $smokeDir "transparent.apng" + $smokeOutput = Join-Path $smokeDir "output.webm" + + & $ffmpeg ` + -hide_banner -loglevel error -y ` + -f lavfi ` + -i "nullsrc=size=64x64:rate=10:duration=0.5,format=rgba,colorchannelmixer=aa=0.5" ` + -frames:v 5 -plays 0 ` + $smokeInput + if ($LASTEXITCODE -ne 0) { + throw "Failed to create the transparent APNG smoke-test input." + } + + $inputProbeOutput = & $ffprobe ` + -v error ` + -select_streams v:0 ` + -show_entries "stream=pix_fmt" ` + -of json ` + $smokeInput + if ($LASTEXITCODE -ne 0) { + throw "ffprobe failed to inspect the smoke-test input." + } + + $inputPixelFormat = ($inputProbeOutput | ConvertFrom-Json).streams[0].pix_fmt + if ($inputPixelFormat -notmatch "a") { + throw "Smoke-test input pixel format '$inputPixelFormat' has no alpha channel." + } + + & $app ` + $smokeInput ` + --emoji ` + --duration 1 ` + --no-optimize ` + --crf 45 ` + --output $smokeOutput + if ($LASTEXITCODE -ne 0) { + throw "Packaged Telemorph smoke conversion failed with exit code $LASTEXITCODE." + } + + $probeOutput = & $ffprobe ` + -v error ` + -select_streams v:0 ` + -show_entries "stream=codec_name:stream_tags=alpha_mode" ` + -of json ` + $smokeOutput + if ($LASTEXITCODE -ne 0) { + throw "ffprobe failed to inspect the smoke-test output." + } + + $stream = ($probeOutput | ConvertFrom-Json).streams[0] + if ($stream.codec_name -ne "vp9") { + throw "Smoke-test output codec is '$($stream.codec_name)', expected 'vp9'." + } + if ($stream.tags.ALPHA_MODE -ne "1") { + throw "Smoke-test output does not advertise WebM alpha metadata." + } - name: Create archive (Windows) if: runner.os == 'Windows' @@ -153,9 +226,9 @@ jobs: if-no-files-found: error release: - name: Create Release + name: Attach release assets needs: build - if: startsWith(github.ref, 'refs/tags/v') + if: github.event_name == 'release' runs-on: ubuntu-latest permissions: contents: write @@ -165,8 +238,8 @@ jobs: with: path: ./artifacts - - name: Create release + - name: Upload assets to release uses: softprops/action-gh-release@v2 with: + tag_name: ${{ github.event.release.tag_name }} files: ./artifacts/**/* - generate_release_notes: true diff --git a/README.md b/README.md index 6ceee0d..90c7119 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,12 @@ metadata and validates every encoded candidate. ## Release packaging -The release workflow publishes self-contained builds for: +Pull requests from `dev` to `main` run formatting checks, a warnings-as-errors +build, and the full test suite. Pushing or merging a commit into `main` does not +create release binaries. + +Publishing a GitHub Release starts the release workflow from that release's tag +and produces self-contained builds for: - `win-x64` - `win-arm64` @@ -134,9 +139,11 @@ The release workflow publishes self-contained builds for: - `osx-arm64` FFmpeg and ffprobe are restored from exact npm dependency versions using the -committed lockfile. Windows ARM64 uses a pinned native BtbN archive with an -explicit SHA-256 check. The tools are copied under `tools//` and verified -by running `telemorph --doctor` before an archive is uploaded. +committed lockfile. The Windows ARM64 release uses a pinned BtbN x64 media +backend through Windows 11 ARM emulation because the native build lacks +`libvpx-vp9`; its archive has an explicit SHA-256 check. The tools are copied +under `tools//`, checked with `telemorph --doctor`, and exercised by a +transparent APNG-to-VP9 WebM smoke test before an archive is uploaded. See [third-party notices](THIRD_PARTY_NOTICES.md) for FFmpeg licensing and source information. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 58f9674..a3e9f8a 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -8,10 +8,12 @@ The packaged executables are obtained through the pinned `ffmpeg-static` 5.3.0 and `@derhuerst/ffprobe-static` 5.3.0 npm packages. The exact npm package contents are verified by the committed `eng/ffmpeg/package-lock.json`. -The Windows ARM64 archive uses the native BtbN FFmpeg build -`ffmpeg-n8.0-30-g71007e6c12-winarm64-gpl-8.0.zip`, pinned to its immutable -release URL and verified against SHA-256 -`5276af84b736b279a70514c5b7e236f03961ab5517d911a88371838459b94c40`. +The Windows ARM64 Telemorph archive uses the BtbN x64 FFmpeg build +`ffmpeg-n8.0-30-g71007e6c12-win64-gpl-8.0.zip` because the corresponding native +Windows ARM64 build does not contain the required `libvpx-vp9` encoder. Windows +11 ARM runs this media backend through x64 emulation. The archive is pinned to +its immutable release URL and verified against SHA-256 +`05ecc01bb03ef1f4d908c3c982512f07f888848429be6e9662a5a7c558c60b4f`. FFmpeg is licensed separately from Telemorph. The packaged builds can include GPL-licensed components. Copyright, license, corresponding-source, and build diff --git a/src/Telemorph.Core/Pipeline/FfmpegEncoder.cs b/src/Telemorph.Core/Pipeline/FfmpegEncoder.cs index 19c0267..bf5c1dc 100644 --- a/src/Telemorph.Core/Pipeline/FfmpegEncoder.cs +++ b/src/Telemorph.Core/Pipeline/FfmpegEncoder.cs @@ -86,7 +86,7 @@ private static void ReportFfmpegProgress( const string prefix = "out_time_us="; if (!line.StartsWith(prefix, StringComparison.Ordinal) || - !long.TryParse(line.AsSpan(prefix.Length),NumberStyles.Integer, CultureInfo.InvariantCulture, out var microseconds)) + !long.TryParse(line.AsSpan(prefix.Length), NumberStyles.Integer, CultureInfo.InvariantCulture, out var microseconds)) return; var duration = Math.Max(plan.Profile.MaxDurationSeconds, 0.001);