Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
776c67a
feat(sandbox): pre-warm deno cache from S3 for deco site repos
igoramf Jun 25, 2026
d4e5554
fix(sandbox): require explicit S3 region and bucket for deco site cache
igoramf Jun 25, 2026
ebf9318
refactor(sandbox): make deno cache warm-up generic and S3-compatible
igoramf Jun 25, 2026
4b5b4ae
feat(sandbox): add pathPrefix and pathFile config for deno cache path
igoramf Jun 25, 2026
c87bf78
refactor(sandbox): rename DENO_CACHE_S3_* to DECO_CACHE_S3_* and auto…
igoramf Jun 25, 2026
106b180
refactor(sandbox): remove template-based cache path derivation from d…
igoramf Jun 25, 2026
ba62f34
refactor(sandbox): remove static path from decoCache Helm values
igoramf Jun 25, 2026
862bf02
refactor(sandbox): rename accessKeyIdKey/secretAccessKeyKey to *Ref i…
igoramf Jun 25, 2026
e883595
feat(sandbox): add Helm validation for decoCache region and bucket
igoramf Jun 25, 2026
ecc79b2
feat(sandbox): add ExternalSecret support for decoCache S3 credentials
igoramf Jun 25, 2026
24dd216
refactor(sandbox): remove hard validation for decoCache region and bu…
igoramf Jun 25, 2026
df20fc2
refactor(sandbox): reuse decoCache.region for ExternalSecret SecretStore
igoramf Jun 25, 2026
647fb74
fix(sandbox): restore separate provider.aws.region for ExternalSecret
igoramf Jun 25, 2026
438aa1a
refactor(sandbox): move deco cache S3 credentials to mesh settings
igoramf Jun 25, 2026
94ddec9
style(sandbox): remove extra blank lines in helpers.tpl and values.yaml
igoramf Jun 25, 2026
ef7978c
refactor(sandbox): move deco-site detection entirely into the daemon
igoramf Jun 25, 2026
3a27d64
refactor(sandbox): simplify deco-site detection to deco-sites org only
igoramf Jun 25, 2026
4cadb6a
revert(sandbox): remove extraEnv from resolveAndPushEnv
igoramf Jun 26, 2026
8962f0f
style(sandbox): remove extra blank line in start.ts
igoramf Jun 26, 2026
0b84bde
chore: remove accidental values-local-dev.yaml from PR
igoramf Jun 26, 2026
8345ffa
refactor(sandbox): replace regex with URL parser in resolveDecoCachePath
igoramf Jun 26, 2026
7dfccfe
refactor(sandbox): pass repo owner explicitly to daemon instead of pa…
igoramf Jun 26, 2026
264ce1e
refactor(sandbox): rename owner to githubOwner in repo config fields
igoramf Jun 26, 2026
7df2a61
refactor(sandbox): simplify resolveDecoCachePath using repoName directly
igoramf Jun 26, 2026
ee13528
refactor(sandbox): rename githubOwner to owner in repo config fields
igoramf Jun 26, 2026
fc12789
fix(sandbox): add owner to repoOpts inline type in start.ts
igoramf Jun 26, 2026
e94d1d6
refactor(sandbox): pass repo name explicitly and build cache path as …
igoramf Jun 26, 2026
43f39a5
refactor(sandbox): drop name field, derive cache path from existing r…
igoramf Jun 26, 2026
7914ad3
refactor(sandbox): pass repo name explicitly to avoid coupling to dis…
igoramf Jun 26, 2026
5b53a0b
feat(sandbox): use AWS SDK + Pod Identity for deco cache S3 download
igoramf Jun 26, 2026
7df9486
feat(sandbox-env): add serviceAccountName value for Pod Identity support
igoramf Jun 26, 2026
2362872
refactor(sandbox): switch deco cache warm-up to pre-signed URL approach
igoramf Jun 26, 2026
6591ab3
refactor(sandbox): remove decoCacheS3Endpoint, use AWS default endpoint
igoramf Jun 26, 2026
a930e64
refactor(sandbox): use AssumeRole to generate deco cache pre-signed URL
igoramf Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/mesh/src/settings/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ export function resolveConfig(
envVars.AWS_S3_TENANT_PROVISIONER_ACCESS_KEY_ID,
awsS3TenantProvisionerSecretAccessKey:
envVars.AWS_S3_TENANT_PROVISIONER_SECRET_ACCESS_KEY,

decoCacheS3Bucket: envVars.DECO_CACHE_S3_BUCKET,
decoCacheS3Region: envVars.DECO_CACHE_S3_REGION,
};

