@spunto/build 0.5.1 → 0.6.0
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/package.json +1 -1
- package/src/script/setup-script.ts +114 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spunto/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Spunto's shared Build engine — the devcontainer image protocol and VS Code extension registry clients, with no database, no HTTP framework and no UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -141,8 +141,10 @@ function cloneRepoBlock(params: {
|
|
|
141
141
|
branch?: string
|
|
142
142
|
githubInstallationTokens?: Record<string, string>
|
|
143
143
|
userSshPrivateKey?: string
|
|
144
|
+
/** Whether that key is registered on the member's provider account — see `originSetUrl`. */
|
|
145
|
+
userSshKeyRegistered?: boolean
|
|
144
146
|
}): { header: string; lines: string[] } {
|
|
145
|
-
const { repo: r, index, total, homeDir, username, branch, githubInstallationTokens, userSshPrivateKey } = params
|
|
147
|
+
const { repo: r, index, total, homeDir, username, branch, githubInstallationTokens, userSshPrivateKey, userSshKeyRegistered } = params
|
|
146
148
|
// Pick the installation token for this repo's owner. A GitHub App installation only grants
|
|
147
149
|
// access to repos owned by its account, so the owner login (== installation accountLogin)
|
|
148
150
|
// uniquely selects the right token among the org's connected installations. Covers legacy
|
|
@@ -153,10 +155,16 @@ function cloneRepoBlock(params: {
|
|
|
153
155
|
const rawCloneCmd = r.provider === "git" && r.cloneUrl
|
|
154
156
|
? `GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ${homeDir}/.ssh/mp_deploy_key" git clone${b} ${shQuote(r.cloneUrl)} /workspace/${r.workspacePath}`
|
|
155
157
|
: r.provider === "github" && repoToken
|
|
156
|
-
? `git clone${b} https://x-access-token:${repoToken}@github.com/${r.project}.git /workspace/${r.workspacePath} &&
|
|
158
|
+
? `git clone${b} https://x-access-token:${repoToken}@github.com/${r.project}.git /workspace/${r.workspacePath} && ${originSetUrl(r, `/workspace/${r.workspacePath}`, userSshKeyRegistered)}`
|
|
157
159
|
: r.provider === "github" && userSshPrivateKey
|
|
158
160
|
? `GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ${homeDir}/.ssh/mp_user_key" git clone${b} git@github.com:${r.project}.git /workspace/${r.workspacePath}`
|
|
159
|
-
:
|
|
161
|
+
// Last resort: a public clone over https. Only valid for GitHub — for any other hosting
|
|
162
|
+
// provider this would quietly clone the wrong host, so say what happened instead. A provider
|
|
163
|
+
// reaches this line only when its credential could not be minted, which is worth reading in
|
|
164
|
+
// the setup log rather than debugging from a confusing 404.
|
|
165
|
+
: r.provider === "github"
|
|
166
|
+
? `git clone${b} https://github.com/${r.project} /workspace/${r.workspacePath}`
|
|
167
|
+
: `echo "No clone credential for ${r.provider} repository ${r.project} — check the organization's integrations." >&2 && false`
|
|
160
168
|
// Cloned as the user, not as root: /workspace is already theirs (the ownership step runs
|
|
161
169
|
// first), so the checkout lands with the right owner and needs no chown afterwards.
|
|
162
170
|
const cloneCmd = asUser(username, rawCloneCmd)
|
|
@@ -209,6 +217,74 @@ function cloneRepoBlock(params: {
|
|
|
209
217
|
* both, and also makes the credentials usable by everything that runs as the user *during* setup
|
|
210
218
|
* (postCreateCommand, dotfiles install script).
|
|
211
219
|
*/
|
|
220
|
+
/**
|
|
221
|
+
* The credential helper's own source, kept whole rather than assembled line by line: it is a
|
|
222
|
+
* shell script inside a TypeScript string inside a heredoc, and every layer of escaping added
|
|
223
|
+
* to that is a layer someone has to debug later.
|
|
224
|
+
*/
|
|
225
|
+
/**
|
|
226
|
+
* Where `origin` points after an installation-token clone.
|
|
227
|
+
*
|
|
228
|
+
* The clone itself is the organization's; `origin` decides who everything *after* it acts as.
|
|
229
|
+
* Three cases, in this order:
|
|
230
|
+
*
|
|
231
|
+
* 1. **The worker has an identity** (`SPUNTO_WORKER_TOKEN`) → HTTPS, and the credential helper
|
|
232
|
+
* answers as the member who owns it (RFC 0024). This is the normal case now: every spawn mints
|
|
233
|
+
* one. Decided in shell rather than here so it stays locked to the same condition the helper's
|
|
234
|
+
* own install is guarded on — two places, one signal.
|
|
235
|
+
* 2. **No identity, but the member's key is registered on their account** → SSH, the old path.
|
|
236
|
+
* 3. **Neither** → HTTPS with nothing behind it. Public repos work; a private one fails *at the
|
|
237
|
+
* push*, which is the honest outcome, rather than an SSH refusal that says nothing.
|
|
238
|
+
*
|
|
239
|
+
* Note what case 2 keys on: the key being **registered**, not merely existing. A key is generated
|
|
240
|
+
* for every account at signup (`auth.router.ts`) and only ever registered on the provider when
|
|
241
|
+
* someone connects it — so "has a private key" is true for everyone and means nothing here.
|
|
242
|
+
*/
|
|
243
|
+
function originSetUrl(
|
|
244
|
+
r: { project: string },
|
|
245
|
+
path: string,
|
|
246
|
+
userSshKeyRegistered?: boolean,
|
|
247
|
+
): string {
|
|
248
|
+
const https = `git -C ${path} remote set-url origin https://github.com/${r.project}.git`
|
|
249
|
+
const ssh = `git -C ${path} remote set-url origin git@github.com:${r.project}.git`
|
|
250
|
+
|
|
251
|
+
// **Undefined is a caller that predates this parameter**, and it gets exactly what it always
|
|
252
|
+
// got: the SSH rewrite. This package is consumed by published version, so a host can adopt a new
|
|
253
|
+
// release before it knows to pass the new field — and for that host there is no worker identity
|
|
254
|
+
// either, so answering HTTPS would leave it with an origin nothing can authenticate. Silence on
|
|
255
|
+
// a push, for every member with a registered key.
|
|
256
|
+
if (userSshKeyRegistered === undefined) return ssh
|
|
257
|
+
|
|
258
|
+
// Told, and the key is not registered: SSH would refuse, so stay on HTTPS and let the credential
|
|
259
|
+
// helper answer as the member.
|
|
260
|
+
if (!userSshKeyRegistered) return https
|
|
261
|
+
|
|
262
|
+
// Told, and registered: the helper still comes first where the worker has an identity.
|
|
263
|
+
return `if [ -n "\${SPUNTO_WORKER_TOKEN:-}" ]; then ${https}; else ${ssh}; fi`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const GIT_CREDENTIAL_HELPER = `#!/bin/sh
|
|
267
|
+
# Asks Spunto for a credential belonging to this worker's owner, at the moment git needs it.
|
|
268
|
+
# Only \`get\` is answered: there is nothing to store (we hold no cache) and nothing to erase.
|
|
269
|
+
[ "$1" = "get" ] || exit 0
|
|
270
|
+
while IFS= read -r line; do
|
|
271
|
+
case "$line" in
|
|
272
|
+
path=*) REPO="\${line#path=}" ;;
|
|
273
|
+
"") break ;;
|
|
274
|
+
esac
|
|
275
|
+
done
|
|
276
|
+
REPO="\${REPO%.git}"
|
|
277
|
+
RESPONSE=$(curl -fsS -m 20 -X POST "$SPUNTO_API_URL/api/workers/git-credential" -H "Authorization: Bearer $SPUNTO_WORKER_TOKEN" -H "Content-Type: application/json" -d "{\\"repo\\":\\"$REPO\\"}" 2>/dev/null)
|
|
278
|
+
PASSWORD=$(printf "%s" "$RESPONSE" | sed -n 's/.*"password":"\\([^"]*\\)".*/\\1/p')
|
|
279
|
+
if [ -z "$PASSWORD" ]; then
|
|
280
|
+
echo "spunto: no git credential for $REPO - authorize the provider in Integrations." >&2
|
|
281
|
+
exit 0
|
|
282
|
+
fi
|
|
283
|
+
USERNAME=$(printf "%s" "$RESPONSE" | sed -n 's/.*"username":"\\([^"]*\\)".*/\\1/p')
|
|
284
|
+
echo "username=$USERNAME"
|
|
285
|
+
echo "password=$PASSWORD"
|
|
286
|
+
`
|
|
287
|
+
|
|
212
288
|
function credentialsBlock(params: {
|
|
213
289
|
homeDir: string
|
|
214
290
|
username: string
|
|
@@ -236,6 +312,30 @@ function credentialsBlock(params: {
|
|
|
236
312
|
)
|
|
237
313
|
}
|
|
238
314
|
|
|
315
|
+
// Git credential helper (RFC 0024). Git runs this at the moment of a `fetch`/`push` and reads
|
|
316
|
+
// `username=` / `password=` from its stdout; anything else it prints is ignored, so a failure
|
|
317
|
+
// message on stderr reaches the user's terminal while git falls through to asking as before.
|
|
318
|
+
//
|
|
319
|
+
// Nothing durable is stored: the credential is fetched per operation and lives only in the
|
|
320
|
+
// process that asked for it. `credential.useHttpPath` is on because the platform resolves *which*
|
|
321
|
+
// provider hosts a repository from its `owner/name`, which git only sends when asked to.
|
|
322
|
+
//
|
|
323
|
+
// Guarded on the worker token being present: a worker spawned before RFC 0024, or one whose
|
|
324
|
+
// token could not be minted, simply keeps the previous behaviour instead of failing to start.
|
|
325
|
+
lines.push(
|
|
326
|
+
'if [ -n "${SPUNTO_WORKER_TOKEN:-}" ] && [ -n "${SPUNTO_API_URL:-}" ]; then',
|
|
327
|
+
' echo "Configuring git credential helper..."',
|
|
328
|
+
` mkdir -p ${homeDir}/.spunto`,
|
|
329
|
+
` cat > ${homeDir}/.spunto/git-credential-spunto <<'SPUNTO_HELPER'`,
|
|
330
|
+
GIT_CREDENTIAL_HELPER,
|
|
331
|
+
"SPUNTO_HELPER",
|
|
332
|
+
` chmod +x ${homeDir}/.spunto/git-credential-spunto`,
|
|
333
|
+
` git config --global credential.helper ${homeDir}/.spunto/git-credential-spunto`,
|
|
334
|
+
" git config --global credential.useHttpPath true",
|
|
335
|
+
' echo "Git credential helper configured"',
|
|
336
|
+
"fi",
|
|
337
|
+
)
|
|
338
|
+
|
|
239
339
|
// Per-project deploy key (RFC 0013) — used only for generic "git" repos, referenced per-clone
|
|
240
340
|
// via GIT_SSH_COMMAND (not added to ~/.ssh/config, so it never shadows the user's own key).
|
|
241
341
|
if (projectDeployKey) {
|
|
@@ -750,6 +850,13 @@ export type SetupScriptParams = {
|
|
|
750
850
|
workerId: string
|
|
751
851
|
userInfo?: { name: string; email?: string | null }
|
|
752
852
|
userSshPrivateKey?: string
|
|
853
|
+
/**
|
|
854
|
+
* Whether that key is **registered on the member's provider account**. Distinct from having one:
|
|
855
|
+
* every account is given a keypair at signup, and it is only ever registered when someone
|
|
856
|
+
* connects their provider — so `userSshPrivateKey` is set for everyone and says nothing about
|
|
857
|
+
* whether SSH would authenticate. See `originSetUrl`.
|
|
858
|
+
*/
|
|
859
|
+
userSshKeyRegistered?: boolean
|
|
753
860
|
sshGatewayPublicKey?: string
|
|
754
861
|
skipFeatures?: boolean
|
|
755
862
|
dotfilesRepo?: string | null
|
|
@@ -768,7 +875,7 @@ export type SetupScriptParams = {
|
|
|
768
875
|
}
|
|
769
876
|
|
|
770
877
|
export function buildSetupScript(params: SetupScriptParams): { script: string } {
|
|
771
|
-
const { project, workerId, userInfo, userSshPrivateKey, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
878
|
+
const { project, workerId, userInfo, userSshPrivateKey, userSshKeyRegistered, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
772
879
|
const homeDir = "/home/vscode"
|
|
773
880
|
const username = "vscode"
|
|
774
881
|
|
|
@@ -951,6 +1058,7 @@ export function buildSetupScript(params: SetupScriptParams): { script: string }
|
|
|
951
1058
|
branch: resolveRepoBranch(r, branch),
|
|
952
1059
|
githubInstallationTokens,
|
|
953
1060
|
userSshPrivateKey,
|
|
1061
|
+
userSshKeyRegistered,
|
|
954
1062
|
})
|
|
955
1063
|
push("")
|
|
956
1064
|
push(header)
|
|
@@ -1695,7 +1803,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
|
|
|
1695
1803
|
initialStatus: SetupStatus
|
|
1696
1804
|
readyStatus: SetupStatus
|
|
1697
1805
|
} {
|
|
1698
|
-
const { project, userInfo, userSshPrivateKey, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
1806
|
+
const { project, userInfo, userSshPrivateKey, userSshKeyRegistered, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
1699
1807
|
const homeDir = "/home/vscode"
|
|
1700
1808
|
const username = "vscode"
|
|
1701
1809
|
|
|
@@ -1873,6 +1981,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
|
|
|
1873
1981
|
branch: resolveRepoBranch(r, branch),
|
|
1874
1982
|
githubInstallationTokens,
|
|
1875
1983
|
userSshPrivateKey,
|
|
1984
|
+
userSshKeyRegistered,
|
|
1876
1985
|
})
|
|
1877
1986
|
l.push("", header, ...cloneLines)
|
|
1878
1987
|
})
|