@ossy/deployment-tools 3.0.9 → 3.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 (33) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +154 -118
  3. package/jest.config.js +5 -0
  4. package/package.json +6 -4
  5. package/src/config/platform-config.js +16 -16
  6. package/src/ecs/platform-ecs-services.js +111 -0
  7. package/src/ecs/platform-ecs-services.spec.js +77 -0
  8. package/src/edge/platform-edge-domains.js +101 -0
  9. package/src/edge/platform-edge-domains.spec.js +87 -0
  10. package/src/index.js +33 -1
  11. package/src/infrastructure/dns-stack.js +47 -3
  12. package/src/infrastructure/dns-stack.spec.js +15 -0
  13. package/src/infrastructure/platform-ci-stack.js +151 -0
  14. package/src/infrastructure/platform-ecs-stack.js +326 -0
  15. package/src/infrastructure/platform-edge-stack.js +213 -0
  16. package/src/infrastructure/platform-secrets-stack.js +103 -0
  17. package/src/infrastructure/platform-stage.js +32 -5
  18. package/src/infrastructure/storage-static-stack.js +4 -5
  19. package/src/secrets/platform-secret-services.js +32 -0
  20. package/src/secrets/platform-secret-services.spec.js +40 -0
  21. package/src/secrets/sync-platform-secrets.js +136 -0
  22. package/src/services/platform-services.js +94 -0
  23. package/src/services/platform-services.spec.js +80 -0
  24. package/src/template/platform-template.js +5 -5
  25. package/src/infrastructure/container-deployment-target/aws-profile.js +0 -25
  26. package/src/infrastructure/container-deployment-target/caddy.service.js +0 -117
  27. package/src/infrastructure/container-deployment-target/container-deployment-target.js +0 -222
  28. package/src/infrastructure/container-deployment-target/container-service.js +0 -162
  29. package/src/infrastructure/container-deployment-target/index.js +0 -3
  30. package/src/infrastructure/container-deployment-target/ossy-api.service.js +0 -54
  31. package/src/infrastructure/container-deployment-target/ossy-runtime.service.js +0 -49
  32. package/src/infrastructure/container-deployment-target/user-data-commands.js +0 -31
  33. package/src/infrastructure/deployment-target-stack.js +0 -53
