@ossy/deployment-tools 3.4.0 → 3.6.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [3.6.0](https://github.com/ossy-se/ossy/compare/v3.5.1...v3.6.0) (2026-08-16)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **deployment-tools:** stabilize ECS cutover edge, OIDC, and ALB rules ([#657](https://github.com/ossy-se/ossy/issues/657)) ([257bd79](https://github.com/ossy-se/ossy/commit/257bd7965d86a09aae2b8f285c5e22e2d29160bb))
11
+
12
+
6
13
  # [3.4.0](https://github.com/ossy-se/ossy/compare/v3.3.0...v3.4.0) (2026-08-14)
7
14
 
8
15
  ### Features
package/cdk.context.json CHANGED
@@ -84,5 +84,18 @@
84
84
  "hosted-zone:account=858451553223:domainName=tepit.se:region=eu-north-1": {
85
85
  "Id": "/hostedzone/Z07859112SU1GH5SKMUXD",
86
86
  "Name": "tepit.se."
87
+ },
88
+ "availability-zones:account=858451553223:region=eu-north-1": [
89
+ "eu-north-1a",
90
+ "eu-north-1b",
91
+ "eu-north-1c"
92
+ ],
93
+ "hosted-zone:account=858451553223:domainName=ossy.se:region=us-east-1": {
94
+ "Id": "/hostedzone/Z09424571CVOTIJV8Z50C",
95
+ "Name": "ossy.se."
96
+ },
97
+ "hosted-zone:account=858451553223:domainName=plexus-sanitas.com:region=us-east-1": {
98
+ "Id": "/hostedzone/Z0222639MQIJ8EOL9VAG",
99
+ "Name": "plexus-sanitas.com."
87
100
  }
88
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ossy/deployment-tools",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "description": "Collection of scripts and tools to aid deployment of containers and static files to Amazon Web Services through GitHub Actions",
5
5
  "source": "./src/index.js",
6
6
  "main": "./src/index.js",
@@ -24,5 +24,5 @@
24
24
  "devDependencies": {
25
25
  "jest": "^30.4.2"
26
26
  },
27
- "gitHead": "d36b69444268d172bc3e8e1dc77e67afc63f8650"
27
+ "gitHead": "d1fd144986b0e819d46926aeb11f7815ac52a784"
28
28
  }
@@ -28,8 +28,29 @@ function rootDomainOf(domain) {
28
28
  return parts.slice(-2).join('.')
29
29
  }
30
30
 
31
+ /**
32
+ * Whether `hostname` is already covered by a `*.root` wildcard in `aliases`.
33
+ * CloudFront rejects overlapping CNAMEs (e.g. both `*.ossy.se` and `api.ossy.se`).
34
+ *
35
+ * @param {string} hostname
36
+ * @param {string[]} aliases
37
+ * @returns {boolean}
38
+ */
39
+ function coveredByWildcardAlias(hostname, aliases) {
40
+ if (!hostname || hostname.includes('*')) return false
41
+ return aliases.some(alias => {
42
+ if (!alias.startsWith('*.')) return false
43
+ const suffix = alias.slice(1) // e.g. ".ossy.se"
44
+ if (!hostname.endsWith(suffix)) return false
45
+ const prefix = hostname.slice(0, -suffix.length)
46
+ // single-label subdomain only (*.ossy.se covers api.ossy.se, not a.b.ossy.se)
47
+ return prefix.length > 0 && !prefix.includes('.')
48
+ })
49
+ }
50
+
31
51
  /**
32
52
  * Unique, stable sorted list of CloudFront aliases from `domains` + HTTP service hosts.
53
+ * Drops hostnames covered by a sibling wildcard so ACM/CloudFront accept the set.
33
54
  *
34
55
  * @param {{
35
56
  * domains?: string[],
@@ -46,9 +67,10 @@ function listEdgeAliases(config) {
46
67
  return entry.domain ? [entry.domain] : []
47
68
  })
48
69
 
49
- return [...new Set([...fromDomains, ...fromServices])].sort((a, b) =>
50
- a.localeCompare(b)
51
- )
70
+ const all = [...new Set([...fromDomains, ...fromServices])]
71
+ return all
72
+ .filter(alias => !coveredByWildcardAlias(alias, all))
73
+ .sort((a, b) => a.localeCompare(b))
52
74
  }
53
75
 
54
76
  /**
@@ -14,7 +14,7 @@ describe('rootDomainOf', () => {
14
14
  })
15
15
 
16
16
  describe('listEdgeAliases', () => {
17
- it('unions domains and HTTP service hosts; skips tcp', () => {
17
+ it('unions domains and HTTP service hosts; skips tcp; drops names under wildcards', () => {
18
18
  expect(listEdgeAliases({
19
19
  domains: ['ossy.se', '*.ossy.se', 'worker.ossy.se'],
20
20
  services: [
@@ -30,7 +30,7 @@ describe('listEdgeAliases', () => {
30
30
  ports: [25565],
31
31
  },
32
32
  ],
33
- })).toEqual(['*.ossy.se', 'api.ossy.se', 'ossy.se', 'worker.ossy.se'])
33
+ })).toEqual(['*.ossy.se', 'ossy.se'])
34
34
  })
35
35
  })
36
36
 
@@ -59,17 +59,14 @@ describe('planEdgeDomains', () => {
59
59
  })
60
60
 
61
61
  expect(plan.primaryDomain).toBe('ossy.se')
62
+ // api/worker covered by *.ossy.se — CloudFront forbids overlapping CNAMEs
62
63
  expect(plan.aliases).toEqual([
63
64
  '*.ossy.se',
64
- 'api.ossy.se',
65
65
  'ossy.se',
66
- 'worker.ossy.se',
67
66
  'www.plexus-sanitas.com',
68
67
  ])
69
68
  expect(plan.subjectAlternativeNames).toEqual([
70
69
  '*.ossy.se',
71
- 'api.ossy.se',
72
- 'worker.ossy.se',
73
70
  'www.plexus-sanitas.com',
74
71
  ])
75
72
  expect(plan.validationDomainToRoot.get('*.ossy.se')).toBe('ossy.se')
@@ -23,8 +23,9 @@ const {
23
23
  * Trust is limited to `ref:refs/heads/main` for `config.githubDeployRepos`
24
24
  * (set in platforms.json — add a repo there to allow it to assume this role).
25
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.
26
+ * Note: AWS allows only one OIDC provider per URL per account. This stack
27
+ * references the account provider (often created earlier by `trust-ci`) instead
28
+ * of creating a second one.
28
29
  */
