refactor: retire git mirror runtime
This commit is contained in:
@@ -32,7 +32,7 @@ const defaultRegistryPrefix =
|
||||
const defaultBaseImage = process.env.HWLAB_DEV_BASE_IMAGE || null;
|
||||
const defaultReportPath = tempReportPath("dev-artifacts.json");
|
||||
const buildBackend = "buildkit";
|
||||
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
|
||||
const v02EnvReuseRuntimeMode = "env-reuse-gitea-checkout";
|
||||
const defaultBuildkitCommand = process.env.HWLAB_BUILDKIT_BUILDCTL || "buildctl-daemonless.sh";
|
||||
const defaultBuildkitAddr = process.env.HWLAB_BUILDKIT_ADDR || null;
|
||||
const defaultBuildCacheMode = process.env.HWLAB_BUILDKIT_CACHE_MODE || "registry";
|
||||
@@ -1028,7 +1028,7 @@ function envReuseDockerfile(baseImage, labels = [], recipe, service = null) {
|
||||
: `COPY ${normalizedRecipe.launcherPath} /usr/local/bin/hwlab-env-reuse-launcher.ts`,
|
||||
`RUN ${stepTimingHelpers}; timed_stage "env-readiness" sh -c ${shellQuote(readinessScript)}`,
|
||||
...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`),
|
||||
"ENV HWLAB_RUNTIME_MODE=env-reuse-git-mirror-checkout",
|
||||
"ENV HWLAB_RUNTIME_MODE=env-reuse-gitea-checkout",
|
||||
...(goCacheEnabled ? [] : [`ENV HWLAB_RUNTIME_NODE_MODULES=${normalizedRecipe.runtimeNodeModulesPath}`]),
|
||||
...goEnv,
|
||||
"ENV PATH=/usr/local/bin:$PATH",
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ function tryCreateNoDepsPlan(options) {
|
||||
affected: false,
|
||||
buildRequired: false,
|
||||
skipImageBuild: true,
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
envChanged: false,
|
||||
codeChanged: false,
|
||||
|
||||
+12
-12
@@ -505,7 +505,7 @@ test("v02 planner rolls cloud-api service code changes without rebuilding servic
|
||||
});
|
||||
const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
assert.equal(cloudApi.affected, true);
|
||||
assert.equal(cloudApi.runtimeMode, "env-reuse-git-mirror-checkout");
|
||||
assert.equal(cloudApi.runtimeMode, "env-reuse-gitea-checkout");
|
||||
assert.equal(cloudApi.envChanged, false);
|
||||
assert.equal(cloudApi.codeChanged, true);
|
||||
assert.equal(cloudApi.buildRequired, false);
|
||||
@@ -818,7 +818,7 @@ test("artifact publish ignores stale external reports for envreuse services that
|
||||
ciCdPlan: {},
|
||||
services: [{
|
||||
serviceId: "hwlab-cloud-web",
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
affected: false,
|
||||
buildRequired: false,
|
||||
@@ -846,7 +846,7 @@ test("artifact publish ignores stale external reports for envreuse services that
|
||||
imageTag: "env-old",
|
||||
digest,
|
||||
repositoryDigest: `127.0.0.1:5000/hwlab/hwlab-cloud-web-env@${digest}`,
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true
|
||||
}]
|
||||
}
|
||||
@@ -926,7 +926,7 @@ test("artifact publish accepts stale reused envreuse external reports for unchan
|
||||
ciCdPlan: {},
|
||||
services: [{
|
||||
serviceId: "hwlab-cloud-web",
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
affected: false,
|
||||
ciAffected: false,
|
||||
@@ -955,7 +955,7 @@ test("artifact publish accepts stale reused envreuse external reports for unchan
|
||||
imageTag: "env-stable",
|
||||
digest,
|
||||
repositoryDigest: `${image}@${digest}`,
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
affected: false,
|
||||
ciAffected: false,
|
||||
@@ -1290,7 +1290,7 @@ test("v02 planner marks cloud-web code-only change as env reuse rollout", async
|
||||
services: ["hwlab-cloud-web"]
|
||||
});
|
||||
const cloudWeb = plan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
assert.equal(cloudWeb.runtimeMode, "env-reuse-git-mirror-checkout");
|
||||
assert.equal(cloudWeb.runtimeMode, "env-reuse-gitea-checkout");
|
||||
assert.equal(cloudWeb.envChanged, false);
|
||||
assert.equal(cloudWeb.codeChanged, true);
|
||||
assert.equal(cloudWeb.buildRequired, false);
|
||||
@@ -1348,7 +1348,7 @@ test("v02 planner enables env reuse for all retained runtime services", async ()
|
||||
assert.deepEqual(plan.buildServices, []);
|
||||
assert.equal(plan.imageBuildRequired, false);
|
||||
for (const service of plan.services) {
|
||||
assert.equal(service.runtimeMode, "env-reuse-git-mirror-checkout", service.serviceId);
|
||||
assert.equal(service.runtimeMode, "env-reuse-gitea-checkout", service.serviceId);
|
||||
assert.equal(service.envReuse, true, service.serviceId);
|
||||
assert.equal(service.envChanged, false, service.serviceId);
|
||||
assert.equal(service.codeChanged, true, service.serviceId);
|
||||
@@ -1541,7 +1541,7 @@ test("v03 planner reads env reuse declarations and catalog from v03 lane config"
|
||||
assert.deepEqual(plan.compatibility.envReuseServiceIds, ["hwlab-cloud-api", "hwlab-cloud-web"]);
|
||||
assert.equal(plan.services.every((service) => service.envReuseRecipe.downloadStack.npmFetchTimeoutMs === 300000), true);
|
||||
for (const service of plan.services) {
|
||||
assert.equal(service.runtimeMode, "env-reuse-git-mirror-checkout", service.serviceId);
|
||||
assert.equal(service.runtimeMode, "env-reuse-gitea-checkout", service.serviceId);
|
||||
assert.equal(service.envReuse, true, service.serviceId);
|
||||
assert.equal(service.bootRepo, "git@github.com:pikasTech/HWLAB.git", service.serviceId);
|
||||
assert.equal(service.bootSh, `deploy/runtime/boot/${service.serviceId}.sh`, service.serviceId);
|
||||
@@ -1565,7 +1565,7 @@ test("v03 TaskTree API and worker share one TypeScript environment artifact", as
|
||||
assert.equal(group.services?.includes(serviceId), true, `${serviceId} shared environment membership`);
|
||||
assert.deepEqual(services[serviceId]?.runtimeReuse?.envIdentity?.paths, expectedEnvIdentity, `${serviceId} runtime env identity`);
|
||||
assert.deepEqual(services[serviceId]?.envReuse?.envIdentityFiles, expectedEnvIdentity, `${serviceId} env reuse identity`);
|
||||
assert.equal(services[serviceId]?.envReuse?.mode, "env-reuse-git-mirror-checkout", `${serviceId} runtime mode`);
|
||||
assert.equal(services[serviceId]?.envReuse?.mode, "env-reuse-gitea-checkout", `${serviceId} runtime mode`);
|
||||
}
|
||||
assert.deepEqual(services["hwlab-tasktree-api"].runtimeReuse.codeIdentity.paths, [
|
||||
"cmd/hwlab-tasktree-api",
|
||||
@@ -1874,7 +1874,7 @@ async function addGoEnvReuseFixture(repo) {
|
||||
envArtifactGroups: {
|
||||
"hwlab-go-runtime-env": {
|
||||
enabled: true,
|
||||
mode: "env-reuse-git-mirror-checkout",
|
||||
mode: "env-reuse-gitea-checkout",
|
||||
buildService: "hwlab-user-billing",
|
||||
imageRepository: "hwlab-go-runtime-env",
|
||||
cacheRepository: "cache/hwlab-go-runtime-env",
|
||||
@@ -1889,7 +1889,7 @@ async function addGoEnvReuseFixture(repo) {
|
||||
},
|
||||
envReuse: {
|
||||
enabled: true,
|
||||
mode: "env-reuse-git-mirror-checkout",
|
||||
mode: "env-reuse-gitea-checkout",
|
||||
envIdentityFiles: ["go.mod", "go.sum", "deploy/runtime/launcher/hwlab-go-env-reuse-launcher.mjs"]
|
||||
}
|
||||
}]))
|
||||
@@ -2048,7 +2048,7 @@ function createCatalogFixture(mode, options = {}) {
|
||||
componentInputHash: options.cloudApiComponentInputHash ?? options.componentInputHashes?.[serviceId]
|
||||
} : {}),
|
||||
...(envReuseCatalogServiceIds.has(serviceId) ? {
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
environmentImage: `127.0.0.1:5000/hwlab/${serviceId}-env:env-abc1234`,
|
||||
environmentDigest: digest,
|
||||
|
||||
@@ -47,10 +47,7 @@ import {
|
||||
usage,
|
||||
workbenchRuntimeRedisConfigForProfile
|
||||
} from "./src/gitops-render/core.mjs";
|
||||
import {
|
||||
devopsInfraGitMirrorManifest,
|
||||
registryManifest
|
||||
} from "./src/gitops-render/infra-manifests.mjs";
|
||||
import { registryManifest } from "./src/gitops-render/infra-manifests.mjs";
|
||||
import {
|
||||
argoApplication,
|
||||
argoProject,
|
||||
@@ -228,7 +225,6 @@ async function plannedFiles(args) {
|
||||
putJson("source.json", sourceDescriptor);
|
||||
putText("README.md", readme({ source, args, deploy }));
|
||||
putJson("registry/registry.yaml", registryManifest());
|
||||
if (isRuntimeLane(args.lane)) putJson("devops-infra/git-mirror.yaml", devopsInfraGitMirrorManifest(args, deploy));
|
||||
putJson(`${settings.tektonDir}/rbac.yaml`, tektonRbac(args, deploy));
|
||||
putJson(`${settings.tektonDir}/tasks.yaml`, { apiVersion: "v1", kind: "List", items: tektonTasks(args, deploy) });
|
||||
putJson(`${settings.tektonDir}/pipeline.yaml`, tektonPipeline(args, deploy));
|
||||
|
||||
@@ -180,11 +180,11 @@ async function runtimeNoopDecision(gitopsPromoteScript, { oldRuntime, newRuntime
|
||||
}
|
||||
}
|
||||
|
||||
test("opencode APK proxy prefers explicit gitMirror proxyUrl", async () => {
|
||||
test("opencode APK proxy prefers the lane egress proxy URL", async () => {
|
||||
assert.equal(configString(" value "), "value");
|
||||
assert.equal(opencodeEgressProxyUrlForProfile({ lanes: { v03: { gitMirror: { egressProxy: { required: true, proxyUrl: "http://10.42.0.1:10808", namespace: "platform-infra", serviceName: "jd01-host-proxy", port: 10808 } } } } }, "v03"), "http://10.42.0.1:10808");
|
||||
assert.equal(opencodeEgressProxyUrlForProfile({ lanes: { v03: { gitMirror: { egressProxy: { required: true, namespace: "platform-infra", serviceName: "sub2api-egress-proxy", port: 10808 } } } } }, "v03"), "http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808");
|
||||
assert.equal(opencodeEgressProxyUrlForProfile({ lanes: { v03: { gitMirror: { egressProxy: { required: false, proxyUrl: "http://10.42.0.1:10808" } } } } }, "v03"), "");
|
||||
assert.equal(opencodeEgressProxyUrlForProfile({ lanes: { v03: { egressProxy: { required: true, proxyUrl: "http://10.42.0.1:10808", namespace: "platform-infra", serviceName: "jd01-host-proxy", port: 10808 } } } }, "v03"), "http://10.42.0.1:10808");
|
||||
assert.equal(opencodeEgressProxyUrlForProfile({ lanes: { v03: { egressProxy: { required: true, namespace: "platform-infra", serviceName: "sub2api-egress-proxy", port: 10808 } } } }, "v03"), "http://sub2api-egress-proxy.platform-infra.svc.cluster.local:10808");
|
||||
assert.equal(opencodeEgressProxyUrlForProfile({ lanes: { v03: { egressProxy: { required: false, proxyUrl: "http://10.42.0.1:10808" } } } }, "v03"), "");
|
||||
});
|
||||
|
||||
test("v02 render follows TypeScript runtime checks and does not self-patch bootstrap RBAC", async () => {
|
||||
@@ -235,14 +235,14 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
const planArtifacts = taskByName(pipelineJson, "plan-artifacts");
|
||||
const planArtifactsScript = planArtifacts.taskSpec.steps[0].script;
|
||||
assert.match(planArtifactsScript, /node scripts\/ci\/restore-artifact-catalog\.mjs/u);
|
||||
assert.match(planArtifactsScript, /HWLAB_GIT_READ_URL="\$\(params\.git-read-url\)"/u);
|
||||
assert.match(planArtifactsScript, /HWLAB_GIT_READ_URL="\$\(params\.gitops-read-url\)"/u);
|
||||
assert.match(planArtifactsScript, /HWLAB_SERVICES="\$\(params\.services\)"/u);
|
||||
assert.match(planArtifactsScript, /rolloutWithoutImageBuildServices/u);
|
||||
assert.match(planArtifactsScript, /serviceReusedCount/u);
|
||||
assert.match(planArtifactsScript, /artifactCatalog: plan\.artifactCatalog/u);
|
||||
assert.match(planArtifactsScript, /--base-ref "\$\(params\.release-base-commit\)"/u);
|
||||
assert.match(planArtifactsScript, /verify-reviewed-plan\.mjs/u);
|
||||
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "git-read-url"));
|
||||
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "gitops-read-url"));
|
||||
assert.ok(planArtifacts.taskSpec.params.some((param) => param.name === "gitops-branch"));
|
||||
assert.ok(planArtifacts.params.some((param) => param.name === "lane" && param.value === "$(params.lane)"));
|
||||
assert.ok(pipelineJson.spec.params.some((param) => param.name === "runtime-ready-timeout-ms" && param.default === "60000"));
|
||||
@@ -272,12 +272,13 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
assert.match(gitopsPromoteScript, /ARGO_APPLICATION="hwlab-node-\$lane" ARGO_FIELD_MANAGER="hwlab-\$lane-gitops-promote"/u);
|
||||
assert.match(gitopsPromoteScript, /argocd\.argoproj\.io\/refresh": "hard"/u);
|
||||
assert.match(gitopsPromoteScript, /applications\/" \+ encodeURIComponent\(application\) \+ "\?fieldManager=" \+ encodeURIComponent\(fieldManager\)/u);
|
||||
assert.match(gitopsPromoteScript, /BatchMode=yes/u);
|
||||
assert.match(gitopsPromoteScript, /ConnectTimeout=10/u);
|
||||
assert.match(gitopsPromoteScript, /GIT_ASKPASS_REQUIRE=force/u);
|
||||
assert.match(gitopsPromoteScript, /test -s \/workspace\/gitea-auth\/token/u);
|
||||
assert.match(gitopsPromoteScript, /git_timed "source-head-\$phase" 45 git ls-remote/u);
|
||||
assert.match(gitopsPromoteScript, /"event":"git-ssh-setup","phase":"gitops-promote","status":"skipped"/u);
|
||||
assert.doesNotMatch(gitopsPromoteScript, /git-ssh-setup/u);
|
||||
assert.match(gitopsPromoteScript, /source_head_url="\$\(params\.git-read-url\)"/u);
|
||||
assert.match(gitopsPromoteScript, /gitops_write_url="\$\(params\.git-write-url\)"/u);
|
||||
assert.match(gitopsPromoteScript, /gitops_read_url="\$\(params\.gitops-read-url\)"/u);
|
||||
assert.match(gitopsPromoteScript, /git clone --no-checkout "\$gitops_read_url"/u);
|
||||
assert.match(gitopsPromoteScript, /git_timed gitops-push 120 git push origin/u);
|
||||
assert.doesNotMatch(gitopsPromoteScript, /gitops-promote-proxy-preflight/u);
|
||||
@@ -285,13 +286,14 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
|
||||
const prepareSource = taskByName(pipelineJson, "prepare-source");
|
||||
assert.ok(prepareSource.taskSpec.params.some((param) => param.name === "git-read-url"));
|
||||
assert.ok(prepareSource.taskSpec.params.some((param) => param.name === "gitops-read-url"));
|
||||
assert.ok(prepareSource.params.some((param) => param.name === "git-read-url" && param.value === "$(params.git-read-url)"));
|
||||
const prepareSourceScript = prepareSource.taskSpec.steps[0].script;
|
||||
assert.match(prepareSourceScript, /git_read_url="\$\(params\.git-read-url\)"/u);
|
||||
assert.match(prepareSourceScript, /"event":"git-ssh-setup","phase":"prepare-source","status":"skipped"/u);
|
||||
assert.doesNotMatch(prepareSourceScript, /git-ssh-setup/u);
|
||||
assert.match(prepareSourceScript, /git_timed source-clone 180 git clone --branch "\$\(params\.source-branch\)" "\$git_read_url"/u);
|
||||
assert.match(prepareSourceScript, /git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads "\$git_read_url"/u);
|
||||
assert.match(prepareSourceScript, /git remote set-url gitops-catalog "\$git_read_url"/u);
|
||||
assert.match(prepareSourceScript, /git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads "\$gitops_read_url"/u);
|
||||
assert.match(prepareSourceScript, /git remote set-url gitops-catalog "\$gitops_read_url"/u);
|
||||
assert.match(prepareSourceScript, /"event":"prepare-source-dependencies","status":"skipped","reason":"renderer-dependency-install-disabled","dependency":"yaml"/u);
|
||||
assert.doesNotMatch(prepareSourceScript, /require\.resolve\("yaml"\)/u);
|
||||
assert.doesNotMatch(prepareSourceScript, /npm ci --ignore-scripts --no-audit --prefer-offline/u);
|
||||
@@ -308,71 +310,17 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
const pipelineGitReadParam = pipelineJson.spec.params.find((param) => param.name === "git-read-url");
|
||||
assert.equal(pipelineGitReadParam?.default, "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git");
|
||||
const pipelineGitWriteParam = pipelineJson.spec.params.find((param) => param.name === "git-write-url");
|
||||
assert.equal(pipelineGitWriteParam?.default, "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
||||
assert.equal(pipelineGitWriteParam?.default, "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git");
|
||||
const pipelineGitopsReadParam = pipelineJson.spec.params.find((param) => param.name === "gitops-read-url");
|
||||
assert.equal(pipelineGitopsReadParam?.default, pipelineGitReadParam?.default);
|
||||
assert.ok(pipelineJson.spec.workspaces.some((workspace) => workspace.name === "gitea-auth"));
|
||||
assert.ok(!pipelineJson.spec.workspaces.some((workspace) => workspace.name === "git-ssh"));
|
||||
|
||||
const sourceDescriptor = JSON.parse(await readFile(path.join(outDir, "source.json"), "utf8"));
|
||||
assert.equal(sourceDescriptor.gitReadUrl, "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git");
|
||||
assert.equal(sourceDescriptor.gitWriteUrl, "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
||||
assert.equal(sourceDescriptor.gitWriteUrl, "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git");
|
||||
|
||||
const gitMirror = await readFile(path.join(outDir, "devops-infra", "git-mirror.yaml"), "utf8");
|
||||
assert.match(gitMirror, /"name": "devops-infra"/u);
|
||||
assert.match(gitMirror, /"name": "git-mirror-http"/u);
|
||||
assert.match(gitMirror, /"name": "git-mirror-write"/u);
|
||||
assert.match(gitMirror, /"name": "git-mirror-sync-script"/u);
|
||||
assert.match(gitMirror, /git-receive-pack/u);
|
||||
assert.match(gitMirror, /last-flush\.json/u);
|
||||
assert.match(gitMirror, /last-write\.json/u);
|
||||
assert.match(gitMirror, /ssh:\/\/git@ssh\.github\.com:443\/pikasTech\/HWLAB\.git/u);
|
||||
assert.doesNotMatch(gitMirror, /"kind": "CronJob"/u);
|
||||
assert.doesNotMatch(gitMirror, /"name": "git-mirror-hwlab-sync"/u);
|
||||
const gitMirrorJson = JSON.parse(gitMirror);
|
||||
const gitMirrorConfigMap = gitMirrorJson.items.find((item) => item.kind === "ConfigMap" && item.metadata?.name === "git-mirror-sync-script");
|
||||
const gitMirrorScripts = gitMirrorConfigMap?.data ?? {};
|
||||
const gitMirrorScript = gitMirrorScripts["sync.sh"] ?? "";
|
||||
assert.match(gitMirrorScript, /uploadpack\.allowReachableSHA1InWant true/u);
|
||||
assert.match(gitMirrorScript, /uploadpack\.allowAnySHA1InWant true/u);
|
||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.2:refs\/mirror-stage\/heads\/v0\.2/u);
|
||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.2-gitops:refs\/mirror-stage\/heads\/v0\.2-gitops/u);
|
||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.3:refs\/mirror-stage\/heads\/v0\.3/u);
|
||||
assert.match(gitMirrorScript, /refs\/heads\/v0\.3-gitops:refs\/mirror-stage\/heads\/v0\.3-gitops/u);
|
||||
assert.match(gitMirrorScript, /refs\/heads\/release:refs\/mirror-stage\/heads\/release/u);
|
||||
assert.match(gitMirrorScript, /refs\/heads\/release-gitops:refs\/mirror-stage\/heads\/release-gitops/u);
|
||||
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14:refs\/mirror-stage\/heads\/G14/u);
|
||||
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/G14-gitops:refs\/mirror-stage\/heads\/G14-gitops/u);
|
||||
assert.doesNotMatch(gitMirrorScript, /refs\/heads\/\*:refs\/mirror-stage\/heads\/\*/u);
|
||||
assert.match(gitMirrorScript, /git -C "\$repo_path" update-ref "refs\/heads\/\$name" "\$sha"/u);
|
||||
assert.match(gitMirrorScript, /source_snapshot_ref_prefix='refs\/unidesk\/snapshots\/hwlab-node-runtime'/u);
|
||||
assert.match(gitMirrorScript, /source_stage_ref="\$\{source_snapshot_ref_prefix%\/\}\/\$source_branch\/\$source_sha"/u);
|
||||
assert.match(gitMirrorScript, /git -C "\$repo_path" update-ref "\$source_stage_ref" "\$source_sha"/u);
|
||||
assert.match(gitMirrorScript, /git-mirror-sync-timing/u);
|
||||
assert.match(gitMirrorScript, /emit_timing fetch succeeded/u);
|
||||
assert.match(gitMirrorScript, /emit_timing total succeeded/u);
|
||||
assert.match(gitMirrorScript, /sourceInSync/u);
|
||||
assert.match(gitMirrorScript, /sourceSnapshots/u);
|
||||
assert.match(gitMirrorScript, /refs\/unidesk\/snapshots\/hwlab-node-runtime/u);
|
||||
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.2/u);
|
||||
assert.match(gitMirrorScript, /refs\/mirror-stage\/heads\/v0\.3/u);
|
||||
assert.match(gitMirrorScript, /for gitops_branch in 'release-gitops' 'v0\.2-gitops' 'v0\.3-gitops'/u);
|
||||
assert.match(gitMirrorScript, /keeps local \$name ahead of GitHub/u);
|
||||
assert.doesNotMatch(gitMirror, /not allowlisted/u);
|
||||
assert.doesNotMatch(gitMirror, /path allowlist/u);
|
||||
assert.doesNotMatch(gitMirror, /outside .* GitOps outputs/u);
|
||||
assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "Secret"));
|
||||
assert.ok((gitMirrorJson.items || []).every((item) => item.kind !== "CronJob"));
|
||||
assert.ok((gitMirrorJson.items || []).every((item) => !item.data?.["ssh-privatekey"]));
|
||||
|
||||
const scriptCheckDir = path.join(outDir, "script-check");
|
||||
await mkdir(scriptCheckDir, { recursive: true });
|
||||
for (const scriptName of ["http.mjs", "sync.sh", "install-hooks.sh", "flush.sh"]) {
|
||||
assert.ok(gitMirrorScripts[scriptName], `missing git mirror script ${scriptName}`);
|
||||
await writeFile(path.join(scriptCheckDir, scriptName), gitMirrorScripts[scriptName]);
|
||||
}
|
||||
const httpCheck = spawnSync("node", ["--check", path.join(scriptCheckDir, "http.mjs")], { encoding: "utf8" });
|
||||
assert.equal(httpCheck.status, 0, httpCheck.stderr || httpCheck.stdout);
|
||||
for (const scriptName of ["sync.sh", "install-hooks.sh", "flush.sh"]) {
|
||||
const shellCheck = spawnSync("sh", ["-n", path.join(scriptCheckDir, scriptName)], { encoding: "utf8" });
|
||||
assert.equal(shellCheck.status, 0, `${scriptName}: ${shellCheck.stderr || shellCheck.stdout}`);
|
||||
}
|
||||
await assert.rejects(() => readFile(path.join(outDir, "devops-infra", "git-mirror.yaml"), "utf8"), { code: "ENOENT" });
|
||||
|
||||
const removedServiceIds = [
|
||||
"hwlab-agent-mgr",
|
||||
@@ -501,11 +449,11 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
const cloudApiEnv = cloudApiContainer.env;
|
||||
const cloudApiCommit = cloudApiEnv.find((entry) => entry.name === "HWLAB_COMMIT_ID")?.value;
|
||||
const cloudApiBootEnv = new Map(cloudApiEnv.map((entry) => [entry.name, entry.value]));
|
||||
assert.equal(cloudApiBootEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-git-mirror-checkout");
|
||||
assert.equal(cloudApiBootEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-gitea-checkout");
|
||||
assert.equal(cloudApiBootEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/hwlab-cloud-api.sh");
|
||||
const codexForwarderEnv = new Map(codexForwarder.env.map((entry) => [entry.name, entry.value]));
|
||||
assert.equal(codexForwarder.command, undefined);
|
||||
assert.equal(codexForwarderEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-git-mirror-checkout");
|
||||
assert.equal(codexForwarderEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-gitea-checkout");
|
||||
assert.equal(codexForwarderEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/hwlab-codex-api-responses-forwarder.sh");
|
||||
assert.equal(codexForwarderEnv.get("HWLAB_BOOT_COMMIT"), cloudApiCommit);
|
||||
assert.equal(codexForwarderEnv.get("HWLAB_BOOT_READ_URL"), "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git");
|
||||
@@ -527,7 +475,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
assert.ok(deepseekResponsesBridge, "expected DeepSeek responses bridge container");
|
||||
const deepseekBridgeEnv = new Map(deepseekResponsesBridge.env.map((entry) => [entry.name, entry.value]));
|
||||
assert.equal(deepseekResponsesBridge.command, undefined);
|
||||
assert.equal(deepseekBridgeEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-git-mirror-checkout");
|
||||
assert.equal(deepseekBridgeEnv.get("HWLAB_RUNTIME_MODE"), "env-reuse-gitea-checkout");
|
||||
assert.equal(deepseekBridgeEnv.get("HWLAB_BOOT_SH"), "deploy/runtime/boot/hwlab-deepseek-responses-bridge.sh");
|
||||
assert.equal(deepseekBridgeEnv.get("HWLAB_BOOT_COMMIT"), cloudApiCommit);
|
||||
assert.equal(deepseekBridgeEnv.get("HWLAB_BOOT_READ_URL"), "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git");
|
||||
@@ -620,7 +568,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots
|
||||
|
||||
const pipelineRunSample = await readFile(path.join(outDir, "tekton-v02", "pipelinerun.sample.yaml"), "utf8");
|
||||
assert.match(pipelineRunSample, /"name": "git-read-url"[\s\S]{0,180}gitea-http\.devops-infra\.svc\.cluster\.local:3000\/mirrors\/pikasTech-HWLAB\.git/u);
|
||||
assert.match(pipelineRunSample, /"name": "git-write-url"[\s\S]{0,140}git-mirror-write\.devops-infra\.svc\.cluster\.local/u);
|
||||
assert.match(pipelineRunSample, /"name": "git-write-url"[\s\S]{0,180}gitea-http\.devops-infra\.svc\.cluster\.local:3000\/mirrors\/pikasTech-HWLAB\.git/u);
|
||||
assert.match(pipelineRunSample, /"name": "revision"[\s\S]{0,80}"value": "[0-9a-f]{40}"/u);
|
||||
assert.match(pipelineRunSample, /"name": "source-branch"[\s\S]{0,80}"value": "v0\.2"/u);
|
||||
} finally {
|
||||
@@ -648,7 +596,7 @@ test("v03 render keeps node identity as data instead of generated structure", as
|
||||
artifactState: "ready",
|
||||
services: [{
|
||||
serviceId: "hwlab-cloud-api",
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
sourceCommitId: staleRevision,
|
||||
commitId: staleRevision,
|
||||
@@ -848,7 +796,7 @@ test("v03 render includes JD01 secret-plane smoke on JD01 gitops root", { timeou
|
||||
artifactState: "ready",
|
||||
services: [{
|
||||
serviceId: "hwlab-cloud-api",
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
sourceCommitId: staleRevision,
|
||||
commitId: staleRevision,
|
||||
|
||||
@@ -26,7 +26,7 @@ const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
const runtimeArtifactLanePattern = /^[a-z][a-z0-9-]*$/u;
|
||||
|
||||
const publishReadyStatuses = new Set(["published", "reused"]);
|
||||
const v02EnvReuseRuntimeMode = "env-reuse-git-mirror-checkout";
|
||||
const v02EnvReuseRuntimeMode = "env-reuse-gitea-checkout";
|
||||
|
||||
function isRuntimeArtifactLane(lane) {
|
||||
return runtimeArtifactLanePattern.test(String(lane ?? ""));
|
||||
|
||||
@@ -431,7 +431,7 @@ test("v03 refresh accepts runtime lane config and env-reuse image tags", async (
|
||||
serviceId,
|
||||
status: "published",
|
||||
artifactRequired: true,
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
runtimeMode: "env-reuse-gitea-checkout",
|
||||
envReuse: true,
|
||||
commitId,
|
||||
sourceCommitId: commitId,
|
||||
@@ -476,7 +476,7 @@ test("v03 refresh accepts runtime lane config and env-reuse image tags", async (
|
||||
assert.equal(payload.endpoint, "https://hwlab-v03.74-48-78-17.nip.io");
|
||||
assert.equal(payload.requiredServiceCount, serviceIds.length);
|
||||
assert.deepEqual(payload.services.map((service) => service.serviceId), serviceIds);
|
||||
assert.equal(payload.services.every((service) => service.runtimeMode === "env-reuse-git-mirror-checkout"), true);
|
||||
assert.equal(payload.services.every((service) => service.runtimeMode === "env-reuse-gitea-checkout"), true);
|
||||
assert.equal(payload.services.every((service) => service.imageTag.startsWith("env-")), true);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { parseStructuredConfig, readStructuredFileIfPresent } from "./structured
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
export const CI_PLAN_VERSION = "v1";
|
||||
export const ENV_REUSE_RUNTIME_MODE = "env-reuse-git-mirror-checkout";
|
||||
export const ENV_REUSE_RUNTIME_MODE = "env-reuse-gitea-checkout";
|
||||
export const V02_ENV_REUSE_RUNTIME_MODE = ENV_REUSE_RUNTIME_MODE;
|
||||
|
||||
export const DEFAULT_RUNTIME_DEP_PATHS = Object.freeze([
|
||||
|
||||
@@ -29,10 +29,10 @@ const defaultOutDir = "deploy/gitops/node";
|
||||
const defaultRegistryPrefix = process.env.HWLAB_NODE_REGISTRY_PREFIX || "127.0.0.1:5000/hwlab";
|
||||
const defaultSourceRepo = process.env.HWLAB_NODE_GIT_URL || "git@github.com:pikasTech/HWLAB.git";
|
||||
const defaultGitReadUrl = process.env.HWLAB_NODE_GIT_READ_URL || null;
|
||||
const defaultV02GitReadUrl = process.env.HWLAB_V02_GIT_READ_URL || "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
|
||||
const defaultV02GitWriteUrl = process.env.HWLAB_V02_GIT_WRITE_URL || "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/HWLAB.git";
|
||||
const defaultV02GitReadUrl = process.env.HWLAB_V02_GIT_READ_URL || "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git";
|
||||
const defaultV02GitWriteUrl = process.env.HWLAB_V02_GIT_WRITE_URL || defaultV02GitReadUrl;
|
||||
const defaultRuntimeLaneGitReadUrl = process.env.HWLAB_RUNTIME_LANE_GIT_READ_URL || "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-HWLAB.git";
|
||||
const defaultRuntimeLaneGitWriteUrl = process.env.HWLAB_RUNTIME_LANE_GIT_WRITE_URL || defaultV02GitWriteUrl;
|
||||
const defaultRuntimeLaneGitWriteUrl = process.env.HWLAB_RUNTIME_LANE_GIT_WRITE_URL || defaultRuntimeLaneGitReadUrl;
|
||||
const defaultBranch = process.env.HWLAB_NODE_BRANCH || "node";
|
||||
const defaultGitopsBranch = process.env.HWLAB_NODE_GITOPS_BRANCH || "node-gitops";
|
||||
const defaultCatalogPath = process.env.HWLAB_ARTIFACT_CATALOG_PATH || "deploy/artifact-catalog.dev.json";
|
||||
@@ -44,7 +44,7 @@ const defaultV02RuntimeEndpoint = process.env.HWLAB_V02_RUNTIME_ENDPOINT || "htt
|
||||
const defaultV02WebEndpoint = process.env.HWLAB_V02_WEB_ENDPOINT || "https://hwlab.74-48-78-17.nip.io";
|
||||
const defaultProxyUrl = process.env.HWLAB_NODE_PROXY_URL || "http://127.0.0.1:10808";
|
||||
const defaultAllProxyUrl = process.env.HWLAB_NODE_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
|
||||
const defaultNoProxy = process.env.HWLAB_NODE_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,git-mirror-http,git-mirror-http.devops-infra,git-mirror-http.devops-infra.svc,git-mirror-http.devops-infra.svc.cluster.local,git-mirror-write,git-mirror-write.devops-infra,git-mirror-write.devops-infra.svc,git-mirror-write.devops-infra.svc.cluster.local";
|
||||
const defaultNoProxy = process.env.HWLAB_NODE_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc,gitea-http,gitea-http.devops-infra,gitea-http.devops-infra.svc,gitea-http.devops-infra.svc.cluster.local";
|
||||
const ciToolsRunnerImage = process.env.HWLAB_NODE_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1";
|
||||
const buildkitRunnerImage = process.env.HWLAB_NODE_BUILDKIT_IMAGE || "moby/buildkit:rootless";
|
||||
const defaultDevBaseImage = process.env.HWLAB_NODE_DEV_BASE_IMAGE || "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
ciToolsRunnerImage,
|
||||
configObject,
|
||||
defaultOutDir,
|
||||
label,
|
||||
optionalConfigString,
|
||||
requiredConfigPositiveInteger,
|
||||
requiredConfigString,
|
||||
runtimeLaneMirrorSpecs
|
||||
} from "./core.mjs";
|
||||
import { renderTemplate, shellSingleQuote } from "./tekton-scripts.mjs";
|
||||
|
||||
function registryManifest() {
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
@@ -55,200 +41,6 @@ function registryManifest() {
|
||||
};
|
||||
}
|
||||
|
||||
function uniqueStrings(values) {
|
||||
return Array.from(new Set(values.filter((value) => typeof value === "string" && value.length > 0)));
|
||||
}
|
||||
|
||||
function renderGitMirrorSshPrelude({ direct, proxySummary, proxyCommand, proxyUrl, proxyNoProxy }) {
|
||||
if (direct) {
|
||||
return renderTemplate("git-mirror-ssh-direct.sh", {
|
||||
"__HWLAB_PROXY_SUMMARY_SHELL__": shellSingleQuote(proxySummary)
|
||||
});
|
||||
}
|
||||
return renderTemplate("git-mirror-ssh-proxied.sh", {
|
||||
"// __HWLAB_GIT_MIRROR_PROXY_CONNECT_MJS__": renderTemplate("git-mirror-proxy-connect.mjs"),
|
||||
"__HWLAB_PROXY_SUMMARY_SHELL__": shellSingleQuote(proxySummary),
|
||||
"__HWLAB_PROXY_COMMAND_SHELL__": shellSingleQuote(proxyCommand),
|
||||
"__HWLAB_PROXY_URL_SHELL__": shellSingleQuote(proxyUrl),
|
||||
"__HWLAB_PROXY_NO_PROXY_SHELL__": shellSingleQuote(proxyNoProxy)
|
||||
});
|
||||
}
|
||||
|
||||
function gitMirrorConfigScripts({
|
||||
fetchConfigCommands,
|
||||
gitMirrorSshPrelude,
|
||||
gitopsBranchArgs,
|
||||
gitopsBranchesJson,
|
||||
mirrorBranchesJson,
|
||||
requiredRefArgs,
|
||||
sourceBranchArgs,
|
||||
sourceBranchesJson,
|
||||
sourceSnapshotRefPrefix
|
||||
}) {
|
||||
const gitopsBranchesJsonShell = shellSingleQuote(gitopsBranchesJson);
|
||||
return {
|
||||
"sync.sh": renderTemplate("git-mirror-sync.sh", {
|
||||
"# __HWLAB_GIT_MIRROR_SSH_PRELUDE__": gitMirrorSshPrelude,
|
||||
"__HWLAB_SOURCE_SNAPSHOT_REF_PREFIX_SHELL__": shellSingleQuote(sourceSnapshotRefPrefix),
|
||||
"# __HWLAB_FETCH_CONFIG_COMMANDS__": fetchConfigCommands,
|
||||
"__HWLAB_REQUIRED_REF_ARGS__": requiredRefArgs,
|
||||
"__HWLAB_SOURCE_BRANCH_ARGS__": sourceBranchArgs,
|
||||
"__HWLAB_GITOPS_BRANCH_ARGS__": gitopsBranchArgs,
|
||||
"__HWLAB_MIRROR_BRANCHES_JSON_SHELL__": shellSingleQuote(mirrorBranchesJson),
|
||||
"__HWLAB_SOURCE_BRANCHES_JSON_SHELL__": shellSingleQuote(sourceBranchesJson),
|
||||
"__HWLAB_GITOPS_BRANCHES_JSON_SHELL__": gitopsBranchesJsonShell
|
||||
}),
|
||||
"install-hooks.sh": renderTemplate("git-mirror-install-hooks.sh", {
|
||||
"__HWLAB_GITOPS_BRANCHES_JSON_SHELL__": gitopsBranchesJsonShell
|
||||
}),
|
||||
"flush.sh": renderTemplate("git-mirror-flush.sh", {
|
||||
"# __HWLAB_GIT_MIRROR_SSH_PRELUDE__": gitMirrorSshPrelude,
|
||||
"__HWLAB_GITOPS_BRANCH_ARGS__": gitopsBranchArgs,
|
||||
"__HWLAB_GITOPS_BRANCHES_JSON_SHELL__": gitopsBranchesJsonShell
|
||||
}),
|
||||
"http.mjs": renderTemplate("git-mirror-http.mjs")
|
||||
};
|
||||
}
|
||||
|
||||
function devopsInfraGitMirrorManifest(args, deploy) {
|
||||
const mirrorSpecs = runtimeLaneMirrorSpecs(deploy, { defaultNodeId: args.nodeId, defaultGitopsRoot: defaultOutDir });
|
||||
assert.ok(mirrorSpecs.length > 0, "runtime lane git mirror requires at least one deploy.lanes entry");
|
||||
const sourceBranches = uniqueStrings(mirrorSpecs.map((spec) => spec.sourceBranch));
|
||||
const gitopsBranches = uniqueStrings(mirrorSpecs.map((spec) => spec.gitopsBranch));
|
||||
const mirrorBranches = uniqueStrings([...sourceBranches, ...gitopsBranches]);
|
||||
const fetchConfigCommands = mirrorBranches
|
||||
.map((branch) => `git -C "$repo_path" config --add remote.origin.fetch '+refs/heads/${branch}:refs/mirror-stage/heads/${branch}'`)
|
||||
.join("\n");
|
||||
const requiredRefArgs = mirrorBranches.map((branch) => shellSingleQuote(`refs/mirror-stage/heads/${branch}`)).join(" ");
|
||||
const sourceBranchArgs = sourceBranches.map((branch) => shellSingleQuote(branch)).join(" ");
|
||||
const gitopsBranchArgs = gitopsBranches.map((branch) => shellSingleQuote(branch)).join(" ");
|
||||
const mirrorBranchesJson = JSON.stringify(mirrorBranches);
|
||||
const sourceBranchesJson = JSON.stringify(sourceBranches);
|
||||
const gitopsBranchesJson = JSON.stringify(gitopsBranches);
|
||||
const sourceSnapshotRefPrefix = "refs/unidesk/snapshots/hwlab-node-runtime";
|
||||
const laneConfig = configObject(configObject(deploy.lanes, "deploy.lanes")[args.lane], `deploy.lanes.${args.lane}`);
|
||||
const gitMirrorConfig = configObject(laneConfig.gitMirror, `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorProxy = configObject(gitMirrorConfig.egressProxy, `deploy.lanes.${args.lane}.gitMirror.egressProxy`);
|
||||
const gitMirrorProxyMode = requiredConfigString(gitMirrorProxy, "mode", `deploy.lanes.${args.lane}.gitMirror.egressProxy`);
|
||||
assert.ok(gitMirrorProxyMode === "node-global" || gitMirrorProxyMode === "host-proxy-client" || gitMirrorProxyMode === "direct", `deploy.lanes.${args.lane}.gitMirror.egressProxy.mode must be node-global, host-proxy-client, or direct`);
|
||||
const useDirectGitMirrorProxy = gitMirrorProxyMode === "direct";
|
||||
const useHostGitMirrorProxy = gitMirrorProxyMode === "host-proxy-client";
|
||||
const gitMirrorNamespace = requiredConfigString(gitMirrorConfig, "namespace", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorReadName = requiredConfigString(gitMirrorConfig, "serviceReadName", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorWriteName = requiredConfigString(gitMirrorConfig, "serviceWriteName", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorCachePvcName = requiredConfigString(gitMirrorConfig, "cachePvcName", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorCachePvcStorage = requiredConfigString(gitMirrorConfig, "cachePvcStorage", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorCacheHostPath = optionalConfigString(gitMirrorConfig, "cacheHostPath", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorServicePort = requiredConfigPositiveInteger(gitMirrorConfig, "servicePort", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const gitMirrorConfigMapName = requiredConfigString(gitMirrorConfig, "syncConfigMapName", `deploy.lanes.${args.lane}.gitMirror`);
|
||||
const proxyNamespace = useDirectGitMirrorProxy || useHostGitMirrorProxy ? "" : requiredConfigString(gitMirrorProxy, "namespace", `deploy.lanes.${args.lane}.gitMirror.egressProxy`);
|
||||
const proxyServiceName = useDirectGitMirrorProxy || useHostGitMirrorProxy ? "" : requiredConfigString(gitMirrorProxy, "serviceName", `deploy.lanes.${args.lane}.gitMirror.egressProxy`);
|
||||
const proxyPort = useDirectGitMirrorProxy ? 0 : requiredConfigPositiveInteger(gitMirrorProxy, "port", `deploy.lanes.${args.lane}.gitMirror.egressProxy`);
|
||||
const proxyClientName = useDirectGitMirrorProxy ? "" : requiredConfigString(gitMirrorProxy, "clientName", `deploy.lanes.${args.lane}.gitMirror.egressProxy`);
|
||||
const proxyNoProxy = !useDirectGitMirrorProxy && Array.isArray(gitMirrorProxy.noProxy) ? gitMirrorProxy.noProxy.filter((value) => typeof value === "string" && value.length > 0).join(",") : "";
|
||||
const proxyHost = useHostGitMirrorProxy
|
||||
? requiredConfigString(gitMirrorProxy, "host", `deploy.lanes.${args.lane}.gitMirror.egressProxy`)
|
||||
: `${proxyServiceName}.${proxyNamespace}.svc.cluster.local`;
|
||||
const proxyUrl = `http://${proxyHost}:${proxyPort}`;
|
||||
const proxySummary = useDirectGitMirrorProxy
|
||||
? "git-mirror-egress-proxy mode=direct required=false transport=ssh source=yaml"
|
||||
: `git-mirror-egress-proxy client=${proxyClientName} mode=${gitMirrorProxyMode} required=true host=${proxyHost} port=${proxyPort} ssh=GIT_SSH-wrapper source=yaml`;
|
||||
const proxyCommand = useDirectGitMirrorProxy ? "" : `ProxyCommand=node /tmp/hwlab-github-proxy-connect.mjs ${proxyHost} ${proxyPort} %h %p`;
|
||||
const gitMirrorSshPrelude = renderGitMirrorSshPrelude({
|
||||
direct: useDirectGitMirrorProxy,
|
||||
proxySummary,
|
||||
proxyCommand,
|
||||
proxyUrl,
|
||||
proxyNoProxy
|
||||
});
|
||||
const cacheVolume = gitMirrorCacheHostPath === null
|
||||
? { name: "cache", persistentVolumeClaim: { claimName: gitMirrorCachePvcName } }
|
||||
: { name: "cache", hostPath: { path: gitMirrorCacheHostPath, type: "DirectoryOrCreate" } };
|
||||
const labels = {
|
||||
"app.kubernetes.io/part-of": "hwlab-node-control-plane",
|
||||
"hwlab.pikastech.local/node": args.nodeId,
|
||||
"hwlab.pikastech.local/lane": args.lane
|
||||
};
|
||||
const configLabels = { ...labels, "app.kubernetes.io/name": "git-mirror" };
|
||||
const readLabels = { ...labels, "app.kubernetes.io/name": gitMirrorReadName, "hwlab.pikastech.local/git-mirror-mode": "read" };
|
||||
const writeLabels = { ...labels, "app.kubernetes.io/name": gitMirrorWriteName, "hwlab.pikastech.local/git-mirror-mode": "write" };
|
||||
return {
|
||||
apiVersion: "v1",
|
||||
kind: "List",
|
||||
items: [
|
||||
{ apiVersion: "v1", kind: "Namespace", metadata: { name: gitMirrorNamespace } },
|
||||
...(gitMirrorCacheHostPath === null ? [{
|
||||
apiVersion: "v1",
|
||||
kind: "PersistentVolumeClaim",
|
||||
metadata: { name: gitMirrorCachePvcName, namespace: gitMirrorNamespace, labels: configLabels },
|
||||
spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: gitMirrorCachePvcStorage } } }
|
||||
}] : []),
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "ConfigMap",
|
||||
metadata: { name: gitMirrorConfigMapName, namespace: gitMirrorNamespace, labels: configLabels },
|
||||
data: gitMirrorConfigScripts({
|
||||
fetchConfigCommands,
|
||||
gitMirrorSshPrelude,
|
||||
gitopsBranchArgs,
|
||||
gitopsBranchesJson,
|
||||
mirrorBranchesJson,
|
||||
requiredRefArgs,
|
||||
sourceBranchArgs,
|
||||
sourceBranchesJson,
|
||||
sourceSnapshotRefPrefix
|
||||
})
|
||||
},
|
||||
{
|
||||
apiVersion: "apps/v1",
|
||||
kind: "Deployment",
|
||||
metadata: { name: gitMirrorReadName, namespace: gitMirrorNamespace, labels: readLabels },
|
||||
spec: {
|
||||
replicas: 1,
|
||||
selector: { matchLabels: { "app.kubernetes.io/name": gitMirrorReadName } },
|
||||
template: {
|
||||
metadata: { labels: readLabels },
|
||||
spec: {
|
||||
containers: [{
|
||||
name: "git-mirror",
|
||||
image: ciToolsRunnerImage,
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
command: ["node"],
|
||||
args: ["/etc/git-mirror/http.mjs"],
|
||||
ports: [{ name: "http", containerPort: 8080 }],
|
||||
readinessProbe: { httpGet: { path: "/pikasTech/HWLAB.git/HEAD", port: "http" }, initialDelaySeconds: 5, periodSeconds: 10 },
|
||||
livenessProbe: { httpGet: { path: "/", port: "http" }, initialDelaySeconds: 10, periodSeconds: 30 },
|
||||
volumeMounts: [
|
||||
{ name: "cache", mountPath: "/cache" },
|
||||
{ name: "config", mountPath: "/etc/git-mirror", readOnly: true }
|
||||
]
|
||||
}],
|
||||
volumes: [
|
||||
cacheVolume,
|
||||
{ name: "config", configMap: { name: gitMirrorConfigMapName, defaultMode: 0o555 } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "Service",
|
||||
metadata: { name: gitMirrorReadName, namespace: gitMirrorNamespace, labels: readLabels },
|
||||
spec: { selector: { "app.kubernetes.io/name": gitMirrorReadName }, ports: [{ name: "http", port: gitMirrorServicePort, targetPort: "http" }] }
|
||||
},
|
||||
{
|
||||
apiVersion: "v1",
|
||||
kind: "Service",
|
||||
metadata: { name: gitMirrorWriteName, namespace: gitMirrorNamespace, labels: writeLabels },
|
||||
spec: { selector: { "app.kubernetes.io/name": gitMirrorReadName }, ports: [{ name: "http", port: gitMirrorServicePort, targetPort: "http" }] }
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
devopsInfraGitMirrorManifest,
|
||||
registryManifest,
|
||||
uniqueStrings
|
||||
registryManifest
|
||||
};
|
||||
|
||||
@@ -1237,7 +1237,7 @@ function opencodeConfigBoolean(value, fallback, label) {
|
||||
}
|
||||
|
||||
function opencodeEgressProxyUrlForProfile(deploy, profile) {
|
||||
const proxy = deploy?.lanes?.[profile]?.gitMirror?.egressProxy;
|
||||
const proxy = deploy?.lanes?.[profile]?.egressProxy;
|
||||
if (!proxy || proxy.required === false) return "";
|
||||
const proxyUrl = configString(proxy.proxyUrl);
|
||||
if (proxyUrl) return proxyUrl;
|
||||
@@ -1249,7 +1249,7 @@ function opencodeEgressProxyUrlForProfile(deploy, profile) {
|
||||
}
|
||||
|
||||
function opencodeEgressNoProxyForProfile(deploy, profile) {
|
||||
const proxy = deploy?.lanes?.[profile]?.gitMirror?.egressProxy;
|
||||
const proxy = deploy?.lanes?.[profile]?.egressProxy;
|
||||
const entries = Array.isArray(proxy?.noProxy) ? proxy.noProxy.map(configString).filter(Boolean) : [];
|
||||
return entries.length > 0 ? entries.join(",") : "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local";
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
|
||||
taskSpec: {
|
||||
params: [
|
||||
{ name: "git-url" },
|
||||
{ name: "git-read-url" },
|
||||
{ name: "gitops-read-url" },
|
||||
{ name: "gitops-branch" },
|
||||
{ name: "lane" },
|
||||
{ name: "revision" },
|
||||
@@ -338,7 +338,7 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
|
||||
},
|
||||
params: [
|
||||
{ name: "git-url", value: "$(params.git-url)" },
|
||||
{ name: "git-read-url", value: "$(params.git-read-url)" },
|
||||
{ name: "gitops-read-url", value: "$(params.gitops-read-url)" },
|
||||
{ name: "gitops-branch", value: "$(params.gitops-branch)" },
|
||||
{ name: "lane", value: "$(params.lane)" },
|
||||
{ name: "revision", value: "$(params.revision)" },
|
||||
@@ -516,11 +516,11 @@ function tektonTasks(args = { lane: "node" }, deploy = null) {
|
||||
name: "prepare-source",
|
||||
taskSpec: {
|
||||
params: [
|
||||
{ name: "git-url" }, { name: "git-read-url" }, { name: "source-branch" },
|
||||
{ name: "git-url" }, { name: "git-read-url" }, { name: "gitops-read-url" }, { name: "source-branch" },
|
||||
{ name: "gitops-branch" }, { name: "lane" }, { name: "catalog-path" },
|
||||
{ name: "image-tag-mode" }, { name: "registry-prefix" }, { name: "services" }, { name: "revision" }
|
||||
],
|
||||
workspaces: [{ name: "source" }, { name: "git-ssh" }],
|
||||
workspaces: [{ name: "source" }],
|
||||
steps: [{ name: "prepare-source", image: ciToolsRunnerImage, env: proxyEnv(), script: prepareSourceScript() }]
|
||||
}
|
||||
};
|
||||
@@ -532,13 +532,13 @@ function tektonTasks(args = { lane: "node" }, deploy = null) {
|
||||
name: "gitops-promote",
|
||||
taskSpec: {
|
||||
params: [
|
||||
{ name: "git-url" }, { name: "git-read-url" }, { name: "git-write-url" },
|
||||
{ name: "git-url" }, { name: "git-read-url" }, { name: "gitops-read-url" }, { name: "git-write-url" }, { name: "gitops-username" },
|
||||
{ name: "source-branch" }, { name: "gitops-branch" }, { name: "lane" },
|
||||
{ name: "catalog-path" }, { name: "image-tag-mode" }, { name: "runtime-path" },
|
||||
{ name: "revision" }, { name: "registry-prefix" }
|
||||
],
|
||||
results: [{ name: "runtime-ready-required", description: "true when GitOps promotion changed runtime desired state and runtime readiness must be observed" }],
|
||||
workspaces: [{ name: "source" }, { name: "git-ssh" }],
|
||||
workspaces: [{ name: "source" }, { name: "gitea-auth" }],
|
||||
steps: [{ name: "promote", image: ciToolsRunnerImage, env: proxyEnv(), script: gitopsPromoteScript() }]
|
||||
}
|
||||
},
|
||||
@@ -576,7 +576,9 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
params: [
|
||||
{ name: "git-url", type: "string", default: defaultSourceRepo },
|
||||
{ name: "git-read-url", type: "string", default: args.gitReadUrl },
|
||||
{ name: "gitops-read-url", type: "string", default: args.gitReadUrl },
|
||||
{ name: "git-write-url", type: "string", default: args.gitWriteUrl },
|
||||
{ name: "gitops-username", type: "string", default: "unidesk-admin" },
|
||||
{ name: "source-branch", type: "string", default: args.sourceBranch },
|
||||
{ name: "gitops-branch", type: "string", default: args.gitopsBranch },
|
||||
{ name: "lane", type: "string", default: settings.lane },
|
||||
@@ -596,19 +598,19 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
],
|
||||
workspaces: [
|
||||
{ name: "source" },
|
||||
{ name: "git-ssh" }
|
||||
{ name: "gitea-auth" }
|
||||
],
|
||||
tasks: [
|
||||
{
|
||||
name: "prepare-source",
|
||||
workspaces: [
|
||||
{ name: "source", workspace: "source" },
|
||||
{ name: "git-ssh", workspace: "git-ssh" }
|
||||
{ name: "source", workspace: "source" }
|
||||
],
|
||||
taskSpec: {
|
||||
params: [
|
||||
{ name: "git-url" },
|
||||
{ name: "git-read-url" },
|
||||
{ name: "gitops-read-url" },
|
||||
{ name: "source-branch" },
|
||||
{ name: "gitops-branch" },
|
||||
{ name: "lane" },
|
||||
@@ -618,12 +620,13 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
{ name: "services" },
|
||||
{ name: "revision" }
|
||||
],
|
||||
workspaces: [{ name: "source" }, { name: "git-ssh" }],
|
||||
workspaces: [{ name: "source" }],
|
||||
steps: [{ name: "prepare-source", image: ciToolsRunnerImage, env: proxyEnv(), script: prepareSourceScript() }]
|
||||
},
|
||||
params: [
|
||||
{ name: "git-url", value: "$(params.git-url)" },
|
||||
{ name: "git-read-url", value: "$(params.git-read-url)" },
|
||||
{ name: "gitops-read-url", value: "$(params.gitops-read-url)" },
|
||||
{ name: "source-branch", value: "$(params.source-branch)" },
|
||||
{ name: "gitops-branch", value: "$(params.gitops-branch)" },
|
||||
{ name: "lane", value: "$(params.lane)" },
|
||||
@@ -641,13 +644,15 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
runAfter: ["collect-artifacts"],
|
||||
workspaces: [
|
||||
{ name: "source", workspace: "source" },
|
||||
{ name: "git-ssh", workspace: "git-ssh" }
|
||||
{ name: "gitea-auth", workspace: "gitea-auth" }
|
||||
],
|
||||
taskSpec: {
|
||||
params: [
|
||||
{ name: "git-url" },
|
||||
{ name: "git-read-url" },
|
||||
{ name: "gitops-read-url" },
|
||||
{ name: "git-write-url" },
|
||||
{ name: "gitops-username" },
|
||||
{ name: "source-branch" },
|
||||
{ name: "gitops-branch" },
|
||||
{ name: "lane" },
|
||||
@@ -658,13 +663,15 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
|
||||
{ name: "registry-prefix" }
|
||||
],
|
||||
results: [{ name: "runtime-ready-required", description: "true when GitOps promotion changed runtime desired state and runtime readiness must be observed" }],
|
||||
workspaces: [{ name: "source" }, { name: "git-ssh" }],
|
||||
workspaces: [{ name: "source" }, { name: "gitea-auth" }],
|
||||
steps: [{ name: "promote", image: ciToolsRunnerImage, env: proxyEnv(), script: gitopsPromoteScript() }]
|
||||
},
|
||||
params: [
|
||||
{ name: "git-url", value: "$(params.git-url)" },
|
||||
{ name: "git-read-url", value: "$(params.git-read-url)" },
|
||||
{ name: "gitops-read-url", value: "$(params.gitops-read-url)" },
|
||||
{ name: "git-write-url", value: "$(params.git-write-url)" },
|
||||
{ name: "gitops-username", value: "$(params.gitops-username)" },
|
||||
{ name: "source-branch", value: "$(params.source-branch)" },
|
||||
{ name: "gitops-branch", value: "$(params.gitops-branch)" },
|
||||
{ name: "lane", value: "$(params.lane)" },
|
||||
@@ -709,7 +716,9 @@ function tektonPipelineRunTemplate({ source, args, deploy = null }) {
|
||||
params: [
|
||||
{ name: "git-url", value: args.sourceRepo },
|
||||
{ name: "git-read-url", value: args.gitReadUrl },
|
||||
{ name: "gitops-read-url", value: args.gitReadUrl },
|
||||
{ name: "git-write-url", value: args.gitWriteUrl },
|
||||
{ name: "gitops-username", value: "unidesk-admin" },
|
||||
{ name: "source-branch", value: args.sourceBranch },
|
||||
{ name: "gitops-branch", value: args.gitopsBranch },
|
||||
{ name: "lane", value: settings.lane },
|
||||
@@ -723,7 +732,7 @@ function tektonPipelineRunTemplate({ source, args, deploy = null }) {
|
||||
],
|
||||
workspaces: [
|
||||
{ name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: "8Gi" } } } } },
|
||||
{ name: "git-ssh", secret: { secretName: "hwlab-git-ssh" } }
|
||||
{ name: "gitea-auth", secret: { secretName: "pac-gitea-hwlab", items: [{ key: "token", path: "token" }] } }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,7 +63,6 @@ function gitSshShellFunction() {
|
||||
function prepareSourceScript() {
|
||||
return renderTemplate("prepare-source.sh", {
|
||||
"# __HWLAB_CI_TIMING_SHELL__\n": ciTimingShellFunction(),
|
||||
"# __HWLAB_GIT_SSH_SHELL__\n": gitSshShellFunction(),
|
||||
"__HWLAB_CI_TOOLS_IMAGE__": ciToolsRunnerImage,
|
||||
"__HWLAB_V02_RUNTIME_ENDPOINT__": JSON.stringify(defaultV02RuntimeEndpoint).slice(1, -1),
|
||||
"__HWLAB_VALIDATION_TASK_COUNT__": primitiveValidationTasks.length
|
||||
@@ -140,7 +139,6 @@ function collectArtifactsScript() {
|
||||
function gitopsPromoteScript() {
|
||||
return renderTemplate("gitops-promote.sh", {
|
||||
"# __HWLAB_CI_TIMING_SHELL__\n": ciTimingShellFunction(),
|
||||
"# __HWLAB_GIT_SSH_SHELL__\n": gitSshShellFunction(),
|
||||
"__HWLAB_CI_TOOLS_IMAGE__": ciToolsRunnerImage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
repo_url="ssh://git@ssh.github.com:443/pikasTech/HWLAB.git"
|
||||
repo_path="/cache/pikasTech/HWLAB.git"
|
||||
lock_dir="/cache/.hwlab-flush.lock"
|
||||
started_ms=$(node -e 'console.log(Date.now())')
|
||||
now_ms() { node -e 'console.log(Date.now())'; }
|
||||
emit_timing() {
|
||||
phase="$1"
|
||||
status="$2"
|
||||
started="$3"
|
||||
finished=$(now_ms)
|
||||
duration=$((finished - started))
|
||||
printf '{"event":"git-mirror-flush-timing","phase":"%s","status":"%s","durationMs":%s,"repository":"pikasTech/HWLAB"}
|
||||
' "$phase" "$status" "$duration"
|
||||
}
|
||||
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||
echo "git mirror flush already running"
|
||||
exit 0
|
||||
fi
|
||||
trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM
|
||||
mkdir -p /root/.ssh
|
||||
cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa
|
||||
chmod 0400 /root/.ssh/id_rsa
|
||||
# __HWLAB_GIT_MIRROR_SSH_PRELUDE__
|
||||
/script/install-hooks.sh
|
||||
for gitops_branch in __HWLAB_GITOPS_BRANCH_ARGS__; do
|
||||
local_head=$(git -C "$repo_path" show-ref --verify --hash "refs/heads/$gitops_branch" 2>/dev/null || true)
|
||||
if [ -z "$local_head" ]; then
|
||||
echo "git mirror flush skips missing local refs/heads/$gitops_branch"
|
||||
continue
|
||||
fi
|
||||
fetch_started=$(now_ms)
|
||||
timeout 90 git -C "$repo_path" fetch --quiet "$repo_url" "refs/heads/$gitops_branch:refs/mirror-stage/heads/$gitops_branch"
|
||||
emit_timing "fetch-$gitops_branch" succeeded "$fetch_started"
|
||||
github_head=$(git -C "$repo_path" show-ref --verify --hash "refs/mirror-stage/heads/$gitops_branch" 2>/dev/null || true)
|
||||
if [ -n "$github_head" ] && [ "$github_head" != "$local_head" ] && ! git -C "$repo_path" merge-base --is-ancestor "$github_head" "$local_head"; then
|
||||
echo "git mirror flush rejected divergent $gitops_branch local=$local_head github=$github_head" >&2
|
||||
exit 52
|
||||
fi
|
||||
if [ "$local_head" != "$github_head" ]; then
|
||||
push_started=$(now_ms)
|
||||
timeout 120 git -C "$repo_path" push "$repo_url" "$local_head:refs/heads/$gitops_branch"
|
||||
emit_timing "push-$gitops_branch" succeeded "$push_started"
|
||||
else
|
||||
echo "git mirror flush $gitops_branch already matches GitHub"
|
||||
fi
|
||||
git -C "$repo_path" update-ref "refs/mirror-stage/heads/$gitops_branch" "$local_head"
|
||||
done
|
||||
git -C "$repo_path" update-server-info
|
||||
flushed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
export flushed_at
|
||||
export gitops_branches_json=__HWLAB_GITOPS_BRANCHES_JSON_SHELL__
|
||||
node - <<'NODE' | tee /cache/HWLAB.last-flush.json
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const repo = "/cache/pikasTech/HWLAB.git";
|
||||
const gitopsBranches = JSON.parse(process.env.gitops_branches_json || "[]");
|
||||
function rev(ref) {
|
||||
try { return execFileSync("git", ["-C", repo, "rev-parse", ref], { encoding: "utf8" }).trim() || null; } catch { return null; }
|
||||
}
|
||||
const refs = {};
|
||||
for (const branch of gitopsBranches) {
|
||||
refs["refs/heads/" + branch] = rev("refs/heads/" + branch);
|
||||
refs["refs/mirror-stage/heads/" + branch] = rev("refs/mirror-stage/heads/" + branch);
|
||||
}
|
||||
const pendingFlush = gitopsBranches.some((branch) => refs["refs/heads/" + branch] && refs["refs/mirror-stage/heads/" + branch] && refs["refs/heads/" + branch] !== refs["refs/mirror-stage/heads/" + branch]);
|
||||
console.log(JSON.stringify({
|
||||
event: "git-mirror-flush",
|
||||
status: "flushed",
|
||||
repository: "pikasTech/HWLAB",
|
||||
flushedAt: process.env.flushed_at,
|
||||
pendingFlush,
|
||||
refs
|
||||
}));
|
||||
NODE
|
||||
emit_timing total succeeded "$started_ms"
|
||||
@@ -1,196 +0,0 @@
|
||||
import { createServer } from "node:http";
|
||||
import { spawn } from "node:child_process";
|
||||
import { createReadStream, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { createGunzip, createInflate } from "node:zlib";
|
||||
|
||||
const cacheRoot = process.env.GIT_MIRROR_CACHE_ROOT || "/cache";
|
||||
const port = Number(process.env.PORT || 8080);
|
||||
|
||||
createServer((req, res) => {
|
||||
handle(req, res).catch((error) => {
|
||||
if (!res.headersSent) res.writeHead(500, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: false, error: error.message }));
|
||||
});
|
||||
}).listen(port, "0.0.0.0", () => {
|
||||
console.log(JSON.stringify({ event: "git-mirror-smart-http-listening", port, cacheRoot }));
|
||||
});
|
||||
|
||||
async function handle(req, res) {
|
||||
const url = new URL(req.url || "/", "http://git-mirror-http");
|
||||
const writeHost = isWriteHost(req.headers.host || "");
|
||||
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/health")) {
|
||||
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-cache" });
|
||||
res.end(JSON.stringify({ ok: true, service: "git-mirror-smart-http", writeHost }));
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && url.pathname.endsWith("/info/refs") && url.searchParams.get("service") === "git-upload-pack") {
|
||||
await runUploadPackAdvertisement(repoPathFromGitServicePath(url.pathname, "/info/refs"), res);
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname.endsWith("/git-upload-pack")) {
|
||||
await runUploadPackRpc(repoPathFromGitServicePath(url.pathname, "/git-upload-pack"), req, res);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && url.pathname.endsWith("/info/refs") && url.searchParams.get("service") === "git-receive-pack") {
|
||||
if (!writeHost) return rejectReadOnly(res);
|
||||
await runReceivePackAdvertisement(repoPathFromGitServicePath(url.pathname, "/info/refs"), res);
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname.endsWith("/git-receive-pack")) {
|
||||
if (!writeHost) return rejectReadOnly(res);
|
||||
await runReceivePackRpc(repoPathFromGitServicePath(url.pathname, "/git-receive-pack"), req, res);
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" || req.method === "HEAD") {
|
||||
serveStaticFile(url.pathname, req, res);
|
||||
return;
|
||||
}
|
||||
res.writeHead(405, { "content-type": "text/plain" });
|
||||
res.end("method not allowed\n");
|
||||
}
|
||||
|
||||
function isWriteHost(hostHeader) {
|
||||
const host = String(hostHeader || "").split(":", 1)[0].toLowerCase();
|
||||
return host === "git-mirror-write" || host.startsWith("git-mirror-write.");
|
||||
}
|
||||
|
||||
function rejectReadOnly(res) {
|
||||
res.writeHead(403, { "content-type": "application/json", "cache-control": "no-cache" });
|
||||
res.end(JSON.stringify({ ok: false, error: "git mirror receive-pack is only available through git-mirror-write" }));
|
||||
}
|
||||
|
||||
function repoPathFromGitServicePath(urlPath, suffix) {
|
||||
const repoUrlPath = urlPath.slice(0, -suffix.length);
|
||||
if (!repoUrlPath.endsWith(".git")) throw new Error(`unsupported git repo path: ${urlPath}`);
|
||||
return safePath(repoUrlPath);
|
||||
}
|
||||
|
||||
function safePath(urlPath) {
|
||||
const decoded = decodeURIComponent(urlPath.replace(/^[/]+/u, ""));
|
||||
const fullPath = path.resolve(cacheRoot, decoded);
|
||||
const normalizedRoot = path.resolve(cacheRoot);
|
||||
if (fullPath !== normalizedRoot && !fullPath.startsWith(`${normalizedRoot}${path.sep}`)) throw new Error("path escapes cache root");
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
function smartHeaders(contentType) {
|
||||
return {
|
||||
"content-type": contentType,
|
||||
"cache-control": "no-cache, max-age=0, must-revalidate",
|
||||
expires: "Fri, 01 Jan 1980 00:00:00 GMT",
|
||||
pragma: "no-cache"
|
||||
};
|
||||
}
|
||||
|
||||
function pktLine(text) {
|
||||
const length = Buffer.byteLength(text) + 4;
|
||||
return `${length.toString(16).padStart(4, "0")}${text}`;
|
||||
}
|
||||
|
||||
function requestBodyStream(req) {
|
||||
const encoding = String(req.headers["content-encoding"] || "identity").toLowerCase();
|
||||
if (encoding === "identity" || encoding === "") return req;
|
||||
if (encoding === "gzip" || encoding === "x-gzip") return req.pipe(createGunzip());
|
||||
if (encoding === "deflate") return req.pipe(createInflate());
|
||||
throw new Error(`unsupported git upload-pack content-encoding: ${encoding}`);
|
||||
}
|
||||
|
||||
function runUploadPackAdvertisement(repoPath, res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git-upload-pack", ["--stateless-rpc", "--advertise-refs", repoPath], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0 && !res.headersSent) reject(new Error(`git-upload-pack advertise failed: ${stderr.trim()}`));
|
||||
else resolve();
|
||||
});
|
||||
res.writeHead(200, smartHeaders("application/x-git-upload-pack-advertisement"));
|
||||
res.write(pktLine("# service=git-upload-pack\n"));
|
||||
res.write("0000");
|
||||
child.stdout.pipe(res, { end: false });
|
||||
child.stdout.on("end", () => res.end());
|
||||
});
|
||||
}
|
||||
|
||||
function runUploadPackRpc(repoPath, req, res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git-upload-pack", ["--stateless-rpc", repoPath], { stdio: ["pipe", "pipe", "pipe"] });
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0 && !res.headersSent) reject(new Error(`git-upload-pack rpc failed: ${stderr.trim()}`));
|
||||
else resolve();
|
||||
});
|
||||
res.writeHead(200, smartHeaders("application/x-git-upload-pack-result"));
|
||||
const body = requestBodyStream(req);
|
||||
body.on("error", (error) => child.stdin.destroy(error));
|
||||
body.pipe(child.stdin);
|
||||
child.stdout.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function runReceivePackAdvertisement(repoPath, res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git-receive-pack", ["--stateless-rpc", "--advertise-refs", repoPath], { stdio: ["ignore", "pipe", "pipe"] });
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0 && !res.headersSent) reject(new Error(`git-receive-pack advertise failed: ${stderr.trim()}`));
|
||||
else resolve();
|
||||
});
|
||||
res.writeHead(200, smartHeaders("application/x-git-receive-pack-advertisement"));
|
||||
res.write(pktLine("# service=git-receive-pack\n"));
|
||||
res.write("0000");
|
||||
child.stdout.pipe(res, { end: false });
|
||||
child.stdout.on("end", () => res.end());
|
||||
});
|
||||
}
|
||||
|
||||
function runReceivePackRpc(repoPath, req, res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("git-receive-pack", ["--stateless-rpc", repoPath], { stdio: ["pipe", "pipe", "pipe"] });
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0 && !res.headersSent) reject(new Error(`git-receive-pack rpc failed: ${stderr.trim()}`));
|
||||
else resolve();
|
||||
});
|
||||
res.writeHead(200, smartHeaders("application/x-git-receive-pack-result"));
|
||||
const body = requestBodyStream(req);
|
||||
body.on("error", (error) => child.stdin.destroy(error));
|
||||
body.pipe(child.stdin);
|
||||
child.stdout.pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function serveStaticFile(urlPath, req, res) {
|
||||
const fullPath = safePath(urlPath);
|
||||
let stat;
|
||||
try { stat = statSync(fullPath); } catch {
|
||||
res.writeHead(404, { "content-type": "text/plain" });
|
||||
res.end("not found\n");
|
||||
return;
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
res.writeHead(404, { "content-type": "text/plain" });
|
||||
res.end("not found\n");
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { "content-length": stat.size, "content-type": contentType(fullPath), "cache-control": "no-cache" });
|
||||
if (req.method === "HEAD") {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
createReadStream(fullPath).pipe(res);
|
||||
}
|
||||
|
||||
function contentType(filePath) {
|
||||
if (filePath.endsWith(".pack")) return "application/x-git-packed-objects";
|
||||
if (filePath.endsWith(".idx")) return "application/x-git-packed-objects-toc";
|
||||
return "text/plain";
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
repo_path="/cache/pikasTech/HWLAB.git"
|
||||
mkdir -p "$repo_path/hooks"
|
||||
cat > "$repo_path/hooks/pre-receive" <<'HOOK'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
zero="0000000000000000000000000000000000000000"
|
||||
status=0
|
||||
while read -r old new ref; do
|
||||
if [ "$new" = "$zero" ]; then
|
||||
echo "git mirror write rejected: deleting $ref is forbidden" >&2
|
||||
status=1
|
||||
continue
|
||||
fi
|
||||
git cat-file -e "$new^{commit}" || status=1
|
||||
git cat-file -e "$new^{tree}" || status=1
|
||||
if git rev-list --objects --missing=print "$new" | grep -q '^?'; then
|
||||
echo "git mirror write rejected: missing objects for $new" >&2
|
||||
status=1
|
||||
continue
|
||||
fi
|
||||
if [ "$old" != "$zero" ] && ! git merge-base --is-ancestor "$old" "$new"; then
|
||||
echo "git mirror write rejected: non-fast-forward $ref" >&2
|
||||
status=1
|
||||
continue
|
||||
fi
|
||||
done
|
||||
exit "$status"
|
||||
HOOK
|
||||
cat > "$repo_path/hooks/post-receive" <<'HOOK'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
repo_path=$(git rev-parse --git-dir)
|
||||
git -C "$repo_path" update-server-info
|
||||
written_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
export repo_path written_at
|
||||
export gitops_branches_json=__HWLAB_GITOPS_BRANCHES_JSON_SHELL__
|
||||
node - <<'NODE' > /cache/HWLAB.last-write.json
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const repo = process.env.repo_path || "/cache/pikasTech/HWLAB.git";
|
||||
const gitopsBranches = JSON.parse(process.env.gitops_branches_json || "[]");
|
||||
function rev(ref) {
|
||||
try { return execFileSync("git", ["-C", repo, "rev-parse", ref], { encoding: "utf8" }).trim() || null; } catch { return null; }
|
||||
}
|
||||
const refs = {};
|
||||
for (const branch of gitopsBranches) {
|
||||
refs["refs/heads/" + branch] = rev("refs/heads/" + branch);
|
||||
refs["refs/mirror-stage/heads/" + branch] = rev("refs/mirror-stage/heads/" + branch);
|
||||
}
|
||||
const pendingFlush = gitopsBranches.some((branch) => refs["refs/heads/" + branch] && refs["refs/heads/" + branch] !== refs["refs/mirror-stage/heads/" + branch]);
|
||||
console.log(JSON.stringify({
|
||||
event: "git-mirror-write",
|
||||
status: "received",
|
||||
repository: "pikasTech/HWLAB",
|
||||
writtenAt: process.env.written_at,
|
||||
pendingFlush,
|
||||
refs
|
||||
}));
|
||||
NODE
|
||||
HOOK
|
||||
chmod 0755 "$repo_path/hooks/pre-receive" "$repo_path/hooks/post-receive"
|
||||
git -C "$repo_path" config http.receivepack true
|
||||
git -C "$repo_path" config receive.denyDeletes true
|
||||
git -C "$repo_path" config receive.denyNonFastForwards true
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import net from "node:net";
|
||||
const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);
|
||||
const proxyPort = Number.parseInt(proxyPortRaw || "", 10);
|
||||
const targetPort = Number.parseInt(targetPortRaw || "", 10);
|
||||
if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {
|
||||
console.error("hwlab git-mirror proxy-connect: invalid ProxyCommand arguments");
|
||||
process.exit(64);
|
||||
}
|
||||
let settled = false;
|
||||
let tunnelEstablished = false;
|
||||
function finish(code, message) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (message) console.error("hwlab git-mirror proxy-connect: " + message);
|
||||
process.exit(code);
|
||||
}
|
||||
const socket = net.createConnection({ host: proxyHost, port: proxyPort });
|
||||
let buffer = Buffer.alloc(0);
|
||||
socket.setTimeout(15000, () => { socket.destroy(); finish(65, "timeout connecting via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort); });
|
||||
socket.on("connect", () => socket.write("CONNECT " + targetHost + ":" + targetPort + " HTTP/1.1\r\nHost: " + targetHost + ":" + targetPort + "\r\nProxy-Connection: Keep-Alive\r\n\r\n"));
|
||||
socket.on("error", (error) => finish(tunnelEstablished ? 69 : 66, (tunnelEstablished ? "tunnel socket error: " : "tcp error connecting to proxy: ") + (error && error.message ? error.message : String(error))));
|
||||
socket.on("close", () => { if (!tunnelEstablished) finish(68, "proxy closed before CONNECT completed via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort); else finish(0); });
|
||||
function onData(chunk) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
const headerEnd = buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd === -1 && buffer.length < 8192) return;
|
||||
if (headerEnd === -1) { socket.destroy(); finish(68, "proxy response header exceeded 8192 bytes before CONNECT status via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort); return; }
|
||||
const head = buffer.slice(0, headerEnd + 4).toString("latin1");
|
||||
const statusLine = head.split("\r\n", 1)[0] || "";
|
||||
const statusCode = Number.parseInt(statusLine.split(" ")[1] || "", 10);
|
||||
if (!statusLine.startsWith("HTTP/1.") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {
|
||||
const safeStatus = statusLine.replace(/[^\x20-\x7e]/g, "?").slice(0, 160);
|
||||
socket.destroy();
|
||||
finish(67, "proxy CONNECT failed via " + proxyHost + ":" + proxyPort + " to " + targetHost + ":" + targetPort + ": " + safeStatus);
|
||||
return;
|
||||
}
|
||||
socket.off("data", onData);
|
||||
socket.setTimeout(0);
|
||||
tunnelEstablished = true;
|
||||
const rest = buffer.slice(headerEnd + 4);
|
||||
if (rest.length) process.stdout.write(rest);
|
||||
process.stdin.on("error", () => {});
|
||||
process.stdout.on("error", () => {});
|
||||
process.stdin.pipe(socket);
|
||||
socket.pipe(process.stdout);
|
||||
}
|
||||
socket.on("data", onData);
|
||||
@@ -1,11 +0,0 @@
|
||||
printf '%s\n' __HWLAB_PROXY_SUMMARY_SHELL__ >&2
|
||||
cat > /tmp/hwlab-git-ssh-proxy.sh <<'SH_PROXY'
|
||||
#!/bin/sh
|
||||
exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 "$@"
|
||||
SH_PROXY
|
||||
chmod 0700 /tmp/hwlab-git-ssh-proxy.sh
|
||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
|
||||
export NO_PROXY='*'
|
||||
export no_proxy='*'
|
||||
export GIT_SSH=/tmp/hwlab-git-ssh-proxy.sh
|
||||
unset GIT_SSH_COMMAND
|
||||
@@ -1,20 +0,0 @@
|
||||
printf '%s\n' __HWLAB_PROXY_SUMMARY_SHELL__ >&2
|
||||
cat > /tmp/hwlab-github-proxy-connect.mjs <<'NODE_PROXY'
|
||||
// __HWLAB_GIT_MIRROR_PROXY_CONNECT_MJS__
|
||||
NODE_PROXY
|
||||
chmod 0700 /tmp/hwlab-github-proxy-connect.mjs
|
||||
cat > /tmp/hwlab-git-ssh-proxy.sh <<'SH_PROXY'
|
||||
#!/bin/sh
|
||||
exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o __HWLAB_PROXY_COMMAND_SHELL__ "$@"
|
||||
SH_PROXY
|
||||
chmod 0700 /tmp/hwlab-git-ssh-proxy.sh
|
||||
export HTTP_PROXY=__HWLAB_PROXY_URL_SHELL__
|
||||
export HTTPS_PROXY=__HWLAB_PROXY_URL_SHELL__
|
||||
export ALL_PROXY=__HWLAB_PROXY_URL_SHELL__
|
||||
export http_proxy=__HWLAB_PROXY_URL_SHELL__
|
||||
export https_proxy=__HWLAB_PROXY_URL_SHELL__
|
||||
export all_proxy=__HWLAB_PROXY_URL_SHELL__
|
||||
export NO_PROXY=__HWLAB_PROXY_NO_PROXY_SHELL__
|
||||
export no_proxy=__HWLAB_PROXY_NO_PROXY_SHELL__
|
||||
export GIT_SSH=/tmp/hwlab-git-ssh-proxy.sh
|
||||
unset GIT_SSH_COMMAND
|
||||
@@ -1,181 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
repo_url="ssh://git@ssh.github.com:443/pikasTech/HWLAB.git"
|
||||
repo_path="/cache/pikasTech/HWLAB.git"
|
||||
source_snapshot_ref_prefix=__HWLAB_SOURCE_SNAPSHOT_REF_PREFIX_SHELL__
|
||||
lock_dir="/cache/.hwlab-sync.lock"
|
||||
sync_started_ms=$(node -e 'console.log(Date.now())')
|
||||
now_ms() { node -e 'console.log(Date.now())'; }
|
||||
emit_timing() {
|
||||
phase="$1"
|
||||
status="$2"
|
||||
started_ms="$3"
|
||||
finished_ms=$(now_ms)
|
||||
duration_ms=$((finished_ms - started_ms))
|
||||
printf '{"event":"git-mirror-sync-timing","phase":"%s","status":"%s","durationMs":%s,"repository":"pikasTech/HWLAB"}
|
||||
' "$phase" "$status" "$duration_ms"
|
||||
}
|
||||
mkdir -p /cache/pikasTech /root/.ssh
|
||||
if ! mkdir "$lock_dir" 2>/dev/null; then
|
||||
echo "git mirror sync already running"
|
||||
exit 0
|
||||
fi
|
||||
trap 'rmdir "$lock_dir" 2>/dev/null || true' EXIT INT TERM
|
||||
cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa
|
||||
chmod 0400 /root/.ssh/id_rsa
|
||||
# __HWLAB_GIT_MIRROR_SSH_PRELUDE__
|
||||
if [ -d "$repo_path/objects" ] && [ -f "$repo_path/HEAD" ]; then
|
||||
git -C "$repo_path" remote set-url origin "$repo_url" || git -C "$repo_path" remote add origin "$repo_url"
|
||||
else
|
||||
rm -rf "$repo_path"
|
||||
git init --bare "$repo_path"
|
||||
git -C "$repo_path" remote add origin "$repo_url"
|
||||
fi
|
||||
/script/install-hooks.sh
|
||||
git -C "$repo_path" config uploadpack.hideRefs refs/mirror-stage
|
||||
git -C "$repo_path" config uploadpack.allowReachableSHA1InWant true
|
||||
git -C "$repo_path" config uploadpack.allowAnySHA1InWant true
|
||||
git -C "$repo_path" config --unset-all remote.origin.fetch 2>/dev/null || true
|
||||
# __HWLAB_FETCH_CONFIG_COMMANDS__
|
||||
fetch_started_ms=$(now_ms)
|
||||
timeout 180 git -C "$repo_path" fetch --quiet --prune origin
|
||||
emit_timing fetch succeeded "$fetch_started_ms"
|
||||
git -C "$repo_path" for-each-ref --format='%(objectname) %(refname)' refs/mirror-stage/heads > /tmp/hwlab-stage-heads
|
||||
git -C "$repo_path" for-each-ref --format='%(refname)' refs/mirror-stage/heads > /tmp/hwlab-stage-head-refs
|
||||
git -C "$repo_path" for-each-ref --format='%(objectname) %(refname)' refs/mirror-stage/tags > /tmp/hwlab-stage-tags
|
||||
git -C "$repo_path" for-each-ref --format='%(refname)' refs/mirror-stage/tags > /tmp/hwlab-stage-tag-refs
|
||||
if [ ! -s /tmp/hwlab-stage-heads ]; then
|
||||
echo "git mirror sync fetched no branch refs" >&2
|
||||
exit 41
|
||||
fi
|
||||
for required_ref in __HWLAB_REQUIRED_REF_ARGS__; do
|
||||
git -C "$repo_path" show-ref --verify --quiet "$required_ref" || { echo "git mirror sync missing required ref $required_ref" >&2; exit 43; }
|
||||
done
|
||||
validate_started_ms=$(now_ms)
|
||||
while read -r sha ref; do
|
||||
[ -n "$sha" ] || continue
|
||||
git -C "$repo_path" cat-file -e "$sha^{commit}"
|
||||
git -C "$repo_path" cat-file -e "$sha^{tree}"
|
||||
if git -C "$repo_path" rev-list --objects --missing=print "$sha" | grep -q '^?'; then
|
||||
echo "git mirror sync found missing objects for $ref $sha" >&2
|
||||
exit 42
|
||||
fi
|
||||
done < /tmp/hwlab-stage-heads
|
||||
emit_timing validate succeeded "$validate_started_ms"
|
||||
publish_started_ms=$(now_ms)
|
||||
while read -r sha ref; do
|
||||
[ -n "$sha" ] || continue
|
||||
name=$(printf '%s
|
||||
' "$ref" | sed 's#^refs/mirror-stage/heads/##')
|
||||
is_gitops_branch=false
|
||||
for gitops_branch in __HWLAB_GITOPS_BRANCH_ARGS__; do
|
||||
if [ "$name" = "$gitops_branch" ]; then is_gitops_branch=true; break; fi
|
||||
done
|
||||
if [ "$is_gitops_branch" = true ]; then
|
||||
local_sha=$(git -C "$repo_path" show-ref --verify --hash "refs/heads/$name" 2>/dev/null || true)
|
||||
if [ -n "$local_sha" ] && [ "$local_sha" != "$sha" ]; then
|
||||
if git -C "$repo_path" merge-base --is-ancestor "$sha" "$local_sha"; then
|
||||
echo "git mirror sync keeps local $name ahead of GitHub"
|
||||
continue
|
||||
fi
|
||||
if ! git -C "$repo_path" merge-base --is-ancestor "$local_sha" "$sha"; then
|
||||
echo "git mirror sync found divergent $name local=$local_sha github=$sha" >&2
|
||||
exit 44
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
git -C "$repo_path" update-ref "refs/heads/$name" "$sha"
|
||||
done < /tmp/hwlab-stage-heads
|
||||
for source_branch in __HWLAB_SOURCE_BRANCH_ARGS__; do
|
||||
source_sha=$(git -C "$repo_path" show-ref --verify --hash "refs/heads/$source_branch" 2>/dev/null || true)
|
||||
if [ -n "$source_sha" ]; then
|
||||
source_stage_ref="${source_snapshot_ref_prefix%/}/$source_branch/$source_sha"
|
||||
git -C "$repo_path" update-ref "$source_stage_ref" "$source_sha"
|
||||
fi
|
||||
done
|
||||
while read -r sha ref; do
|
||||
[ -n "$sha" ] || continue
|
||||
name=$(printf '%s
|
||||
' "$ref" | sed 's#^refs/mirror-stage/tags/##')
|
||||
git -C "$repo_path" update-ref "refs/tags/$name" "$sha"
|
||||
done < /tmp/hwlab-stage-tags
|
||||
git -C "$repo_path" for-each-ref --format='%(refname)' refs/heads > /tmp/hwlab-public-heads
|
||||
while read -r ref; do
|
||||
[ -n "$ref" ] || continue
|
||||
name=$(printf '%s
|
||||
' "$ref" | sed 's#^refs/heads/##')
|
||||
if ! grep -Fxq "refs/mirror-stage/heads/$name" /tmp/hwlab-stage-head-refs; then
|
||||
git -C "$repo_path" update-ref -d "$ref"
|
||||
fi
|
||||
done < /tmp/hwlab-public-heads
|
||||
git -C "$repo_path" for-each-ref --format='%(refname)' refs/tags > /tmp/hwlab-public-tags
|
||||
while read -r ref; do
|
||||
[ -n "$ref" ] || continue
|
||||
name=$(printf '%s
|
||||
' "$ref" | sed 's#^refs/tags/##')
|
||||
if ! grep -Fxq "refs/mirror-stage/tags/$name" /tmp/hwlab-stage-tag-refs; then
|
||||
git -C "$repo_path" update-ref -d "$ref"
|
||||
fi
|
||||
done < /tmp/hwlab-public-tags
|
||||
emit_timing publish succeeded "$publish_started_ms"
|
||||
for head_branch in __HWLAB_SOURCE_BRANCH_ARGS__; do
|
||||
if git -C "$repo_path" show-ref --verify --quiet "refs/heads/$head_branch"; then
|
||||
git -C "$repo_path" symbolic-ref HEAD "refs/heads/$head_branch"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fsck_started_ms=$(now_ms)
|
||||
git -C "$repo_path" fsck --connectivity-only --no-dangling >/tmp/hwlab-git-fsck.out
|
||||
emit_timing fsck succeeded "$fsck_started_ms"
|
||||
git -C "$repo_path" update-server-info
|
||||
published_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
for source_branch in __HWLAB_SOURCE_BRANCH_ARGS__; do
|
||||
git -C "$repo_path" rev-parse "refs/heads/$source_branch" 2>/dev/null | tee "/cache/HWLAB-$source_branch.head" >/dev/null || true
|
||||
done
|
||||
printf '%s
|
||||
' "$published_at" > /cache/HWLAB.last-sync
|
||||
export published_at
|
||||
export source_snapshot_ref_prefix
|
||||
export mirror_branches_json=__HWLAB_MIRROR_BRANCHES_JSON_SHELL__
|
||||
export source_branches_json=__HWLAB_SOURCE_BRANCHES_JSON_SHELL__
|
||||
export gitops_branches_json=__HWLAB_GITOPS_BRANCHES_JSON_SHELL__
|
||||
node - <<'NODE' | tee /cache/HWLAB.last-sync.json
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const repo = "/cache/pikasTech/HWLAB.git";
|
||||
const mirrorBranches = JSON.parse(process.env.mirror_branches_json || "[]");
|
||||
const sourceBranches = JSON.parse(process.env.source_branches_json || "[]");
|
||||
const gitopsBranches = JSON.parse(process.env.gitops_branches_json || "[]");
|
||||
const sourceSnapshotRefPrefix = (process.env.source_snapshot_ref_prefix || "refs/unidesk/snapshots/hwlab-node-runtime").replace(//+$/u, "");
|
||||
function rev(ref) {
|
||||
try { return execFileSync("git", ["-C", repo, "rev-parse", ref], { encoding: "utf8" }).trim() || null; } catch { return null; }
|
||||
}
|
||||
const refs = {};
|
||||
for (const branch of mirrorBranches) {
|
||||
refs["refs/heads/" + branch] = rev("refs/heads/" + branch);
|
||||
refs["refs/mirror-stage/heads/" + branch] = rev("refs/mirror-stage/heads/" + branch);
|
||||
}
|
||||
const sourceSnapshots = {};
|
||||
for (const branch of sourceBranches) {
|
||||
const source = refs["refs/heads/" + branch] || null;
|
||||
const stageRef = source ? sourceSnapshotRefPrefix + "/" + branch + "/" + source : null;
|
||||
const sourceSnapshot = stageRef ? rev(stageRef) : null;
|
||||
if (stageRef) refs[stageRef] = sourceSnapshot;
|
||||
sourceSnapshots[branch] = { stageRef, sourceSnapshot };
|
||||
}
|
||||
const sourceInSync = sourceBranches.every((branch) => refs["refs/heads/" + branch] && refs["refs/heads/" + branch] === refs["refs/mirror-stage/heads/" + branch] && sourceSnapshots[branch]?.sourceSnapshot === refs["refs/mirror-stage/heads/" + branch]);
|
||||
const gitopsInSync = gitopsBranches.every((branch) => refs["refs/heads/" + branch] && refs["refs/heads/" + branch] === refs["refs/mirror-stage/heads/" + branch]);
|
||||
const pendingFlush = gitopsBranches.some((branch) => refs["refs/heads/" + branch] && refs["refs/mirror-stage/heads/" + branch] && refs["refs/heads/" + branch] !== refs["refs/mirror-stage/heads/" + branch]);
|
||||
const payload = {
|
||||
event: "git-mirror-sync",
|
||||
status: "published",
|
||||
repository: "pikasTech/HWLAB",
|
||||
publishedAt: process.env.published_at,
|
||||
pendingFlush,
|
||||
sourceInSync,
|
||||
gitopsInSync,
|
||||
sourceSnapshots,
|
||||
refs
|
||||
};
|
||||
console.log(JSON.stringify(payload));
|
||||
NODE
|
||||
emit_timing total succeeded "$sync_started_ms"
|
||||
@@ -8,9 +8,6 @@ export HWLAB_TEKTON_TASK="gitops-promote"
|
||||
export HWLAB_SOURCE_REVISION="$(params.revision)"
|
||||
echo '{"event":"ci-base-image","phase":"gitops-promote","image":"__HWLAB_CI_TOOLS_IMAGE__","policy":"no-runtime-apt"}'
|
||||
for tool in node git timeout; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"gitops-promote","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
|
||||
# __HWLAB_GIT_SSH_SHELL__
|
||||
|
||||
git_url_requires_ssh() { case "$1" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }
|
||||
lane="$(params.lane)"
|
||||
case "$lane" in
|
||||
node) runtime_lane=false ;;
|
||||
@@ -21,29 +18,27 @@ if [ "$runtime_lane" = "true" ]; then
|
||||
else
|
||||
printf 'true' > /tekton/results/runtime-ready-required
|
||||
fi
|
||||
source_head_url_for_setup="$(params.git-url)"
|
||||
gitops_read_url_for_setup="$(params.git-url)"
|
||||
if [ "$runtime_lane" = "true" ]; then
|
||||
source_head_url_for_setup="$(params.git-read-url)"
|
||||
gitops_read_url_for_setup="$(params.git-read-url)"
|
||||
fi
|
||||
gitops_write_url_for_setup="$(params.git-write-url)"
|
||||
if git_url_requires_ssh "$source_head_url_for_setup" || git_url_requires_ssh "$gitops_read_url_for_setup" || git_url_requires_ssh "$gitops_write_url_for_setup"; then
|
||||
for tool in ssh ssh-keyscan; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","phase":"gitops-promote","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
|
||||
git_ssh_setup
|
||||
else
|
||||
echo '{"event":"git-ssh-setup","phase":"gitops-promote","status":"skipped","reason":"non-ssh-git-url"}'
|
||||
fi
|
||||
test -s /workspace/gitea-auth/token || { echo '{"event":"gitea-auth","phase":"gitops-promote","status":"failed","reason":"token-missing"}'; exit 31; }
|
||||
askpass_dir="$(mktemp -d)"
|
||||
cat > "$askpass_dir/askpass.sh" <<'SH'
|
||||
#!/bin/sh
|
||||
case "$1" in
|
||||
*sername*) printf '%s\n' "$GITEA_USERNAME" ;;
|
||||
*) cat /workspace/gitea-auth/token ;;
|
||||
esac
|
||||
SH
|
||||
chmod 0700 "$askpass_dir/askpass.sh"
|
||||
export GITEA_USERNAME="$(params.gitops-username)"
|
||||
export GIT_ASKPASS="$askpass_dir/askpass.sh"
|
||||
export GIT_ASKPASS_REQUIRE=force
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
cd /workspace/source/repo
|
||||
test "$(git rev-parse HEAD)" = "$(params.revision)"
|
||||
|
||||
check_source_head() {
|
||||
phase="$1"
|
||||
expected="${2:-$(params.revision)}"
|
||||
source_head_url="$(params.git-url)"
|
||||
if [ "$runtime_lane" = "true" ]; then
|
||||
source_head_url="$(params.git-read-url)"
|
||||
fi
|
||||
source_head_url="$(params.git-read-url)"
|
||||
latest_file="$(mktemp)"
|
||||
if ! git_timed "source-head-$phase" 45 git ls-remote "$source_head_url" "refs/heads/$(params.source-branch)" > "$latest_file"; then
|
||||
echo '{"status":"failed","reason":"source-head-check-failed","phase":"'"$phase"'","sourceBranch":"'"$(params.source-branch)"'","sourceRevision":"'"$(params.revision)"'"}'
|
||||
@@ -182,11 +177,8 @@ node scripts/run-bun.mjs scripts/gitops-render.mjs --lane "$lane" --catalog-path
|
||||
ci_timing_emit gitops-render succeeded "$gitops_render_started_ms"
|
||||
check_source_head before-push
|
||||
workdir="$(mktemp -d)"
|
||||
gitops_read_url="$(params.git-url)"
|
||||
gitops_read_url="$(params.gitops-read-url)"
|
||||
gitops_write_url="$(params.git-write-url)"
|
||||
if [ "$runtime_lane" = "true" ]; then
|
||||
gitops_read_url="$(params.git-read-url)"
|
||||
fi
|
||||
gitops_clone_started_ms="$(ci_now_ms)"
|
||||
git_timed gitops-clone 180 git clone --no-checkout "$gitops_read_url" "$workdir/gitops"
|
||||
cd "$workdir/gitops"
|
||||
|
||||
@@ -28,7 +28,7 @@ const catalog = JSON.parse(fs.readFileSync(process.env.HWLAB_ARTIFACT_CATALOG_PA
|
||||
const plan = fs.existsSync("/workspace/source/affected-services.json") ? JSON.parse(fs.readFileSync("/workspace/source/affected-services.json", "utf8")) : {};
|
||||
const service = (catalog.services || []).find((item) => item.serviceId === serviceId) || {};
|
||||
const planned = (plan.services || []).find((item) => item.serviceId === serviceId) || {};
|
||||
const envReuse = planned.runtimeMode === "env-reuse-git-mirror-checkout" || service.runtimeMode === "env-reuse-git-mirror-checkout" || planned.envReuse === true || service.envReuse === true;
|
||||
const envReuse = planned.runtimeMode === "env-reuse-gitea-checkout" || service.runtimeMode === "env-reuse-gitea-checkout" || planned.envReuse === true || service.envReuse === true;
|
||||
const image = envReuse ? (service.environmentImage || service.image || "") : (service.image || "");
|
||||
const digest = envReuse ? (service.environmentDigest || service.digest || "not_published") : (service.digest || "not_published");
|
||||
const imageTag = service.imageTag || (image.includes(":") ? image.slice(image.lastIndexOf(":") + 1) : "");
|
||||
@@ -48,7 +48,7 @@ const values = {
|
||||
"component-input-hash": componentInputHash,
|
||||
"environment-input-hash": environmentInputHash,
|
||||
"code-input-hash": codeInputHash,
|
||||
"runtime-mode": envReuse ? "env-reuse-git-mirror-checkout" : "service-image",
|
||||
"runtime-mode": envReuse ? "env-reuse-gitea-checkout" : "service-image",
|
||||
"boot-repo": planned.bootRepo || service.bootRepo || "",
|
||||
"boot-commit": envReuse ? revision : (service.bootCommit || ""),
|
||||
"boot-sh": planned.bootSh || service.bootSh || "",
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ -d "$ci_node_deps/yaml" ] && [ ! -e node_modules/yaml ]; then
|
||||
ln -s "$ci_node_deps/yaml" node_modules/yaml
|
||||
fi
|
||||
test "$(git rev-parse HEAD)" = "$(params.revision)"
|
||||
HWLAB_CATALOG_PATH="$(params.catalog-path)" HWLAB_GIT_URL="$(params.git-url)" HWLAB_GIT_READ_URL="$(params.git-read-url)" HWLAB_GITOPS_BRANCH="$(params.gitops-branch)" HWLAB_SERVICES="$(params.services)" node scripts/ci/restore-artifact-catalog.mjs
|
||||
HWLAB_CATALOG_PATH="$(params.catalog-path)" HWLAB_GIT_URL="$(params.git-url)" HWLAB_GIT_READ_URL="$(params.gitops-read-url)" HWLAB_GITOPS_BRANCH="$(params.gitops-branch)" HWLAB_SERVICES="$(params.services)" node scripts/ci/restore-artifact-catalog.mjs
|
||||
node scripts/ci-plan.mjs --lane "$(params.lane)" --base-ref "$(params.release-base-commit)" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog "$(params.catalog-path)" --registry-prefix "$(params.registry-prefix)" --services "$(params.services)" --release-target "$(params.release-target)" --release-consumer "$(params.release-consumer)" --verify-reuse-registry > /workspace/source/ci-plan.json
|
||||
node scripts/ci/verify-reviewed-plan.mjs --actual-plan /workspace/source/ci-plan.json --reviewed-plan "$(params.reviewed-plan)"
|
||||
node - <<'NODE'
|
||||
@@ -25,7 +25,7 @@ const entries = allServices.map((serviceId) => {
|
||||
const service = byService.get(serviceId) || {};
|
||||
const serviceSelected = selectedSet.has(serviceId);
|
||||
const rolloutAffected = serviceSelected && affected.has(serviceId);
|
||||
const envReuse = service.runtimeMode === "env-reuse-git-mirror-checkout" || service.envReuse === true;
|
||||
const envReuse = service.runtimeMode === "env-reuse-gitea-checkout" || service.envReuse === true;
|
||||
const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected));
|
||||
return {
|
||||
serviceId,
|
||||
|
||||
@@ -9,16 +9,8 @@ export HWLAB_SOURCE_REVISION="$(params.revision)"
|
||||
echo '{"event":"ci-base-image","phase":"prepare-source","image":"__HWLAB_CI_TOOLS_IMAGE__","policy":"no-runtime-apk"}'
|
||||
for tool in node git timeout; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
|
||||
node -e 'console.log(JSON.stringify({event:"ci-base-image",phase:"prepare-source",ok:true,node:process.version}))'
|
||||
# __HWLAB_GIT_SSH_SHELL__
|
||||
|
||||
git_url_requires_ssh() { case "$1" in git@*|ssh://*) return 0 ;; *) return 1 ;; esac; }
|
||||
git_read_url="$(params.git-read-url)"
|
||||
if git_url_requires_ssh "$git_read_url"; then
|
||||
for tool in ssh ssh-keyscan; do command -v "$tool" >/dev/null 2>&1 || { echo '{"event":"ci-base-image","ok":false,"reason":"missing-tool","tool":"'"$tool"'"}'; exit 31; }; done
|
||||
git_ssh_setup
|
||||
else
|
||||
echo '{"event":"git-ssh-setup","phase":"prepare-source","status":"skipped","reason":"non-ssh-git-read-url"}'
|
||||
fi
|
||||
gitops_read_url="$(params.gitops-read-url)"
|
||||
rm -rf /workspace/source/repo
|
||||
source_clone_started_ms="$(ci_now_ms)"
|
||||
git_timed source-clone 180 git clone --branch "$(params.source-branch)" "$git_read_url" /workspace/source/repo
|
||||
@@ -35,8 +27,8 @@ ci_timing_emit prepare-source-dependencies succeeded "$prepare_source_dependenci
|
||||
catalog_path="$(params.catalog-path)"
|
||||
mkdir -p "$(dirname "$catalog_path")"
|
||||
catalog_fetch_started_ms="$(ci_now_ms)"
|
||||
if git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads "$git_read_url" "$(params.gitops-branch)" >/dev/null; then
|
||||
git remote set-url gitops-catalog "$git_read_url" 2>/dev/null || git remote add gitops-catalog "$git_read_url"
|
||||
if git_timed catalog-ls-remote 45 git ls-remote --exit-code --heads "$gitops_read_url" "$(params.gitops-branch)" >/dev/null; then
|
||||
git remote set-url gitops-catalog "$gitops_read_url" 2>/dev/null || git remote add gitops-catalog "$gitops_read_url"
|
||||
git_timed catalog-fetch 90 git fetch --depth 1 gitops-catalog "$(params.gitops-branch)" >/dev/null || true
|
||||
if git cat-file -e FETCH_HEAD:"$catalog_path" 2>/dev/null; then
|
||||
git show FETCH_HEAD:"$catalog_path" > /tmp/hwlab-gitops-artifact-catalog.json
|
||||
|
||||
Reference in New Issue
Block a user