@@ -0,0 +1,77 @@
1
+ const { describe, expect, it } = require('@jest/globals')
2
+ const {
3
+ RUNTIME_IMAGE,
4
+ hostsFromServiceEntry,
5
+ listPlatformEcsServices,
6
+ } = require('./platform-ecs-services')
7
+
8
+ describe('hostsFromServiceEntry', () => {
9
+ it('uses hosts when provided', () => {
10
+ expect(hostsFromServiceEntry({
11
+ domain: 'ossy.se',
12
+ hosts: ['ossy.se', 'api.ossy.se'],
13
+ })).toEqual(['ossy.se', 'api.ossy.se'])
14
+ })
15
+
16
+ it('falls back to domain', () => {
17
+ expect(hostsFromServiceEntry({ domain: 'ossy.se' })).toEqual(['ossy.se'])
18
+ })
19
+ })
20
+
21
+ describe('listPlatformEcsServices', () => {
22
+ it('includes runtime (default) and HTTP services; skips tcp and ossy-api', () => {
23
+ const services = listPlatformEcsServices({
24
+ platformName: 'ossybot',
25
+ domains: [
26
+ 'api.ossy.se',
27
+ 'ossy.se',
28
+ '*.ossy.se',
29
+ 'www.plexus-sanitas.com',
30
+ 'worker.ossy.se',
31
+ ],
32
+ services: [
33
+ {
34
+ name: 'ossy-website-ossy',
35
+ domain: 'ossy.se',
36
+ hosts: ['ossy.se', 'api.ossy.se'],
37
+ image: 'ghcr.io/ossy-se/website-ossy:latest',
38
+ },
39
+ {
40
+ name: 'ossy-website-plexus-sanitas',
41
+ domain: 'www.plexus-sanitas.com',
42
+ image: 'ghcr.io/ossy-se/website-plexus-sanitas:latest',
43
+ },
44
+ {
45
+ name: 'minecraft',
46
+ type: 'tcp',
47
+ image: 'itzg/minecraft-server',
48
+ ports: [25565],
49
+ },
50
+ ],
51
+ })
52
+
53
+ expect(services.map(s => s.key)).toEqual([
54
+ 'runtime',
55
+ 'website-ossy',
56
+ 'website-plexus-sanitas',
57
+ ])
58
+
59
+ const runtime = services.find(s => s.key === 'runtime')
60
+ expect(runtime.isDefault).toBe(true)
61
+ expect(runtime.image).toBe(RUNTIME_IMAGE)
62
+ expect(runtime.hosts).toEqual(['*.ossy.se', 'worker.ossy.se'])
63
+ expect(runtime.containerPort).toBe(3000)
64
+
65
+ const website = services.find(s => s.key === 'website-ossy')
66
+ expect(website.isDefault).toBe(false)
67
+ expect(website.hosts).toEqual(['ossy.se', 'api.ossy.se'])
68
+ expect(website.image).toBe('ghcr.io/ossy-se/website-ossy:latest')
69
+ expect(website.secretName).toBe('ossybot/website-ossy')
70
+
71
+ expect(services.some(s => s.key.includes('api'))).toBe(false)
72
+ })
73
+
74
+ it('requires platformName', () => {
75
+ expect(() => listPlatformEcsServices({})).toThrow(/platformName is required/)
76
+ })
77
+ })
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Derive CloudFront aliases + ACM certificate coverage from platforms.json.
3
+ *
4
+ * Part of #558: ACM certs for CloudFront must live in us-east-1 and cover every
5
+ * known platform hostname (apex + wildcards). Customer domains pointed at the
6
+ * EIP from a registrar (not in Route53) stay out of scope.
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} EdgeDomainPlan
11
+ * @property {string[]} aliases - CloudFront alternate domain names (viewer Host values)
12
+ * @property {string} primaryDomain - certificate primary domainName
13
+ * @property {string[]} subjectAlternativeNames - certificate SANs (excludes primary)
14
+ * @property {Map<string, string>} validationDomainToRoot - hostname → root domain for DNS validation zones
15
+ */
16
+
17
+ /**
18
+ * Root domain for Route53 hosted-zone lookup (last two labels).
19
+ * @param {string} domain
20
+ * @returns {string}
21
+ */
22
+ function rootDomainOf(domain) {
23
+ const bare = domain.replace(/^\*\./, '')
24
+ const parts = bare.split('.')
25
+ if (parts.length < 2) {
26
+ throw new Error(`[platform-edge-domains] cannot derive root domain from "${domain}"`)
27
+ }
28
+ return parts.slice(-2).join('.')
29
+ }
30
+
31
+ /**
32
+ * Unique, stable sorted list of CloudFront aliases from `domains` + HTTP service hosts.
33
+ *
34
+ * @param {{
35
+ * domains?: string[],
36
+ * services?: Array<{ type?: string, domain?: string, hosts?: string[] }>
37
+ * }} config
38
+ * @returns {string[]}
39
+ */
40
+ function listEdgeAliases(config) {
41
+ const fromDomains = (config.domains || []).filter(Boolean)
42
+ const fromServices = (config.services || [])
43
+ .filter(entry => (entry.type || 'http') === 'http')
44
+ .flatMap(entry => {
45
+ if (Array.isArray(entry.hosts)) return entry.hosts.filter(Boolean)
46
+ return entry.domain ? [entry.domain] : []
47
+ })
48
+
49
+ return [...new Set([...fromDomains, ...fromServices])].sort((a, b) =>
50
+ a.localeCompare(b)
51
+ )
52
+ }
53
+
54
+ /**
55
+ * Build ACM + CloudFront domain plan for a platform.
56
+ *
57
+ * Primary cert domain prefers an apex (no wildcard, not a subdomain) when present,
58
+ * otherwise the first alias. SANs cover the rest. Validation maps each name to its
59
+ * root hosted zone (wildcards validate on the parent zone).
60
+ *
61
+ * @param {{
62
+ * platformName?: string,
63
+ * domains?: string[],
64
+ * services?: Array<{ type?: string, domain?: string, hosts?: string[] }>
65
+ * }} config
66
+ * @returns {EdgeDomainPlan}
67
+ */
68
+ function planEdgeDomains(config) {
69
+ if (!config?.platformName) {
70
+ throw new Error('[platform-edge-domains] platformName is required')
71
+ }
72
+
73
+ const aliases = listEdgeAliases(config)
74
+ if (aliases.length === 0) {
75
+ throw new Error(
76
+ `[platform-edge-domains] platform "${config.platformName}" has no domains for CloudFront`
77
+ )
78
+ }
79
+
80
+ const apex = aliases.find(d => !d.includes('*') && d.split('.').length === 2)
81
+ const primaryDomain = apex || aliases[0]
82
+ const subjectAlternativeNames = aliases.filter(d => d !== primaryDomain)
83
+
84
+ /** @type {Map<string, string>} */
85
+ const validationDomainToRoot = new Map(
86
+ aliases.map(alias => [alias, rootDomainOf(alias)])
87
+ )
88
+
89
+ return {
90
+ aliases,
91
+ primaryDomain,
92
+ subjectAlternativeNames,
93
+ validationDomainToRoot,
94
+ }
95
+ }
96
+
97
+ module.exports = {
98
+ rootDomainOf,
99
+ listEdgeAliases,
100
+ planEdgeDomains,
101
+ }
@@ -0,0 +1,87 @@
1
+ const { describe, expect, it } = require('@jest/globals')
2
+ const {
3
+ rootDomainOf,
4
+ listEdgeAliases,
5
+ planEdgeDomains,
6
+ } = require('./platform-edge-domains')
7
+
8
+ describe('rootDomainOf', () => {
9
+ it('strips wildcards and returns the apex', () => {
10
+ expect(rootDomainOf('*.ossy.se')).toBe('ossy.se')
11
+ expect(rootDomainOf('api.ossy.se')).toBe('ossy.se')
12
+ expect(rootDomainOf('www.plexus-sanitas.com')).toBe('plexus-sanitas.com')
13
+ })
14
+ })
15
+
16
+ describe('listEdgeAliases', () => {
17
+ it('unions domains and HTTP service hosts; skips tcp', () => {
18
+ expect(listEdgeAliases({
19
+ domains: ['ossy.se', '*.ossy.se', 'worker.ossy.se'],
20
+ services: [
21
+ {
22
+ name: 'ossy-website-ossy',
23
+ domain: 'ossy.se',
24
+ hosts: ['ossy.se', 'api.ossy.se'],
25
+ },
26
+ {
27
+ name: 'minecraft',
28
+ type: 'tcp',
29
+ image: 'itzg/minecraft-server',
30
+ ports: [25565],
31
+ },
32
+ ],
33
+ })).toEqual(['*.ossy.se', 'api.ossy.se', 'ossy.se', 'worker.ossy.se'])
34
+ })
35
+ })
36
+
37
+ describe('planEdgeDomains', () => {
38
+ it('picks apex as primary and maps validation zones', () => {
39
+ const plan = planEdgeDomains({
40
+ platformName: 'ossybot',
41
+ domains: [
42
+ 'api.ossy.se',
43
+ 'ossy.se',
44
+ '*.ossy.se',
45
+ 'www.plexus-sanitas.com',
46
+ 'worker.ossy.se',
47
+ ],
48
+ services: [
49
+ {
50
+ name: 'ossy-website-ossy',
51
+ domain: 'ossy.se',
52
+ hosts: ['ossy.se', 'api.ossy.se'],
53
+ },
54
+ {
55
+ name: 'ossy-website-plexus-sanitas',
56
+ domain: 'www.plexus-sanitas.com',
57
+ },
58
+ ],
59
+ })
60
+
61
+ expect(plan.primaryDomain).toBe('ossy.se')
62
+ expect(plan.aliases).toEqual([
63
+ '*.ossy.se',
64
+ 'api.ossy.se',
65
+ 'ossy.se',
66
+ 'worker.ossy.se',
67
+ 'www.plexus-sanitas.com',
68
+ ])
69
+ expect(plan.subjectAlternativeNames).toEqual([
70
+ '*.ossy.se',
71
+ 'api.ossy.se',
72
+ 'worker.ossy.se',
73
+ 'www.plexus-sanitas.com',
74
+ ])
75
+ expect(plan.validationDomainToRoot.get('*.ossy.se')).toBe('ossy.se')
76
+ expect(plan.validationDomainToRoot.get('www.plexus-sanitas.com')).toBe(
77
+ 'plexus-sanitas.com'
78
+ )
79
+ })
80
+
81
+ it('requires platformName and at least one alias', () => {
82
+ expect(() => planEdgeDomains({})).toThrow(/platformName is required/)
83
+ expect(() => planEdgeDomains({ platformName: 'x', domains: [] })).toThrow(
84
+ /no domains for CloudFront/
85
+ )
86
+ })
87
+ })
package/src/index.js CHANGED
@@ -1,5 +1,37 @@
1
1
  const { PlatformTemplateService } = require('./template')
