@synopackageland/cli 0.2.0 → 0.3.0
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.
- package/dist/bin.js +3 -5
- package/dist/cli.js +21 -5
- package/dist/publish/preflight.d.ts +24 -0
- package/dist/publish/preflight.js +116 -0
- package/dist/publish/publish.d.ts +4 -0
- package/dist/publish/publish.js +13 -5
- package/dist/release-bin.d.ts +2 -0
- package/dist/release-bin.js +19 -0
- package/package.json +3 -2
package/dist/bin.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadDotEnv } from "./load-env.js";
|
|
3
3
|
import { runCli } from "./cli.js";
|
|
4
|
-
import { runCuratedReleaseCli } from "./publish/curated-release.js";
|
|
5
4
|
loadDotEnv();
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
execution.then((status) => {
|
|
5
|
+
// Release authority lives in the separate `synopkgland-release` binary, so the
|
|
6
|
+
// developer-facing tool has no publish surface at all.
|
|
7
|
+
runCli(process.argv).then(() => 0).then((status) => {
|
|
10
8
|
process.exitCode = status;
|
|
11
9
|
}).catch((error) => {
|
|
12
10
|
console.error(error instanceof Error ? error.message : String(error));
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { followProjectLogs } from "./log.js";
|
|
|
16
16
|
import { testProjectOnHost } from "./test-host.js";
|
|
17
17
|
import { formatSkillList, readSkill, resolveSkillsRoot } from "./skill.js";
|
|
18
18
|
import { publishProject, resolveCommunityAppsRoot } from "./publish/publish.js";
|
|
19
|
+
import { formatPreflight, runPublicReleasePreflight } from "./publish/preflight.js";
|
|
19
20
|
import { createExecGitHubCli, createExecGitOps } from "./publish/github-pr.js";
|
|
20
21
|
import { communityAppsGitHubRepo } from "./publish/github-repo.js";
|
|
21
22
|
function collectKvOption(value, previous, flag, example) {
|
|
@@ -52,12 +53,11 @@ export async function runCli(argv) {
|
|
|
52
53
|
program
|
|
53
54
|
.command("validate")
|
|
54
55
|
.description("驗證 App Contract 與 Compose")
|
|
55
|
-
.option("--curated", "使用 Curated listing profile(未宣告 privilege 為紅燈)")
|
|
56
56
|
.action((options, command) => {
|
|
57
57
|
const global = command.parent?.opts() ?? {};
|
|
58
58
|
const project = resolveProject(process.cwd(), global.project);
|
|
59
59
|
const result = validateProject(project, {
|
|
60
|
-
profile:
|
|
60
|
+
profile: "personal",
|
|
61
61
|
});
|
|
62
62
|
if (result.warnings && result.warnings.length > 0) {
|
|
63
63
|
console.error(formatErrors(result.warnings));
|
|
@@ -329,15 +329,31 @@ export async function runCli(argv) {
|
|
|
329
329
|
console.log(`已產生 ${result.outputPath}`);
|
|
330
330
|
});
|
|
331
331
|
program
|
|
332
|
-
.command("
|
|
333
|
-
.description("
|
|
332
|
+
.command("public-release-submit")
|
|
333
|
+
.description("送審公開發佈:先跑完本機所有 Curated 檢查,通過才對 Community Apps 建立 Pull Request。"
|
|
334
|
+
+ "不上傳 SPK、不簽署、不發佈——實際發佈由 maintainer 合併後的 trusted CI 執行。")
|
|
335
|
+
.option("--check", "只跑本機檢查,不建立 Pull Request")
|
|
334
336
|
.option("--base <branch>", "PR 目標分支", "master")
|
|
335
337
|
.action(async (options, command) => {
|
|
336
338
|
const global = command.parent?.opts() ?? {};
|
|
337
339
|
const project = resolveProject(process.cwd(), global.project);
|
|
340
|
+
const preflight = await runPublicReleasePreflight(project);
|
|
341
|
+
if (!preflight.ok) {
|
|
342
|
+
// A gate's reasons belong on stderr so CI logs and `2>` capture them.
|
|
343
|
+
console.error(formatPreflight(preflight));
|
|
344
|
+
console.error("\n本機檢查未通過,未建立 Pull Request。");
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
console.log(formatPreflight(preflight));
|
|
348
|
+
if (options.check) {
|
|
349
|
+
console.log("\n本機檢查全部通過。加上 --check 以外的執行才會送審。");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
338
352
|
const communityAppsRoot = resolveCommunityAppsRoot(project.root);
|
|
339
353
|
if (!communityAppsRoot) {
|
|
340
|
-
console.error("
|
|
354
|
+
console.error("送審需要 Community Apps 定義倉庫的本機副本。請先取得它,再從你的 App 專案目錄重跑:\n"
|
|
355
|
+
+ " git clone https://github.com/synopackageland/synopkgland-community-apps.git\n"
|
|
356
|
+
+ " export SYNOPKGLAND_COMMUNITY_APPS_ROOT=$PWD/synopkgland-community-apps");
|
|
341
357
|
process.exit(1);
|
|
342
358
|
}
|
|
343
359
|
const repoRoot = dirname(communityAppsRoot);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProjectRoot } from "../discovery.js";
|
|
2
|
+
export interface PreflightCheck {
|
|
3
|
+
/** Stable id so scripts and docs can refer to a specific gate. */
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
detail?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface PreflightResult {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
checks: PreflightCheck[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Every gate for a public release that can be decided on the developer's own
|
|
15
|
+
* machine. Submitting runs these first so a definition that CI would reject
|
|
16
|
+
* never reaches review; `--check` runs the same set and stops.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately excludes anything needing release authority: Build allocation,
|
|
19
|
+
* attestation signing and catalog publication belong to trusted CI running
|
|
20
|
+
* from the merged revision, and no local run can stand in for a maintainer's
|
|
21
|
+
* approval.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runPublicReleasePreflight(project: ProjectRoot): Promise<PreflightResult>;
|
|
24
|
+
export declare function formatPreflight(result: PreflightResult): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isDevelopmentPackageIdentity } from "@synopackageland/package-source/index-manifest";
|
|
5
|
+
import { buildSpk } from "../build-spk/generator.js";
|
|
6
|
+
import { loadCompose } from "../compose/analyze.js";
|
|
7
|
+
import { loadContract } from "../contract/load.js";
|
|
8
|
+
import { isNativeArchFamily } from "../build-spk/dsm-arch.js";
|
|
9
|
+
import { loadNative } from "../native/load.js";
|
|
10
|
+
import { validateProject } from "../validate.js";
|
|
11
|
+
import { validateCuratedCompose } from "./curated-release.js";
|
|
12
|
+
/**
|
|
13
|
+
* Every gate for a public release that can be decided on the developer's own
|
|
14
|
+
* machine. Submitting runs these first so a definition that CI would reject
|
|
15
|
+
* never reaches review; `--check` runs the same set and stops.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately excludes anything needing release authority: Build allocation,
|
|
18
|
+
* attestation signing and catalog publication belong to trusted CI running
|
|
19
|
+
* from the merged revision, and no local run can stand in for a maintainer's
|
|
20
|
+
* approval.
|
|
21
|
+
*/
|
|
22
|
+
export async function runPublicReleasePreflight(project) {
|
|
23
|
+
const checks = [];
|
|
24
|
+
const add = (check) => {
|
|
25
|
+
checks.push(check);
|
|
26
|
+
return check.ok;
|
|
27
|
+
};
|
|
28
|
+
const validation = validateProject(project, { profile: "listing" });
|
|
29
|
+
add({
|
|
30
|
+
id: "listing-validate",
|
|
31
|
+
title: "App Contract 與 runtime 通過 Curated listing profile",
|
|
32
|
+
ok: validation.ok,
|
|
33
|
+
detail: validation.ok ? undefined : validation.errors.map((error) => error.code).join(", "),
|
|
34
|
+
});
|
|
35
|
+
const contract = loadContract(project.contractPath).contract;
|
|
36
|
+
const packageIdentity = typeof contract.package === "string" ? contract.package : "";
|
|
37
|
+
const formalIdentity = packageIdentity.length > 0 && !isDevelopmentPackageIdentity(packageIdentity);
|
|
38
|
+
add({
|
|
39
|
+
id: "formal-identity",
|
|
40
|
+
title: "Package Identity 是正式身分(不是 .dev)",
|
|
41
|
+
ok: formalIdentity,
|
|
42
|
+
detail: formalIdentity
|
|
43
|
+
? undefined
|
|
44
|
+
: packageIdentity.length === 0
|
|
45
|
+
? "App Contract 缺少 package 欄位。"
|
|
46
|
+
: `Curated 不接受 development identity:${packageIdentity}`,
|
|
47
|
+
});
|
|
48
|
+
const nativePath = project.nativePath;
|
|
49
|
+
const isNative = nativePath !== undefined && existsSync(nativePath);
|
|
50
|
+
if (isNative) {
|
|
51
|
+
const parsedNative = loadNative(nativePath);
|
|
52
|
+
const families = Object.keys(parsedNative?.native.targets ?? {}).filter(isNativeArchFamily);
|
|
53
|
+
const pinned = families.filter((family) => {
|
|
54
|
+
const target = parsedNative?.native.targets?.[family];
|
|
55
|
+
return typeof target?.url === "string" && typeof target?.sha256 === "string";
|
|
56
|
+
});
|
|
57
|
+
add({
|
|
58
|
+
id: "curated-runtime",
|
|
59
|
+
title: "每個 native target 都以 url + sha256 釘住",
|
|
60
|
+
ok: families.length > 0 && pinned.length === families.length,
|
|
61
|
+
detail: families.length === 0
|
|
62
|
+
? "native.yaml 沒有宣告任何 target。"
|
|
63
|
+
: pinned.length === families.length
|
|
64
|
+
? undefined
|
|
65
|
+
: `缺少 url 或 sha256:${families.filter((f) => !pinned.includes(f)).join(", ")}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const parsedCompose = loadCompose(project.composePath);
|
|
70
|
+
const gate = parsedCompose
|
|
71
|
+
? validateCuratedCompose(parsedCompose.compose)
|
|
72
|
+
: { ok: false, errors: ["CURATED_COMPOSE_MISSING"] };
|
|
73
|
+
add({
|
|
74
|
+
id: "curated-runtime",
|
|
75
|
+
title: "Compose 只用釘住 digest 的公開 image,且沒有 build:",
|
|
76
|
+
ok: gate.ok,
|
|
77
|
+
detail: gate.ok ? undefined : gate.errors.join(", "),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// The strongest local gate: actually produce the artifact. For native this
|
|
81
|
+
// fetches each pinned asset and refuses it unless the bytes hash to the
|
|
82
|
+
// declared sha256, so a bad pin fails here instead of in CI.
|
|
83
|
+
if (checks.every((check) => check.ok)) {
|
|
84
|
+
const staging = mkdtempSync(join(tmpdir(), "synopkgland-preflight-"));
|
|
85
|
+
try {
|
|
86
|
+
const families = isNative
|
|
87
|
+
? Object.keys(loadNative(nativePath)?.native.targets ?? {}).filter(isNativeArchFamily)
|
|
88
|
+
: [undefined];
|
|
89
|
+
for (const family of families) {
|
|
90
|
+
const built = await buildSpk({
|
|
91
|
+
project,
|
|
92
|
+
outputPath: join(staging, `${packageIdentity}-${family ?? "noarch"}.spk`),
|
|
93
|
+
...(family ? { archFamily: family } : {}),
|
|
94
|
+
});
|
|
95
|
+
add({
|
|
96
|
+
id: family ? `spk-builds:${family}` : "spk-builds",
|
|
97
|
+
title: family ? `SPK 可建置(${family},含 sha256 比對)` : "SPK 可建置",
|
|
98
|
+
ok: built.ok,
|
|
99
|
+
detail: built.ok ? undefined : built.errors.join(", "),
|
|
100
|
+
});
|
|
101
|
+
if (!built.ok) {
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rmSync(staging, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ok: checks.every((check) => check.ok), checks };
|
|
111
|
+
}
|
|
112
|
+
export function formatPreflight(result) {
|
|
113
|
+
return result.checks
|
|
114
|
+
.map((check) => `${check.ok ? "✔" : "✖"} ${check.title}${check.detail ? `\n ${check.detail}` : ""}`)
|
|
115
|
+
.join("\n");
|
|
116
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ProjectRoot } from "../discovery.js";
|
|
2
|
+
import { type PreflightResult } from "./preflight.js";
|
|
2
3
|
import type { GitHubCli, GitOps } from "./github-pr.js";
|
|
3
4
|
export interface PublishProjectOptions {
|
|
5
|
+
/** Injected in tests; production runs the real local gate set. */
|
|
6
|
+
preflight?: (project: ProjectRoot) => Promise<PreflightResult>;
|
|
4
7
|
project: ProjectRoot;
|
|
5
8
|
communityAppsRoot?: string;
|
|
6
9
|
baseBranch?: string;
|
|
@@ -10,6 +13,7 @@ export interface PublishProjectOptions {
|
|
|
10
13
|
now?: () => Date;
|
|
11
14
|
}
|
|
12
15
|
export interface PublishProjectResult {
|
|
16
|
+
preflight?: PreflightResult;
|
|
13
17
|
ok: boolean;
|
|
14
18
|
code?: string;
|
|
15
19
|
message?: string;
|
package/dist/publish/publish.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import { loadContract } from "../contract/load.js";
|
|
3
|
-
import {
|
|
3
|
+
import { runPublicReleasePreflight } from "./preflight.js";
|
|
4
4
|
import { assertDirectoryMatchesPackage, COMMUNITY_APPS_APPS_DIR, COMMUNITY_APPS_DIR, communityAppDirectory, resolveCommunityAppsRoot, } from "./community-path.js";
|
|
5
5
|
import { listSyncedRelativePaths, syncPackagingDefinition } from "./sync-definition.js";
|
|
6
6
|
function slugifyPackageIdentity(packageIdentity) {
|
|
@@ -28,12 +28,20 @@ function renderPullRequestBody(packageIdentity, appRelativePath, files) {
|
|
|
28
28
|
].join("\n");
|
|
29
29
|
}
|
|
30
30
|
export async function publishProject(options) {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Every locally decidable gate runs before anything is pushed, so a
|
|
32
|
+
// definition CI would reject never costs a maintainer a review.
|
|
33
|
+
const preflight = options.preflight
|
|
34
|
+
? await options.preflight(options.project)
|
|
35
|
+
: await runPublicReleasePreflight(options.project);
|
|
36
|
+
if (!preflight.ok) {
|
|
33
37
|
return {
|
|
34
38
|
ok: false,
|
|
35
|
-
code: "
|
|
36
|
-
message:
|
|
39
|
+
code: "PREFLIGHT_FAILED",
|
|
40
|
+
message: preflight.checks
|
|
41
|
+
.filter((check) => !check.ok)
|
|
42
|
+
.map((check) => `${check.id}${check.detail ? `: ${check.detail}` : ""}`)
|
|
43
|
+
.join("; "),
|
|
44
|
+
preflight,
|
|
37
45
|
};
|
|
38
46
|
}
|
|
39
47
|
const communityRoot = options.communityAppsRoot ?? resolveCommunityAppsRoot(options.project.root);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Trusted-CI entry point. Kept out of the `synopkgland` developer binary on
|
|
4
|
+
* purpose: allocating a Package Build, signing attestations and publishing to
|
|
5
|
+
* the Curated Package Source require release authority that a developer's
|
|
6
|
+
* machine must never hold. Only CI running from the reviewed merged revision
|
|
7
|
+
* has the keyring, the ledger write credential and the publish credential.
|
|
8
|
+
*/
|
|
9
|
+
import { loadDotEnv } from "./load-env.js";
|
|
10
|
+
import { runCuratedReleaseCli } from "./publish/curated-release.js";
|
|
11
|
+
loadDotEnv();
|
|
12
|
+
runCuratedReleaseCli(["node", "synopkgland-release", "release", ...process.argv.slice(2)])
|
|
13
|
+
.then((status) => {
|
|
14
|
+
process.exitCode = status;
|
|
15
|
+
})
|
|
16
|
+
.catch((error) => {
|
|
17
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Syno Package Land Developer Kit CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"synopkgland": "./dist/bin.js"
|
|
7
|
+
"synopkgland": "./dist/bin.js",
|
|
8
|
+
"synopkgland-release": "./dist/release-bin.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"dist",
|