berg-pages 0.1.0-alpha.6
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/CHANGELOG.md +93 -0
- package/LICENSE +183 -0
- package/README.md +93 -0
- package/dist/adapters/local-deploy-execution-ports.d.ts +12 -0
- package/dist/adapters/local-deploy-execution-ports.js +81 -0
- package/dist/adapters/local-environment-probe.d.ts +5 -0
- package/dist/adapters/local-environment-probe.js +54 -0
- package/dist/adapters/local-init-apply-ports.d.ts +10 -0
- package/dist/adapters/local-init-apply-ports.js +47 -0
- package/dist/adapters/local-project-inspector.d.ts +4 -0
- package/dist/adapters/local-project-inspector.js +184 -0
- package/dist/application/contracts/plugins/deploy-mode-plugin.d.ts +20 -0
- package/dist/application/contracts/plugins/deploy-mode-plugin.js +1 -0
- package/dist/application/contracts/plugins/framework-plugin.d.ts +19 -0
- package/dist/application/contracts/plugins/framework-plugin.js +1 -0
- package/dist/application/contracts/ports/environment-probe-port.d.ts +4 -0
- package/dist/application/contracts/ports/environment-probe-port.js +1 -0
- package/dist/application/contracts/ports/project-inspector-port.d.ts +28 -0
- package/dist/application/contracts/ports/project-inspector-port.js +1 -0
- package/dist/application/contracts/project-context.d.ts +6 -0
- package/dist/application/contracts/project-context.js +1 -0
- package/dist/application/index.d.ts +11 -0
- package/dist/application/index.js +11 -0
- package/dist/application/usecases/apply-deploy-plan.d.ts +8 -0
- package/dist/application/usecases/apply-deploy-plan.js +100 -0
- package/dist/application/usecases/apply-init-plan.d.ts +13 -0
- package/dist/application/usecases/apply-init-plan.js +65 -0
- package/dist/application/usecases/build-deploy-plan.d.ts +9 -0
- package/dist/application/usecases/build-deploy-plan.js +177 -0
- package/dist/application/usecases/build-doctor-plan.d.ts +9 -0
- package/dist/application/usecases/build-doctor-plan.js +235 -0
- package/dist/application/usecases/build-init-plan.d.ts +9 -0
- package/dist/application/usecases/build-init-plan.js +140 -0
- package/dist/application/usecases/create-project-context.d.ts +8 -0
- package/dist/application/usecases/create-project-context.js +168 -0
- package/dist/application/usecases/workflow-diagnostics.d.ts +13 -0
- package/dist/application/usecases/workflow-diagnostics.js +137 -0
- package/dist/core/diagnostics/diagnostic.d.ts +11 -0
- package/dist/core/diagnostics/diagnostic.js +1 -0
- package/dist/core/entities/command.d.ts +1 -0
- package/dist/core/entities/command.js +1 -0
- package/dist/core/entities/deploy-mode.d.ts +6 -0
- package/dist/core/entities/deploy-mode.js +1 -0
- package/dist/core/entities/environment.d.ts +16 -0
- package/dist/core/entities/environment.js +1 -0
- package/dist/core/entities/framework.d.ts +8 -0
- package/dist/core/entities/framework.js +1 -0
- package/dist/core/entities/pages-target.d.ts +22 -0
- package/dist/core/entities/pages-target.js +1 -0
- package/dist/core/entities/project.d.ts +45 -0
- package/dist/core/entities/project.js +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/plans/assumption.d.ts +7 -0
- package/dist/core/plans/assumption.js +1 -0
- package/dist/core/plans/build-plan.d.ts +12 -0
- package/dist/core/plans/build-plan.js +1 -0
- package/dist/core/plans/command-plan.d.ts +27 -0
- package/dist/core/plans/command-plan.js +45 -0
- package/dist/core/plans/deploy-plan.d.ts +17 -0
- package/dist/core/plans/deploy-plan.js +1 -0
- package/dist/core/plans/plan-step.d.ts +59 -0
- package/dist/core/plans/plan-step.js +1 -0
- package/dist/core/plans/template-plan.d.ts +11 -0
- package/dist/core/plans/template-plan.js +1 -0
- package/dist/features/deployment-modes/git-pages-deploy-mode-plugin.d.ts +12 -0
- package/dist/features/deployment-modes/git-pages-deploy-mode-plugin.js +49 -0
- package/dist/features/frameworks/astro-framework-plugin.d.ts +36 -0
- package/dist/features/frameworks/astro-framework-plugin.js +97 -0
- package/dist/features/frameworks/static-html-framework-plugin.d.ts +36 -0
- package/dist/features/frameworks/static-html-framework-plugin.js +94 -0
- package/dist/features/frameworks/vite-framework-plugin.d.ts +36 -0
- package/dist/features/frameworks/vite-framework-plugin.js +97 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/interfaces/cli/main.d.ts +2 -0
- package/dist/interfaces/cli/main.js +157 -0
- package/dist/interfaces/cli/render-command-plan.d.ts +2 -0
- package/dist/interfaces/cli/render-command-plan.js +87 -0
- package/docs/CANARY.md +117 -0
- package/docs/COMPATIBILITY.md +88 -0
- package/docs/DEPLOY-SAFETY.md +113 -0
- package/package.json +83 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const CONFIG_FILE_CANDIDATES = [
|
|
8
|
+
"vite.config.ts",
|
|
9
|
+
"vite.config.mts",
|
|
10
|
+
"vite.config.cts",
|
|
11
|
+
"vite.config.js",
|
|
12
|
+
"vite.config.mjs",
|
|
13
|
+
"vite.config.cjs",
|
|
14
|
+
"astro.config.ts",
|
|
15
|
+
"astro.config.mts",
|
|
16
|
+
"astro.config.js",
|
|
17
|
+
"astro.config.mjs"
|
|
18
|
+
];
|
|
19
|
+
const WORKFLOW_DIRECTORY = ".forgejo/workflows";
|
|
20
|
+
const WORKFLOW_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
21
|
+
const BUILD_OUTPUT_DIR_CANDIDATES = ["dist", "public"];
|
|
22
|
+
export class LocalProjectInspector {
|
|
23
|
+
async inspect(projectRoot) {
|
|
24
|
+
const rootDir = resolve(projectRoot);
|
|
25
|
+
const fixtureMetadata = await readFixtureMetadata(rootDir);
|
|
26
|
+
const packageJson = await readPackageJson(rootDir);
|
|
27
|
+
const detectedConfigFiles = await detectConfigFiles(rootDir);
|
|
28
|
+
const detectedBuildOutputDirs = await detectBuildOutputDirs(rootDir);
|
|
29
|
+
const detectedDomainsFile = await detectDomainsFile(rootDir);
|
|
30
|
+
const detectedWorkflowFileContents = await detectWorkflowFileContents(rootDir);
|
|
31
|
+
const detectedWorkflowFiles = Object.keys(detectedWorkflowFileContents);
|
|
32
|
+
const [remoteUrl, currentBranch, isDirty] = await Promise.all([
|
|
33
|
+
resolveGitRemoteUrl(rootDir, fixtureMetadata),
|
|
34
|
+
resolveCurrentBranch(rootDir, fixtureMetadata),
|
|
35
|
+
resolveDirtyState(rootDir, fixtureMetadata)
|
|
36
|
+
]);
|
|
37
|
+
return {
|
|
38
|
+
rootDir,
|
|
39
|
+
git: {
|
|
40
|
+
...(remoteUrl ? { remoteUrl } : {}),
|
|
41
|
+
...(fixtureMetadata.git?.defaultBranch ? { defaultBranch: fixtureMetadata.git.defaultBranch } : {}),
|
|
42
|
+
...(currentBranch ? { currentBranch } : {}),
|
|
43
|
+
...(isDirty !== undefined ? { isDirty } : {})
|
|
44
|
+
},
|
|
45
|
+
...(packageJson ? { packageJson } : {}),
|
|
46
|
+
detectedConfigFiles,
|
|
47
|
+
detectedBuildOutputDirs,
|
|
48
|
+
...(detectedDomainsFile ? { detectedDomainsFile } : {}),
|
|
49
|
+
detectedWorkflowFiles,
|
|
50
|
+
detectedWorkflowFileContents
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function readFixtureMetadata(rootDir) {
|
|
55
|
+
const fixturePath = resolve(rootDir, ".berg-pages-fixture.json");
|
|
56
|
+
try {
|
|
57
|
+
const contents = await readFile(fixturePath, "utf8");
|
|
58
|
+
return JSON.parse(contents);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function readPackageJson(rootDir) {
|
|
65
|
+
const packageJsonPath = resolve(rootDir, "package.json");
|
|
66
|
+
try {
|
|
67
|
+
const contents = await readFile(packageJsonPath, "utf8");
|
|
68
|
+
const parsed = JSON.parse(contents);
|
|
69
|
+
return {
|
|
70
|
+
...(parsed.name ? { name: parsed.name } : {}),
|
|
71
|
+
scripts: parsed.scripts ?? {},
|
|
72
|
+
dependencies: Object.keys(parsed.dependencies ?? {}),
|
|
73
|
+
devDependencies: Object.keys(parsed.devDependencies ?? {})
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function detectConfigFiles(rootDir) {
|
|
81
|
+
const detected = [];
|
|
82
|
+
for (const fileName of CONFIG_FILE_CANDIDATES) {
|
|
83
|
+
if (await fileExists(resolve(rootDir, fileName))) {
|
|
84
|
+
detected.push(fileName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return detected;
|
|
88
|
+
}
|
|
89
|
+
async function detectBuildOutputDirs(rootDir) {
|
|
90
|
+
const detected = [];
|
|
91
|
+
for (const dirName of BUILD_OUTPUT_DIR_CANDIDATES) {
|
|
92
|
+
if (await directoryExists(resolve(rootDir, dirName))) {
|
|
93
|
+
detected.push(dirName);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return detected;
|
|
97
|
+
}
|
|
98
|
+
async function detectDomainsFile(rootDir) {
|
|
99
|
+
const domainsPath = resolve(rootDir, ".domains");
|
|
100
|
+
if (!(await fileExists(domainsPath))) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const contents = await readFile(domainsPath, "utf8");
|
|
104
|
+
const domains = contents
|
|
105
|
+
.split(/\r?\n/)
|
|
106
|
+
.map((line) => line.trim())
|
|
107
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
108
|
+
return {
|
|
109
|
+
path: ".domains",
|
|
110
|
+
domains
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function detectWorkflowFileContents(rootDir) {
|
|
114
|
+
const detected = {};
|
|
115
|
+
const workflowDirectoryPath = resolve(rootDir, WORKFLOW_DIRECTORY);
|
|
116
|
+
let entries;
|
|
117
|
+
try {
|
|
118
|
+
entries = await readdir(workflowDirectoryPath, { withFileTypes: true });
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return detected;
|
|
122
|
+
}
|
|
123
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
124
|
+
if (!entry.isFile()) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const lowerName = entry.name.toLowerCase();
|
|
128
|
+
if (!WORKFLOW_FILE_EXTENSIONS.some((extension) => lowerName.endsWith(extension))) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const workflowPath = `${WORKFLOW_DIRECTORY}/${entry.name}`;
|
|
132
|
+
detected[workflowPath] = await readFile(resolve(rootDir, workflowPath), "utf8");
|
|
133
|
+
}
|
|
134
|
+
return detected;
|
|
135
|
+
}
|
|
136
|
+
async function resolveGitRemoteUrl(rootDir, fixtureMetadata) {
|
|
137
|
+
return fixtureMetadata.git?.remoteUrl ?? runGit(["config", "--get", "remote.origin.url"], rootDir);
|
|
138
|
+
}
|
|
139
|
+
async function resolveCurrentBranch(rootDir, fixtureMetadata) {
|
|
140
|
+
return fixtureMetadata.git?.currentBranch ?? runGit(["branch", "--show-current"], rootDir);
|
|
141
|
+
}
|
|
142
|
+
async function resolveDirtyState(rootDir, fixtureMetadata) {
|
|
143
|
+
if (fixtureMetadata.git?.isDirty !== undefined) {
|
|
144
|
+
return fixtureMetadata.git.isDirty;
|
|
145
|
+
}
|
|
146
|
+
const output = await runGit(["status", "--porcelain"], rootDir);
|
|
147
|
+
if (output === undefined) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return output.length > 0;
|
|
151
|
+
}
|
|
152
|
+
async function runGit(args, cwd) {
|
|
153
|
+
try {
|
|
154
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
155
|
+
cwd,
|
|
156
|
+
windowsHide: true
|
|
157
|
+
});
|
|
158
|
+
const trimmed = stdout.trim();
|
|
159
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function fileExists(path) {
|
|
166
|
+
try {
|
|
167
|
+
await access(path, constants.F_OK);
|
|
168
|
+
const fileStat = await stat(path);
|
|
169
|
+
return fileStat.isFile();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function directoryExists(path) {
|
|
176
|
+
try {
|
|
177
|
+
await access(path, constants.F_OK);
|
|
178
|
+
const fileStat = await stat(path);
|
|
179
|
+
return fileStat.isDirectory();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DeployModeId } from "../../../core/entities/deploy-mode.js";
|
|
2
|
+
import type { PagesTarget } from "../../../core/entities/pages-target.js";
|
|
3
|
+
import type { Project } from "../../../core/entities/project.js";
|
|
4
|
+
import type { Diagnostic } from "../../../core/diagnostics/diagnostic.js";
|
|
5
|
+
import type { DeployPlan, DeployResult } from "../../../core/plans/deploy-plan.js";
|
|
6
|
+
import type { DeleteFileStep, EnsureDirectoryStep, PublishSiteStep, RunCommandStep, WriteFileStep } from "../../../core/plans/plan-step.js";
|
|
7
|
+
export interface DeployExecutionPorts {
|
|
8
|
+
writeFile(step: WriteFileStep): Promise<void>;
|
|
9
|
+
deleteFile(step: DeleteFileStep): Promise<void>;
|
|
10
|
+
ensureDirectory(step: EnsureDirectoryStep): Promise<void>;
|
|
11
|
+
runCommand(step: RunCommandStep): Promise<void>;
|
|
12
|
+
publish(step: PublishSiteStep): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface DeployModePlugin {
|
|
15
|
+
id: DeployModeId;
|
|
16
|
+
supports(target: PagesTarget): boolean;
|
|
17
|
+
getDiagnostics(project: Project): Diagnostic[];
|
|
18
|
+
getDeployPlan(project: Project): DeployPlan;
|
|
19
|
+
execute(plan: DeployPlan, ports: DeployExecutionPorts): Promise<DeployResult>;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BuildPlan } from "../../../core/plans/build-plan.js";
|
|
2
|
+
import type { TemplatePlan } from "../../../core/plans/template-plan.js";
|
|
3
|
+
import type { FrameworkId } from "../../../core/entities/framework.js";
|
|
4
|
+
import type { Project } from "../../../core/entities/project.js";
|
|
5
|
+
import type { Diagnostic } from "../../../core/diagnostics/diagnostic.js";
|
|
6
|
+
import type { ProjectContext } from "../project-context.js";
|
|
7
|
+
export interface DetectionResult {
|
|
8
|
+
matches: boolean;
|
|
9
|
+
confidence: "low" | "medium" | "high";
|
|
10
|
+
reasons: string[];
|
|
11
|
+
detectedFrameworkId?: FrameworkId;
|
|
12
|
+
}
|
|
13
|
+
export interface FrameworkPlugin {
|
|
14
|
+
id: FrameworkId;
|
|
15
|
+
detect(context: ProjectContext): Promise<DetectionResult>;
|
|
16
|
+
getBuildPlan(project: Project): BuildPlan;
|
|
17
|
+
getInitTemplates(project: Project): TemplatePlan[];
|
|
18
|
+
getDiagnostics(project: Project): Diagnostic[];
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface GitInspection {
|
|
2
|
+
remoteUrl?: string;
|
|
3
|
+
defaultBranch?: string;
|
|
4
|
+
currentBranch?: string;
|
|
5
|
+
isDirty?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface PackageJsonInspection {
|
|
8
|
+
name?: string;
|
|
9
|
+
scripts: Record<string, string>;
|
|
10
|
+
dependencies: string[];
|
|
11
|
+
devDependencies: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface ProjectInspection {
|
|
14
|
+
rootDir: string;
|
|
15
|
+
git: GitInspection;
|
|
16
|
+
packageJson?: PackageJsonInspection;
|
|
17
|
+
detectedConfigFiles: string[];
|
|
18
|
+
detectedBuildOutputDirs: string[];
|
|
19
|
+
detectedDomainsFile?: {
|
|
20
|
+
path: ".domains";
|
|
21
|
+
domains: string[];
|
|
22
|
+
};
|
|
23
|
+
detectedWorkflowFiles: string[];
|
|
24
|
+
detectedWorkflowFileContents: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export interface ProjectInspectorPort {
|
|
27
|
+
inspect(projectRoot: string): Promise<ProjectInspection>;
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./contracts/plugins/deploy-mode-plugin.js";
|
|
2
|
+
export * from "./contracts/plugins/framework-plugin.js";
|
|
3
|
+
export * from "./contracts/ports/environment-probe-port.js";
|
|
4
|
+
export * from "./contracts/ports/project-inspector-port.js";
|
|
5
|
+
export * from "./contracts/project-context.js";
|
|
6
|
+
export * from "./usecases/create-project-context.js";
|
|
7
|
+
export * from "./usecases/apply-init-plan.js";
|
|
8
|
+
export * from "./usecases/apply-deploy-plan.js";
|
|
9
|
+
export * from "./usecases/build-doctor-plan.js";
|
|
10
|
+
export * from "./usecases/build-deploy-plan.js";
|
|
11
|
+
export * from "./usecases/build-init-plan.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./contracts/plugins/deploy-mode-plugin.js";
|
|
2
|
+
export * from "./contracts/plugins/framework-plugin.js";
|
|
3
|
+
export * from "./contracts/ports/environment-probe-port.js";
|
|
4
|
+
export * from "./contracts/ports/project-inspector-port.js";
|
|
5
|
+
export * from "./contracts/project-context.js";
|
|
6
|
+
export * from "./usecases/create-project-context.js";
|
|
7
|
+
export * from "./usecases/apply-init-plan.js";
|
|
8
|
+
export * from "./usecases/apply-deploy-plan.js";
|
|
9
|
+
export * from "./usecases/build-doctor-plan.js";
|
|
10
|
+
export * from "./usecases/build-deploy-plan.js";
|
|
11
|
+
export * from "./usecases/build-init-plan.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DeployExecutionPorts } from "../contracts/plugins/deploy-mode-plugin.js";
|
|
2
|
+
import type { Diagnostic } from "../../core/diagnostics/diagnostic.js";
|
|
3
|
+
import type { CommandPlan } from "../../core/plans/command-plan.js";
|
|
4
|
+
export interface DeployApplyResult {
|
|
5
|
+
appliedStepIds: string[];
|
|
6
|
+
diagnostics: Diagnostic[];
|
|
7
|
+
}
|
|
8
|
+
export declare function applyDeployPlan(plan: CommandPlan, ports: DeployExecutionPorts): Promise<DeployApplyResult>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export async function applyDeployPlan(plan, ports) {
|
|
2
|
+
if (plan.command !== "deploy") {
|
|
3
|
+
return rejected("deploy.apply.command-mismatch", "Only deploy plans can be applied.");
|
|
4
|
+
}
|
|
5
|
+
if (!plan.safeToApply) {
|
|
6
|
+
return rejected("deploy.apply.blocked", "The deploy plan has blocking diagnostics and was not applied.");
|
|
7
|
+
}
|
|
8
|
+
const unsupportedStep = plan.steps.find((step) => step.kind === "write-file" ||
|
|
9
|
+
step.kind === "delete-file" ||
|
|
10
|
+
step.kind === "ensure-directory" ||
|
|
11
|
+
step.kind === "run-command");
|
|
12
|
+
if (unsupportedStep) {
|
|
13
|
+
return rejected("deploy.apply.unsupported-step", `The deploy plan contains unsupported apply step "${unsupportedStep.kind}".`);
|
|
14
|
+
}
|
|
15
|
+
const publishSteps = plan.steps.filter((step) => step.kind === "publish-site");
|
|
16
|
+
if (publishSteps.length === 0) {
|
|
17
|
+
return rejected("deploy.apply.publish-step.missing", "The deploy plan does not include a publish step.");
|
|
18
|
+
}
|
|
19
|
+
if (publishSteps.length > 1) {
|
|
20
|
+
return rejected("deploy.apply.publish-step.ambiguous", "The deploy plan includes more than one publish step.");
|
|
21
|
+
}
|
|
22
|
+
const publishStep = publishSteps[0];
|
|
23
|
+
if (!publishStep) {
|
|
24
|
+
return rejected("deploy.apply.publish-step.missing", "The deploy plan does not include a publish step.");
|
|
25
|
+
}
|
|
26
|
+
const invalidPublishStepReason = validatePublishStep(publishStep);
|
|
27
|
+
if (invalidPublishStepReason) {
|
|
28
|
+
return rejected("deploy.apply.publish-step.invalid", invalidPublishStepReason);
|
|
29
|
+
}
|
|
30
|
+
const appliedStepIds = [];
|
|
31
|
+
for (const step of [publishStep]) {
|
|
32
|
+
try {
|
|
33
|
+
await ports.publish(step);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
return {
|
|
38
|
+
appliedStepIds,
|
|
39
|
+
diagnostics: [
|
|
40
|
+
{
|
|
41
|
+
code: "deploy.apply.publish.failed",
|
|
42
|
+
severity: "error",
|
|
43
|
+
summary: `Publish failed for ${step.destination}.`,
|
|
44
|
+
detail: message,
|
|
45
|
+
suggestions: [
|
|
46
|
+
"Inspect the remote, credentials, and pages branch before retrying.",
|
|
47
|
+
"Run deploy --dry-run again to confirm the publish plan still matches expectations."
|
|
48
|
+
],
|
|
49
|
+
source: "application"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
appliedStepIds.push(step.id);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
appliedStepIds,
|
|
58
|
+
diagnostics: []
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function validatePublishStep(step) {
|
|
62
|
+
if (step.modeId !== "git-pages") {
|
|
63
|
+
return `Unsupported deploy mode for apply: ${step.modeId}.`;
|
|
64
|
+
}
|
|
65
|
+
if (step.targetKind !== "codeberg-project") {
|
|
66
|
+
return `Unsupported deploy target for apply: ${step.targetKind}.`;
|
|
67
|
+
}
|
|
68
|
+
if (step.branch !== "pages") {
|
|
69
|
+
return `Refusing to publish to undeclared git-pages branch: ${step.branch}.`;
|
|
70
|
+
}
|
|
71
|
+
if (!isCodebergRemote(step.remote)) {
|
|
72
|
+
return `Refusing to publish to a non-Codeberg remote: ${step.remote}.`;
|
|
73
|
+
}
|
|
74
|
+
if (!step.outputDir) {
|
|
75
|
+
return "Refusing to publish without an output directory.";
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function isCodebergRemote(remote) {
|
|
80
|
+
return /^https:\/\/codeberg\.org\/[^/]+\/[^/]+(?:\.git)?$/i.test(remote) ||
|
|
81
|
+
/^git@codeberg\.org:[^/]+\/[^/]+(?:\.git)?$/i.test(remote);
|
|
82
|
+
}
|
|
83
|
+
function rejected(code, summary) {
|
|
84
|
+
return {
|
|
85
|
+
appliedStepIds: [],
|
|
86
|
+
diagnostics: [
|
|
87
|
+
{
|
|
88
|
+
code,
|
|
89
|
+
severity: "error",
|
|
90
|
+
summary,
|
|
91
|
+
detail: "Deploy apply is intentionally limited to declared publish steps in M6.",
|
|
92
|
+
suggestions: [
|
|
93
|
+
"Resolve diagnostics with deploy --dry-run before applying.",
|
|
94
|
+
"Do not use deploy apply for hidden build, file, or branch mutation outside the plan."
|
|
95
|
+
],
|
|
96
|
+
source: "application"
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CommandPlan } from "../../core/plans/command-plan.js";
|
|
2
|
+
import type { Diagnostic } from "../../core/diagnostics/diagnostic.js";
|
|
3
|
+
import type { EnsureDirectoryStep, WriteFileStep } from "../../core/plans/plan-step.js";
|
|
4
|
+
export interface InitApplyPorts {
|
|
5
|
+
ensureDirectory(step: EnsureDirectoryStep): Promise<void>;
|
|
6
|
+
writeFile(step: WriteFileStep): Promise<void>;
|
|
7
|
+
verifyFile(step: WriteFileStep): Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
export interface InitApplyResult {
|
|
10
|
+
appliedStepIds: string[];
|
|
11
|
+
diagnostics: Diagnostic[];
|
|
12
|
+
}
|
|
13
|
+
export declare function applyInitPlan(plan: CommandPlan, ports: InitApplyPorts): Promise<InitApplyResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export async function applyInitPlan(plan, ports) {
|
|
2
|
+
if (plan.command !== "init") {
|
|
3
|
+
return rejected("init.apply.command-mismatch", "Only init plans can be applied.");
|
|
4
|
+
}
|
|
5
|
+
if (!plan.safeToApply) {
|
|
6
|
+
return rejected("init.apply.blocked", "The init plan has blocking diagnostics and was not applied.");
|
|
7
|
+
}
|
|
8
|
+
const unsupportedStep = plan.steps.find((step) => step.kind === "delete-file" ||
|
|
9
|
+
step.kind === "run-command" ||
|
|
10
|
+
step.kind === "publish-site");
|
|
11
|
+
if (unsupportedStep) {
|
|
12
|
+
return rejected("init.apply.unsupported-step", `The init plan contains unsupported apply step "${unsupportedStep.kind}".`);
|
|
13
|
+
}
|
|
14
|
+
const appliedStepIds = [];
|
|
15
|
+
for (const step of plan.steps) {
|
|
16
|
+
if (step.kind === "ensure-directory") {
|
|
17
|
+
await ports.ensureDirectory(step);
|
|
18
|
+
appliedStepIds.push(step.id);
|
|
19
|
+
}
|
|
20
|
+
if (step.kind === "write-file") {
|
|
21
|
+
await ports.writeFile(step);
|
|
22
|
+
if (!(await ports.verifyFile(step))) {
|
|
23
|
+
return {
|
|
24
|
+
appliedStepIds,
|
|
25
|
+
diagnostics: [
|
|
26
|
+
{
|
|
27
|
+
code: "init.apply.verify-file.failed",
|
|
28
|
+
severity: "error",
|
|
29
|
+
summary: `Post-apply verification failed for ${step.path}.`,
|
|
30
|
+
detail: "The file content on disk did not match the planned content after apply.",
|
|
31
|
+
suggestions: [
|
|
32
|
+
"Inspect the file manually before retrying apply.",
|
|
33
|
+
"Run init --dry-run again to refresh the expected plan."
|
|
34
|
+
],
|
|
35
|
+
source: "application"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
appliedStepIds.push(step.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
appliedStepIds,
|
|
45
|
+
diagnostics: []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function rejected(code, summary) {
|
|
49
|
+
return {
|
|
50
|
+
appliedStepIds: [],
|
|
51
|
+
diagnostics: [
|
|
52
|
+
{
|
|
53
|
+
code,
|
|
54
|
+
severity: "error",
|
|
55
|
+
summary,
|
|
56
|
+
detail: "Apply is intentionally limited to safe local init file steps in M4.",
|
|
57
|
+
suggestions: [
|
|
58
|
+
"Resolve diagnostics with init --dry-run before applying.",
|
|
59
|
+
"Do not use apply for deploy or publish behavior."
|
|
60
|
+
],
|
|
61
|
+
source: "application"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DeployModePlugin } from "../contracts/plugins/deploy-mode-plugin.js";
|
|
2
|
+
import type { FrameworkPlugin } from "../contracts/plugins/framework-plugin.js";
|
|
3
|
+
import type { ProjectContext } from "../contracts/project-context.js";
|
|
4
|
+
export interface BuildDeployPlanInput {
|
|
5
|
+
context: ProjectContext;
|
|
6
|
+
frameworkPlugin?: FrameworkPlugin;
|
|
7
|
+
deployModePlugin?: DeployModePlugin;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildDeployPlan(input: BuildDeployPlanInput): import("../../core/plans/command-plan.js").CommandPlan;
|