2
+ const {
3
+ listPlatformServices,
4
+ listHttpServiceEntries,
5
+ serviceKeyFromName,
6
+ secretNameFor,
7
+ } = require('./services/platform-services')
8
+ const { listPlatformSecretServices } = require('./secrets/platform-secret-services')
9
+ const { syncPlatformSecrets } = require('./secrets/sync-platform-secrets')
10
+ const {
11
+ listPlatformEcsServices,
12
+ hostsFromServiceEntry,
13
+ RUNTIME_IMAGE,
14
+ CONTAINER_PORT,
15
+ } = require('./ecs/platform-ecs-services')
16
+ const {
17
+ rootDomainOf,
18
+ listEdgeAliases,
19
+ planEdgeDomains,
20
+ } = require('./edge/platform-edge-domains')
2
21
 
3
22
  module.exports = {
4
- PlatformTemplateService
23
+ PlatformTemplateService,
24
+ listPlatformServices,
25
+ listHttpServiceEntries,
26
+ serviceKeyFromName,
27
+ secretNameFor,
28
+ listPlatformSecretServices,
29
+ syncPlatformSecrets,
30
+ listPlatformEcsServices,
31
+ hostsFromServiceEntry,
32
+ RUNTIME_IMAGE,
33
+ CONTAINER_PORT,
34
+ rootDomainOf,
35
+ listEdgeAliases,
36
+ planEdgeDomains,
5
37
  }
