Skip to content

Guide customer-owned safe primary-to-secondary provider fallback #90

Guide customer-owned safe primary-to-secondary provider fallback

Guide customer-owned safe primary-to-secondary provider fallback #90

Workflow file for this run

name: Function ZIPs
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: function-zips-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22.x'
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: '8.0.x'
- name: Test JavaScript
working-directory: javascript
run: |
npm ci --ignore-scripts --no-audit --no-fund
npm test
- name: Test Python
working-directory: python
run: |
python -m pip install -r requirements.txt pytest
python -m pytest tests
- name: Test .NET
run: dotnet test dotnet/tests/Epp.Otp.Tests.csproj
- name: Build ZIPs
shell: pwsh
run: |
./package-javascript.ps1
./package-dotnet.ps1
./package-python.ps1
- name: Verify existing ZIPs are never overwritten
shell: pwsh
run: |
$packages = @{
'package-javascript.ps1' = 'artifacts/epp-javascript.zip'
'package-dotnet.ps1' = 'artifacts/epp-dotnet-source.zip'
'package-python.ps1' = 'artifacts/epp-python-source.zip'
}
foreach ($script in ($packages.Keys | Sort-Object)) {
$archive = (Resolve-Path -LiteralPath $packages[$script]).Path
$before = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash
$rejected = $false
try {
& (Join-Path $PWD $script) -OutputPath $archive
} catch {
if (-not $_.Exception.Message.StartsWith('Output already exists:')) { throw }
$rejected = $true
}
if (-not $rejected) { throw "$script did not reject an existing archive" }
if ((Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash -ne $before) {
throw "$script changed the existing archive"
}
Write-Host "$script rejected the existing output; SHA256 unchanged."
}
- name: Verify ZIPs and write checksums
shell: pwsh
run: |
$required = @{
'epp-javascript.zip' = @('host.json', 'src/functions/SendOtp.js', 'node_modules/@azure/functions/package.json')
'epp-dotnet-source.zip' = @('host.json', 'dotnet.csproj', 'Program.cs', 'Functions/SendOtp.cs', 'Src/PhoneProviderBase.cs')
'epp-python-source.zip' = @('host.json', 'function_app.py', 'requirements.txt', 'src/jwe.py', 'src/provider.py')
}
$checksums = foreach ($name in ($required.Keys | Sort-Object)) {
$path = Join-Path 'artifacts' $name
$zip = [IO.Compression.ZipFile]::OpenRead((Resolve-Path $path))
try {
$entries = @($zip.Entries.FullName)
foreach ($file in $required[$name]) {
if ($file -notin $entries) { throw "Missing $file in $name" }
}
if ($entries -match '(^|/)(local\.settings[^/]*|\.env[^/]*|\.git|\.venv|__pycache__)(/|$)|\.(pem|pfx|p12|key|publishsettings|pubxml)$') {
throw "Private files in $name"
}
if ($name -eq 'epp-dotnet-source.zip' -and
($entries -match '(^|/)(bin|obj|tests?|\.azurefunctions)(/|$)|\.(dll|exe|pdb|deps\.json|runtimeconfig\.json)$|(^|/)(functions\.metadata|worker\.config\.json)$')) {
throw 'Build output or tests found in .NET source ZIP'
}
} finally { $zip.Dispose() }
'{0} {1}' -f (Get-FileHash $path -Algorithm SHA256).Hash.ToLowerInvariant(), $name
}
$checksums | Set-Content artifacts/SHA256SUMS.txt -Encoding utf8
- name: Verify .NET source ZIP can be published by customers
shell: pwsh
run: |
$temporary = Join-Path ([IO.Path]::GetTempPath()) ('epp-source-check-' + [guid]::NewGuid().ToString('N'))
try {
$source = Join-Path $temporary 'source'
$publish = Join-Path $temporary 'publish'
[IO.Compression.ZipFile]::ExtractToDirectory((Resolve-Path 'artifacts/epp-dotnet-source.zip'), $source)
dotnet publish (Join-Path $source 'dotnet.csproj') --configuration Release --output $publish --verbosity minimal
if ($LASTEXITCODE -ne 0) { throw 'Extracted .NET source could not be published' }
foreach ($file in @('host.json', 'dotnet.dll', 'functions.metadata', 'worker.config.json', '.azurefunctions')) {
if (-not (Test-Path -LiteralPath (Join-Path $publish $file))) { throw "Missing customer publish output: $file" }
}
} finally {
if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary -Recurse -Force }
}
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: function-zips
path: |
artifacts/epp-javascript.zip
artifacts/epp-dotnet-source.zip
artifacts/epp-python-source.zip
artifacts/SHA256SUMS.txt
if-no-files-found: error
retention-days: 14
publish:
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: function-zips
path: artifacts
- name: Publish versioned ZIP downloads
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
COMMIT_SHA: ${{ github.sha }}
RUN_NUMBER: ${{ github.run_number }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
tag="epp-packages-${RUN_NUMBER}-${RUN_ATTEMPT}"
gh release create "$tag" artifacts/*.zip artifacts/SHA256SUMS.txt \
--target "$COMMIT_SHA" --title "EPP Function ZIPs ${RUN_NUMBER}.${RUN_ATTEMPT}" \
--latest \
--notes "Packaged and tested from commit ${COMMIT_SHA}. Download one language ZIP and verify it against SHA256SUMS.txt. JavaScript includes production dependencies. .NET contains project and C# source only: extract and build/publish the project before deployment. Python is a source ZIP requiring Azure remote build on Linux. Neither source ZIP is a direct run-from-package artifact. Configure runtime, app settings, Key Vault access, and Easy Auth separately. No cloud deployment or live provider verification is performed."