@bongos/core 1.21.95 → 1.21.96
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 +34 -29
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +8 -3
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +109 -7
- package/docs/api-reference.md +6 -4
- package/docs/copy-inventory.md +367 -358
- package/docs/copy-registry.json +536 -425
- package/docs/design/project-startup-direction.md +9 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +351 -348
- package/modules/provisioning/github-setup.js +27 -5
- package/modules/provisioning/routes/github-setup.js +1 -0
- package/modules/provisioning/routes/provisioning.js +29 -4
- package/modules/public-landing/public/projects.html +264 -14
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/github-repos.js +129 -0
- package/src/bongos/routes/github.js +71 -5
- package/src/module-api.js +1 -1
- package/tests/github_org_account.mjs +378 -0
|
@@ -37,8 +37,17 @@ const SETUP_EVENTS = Object.freeze({
|
|
|
37
37
|
installReported: 'github-app-install-reported',
|
|
38
38
|
installed: 'github-app-installed',
|
|
39
39
|
deviceFlow: 'github-app-device-flow',
|
|
40
|
+
// the organisation the founder chose to make the app on (task 1004651); its detail is
|
|
41
|
+
// the org's login. Written by the mint route; absent = the founder's personal account.
|
|
42
|
+
appOrg: 'github-app-org',
|
|
40
43
|
});
|
|
41
44
|
|
|
45
|
+
// A GitHub login (1-39 chars, letters/digits and single inner hyphens). The org name the
|
|
46
|
+
// mint route takes from the page passes this before it reaches a GitHub URL, and the
|
|
47
|
+
// recorded one passes it again before it reaches a link. PURE.
|
|
48
|
+
const GITHUB_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
49
|
+
function validGithubLogin(s) { return typeof s === 'string' && GITHUB_LOGIN_RE.test(s); }
|
|
50
|
+
|
|
42
51
|
// How long after an attempt settles the runner keeps checking for the install and the
|
|
43
52
|
// device-flow tick. Past it the checklist stops promising a self-tick for device flow
|
|
44
53
|
// (the step says "turn it on later"); a fresh install report re-opens the window.
|
|
@@ -65,16 +74,23 @@ function targetRepo(inst) {
|
|
|
65
74
|
// The verified facts for one instance since `since` (the attempt's created_at): when each
|
|
66
75
|
// setup event was first seen, or null. One indexed read (idx_provisioning_events_instance).
|
|
67
76
|
async function githubSetupFacts(db, instanceId, since) {
|
|
77
|
+
// `detail` is the newest row's, for the one event that carries a value (appOrg)
|
|
68
78
|
const { rows } = await db.query(
|
|
69
|
-
`SELECT event, min(created_at) AS at
|
|
79
|
+
`SELECT event, min(created_at) AS at, (array_agg(detail ORDER BY created_at DESC))[1] AS detail
|
|
80
|
+
FROM provisioning_events
|
|
70
81
|
WHERE instance_id = $1 AND event = ANY($2) AND created_at >= $3
|
|
71
82
|
GROUP BY event`,
|
|
72
83
|
[instanceId, Object.values(SETUP_EVENTS), since || new Date(0).toISOString()]);
|
|
84
|
+
const find = (name) => rows.find((x) => x.event === name);
|
|
73
85
|
const at = (name) => {
|
|
74
|
-
const r =
|
|
86
|
+
const r = find(name);
|
|
75
87
|
return r && r.at ? new Date(r.at).toISOString() : null;
|
|
76
88
|
};
|
|
77
|
-
|
|
89
|
+
const org = find(SETUP_EVENTS.appOrg);
|
|
90
|
+
return {
|
|
91
|
+
installReportedAt: at(SETUP_EVENTS.installReported), installedAt: at(SETUP_EVENTS.installed), deviceFlowAt: at(SETUP_EVENTS.deviceFlow),
|
|
92
|
+
appOrg: org && validGithubLogin(org.detail) ? org.detail : null,
|
|
93
|
+
};
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
// Each checklist step's state, from the latest attempt's EFFECTIVE status (manifest-truth's
|
|
@@ -113,7 +129,7 @@ function stillVerifying(row, facts = {}, now = Date.now()) {
|
|
|
113
129
|
// lands on the setup page, which sends the founder back to their projects.
|
|
114
130
|
// A failed facts read is "nothing verified yet" — the honest 'now' states, never a 500.
|
|
115
131
|
async function githubSetupRead(db, inst, row, status, { now = Date.now(), withState = true } = {}) {
|
|
116
|
-
if (!row) return { checklist: githubChecklist(null, null), install_url: null, repo: targetRepo(inst), verifying: false };
|
|
132
|
+
if (!row) return { checklist: githubChecklist(null, null), install_url: null, repo: targetRepo(inst), verifying: false, app_org: null };
|
|
117
133
|
let facts = {};
|
|
118
134
|
// through module.exports so a route test can stub the one DB read on the cached module
|
|
119
135
|
try { facts = await module.exports.githubSetupFacts(db, row.instance_id, row.created_at); } catch (_) { facts = {}; }
|
|
@@ -123,7 +139,13 @@ async function githubSetupRead(db, inst, row, status, { now = Date.now(), withSt
|
|
|
123
139
|
install_url: checklist.install === 'done' ? null : installUrlFor({ appSlug: row.app_slug, state: withState ? row.state : null }),
|
|
124
140
|
repo: targetRepo(inst),
|
|
125
141
|
verifying: stillVerifying(row, facts, now),
|
|
142
|
+
// the organisation this attempt makes the app on, or null for the founder's account
|
|
143
|
+
// (task 1004651): the page links the app's settings under the right owner
|
|
144
|
+
app_org: validGithubLogin(facts.appOrg) ? facts.appOrg : null,
|
|
126
145
|
};
|
|
127
146
|
}
|
|
128
147
|
|
|
129
|
-
module.exports = {
|
|
148
|
+
module.exports = {
|
|
149
|
+
SETUP_EVENTS, VERIFY_WINDOW_MS, installUrlFor, targetRepo, githubSetupFacts, githubChecklist, stillVerifying, githubSetupRead,
|
|
150
|
+
validGithubLogin,
|
|
151
|
+
};
|
|
@@ -100,6 +100,7 @@ module.exports = function buildGithubSetupRouter() {
|
|
|
100
100
|
install_url: setup.install_url,
|
|
101
101
|
repo: setup.repo,
|
|
102
102
|
verifying: setup.verifying,
|
|
103
|
+
app_org: setup.app_org,
|
|
103
104
|
});
|
|
104
105
|
} catch (err) {
|
|
105
106
|
log.error('[provisioning] GET /provisioning/github-app/status', err);
|
|
@@ -1230,11 +1230,22 @@ module.exports = function provisioningRoutes() {
|
|
|
1230
1230
|
// GitHub, the owner approves, and GitHub redirects back to the public callback below.
|
|
1231
1231
|
// The web tier only RECORDS the attempt (state token) — the control-plane runner does
|
|
1232
1232
|
// the token exchange + cred placement (trust boundary intact, ADR 0111 §2).
|
|
1233
|
-
// Body: { account? ('personal'|'org') }.
|
|
1233
|
+
// Body: { account? ('personal'|'org'), org? (a GitHub organisation login) }.
|
|
1234
|
+
// A personal or an organisation account (task 1004651): account 'org' makes the app on
|
|
1235
|
+
// the organisation the founder chose on the page (`org`), else on the repo's owner. The
|
|
1236
|
+
// org name is checked against GitHub's login rules here, never trusted from the page;
|
|
1237
|
+
// whether the founder may make an app there is GitHub's to enforce on its own page
|
|
1238
|
+
// (this module holds no GitHub token to ask with, ADR 0111 §2). Anything other than
|
|
1239
|
+
// account 'org' is the founder's personal account, exactly as before.
|
|
1234
1240
|
router.post('/provisioning/instances/:id/github-app', auth.requireBuilder, async (req, res) => {
|
|
1235
1241
|
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
1236
1242
|
if (!id) return;
|
|
1237
|
-
if (validateOrRespond(req, res, { account: { type: 'string', maxLength: 12 } })) return;
|
|
1243
|
+
if (validateOrRespond(req, res, { account: { type: 'string', maxLength: 12 }, org: { type: 'string', maxLength: 39 } })) return;
|
|
1244
|
+
const wantsOrg = !!(req.body && req.body.account === 'org');
|
|
1245
|
+
const typedOrg = wantsOrg && typeof req.body.org === 'string' ? req.body.org.trim() : '';
|
|
1246
|
+
if (typedOrg && !githubSetup.validGithubLogin(typedOrg)) {
|
|
1247
|
+
return res.fail('bad_org', { status: 400, message: 'That isn’t a GitHub organisation name: letters, numbers and single dashes, up to 39.' });
|
|
1248
|
+
}
|
|
1238
1249
|
try {
|
|
1239
1250
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
1240
1251
|
if (!inst || String(inst.owner_builder_id) !== String(req.builder.id)) {
|
|
@@ -1249,6 +1260,14 @@ module.exports = function provisioningRoutes() {
|
|
|
1249
1260
|
if (!origin) {
|
|
1250
1261
|
return res.fail('mothership_origin_unset', { status: 500, message: 'This control plane has no publicOrigin configured — cannot build the GitHub redirect URL.' });
|
|
1251
1262
|
}
|
|
1263
|
+
// the org candidate: the one the founder chose, else the repo's owner (a login only)
|
|
1264
|
+
const repoOwner = ownerFromTargetRef(inst.target_ref);
|
|
1265
|
+
const orgLogin = typedOrg || (githubSetup.validGithubLogin(repoOwner) ? repoOwner : null);
|
|
1266
|
+
// an explicit organisation choice never falls back to the personal account unseen
|
|
1267
|
+
if (wantsOrg && !orgLogin) {
|
|
1268
|
+
return res.fail('org_required', { status: 400, message: 'Name the GitHub organisation to make the app on.' });
|
|
1269
|
+
}
|
|
1270
|
+
const account = wantsOrg ? 'org' : 'personal';
|
|
1252
1271
|
const state = crypto.randomBytes(32).toString('hex');
|
|
1253
1272
|
const expiresAt = new Date(Date.now() + MANIFEST_STATE_TTL_MS).toISOString();
|
|
1254
1273
|
await provisioning.createOAuthManifest(pool, { instanceId: id, state, requestedBy: req.builder.id, expiresAt });
|
|
@@ -1256,13 +1275,19 @@ module.exports = function provisioningRoutes() {
|
|
|
1256
1275
|
instanceId: id, ownerBuilderId: req.builder.id, event: 'oauth-manifest-start',
|
|
1257
1276
|
detail: 'GitHub App Manifest link minted', actor: 'api:self',
|
|
1258
1277
|
}).catch(() => {});
|
|
1278
|
+
// which account owns this attempt's app, so the page can link its settings there
|
|
1279
|
+
// from any tab or device (github-setup.js reads it back as app_org)
|
|
1280
|
+
if (account === 'org') {
|
|
1281
|
+
await provisioning.recordEvent(pool, {
|
|
1282
|
+
instanceId: id, ownerBuilderId: req.builder.id, event: githubSetup.SETUP_EVENTS.appOrg,
|
|
1283
|
+
detail: orgLogin, actor: 'api:self',
|
|
1284
|
+
}).catch(() => {});
|
|
1285
|
+
}
|
|
1259
1286
|
|
|
1260
1287
|
const manifest = provisioning.buildGithubAppManifest({
|
|
1261
1288
|
instanceName: inst.slug, slug: inst.slug, instanceDomain: inst.domain,
|
|
1262
1289
|
mothershipOrigin: origin, brand: mothershipBrand(),
|
|
1263
1290
|
});
|
|
1264
|
-
const orgLogin = ownerFromTargetRef(inst.target_ref);
|
|
1265
|
-
const account = req.body && req.body.account === 'org' && orgLogin ? 'org' : 'personal';
|
|
1266
1291
|
res.json({
|
|
1267
1292
|
state,
|
|
1268
1293
|
account,
|
|
@@ -459,6 +459,13 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
|
|
|
459
459
|
.hint strong{color:var(--ink);font-weight:500;}
|
|
460
460
|
|
|
461
461
|
.row2{display:grid;grid-template-columns:1fr 1fr;gap:0 22px;}
|
|
462
|
+
/* whose GitHub account (task 1004651): the account, the organisation, and a typed name
|
|
463
|
+
with its Check button sitting on the field's baseline */
|
|
464
|
+
.acctPick{max-width:680px;}
|
|
465
|
+
.acctPick .row2{grid-template-columns:1fr;}
|
|
466
|
+
.acctPick .acctTyped{display:flex;align-items:flex-end;gap:14px;}
|
|
467
|
+
.acctPick .acctTyped .wizField{flex:1;min-width:0;}
|
|
468
|
+
.acctPick .acctTyped .btn{flex:none;margin:0 0 2px;}
|
|
462
469
|
|
|
463
470
|
/* the choice (start fresh / adopt): two cells of one band on hairlines, the
|
|
464
471
|
chosen one carrying the dot — the dot is the mark, so the icon leaves */
|
|
@@ -1810,6 +1817,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1810
1817
|
<!-- visibility is stated on EVERY repo path (task 1002721); updateFreshPreview()
|
|
1811
1818
|
replaces this floor with the same claim plus the resolved repo URL -->
|
|
1812
1819
|
<div class="hint" id="ghFreshHint">A new <strong>private</strong> repo. You can make it public later.</div>
|
|
1820
|
+
<!-- whose account the new repo is made on (task 1004651); the
|
|
1821
|
+
organisation names are GitHub's, so they are built as DOM
|
|
1822
|
+
text, never as markup -->
|
|
1823
|
+
<div class="acctPick" id="raPick">
|
|
1824
|
+
<div class="row2">
|
|
1825
|
+
<label class="wizField"><span>Whose account</span>
|
|
1826
|
+
<select id="raSel"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
|
|
1827
|
+
<label class="wizField" id="raOrgField" hidden><span>Organisation</span><select id="raOrg"></select></label>
|
|
1828
|
+
</div>
|
|
1829
|
+
<div class="row2 acctTyped" id="raTyped" hidden>
|
|
1830
|
+
<label class="wizField"><span>Organisation name</span><input type="text" id="raName" placeholder="acme-studio" autocomplete="off" maxlength="39"></label>
|
|
1831
|
+
<button type="button" class="btn small ghost" id="raCheck">Check</button>
|
|
1832
|
+
</div>
|
|
1833
|
+
<p class="hint" id="raNote" aria-live="polite"></p>
|
|
1834
|
+
</div>
|
|
1813
1835
|
</div>
|
|
1814
1836
|
<div class="row2" id="manualHome">
|
|
1815
1837
|
<label class="wizField"><span>GitHub owner <small>(user or org)</small></span>
|
|
@@ -2104,6 +2126,20 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2104
2126
|
<p class="hint" id="doneRender" role="status" hidden></p>
|
|
2105
2127
|
<p class="hint" id="doneLook" role="status" hidden></p>
|
|
2106
2128
|
<div id="doneApp" hidden></div>
|
|
2129
|
+
<!-- whose account GitHub makes the app on (task 1004651, the mockup's
|
|
2130
|
+
account select beside Connect GitHub); shown while Connect is offered -->
|
|
2131
|
+
<div class="acctPick" id="aaPick" hidden>
|
|
2132
|
+
<div class="row2">
|
|
2133
|
+
<label class="wizField"><span>Make the app on</span>
|
|
2134
|
+
<select id="aaSel" aria-label="Which GitHub account"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
|
|
2135
|
+
<label class="wizField" id="aaOrgField" hidden><span>Organisation</span><select id="aaOrg"></select></label>
|
|
2136
|
+
</div>
|
|
2137
|
+
<div class="row2 acctTyped" id="aaTyped" hidden>
|
|
2138
|
+
<label class="wizField"><span>Organisation name</span><input type="text" id="aaName" placeholder="acme-studio" autocomplete="off" maxlength="39"></label>
|
|
2139
|
+
<button type="button" class="btn small ghost" id="aaCheck">Check</button>
|
|
2140
|
+
</div>
|
|
2141
|
+
<p class="hint" id="aaNote" aria-live="polite"></p>
|
|
2142
|
+
</div>
|
|
2107
2143
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
2108
2144
|
<!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
|
|
2109
2145
|
and a live region that is itself replaced never announces -->
|
|
@@ -5438,13 +5474,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5438
5474
|
Never before identity settles — myLogin()'s 'you' is display copy, and a
|
|
5439
5475
|
draft that stored it would file target_ref "you/<repo>". */
|
|
5440
5476
|
if (!me) return;
|
|
5441
|
-
|
|
5477
|
+
/* the founder's own account, unless they chose an organisation for it (task 1004651) */
|
|
5478
|
+
state.owner = state.repoOrg || myLogin();
|
|
5442
5479
|
$('ghNewName').value = state.repo || '';
|
|
5443
5480
|
updateFreshPreview();
|
|
5481
|
+
if (repoPick) repoPick.load(state.repoAcct === 'org' ? (state.repoOrg || state.repoOrgTyped || '') : null);
|
|
5444
5482
|
}
|
|
5445
5483
|
function updateFreshPreview() {
|
|
5446
5484
|
var hint = $('ghFreshHint');
|
|
5447
|
-
|
|
5485
|
+
/* repoList is the founder's OWN repos, so a clash is only knowable on their account */
|
|
5486
|
+
var clash = !state.repoOrg && state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
|
|
5448
5487
|
if (state.repo && !validRepoName(state.repo)) {
|
|
5449
5488
|
hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
|
|
5450
5489
|
} else if (clash) {
|
|
@@ -5453,11 +5492,177 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5453
5492
|
/* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
|
|
5454
5493
|
private by default (github-repos.js, ADR 0370); the owner can make one
|
|
5455
5494
|
public later from the project's manage page. */
|
|
5456
|
-
hint.className = 'hint'; hint.innerHTML = 'A new <strong>private</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code>. You can make it public later.';
|
|
5495
|
+
hint.className = 'hint'; hint.innerHTML = 'A new <strong>private</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(state.repoOrg || myLogin()) + '/' + esc(state.repo || 'name') + '</code>. You can make it public later.';
|
|
5457
5496
|
}
|
|
5458
5497
|
}
|
|
5459
5498
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
5460
5499
|
|
|
5500
|
+
/* ── whose GitHub account: mine or an organisation's (task 1004651) ──
|
|
5501
|
+
Two pickers share this: the new repo's owner (part 02's Code row) and the app's
|
|
5502
|
+
(part 03). The founder's organisations come from GET /github/orgs; GitHub may not
|
|
5503
|
+
show this sign-in's memberships (it asks for repo access, not org access), so a
|
|
5504
|
+
typed name with a Check button is always offered beside the list, and a role GitHub
|
|
5505
|
+
did not tell us is said as such. For the app, an organisation the founder is known
|
|
5506
|
+
not to own is listed with the reason and cannot be picked: only owners can make
|
|
5507
|
+
apps there. Every organisation name is DOM text (textContent / option text). */
|
|
5508
|
+
var GH_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
5509
|
+
var OTHER_ORG = '__other'; /* a login can never contain "_" */
|
|
5510
|
+
var orgsRead = null;
|
|
5511
|
+
function readOrgs() {
|
|
5512
|
+
if (!orgsRead) {
|
|
5513
|
+
orgsRead = fetch(API + '/github/orgs', { credentials: 'same-origin' })
|
|
5514
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
5515
|
+
.then(function (d) {
|
|
5516
|
+
var orgs = d && Array.isArray(d.orgs) ? d.orgs.filter(function (o) { return o && GH_LOGIN_RE.test(String(o.login || '')); }) : [];
|
|
5517
|
+
return { orgs: orgs, listed: (d && d.listed) || 'none' };
|
|
5518
|
+
})
|
|
5519
|
+
.catch(function () { orgsRead = null; return { orgs: [], listed: 'none' }; });
|
|
5520
|
+
}
|
|
5521
|
+
return orgsRead;
|
|
5522
|
+
}
|
|
5523
|
+
function acctPicker(p, opts) {
|
|
5524
|
+
var el = { box: $(p + 'Pick'), sel: $(p + 'Sel'), orgField: $(p + 'OrgField'), org: $(p + 'Org'), typed: $(p + 'Typed'), name: $(p + 'Name'), check: $(p + 'Check'), note: $(p + 'Note') };
|
|
5525
|
+
var orgs = [], listed = 'none', loaded = false, typed = null, checking = false;
|
|
5526
|
+
function find(login) {
|
|
5527
|
+
var l = String(login || '').toLowerCase();
|
|
5528
|
+
for (var i = 0; i < orgs.length; i++) if (orgs[i].login.toLowerCase() === l) return orgs[i];
|
|
5529
|
+
return null;
|
|
5530
|
+
}
|
|
5531
|
+
/* for the app only: GitHub will not let a known non-owner make it */
|
|
5532
|
+
function refused(o) { return !!(opts.needsOwner && o && o.can_create_apps === false); }
|
|
5533
|
+
function say(text, cls) { el.note.textContent = text || ''; el.note.className = 'hint' + (cls ? ' ' + cls : ''); }
|
|
5534
|
+
function fill(want) {
|
|
5535
|
+
while (el.org.firstChild) el.org.removeChild(el.org.firstChild);
|
|
5536
|
+
var pick = '';
|
|
5537
|
+
orgs.forEach(function (o) {
|
|
5538
|
+
var opt = document.createElement('option');
|
|
5539
|
+
opt.value = o.login;
|
|
5540
|
+
if (refused(o)) {
|
|
5541
|
+
opt.disabled = true;
|
|
5542
|
+
opt.textContent = o.login + (o.role === 'member' ? ' (you’re a member, not an owner)' : ' (not open to you yet)');
|
|
5543
|
+
} else {
|
|
5544
|
+
opt.textContent = o.login;
|
|
5545
|
+
if (!pick) pick = o.login;
|
|
5546
|
+
}
|
|
5547
|
+
el.org.appendChild(opt);
|
|
5548
|
+
});
|
|
5549
|
+
var other = document.createElement('option');
|
|
5550
|
+
other.value = OTHER_ORG;
|
|
5551
|
+
other.textContent = orgs.length ? 'Another organisation…' : 'Type its name…';
|
|
5552
|
+
el.org.appendChild(other);
|
|
5553
|
+
var w = want ? find(want) : null;
|
|
5554
|
+
if (w && !refused(w)) pick = w.login;
|
|
5555
|
+
else if (want) { pick = OTHER_ORG; el.name.value = want; }
|
|
5556
|
+
el.org.value = pick || OTHER_ORG;
|
|
5557
|
+
}
|
|
5558
|
+
/* the choice: personal, or an organisation and whether it can go ahead */
|
|
5559
|
+
function value() {
|
|
5560
|
+
if (el.sel.value !== 'org') return { account: 'personal', ok: true };
|
|
5561
|
+
var v = el.org.value;
|
|
5562
|
+
if (v && v !== OTHER_ORG) { var o = find(v); return { account: 'org', org: o ? o.login : v, ok: !refused(o), access: o }; }
|
|
5563
|
+
var nm = String(el.name.value || '').trim();
|
|
5564
|
+
if (typed && !typed.missing && typed.login.toLowerCase() === nm.toLowerCase()) return { account: 'org', org: typed.login, ok: !refused(typed), access: typed };
|
|
5565
|
+
return { account: 'org', org: '', ok: false, typedName: nm };
|
|
5566
|
+
}
|
|
5567
|
+
function paint() {
|
|
5568
|
+
var isOrg = el.sel.value === 'org';
|
|
5569
|
+
el.orgField.hidden = !isOrg || !loaded || !orgs.length; /* nothing listed: just the typed name */
|
|
5570
|
+
el.typed.hidden = !(isOrg && loaded && el.org.value === OTHER_ORG);
|
|
5571
|
+
var v = value();
|
|
5572
|
+
if (!isOrg) say('');
|
|
5573
|
+
else if (!loaded) say('Reading your organisations from GitHub…');
|
|
5574
|
+
else if (v.org && v.access && v.access.reason && (opts.needsOwner || v.access.can_create_apps === false)) say(v.access.reason, v.access.can_create_apps === false ? 'err' : '');
|
|
5575
|
+
else if (v.org) say(v.access && v.access.can_create_apps ? 'You own ' + v.org + ' on GitHub.' : '');
|
|
5576
|
+
else if (checking) say('Checking with GitHub…');
|
|
5577
|
+
else if (typed && typed.missing && typed.login.toLowerCase() === String(el.name.value || '').trim().toLowerCase()) say(typed.reason, 'err');
|
|
5578
|
+
else say(listed === 'none' && !orgs.length ? 'GitHub didn’t show us your organisations, so type its name and press Check.' : 'Type the organisation’s name and press Check.');
|
|
5579
|
+
if (opts.onChange) opts.onChange(v);
|
|
5580
|
+
}
|
|
5581
|
+
function check() {
|
|
5582
|
+
var nm = String(el.name.value || '').trim();
|
|
5583
|
+
if (!GH_LOGIN_RE.test(nm)) { typed = { login: nm, missing: true, reason: 'That isn’t a GitHub organisation name: letters, numbers and single dashes, up to 39.' }; paint(); return; }
|
|
5584
|
+
checking = true; typed = null; paint();
|
|
5585
|
+
fetch(API + '/github/orgs/' + encodeURIComponent(nm), { credentials: 'same-origin' })
|
|
5586
|
+
.then(function (res) {
|
|
5587
|
+
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
5588
|
+
if (res.ok && d.org && GH_LOGIN_RE.test(String(d.org.login || ''))) return d.org;
|
|
5589
|
+
var msg = (d.error && d.error.message) || '';
|
|
5590
|
+
if (res.status === 404) return { login: nm, missing: true, reason: msg || ('GitHub didn’t show your account an organisation called ' + nm + '.') };
|
|
5591
|
+
return { login: nm, missing: true, reason: 'We couldn’t check that with GitHub just now. Try again in a moment.' };
|
|
5592
|
+
});
|
|
5593
|
+
})
|
|
5594
|
+
.catch(function () { return { login: nm, missing: true, reason: 'We couldn’t check that with GitHub just now. Try again in a moment.' }; })
|
|
5595
|
+
.then(function (r) {
|
|
5596
|
+
checking = false;
|
|
5597
|
+
/* a name typed since the click is not the one this answer is about */
|
|
5598
|
+
if (String(el.name.value || '').trim().toLowerCase() === nm.toLowerCase()) typed = r;
|
|
5599
|
+
paint();
|
|
5600
|
+
});
|
|
5601
|
+
}
|
|
5602
|
+
/* GitHub is asked for the organisations only once one is wanted */
|
|
5603
|
+
function ensureOrgs(want) {
|
|
5604
|
+
if (el.sel.value !== 'org') return;
|
|
5605
|
+
readOrgs().then(function (d) {
|
|
5606
|
+
var first = !loaded;
|
|
5607
|
+
orgs = d.orgs; listed = d.listed; loaded = true;
|
|
5608
|
+
/* a second look keeps what the founder already picked */
|
|
5609
|
+
var keep = first ? want : (value().org || want);
|
|
5610
|
+
fill(keep);
|
|
5611
|
+
if (keep && el.org.value === OTHER_ORG && GH_LOGIN_RE.test(keep)) check();
|
|
5612
|
+
paint();
|
|
5613
|
+
});
|
|
5614
|
+
}
|
|
5615
|
+
el.sel.addEventListener('change', function () { paint(); ensureOrgs(''); });
|
|
5616
|
+
el.org.addEventListener('change', paint);
|
|
5617
|
+
el.name.addEventListener('input', paint);
|
|
5618
|
+
el.name.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); check(); } });
|
|
5619
|
+
el.check.addEventListener('click', check);
|
|
5620
|
+
return {
|
|
5621
|
+
value: value,
|
|
5622
|
+
el: el,
|
|
5623
|
+
/* want: null = the founder's own account; a login = that organisation; '' = an
|
|
5624
|
+
organisation not picked yet */
|
|
5625
|
+
load: function (want) {
|
|
5626
|
+
el.sel.value = want != null ? 'org' : 'personal';
|
|
5627
|
+
paint();
|
|
5628
|
+
ensureOrgs(want);
|
|
5629
|
+
},
|
|
5630
|
+
/* the reason the choice cannot go ahead, said where it is made */
|
|
5631
|
+
explain: function () {
|
|
5632
|
+
if (el.org.value === OTHER_ORG && el.name) el.name.focus(); else el.org.focus();
|
|
5633
|
+
if (!el.note.textContent) say('Pick the organisation, or choose your own account.', 'err');
|
|
5634
|
+
}
|
|
5635
|
+
};
|
|
5636
|
+
}
|
|
5637
|
+
var repoPick = acctPicker('ra', {
|
|
5638
|
+
needsOwner: false,
|
|
5639
|
+
onChange: function (v) {
|
|
5640
|
+
if (state.repoAutoCreated) return; /* GitHub already made it: the owner is a fact now */
|
|
5641
|
+
state.repoAcct = v.account;
|
|
5642
|
+
state.repoOrg = v.account === 'org' && v.ok ? v.org : '';
|
|
5643
|
+
state.repoOrgTyped = v.account === 'org' && !v.org ? (v.typedName || '') : '';
|
|
5644
|
+
if (me && repoAccess === true && state.mode !== 'adopt') state.owner = state.repoOrg || myLogin();
|
|
5645
|
+
updateFreshPreview();
|
|
5646
|
+
saveDraft();
|
|
5647
|
+
if (typeof paintSetup === 'function') paintSetup();
|
|
5648
|
+
}
|
|
5649
|
+
});
|
|
5650
|
+
/* part 03's account picker (task 1004651). Its default is the organisation the app
|
|
5651
|
+
last went to, else the one the new repo went to, else the founder's own account —
|
|
5652
|
+
set once per project, so a poll never undoes a choice. */
|
|
5653
|
+
var appPick = acctPicker('aa', { needsOwner: true });
|
|
5654
|
+
var appPickFor = null;
|
|
5655
|
+
function syncAppPick(offered) {
|
|
5656
|
+
var box = $('aaPick');
|
|
5657
|
+
if (!box || !appPick) return;
|
|
5658
|
+
box.hidden = !offered;
|
|
5659
|
+
if (!offered || appPickFor === instanceId) return;
|
|
5660
|
+
appPickFor = instanceId;
|
|
5661
|
+
var rec = createdRecord() || {};
|
|
5662
|
+
var want = appOrgLogin() || (state.repoAutoCreated && state.repoAcct === 'org' && state.repoOrg) || rec.repo_org || null;
|
|
5663
|
+
appPick.load(want && /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(want) ? want : null);
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5461
5666
|
/* The adopt picker behaves like a standard combobox (owner design review, task
|
|
5462
5667
|
1002247): the dropdown is open only while the visitor is choosing. Selecting a
|
|
5463
5668
|
repo fills the search box with its full_name and collapses the list; focusing or
|
|
@@ -6019,8 +6224,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6019
6224
|
if (works.indexOf(a.main_work) === -1) return homeErr('Mark which of them is your main one.');
|
|
6020
6225
|
}
|
|
6021
6226
|
if (repoAccess === true && state.mode !== 'adopt') {
|
|
6022
|
-
/* Fresh with access: just the new repo NAME (owner is the signed-in account
|
|
6227
|
+
/* Fresh with access: just the new repo NAME (owner is the signed-in account, or
|
|
6228
|
+
the organisation the founder picked, task 1004651). */
|
|
6023
6229
|
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
6230
|
+
if (state.repoAcct === 'org' && !state.repoOrg) return homeErr('Pick the organisation for the repo, or put it on your own account.');
|
|
6024
6231
|
} else {
|
|
6025
6232
|
/* Adopt (picker or manual) and the manual fallback: need a concrete owner + repo. */
|
|
6026
6233
|
if (!state.owner || !state.repo) {
|
|
@@ -6430,10 +6637,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6430
6637
|
function ensureFreshRepo() {
|
|
6431
6638
|
if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
|
|
6432
6639
|
if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
|
|
6640
|
+
/* the founder's own account sends exactly what it always did; an organisation
|
|
6641
|
+
rides as `org`, and the server checks the name again (task 1004651) */
|
|
6642
|
+
var repoBody = state.repoAcct === 'org' && state.repoOrg ? { name: state.repo, org: state.repoOrg } : { name: state.repo };
|
|
6433
6643
|
return fetch(API + '/github/repos', {
|
|
6434
6644
|
method: 'POST', credentials: 'same-origin',
|
|
6435
6645
|
headers: { 'Content-Type': 'application/json' },
|
|
6436
|
-
body: JSON.stringify(
|
|
6646
|
+
body: JSON.stringify(repoBody)
|
|
6437
6647
|
}).then(function (res) {
|
|
6438
6648
|
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
6439
6649
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -6449,6 +6659,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6449
6659
|
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
6450
6660
|
e.data = data;
|
|
6451
6661
|
if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
6662
|
+
/* the organisation said no: its reason, where the owner is chosen */
|
|
6663
|
+
if (data.error && /^(org_app_restricted|org_forbidden|org_not_found|bad_org)$/.test(data.error.code || '')) { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
6452
6664
|
throw e;
|
|
6453
6665
|
}
|
|
6454
6666
|
var full = data.repo && data.repo.full_name;
|
|
@@ -6554,6 +6766,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6554
6766
|
id: data.instance && data.instance.id, slug: state.slug, domain: madeDomain,
|
|
6555
6767
|
noaddr: !madeDomain,
|
|
6556
6768
|
mode: state.mode, name: state.name, at: Date.now(),
|
|
6769
|
+
/* the organisation the new repo went to, so part 03 offers it for the app */
|
|
6770
|
+
repo_org: state.repoAutoCreated && state.repoAcct === 'org' && state.repoOrg ? state.repoOrg : undefined,
|
|
6557
6771
|
/* a demo's teammates, offered once the hall is up (task 1004503) */
|
|
6558
6772
|
demo_invites: state.demo && state.demo.invites && state.demo.invites.length ? state.demo.invites.slice() : undefined
|
|
6559
6773
|
}));
|
|
@@ -6608,7 +6822,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6608
6822
|
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
6609
6823
|
'">' + label + '</a>';
|
|
6610
6824
|
}
|
|
6611
|
-
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
|
|
6825
|
+
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null };
|
|
6612
6826
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
6613
6827
|
var lastStepsHtml = '';
|
|
6614
6828
|
var lastProgHtml = ''; /* same change-only contract, for the progress strip */
|
|
@@ -6660,7 +6874,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6660
6874
|
: '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLink + ' on GitHub for you — public (projects here are open by design), empty, and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>';
|
|
6661
6875
|
}
|
|
6662
6876
|
var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
|
|
6663
|
-
return '<li' + (landed ? ' class="sdone"' : '') + ' 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 owner and name already filled in), press <strong>Create repository</strong>, and leave it completely empty. It must land at exactly ' + repoLabel + ' — leave GitHub’s Owner box as prefilled
|
|
6877
|
+
return '<li' + (landed ? ' class="sdone"' : '') + ' 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 owner and name already filled in), press <strong>Create repository</strong>, and leave it completely empty. It must land at exactly ' + repoLabel + ' — leave GitHub’s Owner box as prefilled, because the build watches that exact address; a repo created under a different owner is never found. GitHub will ask public or private — your choice; both work here, and we never change it. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
|
|
6664
6878
|
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
|
|
6665
6879
|
}
|
|
6666
6880
|
|
|
@@ -6713,6 +6927,17 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6713
6927
|
reaches the markup goes through esc(). */
|
|
6714
6928
|
function ghStep(key) { return live.setup && live.setup[key]; }
|
|
6715
6929
|
function appTitle() { return 'Approve the ' + esc(state.name || state.slug || 'project') + ' app on GitHub'; }
|
|
6930
|
+
/* the organisation that owns the app, when the founder made it on one (task 1004651) */
|
|
6931
|
+
function appOrgLogin() {
|
|
6932
|
+
return typeof live.appOrg === 'string' && /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(live.appOrg) ? live.appOrg : null;
|
|
6933
|
+
}
|
|
6934
|
+
/* the app's own settings on GitHub: under its organisation, or under the founder's
|
|
6935
|
+
account. A raw URL; callers esc() it into an attribute. */
|
|
6936
|
+
function appSettingsUrl() {
|
|
6937
|
+
if (!live.appSlug) return null;
|
|
6938
|
+
var org = appOrgLogin();
|
|
6939
|
+
return 'https://github.com' + (org ? '/organizations/' + encodeURIComponent(org) : '') + '/settings/apps/' + encodeURIComponent(live.appSlug);
|
|
6940
|
+
}
|
|
6716
6941
|
|
|
6717
6942
|
/* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
|
|
6718
6943
|
(gap 3 / idea 1000676); locked until a domain exists (the App needs the real
|
|
@@ -6745,12 +6970,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6745
6970
|
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
|
|
6746
6971
|
else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from ' + projectPageLink('its own page') + ', then come back to this step.';
|
|
6747
6972
|
else if (live.manifestFailure === 'sign_in_unconfirmed') {
|
|
6748
|
-
var appLink = live.appSlug ? '<a href="
|
|
6973
|
+
var appLink = live.appSlug ? '<a href="' + esc(appSettingsUrl()) + '" target="_blank" rel="noopener">your app on GitHub</a>' : 'the app on your GitHub account';
|
|
6749
6974
|
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub approved the app and the platform placed its credentials on your project, but your project didn’t confirm that sign-in works. Trying again would make a second app on your GitHub account and is unlikely to fix this — it needs a look from the platform team. <a href="/contact">Contact us</a> and mention ' + appLink + '.</p>' +
|
|
6750
6975
|
progressMarkup(1, 1, 'Stopped: your project didn’t confirm sign-in.', 'isErr', 'signin') +
|
|
6751
6976
|
'<button type="button" class="linklike" data-action="manifest-start">Try again anyway</button></li>';
|
|
6752
6977
|
}
|
|
6753
|
-
else if (live.manifestFailure === 'finish_failed') snag =
|
|
6978
|
+
else if (live.manifestFailure === 'finish_failed') snag = appOrgLogin()
|
|
6979
|
+
? 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on ' + esc(appOrgLogin()) + ' — delete it first (the organisation’s Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.'
|
|
6980
|
+
: 'GitHub approved the app, but the platform couldn’t finish setting up sign-in on your project. The app GitHub made is still on your account — delete it first (GitHub → Settings → Developer settings → GitHub Apps) so you don’t end up with two, then try again.';
|
|
6754
6981
|
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
6755
6982
|
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
|
|
6756
6983
|
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
@@ -6787,7 +7014,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6787
7014
|
if (live.manifest !== 'done') {
|
|
6788
7015
|
return '<li class="slocked soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. It lets builders sign in from a terminal, with the <code style="font-family:var(--font-mono)">bongos setup</code> command they run on their own machine; web sign-in works without it.</p></li>';
|
|
6789
7016
|
}
|
|
6790
|
-
var appUrl = live.appSlug ?
|
|
7017
|
+
var appUrl = live.appSlug ? esc(appSettingsUrl()) : null;
|
|
6791
7018
|
return '<li class="soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. GitHub leaves it off on new apps and lets only you switch it on: ' +
|
|
6792
7019
|
(appUrl
|
|
6793
7020
|
? 'open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
|
|
@@ -6865,6 +7092,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6865
7092
|
var r = STEP_RENDERERS[s.key];
|
|
6866
7093
|
return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
|
|
6867
7094
|
}).join('');
|
|
7095
|
+
/* the account picker stands beside Connect GitHub, so it shows while that is offered */
|
|
7096
|
+
syncAppPick(html.indexOf('data-action="manifest-start"') !== -1);
|
|
6868
7097
|
if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
|
|
6869
7098
|
lastStepsHtml = html;
|
|
6870
7099
|
$('humanSteps').innerHTML = html;
|
|
@@ -7007,13 +7236,26 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7007
7236
|
var manifestGen = 0;
|
|
7008
7237
|
function stopManifestPoll() { manifestGen += 1; if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
|
|
7009
7238
|
|
|
7239
|
+
/* the organisation the server says this attempt's app is on: change-only */
|
|
7240
|
+
function noteAppOrg(org) {
|
|
7241
|
+
var next = typeof org === 'string' && org ? org : null;
|
|
7242
|
+
if (next === live.appOrg) return false;
|
|
7243
|
+
live.appOrg = next;
|
|
7244
|
+
return true;
|
|
7245
|
+
}
|
|
7246
|
+
|
|
7010
7247
|
function startManifest(btn) {
|
|
7011
7248
|
if (!instanceId) return;
|
|
7249
|
+
/* an organisation that cannot go ahead says why where it is chosen, before any
|
|
7250
|
+
tab opens or any link is minted */
|
|
7251
|
+
var acct = appPick ? appPick.value() : { account: 'personal', ok: true };
|
|
7252
|
+
if (!acct.ok) { appPick.explain(); return; }
|
|
7012
7253
|
btn.disabled = true; btn.textContent = 'Preparing GitHub…';
|
|
7013
7254
|
var tab = window.open('about:blank', 'ghAppSetup');
|
|
7014
7255
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
|
|
7015
7256
|
method: 'POST', credentials: 'same-origin',
|
|
7016
|
-
|
|
7257
|
+
/* the founder's own account posts exactly what it always did */
|
|
7258
|
+
headers: { 'Content-Type': 'application/json' }, body: acct.account === 'org' ? JSON.stringify({ account: 'org', org: acct.org }) : '{}'
|
|
7017
7259
|
}).then(function (res) {
|
|
7018
7260
|
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1'); throw new Error('signing in'); }
|
|
7019
7261
|
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
@@ -7032,6 +7274,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7032
7274
|
form.appendChild(inp); document.body.appendChild(form);
|
|
7033
7275
|
form.submit(); form.remove();
|
|
7034
7276
|
live.manifest = 'pending'; live.manifestMsg = ''; live.manifestFailure = null;
|
|
7277
|
+
noteAppOrg(d.account === 'org' ? d.org_login : null);
|
|
7035
7278
|
manifestStartedAt = Date.now(); manifestStale = false;
|
|
7036
7279
|
rememberManifest(d.state, 'pending');
|
|
7037
7280
|
renderSteps();
|
|
@@ -7083,7 +7326,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7083
7326
|
.then(function (d) {
|
|
7084
7327
|
if (myGen !== manifestGen) return;
|
|
7085
7328
|
if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
|
|
7086
|
-
|
|
7329
|
+
/* the server-verified ticks (task 1004649), and whose account the app is on */
|
|
7330
|
+
var setupMoved = seedSetup(d.checklist, d.install_url);
|
|
7331
|
+
if (noteAppOrg(d.app_org)) setupMoved = true;
|
|
7332
|
+
if (setupMoved) renderSteps();
|
|
7087
7333
|
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
7088
7334
|
var failure = d.failure || null;
|
|
7089
7335
|
if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
|
|
@@ -7329,7 +7575,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7329
7575
|
else noteManifestWait();
|
|
7330
7576
|
}
|
|
7331
7577
|
/* the server-verified checklist rides the same read (task 1004649) */
|
|
7332
|
-
if (data.github_setup && !manifestPollTimer
|
|
7578
|
+
if (data.github_setup && !manifestPollTimer) {
|
|
7579
|
+
var seedMoved = seedSetup(data.github_setup.checklist, data.github_setup.install_url);
|
|
7580
|
+
if (noteAppOrg(data.github_setup.app_org)) seedMoved = true;
|
|
7581
|
+
if (seedMoved) renderSteps();
|
|
7582
|
+
}
|
|
7333
7583
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
7334
7584
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
7335
7585
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
@@ -7369,7 +7619,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7369
7619
|
instanceId = null;
|
|
7370
7620
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
7371
7621
|
manifestStartedAt = 0; manifestStale = false;
|
|
7372
|
-
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
|
|
7622
|
+
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null };
|
|
7373
7623
|
lastStepsHtml = '';
|
|
7374
7624
|
lastProgHtml = '';
|
|
7375
7625
|
hopReset();
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.96",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.96",
|
|
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.21.
|
|
3
|
+
"version": "1.21.96",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9400,5 +9400,11 @@
|
|
|
9400
9400
|
"id": "1004650",
|
|
9401
9401
|
"text": "Connecting GitHub no longer depends on staying in one browser tab. After you approve on GitHub, even on your phone or in another window, you land back on your own project's GitHub checklist. Our server remembers which project"
|
|
9402
9402
|
}
|
|
9403
|
+
],
|
|
9404
|
+
"1.21.96": [
|
|
9405
|
+
{
|
|
9406
|
+
"id": "1004651",
|
|
9407
|
+
"text": "Founders can now put a new project under a GitHub organisation, not only their own account. When connecting GitHub, they choose \"On my account\" or \"On an organisation\". Their organisations are listed; one they only belong to ("
|
|
9408
|
+
}
|
|
9403
9409
|
]
|
|
9404
9410
|
}
|