@@ -2,16 +2,44 @@ const { Stack, Duration } = require('aws-cdk-lib')
2
2
  const {
3
3
  HostedZone,
4
4
  ARecord,
5
+ AaaaRecord,
5
6
  MxRecord,
6
7
  RecordTarget
7
8
  } = require('aws-cdk-lib/aws-route53')
8
9
 
10
+ /** Global Route53 hosted zone id for CloudFront alias records. */
11
+ const CLOUDFRONT_HOSTED_ZONE_ID = 'Z2FDTNDATAQYW2'
12
+
13
+ /**
14
+ * Alias target for a CloudFront distribution domain name (cross-region safe).
15
+ * Avoids requiring the Distribution construct in the platform-region DnsStack.
16
+ */
17
+ class CloudFrontDomainTarget {
18
+ /**
19
+ * @param {string} domainName - e.g. d111111abcdef8.cloudfront.net
20
+ */
21
+ constructor(domainName) {
22
+ this.domainName = domainName
23
+ }
24
+
25
+ /**
26
+ * @returns {import('aws-cdk-lib/aws-route53').AliasRecordTargetConfig}
27
+ */
28
+ bind() {
29
+ // IAliasRecordTarget shape (not the raw CloudFormation AliasTargetProperty).
30
+ return {
31
+ hostedZoneId: CLOUDFRONT_HOSTED_ZONE_ID,
32
+ dnsName: this.domainName,
33
+ }
34
+ }
35
+ }
36
+
9
37
  /**
10
38
  * DnsStackProps
11
39
  * @namespace DnsStack
12
40
  * @typedef {Object} DnsStackProps
13
41
  * @property {PlatformConfig} config - platform config
14
- * @property {string} containerDeploymentTargetPublicIp - public ip address of ec2 instance
42
+ * @property {string} cloudFrontDomainName - CloudFront app distribution domain (#558 / #560)
15
43
  */
16
44
 
