Skip to content
Closed
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
207 changes: 207 additions & 0 deletions .github/workflows/packaging-closure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,27 @@ on:
- 'Brainarr.Plugin/**'
- 'plugin.json'
- 'manifest.json'
workflow_dispatch:
inputs:
common_version:
description: 'Common version for canonical Abstractions (leave empty to use plugin.json)'
required: false
default: ''

permissions:
contents: read

concurrency:
group: packaging-closure-${{ github.ref }}
cancel-in-progress: true

jobs:
verify-package-closure:
name: Verify Package Closure
runs-on: ubuntu-latest
timeout-minutes: 15
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

steps:
- name: Checkout
Expand All @@ -41,13 +53,118 @@ jobs:
with:
dotnet-version: '6.0.x'

- name: Validate plugin manifest (ManifestCheck)
shell: pwsh
run: |
$scriptPath = 'ext/lidarr.plugin.common/tools/ManifestCheck.ps1'
if (-not (Test-Path -LiteralPath $scriptPath)) {
throw "ManifestCheck script not found at '$scriptPath'. Ensure the Common submodule is initialized."
}

& $scriptPath `
-ProjectPath 'Brainarr.Plugin/Brainarr.Plugin.csproj' `
-ManifestPath 'plugin.json' `
-Strict

- name: Extract Lidarr Assemblies
shell: bash
run: |
timeout 15m bash scripts/extract-lidarr-assemblies.sh --mode full --no-tar-fallback --output-dir ext/Lidarr-docker/_output/net8.0
echo "Extracted assemblies (sample):"
ls -1 ext/Lidarr-docker/_output/net8.0 2>/dev/null | head -20 || true

# ═══════════════════════════════════════════════════════════════════════════
# CANONICAL ABSTRACTIONS: Download byte-identical DLL from Common releases
# This eliminates "same code, different binary" drift across plugins
# ═══════════════════════════════════════════════════════════════════════════
- name: Determine Common version for canonical Abstractions
id: common-version
shell: pwsh
run: |
# Use workflow input if provided, otherwise extract from plugin.json commonVersion
$inputVersion = '${{ github.event.inputs.common_version }}'
if ($inputVersion) {
$version = $inputVersion
Write-Host "Using workflow input version: $version"
} else {
# Read from plugin.json commonVersion field
$manifest = Get-Content plugin.json | ConvertFrom-Json
if ($manifest.commonVersion) {
$version = $manifest.commonVersion
Write-Host "Using plugin.json commonVersion: $version"
} else {
Write-Host "::warning::No commonVersion in plugin.json, skipping canonical Abstractions"
echo "skip=true" >> $env:GITHUB_OUTPUT
exit 0
}
}
echo "version=$version" >> $env:GITHUB_OUTPUT
echo "skip=false" >> $env:GITHUB_OUTPUT

- name: Download canonical Abstractions DLL
if: steps.common-version.outputs.skip != 'true'
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Only allow graceful degrade on workflow_dispatch, not on PR/push
ALLOW_DEGRADE: ${{ github.event_name == 'workflow_dispatch' }}
run: |
$version = '${{ steps.common-version.outputs.version }}'
$tag = "v$version"
$repo = 'RicherTunes/Lidarr.Plugin.Common'
$outputDir = 'canonical-abstractions'
$allowDegrade = $env:ALLOW_DEGRADE -eq 'true'

Write-Host "Downloading canonical Abstractions from $repo $tag..."
if ($allowDegrade) {
Write-Host "(workflow_dispatch: graceful degrade allowed)"
} else {
Write-Host "(PR/push: canonical Abstractions required)"
}

New-Item -ItemType Directory -Path $outputDir -Force | Out-Null

# Download using gh CLI (handles auth and rate limits)
gh release download $tag `
--repo $repo `
--pattern "Lidarr.Plugin.Abstractions.dll" `
--pattern "Lidarr.Plugin.Abstractions.dll.sha256" `
--pattern "Lidarr.Plugin.Abstractions.pdb" `
--dir $outputDir `
--clobber

if ($LASTEXITCODE -ne 0) {
if ($allowDegrade) {
Write-Host "::warning::Failed to download canonical Abstractions from $tag"
Write-Host "::warning::Release may not exist yet. Using submodule-built Abstractions."
echo "CANONICAL_ABSTRACTIONS_AVAILABLE=false" >> $env:GITHUB_ENV
exit 0
} else {
Write-Host "::error::CANONICAL ABSTRACTIONS REQUIRED: Failed to download from $tag"
Write-Host "::error::Ensure Common $tag release exists with Abstractions assets."
Write-Host "::error::For local testing, use workflow_dispatch which allows graceful degrade."
exit 1
}
}

# Verify SHA256
Write-Host "Verifying SHA256..."
$sha256File = Join-Path $outputDir "Lidarr.Plugin.Abstractions.dll.sha256"
$dllFile = Join-Path $outputDir "Lidarr.Plugin.Abstractions.dll"

$expectedHash = (Get-Content $sha256File -Raw).Split()[0].Trim().ToLower()
$actualHash = (Get-FileHash -Path $dllFile -Algorithm SHA256).Hash.ToLower()

if ($expectedHash -ne $actualHash) {
Write-Error "SHA256 MISMATCH! Expected: $expectedHash, Actual: $actualHash"
exit 1
}

Write-Host "SHA256 verified: $expectedHash"
Write-Host "Canonical Abstractions downloaded successfully"
echo "CANONICAL_ABSTRACTIONS_AVAILABLE=true" >> $env:GITHUB_ENV
echo "CANONICAL_ABSTRACTIONS_HASH=$expectedHash" >> $env:GITHUB_ENV

- name: Build Plugin
shell: bash
run: |
Expand All @@ -63,6 +180,16 @@ jobs:
run: |
mkdir -p package
cp Brainarr.Plugin/bin/Lidarr.Plugin.Brainarr.dll package/

# Copy Common and Abstractions DLLs from build output
for dll in Lidarr.Plugin.Common.dll Lidarr.Plugin.Abstractions.dll; do
if [ -f "Brainarr.Plugin/bin/$dll" ]; then
cp "Brainarr.Plugin/bin/$dll" package/
elif [ -f "Brainarr.Plugin/bin/Release/net6.0/$dll" ]; then
cp "Brainarr.Plugin/bin/Release/net6.0/$dll" package/
fi
done

cp plugin.json package/
cp manifest.json package/
cp .lidarr.plugin package/
Expand All @@ -71,6 +198,59 @@ jobs:
zip -r ../Brainarr-closure-test.zip .
cd ..

# Replace submodule-built Abstractions with canonical version
- name: Replace Abstractions with canonical DLL
if: env.CANONICAL_ABSTRACTIONS_AVAILABLE == 'true'
shell: pwsh
run: |
$packageDir = 'package'
$canonicalDir = 'canonical-abstractions'

$targetDll = Join-Path $packageDir 'Lidarr.Plugin.Abstractions.dll'
$canonicalDll = Join-Path $canonicalDir 'Lidarr.Plugin.Abstractions.dll'
$canonicalPdb = Join-Path $canonicalDir 'Lidarr.Plugin.Abstractions.pdb'

if (-not (Test-Path $targetDll)) {
Write-Host "::warning::Abstractions DLL not found in package at $targetDll"
Write-Host "Package contents:"
Get-ChildItem $packageDir
exit 0
}

# Record original hash for comparison
$originalHash = (Get-FileHash -Path $targetDll -Algorithm SHA256).Hash.ToLower()
$canonicalHash = (Get-FileHash -Path $canonicalDll -Algorithm SHA256).Hash.ToLower()

if ($originalHash -eq $canonicalHash) {
Write-Host "Submodule Abstractions already matches canonical (no replacement needed)"
} else {
Write-Host "Replacing submodule Abstractions with canonical version..."
Write-Host " Original: $originalHash"
Write-Host " Canonical: $canonicalHash"

Copy-Item -Path $canonicalDll -Destination $targetDll -Force
if (Test-Path $canonicalPdb) {
$targetPdb = Join-Path $packageDir 'Lidarr.Plugin.Abstractions.pdb'
Copy-Item -Path $canonicalPdb -Destination $targetPdb -Force
}

# Verify replacement
$newHash = (Get-FileHash -Path $targetDll -Algorithm SHA256).Hash.ToLower()
if ($newHash -ne $canonicalHash) {
Write-Error "Replacement verification failed!"
exit 1
}

Write-Host "Replaced with canonical Abstractions DLL"
}

# Recreate zip with canonical Abstractions
Write-Host "Recreating package zip with canonical Abstractions..."
Remove-Item -Path 'Brainarr-closure-test.zip' -Force -ErrorAction SilentlyContinue
Push-Location $packageDir
zip -r ../Brainarr-closure-test.zip .
Pop-Location

- name: Verify Dependency Closure
shell: bash
run: |
Expand Down Expand Up @@ -108,6 +288,33 @@ jobs:

echo "Dependency closure OK. Assemblies: $dlls"

# Final verification: shipped Abstractions must match canonical hash
- name: Verify canonical Abstractions hash
if: env.CANONICAL_ABSTRACTIONS_AVAILABLE == 'true'
shell: pwsh
run: |
$packageDir = 'package'
$abstractionsDll = Join-Path $packageDir 'Lidarr.Plugin.Abstractions.dll'
$expectedHash = $env:CANONICAL_ABSTRACTIONS_HASH

if (-not (Test-Path $abstractionsDll)) {
Write-Host "::warning::Abstractions DLL not found in package"
exit 0
}

$actualHash = (Get-FileHash -Path $abstractionsDll -Algorithm SHA256).Hash.ToLower()

if ($actualHash -ne $expectedHash) {
Write-Host "::error::PACKAGING GUARD FAILED: Shipped Abstractions hash doesn't match canonical"
Write-Host "::error::Expected: $expectedHash"
Write-Host "::error::Actual: $actualHash"
Write-Host "::error::This indicates local build output leaked into the package."
exit 1
}

Write-Host "PACKAGING GUARD PASSED: Shipped Abstractions matches canonical"
Write-Host " Hash: $actualHash"

- name: Upload Package Artifact
uses: actions/upload-artifact@v4
with:
Expand Down
1 change: 1 addition & 0 deletions plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "Brainarr",
"version": "1.3.2",
"commonVersion": "1.5.0",
"description": "AI-powered music discovery with 11 providers including local, cloud, and subscription options",
"author": "Brainarr Team",
"minimumVersion": "2.14.2.4786",
Expand Down
Loading