@spunto/build 0.5.0 → 0.5.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 +2 -2
- package/src/script/setup-script.ts +56 -19
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spunto/build",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Spunto's shared Build engine
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
7
7
|
"homepage": "https://spunto.net",
|
|
@@ -35,6 +35,37 @@ function shQuote(v: string): string {
|
|
|
35
35
|
return `'${v.replace(/'/g, `'\\''`)}'`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Runs a command as the workspace user instead of root.
|
|
40
|
+
*
|
|
41
|
+
* The setup script itself is the container's CMD, so it runs as root — it has to, to chown
|
|
42
|
+
* /workspace and write under /etc. Everything it *clones*, though, belongs to the user, and
|
|
43
|
+
* cloning as root has cost us twice: once as `npm install` failing with EACCES on a root-owned
|
|
44
|
+
* node_modules (hence the re-chown after the repo clones), and once as a private dotfiles repo
|
|
45
|
+
* that authenticated with no key at all, because root has no `~/.ssh` and the identity was only
|
|
46
|
+
* named on the repo clones.
|
|
47
|
+
*
|
|
48
|
+
* `su` gives the command `HOME=/home/vscode`, which is what makes ssh pick up the `~/.ssh/config`
|
|
49
|
+
* written in the credentials step — so a clone over SSH needs nothing else to find the user's key.
|
|
50
|
+
*/
|
|
51
|
+
function asUser(username: string, cmd: string): string {
|
|
52
|
+
return `su ${username} -c ${shQuote(cmd)}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A dotfiles setting, as a clonable URL.
|
|
57
|
+
*
|
|
58
|
+
* Anything already naming a transport is left alone; only a bare `owner/repo` shorthand is
|
|
59
|
+
* expanded against GitHub. The previous test — `startsWith("http") || startsWith("git@")` — turned
|
|
60
|
+
* `ssh://git@host/me/dotfiles.git` into `https://github.com/ssh://git@host/me/dotfiles.git`.
|
|
61
|
+
*/
|
|
62
|
+
function normalizeDotfilesUrl(raw: string): string {
|
|
63
|
+
const v = raw.trim()
|
|
64
|
+
return /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/|[^/\s]+@[^/\s]+:)/.test(v)
|
|
65
|
+
? v
|
|
66
|
+
: `https://github.com/${v.replace(/^\/+|\/+$/g, "")}`
|
|
67
|
+
}
|
|
68
|
+
|
|
38
69
|
/**
|
|
39
70
|
* `export EXTENSIONS_GALLERY=…`, or nothing when the org is on the default (Open VSX) registry.
|
|
40
71
|
*
|
|
@@ -105,11 +136,13 @@ function cloneRepoBlock(params: {
|
|
|
105
136
|
index: number
|
|
106
137
|
total: number
|
|
107
138
|
homeDir: string
|
|
139
|
+
/** Who the checkout belongs to — the clone runs as them. */
|
|
140
|
+
username: string
|
|
108
141
|
branch?: string
|
|
109
142
|
githubInstallationTokens?: Record<string, string>
|
|
110
143
|
userSshPrivateKey?: string
|
|
111
144
|
}): { header: string; lines: string[] } {
|
|
112
|
-
const { repo: r, index, total, homeDir, branch, githubInstallationTokens, userSshPrivateKey } = params
|
|
145
|
+
const { repo: r, index, total, homeDir, username, branch, githubInstallationTokens, userSshPrivateKey } = params
|
|
113
146
|
// Pick the installation token for this repo's owner. A GitHub App installation only grants
|
|
114
147
|
// access to repos owned by its account, so the owner login (== installation accountLogin)
|
|
115
148
|
// uniquely selects the right token among the org's connected installations. Covers legacy
|
|
@@ -117,13 +150,16 @@ function cloneRepoBlock(params: {
|
|
|
117
150
|
const owner = (r.project.split("/")[0] ?? "").toLowerCase()
|
|
118
151
|
const repoToken = githubInstallationTokens?.[owner]
|
|
119
152
|
const b = branch ? ` --branch ${shQuote(branch)}` : ""
|
|
120
|
-
const
|
|
153
|
+
const rawCloneCmd = r.provider === "git" && r.cloneUrl
|
|
121
154
|
? `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}`
|
|
122
155
|
: r.provider === "github" && repoToken
|
|
123
156
|
? `git clone${b} https://x-access-token:${repoToken}@github.com/${r.project}.git /workspace/${r.workspacePath} && git -C /workspace/${r.workspacePath} remote set-url origin git@github.com:${r.project}.git`
|
|
124
157
|
: r.provider === "github" && userSshPrivateKey
|
|
125
158
|
? `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}`
|
|
126
159
|
: `git clone${b} https://github.com/${r.project} /workspace/${r.workspacePath}`
|
|
160
|
+
// Cloned as the user, not as root: /workspace is already theirs (the ownership step runs
|
|
161
|
+
// first), so the checkout lands with the right owner and needs no chown afterwards.
|
|
162
|
+
const cloneCmd = asUser(username, rawCloneCmd)
|
|
127
163
|
|
|
128
164
|
// Single-quoted only when a (user-provided) branch is interpolated; the plain form is kept
|
|
129
165
|
// verbatim for the no-branch case so the generated script is unchanged there.
|
|
@@ -851,23 +887,23 @@ export function buildSetupScript(params: SetupScriptParams): { script: string }
|
|
|
851
887
|
|
|
852
888
|
// ── 4. Dotfiles ───────────────────────────────────────────────────────────
|
|
853
889
|
if (dotfilesRepo) {
|
|
854
|
-
const dotfilesUrl =
|
|
855
|
-
? dotfilesRepo
|
|
856
|
-
: `https://github.com/${dotfilesRepo}`
|
|
890
|
+
const dotfilesUrl = normalizeDotfilesUrl(dotfilesRepo)
|
|
857
891
|
push(...banner("SETUP: DOTFILES"))
|
|
858
892
|
mpAt(mkStatus("dotfiles", allReposPending, pc0, null), "dotfiles")
|
|
859
893
|
push(
|
|
860
894
|
`echo "Cloning dotfiles from ${dotfilesUrl}..."`,
|
|
861
895
|
`set +e`,
|
|
862
|
-
|
|
896
|
+
// As the user, like every other clone: root has no `~/.ssh`, so a private dotfiles repo
|
|
897
|
+
// over SSH would authenticate with no key at all.
|
|
898
|
+
`${asUser(username, `git clone ${shQuote(dotfilesUrl)} ${homeDir}/dotfiles`)} 2>&1`,
|
|
863
899
|
`_DOTS_EXIT=$?`,
|
|
864
900
|
`set -e`,
|
|
865
901
|
`if [ $_DOTS_EXIT -ne 0 ]; then`,
|
|
866
902
|
` echo "Dotfiles clone failed (exit $_DOTS_EXIT) — continuing without dotfiles"`,
|
|
867
903
|
`else`,
|
|
868
904
|
` echo "Dotfiles cloned"`,
|
|
869
|
-
//
|
|
870
|
-
//
|
|
905
|
+
// The clone above already lands as the user. Kept for a worker whose ~/dotfiles was left
|
|
906
|
+
// root-owned by an earlier release, where the install script below would fail on it.
|
|
871
907
|
` chown -R ${username}:${username} ${homeDir}/dotfiles`,
|
|
872
908
|
` _INSTALL_SCRIPT=""`,
|
|
873
909
|
` for _candidate in install.sh bootstrap.sh setup.sh script/setup; do`,
|
|
@@ -911,6 +947,7 @@ export function buildSetupScript(params: SetupScriptParams): { script: string }
|
|
|
911
947
|
index: i,
|
|
912
948
|
total: project.repositories.length,
|
|
913
949
|
homeDir,
|
|
950
|
+
username,
|
|
914
951
|
branch: resolveRepoBranch(r, branch),
|
|
915
952
|
githubInstallationTokens,
|
|
916
953
|
userSshPrivateKey,
|
|
@@ -924,11 +961,10 @@ export function buildSetupScript(params: SetupScriptParams): { script: string }
|
|
|
924
961
|
})
|
|
925
962
|
|
|
926
963
|
// ── 5b. Re-own /workspace after cloning ───────────────────────────────────
|
|
927
|
-
//
|
|
928
|
-
//
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
// node_modules). Must run before postCreate, not just in the final step.
|
|
964
|
+
// The clones above run as the user, so their checkouts already belong to them. This stays as a
|
|
965
|
+
// net: a repo left root-owned by an earlier release (when cloning *was* done as root) would
|
|
966
|
+
// otherwise keep failing postCreate with EACCES — `npm install` on a root-owned node_modules —
|
|
967
|
+
// and the final chown in step 8 comes too late for that.
|
|
932
968
|
if (project.repositories.length > 0) {
|
|
933
969
|
push("", `chown -R ${username}:${username} /workspace`)
|
|
934
970
|
}
|
|
@@ -1774,21 +1810,21 @@ export function buildSetupPlan(params: SetupScriptParams): {
|
|
|
1774
1810
|
|
|
1775
1811
|
// ── dotfiles ──────────────────────────────────────────────────────────────
|
|
1776
1812
|
if (dotfilesRepo) {
|
|
1777
|
-
const dotfilesUrl =
|
|
1778
|
-
? dotfilesRepo
|
|
1779
|
-
: `https://github.com/${dotfilesRepo}`
|
|
1813
|
+
const dotfilesUrl = normalizeDotfilesUrl(dotfilesRepo)
|
|
1780
1814
|
const l: string[] = ["set -e", ...banner("SETUP: DOTFILES"),
|
|
1781
1815
|
`echo "Cloning dotfiles from ${dotfilesUrl}..."`,
|
|
1782
1816
|
`set +e`,
|
|
1783
|
-
|
|
1817
|
+
// As the user, like every other clone: root has no `~/.ssh`, so a private dotfiles repo
|
|
1818
|
+
// over SSH would authenticate with no key at all.
|
|
1819
|
+
`${asUser(username, `git clone ${shQuote(dotfilesUrl)} ${homeDir}/dotfiles`)} 2>&1`,
|
|
1784
1820
|
`_DOTS_EXIT=$?`,
|
|
1785
1821
|
`set -e`,
|
|
1786
1822
|
`if [ $_DOTS_EXIT -ne 0 ]; then`,
|
|
1787
1823
|
` echo "Dotfiles clone failed (exit $_DOTS_EXIT) — continuing without dotfiles"`,
|
|
1788
1824
|
`else`,
|
|
1789
1825
|
` echo "Dotfiles cloned"`,
|
|
1790
|
-
//
|
|
1791
|
-
//
|
|
1826
|
+
// The clone above already lands as the user. Kept for a worker whose ~/dotfiles was left
|
|
1827
|
+
// root-owned by an earlier release, where the install script below would fail on it.
|
|
1792
1828
|
` chown -R ${username}:${username} ${homeDir}/dotfiles`,
|
|
1793
1829
|
` _INSTALL_SCRIPT=""`,
|
|
1794
1830
|
` for _candidate in install.sh bootstrap.sh setup.sh script/setup; do`,
|
|
@@ -1833,6 +1869,7 @@ export function buildSetupPlan(params: SetupScriptParams): {
|
|
|
1833
1869
|
index: i,
|
|
1834
1870
|
total: project.repositories.length,
|
|
1835
1871
|
homeDir,
|
|
1872
|
+
username,
|
|
1836
1873
|
branch: resolveRepoBranch(r, branch),
|
|
1837
1874
|
githubInstallationTokens,
|
|
1838
1875
|
userSshPrivateKey,
|