17
45
  /**
@@ -27,9 +55,13 @@ class DnsStack extends Stack {
27
55
  constructor(scope, id, props) {
28
56
  super(scope, id, props)
29
57
 
58
+ if (!props.cloudFrontDomainName) {
59
+ throw new Error('[DnsStack] cloudFrontDomainName is required')
60
+ }
61
+
30
62
  // Group domains by root domain so we can look up each hosted zone once.
31
63
  const domainsByRoot = (props.config.domains ?? []).reduce((map, domain) => {
32
- const parts = domain.split('.')
64
+ const parts = domain.replace(/^\*\./, '').split('.')
33
65
  const rootDomain = parts.slice(-2).join('.')
34
66
  map.has(rootDomain)
35
67
  ? map.set(rootDomain, [...map.get(rootDomain), domain])
@@ -41,10 +73,20 @@ class DnsStack extends Stack {
41
73
  const zone = HostedZone.fromLookup(this, `${rootDomain}-zone`, { domainName: rootDomain })
42
74
 
43
75
  domains.forEach(domain => {
76
+ // Known platform domains → CloudFront (not EIP). Alias A + AAAA.
77
+ const cloudFrontTarget = RecordTarget.fromAlias(
78
+ new CloudFrontDomainTarget(props.cloudFrontDomainName)
79
+ )
44
80
  new ARecord(this, `${domain}-record`, {
45
81
  zone,
46
82
  recordName: domain,
47
- target: RecordTarget.fromIpAddresses(props.containerDeploymentTargetPublicIp),
83
+ target: cloudFrontTarget,
84
+ ttl: Duration.seconds(60)
85
+ })
86
+ new AaaaRecord(this, `${domain}-aaaa-record`, {
87
+ zone,
88
+ recordName: domain,
89
+ target: cloudFrontTarget,
48
90
  ttl: Duration.seconds(60)
49
91
  })
50
92
  })
@@ -73,5 +115,7 @@ class DnsStack extends Stack {
73
115
  }
74
116
 
75
117
  module.exports = {
118
+ CLOUDFRONT_HOSTED_ZONE_ID,
119
+ CloudFrontDomainTarget,
76
120
  DnsStack
77
121
  }
@@ -0,0 +1,15 @@
1
+ const { describe, expect, it } = require('@jest/globals')
2
+ const {
3
+ CLOUDFRONT_HOSTED_ZONE_ID,
4
+ CloudFrontDomainTarget,
5
+ } = require('./dns-stack')
6
+
7
+ describe('CloudFrontDomainTarget', () => {
8
+ it('binds a flat AliasRecordTargetConfig for CloudFront', () => {
9
+ const target = new CloudFrontDomainTarget('d111111abcdef8.cloudfront.net')
10
+ expect(target.bind()).toEqual({
11
+ hostedZoneId: CLOUDFRONT_HOSTED_ZONE_ID,
12
+ dnsName: 'd111111abcdef8.cloudfront.net',
13
+ })
14
+ })
15
+ })
@@ -0,0 +1,151 @@
1
+ /* eslint-disable no-new */
2
+ const { CfnOutput, Stack, Duration } = require('aws-cdk-lib')
3
+ const {
4
+ Effect,
5
+ OpenIdConnectProvider,
6
+ PolicyStatement,
7
+ Role,
8
+ WebIdentityPrincipal,
9
+ } = require('aws-cdk-lib/aws-iam')
10
+
11
+ /**
12
+ * @typedef {Object} PlatformCiStackProps
13
+ * @property {import('../config/platform-config').PlatformConfig} config
14
+ */
15
+
16
+ /**
17
+ * GitHub Actions → AWS OIDC deploy role for ECS rollouts (#559).
18
+ *
19
+ * Creates an OIDC provider for `token.actions.githubusercontent.com` and a
20
+ * platform-scoped role that can register task definitions and update Fargate
21
+ * services. No long-lived access keys in GitHub.
22
+ *
23
+ * Trust is limited to `ref:refs/heads/main` for `config.githubDeployRepos`
24
+ * (set in platforms.json — add a repo there to allow it to assume this role).
25
+ *
26
+ * Note: AWS allows only one OIDC provider per URL per account. This stack is
27
+ * intended for the primary platform stage until a shared account bootstrap exists.
28
+ */
29
+ class PlatformCiStack extends Stack {
30
+ /**
31
+ * @param {object} scope
32
+ * @param {string} id
33
+ * @param {PlatformCiStackProps} props
34
+ */
35
+ constructor(scope, id, props) {
36
+ super(scope, id, props)
37
+
38
+ if (!props?.config?.platformName) {
39
+ throw new Error('[PlatformCiStack] config.platformName is required')
40
+ }
41
+
42
+ const platformName = props.config.platformName
43
+ const account = props.env?.account ?? props.config.awsAccountId
44
+ const region = props.env?.region ?? props.config.awsRegion
45
+ const clusterName = `${platformName}-platform`
46
+ const roleName = `${platformName}-github-deploy`.slice(0, 64)
47
+ const githubDeployRepos = props.config.githubDeployRepos
48
+
49
+ if (!Array.isArray(githubDeployRepos) || githubDeployRepos.length === 0) {
50
+ throw new Error(
51
+ '[PlatformCiStack] config.githubDeployRepos must be a non-empty array in platforms.json'
52
+ )
53
+ }
54
+
55
+ const oidcSubjects = githubDeployRepos.map(repo => {
56
+ if (!repo || typeof repo !== 'string' || !repo.includes('/')) {
57
+ throw new Error(
58
+ `[PlatformCiStack] invalid githubDeployRepos entry "${repo}" (expected owner/repo)`
59
+ )
60
+ }
61
+ return `repo:${repo}:ref:refs/heads/main`
62
+ })
63
+
64
+ const provider = new OpenIdConnectProvider(this, 'GithubOidcProvider', {
65
+ url: 'https://token.actions.githubusercontent.com',
66
+ clientIds: ['sts.amazonaws.com'],
67
+ })
68
+
69
+ const deployRole = new Role(this, 'GithubDeployRole', {
70
+ roleName,
71
+ description: `GitHub Actions OIDC deploy role for ${platformName} ECS (#559)`,
72
+ maxSessionDuration: Duration.hours(1),
73
+ assumedBy: new WebIdentityPrincipal(
74
+ provider.openIdConnectProviderArn,
75
+ {
76
+ StringEquals: {
77
+ 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
78
+ },
79
+ StringLike: {
80
+ 'token.actions.githubusercontent.com:sub': oidcSubjects,
81
+ },
82
+ }
83
+ ),
84
+ })
85
+
86
+ const clusterArn = `arn:aws:ecs:${region}:${account}:cluster/${clusterName}`
87
+ const serviceArn = `arn:aws:ecs:${region}:${account}:service/${clusterName}/*`
88
+ const taskArn = `arn:aws:ecs:${region}:${account}:task/${clusterName}/*`
89
+
90
+ deployRole.addToPolicy(new PolicyStatement({
91
+ sid: 'EcsDeployRead',
92
+ effect: Effect.ALLOW,
93
+ actions: [
94
+ 'ecs:DescribeClusters',
95
+ 'ecs:DescribeServices',
96
+ 'ecs:DescribeTasks',
97
+ 'ecs:ListTasks',
98
+ ],
99
+ resources: [clusterArn, serviceArn, taskArn],
100
+ }))
101
+
102
+ // Task-definition APIs are not cluster-scoped in IAM the same way services are.
103
+ deployRole.addToPolicy(new PolicyStatement({
104
+ sid: 'EcsTaskDefinitions',
105
+ effect: Effect.ALLOW,
106
+ actions: [
107
+ 'ecs:DescribeTaskDefinition',
108
+ 'ecs:RegisterTaskDefinition',
109
+ ],
110
+ resources: ['*'],
111
+ }))
112
+
113
+ deployRole.addToPolicy(new PolicyStatement({
114
+ sid: 'EcsUpdateService',
115
+ effect: Effect.ALLOW,
116
+ actions: ['ecs:UpdateService'],
117
+ resources: [serviceArn],
118
+ }))
119
+
120
+ deployRole.addToPolicy(new PolicyStatement({
121
+ sid: 'PassEcsRoles',
122
+ effect: Effect.ALLOW,
123
+ actions: ['iam:PassRole'],
124
+ resources: [`arn:aws:iam::${account}:role/${platformName}-*`],
125
+ conditions: {
126
+ StringEquals: {
127
+ 'iam:PassedToService': 'ecs-tasks.amazonaws.com',
128
+ },
129
+ },
130
+ }))
131
+
132
+ this.deployRole = deployRole
133
+ this.provider = provider
134
+
135
+ new CfnOutput(this, 'GithubDeployRoleArn', {
136
+ value: deployRole.roleArn,
137
+ description: 'IAM role ARN for GitHub Actions OIDC ECS deploys (#559)',
138
+ exportName: `${platformName}-GithubDeployRoleArn`,
139
+ })
140
+
141
+ new CfnOutput(this, 'GithubDeployRoleName', {
142
+ value: roleName,
143
+ description: 'IAM role name for GitHub Actions OIDC ECS deploys',
144
+ exportName: `${platformName}-GithubDeployRoleName`,
145
+ })
146
+ }
147
+ }
148
+
149
+ module.exports = {
150
+ PlatformCiStack,
151
+ }