@spunto/build 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/script/setup-script.ts +124 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spunto/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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
|
|
@@ -219,9 +295,16 @@ function credentialsBlock(params: {
|
|
|
219
295
|
const { homeDir, username, userInfo, userSshPrivateKey, projectDeployKey } = params
|
|
220
296
|
const lines: string[] = []
|
|
221
297
|
|
|
298
|
+
// Through `asUser`, not plain `git config --global`. The setup script is the container's CMD,
|
|
299
|
+
// so it runs as root, and `--global` follows $HOME — every one of these settings landed in
|
|
300
|
+
// /root/.gitconfig while git, in the user's terminal, reads ${homeDir}/.gitconfig. The identity
|
|
301
|
+
// was silently absent, and so was the credential helper below: git fell back to prompting for a
|
|
302
|
+
// password on a repository it had just cloned.
|
|
222
303
|
if (userInfo) {
|
|
223
|
-
lines.push(`git config --global user.name ${JSON.stringify(userInfo.name)}`)
|
|
224
|
-
if (userInfo.email)
|
|
304
|
+
lines.push(asUser(username, `git config --global user.name ${JSON.stringify(userInfo.name)}`))
|
|
305
|
+
if (userInfo.email) {
|
|
306
|
+
lines.push(asUser(username, `git config --global user.email ${JSON.stringify(userInfo.email)}`))
|
|
307
|
+
}
|
|
225
308
|
}
|
|
226
309
|
|
|
227
310
|
if (userSshPrivateKey) {
|
|
@@ -236,6 +319,31 @@ function credentialsBlock(params: {
|
|
|
236
319
|
)
|
|
237
320
|
}
|
|
238
321
|
|
|
322
|
+
// Git credential helper (RFC 0024). Git runs this at the moment of a `fetch`/`push` and reads
|
|
323
|
+
// `username=` / `password=` from its stdout; anything else it prints is ignored, so a failure
|
|
324
|
+
// message on stderr reaches the user's terminal while git falls through to asking as before.
|
|
325
|
+
//
|
|
326
|
+
// Nothing durable is stored: the credential is fetched per operation and lives only in the
|
|
327
|
+
// process that asked for it. `credential.useHttpPath` is on because the platform resolves *which*
|
|
328
|
+
// provider hosts a repository from its `owner/name`, which git only sends when asked to.
|
|
329
|
+
//
|
|
330
|
+
// Guarded on the worker token being present: a worker spawned before RFC 0024, or one whose
|
|
331
|
+
// token could not be minted, simply keeps the previous behaviour instead of failing to start.
|
|
332
|
+
lines.push(
|
|
333
|
+
'if [ -n "${SPUNTO_WORKER_TOKEN:-}" ] && [ -n "${SPUNTO_API_URL:-}" ]; then',
|
|
334
|
+
' echo "Configuring git credential helper..."',
|
|
335
|
+
` mkdir -p ${homeDir}/.spunto`,
|
|
336
|
+
` cat > ${homeDir}/.spunto/git-credential-spunto <<'SPUNTO_HELPER'`,
|
|
337
|
+
GIT_CREDENTIAL_HELPER,
|
|
338
|
+
"SPUNTO_HELPER",
|
|
339
|
+
` chmod +x ${homeDir}/.spunto/git-credential-spunto`,
|
|
340
|
+
` chown ${username} ${homeDir}/.spunto/git-credential-spunto`,
|
|
341
|
+
` ${asUser(username, `git config --global credential.helper ${homeDir}/.spunto/git-credential-spunto`)}`,
|
|
342
|
+
` ${asUser(username, "git config --global credential.useHttpPath true")}`,
|
|
343
|
+
' echo "Git credential helper configured"',
|
|
344
|
+
"fi",
|
|
345
|
+
)
|
|
346
|
+
|
|
239
347
|
// Per-project deploy key (RFC 0013) — used only for generic "git" repos, referenced per-clone
|
|
240
348
|
// via GIT_SSH_COMMAND (not added to ~/.ssh/config, so it never shadows the user's own key).
|
|
241
349
|
if (projectDeployKey) {
|
|
@@ -750,6 +858,13 @@ export type SetupScriptParams = {
|
|
|
750
858
|
workerId: string
|
|
751
859
|
userInfo?: { name: string; email?: string | null }
|
|
752
860
|
userSshPrivateKey?: string
|
|
861
|
+
/**
|
|
862
|
+
* Whether that key is **registered on the member's provider account**. Distinct from having one:
|
|
863
|
+
* every account is given a keypair at signup, and it is only ever registered when someone
|
|
864
|
+
* connects their provider — so `userSshPrivateKey` is set for everyone and says nothing about
|
|
865
|
+
* whether SSH would authenticate. See `originSetUrl`.
|
|
866
|
+
*/
|
|
867
|
+
userSshKeyRegistered?: boolean
|
|
753
868
|
sshGatewayPublicKey?: string
|
|
754
869
|
skipFeatures?: boolean
|
|
755
870
|
dotfilesRepo?: string | null
|
|
@@ -768,7 +883,7 @@ export type SetupScriptParams = {
|
|
|
768
883
|
}
|
|
769
884
|
|
|
770
885
|
export function buildSetupScript(params: SetupScriptParams): { script: string } {
|
|
771
|
-
const { project, workerId, userInfo, userSshPrivateKey, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
886
|
+
const { project, workerId, userInfo, userSshPrivateKey, userSshKeyRegistered, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
772
887
|
const homeDir = "/home/vscode"
|
|
773
888
|
const username = "vscode"
|
|
774
889
|
|
|
@@ -951,6 +1066,7 @@ export function buildSetupScript(params: SetupScriptParams): { script: string }
|
|
|
951
1066
|
branch: resolveRepoBranch(r, branch),
|
|
952
1067
|
githubInstallationTokens,
|
|
953
1068
|
userSshPrivateKey,
|
|
1069
|
+
userSshKeyRegistered,
|
|
954
1070
|
})
|
|
955
1071
|
push("")
|
|
956
1072
|
push(header)
|
|
@@ -1695,7 +1811,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
|
|
|
1695
1811
|
initialStatus: SetupStatus
|
|
1696
1812
|
readyStatus: SetupStatus
|
|
1697
1813
|
} {
|
|
1698
|
-
const { project, userInfo, userSshPrivateKey, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
1814
|
+
const { project, userInfo, userSshPrivateKey, userSshKeyRegistered, dotfilesRepo, userEnvSecrets, githubInstallationTokens, projectDeployKey, branch } = params
|
|
1699
1815
|
const homeDir = "/home/vscode"
|
|
1700
1816
|
const username = "vscode"
|
|
1701
1817
|
|
|
@@ -1873,6 +1989,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
|
|
|
1873
1989
|
branch: resolveRepoBranch(r, branch),
|
|
1874
1990
|
githubInstallationTokens,
|
|
1875
1991
|
userSshPrivateKey,
|
|
1992
|
+
userSshKeyRegistered,
|
|
1876
1993
|
})
|
|
1877
1994
|
l.push("", header, ...cloneLines)
|
|
1878
1995
|
})
|