@aipt/idp-deploy 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +471 -0
  2. package/bin/idpctl.mjs +8 -0
  3. package/compose/edge.yaml +61 -0
  4. package/compose/flow.yaml +34 -0
  5. package/compose/portal.yaml +38 -0
  6. package/compose/postgresql.yaml +62 -0
  7. package/compose/registry.yaml +35 -0
  8. package/compose/smartgo.yaml +271 -0
  9. package/compose/tech.yaml +64 -0
  10. package/contracts/active-profile.schema.json +19 -0
  11. package/contracts/asset-lifecycle.schema.json +49 -0
  12. package/contracts/backup-generation.schema.json +60 -0
  13. package/contracts/component-runtime.schema.json +32 -0
  14. package/contracts/config-release.schema.json +33 -0
  15. package/contracts/deployment-evidence.schema.json +43 -0
  16. package/contracts/deployment-plan.schema.json +67 -0
  17. package/contracts/release-candidate.schema.json +33 -0
  18. package/contracts/release-defaults.schema.json +56 -0
  19. package/contracts/restore-candidate.schema.json +63 -0
  20. package/contracts/smartgo-component-config.schema.json +79 -0
  21. package/contracts/tech-backup-boundary.schema.json +61 -0
  22. package/contracts/tech-source-credentials.schema.json +17 -0
  23. package/deploy.sh +5 -0
  24. package/docs/restore-runbook.md +56 -0
  25. package/governance/asset-lifecycle.v1.json +79 -0
  26. package/package.json +42 -0
  27. package/release/defaults.v1.json +64 -0
  28. package/src/acceptance.mjs +127 -0
  29. package/src/bindings.mjs +518 -0
  30. package/src/cli.mjs +360 -0
  31. package/src/compose.mjs +19 -0
  32. package/src/config.mjs +903 -0
  33. package/src/delivery.mjs +123 -0
  34. package/src/errors.mjs +12 -0
  35. package/src/foundation-contracts.mjs +128 -0
  36. package/src/foundation.mjs +107 -0
  37. package/src/gitops.mjs +285 -0
  38. package/src/hash.mjs +47 -0
  39. package/src/image-lock.mjs +160 -0
  40. package/src/images.mjs +215 -0
  41. package/src/lifecycle.mjs +58 -0
  42. package/src/local-source.mjs +354 -0
  43. package/src/oci-mirror.mjs +10 -0
  44. package/src/operations.mjs +1484 -0
  45. package/src/process.mjs +46 -0
  46. package/src/profiles.mjs +41 -0
  47. package/src/release.mjs +1760 -0
  48. package/src/render.mjs +330 -0
  49. package/src/security.mjs +156 -0
  50. package/src/source-contracts.mjs +106 -0
  51. package/src/sources.mjs +78 -0
  52. package/src/workbench-projects.mjs +118 -0
