@bridge4dev/runner 0.22.1 → 0.27.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/dist/git.js CHANGED
@@ -3,32 +3,121 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { promisify } from 'node:util';
5
5
  import { previewsDir, worktreesDir } from './paths.js';
6
+ import { firstUnreachableAncestor, inspectPath, looksLikeDubiousOwnership, runnerIdentity, safeDirectoryCommand, } from './environment.js';
6
7
  const execFileAsync = promisify(execFile);
7
8
  const GIT_TIMEOUT_MS = 30_000;
8
9
  async function git(cwd, ...args) {
9
10
  const { stdout } = await execFileAsync('git', args, { cwd, timeout: GIT_TIMEOUT_MS });
10
11
  return stdout.trim();
11
12
  }
13
+ /**
14
+ * Can this runner actually work in this directory — as the user it runs as?
15
+ *
16
+ * Everything here answers with the fix rather than the symptom. Binding a
17
+ * project is the moment the two legitimate install choices (root / dedicated
18
+ * user) start to differ, and until now the second one failed by forwarding
19
+ * git's own words to a dashboard the person may have no shell behind:
20
+ * «git check failed: fatal: detected dubious ownership in repository at
21
+ * '/opt/ids'». That sentence is true and unactionable.
22
+ */
12
23
  export async function validateWorkspacePath(workspacePath) {
13
- if (!fs.existsSync(workspacePath) || !fs.statSync(workspacePath).isDirectory()) {
14
- return { ok: false, exists: false, isGitRepo: false, error: 'Directory does not exist' };
24
+ const me = runnerIdentity();
25
+ const dir = inspectPath(workspacePath);
26
+ // «Cannot look» and «is not there» are opposite instructions to whoever reads
27
+ // this, and both arrive as a thrown `statSync`. A dedicated-user install hits
28
+ // the first one constantly — usually on a directory ABOVE the project, which
29
+ // is why the blocking one is named rather than the one that was asked about.
30
+ if (dir.unreachable) {
31
+ const blocked = firstUnreachableAncestor(workspacePath) ?? workspacePath;
32
+ return {
33
+ ok: false,
34
+ exists: true,
35
+ isGitRepo: false,
36
+ error: `The runner runs as ${me.user} and is not allowed into ${blocked}, so it cannot reach ${workspacePath}. ` +
37
+ `Grant that user access to the directory (for example \`chmod o+x ${blocked}\`, ` +
38
+ `or \`setfacl -m u:${me.user}:x ${blocked}\`), then try again.`,
39
+ };
40
+ }
41
+ if (!dir.exists || !dir.isDirectory) {
42
+ return {
43
+ ok: false,
44
+ exists: dir.exists,
45
+ isGitRepo: false,
46
+ error: dir.exists
47
+ ? `${workspacePath} is not a directory`
48
+ : `${workspacePath} does not exist on this server`,
49
+ };
50
+ }
51
+ // A directory we cannot even enter: as a non-root runner this is the most
52
+ // common outcome of pointing at somebody else's project, and git's error for
53
+ // it says nothing about permissions.
54
+ if (!dir.readable) {
55
+ return {
56
+ ok: false,
57
+ exists: true,
58
+ isGitRepo: false,
59
+ error: `The runner runs as ${me.user} and cannot read ${workspacePath}. ` +
60
+ `Give that user access (for example \`setfacl -R -m u:${me.user}:rX ${workspacePath}\`, ` +
61
+ `or \`chown -R ${me.user} ${workspacePath}\` if the directory is meant to be theirs), then try again.`,
62
+ };
15
63
  }
16
64
  try {
17
65
  const inside = await git(workspacePath, 'rev-parse', '--is-inside-work-tree');
18
66
  if (inside !== 'true') {
19
67
  return { ok: false, exists: true, isGitRepo: false, error: 'Not a git work tree' };
20
68
  }
21
- const branch = await git(workspacePath, 'rev-parse', '--abbrev-ref', 'HEAD');
22
- return { ok: true, exists: true, isGitRepo: true, branch };
23
69
  }
24
70
  catch (error) {
71
+ const message = String(error instanceof Error ? error.message : error);
72
+ if (looksLikeDubiousOwnership(message)) {
73
+ // The single most common failure of a dedicated-user install, and the one
74
+ // whose fix is one line — which is why we print the line.
75
+ const owner = dir.ownerUid >= 0 ? ` (it belongs to uid ${dir.ownerUid})` : '';
76
+ return {
77
+ ok: false,
78
+ exists: true,
79
+ isGitRepo: false,
80
+ error: `Git refuses to use ${workspacePath} because the runner runs as ${me.user} and does not own it${owner}. ` +
81
+ `Run this on the server as ${me.user}: ${safeDirectoryCommand(workspacePath)} — ` +
82
+ `or run \`devbridge-runner doctor --fix\`, which does it for every bound project. ` +
83
+ `Note: if you instead hand the directory over with \`chown\`, add the same line for its previous owner, ` +
84
+ `who will otherwise lose git in that repository.`,
85
+ };
86
+ }
25
87
  return {
26
88
  ok: false,
27
89
  exists: true,
28
90
  isGitRepo: false,
29
- error: `git check failed: ${String(error instanceof Error ? error.message : error).slice(0, 300)}`,
91
+ error: `git check failed: ${message.slice(0, 300)}`,
30
92
  };
31
93
  }
94
+ // Sessions write here: a worktree registers itself inside `.git/worktrees`,
95
+ // and a DIRECT-mode session commits in the tree itself. Read-only access
96
+ // binds fine and then fails on the first session, which is the worst place
97
+ // to learn about it.
98
+ const gitDir = await resolveGitDir(workspacePath);
99
+ const gitAccess = gitDir ? inspectPath(gitDir) : null;
100
+ if (gitAccess && gitAccess.exists && !gitAccess.writable) {
101
+ return {
102
+ ok: false,
103
+ exists: true,
104
+ isGitRepo: true,
105
+ error: `The runner runs as ${me.user} and cannot write to ${gitDir}, so it could not create a branch or a worktree here. ` +
106
+ `Give that user write access to the repository (for example \`chown -R ${me.user} ${workspacePath}\`), then try again.`,
107
+ };
108
+ }
109
+ const branch = await git(workspacePath, 'rev-parse', '--abbrev-ref', 'HEAD');
110
+ return { ok: true, exists: true, isGitRepo: true, branch };
111
+ }
112
+ /** The real `.git` directory of a work tree (a linked worktree has a file there). */
113
+ async function resolveGitDir(workspacePath) {
114
+ try {
115
+ const common = await git(workspacePath, 'rev-parse', '--git-common-dir');
116
+ return path.resolve(workspacePath, common);
117
+ }
118
+ catch {
119
+ return null;
120
+ }
32
121
  }
33
122
  export function sessionShortId(sessionId) {
34
123
  return sessionId.replace(/-/g, '').slice(0, 8);