@org-press/deploy-github-pages 0.9.12
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/LICENSE +29 -0
- package/README.md +177 -0
- package/dist/index.js +235 -0
- package/package.json +48 -0
- package/src/adapter.test.ts +759 -0
- package/src/adapter.ts +365 -0
- package/src/index.ts +27 -0
- package/src/types.ts +58 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Pages Deploy Adapter
|
|
3
|
+
*
|
|
4
|
+
* Deploys static sites to GitHub Pages by pushing to the gh-pages branch.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { writeFileSync, existsSync, rmSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import type {
|
|
11
|
+
DeployAdapter,
|
|
12
|
+
AdapterConfig,
|
|
13
|
+
ValidationResult,
|
|
14
|
+
DeployContext,
|
|
15
|
+
DeployResult,
|
|
16
|
+
} from "@org-press/deploy";
|
|
17
|
+
import type { GitHubPagesConfig } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* GitHub Pages Deploy Adapter
|
|
21
|
+
*
|
|
22
|
+
* Deploys static files to GitHub Pages by:
|
|
23
|
+
* 1. Initializing a git repo in the output directory
|
|
24
|
+
* 2. Adding .nojekyll and CNAME files as configured
|
|
25
|
+
* 3. Committing all files
|
|
26
|
+
* 4. Force pushing to the gh-pages branch
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const adapter = new GitHubPagesAdapter({
|
|
31
|
+
* repo: 'user/my-site',
|
|
32
|
+
* cname: 'mysite.com',
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* const result = await adapter.deploy(context);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class GitHubPagesAdapter implements DeployAdapter {
|
|
39
|
+
readonly name = "github-pages";
|
|
40
|
+
readonly description = "Deploy to GitHub Pages";
|
|
41
|
+
|
|
42
|
+
private config: GitHubPagesConfig;
|
|
43
|
+
|
|
44
|
+
constructor(config: GitHubPagesConfig = {}) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate adapter configuration
|
|
50
|
+
*
|
|
51
|
+
* Checks:
|
|
52
|
+
* - git is available in PATH
|
|
53
|
+
* - Repository format is valid (if specified)
|
|
54
|
+
*/
|
|
55
|
+
async validate(adapterConfig: AdapterConfig): Promise<ValidationResult> {
|
|
56
|
+
const errors: string[] = [];
|
|
57
|
+
const warnings: string[] = [];
|
|
58
|
+
|
|
59
|
+
// Check git is available
|
|
60
|
+
const gitCheck = spawnSync("git", ["--version"], {
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
timeout: 5000,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (gitCheck.status !== 0) {
|
|
66
|
+
errors.push("git is not available in PATH");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Validate repository format if specified
|
|
70
|
+
const repo =
|
|
71
|
+
(adapterConfig.options.repo as string) || this.config.repo;
|
|
72
|
+
|
|
73
|
+
if (repo) {
|
|
74
|
+
const repoPattern = /^[\w.-]+\/[\w.-]+$/;
|
|
75
|
+
if (!repoPattern.test(repo)) {
|
|
76
|
+
errors.push(
|
|
77
|
+
`Invalid repository format: "${repo}". Expected format: "user/repo" or "org/repo"`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate branch name if specified
|
|
83
|
+
const branch =
|
|
84
|
+
(adapterConfig.options.branch as string) ||
|
|
85
|
+
this.config.branch ||
|
|
86
|
+
"gh-pages";
|
|
87
|
+
|
|
88
|
+
const branchPattern = /^[\w./-]+$/;
|
|
89
|
+
if (!branchPattern.test(branch)) {
|
|
90
|
+
errors.push(`Invalid branch name: "${branch}"`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Warn if no repo specified and might fail to auto-detect
|
|
94
|
+
if (!repo) {
|
|
95
|
+
warnings.push(
|
|
96
|
+
"No repository specified. Will attempt to auto-detect from git remote."
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
valid: errors.length === 0,
|
|
102
|
+
errors,
|
|
103
|
+
warnings,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Execute deployment to GitHub Pages
|
|
109
|
+
*
|
|
110
|
+
* Initializes a git repository in the output directory,
|
|
111
|
+
* adds all files, and force pushes to the gh-pages branch.
|
|
112
|
+
*/
|
|
113
|
+
async deploy(context: DeployContext): Promise<DeployResult> {
|
|
114
|
+
const { outDir, metadata, adapterConfig, dryRun, logger } = context;
|
|
115
|
+
|
|
116
|
+
// Resolve configuration (context config overrides constructor config)
|
|
117
|
+
const branch =
|
|
118
|
+
(adapterConfig.branch as string) || this.config.branch || "gh-pages";
|
|
119
|
+
|
|
120
|
+
const cname = (adapterConfig.cname as string) ?? this.config.cname;
|
|
121
|
+
|
|
122
|
+
const noJekyll =
|
|
123
|
+
(adapterConfig.noJekyll as boolean) ?? this.config.noJekyll ?? true;
|
|
124
|
+
|
|
125
|
+
const message =
|
|
126
|
+
(adapterConfig.message as string) ||
|
|
127
|
+
this.config.message ||
|
|
128
|
+
"Deploy to GitHub Pages";
|
|
129
|
+
|
|
130
|
+
const remote =
|
|
131
|
+
(adapterConfig.remote as string) || this.config.remote || "origin";
|
|
132
|
+
|
|
133
|
+
// Determine repository
|
|
134
|
+
let repo = (adapterConfig.repo as string) || this.config.repo;
|
|
135
|
+
|
|
136
|
+
if (!repo) {
|
|
137
|
+
repo = this.autoDetectRepo();
|
|
138
|
+
if (!repo) {
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error:
|
|
142
|
+
"Could not auto-detect repository. Please specify repo in adapter config.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
logger.info(`Auto-detected repository: ${repo}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
logger.info(`Deploying to GitHub Pages: ${repo}`);
|
|
149
|
+
logger.info(`Branch: ${branch}`);
|
|
150
|
+
if (cname) {
|
|
151
|
+
logger.info(`Custom domain: ${cname}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (dryRun) {
|
|
155
|
+
logger.info("Dry run mode - skipping actual deployment");
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
deploymentId: `dry-run-${Date.now()}`,
|
|
159
|
+
url: this.getPageUrl(repo, cname),
|
|
160
|
+
logs: ["Dry run completed successfully"],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Add .nojekyll file if configured
|
|
166
|
+
if (noJekyll) {
|
|
167
|
+
const nojekyllPath = join(outDir, ".nojekyll");
|
|
168
|
+
writeFileSync(nojekyllPath, "");
|
|
169
|
+
logger.debug("Added .nojekyll file");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add CNAME file if custom domain configured
|
|
173
|
+
if (cname) {
|
|
174
|
+
const cnamePath = join(outDir, "CNAME");
|
|
175
|
+
writeFileSync(cnamePath, cname);
|
|
176
|
+
logger.debug(`Added CNAME file: ${cname}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Remove existing .git directory if present
|
|
180
|
+
const gitDir = join(outDir, ".git");
|
|
181
|
+
if (existsSync(gitDir)) {
|
|
182
|
+
rmSync(gitDir, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Initialize git repository
|
|
186
|
+
const initResult = this.runGit(["init"], outDir);
|
|
187
|
+
if (!initResult.success) {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
error: `Failed to initialize git repository: ${initResult.error}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
logger.debug("Initialized git repository");
|
|
194
|
+
|
|
195
|
+
// Configure git user for this repo (required for commit)
|
|
196
|
+
this.runGit(
|
|
197
|
+
["config", "user.email", "github-pages-deploy@org-press"],
|
|
198
|
+
outDir
|
|
199
|
+
);
|
|
200
|
+
this.runGit(["config", "user.name", "org-press deploy"], outDir);
|
|
201
|
+
|
|
202
|
+
// Add all files
|
|
203
|
+
const addResult = this.runGit(["add", "-A"], outDir);
|
|
204
|
+
if (!addResult.success) {
|
|
205
|
+
return {
|
|
206
|
+
success: false,
|
|
207
|
+
error: `Failed to add files: ${addResult.error}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
logger.debug("Added all files to git");
|
|
211
|
+
|
|
212
|
+
// Create commit
|
|
213
|
+
const commitResult = this.runGit(
|
|
214
|
+
["commit", "-m", message],
|
|
215
|
+
outDir
|
|
216
|
+
);
|
|
217
|
+
if (!commitResult.success) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
error: `Failed to create commit: ${commitResult.error}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
logger.debug("Created commit");
|
|
224
|
+
|
|
225
|
+
// Add remote
|
|
226
|
+
const remoteUrl = `https://github.com/${repo}.git`;
|
|
227
|
+
const remoteAddResult = this.runGit(
|
|
228
|
+
["remote", "add", remote, remoteUrl],
|
|
229
|
+
outDir
|
|
230
|
+
);
|
|
231
|
+
if (!remoteAddResult.success) {
|
|
232
|
+
// Remote might already exist, try to set the URL
|
|
233
|
+
this.runGit(["remote", "set-url", remote, remoteUrl], outDir);
|
|
234
|
+
}
|
|
235
|
+
logger.debug(`Set remote ${remote} to ${remoteUrl}`);
|
|
236
|
+
|
|
237
|
+
// Force push to branch
|
|
238
|
+
logger.info(`Pushing to ${remote}/${branch}...`);
|
|
239
|
+
const pushResult = this.runGit(
|
|
240
|
+
["push", "-f", remote, `HEAD:${branch}`],
|
|
241
|
+
outDir
|
|
242
|
+
);
|
|
243
|
+
if (!pushResult.success) {
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
error: `Failed to push to GitHub: ${pushResult.error}`,
|
|
247
|
+
logs: pushResult.output ? [pushResult.output] : undefined,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
logger.info("Successfully deployed to GitHub Pages!");
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
success: true,
|
|
255
|
+
deploymentId: `${repo}@${branch}`,
|
|
256
|
+
url: this.getPageUrl(repo, cname),
|
|
257
|
+
logs: pushResult.output ? [pushResult.output] : undefined,
|
|
258
|
+
};
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
261
|
+
logger.error(`Deployment failed: ${error}`);
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: `Deployment failed: ${error}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Auto-detect repository from git remote
|
|
271
|
+
*/
|
|
272
|
+
private autoDetectRepo(): string | undefined {
|
|
273
|
+
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
274
|
+
encoding: "utf-8",
|
|
275
|
+
timeout: 5000,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (result.status !== 0 || !result.stdout) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const url = result.stdout.trim();
|
|
283
|
+
|
|
284
|
+
// Parse GitHub URL formats:
|
|
285
|
+
// https://github.com/user/repo.git
|
|
286
|
+
// https://github.com/user/repo
|
|
287
|
+
// git@github.com:user/repo.git
|
|
288
|
+
// git@github.com:user/repo
|
|
289
|
+
const httpsMatch = url.match(/github\.com[/:]([^/]+)\/([^/.]+)/);
|
|
290
|
+
if (httpsMatch) {
|
|
291
|
+
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get the GitHub Pages URL for the repository
|
|
299
|
+
*/
|
|
300
|
+
private getPageUrl(repo: string, cname?: string): string {
|
|
301
|
+
if (cname) {
|
|
302
|
+
return `https://${cname}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const [user, repoName] = repo.split("/");
|
|
306
|
+
|
|
307
|
+
// Check if it's a user/org pages repo (e.g., user.github.io)
|
|
308
|
+
if (repoName === `${user}.github.io`) {
|
|
309
|
+
return `https://${user}.github.io`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Project pages URL
|
|
313
|
+
return `https://${user}.github.io/${repoName}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run a git command and return the result
|
|
318
|
+
*/
|
|
319
|
+
private runGit(
|
|
320
|
+
args: string[],
|
|
321
|
+
cwd: string
|
|
322
|
+
): { success: boolean; output?: string; error?: string } {
|
|
323
|
+
const result = spawnSync("git", args, {
|
|
324
|
+
cwd,
|
|
325
|
+
encoding: "utf-8",
|
|
326
|
+
timeout: 60000,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (result.status !== 0) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
output: result.stdout,
|
|
333
|
+
error: result.stderr || result.stdout || "Unknown git error",
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
output: result.stdout,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Factory function to create GitHubPagesAdapter
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* ```typescript
|
|
349
|
+
* import { githubPagesAdapter } from '@org-press/deploy-github-pages';
|
|
350
|
+
*
|
|
351
|
+
* export default defineConfig({
|
|
352
|
+
* deploy: {
|
|
353
|
+
* adapter: githubPagesAdapter({
|
|
354
|
+
* repo: 'user/my-site',
|
|
355
|
+
* cname: 'mysite.com',
|
|
356
|
+
* }),
|
|
357
|
+
* },
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
export function githubPagesAdapter(
|
|
362
|
+
config: GitHubPagesConfig = {}
|
|
363
|
+
): GitHubPagesAdapter {
|
|
364
|
+
return new GitHubPagesAdapter(config);
|
|
365
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @org-press/deploy-github-pages
|
|
3
|
+
*
|
|
4
|
+
* GitHub Pages deploy adapter for org-press.
|
|
5
|
+
*
|
|
6
|
+
* Deploys static sites to GitHub Pages by pushing to the gh-pages branch.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { githubPagesAdapter } from '@org-press/deploy-github-pages';
|
|
11
|
+
*
|
|
12
|
+
* export default defineConfig({
|
|
13
|
+
* deploy: {
|
|
14
|
+
* adapter: githubPagesAdapter({
|
|
15
|
+
* repo: 'user/my-site',
|
|
16
|
+
* cname: 'mysite.com',
|
|
17
|
+
* }),
|
|
18
|
+
* },
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Types
|
|
24
|
+
export type { GitHubPagesConfig } from "./types.ts";
|
|
25
|
+
|
|
26
|
+
// Adapter
|
|
27
|
+
export { GitHubPagesAdapter, githubPagesAdapter } from "./adapter.ts";
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Pages Adapter Types
|
|
3
|
+
*
|
|
4
|
+
* Configuration types for the GitHub Pages deploy adapter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GitHub Pages adapter configuration options
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const config: GitHubPagesConfig = {
|
|
13
|
+
* repo: 'user/my-site',
|
|
14
|
+
* branch: 'gh-pages',
|
|
15
|
+
* cname: 'mysite.com',
|
|
16
|
+
* noJekyll: true,
|
|
17
|
+
* message: 'Deploy from org-press',
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface GitHubPagesConfig {
|
|
22
|
+
/**
|
|
23
|
+
* GitHub repository in format "user/repo" or "org/repo"
|
|
24
|
+
* If not specified, attempts to auto-detect from git remote
|
|
25
|
+
*/
|
|
26
|
+
repo?: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Target branch for GitHub Pages deployment
|
|
30
|
+
* @default "gh-pages"
|
|
31
|
+
*/
|
|
32
|
+
branch?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Custom domain for GitHub Pages (creates CNAME file)
|
|
36
|
+
* Set to empty string or omit to disable
|
|
37
|
+
*/
|
|
38
|
+
cname?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Add .nojekyll file to disable Jekyll processing
|
|
42
|
+
* Recommended when deploying pre-built static sites
|
|
43
|
+
* @default true
|
|
44
|
+
*/
|
|
45
|
+
noJekyll?: boolean;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Commit message for the deployment
|
|
49
|
+
* @default "Deploy to GitHub Pages"
|
|
50
|
+
*/
|
|
51
|
+
message?: string;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Remote name to push to
|
|
55
|
+
* @default "origin"
|
|
56
|
+
*/
|
|
57
|
+
remote?: string;
|
|
58
|
+
}
|