@deployfoundation/foundation-deploy 0.2.47 → 0.2.49
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/bin/app.js +2 -2
- package/dist/bin/foundation-deploy.js +40 -15
- package/dist/bin/release-account.js +5 -0
- package/dist/{chunk-yn8x77x6.js → chunk-6f2hkamj.js} +22 -2
- package/dist/{chunk-k6mcv6dk.js → chunk-gnz747z2.js} +16 -5
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/deploy/assets/slack-app-manifest.yml +4 -0
- package/src/deploy/cli.ts +25 -1
- package/src/deploy/config-sync.ts +15 -1
- package/src/deploy/deploy.ts +5 -3
- package/src/stacks/agent-stack.ts +14 -0
- package/src/stacks/api-stack.ts +11 -3
- package/src/stacks/release-account-stack.ts +7 -0
package/dist/bin/app.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
FoundationPages,
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-6f2hkamj.js";
|
|
12
12
|
import {
|
|
13
13
|
adminsFor,
|
|
14
14
|
instanceNames,
|
|
15
15
|
loadInstanceFile,
|
|
16
16
|
provisionsIntegration
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-gnz747z2.js";
|
|
18
18
|
import"../chunk-dz91avrb.js";
|
|
19
19
|
|
|
20
20
|
// bin/app.ts
|
|
@@ -22,12 +22,15 @@ import {
|
|
|
22
22
|
slackCommandPrefix,
|
|
23
23
|
verifyManifest,
|
|
24
24
|
versionedConfigKey
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-gnz747z2.js";
|
|
26
26
|
import {
|
|
27
27
|
DEFAULT_RELEASE_BUCKET,
|
|
28
28
|
expectedKeyArn
|
|
29
29
|
} from "../chunk-dz91avrb.js";
|
|
30
30
|
|
|
31
|
+
// src/deploy/cli.ts
|
|
32
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
33
|
+
|
|
31
34
|
// src/deploy/aws.ts
|
|
32
35
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
33
36
|
import { tmpdir } from "node:os";
|
|
@@ -282,7 +285,7 @@ async function callerAccountId(ctx) {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
// src/deploy/config-sync.ts
|
|
285
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readdirSync } from "node:fs";
|
|
288
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync } from "node:fs";
|
|
286
289
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
287
290
|
import { join as join3, resolve as resolve3 } from "node:path";
|
|
288
291
|
|
|
@@ -494,6 +497,11 @@ async function uploadRuntimeConfig(ctx, opts) {
|
|
|
494
497
|
if (opts.runtimeConfigPath === undefined && ctx.instance.customization !== undefined)
|
|
495
498
|
throw new Error("customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`");
|
|
496
499
|
const source = opts.runtimeConfigPath ?? ctx.paths.configPath;
|
|
500
|
+
try {
|
|
501
|
+
parseInstanceConfig(readFileSync3(source, "utf8"));
|
|
502
|
+
} catch (error) {
|
|
503
|
+
throw new Error(`config for ${opts.version} does not parse with this release's schema, so nothing was uploaded: ${error instanceof Error ? error.message : String(error)}`);
|
|
504
|
+
}
|
|
497
505
|
const key = versionedConfigKey(ctx.names.configKey, opts.version);
|
|
498
506
|
await awsMutate(ctx, ["s3", "cp", source, `s3://${opts.bucket}/${key}`]);
|
|
499
507
|
return key;
|
|
@@ -908,7 +916,7 @@ var UPWORK_GRAPHQL_DOCUMENTS = Object.freeze({
|
|
|
908
916
|
var ETRADE_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
909
917
|
var MAX_SECRET_JSON_LENGTH = 16 * 1024;
|
|
910
918
|
// src/deploy/config.ts
|
|
911
|
-
import { readFileSync as
|
|
919
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
912
920
|
import { parse } from "yaml";
|
|
913
921
|
function adminsCsv(yamlText, source = "runtime config") {
|
|
914
922
|
const doc = parse(yamlText);
|
|
@@ -922,7 +930,7 @@ function adminsCsv(yamlText, source = "runtime config") {
|
|
|
922
930
|
}).join(",");
|
|
923
931
|
}
|
|
924
932
|
function adminsCsvFromFile(path) {
|
|
925
|
-
return adminsCsv(
|
|
933
|
+
return adminsCsv(readFileSync4(path, "utf8"), path);
|
|
926
934
|
}
|
|
927
935
|
|
|
928
936
|
// src/deploy/endpoint.ts
|
|
@@ -1416,7 +1424,7 @@ function composeRuntimeSecret(inputs) {
|
|
|
1416
1424
|
import {
|
|
1417
1425
|
lstatSync,
|
|
1418
1426
|
mkdirSync as mkdirSync3,
|
|
1419
|
-
readFileSync as
|
|
1427
|
+
readFileSync as readFileSync5,
|
|
1420
1428
|
realpathSync,
|
|
1421
1429
|
rmSync as rmSync2,
|
|
1422
1430
|
statSync,
|
|
@@ -1536,9 +1544,9 @@ function stageCustomization(options) {
|
|
|
1536
1544
|
const sourceFile = resolvedCustomizationFile(sourceRoot, customization.path);
|
|
1537
1545
|
if (sourceFile === undefined)
|
|
1538
1546
|
return migrationFallback(customization.required, log, platformConfigPath);
|
|
1539
|
-
const yaml =
|
|
1547
|
+
const yaml = readFileSync5(sourceFile);
|
|
1540
1548
|
const tenantConfig = parseInstanceConfig(yaml.toString("utf8"));
|
|
1541
|
-
const platformConfig = parseInstanceConfig(
|
|
1549
|
+
const platformConfig = parseInstanceConfig(readFileSync5(platformConfigPath, "utf8"));
|
|
1542
1550
|
if (tenantConfig.name !== instance.displayName)
|
|
1543
1551
|
throw new Error("Customization config name must match the selected instance.");
|
|
1544
1552
|
assertValidTenantJobs(tenantConfig);
|
|
@@ -1895,7 +1903,7 @@ async function deploy(ctx, opts) {
|
|
|
1895
1903
|
import { createServer } from "node:http";
|
|
1896
1904
|
|
|
1897
1905
|
// src/deploy/github-app-manifest.ts
|
|
1898
|
-
import { readFileSync as
|
|
1906
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1899
1907
|
import { join as join6 } from "node:path";
|
|
1900
1908
|
import { parse as parseYaml } from "yaml";
|
|
1901
1909
|
var GITHUB_APP_MANIFEST_PATH = join6(PACKAGE_ASSETS, "github-app-manifest.yml");
|
|
@@ -1916,7 +1924,7 @@ function renderAppManifest(template, instance) {
|
|
|
1916
1924
|
});
|
|
1917
1925
|
}
|
|
1918
1926
|
function loadAppManifest(instance, template) {
|
|
1919
|
-
const text = template ??
|
|
1927
|
+
const text = template ?? readFileSync6(GITHUB_APP_MANIFEST_PATH, "utf8");
|
|
1920
1928
|
const manifest = parseYaml(renderAppManifest(text, instance));
|
|
1921
1929
|
if ("default_permissions" in manifest)
|
|
1922
1930
|
throw new Error("app-manifest.yml: default_permissions are generated from the capability registry; remove them from the template");
|
|
@@ -2034,7 +2042,7 @@ async function githubAppCreate(options) {
|
|
|
2034
2042
|
}
|
|
2035
2043
|
|
|
2036
2044
|
// src/deploy/job-key.ts
|
|
2037
|
-
import { readFileSync as
|
|
2045
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2038
2046
|
function jobWebhookOutputKey(jobId) {
|
|
2039
2047
|
return `JobWebhookUrl${jobId.replace(/[^A-Za-z0-9]/g, "")}`;
|
|
2040
2048
|
}
|
|
@@ -2042,7 +2050,7 @@ function jobsWebhookSecretName(secretPrefix) {
|
|
|
2042
2050
|
return `${secretPrefix}/jobs/webhook`;
|
|
2043
2051
|
}
|
|
2044
2052
|
async function jobKey(ctx, jobId, options = {}) {
|
|
2045
|
-
const config = parseInstanceConfig(
|
|
2053
|
+
const config = parseInstanceConfig(readFileSync7(ctx.paths.configPath, "utf8"));
|
|
2046
2054
|
const job = config.jobs.find((entry) => entry.id === jobId);
|
|
2047
2055
|
if (job === undefined)
|
|
2048
2056
|
throw new Error(`no job "${jobId}" in ${ctx.paths.configPath}`);
|
|
@@ -2108,11 +2116,11 @@ async function postDeploy(ctx, opts = {}) {
|
|
|
2108
2116
|
}
|
|
2109
2117
|
|
|
2110
2118
|
// src/deploy/setup.ts
|
|
2111
|
-
import { existsSync as existsSync5, readFileSync as
|
|
2119
|
+
import { existsSync as existsSync5, readFileSync as readFileSync9 } from "node:fs";
|
|
2112
2120
|
import { resolve as resolve5 } from "node:path";
|
|
2113
2121
|
|
|
2114
2122
|
// src/deploy/slack-manifest.ts
|
|
2115
|
-
import { readFileSync as
|
|
2123
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2116
2124
|
import { join as join7 } from "node:path";
|
|
2117
2125
|
import { parse as parse2 } from "yaml";
|
|
2118
2126
|
var SLACK_APP_MANIFEST_PATH = join7(PACKAGE_ASSETS, "slack-app-manifest.yml");
|
|
@@ -2151,7 +2159,7 @@ function buildSlackManifest(template, instance, env) {
|
|
|
2151
2159
|
return manifest;
|
|
2152
2160
|
}
|
|
2153
2161
|
function slackManifestFor(instance, env) {
|
|
2154
|
-
return buildSlackManifest(
|
|
2162
|
+
return buildSlackManifest(readFileSync8(SLACK_APP_MANIFEST_PATH, "utf8"), {
|
|
2155
2163
|
displayName: instance.displayName,
|
|
2156
2164
|
commandPrefix: slackCommandPrefix(instance),
|
|
2157
2165
|
pages: instance.integrations.pages
|
|
@@ -2377,7 +2385,7 @@ async function writeRuntimeSecret(ctx) {
|
|
|
2377
2385
|
async function seedCodex(ctx, codexFile) {
|
|
2378
2386
|
if (!existsSync5(codexFile))
|
|
2379
2387
|
throw new Error(`${codexFile} not found — sign in locally first, or pass --codex-file`);
|
|
2380
|
-
const document =
|
|
2388
|
+
const document = readFileSync9(codexFile, "utf8");
|
|
2381
2389
|
JSON.parse(document);
|
|
2382
2390
|
await putSecretString(ctx, ctx.names.secretCodex, document);
|
|
2383
2391
|
console.log(` ${ctx.names.secretCodex} seeded from ${codexFile}`);
|
|
@@ -2555,6 +2563,7 @@ Done. Now say \`@${ctx.instance.displayName} hello\` in Slack.`);
|
|
|
2555
2563
|
|
|
2556
2564
|
// src/deploy/cli.ts
|
|
2557
2565
|
var COMMANDS = [
|
|
2566
|
+
"check",
|
|
2558
2567
|
"deploy",
|
|
2559
2568
|
"post-deploy",
|
|
2560
2569
|
"config:sync",
|
|
@@ -2567,6 +2576,10 @@ var COMMANDS = [
|
|
|
2567
2576
|
var USAGE = `usage: foundation-deploy <command> --instance <path> [options]
|
|
2568
2577
|
|
|
2569
2578
|
commands
|
|
2579
|
+
check parse the instance and config files with THIS release's
|
|
2580
|
+
schema; no AWS. Run it on config PRs with the pinned
|
|
2581
|
+
version: a config the deployed release rejects puts the
|
|
2582
|
+
teammate on the fallback config.
|
|
2570
2583
|
deploy build and push the agent image, cdk deploy --all, refresh
|
|
2571
2584
|
the runtime secret, sync config
|
|
2572
2585
|
post-deploy smoke DEFAULT, probe the mount, promote \`live\`, smoke \`live\`
|
|
@@ -2645,6 +2658,18 @@ async function main(argv2 = process.argv.slice(2)) {
|
|
|
2645
2658
|
${USAGE}`);
|
|
2646
2659
|
return 1;
|
|
2647
2660
|
}
|
|
2661
|
+
if (command === "check") {
|
|
2662
|
+
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
2663
|
+
try {
|
|
2664
|
+
parseInstanceConfig(readFileSync10(paths.configPath, "utf8"));
|
|
2665
|
+
} catch (error) {
|
|
2666
|
+
console.error(`${paths.configPath} does not parse with this release (v${toolVersion()}):
|
|
2667
|
+
${error instanceof Error ? error.message : String(error)}`);
|
|
2668
|
+
return 1;
|
|
2669
|
+
}
|
|
2670
|
+
console.log(`${paths.configPath} parses with this release (v${toolVersion()})`);
|
|
2671
|
+
return 0;
|
|
2672
|
+
}
|
|
2648
2673
|
if (command === "slack-manifest") {
|
|
2649
2674
|
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
2650
2675
|
if (process.env.EVENTS_REQUEST_URL === undefined)
|
|
@@ -111,6 +111,11 @@ class ReleaseAccountStack extends cdk.Stack {
|
|
|
111
111
|
actions: ["ecr:GetAuthorizationToken"],
|
|
112
112
|
resources: ["*"]
|
|
113
113
|
}),
|
|
114
|
+
new iam.PolicyStatement({
|
|
115
|
+
sid: "EcrPublicLogin",
|
|
116
|
+
actions: ["ecr-public:GetAuthorizationToken", "sts:GetServiceBearerToken"],
|
|
117
|
+
resources: ["*"]
|
|
118
|
+
}),
|
|
114
119
|
new iam.PolicyStatement({
|
|
115
120
|
sid: "SignManifest",
|
|
116
121
|
actions: ["kms:Sign", "kms:GetPublicKey", "kms:DescribeKey"],
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
scheduledJobBindings,
|
|
25
25
|
slackCommandPrefixes,
|
|
26
26
|
webhookJobBindings
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-gnz747z2.js";
|
|
28
28
|
|
|
29
29
|
// src/stacks/agent-stack.ts
|
|
30
30
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -259,6 +259,12 @@ class FoundationAgent extends cdk.Stack {
|
|
|
259
259
|
resource: "secret",
|
|
260
260
|
resourceName: `${docsSecretPrefix}/personal/*-??????`,
|
|
261
261
|
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
262
|
+
}),
|
|
263
|
+
cdk.Stack.of(this).formatArn({
|
|
264
|
+
service: "secretsmanager",
|
|
265
|
+
resource: "secret",
|
|
266
|
+
resourceName: `${docsSecretPrefix}/personal-drive/*-??????`,
|
|
267
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME
|
|
262
268
|
})
|
|
263
269
|
]
|
|
264
270
|
}));
|
|
@@ -382,6 +388,12 @@ class FoundationAgent extends cdk.Stack {
|
|
|
382
388
|
this.denyDirectKnockSecretAccess(role, props);
|
|
383
389
|
this.denyDirectEmailSecretAccess(role, props.secrets.googleEmail, instanceNames(props.instance).secretGoogleEmail);
|
|
384
390
|
this.denyDirectDocsSecretAccess(role, instanceNames(props.instance).secretGoogleDocs);
|
|
391
|
+
role.addToPolicy(new iam.PolicyStatement({
|
|
392
|
+
sid: "DenySlackSigningSecret",
|
|
393
|
+
effect: iam.Effect.DENY,
|
|
394
|
+
actions: ["secretsmanager:*"],
|
|
395
|
+
resources: [secretArnPattern(props.secrets.signing)]
|
|
396
|
+
}));
|
|
385
397
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
386
398
|
this.denyDirectEtradeAccess(role, props);
|
|
387
399
|
this.pagesPolicy("AgentReadOnlyPagesPolicy", role, props.instance, { invoke: false });
|
|
@@ -906,6 +918,14 @@ class FoundationApi extends cdk2.Stack {
|
|
|
906
918
|
generateStringKey: "placeholder"
|
|
907
919
|
}
|
|
908
920
|
}));
|
|
921
|
+
for (const identity of docsIdentities.filter((entry) => entry.readPersonalDrive === true)) {
|
|
922
|
+
docsSecrets.push(new secretsmanager.Secret(this, `PersonalDriveSecret${identity.owner}`, {
|
|
923
|
+
secretName: `${instanceNames(props.instance).secretGoogleDocs}/personal-drive/${identity.owner}`,
|
|
924
|
+
description: `${display} separate read-only personal Drive grant for Slack ${identity.owner}`,
|
|
925
|
+
removalPolicy: cdk2.RemovalPolicy.DESTROY,
|
|
926
|
+
generateSecretString: { secretStringTemplate: "{}", generateStringKey: "placeholder" }
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
909
929
|
const gatewayRole = new iam2.Role(this, "GatewayRole", {
|
|
910
930
|
assumedBy: new iam2.ServicePrincipal("lambda.amazonaws.com"),
|
|
911
931
|
description: `${display} gateway Lambda role (least privilege)`,
|
|
@@ -1807,7 +1827,7 @@ function configTimezoneFor(configPath) {
|
|
|
1807
1827
|
}
|
|
1808
1828
|
function googleDocsIdentitiesFor(configPath) {
|
|
1809
1829
|
const docs = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.googleDocs;
|
|
1810
|
-
return docs.enabled ? docs.identities.map((
|
|
1830
|
+
return docs.enabled ? docs.identities.map((identity) => ({ ...identity })) : [];
|
|
1811
1831
|
}
|
|
1812
1832
|
function emailIdentitiesFor(configPath) {
|
|
1813
1833
|
const email = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.email;
|
|
@@ -369,7 +369,8 @@ var RawGoogleDriveCapability = z.object({
|
|
|
369
369
|
var RawGoogleDocsIdentity = z.object({
|
|
370
370
|
address: z.string().regex(/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i),
|
|
371
371
|
channel: z.string().regex(/^[CGD][A-Z0-9]{8,}$/),
|
|
372
|
-
owner: z.string().regex(/^U[A-Z0-9]{8,}$/, "owner must be a Slack U id")
|
|
372
|
+
owner: z.string().regex(/^U[A-Z0-9]{8,}$/, "owner must be a Slack U id"),
|
|
373
|
+
readPersonalDrive: z.boolean().optional()
|
|
373
374
|
}).strict();
|
|
374
375
|
var RawGoogleDocsCapability = z.object({
|
|
375
376
|
enabled: z.boolean().default(false),
|
|
@@ -384,6 +385,12 @@ var RawGoogleDocsCapability = z.object({
|
|
|
384
385
|
for (const field of ["owner", "channel"]) {
|
|
385
386
|
const seen = new Set;
|
|
386
387
|
for (const [index, identity] of value.identities.entries()) {
|
|
388
|
+
if (field === "owner" && identity.readPersonalDrive && !identity.channel.startsWith("D"))
|
|
389
|
+
ctx.addIssue({
|
|
390
|
+
code: z.ZodIssueCode.custom,
|
|
391
|
+
path: ["identities", index, "channel"],
|
|
392
|
+
message: "personal Drive reading requires an owner-bound DM"
|
|
393
|
+
});
|
|
387
394
|
if (seen.has(identity[field]))
|
|
388
395
|
ctx.addIssue({
|
|
389
396
|
code: z.ZodIssueCode.custom,
|
|
@@ -675,13 +682,17 @@ var CAPABILITIES = [
|
|
|
675
682
|
},
|
|
676
683
|
{
|
|
677
684
|
id: "googleDocs",
|
|
678
|
-
summary: "Draft Google Docs
|
|
685
|
+
summary: "Draft Google Docs by personal consent; optionally connect a separate read-only personal Drive grant in the owner's DM.",
|
|
679
686
|
config: RawGoogleDocsCapability,
|
|
680
687
|
...NONE,
|
|
681
688
|
secrets: ["google/docs", GOOGLE_OAUTH_SECRET],
|
|
682
689
|
resources: ["googleDocsProxyFunctionName"],
|
|
683
|
-
slackCommands: ["docs-connect"],
|
|
684
|
-
googleScopes: [
|
|
690
|
+
slackCommands: ["docs-connect", "personal-drive"],
|
|
691
|
+
googleScopes: [
|
|
692
|
+
"https://www.googleapis.com/auth/drive.file",
|
|
693
|
+
"https://www.googleapis.com/auth/drive.readonly",
|
|
694
|
+
...GOOGLE_IDENTITY_SCOPES
|
|
695
|
+
]
|
|
685
696
|
},
|
|
686
697
|
{
|
|
687
698
|
id: "googleCalendar",
|
|
@@ -1964,4 +1975,4 @@ function ecrRepositoryArn(repositoryUri) {
|
|
|
1964
1975
|
return `arn:aws:ecr:${region}:${account}:repository/${name}`;
|
|
1965
1976
|
}
|
|
1966
1977
|
|
|
1967
|
-
export { parseSchedule, BASE_SLACK_EVENTS, CAPABILITY_IDS, requiredGithubPermissions, requiredSlackScopes, requiredSlackCommands,
|
|
1978
|
+
export { parseSchedule, BASE_SLACK_EVENTS, CAPABILITY_IDS, requiredGithubPermissions, requiredSlackScopes, requiredSlackCommands, isScheduledJob, isWebhookJob, webhookJobBindings, scheduledJobBindings, jobWebhookKey, capabilityScheduledJobs, crmOutreachConfigured, crmPolicyFingerprint, parseInstanceConfig, encodeProactiveChannels, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME, RELEASE_VERSION_RE, parseManifest, manifestKey, lambdaKey, skillsKey, buildManifest, manifestSigningPayload, verifyManifest, lambdaBundleContext, BUN_VERSION, LAMBDA_ENTRY_POINTS, skipBundle, RELEASE_BUCKET_ENV, RELEASE_DIR_ENV, releaseCacheDir, releaseSource, releaseAccess, lambdaCode, agentImage, agentImageTagRequired, releaseImageRepositoryArn, ecrRepositoryArn };
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-6f2hkamj.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
skillsKey,
|
|
45
45
|
skipBundle,
|
|
46
46
|
verifyManifest
|
|
47
|
-
} from "../chunk-
|
|
47
|
+
} from "../chunk-gnz747z2.js";
|
|
48
48
|
import"../chunk-dz91avrb.js";
|
|
49
49
|
export {
|
|
50
50
|
BUILD_TIMEOUT,
|
package/package.json
CHANGED
|
@@ -73,6 +73,10 @@ features:
|
|
|
73
73
|
url: ${COMMANDS_REQUEST_URL}
|
|
74
74
|
description: Let ${DISPLAY_NAME} draft Google Docs in your own Drive
|
|
75
75
|
should_escape: false
|
|
76
|
+
- command: /${COMMAND_PREFIX}-personal-drive
|
|
77
|
+
url: ${COMMANDS_REQUEST_URL}
|
|
78
|
+
description: Connect your personal Drive read-only in your configured DM
|
|
79
|
+
should_escape: false
|
|
76
80
|
- command: /${COMMAND_PREFIX}-knock-connect
|
|
77
81
|
url: ${COMMANDS_REQUEST_URL}
|
|
78
82
|
description: Connect ${DISPLAY_NAME} to Knock read-only tools (admins)
|
package/src/deploy/cli.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* tree into a deploy would make every release of the tool carry a model
|
|
12
12
|
* runtime it never executes.
|
|
13
13
|
*/
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { parseInstanceConfig } from "@deployfoundation/foundation-core";
|
|
14
16
|
import { type AwsContext, awsContext } from "./aws.ts";
|
|
15
17
|
import { configSync } from "./config-sync.ts";
|
|
16
18
|
import { deploy } from "./deploy.ts";
|
|
@@ -18,12 +20,13 @@ import { githubAppCreate } from "./github-app-create.ts";
|
|
|
18
20
|
import { instanceBanner, loadInstanceContext, resolveInstanceFilePath } from "./instance.ts";
|
|
19
21
|
import { jobKey } from "./job-key.ts";
|
|
20
22
|
import { postDeploy } from "./post-deploy.ts";
|
|
21
|
-
import { releaseRequest, resolveRelease } from "./release.ts";
|
|
23
|
+
import { releaseRequest, resolveRelease, toolVersion } from "./release.ts";
|
|
22
24
|
import { setup } from "./setup.ts";
|
|
23
25
|
import { slackManifestFor } from "./slack-manifest.ts";
|
|
24
26
|
import { stageCustomization } from "./stage-customization.ts";
|
|
25
27
|
|
|
26
28
|
export const COMMANDS = [
|
|
29
|
+
"check",
|
|
27
30
|
"deploy",
|
|
28
31
|
"post-deploy",
|
|
29
32
|
"config:sync",
|
|
@@ -39,6 +42,10 @@ export type Command = (typeof COMMANDS)[number];
|
|
|
39
42
|
export const USAGE = `usage: foundation-deploy <command> --instance <path> [options]
|
|
40
43
|
|
|
41
44
|
commands
|
|
45
|
+
check parse the instance and config files with THIS release's
|
|
46
|
+
schema; no AWS. Run it on config PRs with the pinned
|
|
47
|
+
version: a config the deployed release rejects puts the
|
|
48
|
+
teammate on the fallback config.
|
|
42
49
|
deploy build and push the agent image, cdk deploy --all, refresh
|
|
43
50
|
the runtime secret, sync config
|
|
44
51
|
post-deploy smoke DEFAULT, probe the mount, promote \`live\`, smoke \`live\`
|
|
@@ -120,6 +127,23 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
120
127
|
return 1;
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
// `check` reads files only, so it runs in a pull request with no credentials.
|
|
131
|
+
if (command === "check") {
|
|
132
|
+
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
133
|
+
try {
|
|
134
|
+
parseInstanceConfig(readFileSync(paths.configPath, "utf8"));
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(
|
|
137
|
+
`${paths.configPath} does not parse with this release (v${toolVersion()}):\n${
|
|
138
|
+
error instanceof Error ? error.message : String(error)
|
|
139
|
+
}`,
|
|
140
|
+
);
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
console.log(`${paths.configPath} parses with this release (v${toolVersion()})`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
123
147
|
// `slack-manifest` writes a machine-readable document to stdout and must not
|
|
124
148
|
// print a banner into it; every other command announces its target first.
|
|
125
149
|
if (command === "slack-manifest") {
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* checkout's `skills/` otherwise. A company's own skills are a source in its
|
|
18
18
|
* runtime config (a GitHub repo), never a directory in this package.
|
|
19
19
|
*/
|
|
20
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
|
|
20
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync } from "node:fs";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
22
|
import { join, resolve } from "node:path";
|
|
23
|
+
import { parseInstanceConfig } from "@deployfoundation/foundation-core";
|
|
23
24
|
import { versionedConfigKey } from "@deployfoundation/foundation-core/instance";
|
|
24
25
|
import { skillsKey } from "../release/manifest.ts";
|
|
25
26
|
import { type AwsContext, awsMutate, stackOutput } from "./aws.ts";
|
|
@@ -130,6 +131,19 @@ export async function uploadRuntimeConfig(
|
|
|
130
131
|
"customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`",
|
|
131
132
|
);
|
|
132
133
|
const source = opts.runtimeConfigPath ?? ctx.paths.configPath;
|
|
134
|
+
// Parse with THIS release's schema before anything is written. Redeploying
|
|
135
|
+
// the live version writes the very object the live runtime reads, so an
|
|
136
|
+
// unparseable upload puts every session on the fallback config even though
|
|
137
|
+
// the deploy then fails at synth (Sky, 2026-10-19: no Upwork for a day).
|
|
138
|
+
try {
|
|
139
|
+
parseInstanceConfig(readFileSync(source, "utf8"));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`config for ${opts.version} does not parse with this release's schema, so nothing was uploaded: ${
|
|
143
|
+
error instanceof Error ? error.message : String(error)
|
|
144
|
+
}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
133
147
|
const key = versionedConfigKey(ctx.names.configKey, opts.version);
|
|
134
148
|
await awsMutate(ctx, ["s3", "cp", source, `s3://${opts.bucket}/${key}`]);
|
|
135
149
|
return key;
|
package/src/deploy/deploy.ts
CHANGED
|
@@ -672,9 +672,11 @@ export async function deploy(ctx: AwsContext, opts: DeployOptions): Promise<void
|
|
|
672
672
|
|
|
673
673
|
// The config of the version going in, written BEFORE `cdk deploy` creates
|
|
674
674
|
// that version, so its pre-warmed containers boot with it rather than with
|
|
675
|
-
// the fallback.
|
|
676
|
-
//
|
|
677
|
-
//
|
|
675
|
+
// the fallback. When the version going in is the one serving now (a config
|
|
676
|
+
// change on the same release), the live runtime reads this very object, so
|
|
677
|
+
// uploadRuntimeConfig parses it with this release's schema first and refuses
|
|
678
|
+
// rather than write a config the runtime would reject. A brand-new instance
|
|
679
|
+
// has no bucket yet; the config sync after `cdk deploy` covers it.
|
|
678
680
|
const version = foundationVersion(release);
|
|
679
681
|
const bucket = phase1
|
|
680
682
|
? undefined
|
|
@@ -433,6 +433,12 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
433
433
|
resourceName: `${docsSecretPrefix}/personal/*-??????`,
|
|
434
434
|
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
|
|
435
435
|
}),
|
|
436
|
+
cdk.Stack.of(this).formatArn({
|
|
437
|
+
service: "secretsmanager",
|
|
438
|
+
resource: "secret",
|
|
439
|
+
resourceName: `${docsSecretPrefix}/personal-drive/*-??????`,
|
|
440
|
+
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
|
|
441
|
+
}),
|
|
436
442
|
],
|
|
437
443
|
}),
|
|
438
444
|
);
|
|
@@ -629,6 +635,14 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
629
635
|
namesFor(props.instance).secretGoogleEmail,
|
|
630
636
|
);
|
|
631
637
|
this.denyDirectDocsSecretAccess(role, namesFor(props.instance).secretGoogleDocs);
|
|
638
|
+
role.addToPolicy(
|
|
639
|
+
new iam.PolicyStatement({
|
|
640
|
+
sid: "DenySlackSigningSecret",
|
|
641
|
+
effect: iam.Effect.DENY,
|
|
642
|
+
actions: ["secretsmanager:*"],
|
|
643
|
+
resources: [secretArnPattern(props.secrets.signing)],
|
|
644
|
+
}),
|
|
645
|
+
);
|
|
632
646
|
this.denyDirectUpworkApprovalAccess(role, props.upworkApprovalTable);
|
|
633
647
|
this.denyDirectEtradeAccess(role, props);
|
|
634
648
|
this.pagesPolicy("AgentReadOnlyPagesPolicy", role, props.instance, { invoke: false });
|
package/src/stacks/api-stack.ts
CHANGED
|
@@ -233,6 +233,16 @@ export class FoundationApi extends cdk.Stack {
|
|
|
233
233
|
},
|
|
234
234
|
}),
|
|
235
235
|
);
|
|
236
|
+
for (const identity of docsIdentities.filter((entry) => entry.readPersonalDrive === true)) {
|
|
237
|
+
docsSecrets.push(
|
|
238
|
+
new secretsmanager.Secret(this, `PersonalDriveSecret${identity.owner}`, {
|
|
239
|
+
secretName: `${namesFor(props.instance).secretGoogleDocs}/personal-drive/${identity.owner}`,
|
|
240
|
+
description: `${display} separate read-only personal Drive grant for Slack ${identity.owner}`,
|
|
241
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
242
|
+
generateSecretString: { secretStringTemplate: "{}", generateStringKey: "placeholder" },
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
236
246
|
|
|
237
247
|
const gatewayRole = new iam.Role(this, "GatewayRole", {
|
|
238
248
|
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
|
|
@@ -1495,9 +1505,7 @@ function configTimezoneFor(configPath: string): string {
|
|
|
1495
1505
|
/** The Google Docs drafting identities to provision, read from the instance config at synth. */
|
|
1496
1506
|
export function googleDocsIdentitiesFor(configPath: string): GoogleDocsIdentity[] {
|
|
1497
1507
|
const docs = parseInstanceConfig(readFileSync(configPath, "utf8")).capabilities.googleDocs;
|
|
1498
|
-
return docs.enabled
|
|
1499
|
-
? docs.identities.map(({ address, channel, owner }) => ({ address, channel, owner }))
|
|
1500
|
-
: [];
|
|
1508
|
+
return docs.enabled ? docs.identities.map((identity) => ({ ...identity })) : [];
|
|
1501
1509
|
}
|
|
1502
1510
|
|
|
1503
1511
|
export function emailIdentitiesFor(configPath: string): EmailIdentity[] {
|
|
@@ -208,6 +208,13 @@ export class ReleaseAccountStack extends cdk.Stack {
|
|
|
208
208
|
actions: ["ecr:GetAuthorizationToken"],
|
|
209
209
|
resources: ["*"],
|
|
210
210
|
}),
|
|
211
|
+
// Pull-only login to public ECR. Anonymous base-image pulls share
|
|
212
|
+
// the hosted runners' quota and failed v0.2.48's build with 429.
|
|
213
|
+
new iam.PolicyStatement({
|
|
214
|
+
sid: "EcrPublicLogin",
|
|
215
|
+
actions: ["ecr-public:GetAuthorizationToken", "sts:GetServiceBearerToken"],
|
|
216
|
+
resources: ["*"],
|
|
217
|
+
}),
|
|
211
218
|
new iam.PolicyStatement({
|
|
212
219
|
sid: "SignManifest",
|
|
213
220
|
actions: ["kms:Sign", "kms:GetPublicKey", "kms:DescribeKey"],
|