@symbiosis-lab/moss-plugin-github 1.5.1

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 (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +18 -0
  3. package/assets/icon.svg +3 -0
  4. package/assets/manifest.json +19 -0
  5. package/e2e/deploy-api.test.ts +1129 -0
  6. package/e2e/moss-cli.test.ts +478 -0
  7. package/features/auth/device-flow.feature +41 -0
  8. package/features/deploy/validation.feature +50 -0
  9. package/features/steps/auth.steps.ts +285 -0
  10. package/features/steps/deploy.steps.ts +354 -0
  11. package/package.json +51 -0
  12. package/src/__tests__/auth-flow.integration.test.ts +738 -0
  13. package/src/__tests__/auth.test.ts +147 -0
  14. package/src/__tests__/configure-domain.test.ts +263 -0
  15. package/src/__tests__/deploy.integration.test.ts +798 -0
  16. package/src/__tests__/git.test.ts +190 -0
  17. package/src/__tests__/github-api.test.ts +761 -0
  18. package/src/__tests__/github-deploy.test.ts +2411 -0
  19. package/src/__tests__/progress-timeout.test.ts +209 -0
  20. package/src/__tests__/repo-setup-progress.test.ts +367 -0
  21. package/src/__tests__/repo-setup.test.ts +370 -0
  22. package/src/__tests__/token.test.ts +152 -0
  23. package/src/__tests__/utils.test.ts +129 -0
  24. package/src/__tests__/workflow.test.ts +146 -0
  25. package/src/auth.ts +588 -0
  26. package/src/constants.ts +7 -0
  27. package/src/git.ts +60 -0
  28. package/src/github-api.ts +601 -0
  29. package/src/github-deploy.ts +593 -0
  30. package/src/main.ts +646 -0
  31. package/src/repo-setup.ts +685 -0
  32. package/src/token.ts +202 -0
  33. package/src/types.ts +91 -0
  34. package/src/utils.ts +108 -0
  35. package/src/workflow.ts +79 -0
  36. package/test-helpers/mock-github-api.ts +217 -0
  37. package/tsconfig.json +20 -0
  38. package/vitest.config.ts +50 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * E2E Tests for GitHub Deployer Plugin using moss CLI
3
+ *
4
+ * These tests verify the plugin works correctly when invoked through
5
+ * the moss CLI, testing real-world scenarios.
6
+ *
7
+ * Requirements:
8
+ * - moss binary built and available (set MOSS_BINARY env var or build locally)
9
+ * - Plugin built (npm run build)
10
+ * - Tests create temporary directories for fixtures
11
+ * - Display server available (xvfb-run on Linux CI)
12
+ *
13
+ * Plugin Execution:
14
+ * - Use --wait-plugins flag to wait for plugin hooks to complete
15
+ * - Plugin JavaScript runs in Tauri webview (requires display)
16
+ * - Tests verify plugin validation messages and error handling
17
+ *
18
+ * CI Setup:
19
+ * - The workflow downloads moss binary from releases before running tests
20
+ * - Set MOSS_BINARY environment variable to the binary path
21
+ * - Linux CI uses xvfb-run for display server
22
+ */
23
+
24
+ import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
25
+ import { execSync, spawn, ChildProcess } from "child_process";
26
+ import * as fs from "fs";
27
+ import * as path from "path";
28
+ import * as os from "os";
29
+
30
+ // Path to moss binary - check env var first, then fallback to local dev path
31
+ const MOSS_BINARY = process.env.MOSS_BINARY || path.join(
32
+ __dirname,
33
+ "../../../../moss/develop/src-tauri/target/debug/moss"
34
+ );
35
+
36
+ // Check if we're using a release binary (from CI) vs local dev build
37
+ const IS_CI_BINARY = !!process.env.MOSS_BINARY;
38
+
39
+ // Check if --wait-plugins is supported (v0.3.1+)
40
+ let HAS_WAIT_PLUGINS = false;
41
+
42
+ // Path to plugin dist
43
+ const PLUGIN_DIST = path.join(__dirname, "../dist");
44
+
45
+ // Test fixture directory
46
+ let testDir: string;
47
+ let fixtureCounter = 0;
48
+
49
+ /**
50
+ * Create a test fixture directory with optional git initialization
51
+ */
52
+ function createFixture(options: {
53
+ withGit?: boolean;
54
+ withRemote?: string;
55
+ withPlugin?: boolean;
56
+ content?: Record<string, string>;
57
+ }): string {
58
+ const fixtureName = `moss-e2e-${Date.now()}-${fixtureCounter++}`;
59
+ const fixturePath = path.join(testDir, fixtureName);
60
+ fs.mkdirSync(fixturePath, { recursive: true });
61
+
62
+ // Create content files
63
+ const defaultContent = {
64
+ "index.md": "# Hello World\n\nThis is a test site.",
65
+ "about.md": "# About\n\nAbout page content.",
66
+ };
67
+
68
+ const content = options.content || defaultContent;
69
+ for (const [filename, fileContent] of Object.entries(content)) {
70
+ const filePath = path.join(fixturePath, filename);
71
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
72
+ fs.writeFileSync(filePath, fileContent);
73
+ }
74
+
75
+ // Initialize git if requested
76
+ if (options.withGit) {
77
+ execSync("git init", { cwd: fixturePath, stdio: "pipe" });
78
+ execSync("git config user.email 'test@example.com'", {
79
+ cwd: fixturePath,
80
+ stdio: "pipe",
81
+ });
82
+ execSync("git config user.name 'Test User'", {
83
+ cwd: fixturePath,
84
+ stdio: "pipe",
85
+ });
86
+ execSync("git add .", { cwd: fixturePath, stdio: "pipe" });
87
+ execSync('git commit -m "Initial commit"', {
88
+ cwd: fixturePath,
89
+ stdio: "pipe",
90
+ });
91
+
92
+ // Add remote if requested
93
+ if (options.withRemote) {
94
+ execSync(`git remote add origin ${options.withRemote}`, {
95
+ cwd: fixturePath,
96
+ stdio: "pipe",
97
+ });
98
+ }
99
+ }
100
+
101
+ // Link plugin if requested
102
+ if (options.withPlugin) {
103
+ const mossDir = path.join(fixturePath, ".moss");
104
+ const pluginsDir = path.join(mossDir, "plugins");
105
+ const githubPluginDir = path.join(pluginsDir, "github");
106
+
107
+ fs.mkdirSync(pluginsDir, { recursive: true });
108
+
109
+ // Copy plugin files (symlink might not work on all systems)
110
+ const pluginFiles = ["main.bundle.js", "manifest.json", "icon.svg"];
111
+ fs.mkdirSync(githubPluginDir, { recursive: true });
112
+
113
+ for (const file of pluginFiles) {
114
+ const src = path.join(PLUGIN_DIST, file);
115
+ const dest = path.join(githubPluginDir, file);
116
+ if (fs.existsSync(src)) {
117
+ fs.copyFileSync(src, dest);
118
+ }
119
+ }
120
+ }
121
+
122
+ return fixturePath;
123
+ }
124
+
125
+ /**
126
+ * Run moss CLI and return stdout/stderr
127
+ */
128
+ function runMoss(
129
+ args: string[],
130
+ options?: { cwd?: string; timeout?: number }
131
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
132
+ return new Promise((resolve) => {
133
+ const proc = spawn(MOSS_BINARY, args, {
134
+ cwd: options?.cwd || testDir,
135
+ timeout: options?.timeout || 30000,
136
+ env: {
137
+ ...process.env,
138
+ // Disable interactive prompts
139
+ CI: "true",
140
+ },
141
+ });
142
+
143
+ let stdout = "";
144
+ let stderr = "";
145
+
146
+ proc.stdout?.on("data", (data) => {
147
+ stdout += data.toString();
148
+ });
149
+
150
+ proc.stderr?.on("data", (data) => {
151
+ stderr += data.toString();
152
+ });
153
+
154
+ proc.on("close", (code) => {
155
+ resolve({ stdout, stderr, code: code ?? 1 });
156
+ });
157
+
158
+ proc.on("error", (err) => {
159
+ stderr += err.message;
160
+ resolve({ stdout, stderr, code: 1 });
161
+ });
162
+ });
163
+ }
164
+
165
+ describe("moss CLI E2E Tests", () => {
166
+ beforeAll(async () => {
167
+ // Verify moss binary exists - fail if not available
168
+ if (!fs.existsSync(MOSS_BINARY)) {
169
+ throw new Error(
170
+ `moss binary not found at ${MOSS_BINARY}. ` +
171
+ `Either build moss locally, or set MOSS_BINARY environment variable. ` +
172
+ `In CI, the binary should be downloaded from releases.`
173
+ );
174
+ }
175
+
176
+ // Verify plugin dist exists - fail if not built
177
+ if (!fs.existsSync(PLUGIN_DIST)) {
178
+ throw new Error(
179
+ `Plugin dist not found at ${PLUGIN_DIST}. Please build the plugin first with 'npm run build'.`
180
+ );
181
+ }
182
+
183
+ // Create temp directory for test fixtures
184
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), "moss-e2e-"));
185
+
186
+ // Check if --wait-plugins is supported
187
+ const { stdout } = await runMoss(["--help"]);
188
+ HAS_WAIT_PLUGINS = stdout.includes("--wait-plugins");
189
+ });
190
+
191
+ afterAll(() => {
192
+ // Cleanup test directory
193
+ if (testDir && fs.existsSync(testDir)) {
194
+ fs.rmSync(testDir, { recursive: true, force: true });
195
+ }
196
+ });
197
+
198
+ describe("moss --help", () => {
199
+ it("shows help message with build command", async () => {
200
+ const { stdout, code } = await runMoss(["--help"]);
201
+
202
+ expect(code).toBe(0);
203
+ expect(stdout).toContain("moss");
204
+ expect(stdout).toContain("build");
205
+ });
206
+
207
+ it("shows help message with deploy command", async () => {
208
+ const { stdout, code } = await runMoss(["--help"]);
209
+
210
+ expect(code).toBe(0);
211
+ expect(stdout).toContain("deploy");
212
+ expect(stdout).toContain("GitHub Pages");
213
+ });
214
+ });
215
+
216
+ describe("Basic compilation (no plugins)", () => {
217
+ it("builds a folder with markdown files", async () => {
218
+ const fixture = createFixture({ content: { "index.md": "# Hello" } });
219
+
220
+ const { stdout, stderr, code } = await runMoss([
221
+ "build",
222
+ fixture,
223
+ "--no-plugins",
224
+ ]);
225
+
226
+ // Should succeed
227
+ expect(code).toBe(0);
228
+ expect(stdout + stderr).toContain("Compiling");
229
+
230
+ // Should create .moss/site directory
231
+ const siteDir = path.join(fixture, ".moss", "site");
232
+ expect(fs.existsSync(siteDir)).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe("Deploy command", () => {
237
+ // This test verifies exit code 1 when no deploy plugin is installed.
238
+ // The behavior was fixed in moss v0.3.0 (Bug 5).
239
+ // Only runs in CI with release binary - local dev builds may have different behavior.
240
+ it.skipIf(!IS_CI_BINARY)("shows 'no plugin' message when no deploy plugin installed", async () => {
241
+ const fixture = createFixture({
242
+ withGit: true,
243
+ withRemote: "git@github.com:user/test-repo.git",
244
+ withPlugin: false, // No plugin installed
245
+ });
246
+
247
+ const { stdout, stderr, code } = await runMoss(["deploy", fixture]);
248
+
249
+ // Should exit with error (no plugin)
250
+ expect(code).toBe(1);
251
+ const output = stdout + stderr;
252
+ expect(output).toMatch(/no.*plugin|install.*plugin/i);
253
+ });
254
+
255
+ it("builds before deploying", async () => {
256
+ const fixture = createFixture({
257
+ withGit: true,
258
+ withRemote: "git@github.com:user/test-repo.git",
259
+ });
260
+
261
+ const { stdout, stderr } = await runMoss(["deploy", fixture]);
262
+
263
+ // Should show compilation step
264
+ const output = stdout + stderr;
265
+ expect(output).toContain("Compiling");
266
+
267
+ // Should create .moss/site directory
268
+ const siteDir = path.join(fixture, ".moss", "site");
269
+ expect(fs.existsSync(siteDir)).toBe(true);
270
+ });
271
+
272
+ it("shows progress messages", async () => {
273
+ const fixture = createFixture({
274
+ withGit: true,
275
+ withRemote: "git@github.com:user/test-repo.git",
276
+ });
277
+
278
+ const { stdout, stderr } = await runMoss(["deploy", fixture]);
279
+ const output = stdout + stderr;
280
+
281
+ // Should show deploy step
282
+ expect(output).toContain("Deploying");
283
+ });
284
+ });
285
+
286
+ describe("Plugin discovery", () => {
287
+ it("creates plugin directory structure correctly", async () => {
288
+ const fixture = createFixture({
289
+ withPlugin: true,
290
+ });
291
+
292
+ // Verify plugin files were copied
293
+ const pluginsDir = path.join(fixture, ".moss", "plugins", "github");
294
+ expect(fs.existsSync(pluginsDir)).toBe(true);
295
+ expect(fs.existsSync(path.join(pluginsDir, "manifest.json"))).toBe(true);
296
+ expect(fs.existsSync(path.join(pluginsDir, "main.bundle.js"))).toBe(true);
297
+ });
298
+
299
+ it("manifest.json has correct structure", async () => {
300
+ const fixture = createFixture({
301
+ withPlugin: true,
302
+ });
303
+
304
+ const manifestPath = path.join(fixture, ".moss", "plugins", "github", "manifest.json");
305
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
306
+
307
+ expect(manifest.name).toBe("github");
308
+ expect(manifest.capabilities).toContain("deploy");
309
+ expect(manifest.entry).toBe("main.bundle.js");
310
+ expect(manifest.global_name).toBe("GithubPlugin");
311
+ });
312
+ });
313
+
314
+ /**
315
+ * Plugin Execution Tests (with --wait-plugins)
316
+ *
317
+ * These tests use --wait-plugins to wait for plugin hooks to complete.
318
+ * Requires:
319
+ * - moss v0.3.1+ with --wait-plugins support
320
+ * - Display server (xvfb-run on Linux CI)
321
+ *
322
+ * The plugin validation logic runs in the webview and reports errors
323
+ * via Tauri IPC commands which are captured in the CLI output.
324
+ */
325
+ describe("Plugin execution (with --wait-plugins)", () => {
326
+ it.skipIf(!HAS_WAIT_PLUGINS)("reports validation errors for non-git repos", async () => {
327
+ // Create fixture WITHOUT git initialization - plugin should detect this
328
+ const fixture = createFixture({
329
+ withGit: false, // Not a git repo
330
+ withPlugin: true,
331
+ });
332
+
333
+ // First build the site (creates .moss/site)
334
+ await runMoss(["build", fixture, "--no-plugins"]);
335
+
336
+ // Then deploy - plugin should report "not a git repository" error
337
+ const { stdout, stderr, code } = await runMoss(
338
+ ["deploy", fixture, "--wait-plugins"],
339
+ { timeout: 60000 }
340
+ );
341
+
342
+ const output = stdout + stderr;
343
+
344
+ // Plugin should have executed and reported validation error
345
+ // The exact error message depends on the plugin implementation
346
+ expect(output).toMatch(/not.*git.*repository|git.*init|Repository setup/i);
347
+ });
348
+
349
+ it.skipIf(!HAS_WAIT_PLUGINS)("reports errors for missing remote", async () => {
350
+ // Create git repo WITHOUT remote
351
+ const fixture = createFixture({
352
+ withGit: true,
353
+ withRemote: undefined, // No remote configured
354
+ withPlugin: true,
355
+ });
356
+
357
+ // First build the site
358
+ await runMoss(["build", fixture, "--no-plugins"]);
359
+
360
+ // Then deploy - plugin should report "no remote" error
361
+ const { stdout, stderr, code } = await runMoss(
362
+ ["deploy", fixture, "--wait-plugins"],
363
+ { timeout: 60000 }
364
+ );
365
+
366
+ const output = stdout + stderr;
367
+
368
+ // Plugin should report missing remote or show repo setup UI
369
+ expect(output).toMatch(/no.*remote|Repository setup|cancelled/i);
370
+ });
371
+
372
+ it.skipIf(!HAS_WAIT_PLUGINS)("reports errors for non-GitHub remote", async () => {
373
+ // Create git repo with GitLab remote (not GitHub)
374
+ const fixture = createFixture({
375
+ withGit: true,
376
+ withRemote: "git@gitlab.com:user/repo.git", // GitLab, not GitHub
377
+ withPlugin: true,
378
+ });
379
+
380
+ // First build the site
381
+ await runMoss(["build", fixture, "--no-plugins"]);
382
+
383
+ // Then deploy - plugin should report "not a GitHub URL" error
384
+ const { stdout, stderr, code } = await runMoss(
385
+ ["deploy", fixture, "--wait-plugins"],
386
+ { timeout: 60000 }
387
+ );
388
+
389
+ const output = stdout + stderr;
390
+
391
+ // Plugin should report non-GitHub remote error
392
+ expect(output).toMatch(/not.*GitHub|GitHub.*only|github\.com/i);
393
+ });
394
+
395
+ it.skipIf(!HAS_WAIT_PLUGINS)("validates site is built before deploy", async () => {
396
+ // Create fixture with valid GitHub setup but NO built site
397
+ const fixture = createFixture({
398
+ withGit: true,
399
+ withRemote: "git@github.com:testuser/testrepo.git",
400
+ withPlugin: true,
401
+ });
402
+
403
+ // Do NOT build - go straight to deploy
404
+ // Plugin should report "no site files" error
405
+ const { stdout, stderr, code } = await runMoss(
406
+ ["deploy", fixture, "--wait-plugins"],
407
+ { timeout: 60000 }
408
+ );
409
+
410
+ const output = stdout + stderr;
411
+
412
+ // Plugin should report empty site or compilation needed
413
+ expect(output).toMatch(/site.*empty|build.*first|no.*files/i);
414
+ });
415
+
416
+ it.skipIf(!HAS_WAIT_PLUGINS)("shows deploy progress messages", async () => {
417
+ // Create fixture with valid GitHub setup
418
+ const fixture = createFixture({
419
+ withGit: true,
420
+ withRemote: "git@github.com:testuser/testrepo.git",
421
+ withPlugin: true,
422
+ });
423
+
424
+ // Build first
425
+ await runMoss(["build", fixture, "--no-plugins"]);
426
+
427
+ // Deploy with --wait-plugins to see full output
428
+ const { stdout, stderr, code } = await runMoss(
429
+ ["deploy", fixture, "--wait-plugins"],
430
+ { timeout: 120000 } // Longer timeout for full deploy
431
+ );
432
+
433
+ const output = stdout + stderr;
434
+
435
+ // Should show deploy-related messages
436
+ // Note: actual deployment will fail without real git push,
437
+ // but we should see the validation passing and deploy starting
438
+ expect(output).toMatch(/deploy|GitHub|validat/i);
439
+ });
440
+ });
441
+
442
+ /**
443
+ * Build with plugins tests
444
+ *
445
+ * Tests compilation with plugins enabled (no --no-plugins flag).
446
+ * The GitHub plugin has deploy capability, not process/enhance,
447
+ * so it won't affect compilation, but this verifies plugin loading works.
448
+ */
449
+ describe("Build with plugins", () => {
450
+ it.skipIf(!HAS_WAIT_PLUGINS)("builds successfully with plugin installed", async () => {
451
+ const fixture = createFixture({
452
+ withGit: true,
453
+ withRemote: "git@github.com:testuser/testrepo.git",
454
+ withPlugin: true,
455
+ content: {
456
+ "index.md": "# Test Site\n\nHello world!",
457
+ },
458
+ });
459
+
460
+ // Build WITH plugins (default behavior)
461
+ const { stdout, stderr, code } = await runMoss(
462
+ ["build", fixture, "--wait-plugins"],
463
+ { timeout: 60000 }
464
+ );
465
+
466
+ // Should succeed
467
+ expect(code).toBe(0);
468
+
469
+ // Should create site directory
470
+ const siteDir = path.join(fixture, ".moss", "site");
471
+ expect(fs.existsSync(siteDir)).toBe(true);
472
+
473
+ // Should have generated HTML
474
+ const indexHtml = path.join(siteDir, "index.html");
475
+ expect(fs.existsSync(indexHtml)).toBe(true);
476
+ });
477
+ });
478
+ });
@@ -0,0 +1,41 @@
1
+ Feature: GitHub OAuth Device Flow Authentication
2
+ As a user deploying to GitHub Pages
3
+ I want to authenticate via browser
4
+ So I can push without configuring git credentials manually
5
+
6
+ Scenario: Request device code from GitHub
7
+ Given no existing GitHub credentials
8
+ When I initiate the device flow authentication
9
+ Then I should receive a device code response
10
+ And the response should include user_code, verification_uri, and interval
11
+
12
+ Scenario: Poll for access token after authorization
13
+ Given a valid device code
14
+ And the user has authorized the application
15
+ When I poll for the access token
16
+ Then I should receive an access token
17
+ And the token should have repo and workflow scopes
18
+
19
+ Scenario: Handle authorization pending state
20
+ Given a valid device code
21
+ And the user has not yet authorized
22
+ When I poll for the access token
23
+ Then I should receive authorization_pending error
24
+ And I should continue polling
25
+
26
+ Scenario: Store token in git credential helper
27
+ Given a valid access token
28
+ When I store the token
29
+ Then the token should be stored successfully
30
+ And I should be able to retrieve the token
31
+
32
+ Scenario: Handle expired device code
33
+ Given a device code that has expired
34
+ When I poll for the access token
35
+ Then I should receive an expired_token error
36
+
37
+ Scenario: Validate token with GitHub API
38
+ Given a valid access token
39
+ When I validate the token
40
+ Then I should receive user information
41
+ And the scopes should include repo and workflow
@@ -0,0 +1,50 @@
1
+ Feature: GitHub Deployer Validation
2
+ As a user deploying to GitHub Pages
3
+ I want clear error messages when configuration is incorrect
4
+ So I can fix issues and deploy successfully
5
+
6
+ Scenario: Deploy from non-git directory
7
+ Given the directory is not a git repository
8
+ When I attempt to deploy
9
+ Then the deployment should fail
10
+ And the error should indicate setup was cancelled
11
+
12
+ Scenario: Deploy without any git remote
13
+ Given the directory is a git repository
14
+ And no git remote is configured
15
+ When I attempt to deploy
16
+ Then the deployment should fail
17
+ And the error should mention "No git remote configured"
18
+ And the error should include instructions to add a GitHub remote
19
+
20
+ Scenario: Deploy with non-GitHub remote triggers setup
21
+ Given the directory is a git repository
22
+ And the git remote is not a GitHub URL
23
+ When I attempt to deploy
24
+ Then the deployment should fail
25
+ And the error should indicate setup was cancelled
26
+
27
+ Scenario: Deploy with empty site directory
28
+ Given the directory is a git repository
29
+ And the site directory is empty
30
+ When I attempt to deploy
31
+ Then the deployment should fail
32
+ And the error should mention that the site needs to be built
33
+
34
+ Scenario: Successful deployment with SSH remote
35
+ Given the directory is a git repository
36
+ And the git remote is "git@github.com:testuser/testrepo.git"
37
+ And the site is built with files in ".moss/build/site/"
38
+ And the GitHub Actions workflow already exists
39
+ When I attempt to deploy
40
+ Then the deployment should succeed
41
+ And the deployment URL should be "https://testuser.github.io/testrepo"
42
+
43
+ Scenario: First-time deployment creates workflow
44
+ Given the directory is a git repository
45
+ And the git remote is "git@github.com:user/repo.git"
46
+ And the site is built with files in ".moss/build/site/"
47
+ And the GitHub Actions workflow does not exist
48
+ When I attempt to deploy
49
+ Then the deployment should succeed
50
+ And the result should indicate first-time setup