@deployfoundation/foundation-deploy 0.1.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.
Files changed (54) hide show
  1. package/README.md +174 -0
  2. package/agent-image/Dockerfile +254 -0
  3. package/agent-image/bin/aws +36 -0
  4. package/agent-image/bin/gh +193 -0
  5. package/agent-image/bin/git-credential-sky +89 -0
  6. package/agent-image/security-overlay.yml +176 -0
  7. package/cdk.json +6 -0
  8. package/dist/bin/app.js +112 -0
  9. package/dist/bin/foundation-deploy.js +1906 -0
  10. package/dist/bin/release-account.js +154 -0
  11. package/dist/chunk-4aye5cee.js +2416 -0
  12. package/dist/chunk-9ddxyvq2.js +1455 -0
  13. package/dist/chunk-v7tz8g50.js +428 -0
  14. package/dist/src/index.js +88 -0
  15. package/package.json +38 -0
  16. package/pipeline/buildspec.yml +34 -0
  17. package/src/artifacts.ts +318 -0
  18. package/src/deploy/assets/github-app-manifest.yml +29 -0
  19. package/src/deploy/assets/slack-app-manifest.yml +95 -0
  20. package/src/deploy/aws.ts +265 -0
  21. package/src/deploy/cli.ts +212 -0
  22. package/src/deploy/config-sync.ts +93 -0
  23. package/src/deploy/config.ts +29 -0
  24. package/src/deploy/deploy.ts +566 -0
  25. package/src/deploy/endpoint.ts +242 -0
  26. package/src/deploy/github-app-create.ts +154 -0
  27. package/src/deploy/github-app-manifest.ts +53 -0
  28. package/src/deploy/image.ts +80 -0
  29. package/src/deploy/instance.ts +87 -0
  30. package/src/deploy/license-cache.ts +47 -0
  31. package/src/deploy/license.ts +272 -0
  32. package/src/deploy/paths.ts +65 -0
  33. package/src/deploy/post-deploy.ts +97 -0
  34. package/src/deploy/release.ts +282 -0
  35. package/src/deploy/runtime-secret.ts +241 -0
  36. package/src/deploy/setup.ts +393 -0
  37. package/src/deploy/sh.ts +74 -0
  38. package/src/deploy/slack-manifest.ts +112 -0
  39. package/src/deploy/stage-customization.ts +224 -0
  40. package/src/deploy/tracing.ts +243 -0
  41. package/src/deploy-permissions.ts +165 -0
  42. package/src/index.ts +60 -0
  43. package/src/lambda-bundle-context.ts +64 -0
  44. package/src/names.ts +170 -0
  45. package/src/release/kms.ts +86 -0
  46. package/src/release/manifest.ts +265 -0
  47. package/src/stacks/agent-stack.ts +938 -0
  48. package/src/stacks/api-stack.ts +1005 -0
  49. package/src/stacks/ci-stack.ts +96 -0
  50. package/src/stacks/data-stack.ts +446 -0
  51. package/src/stacks/network-stack.ts +282 -0
  52. package/src/stacks/newsletter-stack.ts +572 -0
  53. package/src/stacks/pipeline-stack.ts +242 -0
  54. package/src/stacks/release-account-stack.ts +229 -0
