@bongos/core 1.21.95 → 1.21.97
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 +45 -35
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +10 -3
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/api/openapi.json +156 -7
- package/docs/api-reference.md +8 -5
- package/docs/copy-inventory.md +367 -358
- package/docs/copy-registry.json +536 -425
- package/docs/design/project-startup-direction.md +22 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +351 -348
- package/modules/provisioning/github-setup.js +98 -7
- package/modules/provisioning/routes/github-setup.js +44 -1
- package/modules/provisioning/routes/provisioning.js +37 -8
- package/modules/public-landing/public/projects.html +441 -36
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-github-setup.js +29 -9
- package/scripts/gds/provision.js +20 -9
- 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_one_trip.mjs +4 -4
- package/tests/github_org_account.mjs +379 -0
- package/tests/github_pick_up.mjs +366 -0
- package/tests/projects_hub_pre_uat.mjs +3 -2
- package/tests/provisioning_github_app_routes.mjs +73 -0
- package/tests/wizard_github_feedback.mjs +95 -29
|
@@ -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 */
|
|
@@ -601,6 +608,10 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
|
|
|
601
608
|
display:inline-flex;align-items:center;justify-content:center;font-family:var(--font-body);font-size:13px;letter-spacing:0;}
|
|
602
609
|
#panel-done .humanSteps>li.sdone::before{content:"✓";text-transform:none;color:var(--ok);background:color-mix(in srgb,var(--ok) 12%,transparent);border:1px solid var(--ok);}
|
|
603
610
|
#panel-done .humanSteps>li.slocked::before{border:1px solid var(--rule-soft);}
|
|
611
|
+
/* a stopped step (task 1004652, the mockup's "!" dot): what stopped, and the button that fixes it */
|
|
612
|
+
#panel-done .humanSteps>li.sstopped::before{content:"!";color:var(--err);background:color-mix(in srgb,var(--err) 10%,transparent);border:1px solid var(--err);}
|
|
613
|
+
.humanSteps .linklike{display:block;margin-top:10px;min-height:24px;padding:3px 0;background:none;border:0;cursor:pointer;font-family:var(--font-body);font-size:13.5px;color:var(--link-ink);text-decoration:underline;text-underline-offset:3px;text-align:left;}
|
|
614
|
+
.humanSteps .linklike:hover{color:var(--ink);}
|
|
604
615
|
/* the optional step (command-line sign-in, task 1004649): a dashed ring until it ticks */
|
|
605
616
|
#panel-done .humanSteps>li.soptional:not(.sdone)::before{border-style:dashed;}
|
|
606
617
|
#panel-done .humanSteps b{font-size:15px;line-height:24px;}
|
|
@@ -1810,6 +1821,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1810
1821
|
<!-- visibility is stated on EVERY repo path (task 1002721); updateFreshPreview()
|
|
1811
1822
|
replaces this floor with the same claim plus the resolved repo URL -->
|
|
1812
1823
|
<div class="hint" id="ghFreshHint">A new <strong>private</strong> repo. You can make it public later.</div>
|
|
1824
|
+
<!-- whose account the new repo is made on (task 1004651); the
|
|
1825
|
+
organisation names are GitHub's, so they are built as DOM
|
|
1826
|
+
text, never as markup -->
|
|
1827
|
+
<div class="acctPick" id="raPick">
|
|
1828
|
+
<div class="row2">
|
|
1829
|
+
<label class="wizField"><span>Whose account</span>
|
|
1830
|
+
<select id="raSel"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
|
|
1831
|
+
<label class="wizField" id="raOrgField" hidden><span>Organisation</span><select id="raOrg"></select></label>
|
|
1832
|
+
</div>
|
|
1833
|
+
<div class="row2 acctTyped" id="raTyped" hidden>
|
|
1834
|
+
<label class="wizField"><span>Organisation name</span><input type="text" id="raName" placeholder="acme-studio" autocomplete="off" maxlength="39"></label>
|
|
1835
|
+
<button type="button" class="btn small ghost" id="raCheck">Check</button>
|
|
1836
|
+
</div>
|
|
1837
|
+
<p class="hint" id="raNote" aria-live="polite"></p>
|
|
1838
|
+
</div>
|
|
1813
1839
|
</div>
|
|
1814
1840
|
<div class="row2" id="manualHome">
|
|
1815
1841
|
<label class="wizField"><span>GitHub owner <small>(user or org)</small></span>
|
|
@@ -2104,6 +2130,20 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
2104
2130
|
<p class="hint" id="doneRender" role="status" hidden></p>
|
|
2105
2131
|
<p class="hint" id="doneLook" role="status" hidden></p>
|
|
2106
2132
|
<div id="doneApp" hidden></div>
|
|
2133
|
+
<!-- whose account GitHub makes the app on (task 1004651, the mockup's
|
|
2134
|
+
account select beside Connect GitHub); shown while Connect is offered -->
|
|
2135
|
+
<div class="acctPick" id="aaPick" hidden>
|
|
2136
|
+
<div class="row2">
|
|
2137
|
+
<label class="wizField"><span>Make the app on</span>
|
|
2138
|
+
<select id="aaSel" aria-label="Which GitHub account"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
|
|
2139
|
+
<label class="wizField" id="aaOrgField" hidden><span>Organisation</span><select id="aaOrg"></select></label>
|
|
2140
|
+
</div>
|
|
2141
|
+
<div class="row2 acctTyped" id="aaTyped" hidden>
|
|
2142
|
+
<label class="wizField"><span>Organisation name</span><input type="text" id="aaName" placeholder="acme-studio" autocomplete="off" maxlength="39"></label>
|
|
2143
|
+
<button type="button" class="btn small ghost" id="aaCheck">Check</button>
|
|
2144
|
+
</div>
|
|
2145
|
+
<p class="hint" id="aaNote" aria-live="polite"></p>
|
|
2146
|
+
</div>
|
|
2107
2147
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
2108
2148
|
<!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
|
|
2109
2149
|
and a live region that is itself replaced never announces -->
|
|
@@ -5438,13 +5478,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5438
5478
|
Never before identity settles — myLogin()'s 'you' is display copy, and a
|
|
5439
5479
|
draft that stored it would file target_ref "you/<repo>". */
|
|
5440
5480
|
if (!me) return;
|
|
5441
|
-
|
|
5481
|
+
/* the founder's own account, unless they chose an organisation for it (task 1004651) */
|
|
5482
|
+
state.owner = state.repoOrg || myLogin();
|
|
5442
5483
|
$('ghNewName').value = state.repo || '';
|
|
5443
5484
|
updateFreshPreview();
|
|
5485
|
+
if (repoPick) repoPick.load(state.repoAcct === 'org' ? (state.repoOrg || state.repoOrgTyped || '') : null);
|
|
5444
5486
|
}
|
|
5445
5487
|
function updateFreshPreview() {
|
|
5446
5488
|
var hint = $('ghFreshHint');
|
|
5447
|
-
|
|
5489
|
+
/* repoList is the founder's OWN repos, so a clash is only knowable on their account */
|
|
5490
|
+
var clash = !state.repoOrg && state.repo && repoList.some(function (r) { return String(r.name).toLowerCase() === state.repo.toLowerCase(); });
|
|
5448
5491
|
if (state.repo && !validRepoName(state.repo)) {
|
|
5449
5492
|
hint.className = 'hint err'; hint.textContent = 'Use letters, numbers, and . _ - (1–100 chars).';
|
|
5450
5493
|
} else if (clash) {
|
|
@@ -5453,11 +5496,177 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5453
5496
|
/* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
|
|
5454
5497
|
private by default (github-repos.js, ADR 0370); the owner can make one
|
|
5455
5498
|
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.';
|
|
5499
|
+
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
5500
|
}
|
|
5458
5501
|
}
|
|
5459
5502
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
5460
5503
|
|
|
5504
|
+
/* ── whose GitHub account: mine or an organisation's (task 1004651) ──
|
|
5505
|
+
Two pickers share this: the new repo's owner (part 02's Code row) and the app's
|
|
5506
|
+
(part 03). The founder's organisations come from GET /github/orgs; GitHub may not
|
|
5507
|
+
show this sign-in's memberships (it asks for repo access, not org access), so a
|
|
5508
|
+
typed name with a Check button is always offered beside the list, and a role GitHub
|
|
5509
|
+
did not tell us is said as such. For the app, an organisation the founder is known
|
|
5510
|
+
not to own is listed with the reason and cannot be picked: only owners can make
|
|
5511
|
+
apps there. Every organisation name is DOM text (textContent / option text). */
|
|
5512
|
+
var GH_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
5513
|
+
var OTHER_ORG = '__other'; /* a login can never contain "_" */
|
|
5514
|
+
var orgsRead = null;
|
|
5515
|
+
function readOrgs() {
|
|
5516
|
+
if (!orgsRead) {
|
|
5517
|
+
orgsRead = fetch(API + '/github/orgs', { credentials: 'same-origin' })
|
|
5518
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
5519
|
+
.then(function (d) {
|
|
5520
|
+
var orgs = d && Array.isArray(d.orgs) ? d.orgs.filter(function (o) { return o && GH_LOGIN_RE.test(String(o.login || '')); }) : [];
|
|
5521
|
+
return { orgs: orgs, listed: (d && d.listed) || 'none' };
|
|
5522
|
+
})
|
|
5523
|
+
.catch(function () { orgsRead = null; return { orgs: [], listed: 'none' }; });
|
|
5524
|
+
}
|
|
5525
|
+
return orgsRead;
|
|
5526
|
+
}
|
|
5527
|
+
function acctPicker(p, opts) {
|
|
5528
|
+
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') };
|
|
5529
|
+
var orgs = [], listed = 'none', loaded = false, typed = null, checking = false;
|
|
5530
|
+
function find(login) {
|
|
5531
|
+
var l = String(login || '').toLowerCase();
|
|
5532
|
+
for (var i = 0; i < orgs.length; i++) if (orgs[i].login.toLowerCase() === l) return orgs[i];
|
|
5533
|
+
return null;
|
|
5534
|
+
}
|
|
5535
|
+
/* for the app only: GitHub will not let a known non-owner make it */
|
|
5536
|
+
function refused(o) { return !!(opts.needsOwner && o && o.can_create_apps === false); }
|
|
5537
|
+
function say(text, cls) { el.note.textContent = text || ''; el.note.className = 'hint' + (cls ? ' ' + cls : ''); }
|
|
5538
|
+
function fill(want) {
|
|
5539
|
+
while (el.org.firstChild) el.org.removeChild(el.org.firstChild);
|
|
5540
|
+
var pick = '';
|
|
5541
|
+
orgs.forEach(function (o) {
|
|
5542
|
+
var opt = document.createElement('option');
|
|
5543
|
+
opt.value = o.login;
|
|
5544
|
+
if (refused(o)) {
|
|
5545
|
+
opt.disabled = true;
|
|
5546
|
+
opt.textContent = o.login + (o.role === 'member' ? ' (you’re a member, not an owner)' : ' (not open to you yet)');
|
|
5547
|
+
} else {
|
|
5548
|
+
opt.textContent = o.login;
|
|
5549
|
+
if (!pick) pick = o.login;
|
|
5550
|
+
}
|
|
5551
|
+
el.org.appendChild(opt);
|
|
5552
|
+
});
|
|
5553
|
+
var other = document.createElement('option');
|
|
5554
|
+
other.value = OTHER_ORG;
|
|
5555
|
+
other.textContent = orgs.length ? 'Another organisation…' : 'Type its name…';
|
|
5556
|
+
el.org.appendChild(other);
|
|
5557
|
+
var w = want ? find(want) : null;
|
|
5558
|
+
if (w && !refused(w)) pick = w.login;
|
|
5559
|
+
else if (want) { pick = OTHER_ORG; el.name.value = want; }
|
|
5560
|
+
el.org.value = pick || OTHER_ORG;
|
|
5561
|
+
}
|
|
5562
|
+
/* the choice: personal, or an organisation and whether it can go ahead */
|
|
5563
|
+
function value() {
|
|
5564
|
+
if (el.sel.value !== 'org') return { account: 'personal', ok: true };
|
|
5565
|
+
var v = el.org.value;
|
|
5566
|
+
if (v && v !== OTHER_ORG) { var o = find(v); return { account: 'org', org: o ? o.login : v, ok: !refused(o), access: o }; }
|
|
5567
|
+
var nm = String(el.name.value || '').trim();
|
|
5568
|
+
if (typed && !typed.missing && typed.login.toLowerCase() === nm.toLowerCase()) return { account: 'org', org: typed.login, ok: !refused(typed), access: typed };
|
|
5569
|
+
return { account: 'org', org: '', ok: false, typedName: nm };
|
|
5570
|
+
}
|
|
5571
|
+
function paint() {
|
|
5572
|
+
var isOrg = el.sel.value === 'org';
|
|
5573
|
+
el.orgField.hidden = !isOrg || !loaded || !orgs.length; /* nothing listed: just the typed name */
|
|
5574
|
+
el.typed.hidden = !(isOrg && loaded && el.org.value === OTHER_ORG);
|
|
5575
|
+
var v = value();
|
|
5576
|
+
if (!isOrg) say('');
|
|
5577
|
+
else if (!loaded) say('Reading your organisations from GitHub…');
|
|
5578
|
+
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' : '');
|
|
5579
|
+
else if (v.org) say(v.access && v.access.can_create_apps ? 'You own ' + v.org + ' on GitHub.' : '');
|
|
5580
|
+
else if (checking) say('Checking with GitHub…');
|
|
5581
|
+
else if (typed && typed.missing && typed.login.toLowerCase() === String(el.name.value || '').trim().toLowerCase()) say(typed.reason, 'err');
|
|
5582
|
+
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.');
|
|
5583
|
+
if (opts.onChange) opts.onChange(v);
|
|
5584
|
+
}
|
|
5585
|
+
function check() {
|
|
5586
|
+
var nm = String(el.name.value || '').trim();
|
|
5587
|
+
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; }
|
|
5588
|
+
checking = true; typed = null; paint();
|
|
5589
|
+
fetch(API + '/github/orgs/' + encodeURIComponent(nm), { credentials: 'same-origin' })
|
|
5590
|
+
.then(function (res) {
|
|
5591
|
+
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
5592
|
+
if (res.ok && d.org && GH_LOGIN_RE.test(String(d.org.login || ''))) return d.org;
|
|
5593
|
+
var msg = (d.error && d.error.message) || '';
|
|
5594
|
+
if (res.status === 404) return { login: nm, missing: true, reason: msg || ('GitHub didn’t show your account an organisation called ' + nm + '.') };
|
|
5595
|
+
return { login: nm, missing: true, reason: 'We couldn’t check that with GitHub just now. Try again in a moment.' };
|
|
5596
|
+
});
|
|
5597
|
+
})
|
|
5598
|
+
.catch(function () { return { login: nm, missing: true, reason: 'We couldn’t check that with GitHub just now. Try again in a moment.' }; })
|
|
5599
|
+
.then(function (r) {
|
|
5600
|
+
checking = false;
|
|
5601
|
+
/* a name typed since the click is not the one this answer is about */
|
|
5602
|
+
if (String(el.name.value || '').trim().toLowerCase() === nm.toLowerCase()) typed = r;
|
|
5603
|
+
paint();
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
5606
|
+
/* GitHub is asked for the organisations only once one is wanted */
|
|
5607
|
+
function ensureOrgs(want) {
|
|
5608
|
+
if (el.sel.value !== 'org') return;
|
|
5609
|
+
readOrgs().then(function (d) {
|
|
5610
|
+
var first = !loaded;
|
|
5611
|
+
orgs = d.orgs; listed = d.listed; loaded = true;
|
|
5612
|
+
/* a second look keeps what the founder already picked */
|
|
5613
|
+
var keep = first ? want : (value().org || want);
|
|
5614
|
+
fill(keep);
|
|
5615
|
+
if (keep && el.org.value === OTHER_ORG && GH_LOGIN_RE.test(keep)) check();
|
|
5616
|
+
paint();
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
el.sel.addEventListener('change', function () { paint(); ensureOrgs(''); });
|
|
5620
|
+
el.org.addEventListener('change', paint);
|
|
5621
|
+
el.name.addEventListener('input', paint);
|
|
5622
|
+
el.name.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); check(); } });
|
|
5623
|
+
el.check.addEventListener('click', check);
|
|
5624
|
+
return {
|
|
5625
|
+
value: value,
|
|
5626
|
+
el: el,
|
|
5627
|
+
/* want: null = the founder's own account; a login = that organisation; '' = an
|
|
5628
|
+
organisation not picked yet */
|
|
5629
|
+
load: function (want) {
|
|
5630
|
+
el.sel.value = want != null ? 'org' : 'personal';
|
|
5631
|
+
paint();
|
|
5632
|
+
ensureOrgs(want);
|
|
5633
|
+
},
|
|
5634
|
+
/* the reason the choice cannot go ahead, said where it is made */
|
|
5635
|
+
explain: function () {
|
|
5636
|
+
if (el.org.value === OTHER_ORG && el.name) el.name.focus(); else el.org.focus();
|
|
5637
|
+
if (!el.note.textContent) say('Pick the organisation, or choose your own account.', 'err');
|
|
5638
|
+
}
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
var repoPick = acctPicker('ra', {
|
|
5642
|
+
needsOwner: false,
|
|
5643
|
+
onChange: function (v) {
|
|
5644
|
+
if (state.repoAutoCreated) return; /* GitHub already made it: the owner is a fact now */
|
|
5645
|
+
state.repoAcct = v.account;
|
|
5646
|
+
state.repoOrg = v.account === 'org' && v.ok ? v.org : '';
|
|
5647
|
+
state.repoOrgTyped = v.account === 'org' && !v.org ? (v.typedName || '') : '';
|
|
5648
|
+
if (me && repoAccess === true && state.mode !== 'adopt') state.owner = state.repoOrg || myLogin();
|
|
5649
|
+
updateFreshPreview();
|
|
5650
|
+
saveDraft();
|
|
5651
|
+
if (typeof paintSetup === 'function') paintSetup();
|
|
5652
|
+
}
|
|
5653
|
+
});
|
|
5654
|
+
/* part 03's account picker (task 1004651). Its default is the organisation the app
|
|
5655
|
+
last went to, else the one the new repo went to, else the founder's own account —
|
|
5656
|
+
set once per project, so a poll never undoes a choice. */
|
|
5657
|
+
var appPick = acctPicker('aa', { needsOwner: true });
|
|
5658
|
+
var appPickFor = null;
|
|
5659
|
+
function syncAppPick(offered) {
|
|
5660
|
+
var box = $('aaPick');
|
|
5661
|
+
if (!box || !appPick) return;
|
|
5662
|
+
box.hidden = !offered;
|
|
5663
|
+
if (!offered || appPickFor === instanceId) return;
|
|
5664
|
+
appPickFor = instanceId;
|
|
5665
|
+
var rec = createdRecord() || {};
|
|
5666
|
+
var want = appOrgLogin() || (state.repoAutoCreated && state.repoAcct === 'org' && state.repoOrg) || rec.repo_org || null;
|
|
5667
|
+
appPick.load(want && /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(want) ? want : null);
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5461
5670
|
/* The adopt picker behaves like a standard combobox (owner design review, task
|
|
5462
5671
|
1002247): the dropdown is open only while the visitor is choosing. Selecting a
|
|
5463
5672
|
repo fills the search box with its full_name and collapses the list; focusing or
|
|
@@ -6019,8 +6228,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6019
6228
|
if (works.indexOf(a.main_work) === -1) return homeErr('Mark which of them is your main one.');
|
|
6020
6229
|
}
|
|
6021
6230
|
if (repoAccess === true && state.mode !== 'adopt') {
|
|
6022
|
-
/* Fresh with access: just the new repo NAME (owner is the signed-in account
|
|
6231
|
+
/* Fresh with access: just the new repo NAME (owner is the signed-in account, or
|
|
6232
|
+
the organisation the founder picked, task 1004651). */
|
|
6023
6233
|
if (!validRepoName(state.repo)) return homeErr('Name the new repository (letters, numbers, and . _ -).');
|
|
6234
|
+
if (state.repoAcct === 'org' && !state.repoOrg) return homeErr('Pick the organisation for the repo, or put it on your own account.');
|
|
6024
6235
|
} else {
|
|
6025
6236
|
/* Adopt (picker or manual) and the manual fallback: need a concrete owner + repo. */
|
|
6026
6237
|
if (!state.owner || !state.repo) {
|
|
@@ -6430,10 +6641,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6430
6641
|
function ensureFreshRepo() {
|
|
6431
6642
|
if (state.mode === 'adopt' || repoAccess !== true) return Promise.resolve();
|
|
6432
6643
|
if (state.repoAutoCreated && state.owner && state.repo) return Promise.resolve();
|
|
6644
|
+
/* the founder's own account sends exactly what it always did; an organisation
|
|
6645
|
+
rides as `org`, and the server checks the name again (task 1004651) */
|
|
6646
|
+
var repoBody = state.repoAcct === 'org' && state.repoOrg ? { name: state.repo, org: state.repoOrg } : { name: state.repo };
|
|
6433
6647
|
return fetch(API + '/github/repos', {
|
|
6434
6648
|
method: 'POST', credentials: 'same-origin',
|
|
6435
6649
|
headers: { 'Content-Type': 'application/json' },
|
|
6436
|
-
body: JSON.stringify(
|
|
6650
|
+
body: JSON.stringify(repoBody)
|
|
6437
6651
|
}).then(function (res) {
|
|
6438
6652
|
return res.json().catch(function () { return {}; }).then(function (data) {
|
|
6439
6653
|
if (res.status === 401 || res.status === 403) {
|
|
@@ -6449,6 +6663,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6449
6663
|
var e = new Error((data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status));
|
|
6450
6664
|
e.data = data;
|
|
6451
6665
|
if (data.error && data.error.code === 'name_taken') { e.gotoHome = true; e.homeMsg = data.error.message; }
|
|
6666
|
+
/* the organisation said no: its reason, where the owner is chosen */
|
|
6667
|
+
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
6668
|
throw e;
|
|
6453
6669
|
}
|
|
6454
6670
|
var full = data.repo && data.repo.full_name;
|
|
@@ -6554,6 +6770,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6554
6770
|
id: data.instance && data.instance.id, slug: state.slug, domain: madeDomain,
|
|
6555
6771
|
noaddr: !madeDomain,
|
|
6556
6772
|
mode: state.mode, name: state.name, at: Date.now(),
|
|
6773
|
+
/* the organisation the new repo went to, so part 03 offers it for the app */
|
|
6774
|
+
repo_org: state.repoAutoCreated && state.repoAcct === 'org' && state.repoOrg ? state.repoOrg : undefined,
|
|
6557
6775
|
/* a demo's teammates, offered once the hall is up (task 1004503) */
|
|
6558
6776
|
demo_invites: state.demo && state.demo.invites && state.demo.invites.length ? state.demo.invites.slice() : undefined
|
|
6559
6777
|
}));
|
|
@@ -6608,7 +6826,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6608
6826
|
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
6609
6827
|
'">' + label + '</a>';
|
|
6610
6828
|
}
|
|
6611
|
-
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
|
|
6829
|
+
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false };
|
|
6612
6830
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
6613
6831
|
var lastStepsHtml = '';
|
|
6614
6832
|
var lastProgHtml = ''; /* same change-only contract, for the progress strip */
|
|
@@ -6660,7 +6878,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6660
6878
|
: '<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
6879
|
}
|
|
6662
6880
|
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
|
|
6881
|
+
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
6882
|
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
|
|
6665
6883
|
}
|
|
6666
6884
|
|
|
@@ -6712,7 +6930,30 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6712
6930
|
the server verified (live.setup, the status poll's `checklist`). Every value that
|
|
6713
6931
|
reaches the markup goes through esc(). */
|
|
6714
6932
|
function ghStep(key) { return live.setup && live.setup[key]; }
|
|
6933
|
+
/* "Pick up where it stopped" (task 1004652): a stopped step says what stopped in one plain
|
|
6934
|
+
sentence and carries the action that fixes it, with the app GitHub already made, never a
|
|
6935
|
+
second one. live.pickUp is the server's word for what that action does (github-setup.js
|
|
6936
|
+
pickUpFor); live.verifying is whether the runner is still checking. Static words only. */
|
|
6937
|
+
var ghTabTrouble = null; /* 'behind' | 'closed': what this page saw of the GitHub tab it opened */
|
|
6938
|
+
var NO_SECOND_APP = 'Nothing is lost, and no second app will be made.';
|
|
6939
|
+
function pickUpBtn() { return '<button type="button" class="ghbtn" data-action="pick-up">Pick up where it stopped</button>'; }
|
|
6940
|
+
function askPlatformBtn() {
|
|
6941
|
+
if (live.askedPlatform === true) return '<p>Sent. The platform team has it and finishes it from here; there is nothing else for you to do.</p>';
|
|
6942
|
+
return (live.askedPlatform === 'failed' ? '<p>That didn’t send. Try again.</p>' : '') +
|
|
6943
|
+
'<button type="button" class="linklike" data-action="ask-platform">Send it to the platform team</button>';
|
|
6944
|
+
}
|
|
6715
6945
|
function appTitle() { return 'Approve the ' + esc(state.name || state.slug || 'project') + ' app on GitHub'; }
|
|
6946
|
+
/* the organisation that owns the app, when the founder made it on one (task 1004651) */
|
|
6947
|
+
function appOrgLogin() {
|
|
6948
|
+
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;
|
|
6949
|
+
}
|
|
6950
|
+
/* the app's own settings on GitHub: under its organisation, or under the founder's
|
|
6951
|
+
account. A raw URL; callers esc() it into an attribute. */
|
|
6952
|
+
function appSettingsUrl() {
|
|
6953
|
+
if (!live.appSlug) return null;
|
|
6954
|
+
var org = appOrgLogin();
|
|
6955
|
+
return 'https://github.com' + (org ? '/organizations/' + encodeURIComponent(org) : '') + '/settings/apps/' + encodeURIComponent(live.appSlug);
|
|
6956
|
+
}
|
|
6716
6957
|
|
|
6717
6958
|
/* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
|
|
6718
6959
|
(gap 3 / idea 1000676); locked until a domain exists (the App needs the real
|
|
@@ -6725,7 +6966,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6725
6966
|
/* GitHub made the app and the platform placed its keys: the slug is recorded only
|
|
6726
6967
|
after the placement (the runner), so a known slug outside a failure is the tick —
|
|
6727
6968
|
a fact, whatever the venue says about starting a NEW trip */
|
|
6728
|
-
|
|
6969
|
+
/* …including when a LATER step stopped (sign-in unconfirmed): the app and its keys are
|
|
6970
|
+
real, so this step stays ticked and the stopped one says so (task 1004652) */
|
|
6971
|
+
var appMade = (ghStep('approve') === 'done' || live.appSlug) && live.manifest !== 'expired' &&
|
|
6972
|
+
(live.manifest !== 'error' || live.manifestFailure === 'sign_in_unconfirmed');
|
|
6973
|
+
if (appMade) {
|
|
6729
6974
|
return '<li class="sdone" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub made the app, and its keys are on your project.</p></li>';
|
|
6730
6975
|
}
|
|
6731
6976
|
/* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
|
|
@@ -6739,22 +6984,24 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6739
6984
|
details (task 1003244). The one refusal a founder can act on — no
|
|
6740
6985
|
address yet — says where to go instead of what the server said. */
|
|
6741
6986
|
var snag, raw = '';
|
|
6742
|
-
/* a failure AFTER GitHub's approval left an app on the founder's account,
|
|
6743
|
-
|
|
6744
|
-
(live.manifestFailure, task 1004572) and
|
|
6745
|
-
|
|
6987
|
+
/* a failure AFTER GitHub's approval left an app on the founder's account, so
|
|
6988
|
+
"nothing was changed" would be false, and a new link would make a second app —
|
|
6989
|
+
the server says how far it got (live.manifestFailure, task 1004572) and what
|
|
6990
|
+
picking it up does (live.pickUp, task 1004652); the words and the action follow */
|
|
6991
|
+
var act = '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails();
|
|
6992
|
+
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) before GitHub was approved, so nothing was made on GitHub. Start a fresh one.';
|
|
6746
6993
|
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
|
-
else if (live.
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6994
|
+
else if (live.pickUp === 'convert') {
|
|
6995
|
+
snag = 'GitHub approved the app, and the platform stopped before it finished collecting the app’s keys. ' + NO_SECOND_APP;
|
|
6996
|
+
act = pickUpBtn();
|
|
6997
|
+
}
|
|
6998
|
+
else if (live.manifestFailure === 'finish_failed') {
|
|
6999
|
+
snag = 'GitHub made the app, but its keys never reached your project, so it can’t be used yet. Making another would leave you with two, so we won’t: the platform team finishes this one. Send it to them from here.';
|
|
7000
|
+
act = askPlatformBtn();
|
|
6752
7001
|
}
|
|
6753
|
-
else if (live.manifestFailure === 'finish_failed') snag = '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
7002
|
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
6755
|
-
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
|
|
6756
|
-
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
6757
|
-
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
|
|
7003
|
+
return '<li class="sstopped" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
|
|
7004
|
+
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') + act + '</li>';
|
|
6758
7005
|
}
|
|
6759
7006
|
if (live.manifest === 'code_received' || live.manifest === 'converting') {
|
|
6760
7007
|
/* GitHub's half of the approval is done — say so, rather than keep telling the
|
|
@@ -6765,9 +7012,19 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6765
7012
|
}
|
|
6766
7013
|
if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
|
|
6767
7014
|
autoOpenArmed = true;
|
|
6768
|
-
|
|
7015
|
+
/* the tab this page opened, as seen from here (task 1004652): behind this window, or
|
|
7016
|
+
closed before the approval. "Reopen GitHub" posts the SAME attempt again (the mint
|
|
7017
|
+
route hands back a link still waiting), so it can never make a second app. */
|
|
7018
|
+
var wait = ghTabTrouble === 'closed'
|
|
7019
|
+
? 'The GitHub tab closed before the app was approved, so nothing was made on GitHub. Reopen it to carry on with this same setup.'
|
|
7020
|
+
: ghTabTrouble === 'behind'
|
|
7021
|
+
? 'Your GitHub tab opened behind this window. Reopen it to bring it to the front; it is this same setup, so no second app will be made.'
|
|
7022
|
+
: 'Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, GitHub takes you straight on to installing it on your repo, then back here; every step ticks itself.';
|
|
7023
|
+
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + wait + '</p>' +
|
|
6769
7024
|
progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
|
|
6770
|
-
(
|
|
7025
|
+
(ghTabTrouble
|
|
7026
|
+
? '<button type="button" class="ghbtn" data-action="reopen-github">' + GH_ICON + 'Reopen GitHub ↗</button>'
|
|
7027
|
+
: '<button type="button" class="linklike" data-action="reopen-github">No GitHub tab? Reopen GitHub</button>') + '</li>';
|
|
6771
7028
|
}
|
|
6772
7029
|
return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>One trip to GitHub: approve the app, install it on your repo, and you land back here. No keys to copy, and the secret never leaves the server side.</p>' +
|
|
6773
7030
|
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
|
|
@@ -6787,12 +7044,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6787
7044
|
if (live.manifest !== 'done') {
|
|
6788
7045
|
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
7046
|
}
|
|
6790
|
-
var appUrl = live.appSlug ?
|
|
7047
|
+
var appUrl = live.appSlug ? esc(appSettingsUrl()) : null;
|
|
6791
7048
|
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
7049
|
(appUrl
|
|
6793
7050
|
? 'open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
|
|
6794
7051
|
: 'GitHub → Settings → Developer settings → GitHub Apps → your app → check <strong>Enable Device Flow</strong> → save. ') +
|
|
6795
|
-
'We tick this when GitHub has it on. Until then builders sign in on the web, and terminal sign-in falls back to a web token from the hall’s Settings page.</p
|
|
7052
|
+
'We tick this when GitHub has it on. Until then builders sign in on the web, and terminal sign-in falls back to a web token from the hall’s Settings page.</p>' +
|
|
7053
|
+
/* past the runner's checking window (task 1004652): ask it to look again. The
|
|
7054
|
+
install step carries the same action while it is unverified, so it shows here
|
|
7055
|
+
only once the install has ticked. */
|
|
7056
|
+
(live.pickUp === 'check' && ghStep('install') === 'done' ? '<button type="button" class="linklike" data-action="pick-up">Turned it on? Check again</button>' : '') + '</li>';
|
|
6796
7057
|
}
|
|
6797
7058
|
|
|
6798
7059
|
/* app-install — "Install it on your repo": the ADR 0055 publish credential (task
|
|
@@ -6809,6 +7070,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6809
7070
|
return '<li class="slocked" data-onboard-step="app-install">' + title + '<p>Comes straight after the approval, on the same trip: pick your repo and you land back here.</p></li>';
|
|
6810
7071
|
}
|
|
6811
7072
|
var installUrl = live.installUrl || ('https://github.com/apps/' + encodeURIComponent(live.appSlug) + '/installations/new');
|
|
7073
|
+
/* the runner stopped checking and GitHub never confirmed the install (task 1004652):
|
|
7074
|
+
the link opens the SAME app's install page, and the click asks the runner to look again */
|
|
7075
|
+
if (live.pickUp === 'check') {
|
|
7076
|
+
return '<li class="sstopped" data-onboard-step="app-install">' + title + '<p>GitHub stopped before the app was installed on your repo. ' + NO_SECOND_APP + '</p>' +
|
|
7077
|
+
'<a class="ghbtn" href="' + esc(installUrl) + '" target="_blank" rel="noopener" data-action="pick-up">' + GH_ICON + 'Pick up where it stopped ↗</a></li>';
|
|
7078
|
+
}
|
|
6812
7079
|
return '<li data-onboard-step="app-install">' + title + '<p>Your GitHub tab goes here next. We check with GitHub that the app is on your repo, and this step ticks itself. Came back without installing it?</p>' +
|
|
6813
7080
|
'<a class="ghbtn" href="' + esc(installUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Install it on your repo ↗</a></li>';
|
|
6814
7081
|
}
|
|
@@ -6823,6 +7090,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6823
7090
|
if (!domain) {
|
|
6824
7091
|
return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Unlocks with an address — then you open it, sign in with GitHub once, and take the owner’s seat (the founding Archon).</p></li>';
|
|
6825
7092
|
}
|
|
7093
|
+
/* the app and its keys are placed, but the project never confirmed sign-in (task
|
|
7094
|
+
1004652): the runner re-asks it with the keys already there, so picking it up never
|
|
7095
|
+
makes a second app — and a project that still will not answer goes to the platform team */
|
|
7096
|
+
if (live.manifest === 'error' && live.manifestFailure === 'sign_in_unconfirmed') {
|
|
7097
|
+
return '<li' + (live.verifying ? '' : ' class="sstopped"') + ' data-onboard-step="first-sign-in">' + title + '<p>' +
|
|
7098
|
+
(live.verifying
|
|
7099
|
+
? 'Your project hasn’t confirmed sign-in yet. We’re asking it again, with the app GitHub already made, and this step ticks itself.'
|
|
7100
|
+
: 'Your project didn’t confirm that sign-in works. ' + NO_SECOND_APP) + '</p>' +
|
|
7101
|
+
(live.pickUp === 'check' ? pickUpBtn() : '') + askPlatformBtn() + '</li>';
|
|
7102
|
+
}
|
|
6826
7103
|
if (live.manifest !== 'done') {
|
|
6827
7104
|
return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Ticks once the platform has seen sign-in work on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.</p></li>';
|
|
6828
7105
|
}
|
|
@@ -6865,6 +7142,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6865
7142
|
var r = STEP_RENDERERS[s.key];
|
|
6866
7143
|
return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
|
|
6867
7144
|
}).join('');
|
|
7145
|
+
/* the account picker stands beside Connect GitHub, so it shows while that is offered */
|
|
7146
|
+
syncAppPick(html.indexOf('data-action="manifest-start"') !== -1);
|
|
6868
7147
|
if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
|
|
6869
7148
|
lastStepsHtml = html;
|
|
6870
7149
|
$('humanSteps').innerHTML = html;
|
|
@@ -6915,6 +7194,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6915
7194
|
$('humanSteps').addEventListener('click', function (ev) {
|
|
6916
7195
|
var mBtn = ev.target.closest('[data-action="manifest-start"]');
|
|
6917
7196
|
if (mBtn) { startManifest(mBtn); return; }
|
|
7197
|
+
/* task 1004652: the pick-up link to GitHub's install page keeps its default (it opens) */
|
|
7198
|
+
var pBtn = ev.target.closest('[data-action="pick-up"]');
|
|
7199
|
+
if (pBtn) { pickUpSetup(pBtn); return; }
|
|
7200
|
+
var rBtn = ev.target.closest('[data-action="reopen-github"]');
|
|
7201
|
+
if (rBtn) { reopenGithub(rBtn); return; }
|
|
7202
|
+
var aBtn = ev.target.closest('[data-action="ask-platform"]');
|
|
7203
|
+
if (aBtn) { askPlatform(aBtn); return; }
|
|
6918
7204
|
var t = ev.target.closest('.copy');
|
|
6919
7205
|
if (!t) return;
|
|
6920
7206
|
navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
|
|
@@ -6970,7 +7256,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6970
7256
|
var manifestState = null; /* the one-time state token — kept so a parked poll can resume */
|
|
6971
7257
|
var manifestPending = false;
|
|
6972
7258
|
var manifestStartedAt = 0; /* when the founder was sent to GitHub */
|
|
6973
|
-
var manifestStale = false; /* ~90s with no approval → offer to start again */
|
|
6974
7259
|
/* the GitHub hand-back (task 1004548): the callback page returns the founder here
|
|
6975
7260
|
with the attempt's one-time state in the fragment (#ghstate=…), so a checklist
|
|
6976
7261
|
that page opened — not only the tab that started the setup — keeps watching the
|
|
@@ -7007,13 +7292,83 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7007
7292
|
var manifestGen = 0;
|
|
7008
7293
|
function stopManifestPoll() { manifestGen += 1; if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
|
|
7009
7294
|
|
|
7295
|
+
/* the organisation the server says this attempt's app is on: change-only */
|
|
7296
|
+
function noteAppOrg(org) {
|
|
7297
|
+
var next = typeof org === 'string' && org ? org : null;
|
|
7298
|
+
if (next === live.appOrg) return false;
|
|
7299
|
+
live.appOrg = next;
|
|
7300
|
+
return true;
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
/* ── Pick up where it stopped (task 1004652). The server says what the action does
|
|
7304
|
+
(live.pickUp). 'convert' and 'check' are the pick-up route's: it resumes the
|
|
7305
|
+
stopped step with the app GitHub already made, and this page watches again.
|
|
7306
|
+
'reopen' and 'fresh' go through the mint route, which hands back a link still
|
|
7307
|
+
waiting on GitHub and refuses a new one once GitHub has made the app; 'operator'
|
|
7308
|
+
is a blocker filed for the platform team. ── */
|
|
7309
|
+
function pickUpSetup(btn) {
|
|
7310
|
+
if (!instanceId) return;
|
|
7311
|
+
if (live.pickUp === 'reopen' || live.pickUp === 'fresh') { startManifest(btn); return; }
|
|
7312
|
+
if (live.pickUp === 'operator') { askPlatform(btn); return; }
|
|
7313
|
+
var isBtn = btn.tagName === 'BUTTON';
|
|
7314
|
+
if (isBtn) { btn.disabled = true; btn.textContent = 'Picking it up…'; }
|
|
7315
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app/pick-up', { method: 'POST', credentials: 'same-origin' })
|
|
7316
|
+
.then(function (res) { return res.json().catch(function () { return {}; }); })
|
|
7317
|
+
.then(function (d) {
|
|
7318
|
+
if (d && d.ok) applyPickUp(d);
|
|
7319
|
+
else if (isBtn) { btn.disabled = false; btn.textContent = 'That didn’t go through. Try again'; }
|
|
7320
|
+
watchSetupAgain();
|
|
7321
|
+
})
|
|
7322
|
+
.catch(function () { if (isBtn) { btn.disabled = false; btn.textContent = 'That didn’t go through. Try again'; } });
|
|
7323
|
+
}
|
|
7324
|
+
/* the pick-up route's answer is the fresh read: seed it and redraw at once */
|
|
7325
|
+
function applyPickUp(d) {
|
|
7326
|
+
if (typeof d.status === 'string') { live.manifest = d.status; live.manifestFailure = d.failure || null; rememberManifest(undefined, d.status); }
|
|
7327
|
+
seedSetup(d.checklist, d.install_url, d);
|
|
7328
|
+
lastStepsHtml = '';
|
|
7329
|
+
renderSteps();
|
|
7330
|
+
}
|
|
7331
|
+
/* …and keep watching: the attempt's own poll when this tab knows it, else the instance read */
|
|
7332
|
+
function watchSetupAgain() {
|
|
7333
|
+
if (manifestState) pollManifest(manifestState);
|
|
7334
|
+
else if (!pollTimer && instanceId) startProgress(instanceId);
|
|
7335
|
+
}
|
|
7336
|
+
/* "Reopen GitHub": bring a tab still open to the front, and post the SAME attempt again */
|
|
7337
|
+
function reopenGithub(btn) {
|
|
7338
|
+
if (ghTab && !ghTab.closed) { try { ghTab.focus(); } catch (e) {} }
|
|
7339
|
+
startManifest(btn);
|
|
7340
|
+
}
|
|
7341
|
+
/* the one stop no button on this page can fix (GitHub made the app but its keys never
|
|
7342
|
+
reached the project): filed as a blocker for the platform team, once per attempt */
|
|
7343
|
+
function askPlatform(btn) {
|
|
7344
|
+
if (!instanceId) return;
|
|
7345
|
+
btn.disabled = true; btn.textContent = 'Sending…';
|
|
7346
|
+
fetch(API + '/blockers', {
|
|
7347
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
7348
|
+
body: JSON.stringify({
|
|
7349
|
+
title: 'A project’s GitHub setup needs the platform team',
|
|
7350
|
+
body_md: 'Project ' + instanceId + ': the founder pressed “Send it to the platform team” on the GitHub step. GitHub made the app, but the project cannot finish connecting with it (its latest provisioning_oauth_manifests row says why in last_error), and a new link would make a second app. Once it is sorted, mark that attempt expired so the founder can start a fresh link.',
|
|
7351
|
+
source: 'github-setup', source_ref: 'instance-' + instanceId + '-' + (manifestStartedAt || 'latest')
|
|
7352
|
+
})
|
|
7353
|
+
}).then(function (res) {
|
|
7354
|
+
live.askedPlatform = res.ok ? true : 'failed';
|
|
7355
|
+
}).catch(function () { live.askedPlatform = 'failed'; })
|
|
7356
|
+
.then(function () { lastStepsHtml = ''; renderSteps(); });
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7010
7359
|
function startManifest(btn) {
|
|
7011
7360
|
if (!instanceId) return;
|
|
7361
|
+
/* an organisation that cannot go ahead says why where it is chosen, before any
|
|
7362
|
+
tab opens or any link is minted */
|
|
7363
|
+
var acct = appPick ? appPick.value() : { account: 'personal', ok: true };
|
|
7364
|
+
if (!acct.ok) { appPick.explain(); return; }
|
|
7012
7365
|
btn.disabled = true; btn.textContent = 'Preparing GitHub…';
|
|
7013
7366
|
var tab = window.open('about:blank', 'ghAppSetup');
|
|
7367
|
+
if (tab) { try { tab.focus(); } catch (e) {} watchGhTab(tab); }
|
|
7014
7368
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
|
|
7015
7369
|
method: 'POST', credentials: 'same-origin',
|
|
7016
|
-
|
|
7370
|
+
/* the founder's own account posts exactly what it always did */
|
|
7371
|
+
headers: { 'Content-Type': 'application/json' }, body: acct.account === 'org' ? JSON.stringify({ account: 'org', org: acct.org }) : '{}'
|
|
7017
7372
|
}).then(function (res) {
|
|
7018
7373
|
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1'); throw new Error('signing in'); }
|
|
7019
7374
|
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
@@ -7032,13 +7387,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7032
7387
|
form.appendChild(inp); document.body.appendChild(form);
|
|
7033
7388
|
form.submit(); form.remove();
|
|
7034
7389
|
live.manifest = 'pending'; live.manifestMsg = ''; live.manifestFailure = null;
|
|
7035
|
-
|
|
7390
|
+
noteAppOrg(d.account === 'org' ? d.org_login : null);
|
|
7391
|
+
manifestStartedAt = Date.now();
|
|
7036
7392
|
rememberManifest(d.state, 'pending');
|
|
7037
7393
|
renderSteps();
|
|
7038
7394
|
pollManifest(d.state);
|
|
7039
7395
|
}).catch(function (err) {
|
|
7040
7396
|
if (String(err && err.message) === 'signing in') return;
|
|
7041
7397
|
if (tab) { try { tab.close(); } catch (e) {} }
|
|
7398
|
+
/* GitHub already approved an app for this project (task 1004652): no second one —
|
|
7399
|
+
this page was behind, so it re-learns the truth and draws that step's own action */
|
|
7400
|
+
if (err && (err.code === 'app_exists' || err.code === 'github_in_progress')) {
|
|
7401
|
+
lastStepsHtml = '';
|
|
7402
|
+
if (instanceId) startProgress(instanceId);
|
|
7403
|
+
return;
|
|
7404
|
+
}
|
|
7042
7405
|
live.manifest = 'error'; live.manifestFailure = null; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
7043
7406
|
live.manifestCode = String((err && err.code) || '');
|
|
7044
7407
|
renderSteps();
|
|
@@ -7053,22 +7416,57 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7053
7416
|
}
|
|
7054
7417
|
/* the checklist the server derived (task 1004649) — change-only; true when it moved.
|
|
7055
7418
|
The install link rides only on a state-keyed read; the owner's read keeps the last. */
|
|
7056
|
-
function seedSetup(checklist, installUrl) {
|
|
7419
|
+
function seedSetup(checklist, installUrl, more) {
|
|
7057
7420
|
if (!checklist || typeof checklist !== 'object') return false;
|
|
7421
|
+
/* what picking it up does, and whether the runner is still checking (task 1004652) */
|
|
7422
|
+
var moreMoved = false;
|
|
7423
|
+
if (more && typeof more === 'object') {
|
|
7424
|
+
var pick = typeof more.pick_up === 'string' ? more.pick_up : null, ver = !!more.verifying;
|
|
7425
|
+
moreMoved = pick !== live.pickUp || ver !== live.verifying;
|
|
7426
|
+
live.pickUp = pick; live.verifying = ver;
|
|
7427
|
+
}
|
|
7058
7428
|
var next = JSON.stringify(checklist), was = live.setup ? JSON.stringify(live.setup) : '';
|
|
7059
7429
|
var url = typeof installUrl === 'string' && /^https:\/\/github\.com\/apps\//.test(installUrl) ? installUrl : null;
|
|
7060
7430
|
var urlMoved = !!url && url !== live.installUrl && (!live.installUrl || /[?&]state=/.test(url) || !/[?&]state=/.test(live.installUrl));
|
|
7061
|
-
if (next === was && !urlMoved) return
|
|
7431
|
+
if (next === was && !urlMoved) return moreMoved;
|
|
7062
7432
|
live.setup = checklist;
|
|
7063
7433
|
if (urlMoved) live.installUrl = url;
|
|
7064
7434
|
return true;
|
|
7065
7435
|
}
|
|
7066
7436
|
/* still worth watching after sign-in: the install not verified yet, while the runner checks */
|
|
7067
7437
|
function setupOpen() {
|
|
7068
|
-
return !!(live.setup && !live.setup.connected && live.manifest === 'done');
|
|
7438
|
+
return !!(live.setup && ((!live.setup.connected && live.manifest === 'done') || (live.verifying && live.manifest === 'error')));
|
|
7439
|
+
}
|
|
7440
|
+
/* The GitHub tab this page opened (task 1004652). A tab that opened BEHIND this window
|
|
7441
|
+
(this page kept the focus) or was closed before the approval is noticed within a few
|
|
7442
|
+
seconds — it used to take 90 — and the step offers "Reopen GitHub". */
|
|
7443
|
+
var ghTab = null, ghTabLeft = false, ghTabSince = 0, ghTabTimer = null, ghTabHeard = false;
|
|
7444
|
+
/* PURE: 'closed' · 'behind' (still focused here a few seconds after opening it) · null */
|
|
7445
|
+
function ghTabState(tab, left, since, now, focusedHere) {
|
|
7446
|
+
if (!tab) return null;
|
|
7447
|
+
if (tab.closed) return 'closed';
|
|
7448
|
+
if (!left && focusedHere && now - since > 4000) return 'behind';
|
|
7449
|
+
return null;
|
|
7450
|
+
}
|
|
7451
|
+
function watchGhTab(tab) {
|
|
7452
|
+
if (!ghTabHeard) { /* this page losing the focus = the GitHub tab came to the front */
|
|
7453
|
+
ghTabHeard = true;
|
|
7454
|
+
window.addEventListener('blur', function () { ghTabLeft = true; });
|
|
7455
|
+
document.addEventListener('visibilitychange', function () { if (document.hidden) ghTabLeft = true; });
|
|
7456
|
+
}
|
|
7457
|
+
ghTab = tab; ghTabLeft = false; ghTabSince = Date.now(); ghTabTrouble = null;
|
|
7458
|
+
if (ghTabTimer) clearInterval(ghTabTimer);
|
|
7459
|
+
ghTabTimer = setInterval(noteManifestWait, 1500);
|
|
7069
7460
|
}
|
|
7070
7461
|
function noteManifestWait() {
|
|
7071
|
-
if (live.manifest
|
|
7462
|
+
if (live.manifest !== 'pending') {
|
|
7463
|
+
if (ghTabTimer) { clearInterval(ghTabTimer); ghTabTimer = null; }
|
|
7464
|
+
ghTabTrouble = null;
|
|
7465
|
+
return;
|
|
7466
|
+
}
|
|
7467
|
+
var focused = !document.hidden && (typeof document.hasFocus !== 'function' || document.hasFocus());
|
|
7468
|
+
var next = ghTabState(ghTab, ghTabLeft, ghTabSince, Date.now(), focused);
|
|
7469
|
+
if (next !== ghTabTrouble) { ghTabTrouble = next; renderSteps(); }
|
|
7072
7470
|
}
|
|
7073
7471
|
function pollManifest(stateToken) {
|
|
7074
7472
|
stopManifestPoll();
|
|
@@ -7083,7 +7481,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7083
7481
|
.then(function (d) {
|
|
7084
7482
|
if (myGen !== manifestGen) return;
|
|
7085
7483
|
if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
|
|
7086
|
-
|
|
7484
|
+
/* the server-verified ticks (task 1004649), and whose account the app is on */
|
|
7485
|
+
var setupMoved = seedSetup(d.checklist, d.install_url, d);
|
|
7486
|
+
if (noteAppOrg(d.app_org)) setupMoved = true;
|
|
7487
|
+
if (setupMoved) renderSteps();
|
|
7087
7488
|
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
7088
7489
|
var failure = d.failure || null;
|
|
7089
7490
|
if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
|
|
@@ -7329,7 +7730,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7329
7730
|
else noteManifestWait();
|
|
7330
7731
|
}
|
|
7331
7732
|
/* the server-verified checklist rides the same read (task 1004649) */
|
|
7332
|
-
if (data.github_setup && !manifestPollTimer
|
|
7733
|
+
if (data.github_setup && !manifestPollTimer) {
|
|
7734
|
+
var seedMoved = seedSetup(data.github_setup.checklist, data.github_setup.install_url, data.github_setup);
|
|
7735
|
+
if (noteAppOrg(data.github_setup.app_org)) seedMoved = true;
|
|
7736
|
+
if (seedMoved) renderSteps();
|
|
7737
|
+
}
|
|
7333
7738
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
7334
7739
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
7335
7740
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
@@ -7368,8 +7773,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7368
7773
|
stopManifestPoll();
|
|
7369
7774
|
instanceId = null;
|
|
7370
7775
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
7371
|
-
manifestStartedAt = 0;
|
|
7372
|
-
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, setup: null, installUrl: null };
|
|
7776
|
+
manifestStartedAt = 0; ghTabTrouble = null;
|
|
7777
|
+
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false };
|
|
7373
7778
|
lastStepsHtml = '';
|
|
7374
7779
|
lastProgHtml = '';
|
|
7375
7780
|
hopReset();
|