@@ -0,0 +1,64 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "releaseVersion": "0.1.2",
4
+ "ociRepositoryRoot": "idp-registry.cn-shanghai.cr.aliyuncs.com/education/oci",
5
+ "platforms": [
6
+ "linux/amd64",
7
+ "linux/arm64"
8
+ ],
9
+ "thirdPartyImages": {
10
+ "IDP_POSTGRES_IMAGE": "postgres:16-alpine@sha256:cf78e76683b9ca8c5733cbbdce6c9262b45b6767934dd0a95e671f9a0fc20685",
11
+ "IDP_CADDY_IMAGE": "caddy:2-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648",
12
+ "IDP_REGISTRY_IMAGE": "verdaccio/verdaccio:6@sha256:fcb86134563534e2f634752e6c6c3edcdb78242ec16578c73ce39d1dadbaa801",
13
+ "IDP_SMARTGO_OBJECT_STORE_IMAGE": "quay.io/minio/minio:RELEASE.2025-04-22T22-12-26Z@sha256:a1ea29fa28355559ef137d71fc570e508a214ec84ff8083e39bc5428980b015e",
14
+ "IDP_SMARTGO_OBJECT_STORE_CLIENT_IMAGE": "quay.io/minio/mc:RELEASE.2025-04-16T18-13-26Z@sha256:aead63c77f9db9107f1696fb08ecb0faeda23729cde94b0f663edf4fe09728e3"
15
+ },
16
+ "buildImages": {
17
+ "tech": {
18
+ "environmentKey": "IDP_TECH_IMAGE",
19
+ "sourceDirectory": "tech",
20
+ "context": ".",
21
+ "dockerfile": "apps/knowledge-service/Dockerfile",
22
+ "repositorySuffix": "",
23
+ "buildArgs": {
24
+ "NODE_IMAGE": "node:22-bookworm-slim@sha256:83f487e0a63425e5b4d146fb5e5be574bcbe1b7b843d3ebafdd95eaf7767a7e5"
25
+ },
26
+ "buildSecretFiles": {},
27
+ "prebuildContract": "none"
28
+ },
29
+ "flow": {
30
+ "environmentKey": "IDP_FLOW_IMAGE",
31
+ "sourceDirectory": "flow",
32
+ "context": ".",
33
+ "dockerfile": "deploy/Dockerfile",
34
+ "repositorySuffix": "/flow",
35
+ "buildArgs": {},
36
+ "buildSecretFiles": {},
37
+ "prebuildContract": "none"
38
+ },
39
+ "portal": {
40
+ "environmentKey": "IDP_PORTAL_IMAGE",
41
+ "sourceDirectory": "portal",
42
+ "context": ".",
43
+ "dockerfile": "packages/backend/Dockerfile",
44
+ "repositorySuffix": "/portal",
45
+ "buildArgs": {},
46
+ "buildSecretFiles": {},
47
+ "prebuildContract": "portal-backstage-v1"
48
+ },
49
+ "smartgo": {
50
+ "environmentKey": "IDP_SMARTGO_IMAGE",
51
+ "sourceDirectory": "smartGO",
52
+ "context": ".",
53
+ "dockerfile": "Dockerfile",
54
+ "repositorySuffix": "/smartgo",
55
+ "buildArgs": {
56
+ "NODE_IMAGE": "node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd"
57
+ },
58
+ "buildSecretFiles": {
59
+ "npmrc": "SMARTGO_NPMRC_FILE"
60
+ },
61
+ "prebuildContract": "smartgo-managed-v1"
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,127 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { doctorConfig, referencedFiles } from './config.mjs';
6
+ import { IdpError, invariant } from './errors.mjs';
7
+ import {
8
+ composeOperation,
9
+ createBackup,
10
+ testRestore,
11
+ verifyBackup,
12
+ verifyDeployment,
13
+ withInstanceLock,
14
+ writeReceipt,
15
+ } from './operations.mjs';
16
+ import { run } from './process.mjs';
17
+ import { imageKeysForProfile } from './images.mjs';
18
+ import { resolveProfile } from './profiles.mjs';
19
+ import { assertSafeRegularFile, createExclusiveFile } from './security.mjs';
20
+
21
+ function parseDockerInfo(stdout) {
22
+ let document;
23
+ try { document = JSON.parse(String(Buffer.isBuffer(stdout) ? stdout.toString('utf8') : stdout)); }
24
+ catch { throw new IdpError('IDP_M4_DOCKER_INFO_INVALID', '无法解析Docker Engine信息'); }
25
+ const architecture = String(document.Architecture ?? document.ArchitectureName ?? '').toLowerCase();
26
+ invariant(['arm64', 'aarch64'].includes(architecture), 'IDP_M4_DOCKER_ARCH_INVALID', `Docker Engine不是ARM64:${architecture || 'unknown'}`);
27
+ invariant(String(document.OSType ?? '').toLowerCase() === 'linux', 'IDP_M4_DOCKER_OS_INVALID', 'Docker Engine容器OS必须是linux');
28
+ return { architecture, osType: 'linux', serverVersion: String(document.ServerVersion ?? 'unknown'), operatingSystem: String(document.OperatingSystem ?? 'unknown') };
29
+ }
30
+
31
+ export function verifyMacM4Host({ runner = run, host = { platform: os.platform(), arch: os.arch() } } = {}) {
32
+ invariant(host.platform === 'darwin' && host.arch === 'arm64', 'IDP_M4_NATIVE_HOST_REQUIRED', `M4正式验收必须在darwin/arm64原生宿主执行,当前为${host.platform}/${host.arch}`);
33
+ const result = runner('docker', ['info', '--format', '{{json .}}'], { capture: true });
34
+ return { host, docker: parseDockerInfo(result.stdout) };
35
+ }
36
+
37
+ function npmEnvironment(runtimeDirectory) {
38
+ const environment = { ...process.env };
39
+ for (const key of Object.keys(environment)) {
40
+ if (key.startsWith('NPM_CONFIG_') || /(?:NPM|NODE)_AUTH_TOKEN/u.test(key)) delete environment[key];
41
+ }
42
+ environment.NPM_CONFIG_CACHE = path.join(runtimeDirectory, 'npm-cache');
43
+ environment.NPM_CONFIG_GLOBALCONFIG = path.join(runtimeDirectory, 'empty-global-npmrc');
44
+ return environment;
45
+ }
46
+
47
+ function prepareNpmAcceptance({ root, env, runner }) {
48
+ const ref = referencedFiles(root).find((entry) => entry.key === 'IDP_REGISTRY_ACCEPTANCE_NPMRC_FILE');
49
+ invariant(ref, 'IDP_REGISTRY_ACCEPTANCE_NPMRC_MISSING', '缺少IDP_REGISTRY_ACCEPTANCE_NPMRC_FILE声明');
50
+ assertSafeRegularFile(ref.absolute, 0o600, { allowEmpty: false, role: 'Registry验收npmrc' });
51
+ const runtimeDirectory = fs.mkdtempSync(path.join(env.IDP_RUNTIME_DIR, 'tmp', 'npm-acceptance-'));
52
+ fs.chmodSync(runtimeDirectory, 0o700);
53
+ fs.mkdirSync(path.join(runtimeDirectory, 'consumer'), { mode: 0o700 });
54
+ createExclusiveFile(path.join(runtimeDirectory, 'empty-global-npmrc'), '', 0o600);
55
+ const packageName = `@aipt/idp-acceptance-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
56
+ const version = '1.0.0';
57
+ createExclusiveFile(path.join(runtimeDirectory, 'package.json'), `${JSON.stringify({ name: packageName, version, private: false, main: 'index.js', scripts: {} }, null, 2)}\n`, 0o600);
58
+ createExclusiveFile(path.join(runtimeDirectory, 'index.js'), "export const acceptance = 'passed';\n", 0o600);
59
+ createExclusiveFile(path.join(runtimeDirectory, 'consumer', 'package.json'), `${JSON.stringify({ name: 'idp-acceptance-consumer', version: '1.0.0', private: true }, null, 2)}\n`, 0o600);
60
+ const registry = `http://127.0.0.1:${env.IDP_REGISTRY_PORT}`;
61
+ const common = ['--userconfig', ref.absolute, '--globalconfig', path.join(runtimeDirectory, 'empty-global-npmrc'), '--registry', registry, '--ignore-scripts'];
62
+ const processEnvironment = npmEnvironment(runtimeDirectory);
63
+ const publish = () => runner('npm', ['publish', ...common], { cwd: runtimeDirectory, env: processEnvironment, capture: true });
64
+ const install = () => {
65
+ runner('npm', ['install', `${packageName}@${version}`, ...common, '--no-audit', '--no-fund'], { cwd: path.join(runtimeDirectory, 'consumer'), env: processEnvironment, capture: true });
66
+ const installed = path.join(runtimeDirectory, 'consumer', 'node_modules', '@aipt', packageName.slice('@aipt/'.length), 'package.json');
67
+ assertSafeRegularFile(installed, 0o644, { allowEmpty: false, role: 'Registry验收安装产物' });
68
+ };
69
+ const unpublish = () => runner('npm', ['unpublish', `${packageName}@${version}`, '--force', ...common], { cwd: runtimeDirectory, env: processEnvironment, capture: true });
70
+ const cleanup = () => fs.rmSync(runtimeDirectory, { recursive: true, force: true });
71
+ return { packageName, version, publish, install, unpublish, cleanup };
72
+ }
73
+
74
+ export function acceptMacM4({ repositoryRoot, configRoot, profile = 'core', confirmation, runner = run, host } = {}) {
75
+ const preliminary = doctorConfig(configRoot, { requireConfigured: true, profile });
76
+ invariant(confirmation === preliminary.env.IDP_INSTANCE_ID, 'IDP_M4_RESTART_CONFIRMATION_REQUIRED', 'M4验收包含一次冷停机,必须用--confirm-restart提供完全相同的IDP_INSTANCE_ID');
77
+ invariant(preliminary.env.IDP_RUNTIME_PLATFORM === 'linux/arm64', 'IDP_M4_RUNTIME_PLATFORM_INVALID', 'M4正式验收要求IDP_RUNTIME_PLATFORM=linux/arm64');
78
+ const native = verifyMacM4Host({ runner, host });
79
+ const active = new Set(resolveProfile(profile));
80
+ invariant(active.has('registry') || active.has('postgresql'), 'IDP_M4_ACCEPTANCE_PROFILE_INVALID', 'M4完整验收要求Profile至少包含Registry或PostgreSQL持久数据');
81
+
82
+ composeOperation({ repositoryRoot, configRoot, profile, command: ['up', '-d', '--wait'], allowProfileSwitch: true, runner });
83
+ const initial = verifyDeployment({ repositoryRoot, configRoot, profile, runner }).receipt;
84
+ const images = [];
85
+ for (const key of imageKeysForProfile(profile)) {
86
+ const inspected = runner('docker', ['image', 'inspect', preliminary.env[key], '--format', '{{.Os}}/{{.Architecture}}'], { capture: true });
87
+ const platform = String(inspected.stdout).trim().toLowerCase();
88
+ invariant(['linux/arm64', 'linux/aarch64'].includes(platform), 'IDP_M4_IMAGE_PLATFORM_INVALID', `${key}本地镜像平台不是linux/arm64:${platform}`);
89
+ images.push({ key, platform });
90
+ }
91
+
92
+ const backup = createBackup({ repositoryRoot, configRoot, profile, runner });
93
+ const backupVerified = verifyBackup({ configRoot, generationId: backup.receipt.generationId }).receipt;
94
+ const restoreTested = testRestore({ repositoryRoot, configRoot, generationId: backup.receipt.generationId, runner }).receipt;
95
+ const npmAcceptance = active.has('registry') ? prepareNpmAcceptance({ root: preliminary.root, env: preliminary.env, runner }) : null;
96
+ let stopped = false;
97
+ let published = false;
98
+ try {
99
+ if (npmAcceptance) { npmAcceptance.publish(); published = true; }
100
+ composeOperation({ repositoryRoot, configRoot, profile, command: ['down'], runner });
101
+ stopped = true;
102
+ composeOperation({ repositoryRoot, configRoot, profile, command: ['up', '-d', '--wait'], runner });
103
+ stopped = false;
104
+ const restarted = verifyDeployment({ repositoryRoot, configRoot, profile, runner }).receipt;
105
+ if (npmAcceptance) npmAcceptance.install();
106
+ if (npmAcceptance) { npmAcceptance.unpublish(); published = false; }
107
+ const { root, env, report } = doctorConfig(configRoot, { requireConfigured: true, profile });
108
+ return withInstanceLock(root, env, 'acceptance:mac-m4', () => writeReceipt(env, 'acceptance-mac-m4', {
109
+ profile, configDigest: report.digest, native, images, initialVerifyDigest: initial.digest,
110
+ coldRestartVerifyDigest: restarted.digest, backupGenerationId: backup.receipt.generationId,
111
+ backupVerifyDigest: backupVerified.digest, restoreTestDigest: restoreTested.digest,
112
+ registryPublishInstall: npmAcceptance ? { packageName: npmAcceptance.packageName, version: npmAcceptance.version, status: 'passed-across-cold-restart' } : null,
113
+ status: 'passed',
114
+ }));
115
+ } catch (error) {
116
+ if (stopped) {
117
+ try { composeOperation({ repositoryRoot, configRoot, profile, command: ['up', '-d', '--wait'], runner }); }
118
+ catch (recoveryError) { throw new IdpError('IDP_M4_ACCEPTANCE_RECOVERY_FAILED', `M4验收失败且服务恢复失败:${recoveryError.message}`, { originalError: error.message }); }
119
+ }
120
+ throw error;
121
+ } finally {
122
+ if (npmAcceptance) {
123
+ if (published) { try { npmAcceptance.unpublish(); } catch {} }
124
+ npmAcceptance.cleanup();
125
+ }
126
+ }
127
+ }