@@ -0,0 +1,242 @@
1
+ import { CUSTOMIZATION_ARTIFACT_NAME } from "@deployfoundation/foundation-core/instance";
2
+ /**
3
+ * FoundationPipeline — the instance's own deployer, living in the instance's own AWS
4
+ * account.
5
+ *
6
+ * Why this exists rather than the GitHub Actions job that does the same work:
7
+ * a customer running Foundation should own its build, its logs and its
8
+ * credentials. A deploy through Actions depends on a workflow in someone
9
+ * else's repository and on an OIDC trust reaching into the customer's account;
10
+ * this depends on neither. One merge no longer deploys every instance in
11
+ * lock-step either — an instance can hold or pin without touching the others.
12
+ *
13
+ * The build runs `foundation-deploy deploy` and then `foundation-deploy
14
+ * post-deploy`: the first stages any reviewed tenant runtime config, builds
15
+ * the image, runs `cdk deploy --all`, refreshes the runtime secret and syncs
16
+ * config; the second smokes DEFAULT, probes the mount, promotes `live` and
17
+ * smokes `live`. `live` moves only after the probes pass.
18
+ *
19
+ * `cdk deploy --all` inside the build includes this stack, so a change to the
20
+ * pipeline definition applies on the pipeline's next run — self-updating,
21
+ * without CDK Pipelines' extra mutate stage. The first deploy is by hand, from
22
+ * a laptop with the instance's profile (`foundation-deploy setup`, or `cdk
23
+ * deploy <Prefix>Pipeline -c instanceFile=<path>`).
24
+ *
25
+ * The buildspec is read from the INSTANCE repository the pipeline sources, not
26
+ * from Foundation: the instance repo owns its own build, and step 8 shrinks
27
+ * that file to one `npx @deployfoundation/foundation-deploy@<version>` line.
28
+ * `pipeline/buildspec.yml` in this package is the reference it starts from.
29
+ *
30
+ * Only synthesized for instances whose `deploy.via` is `codepipeline`; see
31
+ * `bin/app.ts`. Exactly one deployer owns an instance.
32
+ */
33
+ import * as cdk from "aws-cdk-lib";
34
+ import * as codebuild from "aws-cdk-lib/aws-codebuild";
35
+ import * as codepipeline from "aws-cdk-lib/aws-codepipeline";
36
+ import * as actions from "aws-cdk-lib/aws-codepipeline-actions";
37
+ import * as iam from "aws-cdk-lib/aws-iam";
38
+ import type * as kms from "aws-cdk-lib/aws-kms";
39
+ import type * as s3 from "aws-cdk-lib/aws-s3";
40
+ import * as sns from "aws-cdk-lib/aws-sns";
41
+ import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";
42
+ import type { Construct } from "constructs";
43
+ import { deployStatements } from "../deploy-permissions.ts";
44
+ import { type Instance, namesFor } from "../names.ts";
45
+
46
+ export interface FoundationPipelineProps extends cdk.StackProps {
47
+ /** The deployment this pipeline deploys; every name below comes from it. */
48
+ instance: Instance;
49
+ /** The data stack's bucket — config and skills are synced into it. */
50
+ bucket: s3.IBucket;
51
+ /** The CMK that bucket is encrypted with. */
52
+ dataKey: kms.IKey;
53
+ /**
54
+ * Where the instance file sits inside the instance repository the pipeline
55
+ * sources, so the build can name it. Relative to that checkout's root.
56
+ */
57
+ instanceFilePath: string;
58
+ /** Address told when a pipeline run fails. Defaults to the instance's own. */
59
+ alarmEmail?: string;
60
+ /**
61
+ * Where the buildspec sits inside the instance repository the pipeline
62
+ * sources. Overridable because an instance repo chooses its own layout.
63
+ */
64
+ buildspecPath?: string;
65
+ }
66
+
67
+ /**
68
+ * Default buildspec path inside the INSTANCE repository. `packages/infra/pipeline/buildspec.yml`
69
+ * is the reference version of this file; an instance repo copies it and owns it
70
+ * from then on.
71
+ */
72
+ export const DEFAULT_BUILDSPEC_PATH = "buildspec.yml";
73
+
74
+ /** A whole deploy is ~8 minutes warm; an hour is room for a cold image build. */
75
+ export const BUILD_TIMEOUT = cdk.Duration.minutes(60);
76
+
77
+ export class FoundationPipeline extends cdk.Stack {
78
+ public readonly pipeline: codepipeline.Pipeline;
79
+ public readonly buildRole: iam.Role;
80
+
81
+ constructor(scope: Construct, id: string, props: FoundationPipelineProps) {
82
+ super(scope, id, props);
83
+ const names = namesFor(props.instance);
84
+ const { deploy } = props.instance;
85
+ const customization = props.instance.customization;
86
+ if (deploy.connectionArn === undefined)
87
+ throw new Error(
88
+ `instance ${props.instance.name}: deploy.connectionArn is required for a pipeline`,
89
+ );
90
+ const [owner = "", repo = ""] = props.instance.github.repo.split("/");
91
+
92
+ // The deployer itself. Same statements as the GitHub Actions role, from
93
+ // one definition, so the two cannot drift; what may assume this role —
94
+ // CodeBuild in this account, reached only through the pipeline — is the
95
+ // boundary, exactly as the OIDC trust is there.
96
+ this.buildRole = new iam.Role(this, "BuildRole", {
97
+ description: `CodePipeline deployer for ${props.instance.name}`,
98
+ assumedBy: new iam.ServicePrincipal("codebuild.amazonaws.com"),
99
+ inlinePolicies: {
100
+ FoundationDeploy: new iam.PolicyDocument({
101
+ statements: deployStatements(
102
+ this,
103
+ names,
104
+ { bucket: props.bucket, dataKey: props.dataKey },
105
+ props.instance,
106
+ ),
107
+ }),
108
+ },
109
+ });
110
+
111
+ // One topic and subscription cover both failures and approval requests.
112
+ // Keeping it here gives the optional approval action the existing alarm
113
+ // path without creating a second email subscription.
114
+ const failureTopic = new sns.Topic(this, "FailureTopic", {
115
+ displayName: `${props.instance.displayName} deploy failures`,
116
+ });
117
+ const alarmEmail = props.alarmEmail ?? props.instance.aws.alarmEmail;
118
+ if (alarmEmail !== undefined && alarmEmail !== "")
119
+ failureTopic.addSubscription(new subscriptions.EmailSubscription(alarmEmail));
120
+
121
+ const project = new codebuild.PipelineProject(this, "Build", {
122
+ projectName: `${props.instance.naming.prefix}Deploy`,
123
+ description: `Build and deploy ${props.instance.name} from ${props.instance.github.repo}@${deploy.branch}`,
124
+ role: this.buildRole,
125
+ timeout: BUILD_TIMEOUT,
126
+ environment: {
127
+ // arm64 throughout: AgentCore runs arm64 only, and building natively
128
+ // beats qemu by minutes. `privileged` is what lets the build run a
129
+ // Docker daemon at all.
130
+ buildImage: codebuild.LinuxArmBuildImage.AMAZON_LINUX_2023_STANDARD_3_0,
131
+ computeType: codebuild.ComputeType.LARGE,
132
+ privileged: true,
133
+ },
134
+ // Best-effort and free: warm when the build lands on a host that ran a
135
+ // recent build, cold otherwise. `FOUNDATION_IMAGE_CACHE=local` is the deploy
136
+ // script's half of this — buildx with no cache export, because there is
137
+ // no Actions cache backend here to export to.
138
+ cache: codebuild.Cache.local(
139
+ codebuild.LocalCacheMode.DOCKER_LAYER,
140
+ codebuild.LocalCacheMode.SOURCE,
141
+ ),
142
+ environmentVariables: {
143
+ // The instance file is the tool's only input. The path is the instance
144
+ // repository's own layout, so the buildspec and this value agree.
145
+ FOUNDATION_INSTANCE_FILE: { value: props.instanceFilePath },
146
+ // No `--profile`: the build's credentials are the role above.
147
+ FOUNDATION_NO_PROFILE: { value: "1" },
148
+ FOUNDATION_IMAGE_CACHE: { value: "local" },
149
+ // The buildspec runs `post-deploy` as its own phase, so the deploy
150
+ // command must not also promote `live`.
151
+ FOUNDATION_SKIP_POST_DEPLOY: { value: "1" },
152
+ },
153
+ // From the instance repository's source, not inline: that repo reviews
154
+ // and versions its own build, and a change to it needs no stack deploy.
155
+ buildSpec: codebuild.BuildSpec.fromSourceFilename(
156
+ props.buildspecPath ?? DEFAULT_BUILDSPEC_PATH,
157
+ ),
158
+ });
159
+
160
+ const source = new codepipeline.Artifact("Source");
161
+ const sourceActions = [
162
+ new actions.CodeStarConnectionsSourceAction({
163
+ actionName: "GitHub",
164
+ connectionArn: deploy.connectionArn,
165
+ owner,
166
+ repo,
167
+ branch: deploy.branch,
168
+ output: source,
169
+ // Every merge to the tracked branch deploys, as merging to main has
170
+ // always done.
171
+ triggerOnPush: true,
172
+ }),
173
+ ];
174
+ let customizationSource: codepipeline.Artifact | undefined;
175
+ let customizationSourceAction: actions.CodeStarConnectionsSourceAction | undefined;
176
+ if (customization !== undefined) {
177
+ const [customizationOwner = "", customizationRepo = ""] = customization.repository.split("/");
178
+ const customizationArtifact = new codepipeline.Artifact(CUSTOMIZATION_ARTIFACT_NAME);
179
+ customizationSource = customizationArtifact;
180
+ customizationSourceAction = new actions.CodeStarConnectionsSourceAction({
181
+ actionName: CUSTOMIZATION_ARTIFACT_NAME,
182
+ connectionArn: deploy.connectionArn,
183
+ owner: customizationOwner,
184
+ repo: customizationRepo,
185
+ branch: customization.branch,
186
+ output: customizationArtifact,
187
+ triggerOnPush: true,
188
+ });
189
+ sourceActions.push(customizationSourceAction);
190
+ }
191
+
192
+ const stages: codepipeline.StageProps[] = [{ stageName: "Source", actions: sourceActions }];
193
+ if (customization !== undefined && customizationSourceAction !== undefined) {
194
+ const customizationLinkPath = customization.path
195
+ .split("/")
196
+ .map((segment) => encodeURIComponent(segment))
197
+ .join("/");
198
+ stages.push({
199
+ stageName: "Approval",
200
+ actions: [
201
+ new actions.ManualApprovalAction({
202
+ actionName: "ReviewCustomization",
203
+ notificationTopic: failureTopic,
204
+ additionalInformation: `Review ${customization.path}: approve only intended runtime behavior; infrastructure and credentials stay deployment-owned.`,
205
+ externalEntityLink: `https://github.com/${customization.repository}/blob/${customizationSourceAction.variables.commitId}/${customizationLinkPath}`,
206
+ }),
207
+ ],
208
+ });
209
+ }
210
+ stages.push({
211
+ stageName: "Deploy",
212
+ actions: [
213
+ new actions.CodeBuildAction({
214
+ actionName: "BuildAndDeploy",
215
+ project,
216
+ input: source,
217
+ extraInputs: customizationSource === undefined ? undefined : [customizationSource],
218
+ }),
219
+ ],
220
+ });
221
+ this.pipeline = new codepipeline.Pipeline(this, "Pipeline", {
222
+ pipelineName: `${props.instance.naming.prefix}Deploy`,
223
+ // V2: cheaper for a pipeline that idles between merges, and the only
224
+ // type that takes triggers and variables.
225
+ pipelineType: codepipeline.PipelineType.V2,
226
+ restartExecutionOnUpdate: false,
227
+ stages,
228
+ });
229
+
230
+ // Nobody watches a pipeline that usually succeeds, so a failure has to
231
+ // come and find someone. The topic and its one email subscription above
232
+ // also notify the same human when a customization needs approval.
233
+ this.pipeline.onStateChange("OnFailure", {
234
+ description: `${props.instance.name} deploy failed`,
235
+ eventPattern: { detail: { state: ["FAILED"] } },
236
+ target: new cdk.aws_events_targets.SnsTopic(failureTopic),
237
+ });
238
+
239
+ new cdk.CfnOutput(this, "PipelineName", { value: this.pipeline.pipelineName });
240
+ new cdk.CfnOutput(this, "BuildRoleArn", { value: this.buildRole.roleArn });
241
+ }
242
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The Foundry 41 release account: where a Foundation release is published
3
+ * FROM, and what customers are allowed to read.
4
+ *
5
+ * This is the only stack in this package that is not an instance's. It is
6
+ * deployed once, by hand, into Foundry 41's own account, and it holds four
7
+ * things:
8
+ *
9
+ * - the release **bucket** — `releases/vX.Y.Z/{manifest.json,lambda/*.zip,
10
+ * skills.tar.gz}` — readable by the registered customer accounts;
11
+ * - the agent **image repository**, `foundation/agent`, pullable by the same
12
+ * accounts (the adopted decision: a private repository plus a policy
13
+ * listing customers, not ECR Public);
14
+ * - the **signing key**, asymmetric and sign/verify only, which the release
15
+ * workflow signs the manifest with and every deployer verifies against;
16
+ * - the **OIDC role** the release workflow assumes, trusted only for tag
17
+ * pushes on the Foundation repository.
18
+ *
19
+ * The customer account list is the customer list. Adding a customer is a
20
+ * parameter change and a redeploy of this stack — deliberately a Foundry 41
21
+ * action, and the only one in a customer's install path.
22
+ *
23
+ * Nothing here is deployed by `foundation-deploy`, and no instance stack
24
+ * depends on it. `bin/release-account.ts` is its entry point.
25
+ */
26
+ import * as cdk from "aws-cdk-lib";
27
+ import * as ecr from "aws-cdk-lib/aws-ecr";
28
+ import * as iam from "aws-cdk-lib/aws-iam";
29
+ import * as kms from "aws-cdk-lib/aws-kms";
30
+ import * as s3 from "aws-cdk-lib/aws-s3";
31
+ import type { Construct } from "constructs";
32
+
33
+ export interface ReleaseAccountStackProps extends cdk.StackProps {
34
+ /** The release bucket's name. Global, so it is an input rather than derived. */
35
+ bucketName: string;
36
+ /** The agent image repository, without a registry. */
37
+ repositoryName?: string;
38
+ /**
39
+ * Customer AWS account ids allowed to read releases and pull the image.
40
+ * May be empty before the first customer is registered.
41
+ */
42
+ customerAccountIds: readonly string[];
43
+ /** `owner/repo` whose TAG pushes may publish a release. */
44
+ githubRepository: string;
45
+ /**
46
+ * The repository's immutable numeric ids.
47
+ *
48
+ * GitHub can issue the OIDC `sub` claim in two shapes, and which one it
49
+ * sends is the repository's setting, not ours: a repository with immutable
50
+ * subject claims on sends
51
+ * `repo:<owner>@<ownerId>/<repo>@<repoId>:ref:refs/tags/vX`, and the plain
52
+ * `repo:<owner>/<repo>:...` form never appears. Trusting only the plain form
53
+ * is why the first `v0.1.0` tag could not assume this role. `ci-stack` had
54
+ * always trusted both; this stack had not.
55
+ */
56
+ githubRepositoryIds: { owner: number; repo: number };
57
+ /** Alias for the signing key, without the `alias/` prefix. */
58
+ signingKeyAlias?: string;
59
+ /** The role the release workflow assumes through OIDC. */
60
+ releaseRoleName?: string;
61
+ }
62
+
63
+ export class ReleaseAccountStack extends cdk.Stack {
64
+ public readonly bucket: s3.Bucket;
65
+ public readonly repository: ecr.Repository;
66
+ public readonly signingKey: kms.Key;
67
+ public readonly releaseRole: iam.Role;
68
+
69
+ constructor(scope: Construct, id: string, props: ReleaseAccountStackProps) {
70
+ super(scope, id, props);
71
+
72
+ const repositoryName = props.repositoryName ?? "foundation/agent";
73
+ const customers = [...props.customerAccountIds];
74
+ // An empty list must not become a policy that names nobody and therefore
75
+ // grants nothing to everybody by accident; each grant below is omitted
76
+ // entirely until there is an account to name.
77
+ const customerPrincipals = customers.map((account) => new iam.AccountPrincipal(account));
78
+
79
+ this.bucket = new s3.Bucket(this, "ReleaseBucket", {
80
+ bucketName: props.bucketName,
81
+ // Every release is immutable, and versioning is what makes deleting one
82
+ // by accident recoverable rather than terminal.
83
+ versioned: true,
84
+ encryption: s3.BucketEncryption.S3_MANAGED,
85
+ blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
86
+ enforceSSL: true,
87
+ removalPolicy: cdk.RemovalPolicy.RETAIN,
88
+ });
89
+ if (customerPrincipals.length > 0)
90
+ this.bucket.addToResourcePolicy(
91
+ new iam.PolicyStatement({
92
+ sid: "CustomersReadReleases",
93
+ principals: customerPrincipals,
94
+ actions: ["s3:GetObject", "s3:GetObjectVersion"],
95
+ // Only under `releases/`: nothing else in this bucket is published.
96
+ resources: [this.bucket.arnForObjects("releases/*")],
97
+ }),
98
+ );
99
+
100
+ this.repository = new ecr.Repository(this, "AgentRepository", {
101
+ repositoryName,
102
+ imageScanOnPush: true,
103
+ // A release tag is written once and referenced by digest thereafter.
104
+ imageTagMutability: ecr.TagMutability.IMMUTABLE,
105
+ encryption: ecr.RepositoryEncryption.AES_256,
106
+ removalPolicy: cdk.RemovalPolicy.RETAIN,
107
+ });
108
+ if (customerPrincipals.length > 0)
109
+ this.repository.addToResourcePolicy(
110
+ new iam.PolicyStatement({
111
+ sid: "CustomersPullAgentImage",
112
+ principals: customerPrincipals,
113
+ actions: [
114
+ "ecr:BatchGetImage",
115
+ "ecr:GetDownloadUrlForLayer",
116
+ "ecr:BatchCheckLayerAvailability",
117
+ "ecr:DescribeImages",
118
+ ],
119
+ }),
120
+ );
121
+
122
+ // Asymmetric and SIGN_VERIFY: the private half never leaves KMS, and a
123
+ // deployer needs only `kms:Verify`, which the policy below grants
124
+ // cross-account. RSA_4096 with RSASSA_PSS_SHA_256 is what
125
+ // `src/release/kms.ts` signs with.
126
+ this.signingKey = new kms.Key(this, "ReleaseSigningKey", {
127
+ alias: props.signingKeyAlias ?? "foundation-release",
128
+ description: "Signs Foundation release manifests. Customers verify against it.",
129
+ keySpec: kms.KeySpec.RSA_4096,
130
+ keyUsage: kms.KeyUsage.SIGN_VERIFY,
131
+ removalPolicy: cdk.RemovalPolicy.RETAIN,
132
+ });
133
+ if (customerPrincipals.length > 0)
134
+ this.signingKey.addToResourcePolicy(
135
+ new iam.PolicyStatement({
136
+ sid: "CustomersVerifyManifests",
137
+ principals: customerPrincipals,
138
+ // Verify and GetPublicKey only. Nothing here can sign.
139
+ actions: ["kms:Verify", "kms:GetPublicKey", "kms:DescribeKey"],
140
+ resources: ["*"],
141
+ }),
142
+ );
143
+
144
+ // Imported, not created: an account may hold only one provider per url, and
145
+ // a `cdk destroy` of a created one would take it from every other
146
+ // repository that trusts GitHub.
147
+ //
148
+ // A DEDICATED release account starts with no provider at all, though, and
149
+ // then this import silently yields an ARN that does not resolve: the role
150
+ // is created, and every assume fails with "the web identity token provided
151
+ // could not be validated". That is what happened to this account. Create it
152
+ // once, before deploying this stack:
153
+ //
154
+ // aws iam create-open-id-connect-provider \
155
+ // --url https://token.actions.githubusercontent.com \
156
+ // --client-id-list sts.amazonaws.com \
157
+ // --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
158
+ const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
159
+ this,
160
+ "GitHubOidc",
161
+ `arn:${this.partition}:iam::${this.account}:oidc-provider/token.actions.githubusercontent.com`,
162
+ );
163
+
164
+ const [owner, repo] = props.githubRepository.split("/");
165
+ const ids = props.githubRepositoryIds;
166
+ const tagSubjects = [
167
+ `repo:${props.githubRepository}:ref:refs/tags/v*`,
168
+ `repo:${owner}@${ids.owner}/${repo}@${ids.repo}:ref:refs/tags/v*`,
169
+ ];
170
+
171
+ this.releaseRole = new iam.Role(this, "ReleaseRole", {
172
+ roleName: props.releaseRoleName ?? "FoundationRelease",
173
+ description: `Publishes Foundation releases from tag pushes on ${props.githubRepository}`,
174
+ maxSessionDuration: cdk.Duration.hours(1),
175
+ assumedBy: new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, {
176
+ StringEquals: { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" },
177
+ // TAGS ONLY. A branch push, a pull request and a workflow_dispatch all
178
+ // carry a different subject, so none of them can publish a release.
179
+ // Both shapes GitHub can issue, for the reason on
180
+ // {@link ReleaseAccountStackProps.githubRepositoryIds}.
181
+ StringLike: { "token.actions.githubusercontent.com:sub": tagSubjects },
182
+ }),
183
+ inlinePolicies: {
184
+ PublishRelease: new iam.PolicyDocument({
185
+ statements: [
186
+ new iam.PolicyStatement({
187
+ sid: "UploadRelease",
188
+ // No delete: a release is written once. Overwriting one is a
189
+ // separate, deliberate act by a human with other credentials.
190
+ actions: ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
191
+ resources: [this.bucket.bucketArn, this.bucket.arnForObjects("releases/*")],
192
+ }),
193
+ new iam.PolicyStatement({
194
+ sid: "PushAgentImage",
195
+ actions: [
196
+ "ecr:BatchCheckLayerAvailability",
197
+ "ecr:CompleteLayerUpload",
198
+ "ecr:InitiateLayerUpload",
199
+ "ecr:PutImage",
200
+ "ecr:UploadLayerPart",
201
+ "ecr:BatchGetImage",
202
+ "ecr:DescribeImages",
203
+ ],
204
+ resources: [this.repository.repositoryArn],
205
+ }),
206
+ new iam.PolicyStatement({
207
+ sid: "EcrLogin",
208
+ actions: ["ecr:GetAuthorizationToken"],
209
+ resources: ["*"],
210
+ }),
211
+ new iam.PolicyStatement({
212
+ sid: "SignManifest",
213
+ actions: ["kms:Sign", "kms:GetPublicKey", "kms:DescribeKey"],
214
+ resources: [this.signingKey.keyArn],
215
+ }),
216
+ ],
217
+ }),
218
+ },
219
+ });
220
+
221
+ // The three values a release needs: the workflow's variables, and the key
222
+ // ARN customers verify against.
223
+ new cdk.CfnOutput(this, "ReleaseBucketName", { value: this.bucket.bucketName });
224
+ new cdk.CfnOutput(this, "AgentRepositoryUri", { value: this.repository.repositoryUri });
225
+ new cdk.CfnOutput(this, "SigningKeyArn", { value: this.signingKey.keyArn });
226
+ new cdk.CfnOutput(this, "ReleaseRoleArn", { value: this.releaseRole.roleArn });
227
+ new cdk.CfnOutput(this, "CustomerAccountIds", { value: customers.join(",") });
228
+ }
229
+ }