return {
Expand Down
4 changes: 4 additions & 0 deletions apps/mesh/src/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ export interface Settings {
awsS3TenantRoleArn: string | undefined;
awsS3TenantProvisionerAccessKeyId: string | undefined;
awsS3TenantProvisionerSecretAccessKey: string | undefined;

// deco S3 cache — pre-signed URL generation for deco-sites sandbox warm-up.
decoCacheS3Bucket: string | undefined;
decoCacheS3Region: string | undefined;
}

export interface CliFlags {
Expand Down
59 changes: 59 additions & 0 deletions apps/mesh/src/tools/sandbox/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
* (user, branch) key — no stale-sandbox teardown is needed on kind change.
*/

import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { z } from "zod";
import type { SandboxRecord } from "@decocms/mesh-sdk";
import {
Expand Down Expand Up @@ -66,6 +69,53 @@ const sandboxProviderKindInputSchema = z.enum([
"cluster",
]);

async function tryGenerateDecoCachePresignedUrl(
owner: string,
repo: string,
): Promise<string | undefined> {
if (owner !== "deco-sites") return undefined;
const settings = getSettings();
const bucket = settings.decoCacheS3Bucket;
const region = settings.decoCacheS3Region;
const roleArn = settings.awsS3TenantRoleArn;
if (!bucket || !region || !roleArn) return undefined;
try {
const accessKeyId = settings.awsS3TenantProvisionerAccessKeyId;
const secretAccessKey = settings.awsS3TenantProvisionerSecretAccessKey;
const sts = new STSClient({
region,
...(accessKeyId && secretAccessKey
? { credentials: { accessKeyId, secretAccessKey } }
: {}),
});
const { Credentials: creds } = await sts.send(
new AssumeRoleCommand({
RoleArn: roleArn,
RoleSessionName: "deco-cache-presign",
}),
);
if (!creds?.AccessKeyId || !creds.SecretAccessKey) return undefined;
const s3 = new S3Client({
region,
credentials: {
accessKeyId: creds.AccessKeyId,
secretAccessKey: creds.SecretAccessKey,
sessionToken: creds.SessionToken,
},
});
return await getSignedUrl(
s3,
new GetObjectCommand({
Bucket: bucket,
Key: `${owner}/${repo}/cache.tar.zst`,
}),
{ expiresIn: 900 },
);
} catch {
return undefined;
}
}

export const SANDBOX_START = defineTool({
name: "SANDBOX_START",
description: "Start a sandbox with the connected GitHub repo and dev server.",
Expand Down Expand Up @@ -297,6 +347,9 @@ async function provisionSandbox(
userEmail: string;
branch: string;
displayName: string;
owner?: string;
name?: string;
denoCachePresignedUrl?: string;
}
| undefined;

Expand Down Expand Up @@ -376,6 +429,12 @@ async function provisionSandbox(
userEmail: gitUserEmail,
branch,
displayName: `${githubRepo.owner}/${githubRepo.name}`,
owner: githubRepo.owner,
name: githubRepo.name,
denoCachePresignedUrl: await tryGenerateDecoCachePresignedUrl(
githubRepo.owner,
githubRepo.name,
),
};
}

Expand Down
7 changes: 4 additions & 3 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions deploy/helm/sandbox-env/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ atomic and gives a pointer to the right install command.
{{- end }}
{{- end }}


{{- define "sandbox-env.housekeeperName" -}}
{{- printf "sandbox-housekeeper-%s" (include "sandbox-env.envName" .) -}}
{{- end }}
Expand Down
60 changes: 60 additions & 0 deletions packages/sandbox/daemon/setup/install.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,70 @@
import { existsSync } from "node:fs";
import { createWriteStream } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { pipeline } from "node:stream/promises";
import { PACKAGE_MANAGER_DAEMON_CONFIG } from "../constants";
import { resolvePmRoot } from "../paths";
import type { Config } from "../types";
import { spawnSetupStep } from "./spawn-step";

/**
* For Deno projects, tries to pre-populate $DENO_DIR from a pre-signed S3 URL
* provided by mesh. The URL is scoped to exactly one object (owner/repo/cache.tar.zst)
* and expires in 15 minutes — no AWS credentials needed in the sandbox pod.
*
* Non-fatal: any failure (URL expired, object not found, network error) is logged
* and the sandbox continues to start normally.
*/
export function tryWarmDenoCache(deps: InstallDeps): Promise<void> | null {
const { config } = deps;
if (config.application?.packageManager?.name !== "deno") return null;

const presignedUrl = config.denoCache?.presignedUrl;
if (!presignedUrl) return null;

return (async () => {
deps.onChunk(
"setup",
"\r\n[deno cache] fetching from pre-signed URL...\r\n",
);

let tmpDir: string | null = null;
try {
const res = await fetch(presignedUrl);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
if (!res.body) throw new Error("empty response body");

tmpDir = await mkdtemp(join(tmpdir(), "deco-cache-"));
const tmpFile = join(tmpDir, "cache.tar.zst");
const ws = createWriteStream(tmpFile);
await pipeline(res.body as unknown as NodeJS.ReadableStream, ws);

const denoDir =
deps.env?.DENO_DIR ?? process.env.DENO_DIR ?? join(homedir(), ".deno");
const cmd = [
`mkdir -p "${denoDir}"`,
`zstd -d "${tmpFile}" --stdout | tar xf - -C "${denoDir}" --no-same-permissions --no-same-owner`,
`echo "[deno cache] restored from S3"`,
].join(" && ");

await spawnSetupStep(cmd, deps.onChunk, {
dropPrivileges: deps.dropPrivileges,
env: deps.env,
});
} catch {
deps.onChunk(
"setup",
"[deno cache] not available — deno will fetch deps on first run\r\n",
);
} finally {
if (tmpDir)
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
}
})();
}

export interface InstallDeps {
config: Config;
dropPrivileges?: boolean;
Expand Down
13 changes: 12 additions & 1 deletion packages/sandbox/daemon/setup/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { Application, Config } from "../types";
import { autodetectApplication } from "./autodetect";
import { spawnClone } from "./clone";
import { configureGitIdentity } from "./identity";
import { spawnInstall } from "./install";
import { spawnInstall, tryWarmDenoCache } from "./install";
import { spawnSetupStep } from "./spawn-step";
import { installProtectedBranchHook } from "../git/protect-branch";

Expand Down Expand Up @@ -323,6 +323,17 @@ export class SetupOrchestrator {
// present yet). Treat as success so the caller proceeds to start; mark
// the install fingerprint so resume doesn't retry on every boot.
if (!installPromise) {
// For deno, opportunistically pre-populate the cache from S3.
// Non-fatal: failure just means a cold first run.
const cachePromise = tryWarmDenoCache({
config,
env: config.env,
onChunk: (_src, data) => {
this.rawChunk(data);
installTee.write(data);
},
});
if (cachePromise) await cachePromise;
installTee.close();
this.markInstallSucceeded(config);
return true;
Expand Down
5 changes: 5 additions & 0 deletions packages/sandbox/daemon/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface GitRepository {
readonly cloneUrl: string;
readonly branch?: string;
readonly repoName?: string;
/** GitHub org or user that owns the repo (e.g. "deco-sites"). */
readonly owner?: string;
/** Repository name without owner (e.g. "my-site"). */
readonly name?: string;
}

export interface GitConfig {
Expand Down Expand Up @@ -67,6 +71,7 @@ export interface TenantConfig {
readonly operator?: OperatorIdentity;
readonly application?: Application;
readonly env?: Readonly<Record<string, string>>;
readonly denoCache?: { readonly presignedUrl: string };
}

/** In-memory enriched view: TenantConfig + derivations. */
Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox/image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ARG DENO_VERSION=v1.46.3
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash ca-certificates curl git gnupg locales python3 python3-pip \
ripgrep unzip \
ripgrep unzip zstd \
&& sed -i 's/^#\s*en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
&& locale-gen \
&& curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \
Expand Down
1 change: 1 addition & 0 deletions packages/sandbox/server/provider/agent-sandbox/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,7 @@ export class AgentSandboxProvider implements SandboxProvider {
repo: opts?.repo ?? null,
port: opts?.workload?.devPort ?? DEFAULT_DEV_PORT,
tenant: opts?.tenant ?? undefined,
denoCachePresignedUrl: opts?.repo?.denoCachePresignedUrl,
});
}

Expand Down
10 changes: 9 additions & 1 deletion packages/sandbox/server/provider/shared/build-config-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export function buildConfigPayload(args: {
port?: number;
repo: NonNullable<EnsureOptions["repo"]> | null;
tenant?: EnsureOptions["tenant"];
denoCachePresignedUrl?: string;
}): Partial<TenantConfig> | null {
const repo = args.repo;
const git = repo
Expand All @@ -21,6 +22,8 @@ export function buildConfigPayload(args: {
cloneUrl: repo.cloneUrl,
repoName: repo.displayName ?? deriveRepoLabel(repo.cloneUrl),
...(repo.branch ? { branch: repo.branch } : {}),
...(repo.owner ? { owner: repo.owner } : {}),
...(repo.name ? { name: repo.name } : {}),
},
identity: {
userName: repo.userName,
Expand Down Expand Up @@ -51,11 +54,16 @@ export function buildConfigPayload(args: {
}
: undefined;

if (!git && !application && !operator) return null;
const denoCache = args.denoCachePresignedUrl
? { presignedUrl: args.denoCachePresignedUrl }
: undefined;

if (!git && !application && !operator && !denoCache) return null;
return {
...(git ? { git } : {}),
...(operator ? { operator } : {}),
...(application ? { application } : {}),
...(denoCache ? { denoCache } : {}),
};
}

Expand Down
6 changes: 6 additions & 0 deletions packages/sandbox/server/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export interface EnsureOptions {
branch?: string;
/** Human-readable label for logs/UI; no functional effect. */
displayName?: string;
/** GitHub org or user that owns the repo (e.g. "deco-sites"). */
owner?: string;
/** Pre-signed URL to download the Deno module cache from S3. */
denoCachePresignedUrl?: string;
/** Repository name without owner (e.g. "my-site"). */
name?: string;
};
/** Image override. Non-image runners MUST ignore. */
image?: string;
Expand Down