@scrymore/scry-deployer 0.3.2 → 0.4.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.
package/bin/cli.js CHANGED
@@ -409,9 +409,13 @@ async function main() {
409
409
  alias: 'skipGhSetup'
410
410
  })
411
411
  .option('commit-api-key', {
412
- describe: 'Commit API key in config file (not recommended)',
412
+ describe: 'Write the API key into the committed config file (not recommended)',
413
413
  type: 'boolean',
414
- default: true,
414
+ // False by default. The description has always said "not
415
+ // recommended" while the default said otherwise, and the
416
+ // default won: every `init` wrote a customer's key into a
417
+ // file it then committed.
418
+ default: false,
415
419
  alias: 'commitApiKey'
416
420
  })
417
421
  .option('verbose', {
package/lib/init.js CHANGED
@@ -47,19 +47,27 @@ async function runInit(argv) {
47
47
  // Step 3: Create config file
48
48
  const step3Start = Date.now();
49
49
  logger.info('3/8: Creating configuration file...');
50
- createConfigFile(argv.project, argv.apiKey, argv.apiUrl, envInfo);
50
+ createConfigFile(argv.project, argv.apiKey, argv.apiUrl, envInfo, argv.commitApiKey);
51
51
  const step3Duration = Date.now() - step3Start;
52
52
  logger.success(`✅ Created .storybook-deployer.json [${step3Duration}ms]\n`);
53
53
 
54
- // Step 4: Add to gitignore (optional - keep API key out of git if user prefers)
54
+ // Step 4: Report what the committed config does and does not contain.
55
+ //
56
+ // This step used to add `.storybook-deployer.json` to .gitignore, which was
57
+ // wrong in both directions. The config now holds no credential, so ignoring
58
+ // it would only stop a team sharing its project id — and the entry it wrote,
59
+ // `.storybook-deployer.json # Contains API key`, never matched anything:
60
+ // in .gitignore a `#` is only a comment at the start of a line, so the
61
+ // pattern included the trailing text and git ignored no file.
55
62
  const step4Start = Date.now();
56
- if (!argv.commitApiKey) {
57
- logger.info('4/8: Updating .gitignore...');
58
- updateGitignore();
59
- const step4Duration = Date.now() - step4Start;
60
- logger.success(`✅ Updated .gitignore (API key will use env vars in CI) [${step4Duration}ms]\n`);
63
+ if (argv.commitApiKey) {
64
+ logger.info('4/8: Checking credentials...');
65
+ logger.error(`⚠️ --commit-api-key: your API key will be written to .storybook-deployer.json and committed.
66
+ Git history keeps it after rotation, and CI does not need it — the key is
67
+ already stored as the SCRY_API_KEY repository secret. [${Date.now() - step4Start}ms]\n`);
61
68
  } else {
62
- logger.info('4/8: Skipping .gitignore update (--commit-api-key flag set)\n');
69
+ logger.info('4/8: Checking credentials...');
70
+ logger.success(`✅ .storybook-deployer.json holds no credentials — safe to commit [${Date.now() - step4Start}ms]\n`);
63
71
  }
64
72
 
65
73
  // Step 5: Generate workflow files
@@ -71,14 +79,18 @@ async function runInit(argv) {
71
79
  logger.success('✅ Created .github/workflows/deploy-pr-preview.yml\n');
72
80
 
73
81
  // Step 6: Setup GitHub variables (if gh CLI available)
82
+ // Tracked, not assumed — the closing summary reports what actually happened.
83
+ let ghConfigured = null;
74
84
  const step6Start = Date.now();
75
85
  if (!argv.skipGhSetup && isGhCliAvailable()) {
76
86
  logger.info('6/8: Setting up GitHub repository variables...');
77
87
  try {
78
88
  await setupGitHubVariables(argv.project, argv.apiKey, argv.apiUrl, logger);
89
+ ghConfigured = true;
79
90
  const step6Duration = Date.now() - step6Start;
80
91
  logger.success(`✅ GitHub variables configured [${step6Duration}ms]\n`);
81
92
  } catch (error) {
93
+ ghConfigured = false;
82
94
  const step6Duration = Date.now() - step6Start;
83
95
  logger.error(`⚠️ GitHub setup failed: ${error.message} [${step6Duration}ms]`);
84
96
  logger.info('You can set these up manually later.\n');
@@ -94,7 +106,7 @@ async function runInit(argv) {
94
106
  // Step 7: Git commit
95
107
  const step7Start = Date.now();
96
108
  logger.info('7/8: Committing changes...');
97
- const commitResult = gitCommit(argv.commitApiKey, logger);
109
+ const commitResult = gitCommit(logger);
98
110
  const step7Duration = Date.now() - step7Start;
99
111
  if (commitResult.success) {
100
112
  logger.success(`✅ Changes committed: ${commitResult.sha} [${step7Duration}ms]\n`);
@@ -118,7 +130,10 @@ async function runInit(argv) {
118
130
  logger.info('━'.repeat(50));
119
131
  logger.info(`[TIMING] Total setup time: ${totalDuration}ms (${(totalDuration / 1000).toFixed(2)}s)\n`);
120
132
 
121
- showSuccessMessage(argv.project, envInfo, argv.apiUrl, pushResult.success);
133
+ showSuccessMessage(argv.project, envInfo, argv.apiUrl, pushResult.success, {
134
+ ghConfigured,
135
+ committed: commitResult.success,
136
+ });
122
137
 
123
138
  } catch (error) {
124
139
  logger.error(`\n❌ Setup failed: ${error.message}`);
@@ -236,7 +251,7 @@ function parseGitHubRemote(remote) {
236
251
  /**
237
252
  * Create the configuration file
238
253
  */
239
- function createConfigFile(projectId, apiKey, apiUrl, envInfo) {
254
+ function createConfigFile(projectId, apiKey, apiUrl, envInfo, commitApiKey) {
240
255
  const config = {
241
256
  apiUrl: apiUrl,
242
257
  project: projectId,
@@ -245,10 +260,19 @@ function createConfigFile(projectId, apiKey, apiUrl, envInfo) {
245
260
  verbose: false
246
261
  };
247
262
 
248
- // Only include apiKey in config if user wants it committed
249
- // Otherwise it should be set via environment variable
250
- // For now, we'll include it but note in .gitignore
251
- config.apiKey = apiKey;
263
+ // The key is deliberately absent unless explicitly asked for.
264
+ //
265
+ // This file gets committed, and CI never reads the key from it `init` sets
266
+ // SCRY_API_KEY as a GitHub *secret* and the generated workflow reads
267
+ // ${{ secrets.SCRY_API_KEY }}. So a copy here buys nothing and lands
268
+ // somewhere git history makes permanent: rotating the key afterwards does
269
+ // not remove it, and on a public repository it is simply disclosed.
270
+ //
271
+ // Local runs read SCRY_API_KEY from the environment, which lib/config.js
272
+ // already resolves.
273
+ if (commitApiKey) {
274
+ config.apiKey = apiKey;
275
+ }
252
276
 
253
277
  const configPath = '.storybook-deployer.json';
254
278
  fs.writeFileSync(
@@ -258,28 +282,6 @@ function createConfigFile(projectId, apiKey, apiUrl, envInfo) {
258
282
  );
259
283
  }
260
284
 
261
- /**
262
- * Update .gitignore to exclude sensitive files (optional)
263
- */
264
- function updateGitignore() {
265
- const gitignorePath = '.gitignore';
266
- const entries = [
267
- '# Scry Storybook Deployer',
268
- '.storybook-deployer.json # Contains API key'
269
- ];
270
-
271
- let gitignoreContent = '';
272
- if (fs.existsSync(gitignorePath)) {
273
- gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
274
- }
275
-
276
- // Check if already added
277
- if (!gitignoreContent.includes('.storybook-deployer.json')) {
278
- gitignoreContent += '\n' + entries.join('\n') + '\n';
279
- fs.writeFileSync(gitignorePath, gitignoreContent, 'utf8');
280
- }
281
- }
282
-
283
285
  /**
284
286
  * Generate workflow files
285
287
  */
@@ -318,22 +320,63 @@ function isGhCliAvailable() {
318
320
  /**
319
321
  * Setup GitHub variables using gh CLI
320
322
  */
321
- async function setupGitHubVariables(projectId, apiKey, apiUrl, logger) {
323
+ /**
324
+ * Does this `gh` know about `gh variable`? It arrived in gh 2.21 (December 2022),
325
+ * and Ubuntu 22.04 still ships 2.4.0 — so plenty of machines do not have it.
326
+ * Without this check the very first call throws, the secret after it is never
327
+ * reached, and CI ends up with no credentials at all.
328
+ */
329
+ function ghSupportsVariables() {
322
330
  try {
323
- // Set variables
324
- execSync(`gh variable set SCRY_PROJECT_ID --body "${projectId}"`, { stdio: 'pipe' });
325
- logger.debug(' ✓ Set SCRY_PROJECT_ID');
331
+ execSync('gh variable --help', { stdio: 'pipe' });
332
+ return true;
333
+ } catch {
334
+ return false;
335
+ }
336
+ }
326
337
 
327
- execSync(`gh variable set SCRY_API_URL --body "${apiUrl}"`, { stdio: 'pipe' });
328
- logger.debug(' ✓ Set SCRY_API_URL');
338
+ function setVariableViaApi(repo, name, value) {
339
+ // `gh api` predates `gh variable` by years, so it is the safe fallback.
340
+ const payload = JSON.stringify({ name, value });
341
+ try {
342
+ execSync(`gh api -X POST repos/${repo}/actions/variables --input -`, {
343
+ stdio: ['pipe', 'pipe', 'pipe'], input: payload,
344
+ });
345
+ } catch {
346
+ // Already present: update rather than create.
347
+ execSync(`gh api -X PATCH repos/${repo}/actions/variables/${name} --input -`, {
348
+ stdio: ['pipe', 'pipe', 'pipe'], input: payload,
349
+ });
350
+ }
351
+ }
329
352
 
330
- // Set secret (API key)
331
- execSync(`gh secret set SCRY_API_KEY --body "${apiKey}"`, { stdio: 'pipe' });
332
- logger.debug(' ✓ Set SCRY_API_KEY');
353
+ async function setupGitHubVariables(projectId, apiKey, apiUrl, logger) {
354
+ const repo = execSync('gh repo view --json nameWithOwner -q .nameWithOwner', { stdio: 'pipe' })
355
+ .toString().trim();
333
356
 
334
- } catch (error) {
335
- throw new Error(`Failed to set GitHub variables: ${error.message}`);
357
+ const useCli = ghSupportsVariables();
358
+ if (!useCli) {
359
+ logger.debug(' gh is too old for `gh variable`; using `gh api` instead');
336
360
  }
361
+
362
+ // Each of these is reported separately. A failure part-way through used to
363
+ // abandon the rest silently, which is how a repository ended up with the
364
+ // variables missing *and* the secret missing while setup reported success.
365
+ const setVar = (name, value) => {
366
+ if (useCli) execSync(`gh variable set ${name} --body "${value}"`, { stdio: 'pipe' });
367
+ else setVariableViaApi(repo, name, value);
368
+ };
369
+
370
+ setVar('SCRY_PROJECT_ID', projectId);
371
+ logger.debug(' ✓ Set SCRY_PROJECT_ID');
372
+
373
+ setVar('SCRY_API_URL', apiUrl);
374
+ logger.debug(' ✓ Set SCRY_API_URL');
375
+
376
+ // `gh secret set` needs libsodium encryption, so there is no simple `gh api`
377
+ // fallback — but it has existed since gh 1.x, so it works where variables do not.
378
+ execSync(`gh secret set SCRY_API_KEY --body "${apiKey}"`, { stdio: 'pipe' });
379
+ logger.debug(' ✓ Set SCRY_API_KEY');
337
380
  }
338
381
 
339
382
  /**
@@ -367,7 +410,7 @@ Or install GitHub CLI and run:
367
410
  /**
368
411
  * Commit the changes to git
369
412
  */
370
- function gitCommit(commitApiKey, logger) {
413
+ function gitCommit(logger) {
371
414
  try {
372
415
  // Check if there are changes to commit
373
416
  const status = execSync('git status --porcelain', { encoding: 'utf8' });
@@ -382,16 +425,25 @@ function gitCommit(commitApiKey, logger) {
382
425
  '.storybook-deployer.json'
383
426
  ];
384
427
 
385
- if (!commitApiKey) {
386
- filesToAdd.push('.gitignore');
387
- }
388
-
428
+ // Each file is staged on its own. `git add` fails on a path that
429
+ // .gitignore excludes, and one throw used to abort the loop before the
430
+ // commit — so a leftover `.storybook-deployer.json` ignore rule from the
431
+ // pre-0.4.0 workaround silently prevented the *workflows* being committed,
432
+ // and CI was never set up at all.
433
+ let staged = 0;
389
434
  for (const file of filesToAdd) {
390
- if (fs.existsSync(file)) {
435
+ if (!fs.existsSync(file)) continue;
436
+ try {
391
437
  execSync(`git add "${file}"`, { stdio: 'pipe' });
438
+ staged++;
392
439
  logger.debug(` ✓ Added ${file}`);
440
+ } catch {
441
+ logger.debug(` • Skipped ${file} (ignored by .gitignore)`);
393
442
  }
394
443
  }
444
+ if (staged === 0) {
445
+ return { success: false, message: 'Nothing could be staged' };
446
+ }
395
447
 
396
448
  // Commit
397
449
  const commitMessage = 'chore: add Scry Storybook deployment workflows';
@@ -442,18 +494,23 @@ function gitPush(logger) {
442
494
  /**
443
495
  * Show success message
444
496
  */
445
- function showSuccessMessage(projectId, envInfo, apiUrl, pushed) {
497
+ function showSuccessMessage(projectId, envInfo, apiUrl, pushed, results = {}) {
446
498
  console.log(`
447
499
  🎉 ${pushed ? 'Setup Complete and Deployed!' : 'Setup Complete!'}
448
500
 
449
501
  Your Storybook deployment is configured and ready to go.
450
502
 
451
503
  📦 What was set up:
452
- ✅ Configuration file (.storybook-deployer.json)
504
+ ✅ Configuration file (.storybook-deployer.json — no credentials, safe to commit)
453
505
  ✅ GitHub Actions workflows (.github/workflows/)
454
- Repository variables (SCRY_PROJECT_ID, SCRY_API_URL)
455
- ✅ Repository secret (SCRY_API_KEY)
456
- ${pushed ? '✅ Changes committed and pushed' : '⚠️ Manual push required'}
506
+ ${results.ghConfigured === true
507
+ ? '✅ Repository variables and secret (SCRY_PROJECT_ID, SCRY_API_URL, SCRY_API_KEY)'
508
+ : results.ghConfigured === false
509
+ ? '❌ Repository variables and secret NOT set — CI cannot deploy until you fix this'
510
+ : '⚠️ Repository variables and secret not attempted — CI cannot deploy until you set them'}
511
+ ${results.committed === false
512
+ ? '❌ Changes NOT committed — the workflows are staged but uncommitted'
513
+ : (pushed ? '✅ Changes committed and pushed' : '⚠️ Committed; manual push required')}
457
514
 
458
515
  ${!pushed ? `
459
516
  📌 Next Step:
@@ -461,6 +518,12 @@ ${!pushed ? `
461
518
  git push
462
519
  ` : ''}
463
520
 
521
+ 🔑 Running a deploy locally:
522
+ CI reads the key from the SCRY_API_KEY secret. On your own machine, export it
523
+ rather than writing it into the config file, which is committed:
524
+
525
+ export SCRY_API_KEY=<your key>
526
+
464
527
  🚀 Deployment:
465
528
  Your Storybook will deploy automatically on:
466
529
  • Every push to ${envInfo.currentBranch || 'main'} branch
@@ -477,4 +540,4 @@ Happy deploying! ✨
477
540
  `);
478
541
  }
479
542
 
480
- module.exports = { runInit };
543
+ module.exports = { runInit, createConfigFile, gitCommit };
package/lib/templates.js CHANGED
@@ -59,11 +59,27 @@ function getCacheValue(packageManager) {
59
59
  /**
60
60
  * Generate main deployment workflow
61
61
  */
62
+ /**
63
+ * The command that fetches and runs a package that is not a project dependency.
64
+ *
65
+ * `npx` cannot be assumed. In a pnpm workflow it resolves against the
66
+ * pnpm-managed environment and reports "playwright: not found" — exit 127 —
67
+ * even with --yes. Each package manager has its own equivalent, so use it.
68
+ */
69
+ function getDlxCommand(packageManager) {
70
+ switch (packageManager) {
71
+ case 'pnpm': return 'pnpm dlx';
72
+ case 'yarn': return 'yarn dlx';
73
+ default: return 'npx --yes';
74
+ }
75
+ }
76
+
62
77
  function generateMainWorkflow(projectId, apiUrl, packageManager, buildCmd) {
63
78
  const pmSetup = getPackageManagerSetup(packageManager);
64
79
  const installCmd = getInstallCommand(packageManager);
65
80
  const cache = getCacheValue(packageManager);
66
81
  const runCmd = packageManager === 'npm' ? `npm run ${buildCmd}` : `${packageManager} run ${buildCmd}`;
82
+ const dlx = getDlxCommand(packageManager);
67
83
 
68
84
  return `# Auto-generated by Scry Storybook Deployer
69
85
  # Deploy Storybook to production on push to main branch
@@ -96,6 +112,16 @@ ${cache}
96
112
  - name: Build Storybook
97
113
  run: ${runCmd}
98
114
 
115
+ - name: Install Playwright browser
116
+ # Screenshot capture drives --with-analysis, and without a browser every
117
+ # story fails, no metadata archive is produced, and nothing is ever
118
+ # indexed — while the deploy still exits 0 and the workflow goes green.
119
+ # Chromium headless shell only: a few seconds, versus minutes for the
120
+ # full browser set.
121
+ # The version is left floating so it tracks sbcov's own playwright
122
+ # ^1.41.0 optional dependency and installs a matching browser build.
123
+ run: ${dlx} playwright install --with-deps chromium-headless-shell
124
+
99
125
  - name: Deploy to Scry
100
126
  run: |
101
127
  npx @scrymore/scry-deployer \\
@@ -121,6 +147,7 @@ function generatePRWorkflow(projectId, apiUrl, packageManager, buildCmd) {
121
147
  const installCmd = getInstallCommand(packageManager);
122
148
  const cache = getCacheValue(packageManager);
123
149
  const runCmd = packageManager === 'npm' ? `npm run ${buildCmd}` : `${packageManager} run ${buildCmd}`;
150
+ const dlx = getDlxCommand(packageManager);
124
151
 
125
152
  return `# Auto-generated by Scry Storybook Deployer
126
153
  # Deploy Storybook preview for pull requests
@@ -157,6 +184,16 @@ ${cache}
157
184
  - name: Build Storybook
158
185
  run: ${runCmd}
159
186
 
187
+ - name: Install Playwright browser
188
+ # Screenshot capture drives --with-analysis, and without a browser every
189
+ # story fails, no metadata archive is produced, and nothing is ever
190
+ # indexed — while the deploy still exits 0 and the workflow goes green.
191
+ # Chromium headless shell only: a few seconds, versus minutes for the
192
+ # full browser set.
193
+ # The version is left floating so it tracks sbcov's own playwright
194
+ # ^1.41.0 optional dependency and installs a matching browser build.
195
+ run: ${dlx} playwright install --with-deps chromium-headless-shell
196
+
160
197
  - name: Deploy Preview
161
198
  id: deploy
162
199
  run: |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrymore/scry-deployer",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "A CLI to automate the deployment of Storybook static builds.",
5
5
  "main": "index.js",
6
6
  "bin": {