Consolidate to single dlp_io module and publish release artifacts via Gitea Actions
publish-dlp-io / publish (push) Successful in 1m27s
publish-dlp-io / publish (push) Successful in 1m27s
This commit is contained in:
@@ -95,11 +95,11 @@ jobs:
|
||||
"DLP_IO_PACKAGE_VERSION=$packageVersion" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
Write-Host "package source: $tag @ $headCommit"
|
||||
|
||||
- name: Test Python 3.9-3.13
|
||||
- name: Test Python 3.10-3.14
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
foreach ($pythonVersion in @("3.9", "3.10", "3.11", "3.12", "3.13")) {
|
||||
foreach ($pythonVersion in @("3.10", "3.11", "3.12", "3.13", "3.14")) {
|
||||
& py "-$pythonVersion" --version
|
||||
if ($LASTEXITCODE -ne 0) { throw "Python $pythonVersion is required" }
|
||||
& py "-$pythonVersion" -m pip install -r requirements-dev.txt --progress-bar off
|
||||
@@ -118,20 +118,57 @@ jobs:
|
||||
if ($LASTEXITCODE -ne 0) { throw "build dependency install failed" }
|
||||
py -3.13 -m build
|
||||
if ($LASTEXITCODE -ne 0) { throw "dlp-io build failed" }
|
||||
py -3.13 -m twine check "dist\dlp_io-$($env:DLP_IO_PACKAGE_VERSION)*"
|
||||
if ($LASTEXITCODE -ne 0) { throw "twine check failed" }
|
||||
|
||||
$version = $env:DLP_IO_PACKAGE_VERSION
|
||||
$expected = @(
|
||||
"dlp_io-$version-py3-none-any.whl",
|
||||
"dlp_io-$version.tar.gz"
|
||||
)
|
||||
foreach ($pythonVersion in @("3.10", "3.11", "3.12", "3.13", "3.14")) {
|
||||
$abi = $pythonVersion.Replace(".", "")
|
||||
$wheel = "dlp_io-$version-cp$abi-cp$abi-win_amd64.whl"
|
||||
$env:DLP_IO_PYD = "1"
|
||||
try {
|
||||
& py "-$pythonVersion" -m build --wheel --no-isolation
|
||||
if ($LASTEXITCODE -ne 0) { throw "pyd build failed on $pythonVersion" }
|
||||
} finally {
|
||||
Remove-Item Env:DLP_IO_PYD
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path dist $wheel) -PathType Leaf)) {
|
||||
throw "expected pyd wheel missing: $wheel"
|
||||
}
|
||||
$expected += $wheel
|
||||
}
|
||||
|
||||
py -3.13 -m twine check "dist\dlp_io-$($env:DLP_IO_PACKAGE_VERSION)*"
|
||||
if ($LASTEXITCODE -ne 0) { throw "twine check failed" }
|
||||
$actual = @(Get-ChildItem -LiteralPath dist -File | ForEach-Object Name)
|
||||
if (Compare-Object $expected $actual) {
|
||||
throw "distribution file set mismatch: $($actual -join ', ')"
|
||||
}
|
||||
|
||||
- name: Smoke test isolated wheel
|
||||
$assetsDir = (New-Item -ItemType Directory -Force assets).FullName
|
||||
Copy-Item -LiteralPath (Resolve-Path dlp_io.py) -Destination $assetsDir
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
foreach ($wheelFile in @(Get-ChildItem -LiteralPath dist -Filter "*-cp3*-win_amd64.whl" -File)) {
|
||||
$zip = [System.IO.Compression.ZipFile]::OpenRead($wheelFile.FullName)
|
||||
try {
|
||||
$pydEntries = @($zip.Entries | Where-Object { $_.FullName -like "*.pyd" })
|
||||
if ($pydEntries.Count -ne 1) {
|
||||
throw "pyd wheel must contain exactly one .pyd: $($wheelFile.Name)"
|
||||
}
|
||||
$target = Join-Path $assetsDir $pydEntries[0].Name
|
||||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($pydEntries[0], $target, $true)
|
||||
} finally {
|
||||
$zip.Dispose()
|
||||
}
|
||||
}
|
||||
$bareAssets = @(Get-ChildItem -LiteralPath $assetsDir -File | ForEach-Object Name)
|
||||
if ($bareAssets.Count -ne 6) {
|
||||
throw "bare asset set mismatch: $($bareAssets -join ', ')"
|
||||
}
|
||||
|
||||
- name: Smoke test release artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -145,8 +182,8 @@ jobs:
|
||||
py -3.13 -m venv $rootFull
|
||||
if ($LASTEXITCODE -ne 0) { throw "wheel smoke venv creation failed" }
|
||||
$python = Join-Path $rootFull "Scripts\python.exe"
|
||||
$dist = (Resolve-Path dist).Path
|
||||
& $python -m pip install --no-index --find-links $dist "dlp-io==$env:DLP_IO_PACKAGE_VERSION" --progress-bar off
|
||||
$pureWheel = (Resolve-Path (Join-Path dist "dlp_io-$($env:DLP_IO_PACKAGE_VERSION)-py3-none-any.whl")).Path
|
||||
& $python -m pip install --no-index $pureWheel --progress-bar off
|
||||
if ($LASTEXITCODE -ne 0) { throw "wheel installation failed" }
|
||||
Push-Location $rootFull
|
||||
try {
|
||||
@@ -161,293 +198,146 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
- name: Upload package to Gitea PyPI
|
||||
shell: pwsh
|
||||
env:
|
||||
TWINE_USERNAME: antior
|
||||
TWINE_PASSWORD: ${{ secrets.CI_PACKAGE_TOKEN }}
|
||||
TWINE_REPOSITORY_URL: https://gitea.docker.antior.cn/api/packages/antior/pypi
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ([string]::IsNullOrWhiteSpace($env:TWINE_PASSWORD)) {
|
||||
throw "CI_PACKAGE_TOKEN repository secret is required"
|
||||
}
|
||||
|
||||
function Get-RegistryEntries([string]$SimpleUrl, [hashtable]$Headers) {
|
||||
foreach ($pythonVersion in @("3.10", "3.11", "3.12", "3.13", "3.14")) {
|
||||
$abi = $pythonVersion.Replace(".", "")
|
||||
$pydWheel = (Resolve-Path (Join-Path dist "dlp_io-$($env:DLP_IO_PACKAGE_VERSION)-cp$abi-cp$abi-win_amd64.whl")).Path
|
||||
$pydRoot = Join-Path $env:TEMP ("dlp-io-pyd-smoke-$abi-" + $PID)
|
||||
$pydRootFull = [IO.Path]::GetFullPath($pydRoot)
|
||||
if (-not $pydRootFull.StartsWith($tempFull, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "unsafe pyd smoke path: $pydRootFull"
|
||||
}
|
||||
try {
|
||||
$response = Invoke-WebRequest -Headers $Headers -Uri $SimpleUrl
|
||||
} catch {
|
||||
if ([int]$_.Exception.Response.StatusCode -eq 404) { return @() }
|
||||
throw
|
||||
}
|
||||
$pattern = '<a\s+href="(?<href>[^"]+)"[^>]*>(?<name>[^<]+)</a>'
|
||||
foreach ($match in [regex]::Matches($response.Content, $pattern, "IgnoreCase")) {
|
||||
$href = [Net.WebUtility]::HtmlDecode($match.Groups["href"].Value)
|
||||
$uri = [Uri]$href
|
||||
$hashMatch = [regex]::Match($uri.Fragment, '^#sha256=(?<hash>[0-9a-fA-F]{64})$')
|
||||
if (-not $uri.IsAbsoluteUri -or -not $hashMatch.Success) {
|
||||
throw "registry simple index contains an invalid artifact link"
|
||||
& py "-$pythonVersion" -m venv $pydRootFull
|
||||
if ($LASTEXITCODE -ne 0) { throw "pyd smoke venv creation failed on $pythonVersion" }
|
||||
$pydPython = Join-Path $pydRootFull "Scripts\python.exe"
|
||||
& $pydPython -m pip install --no-index $pydWheel --progress-bar off
|
||||
if ($LASTEXITCODE -ne 0) { throw "pyd wheel installation failed on $pythonVersion" }
|
||||
Push-Location $pydRootFull
|
||||
try {
|
||||
& $pydPython -c "import dlp_io,sys,tempfile; from pathlib import Path; assert dlp_io.__version__ == '$env:DLP_IO_PACKAGE_VERSION'; td=tempfile.TemporaryDirectory(); p=Path(td.name)/'payload.bin'; w=dlp_io.open(p,'wb'); assert w.write(b'0123456789') == 10; w.close(); dlp_io.configure(python_executable=sys.executable); r=dlp_io.open(p,'rb'); assert r.read(4) == b'0123'; assert r.seek(-3,2) == 7; assert r.read() == b'789'; r.close(); dlp_io.shutdown(); td.cleanup(); print('pyd smoke passed on $pythonVersion')"
|
||||
if ($LASTEXITCODE -ne 0) { throw "pyd wheel smoke failed on $pythonVersion" }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
$builder = [UriBuilder]$uri
|
||||
$builder.Fragment = ""
|
||||
[pscustomobject]@{
|
||||
filename = [Net.WebUtility]::HtmlDecode($match.Groups["name"].Value)
|
||||
url = $builder.Uri.AbsoluteUri
|
||||
sha256 = $hashMatch.Groups["hash"].Value.ToLowerInvariant()
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $pydRootFull) {
|
||||
Remove-Item -LiteralPath $pydRootFull -Recurse -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ArchiveContentDigest([string]$Path) {
|
||||
$script = @'
|
||||
import hashlib
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
entries = []
|
||||
if path.suffix == ".whl":
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
entries = [
|
||||
(item.filename, archive.read(item))
|
||||
for item in archive.infolist()
|
||||
if not item.is_dir()
|
||||
]
|
||||
else:
|
||||
with tarfile.open(path, "r:*") as archive:
|
||||
for item in archive.getmembers():
|
||||
if not item.isfile():
|
||||
continue
|
||||
stream = archive.extractfile(item)
|
||||
parts = PurePosixPath(item.name).parts[1:]
|
||||
if stream is not None and parts:
|
||||
entries.append(("/".join(parts), stream.read()))
|
||||
|
||||
digest = hashlib.sha256()
|
||||
for name, content in sorted(entries):
|
||||
digest.update(name.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(len(content).to_bytes(8, "big"))
|
||||
digest.update(hashlib.sha256(content).digest())
|
||||
print(digest.hexdigest())
|
||||
'@
|
||||
$digest = (& py -3.13 -c $script $Path).Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or $digest -notmatch '^[0-9a-f]{64}$') {
|
||||
throw "failed to calculate archive content digest: $Path"
|
||||
}
|
||||
return $digest
|
||||
}
|
||||
|
||||
$packageVersion = $env:DLP_IO_PACKAGE_VERSION
|
||||
$expected = @(
|
||||
"dlp_io-$packageVersion-py3-none-any.whl",
|
||||
"dlp_io-$packageVersion.tar.gz"
|
||||
)
|
||||
$headers = @{ Authorization = "token $env:TWINE_PASSWORD" }
|
||||
$simpleUrl = "$env:TWINE_REPOSITORY_URL/simple/dlp-io/"
|
||||
$registryOrigin = [Uri]$env:TWINE_REPOSITORY_URL
|
||||
$entries = @(Get-RegistryEntries $simpleUrl $headers)
|
||||
$missingArtifacts = [System.Collections.Generic.List[string]]::new()
|
||||
$root = Join-Path $env:TEMP ("dlp-io-upload-state-" + [Guid]::NewGuid().ToString("N"))
|
||||
$rootFull = [IO.Path]::GetFullPath($root)
|
||||
$tempFull = [IO.Path]::GetFullPath($env:TEMP).TrimEnd('\') + '\'
|
||||
if (-not $rootFull.StartsWith($tempFull, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "unsafe upload verification path: $rootFull"
|
||||
$bareRoot = Join-Path $env:TEMP ("dlp-io-bare-smoke-" + $PID)
|
||||
$bareRootFull = [IO.Path]::GetFullPath($bareRoot)
|
||||
if (-not $bareRootFull.StartsWith($tempFull, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "unsafe bare smoke path: $bareRootFull"
|
||||
}
|
||||
try {
|
||||
New-Item -ItemType Directory -Force $rootFull | Out-Null
|
||||
foreach ($filename in $expected) {
|
||||
$local = (Resolve-Path (Join-Path dist $filename)).Path
|
||||
$entry = @($entries | Where-Object filename -eq $filename)
|
||||
if ($entry.Count -eq 0) {
|
||||
$missingArtifacts.Add($local)
|
||||
continue
|
||||
foreach ($pythonVersion in @("3.10", "3.11", "3.12", "3.13", "3.14")) {
|
||||
$abi = $pythonVersion.Replace(".", "")
|
||||
$sandbox = Join-Path $bareRootFull "cp$abi"
|
||||
New-Item -ItemType Directory -Force $sandbox | Out-Null
|
||||
Copy-Item -LiteralPath (Resolve-Path (Join-Path assets "dlp_io.cp$abi-win_amd64.pyd")) -Destination $sandbox
|
||||
Push-Location $sandbox
|
||||
try {
|
||||
& py "-$pythonVersion" -c "import dlp_io,sys,tempfile; from pathlib import Path; assert dlp_io.__version__ == '$env:DLP_IO_PACKAGE_VERSION'; assert dlp_io.__file__.endswith('.pyd'); td=tempfile.TemporaryDirectory(); p=Path(td.name)/'payload.bin'; w=dlp_io.open(p,'wb'); assert w.write(b'0123456789') == 10; w.close(); dlp_io.configure(python_executable=sys.executable); r=dlp_io.open(p,'rb'); assert r.read(4) == b'0123'; assert r.seek(-3,2) == 7; assert r.read() == b'789'; r.close(); dlp_io.shutdown(); td.cleanup(); print('bare pyd smoke passed on $pythonVersion')"
|
||||
if ($LASTEXITCODE -ne 0) { throw "bare pyd smoke failed on $pythonVersion" }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
if ($entry.Count -ne 1) {
|
||||
throw "registry simple index contains duplicate artifact links: $filename"
|
||||
}
|
||||
$downloadUri = [Uri]$entry[0].url
|
||||
if (-not $downloadUri.Scheme.Equals($registryOrigin.Scheme, [StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not $downloadUri.Authority.Equals($registryOrigin.Authority, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "download URL is not on the package registry origin: $filename"
|
||||
}
|
||||
$remote = Join-Path $rootFull $filename
|
||||
Invoke-WebRequest -Headers $headers -Uri $downloadUri.AbsoluteUri -OutFile $remote
|
||||
$remoteHash = (Get-FileHash -LiteralPath $remote -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($remoteHash -ne $entry[0].sha256) {
|
||||
throw "registry download hash mismatch: $filename"
|
||||
}
|
||||
if ((Get-ArchiveContentDigest $local) -ne (Get-ArchiveContentDigest $remote)) {
|
||||
throw "existing registry artifact differs from tagged build: $filename"
|
||||
}
|
||||
Write-Host "registry artifact already matches tagged build: $filename"
|
||||
}
|
||||
|
||||
if ($missingArtifacts.Count -gt 0) {
|
||||
$twineArgs = @(
|
||||
"-3.13", "-m", "twine", "upload", "--non-interactive",
|
||||
"--disable-progress-bar", "--repository-url", $env:TWINE_REPOSITORY_URL
|
||||
) + $missingArtifacts
|
||||
& py @twineArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "twine upload failed" }
|
||||
} else {
|
||||
Write-Host "all registry artifacts already match the tagged build"
|
||||
}
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $rootFull) {
|
||||
Remove-Item -LiteralPath $rootFull -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
- name: Test registry package files
|
||||
shell: pwsh
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.CI_PACKAGE_TOKEN }}
|
||||
TWINE_REPOSITORY_URL: https://gitea.docker.antior.cn/api/packages/antior/pypi
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$packageVersion = $env:DLP_IO_PACKAGE_VERSION
|
||||
$headers = @{ Authorization = "token $env:REGISTRY_TOKEN" }
|
||||
|
||||
function Get-RegistryEntries([string]$SimpleUrl, [hashtable]$Headers) {
|
||||
$pySandbox = Join-Path $bareRootFull "py"
|
||||
New-Item -ItemType Directory -Force $pySandbox | Out-Null
|
||||
Copy-Item -LiteralPath (Resolve-Path (Join-Path assets "dlp_io.py")) -Destination $pySandbox
|
||||
Push-Location $pySandbox
|
||||
try {
|
||||
$response = Invoke-WebRequest -Headers $Headers -Uri $SimpleUrl
|
||||
} catch {
|
||||
if ([int]$_.Exception.Response.StatusCode -eq 404) { return @() }
|
||||
throw
|
||||
}
|
||||
$pattern = '<a\s+href="(?<href>[^"]+)"[^>]*>(?<name>[^<]+)</a>'
|
||||
foreach ($match in [regex]::Matches($response.Content, $pattern, "IgnoreCase")) {
|
||||
$href = [Net.WebUtility]::HtmlDecode($match.Groups["href"].Value)
|
||||
$uri = [Uri]$href
|
||||
$hashMatch = [regex]::Match($uri.Fragment, '^#sha256=(?<hash>[0-9a-fA-F]{64})$')
|
||||
if (-not $uri.IsAbsoluteUri -or -not $hashMatch.Success) {
|
||||
throw "registry simple index contains an invalid artifact link"
|
||||
}
|
||||
$builder = [UriBuilder]$uri
|
||||
$builder.Fragment = ""
|
||||
[pscustomobject]@{
|
||||
filename = [Net.WebUtility]::HtmlDecode($match.Groups["name"].Value)
|
||||
url = $builder.Uri.AbsoluteUri
|
||||
sha256 = $hashMatch.Groups["hash"].Value.ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ArchiveContentDigest([string]$Path) {
|
||||
$script = @'
|
||||
import hashlib
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
entries = []
|
||||
if path.suffix == ".whl":
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
entries = [
|
||||
(item.filename, archive.read(item))
|
||||
for item in archive.infolist()
|
||||
if not item.is_dir()
|
||||
]
|
||||
else:
|
||||
with tarfile.open(path, "r:*") as archive:
|
||||
for item in archive.getmembers():
|
||||
if not item.isfile():
|
||||
continue
|
||||
stream = archive.extractfile(item)
|
||||
parts = PurePosixPath(item.name).parts[1:]
|
||||
if stream is not None and parts:
|
||||
entries.append(("/".join(parts), stream.read()))
|
||||
|
||||
digest = hashlib.sha256()
|
||||
for name, content in sorted(entries):
|
||||
digest.update(name.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(len(content).to_bytes(8, "big"))
|
||||
digest.update(hashlib.sha256(content).digest())
|
||||
print(digest.hexdigest())
|
||||
'@
|
||||
$digest = (& py -3.13 -c $script $Path).Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or $digest -notmatch '^[0-9a-f]{64}$') {
|
||||
throw "failed to calculate archive content digest: $Path"
|
||||
}
|
||||
return $digest
|
||||
}
|
||||
|
||||
$expected = @(
|
||||
"dlp_io-$packageVersion-py3-none-any.whl",
|
||||
"dlp_io-$packageVersion.tar.gz"
|
||||
)
|
||||
$simpleUrl = "$env:TWINE_REPOSITORY_URL/simple/dlp-io/"
|
||||
$entries = @()
|
||||
$actual = @()
|
||||
for ($attempt = 1; $attempt -le 20; $attempt++) {
|
||||
$entries = @(Get-RegistryEntries $simpleUrl $headers)
|
||||
$actual = @($entries | ForEach-Object filename)
|
||||
if (-not (Compare-Object $expected $actual)) { break }
|
||||
if ($attempt -lt 20) { Start-Sleep -Seconds 3 }
|
||||
}
|
||||
if (Compare-Object $expected $actual) {
|
||||
throw "registry package file mismatch: $($actual -join ', ')"
|
||||
}
|
||||
|
||||
$registryRoot = Join-Path $env:TEMP ("dlp-io-registry-smoke-" + $PID)
|
||||
$registryRootFull = [IO.Path]::GetFullPath($registryRoot)
|
||||
$tempFull = [IO.Path]::GetFullPath($env:TEMP).TrimEnd('\') + '\'
|
||||
if (-not $registryRootFull.StartsWith($tempFull, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "unsafe registry smoke path: $registryRootFull"
|
||||
}
|
||||
try {
|
||||
$downloads = Join-Path $registryRootFull "downloads"
|
||||
New-Item -ItemType Directory -Force $downloads | Out-Null
|
||||
$registryOrigin = [Uri]$env:TWINE_REPOSITORY_URL
|
||||
foreach ($filename in $expected) {
|
||||
$entry = @($entries | Where-Object filename -eq $filename)
|
||||
if ($entry.Count -ne 1 -or [string]::IsNullOrWhiteSpace($entry[0].url)) {
|
||||
throw "registry simple index is missing a download URL: $filename"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($entry[0].sha256)) {
|
||||
throw "registry simple index is missing SHA-256: $filename"
|
||||
}
|
||||
$downloadUri = [Uri]$entry[0].url
|
||||
if (-not $downloadUri.IsAbsoluteUri -or
|
||||
-not $downloadUri.Scheme.Equals($registryOrigin.Scheme, [StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not $downloadUri.Authority.Equals($registryOrigin.Authority, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "download URL is not on the package registry origin: $filename"
|
||||
}
|
||||
$target = Join-Path $downloads $filename
|
||||
Invoke-WebRequest -Headers $headers -Uri $downloadUri.AbsoluteUri -OutFile $target
|
||||
$downloadHash = (Get-FileHash -LiteralPath $target -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($downloadHash -ne $entry[0].sha256) {
|
||||
throw "registry download hash mismatch: $filename"
|
||||
}
|
||||
$local = (Resolve-Path (Join-Path dist $filename)).Path
|
||||
if ((Get-ArchiveContentDigest $local) -ne (Get-ArchiveContentDigest $target)) {
|
||||
throw "registry artifact differs from tagged build: $filename"
|
||||
}
|
||||
}
|
||||
|
||||
$venv = Join-Path $registryRootFull "venv"
|
||||
py -3.13 -m venv $venv
|
||||
if ($LASTEXITCODE -ne 0) { throw "registry smoke venv creation failed" }
|
||||
$python = Join-Path $venv "Scripts\python.exe"
|
||||
$registryWheel = Join-Path $downloads "dlp_io-$packageVersion-py3-none-any.whl"
|
||||
& $python -m pip install --no-index --no-deps $registryWheel --progress-bar off
|
||||
if ($LASTEXITCODE -ne 0) { throw "registry package installation failed" }
|
||||
Push-Location $registryRootFull
|
||||
try {
|
||||
& $python -c "import dlp_io,sys,tempfile; from pathlib import Path; assert dlp_io.__version__ == '$packageVersion'; td=tempfile.TemporaryDirectory(); p=Path(td.name)/'payload.bin'; w=dlp_io.open(p,'wb'); w.write(b'registry'); w.close(); dlp_io.configure(python_executable=sys.executable); r=dlp_io.open(p,'rb'); assert r.read() == b'registry'; r.close(); dlp_io.shutdown(); td.cleanup(); print('registry install passed')"
|
||||
if ($LASTEXITCODE -ne 0) { throw "registry installation smoke failed" }
|
||||
& py -3.13 -c "import dlp_io,sys,tempfile; from pathlib import Path; assert dlp_io.__version__ == '$env:DLP_IO_PACKAGE_VERSION'; assert dlp_io.__file__.endswith('.py'); td=tempfile.TemporaryDirectory(); p=Path(td.name)/'payload.bin'; w=dlp_io.open(p,'wb'); assert w.write(b'0123456789') == 10; w.close(); dlp_io.configure(python_executable=sys.executable); r=dlp_io.open(p,'rb'); assert r.read(4) == b'0123'; assert r.seek(-3,2) == 7; assert r.read() == b'789'; r.close(); dlp_io.shutdown(); td.cleanup(); print('bare py smoke passed')"
|
||||
if ($LASTEXITCODE -ne 0) { throw "bare py smoke failed" }
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $registryRootFull) {
|
||||
Remove-Item -LiteralPath $registryRootFull -Recurse -Force
|
||||
if (Test-Path -LiteralPath $bareRootFull) {
|
||||
Remove-Item -LiteralPath $bareRootFull -Recurse -Force
|
||||
}
|
||||
}
|
||||
Write-Host "registry verified: dlp-io==$packageVersion ($($actual.Count) files)"
|
||||
|
||||
- name: Create Gitea release with wheels
|
||||
shell: pwsh
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITEA_SERVER: ${{ gitea.server_url }}
|
||||
GITEA_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ([string]::IsNullOrWhiteSpace($env:GITEA_TOKEN)) {
|
||||
throw "GITHUB_TOKEN is required to publish the release"
|
||||
}
|
||||
$tag = $env:DLP_IO_PACKAGE_TAG
|
||||
$version = $env:DLP_IO_PACKAGE_VERSION
|
||||
$api = "$env:GITEA_SERVER/api/v1/repos/$env:GITEA_REPOSITORY"
|
||||
$headers = @{ Authorization = "token $env:GITEA_TOKEN" }
|
||||
|
||||
$artifacts = @(Get-ChildItem -LiteralPath dist -File) + @(Get-ChildItem -LiteralPath assets -File)
|
||||
if ($artifacts.Count -eq 0) { throw "no distribution files to publish" }
|
||||
|
||||
$release = $null
|
||||
try {
|
||||
$release = Invoke-RestMethod -Headers $headers -Uri "$api/releases/tags/$tag"
|
||||
} catch {
|
||||
if ([int]$_.Exception.Response.StatusCode -ne 404) { throw }
|
||||
}
|
||||
if ($null -eq $release) {
|
||||
$notes = @(
|
||||
"dlp-io $version",
|
||||
"",
|
||||
"## 产物",
|
||||
"",
|
||||
"- ``dlp_io-$version-py3-none-any.whl``:纯 Python wheel,pip 安装,跨平台",
|
||||
"- ``dlp_io-$version-cp3XX-cp3XX-win_amd64.whl``:pyd wheel,pip 安装,Windows 按解释器版本选用(cp310-cp314)",
|
||||
"- ``dlp_io-$version.tar.gz``:sdist 源码包",
|
||||
"- ``dlp_io.py`` / ``dlp_io.cp3XX-win_amd64.pyd``:免安装单文件,直接放进项目目录即可 ``import dlp_io``",
|
||||
"",
|
||||
"## 安装",
|
||||
"",
|
||||
"纯 Python wheel(跨平台):",
|
||||
"",
|
||||
"``````",
|
||||
"py -m pip install $env:GITEA_SERVER/$env:GITEA_REPOSITORY/releases/download/$tag/dlp_io-$version-py3-none-any.whl",
|
||||
"``````",
|
||||
"",
|
||||
"pyd wheel(Windows,示例为 CPython 3.13,其它版本替换文件名中的 cp313):",
|
||||
"",
|
||||
"``````",
|
||||
"py -3.13 -m pip install $env:GITEA_SERVER/$env:GITEA_REPOSITORY/releases/download/$tag/dlp_io-$version-cp313-cp313-win_amd64.whl",
|
||||
"``````"
|
||||
) -join "`n"
|
||||
$body = @{
|
||||
tag_name = $tag
|
||||
name = "dlp-io $version"
|
||||
body = $notes
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
} | ConvertTo-Json
|
||||
$release = Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases" -Body $body -ContentType "application/json"
|
||||
Write-Host "created release: $tag"
|
||||
} else {
|
||||
Write-Host "release already exists: $tag"
|
||||
}
|
||||
|
||||
foreach ($artifact in $artifacts) {
|
||||
$existing = @($release.assets | Where-Object name -eq $artifact.Name)
|
||||
foreach ($asset in $existing) {
|
||||
Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/assets/$($asset.id)" | Out-Null
|
||||
Write-Host "replaced asset: $($artifact.Name)"
|
||||
}
|
||||
Invoke-RestMethod -Method Post -Headers $headers -Uri "$api/releases/$($release.id)/assets?name=$($artifact.Name)" -Form @{ attachment = Get-Item $artifact.FullName } | Out-Null
|
||||
Write-Host "uploaded asset: $($artifact.Name)"
|
||||
}
|
||||
|
||||
$published = Invoke-RestMethod -Headers $headers -Uri "$api/releases/$($release.id)"
|
||||
$publishedNames = @($published.assets | ForEach-Object name)
|
||||
$localNames = @($artifacts | ForEach-Object Name)
|
||||
if (Compare-Object $localNames $publishedNames) {
|
||||
throw "release asset mismatch: $($publishedNames -join ', ')"
|
||||
}
|
||||
Write-Host "release verified: $tag ($($localNames.Count) files)"
|
||||
|
||||
Reference in New Issue
Block a user