Merge pull request #2873 from pikasTech/fix/sentinel-gitea-publish
fix: 收窄通用 PaC 发布输入
This commit is contained in:
@@ -1168,6 +1168,20 @@ templates:
|
||||
argoApplication: "hwlab-web-probe-sentinel-${nodeLower}"
|
||||
deliveryObservation:
|
||||
runtimeEvidence: required
|
||||
releaseInputPaths:
|
||||
- config
|
||||
- config.json
|
||||
- scripts
|
||||
- src
|
||||
- package.json
|
||||
- bun.lock
|
||||
- bun.lockb
|
||||
releaseExcludePaths:
|
||||
- config/platform-infra/pipelines-as-code.yaml
|
||||
- scripts/native/cicd
|
||||
- scripts/src/hwlab-node-web-sentinel-cicd*
|
||||
- scripts/src/platform-infra-pac-*
|
||||
- scripts/src/platform-infra-pipelines-as-code*
|
||||
closeoutGitOpsMirrorFlush: false
|
||||
closeoutGitOpsMirrorLane: v03
|
||||
hwlabV03:
|
||||
|
||||
@@ -106,6 +106,10 @@ test("actual renderer replaces unexpanded inputs without changing business exit"
|
||||
expect(source).not.toContain("GIT_SSH_COMMAND");
|
||||
expect(source).toContain("chmod -R a+rwX \"$worktree\" \"$meta_dir\"");
|
||||
expect(source).not.toContain("chmod -R a+rwX /workspace");
|
||||
expect(source).toContain("CONTEXT_EXCLUDE_PATHS_B64");
|
||||
const contextExcludes = /context_exclude_paths_b64='([^']+)'/u.exec(source);
|
||||
expect(contextExcludes).not.toBeNull();
|
||||
expect(JSON.parse(Buffer.from(contextExcludes![1]!, "base64").toString("utf8"))).toContain("scripts/src/platform-infra-pipelines-as-code*");
|
||||
const publish = readFileSync(join(result.outputDir, "publish.sh"), "utf8");
|
||||
expect(publish).toContain("UNIDESK_PAC_CONSUMER='sentinel-nc01-v03'");
|
||||
expect(publish).toContain("feature-config-schema-validation");
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "../../src/hwlab-node-web-sentinel-cicd-jobs";
|
||||
import { exportCicdOtelPayload, loadCicdOtelRuntimeConfig } from "./otel-runtime-config";
|
||||
import { renderPacFeatureConfigSchemaStage } from "../../src/pac-feature-config-schema-stage";
|
||||
import { readPacConfig, resolveConsumer } from "../../src/platform-infra-pipelines-as-code/config";
|
||||
|
||||
const options = parseOptions(process.argv.slice(2));
|
||||
const sourceAuthority = options.sourceAuthority === "gitea-snapshot" ? "gitea-snapshot" : fail("--source-authority must be gitea-snapshot");
|
||||
@@ -26,6 +27,8 @@ const state = loadSentinelCicdState(
|
||||
},
|
||||
);
|
||||
const outputDir = resolve(options.outputDir);
|
||||
const pacConfig = readPacConfig({ consumerId: options.repositoryId });
|
||||
const pacConsumer = resolveConsumer(pacConfig, options.repositoryId);
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const otelConfig = loadCicdOtelRuntimeConfig(options.repositoryId);
|
||||
const otelSetup = [
|
||||
@@ -51,7 +54,7 @@ writeExecutable("source.sh", [
|
||||
otelSetup,
|
||||
`incoming_traceparent=${shellQuote(process.env.TRACEPARENT ?? "")}`,
|
||||
`incoming_tracestate=${shellQuote(process.env.TRACESTATE ?? "")}`,
|
||||
sentinelPublishSourceShell(state, options.pipelineRun),
|
||||
sentinelPublishSourceShell(state, options.pipelineRun, pacConsumer.releaseExcludePaths),
|
||||
"if [ -n \"$incoming_traceparent\" ]; then printf '%s' \"$incoming_traceparent\" > /workspace/meta/traceparent; fi",
|
||||
"if [ -n \"$incoming_tracestate\" ]; then printf '%s' \"$incoming_tracestate\" > /workspace/meta/tracestate; fi",
|
||||
].join("\n"));
|
||||
|
||||
@@ -579,12 +579,18 @@ export function sentinelImageBuildProxyEnv(state: SentinelCicdState): Array<{ na
|
||||
];
|
||||
}
|
||||
|
||||
export function sentinelPublishSourceShell(state: SentinelCicdState, jobName: string): string {
|
||||
export function sentinelPublishSourceShell(state: SentinelCicdState, jobName: string, contextExcludePaths: readonly string[] = []): string {
|
||||
const monitorWeb = record(state.image.monitorWeb);
|
||||
const checkoutPathsB64 = Buffer.from(JSON.stringify(arrayAt(state.cicd, "source.checkoutPaths").map((item) => {
|
||||
if (typeof item !== "string" || item.length === 0 || item.startsWith("/") || item.includes("..")) throw new Error("source.checkoutPaths must contain safe relative paths");
|
||||
return item;
|
||||
})), "utf8").toString("base64");
|
||||
const contextExcludePathsB64 = Buffer.from(JSON.stringify(contextExcludePaths.map((item) => {
|
||||
if (item.length === 0 || item.startsWith("/") || item.includes("..") || (item.includes("*") && !item.endsWith("*"))) {
|
||||
throw new Error("releaseExcludePaths must contain safe relative paths with only an optional trailing wildcard");
|
||||
}
|
||||
return item;
|
||||
})), "utf8").toString("base64");
|
||||
const dockerfileB64 = Buffer.from(state.image.dockerfilePreview, "utf8").toString("base64");
|
||||
const envReuseMode = stringAt(monitorWeb, "envReuseMode");
|
||||
const envReuseNodeDepsPath = stringAt(monitorWeb, "envReuseNodeDepsPath");
|
||||
@@ -598,6 +604,7 @@ export function sentinelPublishSourceShell(state: SentinelCicdState, jobName: st
|
||||
`source_commit=${shellQuote(state.sourceHead.commit ?? "")}`,
|
||||
`source_stage_ref=${shellQuote(state.sourceHead.stageRef ?? "")}`,
|
||||
`checkout_paths_b64=${shellQuote(checkoutPathsB64)}`,
|
||||
`context_exclude_paths_b64=${shellQuote(contextExcludePathsB64)}`,
|
||||
`dockerfile_b64=${shellQuote(dockerfileB64)}`,
|
||||
`env_reuse_mode=${shellQuote(envReuseMode)}`,
|
||||
`env_reuse_node_deps_path=${shellQuote(envReuseNodeDepsPath)}`,
|
||||
@@ -708,6 +715,11 @@ export function sentinelPublishSourceShell(state: SentinelCicdState, jobName: st
|
||||
".env",
|
||||
".env.*",
|
||||
"EOF_DOCKERIGNORE",
|
||||
"CONTEXT_EXCLUDE_PATHS_B64=\"$context_exclude_paths_b64\" node <<'NODE'",
|
||||
"const fs = require('node:fs');",
|
||||
"const paths = JSON.parse(Buffer.from(process.env.CONTEXT_EXCLUDE_PATHS_B64 || '', 'base64').toString('utf8'));",
|
||||
"if (paths.length > 0) fs.appendFileSync('.dockerignore', `${paths.join('\\n')}\\n`);",
|
||||
"NODE",
|
||||
"context_ignore_entries=$(wc -l < .dockerignore | tr -d ' ')",
|
||||
"write_meta context_ignore_entries \"$context_ignore_entries\"",
|
||||
"touch /workspace/image-build.log /workspace/build-metadata.json /workspace/buildkitd.log",
|
||||
|
||||
@@ -482,6 +482,23 @@ test("materialization-only release consumers never inherit shared repository ima
|
||||
)).toThrow("materializationPaths must not be empty when materializationOnly is true");
|
||||
});
|
||||
|
||||
test("Sentinel release scope and image context share YAML-owned path exclusions", () => {
|
||||
const pac = readPacConfig({ consumerId: "sentinel-nc01-v03" });
|
||||
const consumer = resolveConsumer(pac, "sentinel-nc01-v03");
|
||||
expect(consumer.releaseInputPaths).toContain("scripts");
|
||||
expect(consumer.releaseExcludePaths).toContain("scripts/src/platform-infra-pipelines-as-code*");
|
||||
expect(() => validateConsumerConfig(
|
||||
pac,
|
||||
{ ...consumer, releaseExcludePaths: ["../outside"] },
|
||||
new Set<string>(),
|
||||
new Set<string>(),
|
||||
)).toThrow("releaseExcludePaths must contain safe repository-relative paths");
|
||||
const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
|
||||
expect(remote).toContain("UNIDESK_PAC_MANUAL_RELEASE_INPUT_PATHS_JSON");
|
||||
expect(remote).toContain("UNIDESK_PAC_MANUAL_RELEASE_EXCLUDE_PATHS_JSON");
|
||||
expect(remote).toContain("releaseExcludePaths.some((inputPath) => pathMatches(changedPath, inputPath))");
|
||||
});
|
||||
|
||||
test("PaC failed or expanded plan never points next to trigger", () => {
|
||||
const identity = `sha256:${"d".repeat(64)}`;
|
||||
const args = ["plan", "NC01", "hwlab-nc01-v03", "a".repeat(40), "b".repeat(40), identity] as const;
|
||||
|
||||
@@ -542,9 +542,15 @@ const crypto = require('node:crypto');
|
||||
const changedPaths = fs.readFileSync(process.argv[2], 'utf8').split('\n').filter(Boolean);
|
||||
const domain = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
|
||||
const materializationPaths = JSON.parse(process.env.UNIDESK_PAC_MANUAL_MATERIALIZATION_PATHS_JSON || '[]');
|
||||
const effectiveChangedPaths = materializationPaths.length === 0
|
||||
? changedPaths
|
||||
: changedPaths.filter((changedPath) => materializationPaths.some((inputPath) => changedPath === inputPath || changedPath.startsWith(`${inputPath}/`)));
|
||||
const releaseInputPaths = JSON.parse(process.env.UNIDESK_PAC_MANUAL_RELEASE_INPUT_PATHS_JSON || '[]');
|
||||
const releaseExcludePaths = JSON.parse(process.env.UNIDESK_PAC_MANUAL_RELEASE_EXCLUDE_PATHS_JSON || '[]');
|
||||
const selectedInputPaths = materializationPaths.length > 0 ? materializationPaths : releaseInputPaths;
|
||||
const pathMatches = (changedPath, inputPath) => inputPath.endsWith('*')
|
||||
? changedPath.startsWith(inputPath.slice(0, -1))
|
||||
: changedPath === inputPath || changedPath.startsWith(`${inputPath}/`);
|
||||
const effectiveChangedPaths = changedPaths.filter((changedPath) =>
|
||||
(selectedInputPaths.length === 0 || selectedInputPaths.some((inputPath) => pathMatches(changedPath, inputPath)))
|
||||
&& !releaseExcludePaths.some((inputPath) => pathMatches(changedPath, inputPath)));
|
||||
const configuredImages = JSON.parse(process.env.UNIDESK_PAC_MANUAL_IMAGE_REPOSITORIES_JSON || '[]');
|
||||
const affectedServices = domain?.affectedServices || (effectiveChangedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []);
|
||||
const rolloutServices = domain?.rolloutServices || affectedServices;
|
||||
@@ -610,6 +616,8 @@ const plan = {
|
||||
changedPaths: effectiveChangedPaths,
|
||||
sourceChangedPathCount: changedPaths.length,
|
||||
materializationPaths,
|
||||
releaseInputPaths,
|
||||
releaseExcludePaths,
|
||||
artifactCatalog: domain?.artifactCatalog || null,
|
||||
rangeBaseline: {
|
||||
mode: process.env.BASE_SOURCE,
|
||||
|
||||
@@ -1566,6 +1566,8 @@ function remoteScript(action: "apply" | "status" | "history" | "diagnose-regress
|
||||
UNIDESK_PAC_MANUAL_RUNTIME_SERVICE: manualRelease?.runtimeService ?? "",
|
||||
UNIDESK_PAC_MANUAL_RENDERER: consumer.sourceArtifact?.renderer ?? "generic",
|
||||
UNIDESK_PAC_MANUAL_MATERIALIZATION_PATHS_JSON: JSON.stringify(consumer.materializationPaths),
|
||||
UNIDESK_PAC_MANUAL_RELEASE_INPUT_PATHS_JSON: JSON.stringify(consumer.releaseInputPaths),
|
||||
UNIDESK_PAC_MANUAL_RELEASE_EXCLUDE_PATHS_JSON: JSON.stringify(consumer.releaseExcludePaths),
|
||||
UNIDESK_PAC_MANUAL_NODE_MODULES: manualRelease?.planningNodeModules ?? "",
|
||||
UNIDESK_PAC_MANUAL_ARTIFACT_CATALOG_PATH: manualRelease?.artifactCatalogPath ?? "",
|
||||
UNIDESK_PAC_MANUAL_GITOPS_READ_URL: manualRelease?.gitopsReadUrl ?? "",
|
||||
|
||||
@@ -266,6 +266,8 @@ export interface PacConsumer {
|
||||
params: Record<string, string>;
|
||||
materializationOnly: boolean;
|
||||
materializationPaths: string[];
|
||||
releaseInputPaths: string[];
|
||||
releaseExcludePaths: string[];
|
||||
closeoutGitOpsMirrorFlush: boolean;
|
||||
closeoutGitOpsMirrorLane: "v02" | "v03" | null;
|
||||
sourceArtifact: PacSourceArtifactSpec | null;
|
||||
@@ -714,6 +716,12 @@ export function parseConsumer(consumer: Record<string, unknown>, path: string, d
|
||||
materializationPaths: consumer.materializationPaths === undefined
|
||||
? []
|
||||
: y.stringArrayField(consumer, "materializationPaths", path),
|
||||
releaseInputPaths: consumer.releaseInputPaths === undefined
|
||||
? []
|
||||
: y.stringArrayField(consumer, "releaseInputPaths", path),
|
||||
releaseExcludePaths: consumer.releaseExcludePaths === undefined
|
||||
? []
|
||||
: y.stringArrayField(consumer, "releaseExcludePaths", path),
|
||||
closeoutGitOpsMirrorFlush: y.booleanField(consumer, "closeoutGitOpsMirrorFlush", path),
|
||||
closeoutGitOpsMirrorLane: consumer.closeoutGitOpsMirrorLane === undefined ? null : y.enumField(consumer, "closeoutGitOpsMirrorLane", path, ["v02", "v03"] as const),
|
||||
sourceArtifact: consumer.sourceArtifact === undefined ? null : parseSourceArtifact(y.objectField(consumer, "sourceArtifact", path), `${path}.sourceArtifact`),
|
||||
@@ -1132,6 +1140,13 @@ export function validateConsumerConfig(
|
||||
if (consumer.materializationOnly && consumer.materializationPaths.length === 0) {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.materializationPaths must not be empty when materializationOnly is true`);
|
||||
}
|
||||
for (const [field, paths] of [["releaseInputPaths", consumer.releaseInputPaths], ["releaseExcludePaths", consumer.releaseExcludePaths]] as const) {
|
||||
for (const inputPath of paths) {
|
||||
if (inputPath.startsWith("/") || inputPath.includes("\0") || inputPath.split("/").includes("..")) {
|
||||
throw new Error(`${configLabel}.consumers.${consumer.id}.${field} must contain safe repository-relative paths`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (consumer.id.startsWith("sentinel-")) {
|
||||
if (repository.params.otel_traces_endpoint !== config.observability.tracesEndpoint) {
|
||||
throw new Error(`${configLabel}.repositories.${repository.id}.params.otel_traces_endpoint must match observability.tracesEndpoint`);
|
||||
|
||||
Reference in New Issue
Block a user