@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 +6 -2
- package/lib/init.js +123 -60
- package/lib/templates.js +37 -0
- package/package.json +1 -1
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: '
|
|
412
|
+
describe: 'Write the API key into the committed config file (not recommended)',
|
|
413
413
|
type: 'boolean',
|
|
414
|
-
default
|
|
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:
|
|
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 (
|
|
57
|
-
logger.info('4/8:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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:
|
|
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(
|
|
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
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
331
|
+
execSync('gh variable --help', { stdio: 'pipe' });
|
|
332
|
+
return true;
|
|
333
|
+
} catch {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
326
337
|
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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: |
|