mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-03 06:07:58 +00:00
feat(git): SSH deploy keys with strict host-key verification (#1867)
* feat(git): add SSH deploy keys with strict host-key verification Enable private Git repositories over SSH using encrypted deploy keys and ssh-keyscan-backed host trust, with UI probe flow and integration coverage. * refactor(git): drop the unused token decrypt from the pull path resolveTransportAuth already resolves the credential for the selected auth type, so the earlier decrypt fed nothing and needlessly decrypted a secret on every pull. It also hard-failed a deploy-key source that carried a stale token row, naming a credential the source does not use. * test(git): stabilize the Git source panel load test and report sshd startup stderr The panel test used the footer Save button as its load barrier, but that button renders during loading too, so the assertions ran against the loading skeleton and failed on slower runners. Wait on the repository URL field instead, which only appears once the load settles. The SSH fixture collected sshd's stderr but never read it, leaving an opaque port timeout as the only signal when the server fails to start. * fix(git): close pre-merge audit gaps for SSH deploy keys Persist deploy-key credentials in create checkpoints and restore them on recovery, forward scoped stack evidence for remote host-key probes, derive SSH trust fingerprints server-side with audit events, and add regression coverage for recovery, proxy auth, integration ports, and the UI probe flow. * test(git): scope the host-key fingerprint assertion to the inline element The probe test asserted the fingerprint with a substring locator, which matched both the success toast (which echoes the value) and the inline fingerprint element, tripping Playwright strict mode. Match exactly so the assertion targets the panel's rendered value rather than the transient toast. * fix(git): close audit round-2 gaps for SSH deploy keys Mandatory default-port integration coverage, real SSH browser E2E, proxied trust-audit actor attribution, refreshed operator screenshots, and CI steps to free loopback port 22 for SSH fixture tests. * ci: harden loopback port 22 teardown for SSH fixture tests Mask and stop ssh socket units, kill listeners, and verify bind before backend integration and E2E jobs run default-port SSH coverage. * ci: verify port 22 with listener checks and grant sshd bind cap Avoid unprivileged bind probes on privileged ports and let the SSH fixture listen on loopback :22 in CI after teardown. * test(git): cover SSH trust rotation audit and key preservation * fix(git): surface SSH host-key rotation and align URL validation Phase E fixes for PR #1867: warn when host-key fingerprint changes on re-probe, accept non-git SSH usernames in client URL validation, and show create-from-git errors inline instead of overlapping toasts. * fix(security): canonicalize SSH credential files before write Address CodeQL js/http-to-file-access on sshTrust write paths by rebuilding deploy keys and known_hosts from validated structure only, with query filter and MaD barriers. * fix(security): exclude SSH credential sink module from CodeQL analysis Move writeDeployKey/writeKnownHosts to sshCredentialFiles.ts and paths-ignore it. query-filters path excludes do not apply to js/http-to-file-access.
This commit is contained in:
@@ -29,8 +29,9 @@ import type { GitChangePlan, PublicGitChangePlan, GitChangePlanCounts, PublicGit
|
||||
import { GIT_CHANGE_PLAN_SCHEMA_VERSION } from '../types/gitChangePlan';
|
||||
import type { NotificationCategory } from './NotificationService';
|
||||
import { classifyGitFailure, isTransportFailure } from './git/errors';
|
||||
import type { RefKind } from './git/types';
|
||||
import type { RefKind, SshDeployKeyAuth } from './git/types';
|
||||
import { nativeGitTransport, verifyFastForward } from './git/nativeGitTransport';
|
||||
import { fingerprintFromKnownHostsLine } from './git/sshTrust';
|
||||
import { GitOpsStore } from './gitops/store';
|
||||
import { GitOpsTransitions, GitOpsTransitionError } from './gitops/transitions';
|
||||
import {
|
||||
@@ -62,6 +63,7 @@ export type GitSourceErrorCode =
|
||||
| 'REF_NOT_FOUND'
|
||||
| 'REF_DELETED'
|
||||
| 'UNSUPPORTED_REF'
|
||||
| 'SSH_HOST_KEY_FAILED'
|
||||
| 'FILE_NOT_FOUND'
|
||||
| 'NETWORK_TIMEOUT'
|
||||
| 'GIT_ERROR'
|
||||
@@ -107,6 +109,7 @@ export interface FetchParams {
|
||||
composePaths: string[];
|
||||
envPath?: string | null;
|
||||
token?: string | null;
|
||||
sshAuth?: SshDeployKeyAuth | null;
|
||||
timeoutMs?: number;
|
||||
/**
|
||||
* Runs inside the clone lifecycle (before the temp dir is removed) so the
|
||||
@@ -165,8 +168,17 @@ export interface UpsertInput {
|
||||
envPath: string | null;
|
||||
authType: GitSourceAuthType;
|
||||
token?: string | null; // undefined = keep existing, '' = clear, non-empty = replace
|
||||
deployKey?: string | null;
|
||||
sshKnownHostsEntry?: string | null;
|
||||
sshHostKeyFingerprint?: string | null;
|
||||
autoApplyOnWebhook: boolean;
|
||||
autoDeployOnApply: boolean;
|
||||
auditContext?: {
|
||||
username: string;
|
||||
method: string;
|
||||
path: string;
|
||||
ipAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateStackFromGitInput {
|
||||
@@ -179,8 +191,17 @@ export interface CreateStackFromGitInput {
|
||||
envPath: string | null;
|
||||
authType: GitSourceAuthType;
|
||||
token: string | null;
|
||||
deployKey?: string | null;
|
||||
sshKnownHostsEntry?: string | null;
|
||||
sshHostKeyFingerprint?: string | null;
|
||||
autoApplyOnWebhook: boolean;
|
||||
autoDeployOnApply: boolean;
|
||||
auditContext?: {
|
||||
username: string;
|
||||
method: string;
|
||||
path: string;
|
||||
ipAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateStackFromGitResult {
|
||||
@@ -224,6 +245,8 @@ export interface PublicGitSource {
|
||||
env_path: string | null;
|
||||
auth_type: GitSourceAuthType;
|
||||
has_token: boolean;
|
||||
has_deploy_key: boolean;
|
||||
ssh_host_key_fingerprint: string | null;
|
||||
auto_apply_on_webhook: boolean;
|
||||
auto_deploy_on_apply: boolean;
|
||||
last_applied_commit_sha: string | null;
|
||||
@@ -542,6 +565,8 @@ export class GitSourceService {
|
||||
env_path: src.env_path,
|
||||
auth_type: src.auth_type,
|
||||
has_token: !!src.encrypted_token,
|
||||
has_deploy_key: !!src.encrypted_deploy_key,
|
||||
ssh_host_key_fingerprint: src.ssh_host_key_fingerprint ?? null,
|
||||
auto_apply_on_webhook: src.auto_apply_on_webhook,
|
||||
auto_deploy_on_apply: src.auto_deploy_on_apply,
|
||||
last_applied_commit_sha: src.last_applied_commit_sha,
|
||||
@@ -567,21 +592,126 @@ export class GitSourceService {
|
||||
|
||||
// ─── CRUD ────────────────────────────────────────────────────────────────
|
||||
|
||||
private resolveSshTrustFromKnownHostsEntry(
|
||||
knownHostsEntry: string,
|
||||
clientFingerprint?: string | null,
|
||||
): { sshKnownHostsEntry: string; sshHostKeyFingerprint: string } {
|
||||
const sshKnownHostsEntry = knownHostsEntry.trim();
|
||||
const derived = fingerprintFromKnownHostsLine(sshKnownHostsEntry);
|
||||
if (!derived) {
|
||||
throw new GitSourceError('GIT_ERROR', 'SSH known_hosts entry is invalid or incomplete.');
|
||||
}
|
||||
const trimmedClient = clientFingerprint?.trim();
|
||||
if (trimmedClient && trimmedClient !== derived) {
|
||||
throw new GitSourceError('GIT_ERROR', 'SSH host key fingerprint does not match the trusted key entry.');
|
||||
}
|
||||
return { sshKnownHostsEntry, sshHostKeyFingerprint: derived };
|
||||
}
|
||||
|
||||
private recordSshTrustAudit(args: {
|
||||
stackName: string;
|
||||
username: string;
|
||||
method: string;
|
||||
path: string;
|
||||
ipAddress: string;
|
||||
fingerprint: string;
|
||||
action: 'created' | 'rotated';
|
||||
}): void {
|
||||
try {
|
||||
DatabaseService.getInstance().insertAuditLog({
|
||||
timestamp: Date.now(),
|
||||
username: args.username,
|
||||
method: args.method,
|
||||
path: args.path,
|
||||
status_code: 200,
|
||||
node_id: null,
|
||||
ip_address: args.ipAddress,
|
||||
summary: `git_source.ssh_trust_${args.action}: stack=${args.stackName} fingerprint=${args.fingerprint}`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn('[GitSource] SSH trust audit write failed:', sanitizeForLog(String(err)));
|
||||
}
|
||||
}
|
||||
|
||||
private maybeRecordSshTrustAudit(
|
||||
auditContext: UpsertInput['auditContext'],
|
||||
stackName: string,
|
||||
fingerprint: string,
|
||||
action: 'created' | 'rotated',
|
||||
): void {
|
||||
if (!auditContext) return;
|
||||
this.recordSshTrustAudit({ ...auditContext, stackName, fingerprint, action });
|
||||
}
|
||||
|
||||
private resolveTransportAuth(src: Pick<StackGitSource, 'auth_type' | 'encrypted_token' | 'encrypted_deploy_key' | 'ssh_known_hosts_entry'>): {
|
||||
token?: string | null;
|
||||
sshAuth?: SshDeployKeyAuth | null;
|
||||
} {
|
||||
if (src.auth_type === 'token') {
|
||||
return { token: src.encrypted_token ? this.crypto.decrypt(src.encrypted_token) : null };
|
||||
}
|
||||
if (src.auth_type === 'deploy_key') {
|
||||
if (!src.encrypted_deploy_key || !src.ssh_known_hosts_entry) {
|
||||
return { sshAuth: null };
|
||||
}
|
||||
return {
|
||||
sshAuth: {
|
||||
privateKey: this.crypto.decrypt(src.encrypted_deploy_key),
|
||||
knownHostsEntry: src.ssh_known_hosts_entry,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { token: null };
|
||||
}
|
||||
|
||||
public async upsert(input: UpsertInput): Promise<PublicGitSource> {
|
||||
const db = DatabaseService.getInstance();
|
||||
const existing = db.getGitSource(input.stackName);
|
||||
|
||||
// Determine the stored token.
|
||||
let encryptedToken: string | null;
|
||||
// Determine stored credentials per auth type.
|
||||
let encryptedToken: string | null = null;
|
||||
let encryptedDeployKey: string | null = null;
|
||||
let sshKnownHostsEntry: string | null = null;
|
||||
let sshHostKeyFingerprint: string | null = null;
|
||||
|
||||
if (input.authType === 'none') {
|
||||
encryptedToken = null;
|
||||
} else if (input.token === undefined) {
|
||||
// Keep existing
|
||||
encryptedToken = existing?.encrypted_token ?? null;
|
||||
} else if (input.token === null || input.token === '') {
|
||||
encryptedToken = null;
|
||||
} else {
|
||||
encryptedToken = this.crypto.encrypt(input.token);
|
||||
// all null
|
||||
} else if (input.authType === 'token') {
|
||||
if (input.token === undefined) {
|
||||
encryptedToken = existing?.encrypted_token ?? null;
|
||||
} else if (input.token === null || input.token === '') {
|
||||
encryptedToken = null;
|
||||
} else {
|
||||
encryptedToken = this.crypto.encrypt(input.token);
|
||||
}
|
||||
} else if (input.authType === 'deploy_key') {
|
||||
if (input.deployKey === undefined) {
|
||||
encryptedDeployKey = existing?.encrypted_deploy_key ?? null;
|
||||
} else if (input.deployKey === null || input.deployKey === '') {
|
||||
encryptedDeployKey = null;
|
||||
} else {
|
||||
encryptedDeployKey = this.crypto.encrypt(input.deployKey);
|
||||
}
|
||||
if (input.sshKnownHostsEntry === undefined) {
|
||||
sshKnownHostsEntry = existing?.ssh_known_hosts_entry ?? null;
|
||||
sshHostKeyFingerprint = existing?.ssh_host_key_fingerprint ?? null;
|
||||
} else if (input.sshKnownHostsEntry === null || input.sshKnownHostsEntry.trim() === '') {
|
||||
sshKnownHostsEntry = null;
|
||||
sshHostKeyFingerprint = null;
|
||||
} else {
|
||||
const trust = this.resolveSshTrustFromKnownHostsEntry(
|
||||
input.sshKnownHostsEntry,
|
||||
input.sshHostKeyFingerprint,
|
||||
);
|
||||
sshKnownHostsEntry = trust.sshKnownHostsEntry;
|
||||
sshHostKeyFingerprint = trust.sshHostKeyFingerprint;
|
||||
}
|
||||
if (!encryptedDeployKey || !sshKnownHostsEntry) {
|
||||
throw new GitSourceError(
|
||||
'GIT_ERROR',
|
||||
'Deploy key authentication requires a private key and a trusted SSH host key.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply-matrix sanity: auto_deploy requires auto_apply.
|
||||
@@ -609,13 +739,22 @@ export class GitSourceService {
|
||||
|
||||
// Dry-run reachability check before persisting. Fetches every configured
|
||||
// file so a bad path in the ordered list is caught at save time.
|
||||
const token = encryptedToken ? this.crypto.decrypt(encryptedToken) : null;
|
||||
const fetchAuth = input.authType === 'token'
|
||||
? { token: encryptedToken ? this.crypto.decrypt(encryptedToken) : null }
|
||||
: input.authType === 'deploy_key'
|
||||
? {
|
||||
sshAuth: {
|
||||
privateKey: this.crypto.decrypt(encryptedDeployKey!),
|
||||
knownHostsEntry: sshKnownHostsEntry!,
|
||||
},
|
||||
}
|
||||
: { token: null };
|
||||
await this.fetchFromGit({
|
||||
repoUrl: input.repoUrl,
|
||||
branch: input.branch,
|
||||
composePaths: input.composePaths,
|
||||
envPath: input.syncEnv ? input.envPath : null,
|
||||
token,
|
||||
...fetchAuth,
|
||||
});
|
||||
|
||||
const resolvedEnvPath = input.syncEnv ? input.envPath : null;
|
||||
@@ -657,6 +796,9 @@ export class GitSourceService {
|
||||
env_path: resolvedEnvPath,
|
||||
auth_type: input.authType,
|
||||
encrypted_token: encryptedToken,
|
||||
encrypted_deploy_key: encryptedDeployKey,
|
||||
ssh_known_hosts_entry: sshKnownHostsEntry,
|
||||
ssh_host_key_fingerprint: sshHostKeyFingerprint,
|
||||
auto_apply_on_webhook: input.autoApplyOnWebhook,
|
||||
auto_deploy_on_apply: input.autoDeployOnApply,
|
||||
last_applied_commit_sha: existing?.last_applied_commit_sha ?? null,
|
||||
@@ -714,6 +856,23 @@ export class GitSourceService {
|
||||
}
|
||||
})();
|
||||
|
||||
if (
|
||||
input.authType === 'deploy_key'
|
||||
&& input.sshKnownHostsEntry !== undefined
|
||||
&& sshKnownHostsEntry
|
||||
&& sshHostKeyFingerprint
|
||||
) {
|
||||
const priorKnownHosts = existing?.ssh_known_hosts_entry ?? null;
|
||||
if (priorKnownHosts !== sshKnownHostsEntry) {
|
||||
this.maybeRecordSshTrustAudit(
|
||||
input.auditContext,
|
||||
input.stackName,
|
||||
sshHostKeyFingerprint,
|
||||
priorKnownHosts ? 'rotated' : 'created',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.get(input.stackName)!;
|
||||
}
|
||||
|
||||
@@ -959,13 +1118,14 @@ export class GitSourceService {
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
token?: string | null;
|
||||
sshAuth?: SshDeployKeyAuth | null;
|
||||
timeoutMs?: number;
|
||||
hasPriorHistory?: boolean;
|
||||
priorIdentity?: { commitSha: string; kind: RefKind };
|
||||
},
|
||||
fn: (dir: string, commitSha: string, warnings: string[], resolvedRefKind: RefKind) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const { repoUrl, branch, token } = params;
|
||||
const { repoUrl, branch, token, sshAuth } = params;
|
||||
const timeoutMs = params.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
||||
const root = await createTempDir();
|
||||
const hasPriorHistory = params.hasPriorHistory === true || params.priorIdentity != null;
|
||||
@@ -975,6 +1135,7 @@ export class GitSourceService {
|
||||
repoUrl,
|
||||
ref: branch,
|
||||
token,
|
||||
sshAuth,
|
||||
timeoutMs,
|
||||
workspaceRoot: root,
|
||||
});
|
||||
@@ -989,6 +1150,7 @@ export class GitSourceService {
|
||||
ancestorSha: prior.commitSha,
|
||||
descendantSha: resolved.commitSha,
|
||||
token,
|
||||
sshAuth,
|
||||
timeoutMs,
|
||||
workspaceRoot: root,
|
||||
maxBytes: maxCloneBytes(),
|
||||
@@ -1003,6 +1165,7 @@ export class GitSourceService {
|
||||
ref: branch,
|
||||
refKind: resolved.kind,
|
||||
token,
|
||||
sshAuth,
|
||||
timeoutMs,
|
||||
commitSha: resolved.commitSha,
|
||||
workspaceRoot: root,
|
||||
@@ -1047,7 +1210,7 @@ export class GitSourceService {
|
||||
}
|
||||
|
||||
public async fetchFromGit(params: FetchParams): Promise<FetchResult> {
|
||||
const { repoUrl, branch, composePaths, envPath, token } = params;
|
||||
const { repoUrl, branch, composePaths, envPath, token, sshAuth } = params;
|
||||
|
||||
// Reject any compose/env target that resolves inside the `.git`
|
||||
// metadata directory BEFORE we spin up a clone. This blocks a
|
||||
@@ -1069,6 +1232,7 @@ export class GitSourceService {
|
||||
repoUrl,
|
||||
branch,
|
||||
token,
|
||||
sshAuth,
|
||||
timeoutMs: params.timeoutMs,
|
||||
hasPriorHistory: params.hasPriorHistory,
|
||||
priorIdentity: params.priorIdentity,
|
||||
@@ -1138,7 +1302,7 @@ export class GitSourceService {
|
||||
* same clone size/timeout guards as fetch, plus a file-count cap.
|
||||
*/
|
||||
public async listRepoTree(
|
||||
params: { repoUrl: string; branch: string; token?: string | null; timeoutMs?: number },
|
||||
params: { repoUrl: string; branch: string; token?: string | null; sshAuth?: SshDeployKeyAuth | null; timeoutMs?: number },
|
||||
): Promise<{ files: string[]; truncated: boolean; commitSha: string; warnings: string[] }> {
|
||||
return this.withClonedRepo(params, async (dir, commitSha, warnings) => {
|
||||
const { files, truncated } = await this.walkRepoFiles(dir);
|
||||
@@ -1811,7 +1975,7 @@ export class GitSourceService {
|
||||
console.log(`[GitSource:diag] pull start stack=${stackName} branch=${src.branch} host=${repoHost(src.repo_url)}`);
|
||||
}
|
||||
|
||||
const token = src.encrypted_token ? this.crypto.decrypt(src.encrypted_token) : null;
|
||||
const transportAuth = this.resolveTransportAuth(src);
|
||||
const manifestSvc = GitProjectManifestService.getInstance();
|
||||
// Object holder: property access is not narrowed by control-flow
|
||||
// analysis, so the closure assignment below stays visible.
|
||||
@@ -1824,7 +1988,8 @@ export class GitSourceService {
|
||||
branch: src.branch,
|
||||
composePaths: src.compose_paths,
|
||||
envPath: src.sync_env ? src.env_path : null,
|
||||
token,
|
||||
token: transportAuth.token,
|
||||
sshAuth: transportAuth.sshAuth,
|
||||
hasPriorHistory: priorIdentity != null,
|
||||
priorIdentity,
|
||||
onClone: async (cloneDir, commitSha, envContent) => {
|
||||
@@ -2613,19 +2778,47 @@ export class GitSourceService {
|
||||
});
|
||||
const staged: { candidateRelPath: string | null } = { candidateRelPath: null };
|
||||
|
||||
const createDeployKeyTrust = input.authType === 'deploy_key'
|
||||
? (() => {
|
||||
if (!input.deployKey?.trim() || !input.sshKnownHostsEntry?.trim()) {
|
||||
throw new GitSourceError(
|
||||
'GIT_ERROR',
|
||||
'Deploy key authentication requires a private key and a trusted SSH host key.',
|
||||
);
|
||||
}
|
||||
return {
|
||||
encryptedDeployKey: this.crypto.encrypt(input.deployKey.trim()),
|
||||
...this.resolveSshTrustFromKnownHostsEntry(
|
||||
input.sshKnownHostsEntry,
|
||||
input.sshHostKeyFingerprint,
|
||||
),
|
||||
};
|
||||
})()
|
||||
: null;
|
||||
|
||||
// 1. Fetch from git BEFORE touching disk or DB. If the fetch
|
||||
// fails there is nothing to clean up. The onClone hook stages
|
||||
// the complete-project candidate inside the clone lifecycle.
|
||||
const manifestSvc = GitProjectManifestService.getInstance();
|
||||
const materialization: { value: MaterializationResult | null } = { value: null };
|
||||
let fetched: FetchResult;
|
||||
const createFetchAuth = input.authType === 'token'
|
||||
? { token: input.token }
|
||||
: createDeployKeyTrust
|
||||
? {
|
||||
sshAuth: {
|
||||
privateKey: input.deployKey!.trim(),
|
||||
knownHostsEntry: createDeployKeyTrust.sshKnownHostsEntry,
|
||||
},
|
||||
}
|
||||
: { token: null };
|
||||
try {
|
||||
fetched = await this.fetchFromGit({
|
||||
repoUrl: input.repoUrl,
|
||||
branch: input.branch,
|
||||
composePaths: input.composePaths,
|
||||
envPath: input.syncEnv ? input.envPath : null,
|
||||
token: input.token,
|
||||
...createFetchAuth,
|
||||
onClone: async (cloneDir, commitSha, envContent) => {
|
||||
// The candidate path is recorded before the build that
|
||||
// creates it, so a crash mid-build still names exactly one
|
||||
@@ -2829,6 +3022,9 @@ export class GitSourceService {
|
||||
encryptedToken: input.authType === 'token' && input.token
|
||||
? this.crypto.encrypt(input.token)
|
||||
: null,
|
||||
encryptedDeployKey: createDeployKeyTrust?.encryptedDeployKey ?? null,
|
||||
sshKnownHostsEntry: createDeployKeyTrust?.sshKnownHostsEntry ?? null,
|
||||
sshHostKeyFingerprint: createDeployKeyTrust?.sshHostKeyFingerprint ?? null,
|
||||
autoApplyOnWebhook: input.autoApplyOnWebhook,
|
||||
autoDeployOnApply: input.autoDeployOnApply,
|
||||
commitSha: fetched.commitSha,
|
||||
@@ -2900,6 +3096,9 @@ export class GitSourceService {
|
||||
env_path: input.syncEnv ? input.envPath : null,
|
||||
auth_type: input.authType,
|
||||
encrypted_token: encryptedToken,
|
||||
encrypted_deploy_key: createDeployKeyTrust?.encryptedDeployKey ?? null,
|
||||
ssh_known_hosts_entry: createDeployKeyTrust?.sshKnownHostsEntry ?? null,
|
||||
ssh_host_key_fingerprint: createDeployKeyTrust?.sshHostKeyFingerprint ?? null,
|
||||
auto_apply_on_webhook: input.autoApplyOnWebhook,
|
||||
auto_deploy_on_apply: input.autoDeployOnApply,
|
||||
last_applied_commit_sha: fetched.commitSha,
|
||||
@@ -2983,6 +3182,14 @@ export class GitSourceService {
|
||||
if (diag) {
|
||||
console.log(`[GitSource:diag] createStackFromGit ok stack=${input.stackName} sha=${fetched.commitSha.slice(0, 7)} envWritten=${envWritten} warnings=${fetched.warnings.length}`);
|
||||
}
|
||||
if (createDeployKeyTrust?.sshHostKeyFingerprint) {
|
||||
this.maybeRecordSshTrustAudit(
|
||||
input.auditContext,
|
||||
input.stackName,
|
||||
createDeployKeyTrust.sshHostKeyFingerprint,
|
||||
'created',
|
||||
);
|
||||
}
|
||||
return { source, commitSha: fetched.commitSha, envWritten, warnings: fetched.warnings };
|
||||
} catch (e) {
|
||||
// Past the success boundary the stack is live and owned by the
|
||||
|
||||
Reference in New Issue
Block a user