From 8c58b1c43ec721a31128f0b1930035cbf3bd745d Mon Sep 17 00:00:00 2001 From: lincube Date: Mon, 20 Apr 2026 14:25:17 +0800 Subject: [PATCH] ci: add local pdc mock fallback for release publish --- .github/workflows/release.yml | 82 ++++++++++++++++++++ scripts/pdc-mock-server.py | 141 ++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 scripts/pdc-mock-server.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4382019..6a9c2a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -785,6 +785,87 @@ jobs: python3 -m pip install --user --upgrade awscli Add-Content -Path $env:GITHUB_PATH -Value "$HOME/.local/bin" + - name: Bootstrap PDC Endpoint and Token + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $endpoint = $env:PDC_ENDPOINT + if ([string]::IsNullOrWhiteSpace($endpoint)) { + $endpoint = "http://127.0.0.1:18765" + } + + $token = $env:PDC_TOKEN + if ([string]::IsNullOrWhiteSpace($token)) { + $token = "lmd-pdc-local-token" + } + + Add-Content -Path $env:GITHUB_ENV -Value "PDC_ENDPOINT=$endpoint" + Add-Content -Path $env:GITHUB_ENV -Value "PDC_TOKEN=$token" + Write-Host "Using PDC endpoint: $endpoint" + + - name: Start Local PDC Mock (Fallback) + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + if ([string]::IsNullOrWhiteSpace($env:PDC_ENDPOINT)) { + throw "PDC_ENDPOINT is empty after bootstrap." + } + + $uri = [Uri]$env:PDC_ENDPOINT + $isLocalHost = $uri.Host -eq "127.0.0.1" -or $uri.Host -eq "localhost" + if (-not $isLocalHost) { + Write-Host "Using external PDC endpoint: $($env:PDC_ENDPOINT)" + exit 0 + } + + if ([string]::IsNullOrWhiteSpace($env:PDC_TOKEN)) { + throw "PDC_TOKEN is empty after bootstrap." + } + + $port = if ($uri.Port -gt 0) { $uri.Port } else { 18765 } + $dataDir = Join-Path $PWD "pdc-output/mock-pdc" + $logPath = Join-Path $PWD "pdc-work/pdc-mock.log" + + New-Item -ItemType Directory -Path $dataDir -Force | Out-Null + if (Test-Path $logPath) { + Remove-Item -LiteralPath $logPath -Force + } + + $args = @( + "scripts/pdc-mock-server.py", + "--host", "127.0.0.1", + "--port", $port.ToString(), + "--token", $env:PDC_TOKEN, + "--data-dir", $dataDir + ) + $process = Start-Process -FilePath "python3" -ArgumentList $args -PassThru -RedirectStandardOutput $logPath -RedirectStandardError $logPath + if (-not $process) { + throw "Failed to launch PDC mock server." + } + + $healthUrl = "http://127.0.0.1:$port/healthz" + $ready = $false + for ($i = 0; $i -lt 20; $i++) { + Start-Sleep -Seconds 1 + try { + $response = Invoke-WebRequest -Uri $healthUrl -Method Get -TimeoutSec 2 + if ($response.StatusCode -eq 200) { + $ready = $true + break + } + } + catch { + } + } + + if (-not $ready) { + throw "PDC mock server did not become ready in time. See $logPath." + } + + Write-Host "Local PDC mock is running at http://127.0.0.1:$port" + - name: Install PDCC shell: pwsh env: @@ -804,6 +885,7 @@ jobs: $env:PDC_Token = $env:PDC_TOKEN $env:S3_AccessKey = $env:S3_ACCESS_KEY $env:S3_SecretKey = $env:S3_SECRET_KEY + $env:PDC_SigningKeyPs = "" if ([string]::IsNullOrWhiteSpace($env:PDC_SigningKey)) { $env:PDC_SigningKey = $env:PDC_SIGNING_KEY } diff --git a/scripts/pdc-mock-server.py b/scripts/pdc-mock-server.py new file mode 100644 index 0000000..93f1e13 --- /dev/null +++ b/scripts/pdc-mock-server.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +import argparse +import json +import re +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path + + +def _utc_now_text() -> str: + return datetime.now(timezone.utc).isoformat() + + +class PdcMockHandler(BaseHTTPRequestHandler): + token = "" + data_dir = Path(".") + + def _write_json(self, status_code: int, payload: dict) -> None: + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + self.send_response(status_code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def _read_json_body(self) -> dict: + length = int(self.headers.get("Content-Length", "0")) + if length <= 0: + return {} + raw = self.rfile.read(length) + if not raw: + return {} + try: + return json.loads(raw.decode("utf-8")) + except Exception: + return {} + + def _save_payload(self, name: str, payload: dict) -> None: + out = self.data_dir / f"{name}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text( + json.dumps( + { + "savedAtUtc": _utc_now_text(), + "path": self.path, + "payload": payload, + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + + def _check_token(self) -> bool: + expected = (self.token or "").strip() + if not expected: + return True + provided = (self.headers.get("X-PDC-Token") or "").strip() + return provided == expected + + def do_GET(self) -> None: + if self.path == "/healthz": + self._write_json(200, {"ok": True, "timeUtc": _utc_now_text()}) + return + + self._write_json(404, {"error": "not_found", "path": self.path}) + + def do_POST(self) -> None: + if not self._check_token(): + self._write_json(401, {"error": "unauthorized"}) + return + + payload = self._read_json_body() + + if self.path == "/api/v1/fileMaps/diff": + items = payload.get("items") if isinstance(payload, dict) else {} + keys = sorted(items.keys()) if isinstance(items, dict) else [] + self._save_payload("filemaps-diff-request", payload) + result = { + "success": True, + "code": 0, + "message": "ok", + "content": keys, + "Content": keys, + } + self._write_json(200, result) + return + + if self.path == "/api/v1/fileMaps/upload": + self._save_payload("filemaps-upload-request", payload) + result = { + "success": True, + "code": 0, + "message": "ok", + "content": True, + "Content": True, + } + self._write_json(200, result) + return + + m = re.match(r"^/api/v1/distribution/([^/]+)/([^/]+)$", self.path) + if m: + primary_version = m.group(1) + version = m.group(2) + distribution_id = f"{primary_version}-{version}" + self._save_payload("distribution-request", payload) + result = { + "success": True, + "code": 0, + "message": "ok", + "content": {"distributionId": distribution_id}, + "Content": {"distributionId": distribution_id}, + } + self._write_json(200, result) + return + + self._write_json(404, {"error": "not_found", "path": self.path}) + + def log_message(self, fmt: str, *args) -> None: + print(f"[pdc-mock] {self.address_string()} - {fmt % args}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="PDC mock server for CI fallback") + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=18765) + parser.add_argument("--token", default="") + parser.add_argument("--data-dir", required=True) + args = parser.parse_args() + + PdcMockHandler.token = args.token + PdcMockHandler.data_dir = Path(args.data_dir) + PdcMockHandler.data_dir.mkdir(parents=True, exist_ok=True) + + server = ThreadingHTTPServer((args.host, args.port), PdcMockHandler) + print(f"[pdc-mock] listening on http://{args.host}:{args.port}") + server.serve_forever() + + +if __name__ == "__main__": + main()