@bongos/core 1.18.0 → 1.18.3
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/.bongos-core.json +132 -322
- package/README.md +1 -1
- package/config/branding.neutral.json +5 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/README.md +5 -2
- package/docs/api/openapi.json +1789 -450
- package/docs/api-reference.md +52 -5
- package/docs/file-map.md +7 -7
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +211 -14
- package/scripts/gds/cli-lib.js +11 -5
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +42 -10
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/provision.js +145 -7
- package/scripts/gds/publish-manifest.js +15 -0
- package/scripts/gds/run-unit-tests.js +38 -14
- package/scripts/gds/setup.js +41 -0
- package/scripts/gds/ship.js +115 -31
- package/scripts/gds/upgrade.js +147 -5
- package/scripts/migrate.sh +10 -0
- package/src/bongos/auth.js +362 -5
- package/src/bongos/db.js +66 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/routes/auth.js +93 -11
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes.js +2 -0
- package/src/branding.js +17 -2
- package/src/instance-config.js +16 -0
- package/src/module-api.js +29 -1
- package/tests/branding.mjs +29 -23
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/idp_device_exchange.mjs +105 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/package_core.mjs +246 -1
- package/tests/provision.mjs +157 -0
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/ship_ci_deploy.mjs +60 -0
- package/tests/upgrade.mjs +254 -5
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Explore — featured projects</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Self-contained so the showcase renders correctly whether or not the host
|
|
9
|
+
injects theme tokens. Warm Cloud Bongos defaults; overridable by the pack. */
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #fff7ed; --card: #ffffff; --ink: #2a1f17; --ink-soft: #6b5d52;
|
|
12
|
+
--rule: #fed7aa; --accent: #ea580c;
|
|
13
|
+
}
|
|
14
|
+
* { box-sizing: border-box; }
|
|
15
|
+
body {
|
|
16
|
+
margin: 0; padding: 2.5rem 1.25rem 4rem;
|
|
17
|
+
background: var(--bg); color: var(--ink);
|
|
18
|
+
font-family: "Quicksand", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
}
|
|
21
|
+
.wrap { max-width: 960px; margin: 0 auto; }
|
|
22
|
+
header { text-align: center; margin-bottom: 2.5rem; }
|
|
23
|
+
h1 { font-size: 2rem; margin: 0 0 .35rem; letter-spacing: .01em; }
|
|
24
|
+
.sub { color: var(--ink-soft); margin: 0; }
|
|
25
|
+
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
|
|
26
|
+
.card {
|
|
27
|
+
background: var(--card); border: 1px solid var(--rule); border-radius: 14px;
|
|
28
|
+
padding: 1.1rem 1.2rem; text-decoration: none; color: inherit;
|
|
29
|
+
display: flex; flex-direction: column; gap: .4rem; transition: transform .12s ease, box-shadow .12s ease;
|
|
30
|
+
}
|
|
31
|
+
.card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(234,88,12,.12); }
|
|
32
|
+
.card h2 { font-size: 1.15rem; margin: 0; }
|
|
33
|
+
.card p { margin: 0; color: var(--ink-soft); font-size: .95rem; }
|
|
34
|
+
.card .origin { color: var(--accent); font-size: .82rem; word-break: break-all; margin-top: auto; }
|
|
35
|
+
.card .art { width: 100%; height: 120px; object-fit: cover; border-radius: 10px; margin-bottom: .3rem; }
|
|
36
|
+
.empty, .err { text-align: center; color: var(--ink-soft); padding: 3rem 1rem; }
|
|
37
|
+
@media (prefers-color-scheme: dark) {
|
|
38
|
+
:root { --bg: #1a1410; --card: #241c16; --ink: #f5ece3; --ink-soft: #b9a998; --rule: #4a3728; }
|
|
39
|
+
}
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div class="wrap">
|
|
44
|
+
<header>
|
|
45
|
+
<h1>Explore</h1>
|
|
46
|
+
<p class="sub">Featured projects built on the platform.</p>
|
|
47
|
+
</header>
|
|
48
|
+
<div id="grid" class="grid" hidden></div>
|
|
49
|
+
<div id="empty" class="empty" hidden>No featured projects yet — check back soon.</div>
|
|
50
|
+
<div id="err" class="err" hidden>Couldn't load projects right now.</div>
|
|
51
|
+
</div>
|
|
52
|
+
<script src="/explore.js"></script>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// explore.js — the public Explore showcase (ADR 0141 §5, task #1002110). Fetches the
|
|
2
|
+
// hub's featured projects and renders cards. On a NON-hub instance (platform-identity
|
|
3
|
+
// off) the endpoint 404s / returns nothing, so the page shows a graceful empty state.
|
|
4
|
+
//
|
|
5
|
+
// Project fields come from the network, so every value is set via textContent /
|
|
6
|
+
// attribute assignment — never innerHTML — so a project name/tagline can't inject markup.
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict';
|
|
9
|
+
var grid = document.getElementById('grid');
|
|
10
|
+
var empty = document.getElementById('empty');
|
|
11
|
+
var err = document.getElementById('err');
|
|
12
|
+
|
|
13
|
+
function show(el) { if (el) el.hidden = false; }
|
|
14
|
+
|
|
15
|
+
// Only http(s) origins are linkified — never javascript:/data: etc.
|
|
16
|
+
function safeHref(origin) {
|
|
17
|
+
try {
|
|
18
|
+
var u = new URL(String(origin));
|
|
19
|
+
return (u.protocol === 'https:' || u.protocol === 'http:') ? u.toString() : null;
|
|
20
|
+
} catch (_) { return null; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function card(p) {
|
|
24
|
+
var href = safeHref(p.origin);
|
|
25
|
+
var el = document.createElement(href ? 'a' : 'div');
|
|
26
|
+
el.className = 'card';
|
|
27
|
+
if (href) { el.href = href; el.target = '_blank'; el.rel = 'noopener noreferrer'; }
|
|
28
|
+
|
|
29
|
+
if (p.art_url && safeHref(p.art_url)) {
|
|
30
|
+
var img = document.createElement('img');
|
|
31
|
+
img.className = 'art';
|
|
32
|
+
img.src = p.art_url;
|
|
33
|
+
img.alt = '';
|
|
34
|
+
img.loading = 'lazy';
|
|
35
|
+
el.appendChild(img);
|
|
36
|
+
}
|
|
37
|
+
var h = document.createElement('h2');
|
|
38
|
+
h.textContent = p.name || p.origin || 'Untitled project';
|
|
39
|
+
el.appendChild(h);
|
|
40
|
+
if (p.tagline) {
|
|
41
|
+
var tag = document.createElement('p');
|
|
42
|
+
tag.textContent = p.tagline;
|
|
43
|
+
el.appendChild(tag);
|
|
44
|
+
}
|
|
45
|
+
if (p.origin) {
|
|
46
|
+
var o = document.createElement('span');
|
|
47
|
+
o.className = 'origin';
|
|
48
|
+
o.textContent = String(p.origin).replace(/^https?:\/\//, '');
|
|
49
|
+
el.appendChild(o);
|
|
50
|
+
}
|
|
51
|
+
return el;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fetch('/api/gds/projects/featured', { headers: { Accept: 'application/json' } })
|
|
55
|
+
.then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
|
|
56
|
+
.then(function (data) {
|
|
57
|
+
var projects = (data && Array.isArray(data.projects)) ? data.projects : [];
|
|
58
|
+
if (projects.length === 0) { show(empty); return; }
|
|
59
|
+
projects.forEach(function (p) { grid.appendChild(card(p)); });
|
|
60
|
+
show(grid);
|
|
61
|
+
})
|
|
62
|
+
.catch(function () {
|
|
63
|
+
// A missing endpoint (non-hub instance) is not an error to the visitor — show
|
|
64
|
+
// the empty state rather than a scary failure.
|
|
65
|
+
show(empty);
|
|
66
|
+
});
|
|
67
|
+
}());
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.18.
|
|
9
|
+
"version": "1.18.3",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.3",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
|
@@ -29,6 +29,13 @@
|
|
|
29
29
|
--font-mono:"Fira Code",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
30
30
|
}
|
|
31
31
|
*{box-sizing:border-box;margin:0;padding:0;}
|
|
32
|
+
/* The `hidden` attribute must always win. Several blocks below set an explicit
|
|
33
|
+
`display` via a class (.field→block, .row2→grid); an author rule like that
|
|
34
|
+
silently defeats the user-agent [hidden] rule, so `el.hidden = true` fails to
|
|
35
|
+
hide them. The Home step (task 1002228) toggles el.hidden to switch between the
|
|
36
|
+
adopt-picker / fresh-name / manual sub-panels — without this they ALL render at
|
|
37
|
+
once. Make hidden authoritative for the whole page. */
|
|
38
|
+
[hidden]{display:none !important;}
|
|
32
39
|
html{scroll-behavior:smooth;}
|
|
33
40
|
body{
|
|
34
41
|
font-family:var(--font-body); color:var(--ink);
|
|
@@ -327,6 +334,16 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
327
334
|
@media (prefers-reduced-motion:reduce){
|
|
328
335
|
*,*::before,*::after{animation:none !important;transition:none !important;}
|
|
329
336
|
}
|
|
337
|
+
/* GitHub repo picker (task 1002099) */
|
|
338
|
+
.linklike{background:none;border:0;padding:0;margin-left:12px;color:var(--accent-deep);font:inherit;font-size:13px;cursor:pointer;text-decoration:underline;}
|
|
339
|
+
#ghConnect .btn{margin-top:4px;}
|
|
340
|
+
.ghlist{margin-top:8px;max-height:220px;overflow-y:auto;border:1px solid var(--line,#e7d8c4);border-radius:10px;background:var(--bg-deep,#fff);}
|
|
341
|
+
.ghlist:empty{display:none;}
|
|
342
|
+
.ghrow{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:0;border-bottom:1px solid rgba(42,31,23,0.06);padding:9px 12px;font:inherit;font-size:13.5px;color:var(--ink,#2a1f17);cursor:pointer;}
|
|
343
|
+
.ghrow:last-child{border-bottom:0;}
|
|
344
|
+
.ghrow:hover{background:#fff9f2;}
|
|
345
|
+
.ghrow[aria-selected=true]{background:#fff3e6;box-shadow:inset 3px 0 0 var(--accent,#f97316);font-weight:600;}
|
|
346
|
+
.ghrow .priv{margin-left:auto;font-size:11px;font-weight:600;color:var(--ink-faint,#9b8a78);background:rgba(42,31,23,0.06);border-radius:5px;padding:1px 6px;}
|
|
330
347
|
</style>
|
|
331
348
|
</head>
|
|
332
349
|
<body>
|
|
@@ -405,7 +422,31 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
405
422
|
</button>
|
|
406
423
|
</div>
|
|
407
424
|
<div class="reassure" id="adoptNote">Adopting never overwrites your files: you get a conflict pre-flight first, nothing is written until you accept, and your existing backlog (issues, TODOs) is imported as real tasks.</div>
|
|
408
|
-
|
|
425
|
+
|
|
426
|
+
<!-- GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099.
|
|
427
|
+
Progressive enhancement: when the signed-in user grants repo access we let
|
|
428
|
+
them PICK a repo (adopt) or NAME one we auto-create (fresh). Without that
|
|
429
|
+
access — not granted yet, or the server can't store the token — we fall back
|
|
430
|
+
to the manual owner/repo inputs below (#manualHome), which stay authoritative. -->
|
|
431
|
+
<div id="ghConnect" class="reassure">
|
|
432
|
+
<p id="ghConnectMsg"> </p>
|
|
433
|
+
<button type="button" class="btn" id="ghConnectBtn">Connect GitHub</button>
|
|
434
|
+
<button type="button" class="linklike" id="ghManualToggle">or enter it manually</button>
|
|
435
|
+
</div>
|
|
436
|
+
<div id="ghBusy" class="hint" hidden>Reading your GitHub repositories…</div>
|
|
437
|
+
<div id="ghAdopt" class="field" hidden>
|
|
438
|
+
<span>Pick one of your repositories</span>
|
|
439
|
+
<input type="text" id="ghSearch" placeholder="Search your repos…" autocomplete="off">
|
|
440
|
+
<div id="ghList" class="ghlist" role="listbox" aria-label="Your repositories"></div>
|
|
441
|
+
<div class="hint" id="ghAdoptHint"> </div>
|
|
442
|
+
</div>
|
|
443
|
+
<div id="ghFresh" class="field" hidden>
|
|
444
|
+
<span>New repository name</span>
|
|
445
|
+
<input type="text" id="ghNewName" placeholder="mercury" autocomplete="off" maxlength="100">
|
|
446
|
+
<div class="hint" id="ghFreshHint">We’ll create an empty repo at <code id="ghFreshPreview" style="font-family:var(--font-mono)">github.com/you/mercury</code>.</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<div class="row2" id="manualHome">
|
|
409
450
|
<label class="field"><span>GitHub owner <small>(user or org)</small></span>
|
|
410
451
|
<input type="text" id="f-owner" placeholder="acme" autocomplete="off" maxlength="60">
|
|
411
452
|
</label>
|
|
@@ -595,6 +636,7 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
595
636
|
saveDraft();
|
|
596
637
|
var h = $(panels[step]).querySelector('h1');
|
|
597
638
|
if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false }); }
|
|
639
|
+
if (step === 2) { syncHome(); ensureRepoAccess(); }
|
|
598
640
|
if (step === 4) renderReview();
|
|
599
641
|
}
|
|
600
642
|
|
|
@@ -641,11 +683,108 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
641
683
|
$('ghCreate').classList.toggle('on', mode === 'greenfield');
|
|
642
684
|
updateGhCreateHref();
|
|
643
685
|
saveDraft();
|
|
686
|
+
syncHome();
|
|
644
687
|
}
|
|
645
688
|
$('c-green').addEventListener('click', function () { setMode('greenfield'); });
|
|
646
689
|
$('c-adopt').addEventListener('click', function () { setMode('adopt'); });
|
|
647
690
|
setMode(state.mode);
|
|
648
691
|
|
|
692
|
+
/* ── GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099 / ADR 0143.
|
|
693
|
+
Progressive enhancement: probe GET /github/repos once we reach step 2.
|
|
694
|
+
200 → we hold repo access → show the picker (adopt) / name field (fresh);
|
|
695
|
+
403 → offer "Connect GitHub" (a ?repos=1 re-sign-in);
|
|
696
|
+
503 (server can't store the token) or any error → the manual inputs.
|
|
697
|
+
#manualHome (the classic owner/repo fields) is the always-safe fallback. ── */
|
|
698
|
+
var repoAccess = null; // null=unknown/loading · true · 'need-connect' · 'unavailable' · 'manual'
|
|
699
|
+
var repoList = []; // the user's repos (from the probe) — feeds the picker + the fresh-name clash hint
|
|
700
|
+
var ghProbed = false;
|
|
701
|
+
|
|
702
|
+
function isManualHome() { return repoAccess === 'manual' || repoAccess === 'unavailable'; }
|
|
703
|
+
function validRepoName(s) { return /^[A-Za-z0-9._-]{1,100}$/.test(String(s || '')); }
|
|
704
|
+
function myLogin() { return (me && me.github_login) || 'you'; }
|
|
705
|
+
|
|
706
|
+
function connectRepos(e) {
|
|
707
|
+
if (e) e.preventDefault();
|
|
708
|
+
// Same-tab navigation so the sessionStorage draft round-trips back to step 2.
|
|
709
|
+
window.location.assign(API + '/auth/web/start?return=/create&repos=1');
|
|
710
|
+
}
|
|
711
|
+
$('ghConnectBtn').addEventListener('click', connectRepos);
|
|
712
|
+
$('ghManualToggle').addEventListener('click', function () { repoAccess = 'manual'; syncHome(); $('f-owner').focus(); });
|
|
713
|
+
|
|
714
|
+
function syncHome() {
|
|
715
|
+
var need = repoAccess === 'need-connect';
|
|
716
|
+
var ok = repoAccess === true;
|
|
717
|
+
var adopt = state.mode === 'adopt';
|
|
718
|
+
$('ghBusy').hidden = !!repoAccess; // busy only while still probing (null)
|
|
719
|
+
$('ghConnect').classList.toggle('on', need); // .reassure box: shown via .on (not hidden — .reassure sets display:none)
|
|
720
|
+
$('ghAdopt').hidden = !(ok && adopt);
|
|
721
|
+
$('ghFresh').hidden = !(ok && !adopt);
|
|
722
|
+
$('manualHome').hidden = !isManualHome();
|
|
723
|
+
if (need) {
|
|
724
|
+
$('ghConnectMsg').textContent = adopt
|
|
725
|
+
? 'Connect your GitHub account and pick the repository to adopt.'
|
|
726
|
+
: 'Connect your GitHub account and we’ll create the empty repo for you.';
|
|
727
|
+
}
|
|
728
|
+
if (ok && adopt) renderRepoList();
|
|
729
|
+
if (ok && !adopt) syncFresh();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function syncFresh() {
|
|
733
|
+
// Fresh + access: the repo is created under the signed-in account. Seed owner for
|
|
734
|
+
// the review/target now; the create response confirms the real full_name.
|
|
735
|
+
state.owner = myLogin();
|
|
736
|
+
$('ghNewName').value = state.repo || '';
|
|
737
|
+
updateFreshPreview();
|
|
738
|
+
}
|
|
739
|
+
function updateFreshPreview() {
|
|
740
|
+
var hint = $('ghFreshHint');
|
|
741
|
+
var clash = state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
|
|
742
|
+
if (state.repo && !validRepoName(state.repo)) {
|
|
743
|
+
hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
|
|
744
|
+
} else if (clash) {
|
|
745
|
+
hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
|
|
746
|
+
} else {
|
|
747
|
+
hint.className = 'hint'; hint.innerHTML = 'We’ll create an empty repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code>.';
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
751
|
+
|
|
752
|
+
function renderRepoList() {
|
|
753
|
+
var q = ($('ghSearch').value || '').trim().toLowerCase();
|
|
754
|
+
var rows = repoList.filter(function (r) { return !q || String(r.full_name).toLowerCase().indexOf(q) >= 0; }).slice(0, 100);
|
|
755
|
+
var sel = state.owner && state.repo ? (state.owner + '/' + state.repo) : '';
|
|
756
|
+
$('ghList').innerHTML = rows.length
|
|
757
|
+
? rows.map(function (r) {
|
|
758
|
+
return '<button type="button" class="ghrow" role="option" data-full="' + esc(r.full_name) + '" aria-selected="' + (r.full_name === sel) + '">' +
|
|
759
|
+
'<span>' + esc(r.full_name) + '</span>' + (r.private ? '<span class="priv">Private</span>' : '') + '</button>';
|
|
760
|
+
}).join('')
|
|
761
|
+
: '<div class="hint" style="padding:10px 12px">No repositories match “' + esc(q) + '”.</div>';
|
|
762
|
+
}
|
|
763
|
+
$('ghSearch').addEventListener('input', renderRepoList);
|
|
764
|
+
$('ghList').addEventListener('click', function (ev) {
|
|
765
|
+
var row = ev.target.closest('.ghrow'); if (!row) return;
|
|
766
|
+
var full = row.getAttribute('data-full'); var slash = full.indexOf('/');
|
|
767
|
+
state.owner = full.slice(0, slash); state.repo = full.slice(slash + 1);
|
|
768
|
+
saveDraft();
|
|
769
|
+
Array.prototype.forEach.call($('ghList').querySelectorAll('.ghrow'), function (b) { b.setAttribute('aria-selected', String(b === row)); });
|
|
770
|
+
var h = $('ghAdoptHint'); h.className = 'hint ok'; h.innerHTML = 'Adopting <strong>' + esc(full) + '</strong>.';
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
function ensureRepoAccess() {
|
|
774
|
+
if (ghProbed && repoAccess !== null) return; // already known this session
|
|
775
|
+
if (isManualHome()) return; // user chose manual — don't nag
|
|
776
|
+
repoAccess = null; syncHome(); // show "reading your repos…"
|
|
777
|
+
fetch(API + '/github/repos', { credentials: 'same-origin' })
|
|
778
|
+
.then(function (res) {
|
|
779
|
+
ghProbed = true;
|
|
780
|
+
if (res.status === 200) return res.json().then(function (d) { repoList = (d && d.repos) || []; repoAccess = true; });
|
|
781
|
+
if (res.status === 403 || res.status === 401) { repoAccess = 'need-connect'; return; }
|
|
782
|
+
repoAccess = 'unavailable'; return; // 503 or anything else → manual fallback
|
|
783
|
+
})
|
|
784
|
+
.catch(function () { ghProbed = true; repoAccess = 'unavailable'; })
|
|
785
|
+
.then(syncHome);
|
|
786
|
+
}
|
|
787
|
+
|
|
649
788
|
$('f-owner').addEventListener('input', function () { state.owner = this.value.trim(); state.ownerTouched = true; updateGhCreateHref(); saveDraft(); });
|
|
650
789
|
$('f-repo').addEventListener('input', function () { state.repo = this.value.trim(); state.repoTouched = true; updateGhCreateHref(); saveDraft(); });
|
|
651
790
|
|
|
@@ -675,7 +814,17 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
675
814
|
return true;
|
|
676
815
|
}
|
|
677
816
|
if (step === 2) {
|
|
678
|
-
|
|
817
|
+
var homeErr = function (msg) { $('h-home').textContent = msg; $('h-home').className = 'hint err'; return false; };
|
|
818
|
+
if (repoAccess === 'need-connect') return homeErr('Connect GitHub to pick or create your repository — or choose “enter it manually”.');
|
|
819
|
+
if (repoAccess === true && state.mode !== 'adopt') {
|
|
820
|
+
// Fresh with access: just the new repo NAME (owner is the signed-in account).
|
|
821
|
+
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
822
|
+
} else {
|
|
823
|
+
// Adopt (picker or manual) and the manual fallback: need a concrete owner + repo.
|
|
824
|
+
if (!state.owner || !state.repo) {
|
|
825
|
+
return homeErr(state.mode === 'adopt' && repoAccess === true ? 'Pick one of your repositories.' : 'Both owner and repository name are needed.');
|
|
826
|
+
}
|
|
827
|
+
}
|
|
679
828
|
$('h-home').innerHTML = ' '; $('h-home').className = 'hint';
|
|
680
829
|
return true;
|
|
681
830
|
}
|
|
@@ -725,20 +874,58 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
725
874
|
return 'Couldn’t file the request: ' + (err && err.message ? err.message : 'something went sideways. Try again.');
|
|
726
875
|
}
|
|
727
876
|
|
|
877
|
+
/* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
|
|
878
|
+
then hand its real full_name to provisioning. No-op for adopt / manual / no-access.
|
|
879
|
+
Idempotent across a retry — once created we reuse the stashed full_name so a second
|
|
880
|
+
click doesn't trip GitHub's 422 "name already exists". Errors set err.gotoHome so
|
|
881
|
+
the caller bounces the user back to the Home step to fix the name / reconnect. */
|
|
882
|
+
function ensureFreshRepo() {
|
|
883
|
+
if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
|
|
884
|
+
if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
|
|
885
|
+
return fetch(API + '/github/repos', {
|
|
886
|
+
method: 'POST', credentials: 'same-origin',
|
|
887
|
+
headers: { 'Content-Type': 'application/json' },
|
|
888
|
+
body: JSON.stringify({ name: state.repo })
|
|
889
|
+
}).then(function (res) {
|
|
890
|
+
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
891
|
+
if (res.status === 401 || res.status === 403) {
|
|
892
|
+
repoAccess = 'need-connect';
|
|
893
|
+
var e0 = new Error('reconnect'); e0.gotoHome = true;
|
|
894
|
+
e0.homeMsg = 'Your GitHub authorization expired — reconnect on the Home step to create the repo.';
|
|
895
|
+
throw e0;
|
|
896
|
+
}
|
|
897
|
+
if (!res.ok) {
|
|
898
|
+
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
899
|
+
e.data = data;
|
|
900
|
+
if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
901
|
+
throw e;
|
|
902
|
+
}
|
|
903
|
+
var full = data.repo && data.repo.full_name;
|
|
904
|
+
if (full && full.indexOf('/') > 0) {
|
|
905
|
+
state.owner = full.slice(0, full.indexOf('/'));
|
|
906
|
+
state.repo = full.slice(full.indexOf('/') + 1);
|
|
907
|
+
}
|
|
908
|
+
state.repoAutoCreated = true; saveDraft();
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
728
913
|
$('doCreate').addEventListener('click', function () {
|
|
729
914
|
var btn = this;
|
|
730
915
|
var alertBox = $('createAlert');
|
|
731
916
|
alertBox.classList.remove('on');
|
|
732
917
|
btn.disabled = true; btn.textContent = 'Raising it into the sky…';
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
918
|
+
ensureFreshRepo().then(function () {
|
|
919
|
+
return fetch(API + '/provisioning/instances', {
|
|
920
|
+
method: 'POST', credentials: 'same-origin',
|
|
921
|
+
headers: { 'Content-Type': 'application/json' },
|
|
922
|
+
body: JSON.stringify({
|
|
923
|
+
slug: state.slug,
|
|
924
|
+
hosting_shape: 'standalone',
|
|
925
|
+
domain: state.domain || undefined,
|
|
926
|
+
target_ref: state.owner + '/' + state.repo
|
|
927
|
+
})
|
|
928
|
+
});
|
|
742
929
|
}).then(function (res) {
|
|
743
930
|
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/create'); throw new Error('signing in'); }
|
|
744
931
|
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
@@ -761,9 +948,17 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
761
948
|
startProgress(data.instance && data.instance.id);
|
|
762
949
|
}).catch(function (err) {
|
|
763
950
|
if (String(err && err.message) === 'signing in') return;
|
|
951
|
+
btn.disabled = false; btn.textContent = 'Create my project';
|
|
952
|
+
if (err && err.gotoHome) {
|
|
953
|
+
// A repo-creation problem (name taken / reconnect) — bounce back to the Home
|
|
954
|
+
// step with the reason, so the user fixes it where the repo is chosen.
|
|
955
|
+
show(2);
|
|
956
|
+
$('h-home').textContent = err.homeMsg || 'Please revisit the repository step.';
|
|
957
|
+
$('h-home').className = 'hint err';
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
764
960
|
alertBox.textContent = friendlyCreateError(err);
|
|
765
961
|
alertBox.classList.add('on');
|
|
766
|
-
btn.disabled = false; btn.textContent = 'Create my project';
|
|
767
962
|
});
|
|
768
963
|
});
|
|
769
964
|
|
|
@@ -786,8 +981,10 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
786
981
|
var ghIcon = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
|
|
787
982
|
steps.push(adopt
|
|
788
983
|
? '<li data-onboard-step="github-repo"><b>Layer onto your existing repo</b><p>In your repo run <code style="font-family:var(--font-mono)">bongos init --adopt</code>. It shows a conflict pre-flight and writes <em>nothing</em> until you accept — your files and history stay yours, and your existing backlog is imported. Commit and push.</p></li>'
|
|
789
|
-
:
|
|
790
|
-
'<
|
|
984
|
+
: (state.repoAutoCreated
|
|
985
|
+
? '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLabel + ' on GitHub for you — empty and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>'
|
|
986
|
+
: '<li data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
|
|
987
|
+
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + ghIcon + 'Create ' + repoLabel + ' on GitHub</a></li>'));
|
|
791
988
|
if (!state.domain) {
|
|
792
989
|
steps.push('<li><b>Add a domain you own</b><p>Sign-in on your new instance needs a real domain. When you have one, come back here or ask your hall — the address gets wired in minutes.</p></li>');
|
|
793
990
|
}
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -27,10 +27,16 @@ const SESSION_READ_PATHS = [
|
|
|
27
27
|
...ic.configReadPaths('gds-session.json'),
|
|
28
28
|
...ic.configReadPaths('pms-session.json'),
|
|
29
29
|
];
|
|
30
|
-
//
|
|
30
|
+
// The last-resort default API base — THIS instance's own publicOrigin from the
|
|
31
|
+
// branding pack (task 1002201). One source of truth in instance-config so no CLI
|
|
32
|
+
// script hard-codes another instance's URL; pre-ADR-0139 the literal
|
|
33
|
+
// 'https://amazonprimea.com' was baked in at four sites here, so an unconfigured
|
|
34
|
+
// consumer targeted the founder's (game-only) box. Override with <PREFIX>_API_BASE
|
|
35
|
+
// for local dev / staging:
|
|
31
36
|
// CLOUDBONGOS_API_BASE=http://localhost:3000 node scripts/gds/start.js
|
|
32
37
|
// Resolves the configured prefix first, then legacy GDS_/OTB_/PMS_API_BASE.
|
|
33
|
-
const
|
|
38
|
+
const DEFAULT_API_BASE = ic.defaultApiBase();
|
|
39
|
+
const API_BASE = ic.resolveEnv('API_BASE') || DEFAULT_API_BASE;
|
|
34
40
|
|
|
35
41
|
async function readSessionFile(p) {
|
|
36
42
|
try {
|
|
@@ -102,7 +108,7 @@ function readSessionToken() {
|
|
|
102
108
|
if (!j || !j.token) return null;
|
|
103
109
|
return {
|
|
104
110
|
token: j.token,
|
|
105
|
-
apiBase: ic.resolveEnv('API_BASE') || j.api_base ||
|
|
111
|
+
apiBase: ic.resolveEnv('API_BASE') || j.api_base || DEFAULT_API_BASE,
|
|
106
112
|
};
|
|
107
113
|
}
|
|
108
114
|
|
|
@@ -278,7 +284,7 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
|
|
|
278
284
|
// (local dev / staging override), then the logged-in instance, then prod. OTB
|
|
279
285
|
// sessions already store api_base='https://amazonprimea.com', so this is a no-op
|
|
280
286
|
// for the OTB instance and only bites once someone logs in to a different one.
|
|
281
|
-
const base = ic.resolveEnv('API_BASE') || (session && session.api_base) ||
|
|
287
|
+
const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
|
|
282
288
|
const headers = {
|
|
283
289
|
'Content-Type': 'application/json',
|
|
284
290
|
'User-Agent': 'amazonprimea-gds-cli/1.0',
|
|
@@ -349,7 +355,7 @@ async function cliClient(opts = {}) {
|
|
|
349
355
|
printInstanceMismatchAndExit(mm);
|
|
350
356
|
}
|
|
351
357
|
}
|
|
352
|
-
const base = ic.resolveEnv('API_BASE') || (session && session.api_base) ||
|
|
358
|
+
const base = ic.resolveEnv('API_BASE') || (session && session.api_base) || DEFAULT_API_BASE;
|
|
353
359
|
const token = (session && session.token) || null;
|
|
354
360
|
const key = `${base}|${token || ''}|${allowUnauthenticated}|${allowBoxScope}`;
|
|
355
361
|
if (_clientCache.has(key)) return _clientCache.get(key);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// scripts/gds/lint-migrations.js — core-migration FORWARD-SAFETY guard (task 1002223 / audit H10,
|
|
4
|
+
// finding F6).
|
|
5
|
+
//
|
|
6
|
+
// `bongos upgrade` auto-rollback reverts the CODE pin but DELIBERATELY NOT the database
|
|
7
|
+
// (upgrade.js: "the DB is deliberately NOT rolled back … core migrations are additive + forward-
|
|
8
|
+
// safe"). That safety rested on a convention nothing checked. A core migration is FORWARD-SAFE when
|
|
9
|
+
// the PREVIOUS code version still runs against the post-migration schema — so a code-only rollback
|
|
10
|
+
// never strands old code on a schema it can't read. This lints core migrations for operations that
|
|
11
|
+
// BREAK that property and fails unless the migration explicitly acknowledges the exception with a
|
|
12
|
+
// -- forward-unsafe: <reason>
|
|
13
|
+
// pragma (a deliberate, documented decision — e.g. a historical migration, or one paired with a
|
|
14
|
+
// two-step deploy).
|
|
15
|
+
//
|
|
16
|
+
// SAFE (never flagged): CREATE TABLE/INDEX/…, ADD COLUMN, INSERT/UPDATE, and DROP-then-CREATE of an
|
|
17
|
+
// INDEX / TRIGGER / VIEW / FUNCTION / POLICY (the recreate-in-place idempotency pattern) — none of
|
|
18
|
+
// these remove state the old code depends on.
|
|
19
|
+
// UNSAFE (flagged): DROP TABLE, DROP COLUMN, RENAME COLUMN, ALTER TABLE … RENAME TO, ALTER COLUMN …
|
|
20
|
+
// TYPE, SET NOT NULL — each removes / renames / retypes existing state old code reads.
|
|
21
|
+
//
|
|
22
|
+
// Usage:
|
|
23
|
+
// node scripts/gds/lint-migrations.js # STRICT: exit 1 on any unacknowledged unsafe migration
|
|
24
|
+
// node scripts/gds/lint-migrations.js --warn # report only, always exit 0 (migrate.sh uses this)
|
|
25
|
+
// node scripts/gds/lint-migrations.js --dir <path> # scan a specific migrations dir (default: <repo>/migrations)
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
31
|
+
|
|
32
|
+
// The acknowledgement pragma. Lives in a SQL comment; matched on the raw file text.
|
|
33
|
+
const ACK_RE = /^\s*--\s*forward-unsafe\s*:\s*(.+)$/im;
|
|
34
|
+
|
|
35
|
+
// Genuinely forward-unsafe statement shapes. Ordered; a file matching ANY is unsafe.
|
|
36
|
+
const UNSAFE = [
|
|
37
|
+
['drop-table', /\bdrop\s+table\b/i],
|
|
38
|
+
['drop-column', /\bdrop\s+column\b/i],
|
|
39
|
+
['rename-column', /\brename\s+column\b/i],
|
|
40
|
+
['rename-table', /\balter\s+table\s+[^\s;]+\s+rename\s+to\b/i],
|
|
41
|
+
['alter-column-type', /\balter\s+column\b[^;]*\btype\b/i],
|
|
42
|
+
['set-not-null', /\bset\s+not\s+null\b/i],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Strip block + line comments so a pattern that appears only in prose/comments never counts as an
|
|
46
|
+
// operation. (The ACK pragma is detected separately, on the raw text, since it IS a comment.)
|
|
47
|
+
function stripSqlComments(sql) {
|
|
48
|
+
return String(sql).replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/--[^\n]*/g, ' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Classify one migration's SQL: { unsafe: [names], acknowledged: bool, ackReason }.
|
|
52
|
+
function scanMigrationSql(sql) {
|
|
53
|
+
const code = stripSqlComments(sql);
|
|
54
|
+
const unsafe = [];
|
|
55
|
+
for (const [name, re] of UNSAFE) if (re.test(code)) unsafe.push(name);
|
|
56
|
+
const ack = String(sql).match(ACK_RE);
|
|
57
|
+
return { unsafe, acknowledged: !!ack, ackReason: ack ? ack[1].trim() : null };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Scan a migrations directory. Returns { violations, acknowledged } — each entry { file, unsafe, ackReason }.
|
|
61
|
+
// `violations` = unsafe WITHOUT the pragma (fail-closed); `acknowledged` = unsafe WITH it (documented).
|
|
62
|
+
function scanMigrationsDir(dir, fsImpl = fs) {
|
|
63
|
+
let files;
|
|
64
|
+
try { files = fsImpl.readdirSync(dir).filter((f) => f.endsWith('.sql')); }
|
|
65
|
+
catch { return { violations: [], acknowledged: [], scanned: 0 }; }
|
|
66
|
+
const violations = [];
|
|
67
|
+
const acknowledged = [];
|
|
68
|
+
for (const f of files.sort()) {
|
|
69
|
+
const r = scanMigrationSql(fsImpl.readFileSync(path.join(dir, f), 'utf8'));
|
|
70
|
+
if (r.unsafe.length === 0) continue;
|
|
71
|
+
(r.acknowledged ? acknowledged : violations).push({ file: f, unsafe: r.unsafe, ackReason: r.ackReason });
|
|
72
|
+
}
|
|
73
|
+
return { violations, acknowledged, scanned: files.length };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function arg(name, fallback = null) {
|
|
77
|
+
const i = process.argv.indexOf(name);
|
|
78
|
+
return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : fallback;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function main() {
|
|
82
|
+
const warnOnly = process.argv.includes('--warn');
|
|
83
|
+
const dir = path.resolve(arg('--dir', path.join(REPO_ROOT, 'migrations')));
|
|
84
|
+
const { violations, acknowledged, scanned } = scanMigrationsDir(dir);
|
|
85
|
+
|
|
86
|
+
for (const a of acknowledged) {
|
|
87
|
+
console.error(`[migrations] forward-unsafe (acknowledged): ${a.file} [${a.unsafe.join(', ')}] — ${a.ackReason}`);
|
|
88
|
+
}
|
|
89
|
+
if (violations.length === 0) {
|
|
90
|
+
console.error(`[migrations] forward-safety: OK — ${scanned} core migration(s) scanned, ${acknowledged.length} acknowledged exception(s), 0 unacknowledged.`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
console.error(`[migrations] forward-safety: ${warnOnly ? 'WARNING' : 'FAILED'} — ${violations.length} core migration(s) contain a forward-UNSAFE operation with no acknowledgement:`);
|
|
94
|
+
for (const v of violations) console.error(` ${v.file} [${v.unsafe.join(', ')}]`);
|
|
95
|
+
console.error(' A code-only `bongos upgrade` rollback does NOT revert the DB, so old code would run on this schema.');
|
|
96
|
+
console.error(' If intentional, add a top-of-file SQL comment: -- forward-unsafe: <why it is safe here / the two-step plan>');
|
|
97
|
+
return warnOnly ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { scanMigrationSql, scanMigrationsDir, stripSqlComments, UNSAFE, ACK_RE };
|
|
101
|
+
|
|
102
|
+
if (require.main === module) process.exit(main());
|