29
30
  class PlatformCiStack extends Stack {
30
31
  /**
@@ -61,10 +62,12 @@ class PlatformCiStack extends Stack {
61
62
  return `repo:${repo}:ref:refs/heads/main`
62
63
  })
63
64
 
64
- const provider = new OpenIdConnectProvider(this, 'GithubOidcProvider', {
65
- url: 'https://token.actions.githubusercontent.com',
66
- clientIds: ['sts.amazonaws.com'],
67
- })
65
+ // One provider per issuer URL per account — reuse the existing GitHub OIDC IdP.
66
+ const provider = OpenIdConnectProvider.fromOpenIdConnectProviderArn(
67
+ this,
68
+ 'GithubOidcProvider',
69
+ `arn:aws:iam::${account}:oidc-provider/token.actions.githubusercontent.com`
70
+ )
68
71
 
69
72
  const deployRole = new Role(this, 'GithubDeployRole', {
70
73
  roleName,
@@ -281,23 +281,18 @@ class PlatformEcsStack extends Stack {
281
281
 
282
282
  // Lower priority number = evaluated first. Specific hosts before wildcards
283
283
  // so `api.ossy.se` is not swallowed by `*.ossy.se`.
284
- const orderedHostRules = [...hostRules].sort((a, b) => {
285
- const aWild = a.host.includes('*') ? 1 : 0
286
- const bWild = b.host.includes('*') ? 1 : 0
287
- return aWild - bWild
288
- })
289
-
290
- let rulePriority = 10
291
- for (const rule of orderedHostRules) {
284
+ // Priorities are hash-stable per host — sequential 10,11,12… renumbers
285
+ // every insert and CloudFormation cannot swap ALB priorities in one update.
286
+ const usedPriorities = new Set()
287
+ for (const rule of hostRules) {
292
288
  listener.addTargetGroups(
293
289
  `Rule${rule.idSuffix}${toConstructId(rule.host)}`,
294
290
  {
295
- priority: rulePriority,
291
+ priority: albHostRulePriority(rule.host, usedPriorities),
296
292
  conditions: [ListenerCondition.hostHeaders([rule.host])],
297
293
  targetGroups: [rule.targetGroup],
298
294
  }
299
295
  )
300
- rulePriority += 1
301
296
  }
302
297
 
303
298
  this.vpc = vpc
@@ -325,6 +320,29 @@ class PlatformEcsStack extends Stack {
325
320
  }
326
321
  }
327
322
 
323
+ /**
324
+ * Stable ALB listener-rule priority for a host header (1–50000).
325
+ * Specific hosts: 10000–29999; wildcards: 40000–49999 (evaluated later).
326
+ * @param {string} host
327
+ * @param {Set<number>} used
328
+ * @returns {number}
329
+ */
330
+ function albHostRulePriority(host, used) {
331
+ const band = host.includes('*') ? 40000 : 10000
332
+ let hash = 2166136261
333
+ for (let i = 0; i < host.length; i += 1) {
334
+ hash ^= host.charCodeAt(i)
335
+ hash = Math.imul(hash, 16777619) >>> 0
336
+ }
337
+ let priority = band + (hash % 20000)
338
+ while (used.has(priority)) {
339
+ priority += 1
340
+ if (priority >= band + 20000) priority = band
341
+ }
342
+ used.add(priority)
343
+ return priority
344
+ }
345
+
328
346
  /**
329
347
  * CloudFormation-safe construct id fragment.
330
348
  * @param {string} key
@@ -346,4 +364,5 @@ function truncateAwsName(name, max) {
346
364
 
347
365
  module.exports = {
348
366
  PlatformEcsStack,
367
+ albHostRulePriority,
349
368
  }