实现(08-08-production-deployment-execution): 校准生产身份只读门禁
这个提交包含在:
@@ -14,6 +14,8 @@ const REFRESH_HELPER = path.join(ROOT_DIR, "scripts/refresh-deployment-contract.
|
||||
const REFRESH_INSTALLER = path.join(ROOT_DIR, "deploy/trialrun/install-deployment-contract-refresh.sh");
|
||||
const PRODUCTION_PREFLIGHT = path.join(ROOT_DIR, "deploy/trialrun/production-preflight.mjs");
|
||||
const PRODUCTION_PREFLIGHT_REMOTE = path.join(ROOT_DIR, "deploy/trialrun/production-preflight-remote.sh");
|
||||
const PRODUCTION_IDENTITY_BOOTSTRAP = path.join(ROOT_DIR, "deploy/trialrun/production-identity-bootstrap.mjs");
|
||||
const PRODUCTION_IDENTITY_BOOTSTRAP_REMOTE = path.join(ROOT_DIR, "deploy/trialrun/production-identity-bootstrap-remote.sh");
|
||||
|
||||
describe("deployment safety contract", () => {
|
||||
let fixtureRoot;
|
||||
@@ -108,13 +110,21 @@ describe("deployment safety contract", () => {
|
||||
const wrapper = await readFile(path.join(ROOT_DIR, "deploy/trialrun/deploy-existing.sh"), "utf8");
|
||||
const preflight = await readFile(PRODUCTION_PREFLIGHT, "utf8");
|
||||
const remote = await readFile(PRODUCTION_PREFLIGHT_REMOTE, "utf8");
|
||||
const bootstrap = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP, "utf8");
|
||||
|
||||
expect(wrapper).toContain("EXPECTED_PRODUCTION_ORIGIN");
|
||||
expect(wrapper).toContain("EXPECTED_PRODUCTION_DB_NAME");
|
||||
expect(wrapper).toContain("EXPECTED_PRODUCTION_DB_NAME_SHA256");
|
||||
expect(wrapper).toContain("EXPECTED_PENDING_MIGRATIONS");
|
||||
expect(wrapper).toContain("EXPECTED_DEPLOY_SERVER");
|
||||
expect(wrapper).toContain("EXPECTED_REMOTE_DIR");
|
||||
expect(wrapper).toContain("EXPECTED_COMPOSE_PROJECT_NAME");
|
||||
expect(wrapper).toContain("PRODUCTION_READ_ONLY_PREFLIGHT");
|
||||
expect(wrapper.indexOf('node "$ROOT_DIR/$PRODUCTION_PREFLIGHT_SCRIPT"')).toBeLessThan(
|
||||
wrapper.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"'),
|
||||
);
|
||||
expect(wrapper.indexOf('if [[ "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" == "1" ]]')).toBeLessThan(
|
||||
wrapper.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"'),
|
||||
);
|
||||
expect(preflight).toContain("CANDIDATE_MIGRATIONS_CSV");
|
||||
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_MISSING");
|
||||
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_UNREADABLE");
|
||||
@@ -128,6 +138,12 @@ describe("deployment safety contract", () => {
|
||||
expect(remote).toContain("rollbackWindow=OPEN");
|
||||
expect(remote).not.toMatch(/\b(prisma migrate reset|prisma db push|prisma migrate resolve)\b/);
|
||||
expect(remote.indexOf("PRISMA_STATUS_BEGIN")).toBeLessThan(remote.indexOf("_prisma_migrations"));
|
||||
expect(bootstrap.indexOf("assertExactHistoricalContract")).toBeLessThan(
|
||||
bootstrap.indexOf("bootstrapDatabaseIdentityHash"),
|
||||
);
|
||||
expect(bootstrap.indexOf("bootstrapDatabaseIdentityHash(identity")).toBeLessThan(
|
||||
bootstrap.indexOf("spawnSync(\"bash\", [DEPLOY_EXISTING_SCRIPT]"),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects loopback/staging production identities without invoking SSH", async () => {
|
||||
@@ -147,6 +163,53 @@ describe("deployment safety contract", () => {
|
||||
expect(() => target.validateProductionDbName("hyb_oa_regression")).toThrowError(
|
||||
expect.objectContaining({ code: "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY" }),
|
||||
);
|
||||
expect(() => target.validateExactProductionIdentity({
|
||||
deployServer: "deploy.production.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
expectedDeployServer: "deploy.production.invalid",
|
||||
expectedRemoteDir: "/srv/hyb-oa-trialrun",
|
||||
expectedComposeProjectName: "hyb-oa-trialrun",
|
||||
})).not.toThrow();
|
||||
expect(() => target.validateExactProductionIdentity({
|
||||
deployServer: "deploy.production.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
expectedDeployServer: "deploy.production.invalid",
|
||||
expectedRemoteDir: "/srv/different",
|
||||
expectedComposeProjectName: "hyb-oa-trialrun",
|
||||
})).toThrowError(expect.objectContaining({ code: "REMOTE_DIR_MISMATCH" }));
|
||||
expect(() => target.validateExactProductionIdentity({
|
||||
deployServer: "deploy.production.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
expectedDeployServer: "different.production.invalid",
|
||||
expectedRemoteDir: "/srv/hyb-oa-trialrun",
|
||||
expectedComposeProjectName: "hyb-oa-trialrun",
|
||||
})).toThrowError(expect.objectContaining({ code: "DEPLOY_SERVER_MISMATCH" }));
|
||||
expect(() => target.validateExactProductionIdentity({
|
||||
deployServer: "deploy.production.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
expectedDeployServer: "deploy.production.invalid",
|
||||
expectedRemoteDir: "/srv/hyb-oa-trialrun",
|
||||
expectedComposeProjectName: "different",
|
||||
})).toThrowError(expect.objectContaining({ code: "COMPOSE_PROJECT_NAME_MISMATCH" }));
|
||||
expect(() => target.validateExactProductionIdentity({
|
||||
deployServer: "deploy.production.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
expectedDeployServer: "deploy.production.invalid",
|
||||
expectedRemoteDir: "/srv/hyb-oa-trialrun",
|
||||
})).toThrowError(expect.objectContaining({ code: "EXPECTED_COMPOSE_PROJECT_NAME_MISSING" }));
|
||||
expect(() => target.validateExactProductionIdentity({
|
||||
deployServer: "deploy.staging.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
expectedDeployServer: "deploy.staging.invalid",
|
||||
expectedRemoteDir: "/srv/hyb-oa-trialrun",
|
||||
expectedComposeProjectName: "hyb-oa-trialrun",
|
||||
})).toThrowError(expect.objectContaining({ code: "DEPLOY_SERVER_NON_PRODUCTION_IDENTITY" }));
|
||||
expect(() => target.validatePrismaStatusText("")).toThrowError(
|
||||
expect.objectContaining({ code: "PRISMA_STATUS_OUTPUT_UNREADABLE" }),
|
||||
);
|
||||
@@ -188,16 +251,19 @@ describe("deployment safety contract", () => {
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`,
|
||||
DEPLOY_SERVER: "deploy.example.invalid",
|
||||
DEPLOY_SERVER: "deploy.production.invalid",
|
||||
REMOTE_DIR: remoteDir,
|
||||
REMOTE_ENV_FILE: envFile,
|
||||
COMPOSE_FILE: "deploy/trialrun/docker-compose.yml",
|
||||
COMPOSE_PROJECT_NAME: "production_app",
|
||||
COMPOSE_PROJECT_NAME: "hyb-oa-trialrun",
|
||||
POSTGRES_DATA_DIR: postgresDir,
|
||||
STORAGE_HOST_DIR: storageDir,
|
||||
EXPECTED_PRODUCTION_ORIGIN: "https://oa.hyinda.com",
|
||||
EXPECTED_PRODUCTION_DB_NAME: "hyb_oa",
|
||||
EXPECTED_PRODUCTION_DB_NAME_SHA256: createHash("sha256").update("hyb_oa", "utf8").digest("hex"),
|
||||
EXPECTED_PENDING_MIGRATIONS: "",
|
||||
EXPECTED_DEPLOY_SERVER: "deploy.production.invalid",
|
||||
EXPECTED_REMOTE_DIR: remoteDir,
|
||||
EXPECTED_COMPOSE_PROJECT_NAME: "hyb-oa-trialrun",
|
||||
CANDIDATE_COMMIT: candidateCommit,
|
||||
PRODUCTION_PREFLIGHT_REPORT_DIR: reportDir,
|
||||
FAKE_SSH_CAPTURE: sshCapture,
|
||||
@@ -218,6 +284,63 @@ describe("deployment safety contract", () => {
|
||||
expect(sshCommand).toContain("EXPECTED_PRODUCTION_DB_NAME_SHA256");
|
||||
});
|
||||
|
||||
it("bootstraps only an exact historical identity and a unique refresh probe", async () => {
|
||||
const bootstrap = await import(pathToFileURL(PRODUCTION_IDENTITY_BOOTSTRAP).href);
|
||||
const historicalTarget = [
|
||||
'DEPLOY_SERVER="${DEPLOY_SERVER:-deploy.production.invalid}"',
|
||||
'REMOTE_DIR="${REMOTE_DIR:-/srv/hyb-oa-trialrun}"',
|
||||
].join("\n");
|
||||
const historicalProject = [
|
||||
'REMOTE_DIR="${REMOTE_DIR:-/srv/hyb-oa-trialrun}"',
|
||||
'COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-hyb-oa-trialrun}"',
|
||||
].join("\n");
|
||||
expect(bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject)).toEqual({
|
||||
deployServer: "deploy.production.invalid",
|
||||
remoteDir: "/srv/hyb-oa-trialrun",
|
||||
composeProjectName: "hyb-oa-trialrun",
|
||||
});
|
||||
expect(bootstrap.selectUniqueRefreshTarget([
|
||||
{ target: "ordinary", status: 1, stdout: "" },
|
||||
{ target: "refresh", status: 0, stdout: "[deployment-contract-refresh][PASS] probe\n" },
|
||||
])).toBe("refresh");
|
||||
expect(() => bootstrap.selectUniqueRefreshTarget([])).toThrowError(
|
||||
expect.objectContaining({ code: "REFRESH_IDENTITY_NOT_FOUND" }),
|
||||
);
|
||||
expect(() => bootstrap.selectUniqueRefreshTarget([
|
||||
{ target: "refresh-a", status: 0, stdout: "[deployment-contract-refresh][PASS] probe" },
|
||||
{ target: "refresh-b", status: 0, stdout: "[deployment-contract-refresh][PASS] probe" },
|
||||
])).toThrowError(expect.objectContaining({ code: "REFRESH_IDENTITY_NOT_UNIQUE" }));
|
||||
|
||||
const contract = bootstrap.parseDeploymentContract([
|
||||
"REMOTE_DIR=/srv/hyb-oa-trialrun",
|
||||
"REMOTE_ENV_FILE=/srv/hyb-oa-trialrun/deploy/trialrun/.env",
|
||||
"COMPOSE_FILE=deploy/trialrun/docker-compose.yml",
|
||||
"COMPOSE_PROJECT_NAME=hyb-oa-trialrun",
|
||||
"POSTGRES_DATA_DIR=/srv/data/postgres",
|
||||
"STORAGE_HOST_DIR=/srv/data/storage",
|
||||
].join(" "));
|
||||
expect(() => bootstrap.assertExactHistoricalContract(
|
||||
bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject),
|
||||
contract,
|
||||
)).not.toThrow();
|
||||
expect(() => bootstrap.assertExactHistoricalContract(
|
||||
bootstrap.parseHistoricalDeploymentIdentity(historicalTarget, historicalProject),
|
||||
{ ...contract, COMPOSE_PROJECT_NAME: "different" },
|
||||
)).toThrowError(expect.objectContaining({ code: "SYSTEMD_COMPOSE_PROJECT_MISMATCH" }));
|
||||
expect(() => bootstrap.parseHistoricalDeploymentIdentity(
|
||||
historicalTarget,
|
||||
historicalProject.replace("/srv/hyb-oa-trialrun", "/srv/different"),
|
||||
)).toThrowError(expect.objectContaining({ code: "HISTORICAL_REMOTE_DIR_DIVERGED" }));
|
||||
|
||||
const bootstrapSource = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP, "utf8");
|
||||
const remoteSource = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP_REMOTE, "utf8");
|
||||
expect(bootstrapSource).not.toContain("console.log(databaseIdentityHash)");
|
||||
expect(bootstrapSource).not.toContain("console.log(identity)");
|
||||
expect(remoteSource).toContain("DB_SHA256");
|
||||
expect(remoteSource).not.toContain("DB_BASE64");
|
||||
expect(remoteSource).not.toContain("prisma migrate status");
|
||||
});
|
||||
|
||||
it("runs the remote gate against synthetic status and migration history", async () => {
|
||||
const fakeBin = path.join(fixtureRoot, "remote-bin");
|
||||
await mkdir(fakeBin, { recursive: true });
|
||||
@@ -277,7 +400,7 @@ describe("deployment safety contract", () => {
|
||||
REMOTE_DIR: remoteDir,
|
||||
REMOTE_ENV_FILE: envFile,
|
||||
COMPOSE_FILE: "deploy/trialrun/docker-compose.yml",
|
||||
COMPOSE_PROJECT_NAME: "production_app",
|
||||
COMPOSE_PROJECT_NAME: "hyb-oa-trialrun",
|
||||
POSTGRES_DATA_DIR: postgresDir,
|
||||
STORAGE_HOST_DIR: storageDir,
|
||||
EXPECTED_PRODUCTION_ORIGIN_SHA256: hash("https://oa.example.com"),
|
||||
|
||||
在新工单中引用
屏蔽一个用户