#!/usr/bin/env python3 """Accept a fixed managed-project release using a fresh remote module cache.""" import argparse import hashlib import json import os from pathlib import Path import re import shutil import signal import tempfile import time from verifyErrorInheritanceRelease import run from verifyManagedProject import serve, verify_contract_changes ROOT = Path(__file__).resolve().parents[1] MODULE = "git.yuntuo.hk/k2safe/go-omni" def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--version", required=True) parser.add_argument("--report", type=Path, default=ROOT / "performance/managedProjectRemote.json") args = parser.parse_args() if not re.fullmatch(r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", args.version): parser.error("a fixed release version is required") env = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")} env.update(GOWORK="off", GOFLAGS="", GOPRIVATE="git.yuntuo.hk", GONOSUMDB="git.yuntuo.hk") started = time.monotonic() checks = ["remoteCLIInstall", "freshModuleCache", "fixedVersionNoReplace"] with tempfile.TemporaryDirectory(prefix="goOmniManagedRelease-") as directory: temp = Path(directory) (temp / "bin").mkdir() env.update(GOBIN=str(temp / "bin"), GOMODCACHE=str(temp / "modcache")) print(f"Installing remote CLI {args.version} in an empty module cache", flush=True) run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env) cli = temp / "bin/go-omni" # Do not pass --framework-version: verify that the installed CLI pins # its own downloaded version, without a source-directory replacement. starter = temp / "starter" run([cli, "new", "--module", "example.com/releaseStarter", starter], temp, env) run(["go", "mod", "tidy"], starter, env) dependency = json.loads(run(["go", "list", "-m", "-json", MODULE], starter, env)) assert dependency["Version"] == args.version and dependency.get("Sum") and not dependency.get("Replace") verify_contract_changes(cli, starter, env) checks += ["contractChangePreview", "persistentLogicSignatureFailure", "businessSourcePreserved", "fixedConsumerBuild"] serve(["go", "run", "."], starter, env, "nethttp", signal.SIGTERM, False, logs=temp) for transport in ("nethttp", "native"): for sig in (signal.SIGINT, signal.SIGTERM): serve([str(starter / "releaseStarter")], starter, env, transport, sig, True, logs=temp) checks += ["rootGoRun", "nethttpAndNativeSignals", "listenerReleased"] # Only project-owned business implementations come from the example. # Main, handlers, DTOs and startup are emitted by the downloaded CLI. managed = temp / "managed" run([cli, "new", "--api", ROOT / "examples/contracts/managed.api", "--module", "example.com/managed", managed], temp, env) example = ROOT / "examples/managedService" sources = list((example / "internal/logic/demo").glob("*.go")) sources += [example / "internal/svc/options.go", example / "internal/apierrors/catalog.go"] for source in sources: target = managed / source.relative_to(example) target.parent.mkdir(parents=True, exist_ok=True) shutil.copyfile(source, target) run([cli, "api", "generate", "--check"], managed, env) run(["go", "mod", "tidy"], managed, env) run(["go", "build", "-o", temp / "managedServer", "."], managed, env) managed_dependency = json.loads(run(["go", "list", "-m", "-json", MODULE], managed, env)) assert managed_dependency["Sum"] == dependency["Sum"] and not managed_dependency.get("Replace") for transport in ("nethttp", "native"): serve([str(temp / "managedServer")], managed, env, transport, signal.SIGTERM, True, contract=True, logs=temp) checks += ["default404And405", "goOwnedErrorCatalog", "unwrappedDownloads", "rawCallback", "multipart400And413", "nethttpAndNativeTransfers"] env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version) tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject"] print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True) output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env) events = [json.loads(line) for line in output.splitlines() if line.startswith("{")] passed = [e["Test"] for e in events if e.get("Action") == "pass" and "Test" in e] assert sorted(passed) == sorted(tests), output evidence = "".join(e.get("Output", "") for e in events) sums = re.findall(r"checksum (h1:\S+); no local replace", evidence) assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch" checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "strictRequestPolicies", "declarativeMultipartBinding", "backgroundStopDrainResourceOrder", "backgroundInitAndListenFailureCleanup", "backgroundTimeoutRetainsResourcesAndExits"] sources = ["lifecycle/runtime.go", "lifecycle/phases.go", "config/lifecycle_test.go", "task/task.go", "cmd/go-omni/scaffold/templates/api-runtime.go.tmpl", "cmd/go-omni/apigen/templates.go", "config/project.go", "contract/bind.go", "contract/policy.go", "contract/multipart.go", "contract/file.go", "cmd/go-omni/apigen/business_test.go", "cmd/go-omni/apigen/generate.go", "lifecycle/controller.go", "config/endpoints.go", "health/health.go", "httpx/response.go", "app/app.go", "app/transport.go", "scripts/verifyManagedRelease.py", "scripts/verifyManagedProject.py", "cmd/go-omni/apigen/managed_test.go"] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/managed").glob("*.go")] sources += [str(p.relative_to(ROOT)) for p in (ROOT / "cmd/go-omni/apigen/testdata/business").glob("*.go")] report = {"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"], "freshModuleCache": True, "noLocalReplace": True, "durationSeconds": round(time.monotonic() - started, 2), "checks": checks, "sourceHashes": {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in sources}} args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") print(f"PASS remote managed project release {args.version}; temporary files and processes removed", flush=True) if __name__ == "__main__": main()