hereya-cli 0.100.6 → 0.101.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.
@@ -2,6 +2,19 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { gitUtils } from './git-utils.js';
6
+ // DI seam for tests. Defaults to the real `simple-git` (loaded lazily so the
7
+ // module stays cheap to import). Tests replace this via `setSimpleGitFactory`.
8
+ let simpleGitFactory;
9
+ export function setSimpleGitFactory(factory) {
10
+ simpleGitFactory = factory;
11
+ }
12
+ async function getSimpleGit() {
13
+ if (simpleGitFactory)
14
+ return simpleGitFactory;
15
+ const { simpleGit } = await import('simple-git');
16
+ return simpleGit;
17
+ }
5
18
  /**
6
19
  * Clone an app's source repository (by repo URL + commit SHA) into a fresh
7
20
  * temp dir under ~/.hereya/downloads/<id>. Returns the rootDir path along
@@ -24,16 +37,40 @@ export const appSource = {
24
37
  }
25
38
  };
26
39
  try {
27
- const { simpleGit } = await import('simple-git');
28
- const git = simpleGit();
40
+ const simpleGit = await getSimpleGit();
29
41
  input.logger?.debug?.(`Cloning app source from ${input.repository}${input.commit ? `@${input.commit}` : ''} into ${rootDir}`);
30
- if (input.commit) {
31
- await git.clone(input.repository, rootDir, ['--no-checkout']);
32
- const repoGit = simpleGit(rootDir);
33
- await repoGit.checkout(input.commit);
42
+ if (input.auth?.password) {
43
+ // Authenticated clone through the hereya credential helper. We always
44
+ // do a full clone here (no shallow/no-checkout optimization) because
45
+ // the credential helper path uses a plain `git clone`; the optional
46
+ // commit checkout is performed afterwards.
47
+ input.logger?.debug?.('Using hereya credential helper for authenticated app-source clone');
48
+ const cloneResult = await gitUtils.cloneWithCredentialHelper({
49
+ gitUrl: input.repository,
50
+ hereyaBinPath: input.auth.hereyaBinPath,
51
+ password: input.auth.password,
52
+ targetDir: rootDir,
53
+ username: input.auth.username,
54
+ });
55
+ if (!cloneResult.success) {
56
+ throw new Error(cloneResult.reason);
57
+ }
58
+ if (input.commit) {
59
+ const repoGit = simpleGit(rootDir);
60
+ await repoGit.checkout(input.commit);
61
+ }
34
62
  }
35
63
  else {
36
- await git.clone(input.repository, rootDir, ['--depth=1']);
64
+ // Unauthenticated clone (public-repo path).
65
+ const git = simpleGit();
66
+ if (input.commit) {
67
+ await git.clone(input.repository, rootDir, ['--no-checkout']);
68
+ const repoGit = simpleGit(rootDir);
69
+ await repoGit.checkout(input.commit);
70
+ }
71
+ else {
72
+ await git.clone(input.repository, rootDir, ['--depth=1']);
73
+ }
37
74
  }
38
75
  if (input.sha256) {
39
76
  const repoGit = simpleGit(rootDir);
@@ -1,4 +1,5 @@
1
1
  import { CloudBackend } from '../../backend/cloud/cloud-backend.js';
2
+ import { LocalExecutor } from '../../executor/local.js';
2
3
  import { Logger } from '../../lib/log.js';
3
4
  export type AppJobResult = {
4
5
  reason: string;
@@ -10,4 +11,4 @@ export declare function executeAppJob(job: {
10
11
  id: string;
11
12
  payload: any;
12
13
  type: string;
13
- }, cloudBackend: CloudBackend, logger: Logger): Promise<AppJobResult>;
14
+ }, cloudBackend: CloudBackend, logger: Logger, executor: LocalExecutor): Promise<AppJobResult>;
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import * as appSourceLib from '../../lib/app-source.js';
4
4
  import { shellUtils } from '../../lib/shell.js';
5
5
  import { sanitizeWorkspaceForFilename } from './format.js';
6
- export async function executeAppJob(job, cloudBackend, logger) {
6
+ export async function executeAppJob(job, cloudBackend, logger, executor) {
7
7
  const isDeploy = job.type === 'app-deploy';
8
8
  const subcommand = isDeploy ? 'deploy' : 'destroy';
9
9
  const payload = job.payload;
@@ -17,7 +17,8 @@ export async function executeAppJob(job, cloudBackend, logger) {
17
17
  let cleanup;
18
18
  try {
19
19
  logger.info(`Downloading app source ${repository}${commit ? `@${commit}` : ''}...`);
20
- const downloadResult = await appSourceLib.appSource.download({ commit, logger, repository, sha256 });
20
+ const auth = await resolveAppRepoCredentials({ appName, executor, logger, repository, workspace });
21
+ const downloadResult = await appSourceLib.appSource.download({ auth, commit, logger, repository, sha256 });
21
22
  cleanup = downloadResult.cleanup;
22
23
  const { rootDir } = downloadResult;
23
24
  await cloudBackend.updateAppDeployment({
@@ -74,6 +75,58 @@ export async function executeAppJob(job, cloudBackend, logger) {
74
75
  }
75
76
  }
76
77
  }
78
+ /**
79
+ * Resolve git credentials for cloning the app's source repository, reusing the
80
+ * SAME hereya GitHub-App credential mechanism used for package/project clones.
81
+ *
82
+ * We build a synthetic git env map (`hereyaGitRemoteUrl` + a `github-app:`
83
+ * marker for the password) and run it through `executor.resolveEnvValues`,
84
+ * which:
85
+ * 1. reads the GitHub-App config (`hereyaGithubAppId`,
86
+ * `hereyaGithubAppPrivateKey`, optional `hereyaGithubAppInstallationId`)
87
+ * from the workspace env, and
88
+ * 2. mints an installation token scoped to the repo named by
89
+ * `hereyaGitRemoteUrl`.
90
+ *
91
+ * If the GitHub-App config is not available (token not minted, marker left
92
+ * unresolved), we return `undefined` so the caller falls back to an
93
+ * unauthenticated clone (public-repo path preserved).
94
+ */
95
+ async function resolveAppRepoCredentials(input) {
96
+ const { appName, executor, logger, repository, workspace } = input;
97
+ try {
98
+ const gitEnvToResolve = {
99
+ // Empty installation id => resolveGithubAppMarkers falls back to
100
+ // hereyaGithubAppInstallationId from the workspace env.
101
+ hereyaGitPassword: 'github-app:',
102
+ // Peer URL used to scope the minted installation token to this repo.
103
+ hereyaGitRemoteUrl: repository,
104
+ hereyaGitUsername: 'x-access-token',
105
+ };
106
+ // For apps, workspace env is keyed by the app name (see LocalExecutor.getWorkspaceEnv).
107
+ const resolved = await executor.resolveEnvValues({
108
+ env: gitEnvToResolve,
109
+ project: appName,
110
+ workspace,
111
+ });
112
+ const password = resolved.hereyaGitPassword;
113
+ // If the marker was left unresolved (no GitHub-App config available), the
114
+ // value still starts with `github-app:` — treat that as "no auth".
115
+ if (!password || password.startsWith('github-app:')) {
116
+ logger.debug?.('No hereya GitHub-App credentials resolved for app source; falling back to unauthenticated clone');
117
+ return undefined;
118
+ }
119
+ return {
120
+ hereyaBinPath: process.argv[1],
121
+ password,
122
+ username: resolved.hereyaGitUsername || 'x-access-token',
123
+ };
124
+ }
125
+ catch (error) {
126
+ logger.debug?.(`Failed to resolve hereya GitHub-App credentials for app source (${error?.message ?? error}); falling back to unauthenticated clone`);
127
+ return undefined;
128
+ }
129
+ }
77
130
  async function runAppSubcommand(input) {
78
131
  const { appName, logger, paramFlags, rootDir, subcommand, versionFlags, workspace } = input;
79
132
  let runResult;
@@ -90,7 +90,7 @@ export async function dispatchJob(job, executor, cloudBackend, hooks) {
90
90
  return;
91
91
  }
92
92
  if (job.type === 'app-deploy' || job.type === 'app-destroy') {
93
- const appJobResult = await executeAppJob(job, cloudBackend, logger);
93
+ const appJobResult = await executeAppJob(job, cloudBackend, logger, executor);
94
94
  await finalize(appJobResult, job.type);
95
95
  return;
96
96
  }