@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/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
+ }