Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: seek-oss/skuba
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: skuba@9.1.0
Choose a base ref
...
head repository: seek-oss/skuba
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: skuba@10.0.0
Choose a head ref

Commits on Oct 21, 2024

  1. devDeps: memfs (#1716)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Oct 21, 2024
    Copy the full SHA
    f6c989b View commit details
  2. template: @types/express (#1717)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Oct 21, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    defo89 Dmitri Fedotov
    Copy the full SHA
    8a1cbc5 View commit details

Commits on Oct 26, 2024

  1. template: opentelemetry (#1718)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Oct 26, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    6ebc53b View commit details

Commits on Oct 28, 2024

  1. update: dependency pnpm to v9.12.3 (#1719)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Oct 28, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    sxd Jonathan Gonzalez V.
    Copy the full SHA
    cfb261a View commit details

Commits on Oct 31, 2024

  1. Document Hidden Feature (#1723)

    Co-authored-by: skuba <34733141+seek-oss-ci@users.noreply.github.com>
    Co-authored-by: Ryan Ling <ryan@outlook.com.au>
    3 people authored Oct 31, 2024

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    eb23b2c View commit details

Commits on Nov 7, 2024

  1. template: seek-oss/docker-ecr-cache v2.2.1 (#1725)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 7, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    e9d1d57 View commit details
  2. update: dependency seek-oss/docker-ecr-cache to v2.2.1 (#1726)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 7, 2024

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    5af09fd View commit details

Commits on Nov 11, 2024

  1. improve cdk template (#1724)

    Co-authored-by: samchungy <samchungy@gmail.com>
    Co-authored-by: Sam Chung <samc@seek.com.au>
    3 people authored Nov 11, 2024

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    c80e5d9 View commit details
  2. template: docker/dockerfile 1.11 (#1728)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 11, 2024

    Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    c6fdb08 View commit details
  3. template: @opentelemetry/instrumentation-aws-sdk ^0.46.0 (#1727)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 11, 2024
    Copy the full SHA
    5ed1154 View commit details

Commits on Nov 12, 2024

  1. update: dependency docker to v5.12.0 (#1731)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 12, 2024
    Copy the full SHA
    fa61cdf View commit details
  2. template: seek-oss/private-npm v1.3.0 (#1730)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 12, 2024
    Copy the full SHA
    3051bb6 View commit details
  3. update: dependency seek-oss/private-npm to v1.3.0 (#1734)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 12, 2024
    Copy the full SHA
    41ad982 View commit details

Commits on Nov 13, 2024

  1. update: dependency pnpm to v9.13.0 (#1736)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 13, 2024
    Copy the full SHA
    fabf51f View commit details

Commits on Nov 14, 2024

  1. Fix crash in configure when detecting whether the working tree is cle…

    …an (#1737)
    AaronMoat authored Nov 14, 2024
    Copy the full SHA
    aa32a16 View commit details

Commits on Nov 15, 2024

  1. update: dependency pnpm to v9.13.2 (#1738)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 15, 2024
    Copy the full SHA
    3e0888f View commit details

Commits on Nov 17, 2024

  1. update: lock file maintenance (#1739)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    Co-authored-by: AaronMoat <2937187+AaronMoat@users.noreply.github.com>
    renovate[bot] and AaronMoat authored Nov 17, 2024
    Copy the full SHA
    1274b96 View commit details

Commits on Nov 18, 2024

  1. Bump aws-cdk to latest due to Potential Account Takeover Risks Vulner…

    …ability (#1740)
    
    Co-authored-by: Aaron Moat <2937187+AaronMoat@users.noreply.github.com>
    NathanLamSeekasia and AaronMoat authored Nov 18, 2024
    Copy the full SHA
    62f0799 View commit details
  2. update: lock file maintenance (#1743)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 18, 2024
    Copy the full SHA
    e3a6e74 View commit details
  3. devDeps: fastify (#1741)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 18, 2024
    Copy the full SHA
    14b83ab View commit details
  4. template: pino-pretty (#1742)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 18, 2024
    Copy the full SHA
    133bb88 View commit details

Commits on Nov 19, 2024

  1. update: lock file maintenance (#1744)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 19, 2024
    Copy the full SHA
    00dd9f0 View commit details
  2. update: lock file maintenance (#1745)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 19, 2024
    Copy the full SHA
    808fbf5 View commit details
  3. update: lock file maintenance (#1746)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 19, 2024
    Copy the full SHA
    f9583cf View commit details

Commits on Nov 21, 2024

  1. update: dependency pnpm to v9.14.2 (#1748)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 21, 2024
    Copy the full SHA
    fc7f042 View commit details

Commits on Nov 24, 2024

  1. deps: eslint-plugin-tsdoc ^0.4.0 (#1749)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 24, 2024
    Copy the full SHA
    894752b View commit details

Commits on Nov 26, 2024

  1. deps: typescript ~5.7.0 (#1750)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    Co-authored-by: Sam Chung <samc@seek.com.au>
    Co-authored-by: samchungy <samchungy@gmail.com>
    3 people authored Nov 26, 2024
    Copy the full SHA
    1cb4b77 View commit details
  2. deps: prettier ~3.4.0 (#1751)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    Co-authored-by: Aaron Moat <2937187+AaronMoat@users.noreply.github.com>
    renovate[bot] and AaronMoat authored Nov 26, 2024
    Copy the full SHA
    6c612b0 View commit details

Commits on Nov 27, 2024

  1. template: opentelemetry (#1747)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Nov 27, 2024
    Copy the full SHA
    fee2294 View commit details

Commits on Dec 17, 2024

  1. template: opentelemetry (#1754)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 17, 2024
    Copy the full SHA
    0be1dd7 View commit details

Commits on Dec 18, 2024

  1. update: dependency docker-compose to v5.5.0 (#1755)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 18, 2024
    Copy the full SHA
    05cc0dd View commit details
  2. template: docker-compose v5.5.0 (#1752)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 18, 2024
    Copy the full SHA
    3e1304a View commit details
  3. template: docker/dockerfile 1.12 (#1753)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 18, 2024
    Copy the full SHA
    c6dda92 View commit details

Commits on Dec 20, 2024

  1. Drop support for failOnScanFindings for gantry 4 support (#1759)

    AaronMoat authored Dec 20, 2024
    Copy the full SHA
    5cfcb70 View commit details

Commits on Dec 29, 2024

  1. update: dependency pnpm to v9.15.2 (#1757)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 29, 2024
    Copy the full SHA
    c1e83c5 View commit details
  2. template: opentelemetry (#1756)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 29, 2024
    Copy the full SHA
    4aaaa4b View commit details

Commits on Dec 30, 2024

  1. devDeps: npm dev dependencies (#1760)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 30, 2024
    Copy the full SHA
    224cb62 View commit details
  2. deps: ignore ^7.0.0 (#1762)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Dec 30, 2024
    Copy the full SHA
    651123d View commit details

Commits on Jan 7, 2025

  1. deps: drop serialize-error

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    Co-authored-by: Ryan Ling <ryan@outlook.com.au>
    Co-authored-by: Aaron Moat <2937187+AaronMoat@users.noreply.github.com>
    3 people authored Jan 7, 2025
    Copy the full SHA
    9a98943 View commit details

Commits on Jan 8, 2025

  1. update: dependency pnpm to v9.15.3 (#1764)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jan 8, 2025
    Copy the full SHA
    fee1374 View commit details

Commits on Jan 15, 2025

  1. Fix self lint (#1767)

    72636c authored Jan 15, 2025
    Copy the full SHA
    e21e537 View commit details
  2. Try to fix ESLint ConfigErrors (#1766)

    Co-authored-by: skuba <34733141+seek-oss-ci@users.noreply.github.com>
    72636c and seek-oss-ci authored Jan 15, 2025
    Copy the full SHA
    5c49b90 View commit details
  3. Copy the full SHA
    1c9befa View commit details
  4. update: lock file maintenance (#1765)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    Co-authored-by: Ryan Ling <ryan@outlook.com.au>
    renovate[bot] and 72636c authored Jan 15, 2025
    Copy the full SHA
    a6b76c5 View commit details
  5. update: dependency pnpm to v9.15.4 (#1770)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jan 15, 2025
    Copy the full SHA
    631b554 View commit details
  6. RFC: Try to remove ESLint languageOptions (#1769)

    72636c authored Jan 15, 2025
    Copy the full SHA
    fc74c35 View commit details

Commits on Jan 16, 2025

  1. template: docker-compose v5.6.0 (#1771)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jan 16, 2025
    Copy the full SHA
    d0443bd View commit details

Commits on Jan 19, 2025

  1. devDeps: @types/express (#1761)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    Co-authored-by: Ryan Ling <ryan@outlook.com.au>
    renovate[bot] and 72636c authored Jan 19, 2025
    Copy the full SHA
    65af6e4 View commit details
  2. update: dependency docker-compose to v5.6.0 (#1772)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jan 19, 2025
    Copy the full SHA
    4589cd6 View commit details

Commits on Jan 22, 2025

  1. template: docker/dockerfile 1.13 (#1773)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Jan 22, 2025
    Copy the full SHA
    b8cdc43 View commit details
Showing with 5,914 additions and 10,546 deletions.
  1. +10 −10 .github/renovate.json5
  2. +1 −1 .github/workflows/release.yml
  3. +1 −1 .github/workflows/snapshot.yml
  4. +5 −4 .github/workflows/validate.yml
  5. +1 −1 .nvmrc
  6. +94 −0 CHANGELOG.md
  7. +22 −0 docs/cli/build.md
  8. +0 −1 docs/cli/init.md
  9. +27 −0 docs/cli/lint.md
  10. +128 −11 docs/cli/migrate.md
  11. +1 −1 docs/deep-dives/arm64.md
  12. +2 −2 docs/deep-dives/buildkite.md
  13. +2 −2 docs/deep-dives/github.md
  14. +3 −3 docs/deep-dives/pnpm.md
  15. +0 −14 docs/templates/worker.md
  16. +20 −22 package.json
  17. +27 −0 packages/eslint-config-skuba/CHANGELOG.md
  18. +8 −22 packages/eslint-config-skuba/index.js
  19. +4 −5 packages/eslint-config-skuba/package.json
  20. +0 −1 packages/skuba-dive/eslint.config.js
  21. +3 −0 packages/skuba-dive/eslint.config.mjs
  22. +4,145 −8,704 pnpm-lock.yaml
  23. +21 −1 src/cli/__snapshots__/format.int.test.ts.snap
  24. +1 −1 src/cli/__snapshots__/lint.int.test.ts.snap
  25. +6 −1 src/cli/adapter/prettier.ts
  26. +6 −2 src/cli/configure/analyseDependencies.ts
  27. +16 −1 src/cli/configure/analysis/git.ts
  28. +1 −1 src/cli/configure/processing/module.test.ts
  29. +5 −0 src/cli/format.int.test.ts
  30. +2 −2 src/cli/init/index.ts
  31. +6 −1 src/cli/lint/annotate/buildkite/prettier.ts
  32. +4 −1 src/cli/lint/annotate/github/prettier.ts
  33. +2 −1 src/cli/lint/internal.ts
  34. +107 −0 src/cli/lint/internalLints/detectBadCodeowners.test.ts
  35. +60 −0 src/cli/lint/internalLints/detectBadCodeowners.ts
  36. +1 −1 src/cli/lint/internalLints/upgrade/patches/7.3.1/moveNpmrcOutOfIgnoreManagedSection.test.ts
  37. +10 −0 src/cli/lint/internalLints/upgrade/patches/9.1.0/index.ts
  38. +37 −0 src/cli/lint/internalLints/upgrade/patches/9.1.0/upgradeNode.ts
  39. +12 −1 src/cli/migrate/index.ts
  40. +286 −0 src/cli/migrate/nodeVersion/checks.test.ts
  41. +151 −0 src/cli/migrate/nodeVersion/checks.ts
  42. +92 −0 src/cli/migrate/nodeVersion/getNodeTypesVersion.test.ts
  43. +48 −0 src/cli/migrate/nodeVersion/getNodeTypesVersion.ts
  44. +151 −29 src/cli/migrate/nodeVersion/index.test.ts
  45. +159 −39 src/cli/migrate/nodeVersion/index.ts
  46. +25 −3 src/utils/copy.test.ts
  47. +12 −5 src/utils/copy.ts
  48. +0 −5 src/utils/template.ts
  49. +55 −16 src/utils/version.ts
  50. +11 −11 src/wrapper/http.ts
  51. +5 −21 src/wrapper/main.test.ts
  52. +4 −2 src/wrapper/testing/expressRequestListener.ts
  53. +2 −2 template/base/tsconfig.json
  54. +7 −7 template/express-rest-api/.buildkite/pipeline.yml
  55. +1 −3 template/express-rest-api/.gantry/common.yml
  56. +1 −1 template/express-rest-api/.nvmrc
  57. +1 −1 template/express-rest-api/Dockerfile
  58. +2 −2 template/express-rest-api/Dockerfile.dev-deps
  59. +0 −3 template/express-rest-api/gantry.build.yml
  60. +6 −6 template/express-rest-api/package.json
  61. +3 −3 template/greeter/.buildkite/pipeline.yml
  62. +1 −1 template/greeter/.nvmrc
  63. +2 −2 template/greeter/Dockerfile
  64. +1 −1 template/greeter/README.md
  65. +3 −3 template/greeter/package.json
  66. +7 −7 template/koa-rest-api/.buildkite/pipeline.yml
  67. +1 −3 template/koa-rest-api/.gantry/common.yml
  68. +1 −1 template/koa-rest-api/.nvmrc
  69. +1 −1 template/koa-rest-api/Dockerfile
  70. +2 −2 template/koa-rest-api/Dockerfile.dev-deps
  71. +0 −3 template/koa-rest-api/gantry.build.yml
  72. +8 −8 template/koa-rest-api/package.json
  73. +0 −1 template/koa-rest-api/src/framework/server.test.ts
  74. +2 −2 template/koa-rest-api/tsconfig.json
  75. +5 −5 template/lambda-sqs-worker-cdk/.buildkite/pipeline.yml
  76. +1 −1 template/lambda-sqs-worker-cdk/.nvmrc
  77. +3 −3 template/lambda-sqs-worker-cdk/Dockerfile
  78. +4 −3 template/lambda-sqs-worker-cdk/README.md
  79. +16 −4 template/lambda-sqs-worker-cdk/infra/__snapshots__/appStack.test.ts.snap
  80. +5 −3 template/lambda-sqs-worker-cdk/infra/appStack.test.ts
  81. +6 −4 template/lambda-sqs-worker-cdk/infra/appStack.ts
  82. +1 −1 template/lambda-sqs-worker-cdk/infra/config.ts
  83. +3 −5 template/lambda-sqs-worker-cdk/infra/index.ts
  84. +9 −8 template/lambda-sqs-worker-cdk/package.json
  85. +2 −2 template/lambda-sqs-worker-cdk/tsconfig.json
  86. +0 −108 template/lambda-sqs-worker/.buildkite/pipeline.yml
  87. +0 −1 template/lambda-sqs-worker/.env
  88. +0 −1 template/lambda-sqs-worker/.nvmrc
  89. +0 −17 template/lambda-sqs-worker/Dockerfile
  90. +0 −132 template/lambda-sqs-worker/README.md
  91. +0 −13 template/lambda-sqs-worker/_.npmrc
  92. +0 −10 template/lambda-sqs-worker/docker-compose.yml
  93. +0 −45 template/lambda-sqs-worker/package.json
  94. +0 −213 template/lambda-sqs-worker/serverless.yml
  95. +0 −33 template/lambda-sqs-worker/skuba.template.js
  96. +0 −116 template/lambda-sqs-worker/src/app.test.ts
  97. +0 −57 template/lambda-sqs-worker/src/app.ts
  98. +0 −62 template/lambda-sqs-worker/src/config.ts
  99. +0 −61 template/lambda-sqs-worker/src/framework/handler.test.ts
  100. +0 −43 template/lambda-sqs-worker/src/framework/handler.ts
  101. +0 −27 template/lambda-sqs-worker/src/framework/logging.ts
  102. +0 −14 template/lambda-sqs-worker/src/framework/metrics.ts
  103. +0 −84 template/lambda-sqs-worker/src/framework/validation.test.ts
  104. +0 −10 template/lambda-sqs-worker/src/framework/validation.ts
  105. +0 −95 template/lambda-sqs-worker/src/hooks.ts
  106. +0 −22 template/lambda-sqs-worker/src/mapping/jobScorer.ts
  107. +0 −5 template/lambda-sqs-worker/src/services/aws.ts
  108. +0 −44 template/lambda-sqs-worker/src/services/jobScorer.test.ts
  109. +0 −59 template/lambda-sqs-worker/src/services/jobScorer.ts
  110. +0 −40 template/lambda-sqs-worker/src/services/pipelineEventSender.test.ts
  111. +0 −33 template/lambda-sqs-worker/src/services/pipelineEventSender.ts
  112. +0 −13 template/lambda-sqs-worker/src/testing/handler.ts
  113. +0 −19 template/lambda-sqs-worker/src/testing/logging.ts
  114. +0 −28 template/lambda-sqs-worker/src/testing/services.ts
  115. +0 −33 template/lambda-sqs-worker/src/testing/types.ts
  116. +0 −15 template/lambda-sqs-worker/src/types/jobScorer.ts
  117. +0 −21 template/lambda-sqs-worker/src/types/pipelineEvents.ts
  118. +0 −13 template/lambda-sqs-worker/tsconfig.json
  119. +1 −1 template/oss-npm-package/.github/workflows/release.yml
  120. +1 −1 template/oss-npm-package/.github/workflows/validate.yml
  121. +1 −1 template/oss-npm-package/.nvmrc
  122. +1 −1 template/oss-npm-package/_package.json
  123. +1 −1 template/private-npm-package/.nvmrc
  124. +2 −2 template/private-npm-package/_package.json
20 changes: 10 additions & 10 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@
},
{
matchManagers: ['npm'],
matchPackagePatterns: ['^@opentelemetry/'],
matchPackageNames: ['/^@opentelemetry//'],
groupName: 'opentelemetry',
},
{
@@ -41,7 +41,7 @@
// Should be synchronised with AWS Lambda runtimes and Docker images
'@types/node',
],
matchPaths: ['template/**'],
matchFileNames: ['template/**'],

enabled: false,
},
@@ -65,27 +65,27 @@
{
matchDepTypes: ['devDependencies'],
matchManagers: ['npm'],
matchPaths: ['*'],
matchUpdateTypes: ['major', 'minor', 'patch'],
matchFileNames: ['*'],

matchUpdateTypes: ['major', 'minor', 'patch'],
automerge: true,
commitMessageExtra: '',
groupName: 'npm dev dependencies',
prPriority: 99,
recreateClosed: true,
recreateWhen: 'always',
schedule: 'before 3:00 am every 2 weeks on Tuesday',
},
{
matchPaths: ['*'],
matchFileNames: ['*'],
matchUpdateTypes: ['lockFileMaintenance'],

automerge: true,
prPriority: 99,
schedule: 'before 3:00 am every 2 weeks on Wednesday',
},
{
excludePackageNames: ['@types/node'],
matchPaths: ['*'],
matchPackageNames: ['!@types/node'],
matchFileNames: ['*'],
matchUpdateTypes: ['pin'],

automerge: true,
@@ -103,8 +103,8 @@
semanticCommitType: 'deps',
},
{
matchPaths: ['template/**'],
excludePackageNames: ['pnpm'],
matchFileNames: ['template/**'],
matchPackageNames: ['!pnpm'],

branchPrefix: 'renovate-template--',
rangeStrategy: 'replace',
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: ^22.14

- name: Set up pnpm
run: corepack enable pnpm && corepack install
2 changes: 1 addition & 1 deletion .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: ^22.14

- name: Set up pnpm
run: corepack enable pnpm && corepack install
9 changes: 5 additions & 4 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: ^22.14

- name: Set up pnpm
run: corepack enable pnpm && corepack install
@@ -92,7 +92,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: ^22.14

- name: Set up pnpm
run: corepack enable pnpm
@@ -105,6 +105,8 @@ jobs:

- if: github.head_ref != 'changeset-release/main' && github.ref_name != 'changeset-release/main'
name: Lint package
env:
SKIP_NODE_UPGRADE: true
run: pnpm --filter ${{ matrix.template }} lint

- if: github.head_ref != 'changeset-release/main' && github.ref_name != 'changeset-release/main'
@@ -122,7 +124,6 @@ jobs:
- express-rest-api
- greeter
- koa-rest-api
- lambda-sqs-worker
- lambda-sqs-worker-cdk
- oss-npm-package
- private-npm-package
@@ -133,7 +134,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.x
node-version: ^22.14

- name: Set up pnpm
run: corepack enable pnpm && corepack install
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
94 changes: 94 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,99 @@
# skuba

## 10.0.0

### Major Changes

- **format, lint:** Migrate projects to Node.js 22 ([#1804](https://github.com/seek-oss/skuba/pull/1804))

As of **skuba** 10, `skuba format` and `skuba lint` include patches that attempt to automatically migrate your project to the [active LTS version] of Node.js. This is intended to minimise effort required to keep up with annual Node.js releases.

With each **skuba** upgrade that includes these patches, you can locally opt out of the migration by setting the `SKIP_NODE_UPGRADE` environment variable, running `skuba format`, and committing the result.

Changes must be manually reviewed by an engineer before merging the migration output. See [`skuba migrate node`](https://seek-oss.github.io/skuba/docs/cli/migrate.html#skuba-migrate-node) for more information on this feature and how to use it responsibly.

[active LTS version]: https://nodejs.org/en/about/previous-releases#nodejs-releases

- **migrate:** Introduce `skuba migrate node22` ([#1735](https://github.com/seek-oss/skuba/pull/1735))

[`skuba migrate node22`](https://seek-oss.github.io/skuba/docs/cli/migrate.html#skuba-migrate-node22) attempts to automatically upgrade your project to Node.js 22. Changes must be manually reviewed by an engineer before committing the migration output. See [`skuba migrate node`](https://seek-oss.github.io/skuba/docs/cli/migrate.html#skuba-migrate-node) for more information on this feature and how to use it responsibly.

**skuba** may not be able to upgrade all projects. Check your project for files that may have been missed, review and test the modified code as appropriate before releasing to production, and [open an issue](https://github.com/seek-oss/skuba/issues/new) if your project files were corrupted by the migration.

Node.js 22 includes breaking changes. For more information on the upgrade, refer to:

- The Node.js [release notes][node-22]
- The AWS [release announcement][aws-22] for the Lambda `nodejs22.x` runtime update

You may need to manually upgrade CDK and Serverless package versions as appropriate to support `nodejs22.x`.

[aws-22]: https://aws.amazon.com/blogs/compute/node-js-22-runtime-now-available-in-aws-lambda/
[node-22]: https://nodejs.org/en/blog/announcements/v22-release-announce

### Minor Changes

- **lint:** Flag CODEOWNERS files that appear to have been incorrectly formatted ([#1796](https://github.com/seek-oss/skuba/pull/1796))

Some code editors incorrectly detect `CODEOWNERS` files as markdown files and re-format them as such, breaking their syntax. `skuba lint` now attempts to detect this scenario and flags the file as being incorrect.

- **deps:** Drop dependencies `validate-npm-package-name` and `libnpmsearch` ([#1809](https://github.com/seek-oss/skuba/pull/1809))

These dependencies have been removed, replaced by `npm-registry-fetch`.

- **template/\*-rest-api:** Remove `seek:source:url` tag from gantry files ([#1797](https://github.com/seek-oss/skuba/pull/1797))

- **template/lambda-sqs-worker:** Remove template ([#1789](https://github.com/seek-oss/skuba/pull/1789))

It is recommended to use the `lambda-sqs-worker-cdk` template instead.

- **deps:** TypeScript 5.8 ([#1750](https://github.com/seek-oss/skuba/pull/1750))

This major release includes breaking changes. See the [TypeScript 5.7](https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/) and [TypeScript 5.8](https://devblogs.microsoft.com/typescript/announcing-typescript-5-8/) announcements for more information.

### Patch Changes

- **configure:** Fix crash during detecting whether the working tree is clean ([#1737](https://github.com/seek-oss/skuba/pull/1737))

- **template/express-rest-api:** express 5 ([#1761](https://github.com/seek-oss/skuba/pull/1761))

- **init:** Skip malformed template files ([#1808](https://github.com/seek-oss/skuba/pull/1808))

`skuba init` runs templates, either bundled or [BYO](https://seek-oss.github.io/skuba/docs/templates/byo.html), through a series of string templating and processing steps.

Occasionally, binary files can include substrings that appear to be directives for skuba to translate the file contents, which may then proceed to crash.

To work around this, `skuba init` now skips templating of a given file when encountering an error.

- **template/lambda-sqs-worker-cdk:** Add `git` to the base Docker image ([#1775](https://github.com/seek-oss/skuba/pull/1775))

- **template/lambda-sqs-worker-cdk:** Upgrade `aws-cdk` and `aws-cdk-lib` to `^2.167.1` ([#1740](https://github.com/seek-oss/skuba/pull/1740))

- **template/lambda-sqs-worker-cdk:** Upgrade to datadog-cdk-constructs-v2 2 ([#1799](https://github.com/seek-oss/skuba/pull/1799))

- **template/lambda-sqs-worker-cdk:** Fix failing unit test and add `start` command ([#1724](https://github.com/seek-oss/skuba/pull/1724))

- **deps:** ignore ^7.0.0 ([#1762](https://github.com/seek-oss/skuba/pull/1762))

- **deps:** prettier ~3.5.0 ([#1788](https://github.com/seek-oss/skuba/pull/1788))

- **deps:** esbuild ~0.25.0 ([#1787](https://github.com/seek-oss/skuba/pull/1787))

- **deps:** prettier ~3.4.0 ([#1751](https://github.com/seek-oss/skuba/pull/1751))

This change may contain some formatting changes. Review the release notes: https://prettier.io/blog/2024/11/26/3.4.0.html

- **template/\*:** Upgrade to Node 22 ([#1789](https://github.com/seek-oss/skuba/pull/1789))

- **template:** Align with latest AWS tagging guidance ([#1782](https://github.com/seek-oss/skuba/pull/1782))

- **template/express-rest-api, template/koa-rest-api:** Drop support for `failOnScanFindings` for gantry 4 support ([#1759](https://github.com/seek-oss/skuba/pull/1759))

- **deps:** Drop `serialize-error` ([#1763](https://github.com/seek-oss/skuba/pull/1763))

- **template/\*-rest-api:** seek-jobs/gantry v4.0.0 ([#1785](https://github.com/seek-oss/skuba/pull/1785))

- **init:** Fix `pnpm dlx skuba init` usage ([#1793](https://github.com/seek-oss/skuba/pull/1793))

## 9.1.0

### Minor Changes
22 changes: 22 additions & 0 deletions docs/cli/build.md
Original file line number Diff line number Diff line change
@@ -62,6 +62,24 @@ With esbuild, you can supply the following options:
| `--debug` | Enable debug console output |
| `--project` | Point to a different `tsconfig.json` file |

## Bundling assets

To bundle additional assets alongside your build, add an `assets` field inside the `skuba` section within your `package.json`.

```json
{
"skuba": {
"entryPoint": "src/index.ts",
"template": "koa-rest-api",
"type": "application",
"version": "8.1.0",
"assets": ["**/*.vocab/*translations.json"]
}
}
```

In this example, all `*.vocab/*translations.json` files found within `src` will be copied into the corresponding `lib` directory.

---

## skuba build-package
@@ -86,6 +104,10 @@ On a resource-constrained Buildkite agent,
you can limit this with the `--serial` flag.
See our [Buildkite guide] for more information.

To bundle additional assets alongside your package, view the [bundling assets](#bundling-assets) section above.

These files will be copied into the corresponding `lib-commonjs` and `lib-es2015` directories.

| Option | Description |
| :--------- | :----------------------------------------------- |
| `--serial` | Force serial execution of compilation operations |
1 change: 0 additions & 1 deletion docs/cli/init.md
Original file line number Diff line number Diff line change
@@ -59,7 +59,6 @@ You're now presented with a selection of templates:
express-rest-api
❯ greeter
koa-rest-api
lambda-sqs-worker
lambda-sqs-worker-cdk
oss-npm-package
private-npm-package
27 changes: 27 additions & 0 deletions docs/cli/lint.md
Original file line number Diff line number Diff line change
@@ -69,7 +69,33 @@ you can limit this with the `--serial` flag.
- [Buildkite annotations] are enabled when Buildkite environment variables and the `buildkite-agent` binary are present.
- [GitHub annotations] are enabled when CI and GitHub environment variables are present.

---

## Patches

`skuba format` and `skuba lint` include rudimentary support for patching your project.
These simple codemods are applied the first time that you run a relevant command after upgrading to a new version of **skuba**.

Patches are not guaranteed to work perfectly on all projects,
and typically work best when a project closely matches a built-in [template].
Review and test modified code as appropriate before releasing to production,
and [open an issue](https://github.com/seek-oss/skuba/issues/new) if your project files were corrupted by a patch.

### Node.js migrations

As of **skuba** 10,
`skuba format` and `skuba lint` include patches that attempt to automatically migrate your project to the [active LTS version] of Node.js.
This is intended to minimise effort required to keep up with annual Node.js releases.

With each **skuba** upgrade that includes these patches,
you can locally opt out of the migration by setting the `SKIP_NODE_UPGRADE` environment variable, running `skuba format`, and committing the result.

Changes must be manually reviewed by an engineer before merging the migration output.
See [`skuba migrate node`] for more information on this feature and how to use it responsibly.

[`skuba format`]: #skuba-format
[`skuba migrate node`]: ./migrate.md#skuba-migrate-node
[active LTS version]: https://nodejs.org/en/about/previous-releases#nodejs-releases
[Buildkite annotations]: ../deep-dives/buildkite.md#buildkite-annotations
[CPU core count]: https://nodejs.org/api/os.html#os_os_cpus
[eslint deep dive]: ../deep-dives/eslint.md
@@ -79,4 +105,5 @@ you can limit this with the `--serial` flag.
[GitHub autofixes]: ../deep-dives/github.md#github-autofixes
[prescribe ESLint]: https://myseek.atlassian.net/wiki/spaces/AA/pages/2358346041/#TypeScript
[Prettier]: https://prettier.io/
[template]: ../templates/index.md
[tsc]: https://www.typescriptlang.org/docs/handbook/compiler-options.html
139 changes: 128 additions & 11 deletions docs/cli/migrate.md
Original file line number Diff line number Diff line change
@@ -9,29 +9,146 @@ nav_order: 7

## skuba migrate help

Echoes the available **skuba** migrations
Echoes the available **skuba** migrations.

```shell
skuba migrate help
```

---

## skuba migrate node20
## skuba migrate node

`skuba migrate node20` will attempt to automatically upgrade projects to Node.js 20.
It will look in the project root for Dockerfiles, `.nvmrc`, and Serverless files,
as well as CDK files in `infra/` and `.buildkite/` files, and try to upgrade them to a Node.js 20 version.
**skuba** includes migrations to upgrade your project to the [active LTS version] of Node.js.
This is intended to minimise effort required to keep up with annual Node.js releases.

**skuba** might not be able to upgrade all projects, so please check your project for any files that **skuba** missed. It's
possible that **skuba** will modify a file incorrectly, in which case please
[open an issue](https://github.com/seek-oss/skuba/issues/new).
The following files are scanned:

Node.js 20 comes with its own breaking changes, so please read the [Node.js 20 release notes](https://nodejs.org/en/blog/announcements/v20-release-announce) alongside the skuba release notes. In addition,
- `.node-version`
- `.nvmrc`
- `package.json`s
- `tsconfig.json`s
- Buildkite pipelines in `.buildkite/` directories
- CDK files in `infra/` directories
- Dockerfiles & Docker Compose files
- Serverless files

- For AWS Lambda runtime updates to `nodejs20.x`, consider reading the [release announcement](https://aws.amazon.com/blogs/compute/node-js-20-x-runtime-now-available-in-aws-lambda/) as there are some breaking changes with this upgrade.
- You may need to upgrade your versions of CDK and Serverless as appropriate to support nodejs20.x.
**skuba** may not be able to upgrade all projects,
and typically works best when a project closely matches a built-in [template].
Check your project for files that may have been missed,
review and test the modified code as appropriate before releasing to production,
and [open an issue](https://github.com/seek-oss/skuba/issues/new) if your project files were corrupted by the migration.
Exercise particular caution with monorepos,
as some may have employed unique configurations that the migration has not accounted for.

The migration will attempt to proceed if your project specifies:

- A Node.js version in `.node-version`, `.nvmrc`, and/or `package.json#/engines/node`

- A project type in `package.json#/skuba/type` that is not `package`

Well-known project types currently include `application` and `package`.
While we intend to improve support for monorepo projects in a future version,
you may enable migrations in the interim by setting your root `/package.json` project type to `root`.

**skuba** upgrades your `tsconfig.json`s in line with the official [Node Target Mapping] guidance.
`tsconfig.json`s contain two options that are linked to Node.js versions:

- `lib` configures the language features available to your source code.

For example, including `ES2024` allows you to use the [`Object.groupBy()` static method].
The features available in each new ECMAScript version are summarised on [node.green](https://node.green/).

- `target` configures the transpilation behaviour of the TypeScript compiler.

Back-end applications typically synchronise `lib` with their Node.js runtime.
In this scenario, there is no need to transpile language features and `target` can match the ECMAScript version in `lib`.

On the other hand, you may wish to use recent language features when authoring your npm packages while retaining support for package consumers on older Node.js runtimes.
In this scenario, see the note below on transpilation for npm packages.

For npm packages,
manually review the following configuration options:

- `package.json#/engines/node`

The `engines` property propagates to your package consumers.
For example, if you specify a minimum Node.js version of 22,
it will prevent your package from being installed in a Node.js 20 environment:

```json
{
"engines": {
"node": ">=22"
}
}
```

Take care with the `engines` property of an npm package;
modifications typically necessitate a new major release per [semantic versioning].

- `tsconfig.json#/target`

Refer to the official [Node Target Mapping] guidance and ensure that the transpilation target corresponds to the minimum Node.js version in `engines`.

For monorepo projects,
check whether your npm packages inherit from another `tsconfig.json`.
You may need to define explicit overrides for npm packages like so:

```diff
{
+ "compilerOptions": {
+ "removeComments": false,
+ "target": "ES2023" // Continue to support package consumers on Node.js 20
+ },
"extends": "../../tsconfig.json"
}
```

As of **skuba** 10,
`skuba format` and `skuba lint` will automatically run these migrations as [patches].

[`Object.groupBy()` static method]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy
[active LTS version]: https://nodejs.org/en/about/previous-releases#nodejs-releases
[Node Target Mapping]: https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping.
[patches]: ./lint.md#patches
[semantic versioning]: https://semver.org/
[template]: ../templates/index.md

### skuba migrate node22

Attempts to automatically upgrade your project to Node.js 22.

```shell
skuba migrate node22
```

Node.js 22 includes breaking changes.
For more information on the upgrade, refer to:

- The Node.js [release notes][node-22]
- The AWS [release announcement][aws-22] for the Lambda `nodejs22.x` runtime update

You may need to manually upgrade CDK and Serverless package versions as appropriate to support `nodejs22.x`.

[aws-22]: https://aws.amazon.com/blogs/compute/node-js-22-runtime-now-available-in-aws-lambda/
[node-22]: https://nodejs.org/en/blog/announcements/v22-release-announce

### skuba migrate node20

Attempts to automatically upgrade your project to Node.js 20.

```shell
skuba migrate node20
```

Node.js 20 includes breaking changes.
For more information on the upgrade, refer to:

- The Node.js [release notes][node-20]
- The AWS [release announcement][aws-20] for the Lambda `nodejs20.x` runtime update

You may need to manually upgrade CDK and Serverless package versions as appropriate to support `nodejs20.x`.

[aws-20]: https://aws.amazon.com/blogs/compute/node-js-20-x-runtime-now-available-in-aws-lambda/
[node-20]: https://nodejs.org/en/blog/announcements/v20-release-announce
2 changes: 1 addition & 1 deletion docs/deep-dives/arm64.md
Original file line number Diff line number Diff line change
@@ -270,7 +270,7 @@ As these have no set naming convention, you can look for:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: build
file: gantry.build.yml # <-- here
region: ap-southeast-2
4 changes: 2 additions & 2 deletions docs/deep-dives/buildkite.md
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker#v5.11.0:
- docker#v5.12.0:
environment:
- BUILDKITE_AGENT_ACCESS_TOKEN
propagate-environment: true
@@ -62,7 +62,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
environment:
- BUILDKITE_AGENT_ACCESS_TOKEN
propagate-environment: true
4 changes: 2 additions & 2 deletions docs/deep-dives/github.md
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker#v5.11.0:
- docker#v5.12.0:
# Enable GitHub integrations.
environment:
- GITHUB_API_TOKEN
@@ -67,7 +67,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
environment:
- GITHUB_API_TOKEN
propagate-environment: true
6 changes: 3 additions & 3 deletions docs/deep-dives/pnpm.md
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ This migration guide assumes that your project was scaffolded with a **skuba** t
2. Add a `packageManager` key to `package.json`

```json
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@10.6.2",
```

3. Install pnpm
@@ -287,14 +287,14 @@ This migration guide assumes that your project was scaffolded with a **skuba** t
We are also using an updated caching syntax on `package.json` which caches only on the `packageManager` key. This requires the [seek-oss/docker-ecr-cache](https://github.com/seek-oss/docker-ecr-cache-buildkite-plugin) plugin version to be >= 2.2.0.

```diff
seek-oss/private-npm#v1.2.0:
seek-oss/private-npm#v1.3.0:
env: NPM_READ_TOKEN
+ output-path: /tmp/
```

```diff
- seek-oss/docker-ecr-cache#v2.1.0:
+ seek-oss/docker-ecr-cache#v2.2.0:
+ seek-oss/docker-ecr-cache#v2.2.1:
cache-on:
- - package.json
- - yarn.lock
14 changes: 0 additions & 14 deletions docs/templates/worker.md
Original file line number Diff line number Diff line change
@@ -7,19 +7,6 @@ parent: Templates

---

## lambda-sqs-worker

An asynchronous [worker] built on [AWS Lambda] and deployed with [Serverless].

Modelled after the "enricher" pattern where an event is received from a source SNS topic and a corresponding enrichment is published to a destination SNS topic.
For fault tolerance,
a message queue is employed between the source topic and the Lambda function,
and unprocessed events are sent to a dead-letter queue for manual triage.

[View on GitHub](https://github.com/seek-oss/skuba/tree/main/template/lambda-sqs-worker)

---

## lambda-sqs-worker-cdk

An asynchronous [worker] built on [AWS Lambda] and deployed with [AWS CDK].
@@ -34,5 +21,4 @@ Comes with configuration validation and infrastructure snapshot testing.

[aws cdk]: https://myseek.atlassian.net/wiki/spaces/AA/pages/2358346041/#CDK
[aws lambda]: https://myseek.atlassian.net/wiki/spaces/AA/pages/2358346041/#Lambda-updated
[serverless]: https://serverless.com/
[worker]: https://myseek.atlassian.net/wiki/spaces/AA/pages/2358346236/#Worker
42 changes: 20 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skuba",
"version": "9.1.0",
"version": "10.0.0",
"private": false,
"description": "SEEK development toolkit for backend applications and packages",
"homepage": "https://github.com/seek-oss/skuba#readme",
@@ -83,7 +83,7 @@
"dotenv": "^16.0.0",
"ejs": "^3.1.6",
"enquirer": "^2.3.6",
"esbuild": "~0.24.0",
"esbuild": "~0.25.0",
"eslint": "^9.11.1",
"eslint-config-skuba": "workspace:*",
"execa": "^5.0.0",
@@ -93,61 +93,59 @@
"function-arguments": "^1.0.9",
"get-port": "^5.1.1",
"golden-fleece": "^1.0.9",
"ignore": "^5.1.8",
"ignore": "^7.0.0",
"is-installed-globally": "^0.4.0",
"isomorphic-git": "^1.11.1",
"jest": "^29.0.1",
"jest-watch-typeahead": "^2.1.1",
"libnpmsearch": "^8.0.0",
"lodash.mergewith": "^4.6.2",
"minimist": "^1.2.6",
"normalize-package-data": "^7.0.0",
"npm-registry-fetch": "^18.0.2",
"npm-run-path": "^4.0.1",
"npm-which": "^3.0.1",
"picomatch": "^4.0.0",
"prettier": "~3.3.0",
"prettier": "~3.5.0",
"prettier-plugin-packagejson": "^2.4.10",
"read-pkg-up": "^7.0.1",
"semantic-release": "^22.0.12",
"serialize-error": "^8.0.1",
"simple-git": "^3.5.0",
"ts-dedent": "^2.2.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.0.0",
"tsconfig-seek": "2.0.0",
"tsx": "^4.16.2",
"typescript": "~5.6.0",
"validate-npm-package-name": "^6.0.0",
"typescript": "~5.8.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@changesets/cli": "2.27.9",
"@changesets/cli": "2.28.1",
"@changesets/get-github-info": "0.6.0",
"@jest/reporters": "29.7.0",
"@jest/test-result": "29.7.0",
"@types/ejs": "3.1.5",
"@types/express": "4.17.21",
"@types/express": "5.0.0",
"@types/fs-extra": "11.0.4",
"@types/koa": "2.15.0",
"@types/libnpmsearch": "2.0.7",
"@types/lodash.mergewith": "4.6.9",
"@types/minimist": "1.2.5",
"@types/module-alias": "2.0.4",
"@types/npm-registry-fetch": "8.0.7",
"@types/npm-which": "3.0.3",
"@types/picomatch": "3.0.1",
"@types/picomatch": "3.0.2",
"@types/semver": "7.5.8",
"@types/supertest": "6.0.2",
"@types/validate-npm-package-name": "4.0.2",
"enhanced-resolve": "5.17.1",
"express": "4.21.1",
"fastify": "5.0.0",
"enhanced-resolve": "5.18.1",
"express": "5.0.1",
"fastify": "5.2.1",
"jest-diff": "29.7.0",
"jsonfile": "6.1.0",
"koa": "2.15.3",
"memfs": "4.13.0",
"koa": "2.16.0",
"memfs": "4.17.0",
"remark-cli": "12.0.1",
"remark-preset-lint-recommended": "7.0.0",
"semver": "7.6.3",
"remark-preset-lint-recommended": "7.0.1",
"semver": "7.7.1",
"supertest": "7.0.0",
"type-fest": "2.19.0"
},
@@ -159,7 +157,7 @@
"optional": true
}
},
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@10.6.2",
"engines": {
"node": ">=18.18.0"
},
@@ -171,6 +169,6 @@
"entryPoint": "src/index.ts",
"template": null,
"type": "package",
"version": "9.1.0"
"version": "10.0.0"
}
}
27 changes: 27 additions & 0 deletions packages/eslint-config-skuba/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# eslint-config-skuba

## 5.1.0

### Minor Changes

- Disable `@typescript-eslint/no-base-to-string` in tests ([#1765](https://github.com/seek-oss/skuba/pull/1765))

- **deps:** typescript-eslint 8.26 ([#1750](https://github.com/seek-oss/skuba/pull/1750))

This bumps typescript-eslint to ^8.26.0 to support TypeScript 5.8

- Remove `eslint-plugin-tsdoc` ([#1766](https://github.com/seek-oss/skuba/pull/1766))

This plugin is [currently incompatible](https://github.com/microsoft/tsdoc/issues/374) with our config.

- Revert to modern JavaScript language option defaults ([#1769](https://github.com/seek-oss/skuba/pull/1769))

- `ecmaVersion: 5 => latest`
- `sourceType: script => module`

See [JavaScript language options](https://eslint.org/docs/latest/use/configure/language-options#specifying-javascript-options) for more information.

### Patch Changes

- Remove duplicate `@typescript-eslint` definitions ([#1766](https://github.com/seek-oss/skuba/pull/1766))

- **deps:** eslint-plugin-tsdoc ^0.4.0 ([#1749](https://github.com/seek-oss/skuba/pull/1749))

## 5.0.0

### Major Changes
30 changes: 8 additions & 22 deletions packages/eslint-config-skuba/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const base = require('eslint-config-seek/base');
const extensions = require('eslint-config-seek/extensions');
const tsdoc = require('eslint-plugin-tsdoc');
const eslintPluginYml = require('eslint-plugin-yml');
const tseslint = require('typescript-eslint');

const { js: jsExtensions, ts: tsExtensions } = extensions;

module.exports = [
{
name: 'skuba/ignores',
ignores: [
// Gantry resource files support non-standard syntax (Go templating)
'**/.gantry/**/*.yaml',
@@ -29,6 +29,7 @@ module.exports = [
},
...base,
{
name: 'skuba/javascript',
rules: {
'import-x/no-duplicates': 'error',

@@ -91,22 +92,14 @@ module.exports = [
...[
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
].map((config) => ({
].map(({ plugins, ...config }) => ({
...config,
files: [`**/*.{${tsExtensions}}`],
})),
{
name: 'skuba/typescript',
files: [`**/*.{${tsExtensions}}`],

languageOptions: {
ecmaVersion: 5,
sourceType: 'script',

parserOptions: {
projectService: true,
},
},

rules: {
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/no-floating-promises': 'error',
@@ -137,17 +130,7 @@ module.exports = [
},
},
{
files: [`**/*.{${tsExtensions}}`],

plugins: {
tsdoc,
},

rules: {
'tsdoc/syntax': 'error',
},
},
{
name: 'skuba/typescript-tests',
files: [`**/*.test.{${tsExtensions}}`, `**/testing/**/*.{${tsExtensions}}`],

rules: {
@@ -159,6 +142,9 @@ module.exports = [
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-explicit-any': 'off',

// Allow e.g. `String(unknown)`
'@typescript-eslint/no-base-to-string': 'off',

// Allow ! in tests
'@typescript-eslint/no-non-null-assertion': 'off',

9 changes: 4 additions & 5 deletions packages/eslint-config-skuba/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eslint-config-skuba",
"version": "5.0.0",
"version": "5.1.0",
"private": false,
"description": "ESLint config for skuba",
"homepage": "https://github.com/seek-oss/skuba/tree/main/packages/eslint-config-skuba#readme",
@@ -27,19 +27,18 @@
},
"dependencies": {
"eslint-config-seek": "^14.0.1",
"eslint-plugin-tsdoc": "^0.3.0",
"eslint-plugin-yml": "^1.14.0",
"typescript-eslint": "^8.6.0"
"typescript-eslint": "^8.26.0"
},
"devDependencies": {
"eslint": "^9.11.1",
"typescript": "~5.6.0"
"typescript": "~5.8.0"
},
"peerDependencies": {
"eslint": ">=9.11.1",
"typescript": ">=5.5.4"
},
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@10.6.2",
"engines": {
"node": ">=18.18.0"
},
1 change: 0 additions & 1 deletion packages/skuba-dive/eslint.config.js

This file was deleted.

3 changes: 3 additions & 0 deletions packages/skuba-dive/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintConfig from 'eslint-config-skuba';

export default eslintConfig;
12,849 changes: 4,145 additions & 8,704 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion src/cli/__snapshots__/format.int.test.ts.snap
Original file line number Diff line number Diff line change
@@ -30,6 +30,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove -
Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found
Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found
Upgrading to Node.js 22
Proceeding with migration from Node.js 22.0.0 to 22.0.0
Upgraded to Node.js 22
Patch applied: Upgrade Node.js to version 22
skuba update complete.
@@ -116,6 +121,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove -
Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found
Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found
Upgrading to Node.js 22
Proceeding with migration from Node.js 22.0.0 to 22.0.0
Upgraded to Node.js 22
Patch applied: Upgrade Node.js to version 22
skuba update complete.
@@ -129,8 +139,8 @@ ESLint
Initialising ESLint...
Processing files...
Processed 2 files in <random>s.
○ d.js
○ a/a/a.ts
○ d.js
Prettier
Initialising Prettier...
@@ -197,6 +207,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove -
Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found
Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found
Upgrading to Node.js 22
Proceeding with migration from Node.js 22.0.0 to 22.0.0
Upgraded to Node.js 22
Patch applied: Upgrade Node.js to version 22
skuba update complete.
@@ -249,6 +264,11 @@ Patch skipped: Update docker image references to use public.ecr.aws and remove -
Patch skipped: Move .npmrc mounts from tmp/.npmrc to /tmp/.npmrc - no Buildkite files found
Patch skipped: Use pinned pnpm version in Dockerfiles - no Dockerfiles found
Upgrading to Node.js 22
Proceeding with migration from Node.js 22.0.0 to 22.0.0
Upgraded to Node.js 22
Patch applied: Upgrade Node.js to version 22
skuba update complete.
2 changes: 1 addition & 1 deletion src/cli/__snapshots__/lint.int.test.ts.snap
Original file line number Diff line number Diff line change
@@ -101,8 +101,8 @@ exports[`ok --debug 1`] = `
ESLint │ Initialising ESLint...
ESLint │ Processing files...
ESLint │ Processed 2 files in <random>s.
ESLint │ ○ d.js
ESLint │ ○ a/a/a.ts
ESLint │ ○ d.js
Prettier │ Initialising Prettier...
Prettier │ Detected project root: <random>
Prettier │ Discovering files...
7 changes: 6 additions & 1 deletion src/cli/adapter/prettier.ts
Original file line number Diff line number Diff line change
@@ -248,7 +248,12 @@ export const runPrettier = async (
if (result.errored.length) {
logger.plain(`Flagged ${pluralise(result.errored.length, 'file')}:`);
for (const { err, filepath } of result.errored) {
logger.warn(filepath, ...(err ? [String(err)] : []));
logger.warn(
filepath,
...(typeof err === 'string' || err instanceof Error
? [String(err)]
: []),
);
}
}

8 changes: 6 additions & 2 deletions src/cli/configure/analyseDependencies.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import type { NormalizedReadResult } from 'read-pkg-up';
import { type TextProcessor, copyFiles } from '../../utils/copy';
import { log } from '../../utils/logging';
import type { ProjectType } from '../../utils/manifest';
import { getSkubaVersion, latestNpmVersion } from '../../utils/version';
import { getLatestNpmVersion, getSkubaVersion } from '../../utils/version';

import { diffDependencies } from './analysis/package';
import * as dependencyMutators from './dependencies';
@@ -40,7 +40,11 @@ const pinUnspecifiedVersions = async (
.map(async ([name]) => {
const version = await (name === 'skuba'
? getSkubaVersion()
: latestNpmVersion(name));
: getLatestNpmVersion(name));

if (version === null) {
throw new Error(`Failed to fetch latest version of ${name}`);
}

return [name, version] as const;
}),
17 changes: 16 additions & 1 deletion src/cli/configure/analysis/git.ts
Original file line number Diff line number Diff line change
@@ -7,8 +7,18 @@ import { log } from '../../../utils/logging';
export const auditWorkingTree = async (dir: string) => {
const filepaths = await crawlDirectory(dir);

let anyFailed = false;

const statuses = await Promise.all(
filepaths.map((filepath) => git.status({ dir, fs, filepath })),
filepaths.map(async (filepath) => {
try {
return await git.status({ dir, fs, filepath });
} catch {
// TODO: Why does isomorphic-git sometimes just _fail_?
anyFailed = true;
return 'absent';
}
}),
);

if (
@@ -19,5 +29,10 @@ export const auditWorkingTree = async (dir: string) => {
) {
log.newline();
log.warn('You have dirty/untracked files that may be overwritten.');
} else if (anyFailed) {
log.newline();
log.warn(
"Some files failed to be read. Check that you don't have any dirty/untracked files that may be overwritten.",
);
}
};
2 changes: 1 addition & 1 deletion src/cli/configure/processing/module.test.ts
Original file line number Diff line number Diff line change
@@ -35,6 +35,6 @@ describe('replacePackageReferences', () => {
},
});

expect(process(input)).toBe(expected);
expect(process('filename.txt', input)).toBe(expected);
});
});
5 changes: 5 additions & 0 deletions src/cli/format.int.test.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import git from 'isomorphic-git';
import { diff } from 'jest-diff';

import { format } from './format';
import * as getNodeTypesVersionModule from './migrate/nodeVersion/getNodeTypesVersion';

jest.setTimeout(15_000);

@@ -15,6 +16,10 @@ jest
.spyOn(console, 'log')
.mockImplementation((...args) => stdoutMock(`${args.join(' ')}\n`));

jest
.spyOn(getNodeTypesVersionModule, 'getNodeTypesVersion')
.mockReturnValue(Promise.resolve({ version: '22.9.0' }));

jest
.spyOn(git, 'listRemotes')
.mockResolvedValue([
4 changes: 2 additions & 2 deletions src/cli/init/index.ts
Original file line number Diff line number Diff line change
@@ -90,8 +90,8 @@ export const init = async (args = process.argv.slice(2)) => {
await initialiseRepo(destinationDir, templateData);

const [manifest, packageManagerConfig] = await Promise.all([
getConsumerManifest(),
detectPackageManager(),
getConsumerManifest(destinationDir),
detectPackageManager(destinationDir),
]);

if (!manifest) {
7 changes: 6 additions & 1 deletion src/cli/lint/annotate/buildkite/prettier.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,12 @@ export const createPrettierAnnotations = (
Buildkite.md.terminal(
prettier.result.errored
.map(({ err, filepath }) =>
[filepath, ...(err ? [String(err)] : [])].join(' '),
[
filepath,
...(typeof err === 'string' || err instanceof Error
? [String(err)]
: []),
].join(' '),
)
.join('\n'),
),
5 changes: 4 additions & 1 deletion src/cli/lint/annotate/github/prettier.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,10 @@ export const createPrettierAnnotations = (
start_line: 1,
end_line: 1,
path: result.filepath,
message: message ? String(message) : 'This file has not been formatted.',
message:
typeof message === 'string' || message instanceof Error
? String(message)
: 'This file has not been formatted.',
title: 'Prettier',
};
});
3 changes: 2 additions & 1 deletion src/cli/lint/internal.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import chalk from 'chalk';

import { type Logger, createLogger } from '../../utils/logging';

import { tryDetectBadCodeowners } from './internalLints/detectBadCodeowners';
import { noSkubaTemplateJs } from './internalLints/noSkubaTemplateJs';
import { tryRefreshConfigFiles } from './internalLints/refreshConfigFiles';
import { upgradeSkuba } from './internalLints/upgrade';
@@ -26,7 +27,7 @@ const lints: Array<
>
> = [
// Run upgradeSkuba before refreshConfigFiles for npmrc handling
[upgradeSkuba],
[upgradeSkuba, tryDetectBadCodeowners],
[noSkubaTemplateJs, tryRefreshConfigFiles],
];

107 changes: 107 additions & 0 deletions src/cli/lint/internalLints/detectBadCodeowners.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// eslint-disable-next-line no-restricted-imports -- want to access unmocked fs in the tests itself
import * as realFs from 'fs/promises';
import path from 'path';

import memfs, { vol } from 'memfs';

import { detectBadCodeowners } from './detectBadCodeowners';

jest.mock('fs-extra', () => memfs);

jest.mock('../../..', () => ({
Git: {
findRoot: () => Promise.resolve('/path/to/git/root'),
},
}));

const volToJson = () => vol.toJSON('/', undefined, true);

afterEach(() => vol.reset());
afterEach(jest.resetAllMocks);

describe('detectBadCodeowners', () => {
const CODEOWNERS_PATH = '/path/to/git/root/.github/CODEOWNERS';

it('should report ok if no file found', async () => {
vol.fromJSON({});
await expect(detectBadCodeowners()).resolves.toEqual({
ok: true,
fixable: false,
annotations: [],
});
expect(volToJson()).toEqual({});
});

it("should report ok on skuba's file", async () => {
const contents = await realFs.readFile(
path.resolve(__dirname, '../../../../.github/CODEOWNERS'),
'utf8',
);
vol.fromJSON({ [CODEOWNERS_PATH]: contents });
await expect(detectBadCodeowners()).resolves.toEqual({
ok: true,
fixable: false,
annotations: [],
});
expect(volToJson()).toEqual({ [CODEOWNERS_PATH]: contents });
});

it('should report ok on skuba templated files', async () => {
const contents = await realFs.readFile(
path.resolve(__dirname, '../../../../template/base/.github/CODEOWNERS'),
'utf8',
);
vol.fromJSON({ [CODEOWNERS_PATH]: contents });
await expect(detectBadCodeowners()).resolves.toEqual({
ok: true,
fixable: false,
annotations: [],
});
expect(volToJson()).toEqual({ [CODEOWNERS_PATH]: contents });
});

it('should report not ok on bad CODEOWNERS', async () => {
const contents = `# Some comment
- @skuba-team
`;
vol.fromJSON({ [CODEOWNERS_PATH]: contents });
await expect(detectBadCodeowners()).resolves.toEqual({
ok: false,
fixable: false,
annotations: [
{
message:
'CODEOWNERS file has a line starting with `- `. This is probably an autoformatting mistake, where your editor thinks this file is a markdown file and is trying to format a list item. Did you mean to use `*` instead?',
path: '.github/CODEOWNERS',
},
],
});
expect(volToJson()).toEqual({ [CODEOWNERS_PATH]: contents });
});

it('should report on /CODEOWNERS and /docs/CODEOWNERS', async () => {
const contents = `# Some comment
- @skuba-team
`;
vol.fromJSON({
'/path/to/git/root/CODEOWNERS': contents,
'/path/to/git/root/docs/CODEOWNERS': contents,
});
await expect(detectBadCodeowners()).resolves.toEqual({
ok: false,
fixable: false,
annotations: [
{
message:
'CODEOWNERS file has a line starting with `- `. This is probably an autoformatting mistake, where your editor thinks this file is a markdown file and is trying to format a list item. Did you mean to use `*` instead?',
path: 'CODEOWNERS',
},
{
message:
'CODEOWNERS file has a line starting with `- `. This is probably an autoformatting mistake, where your editor thinks this file is a markdown file and is trying to format a list item. Did you mean to use `*` instead?',
path: 'docs/CODEOWNERS',
},
],
});
});
});
60 changes: 60 additions & 0 deletions src/cli/lint/internalLints/detectBadCodeowners.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { inspect } from 'util';

import { Git } from '../../..';
import type { Logger } from '../../../utils/logging';
import { createDestinationFileReader } from '../../configure/analysis/project';
import type { InternalLintResult } from '../internal';

export const detectBadCodeowners = async (): Promise<InternalLintResult> => {
const gitRoot = await Git.findRoot({ dir: process.cwd() });
const reader = createDestinationFileReader(gitRoot ?? process.cwd());

const annotations = (
await Promise.all(
['.github/CODEOWNERS', 'CODEOWNERS', 'docs/CODEOWNERS'].map(
async (filename) => {
const lines = (await reader(filename))?.split('\n');

if (lines?.some((line) => line.startsWith('- '))) {
return [
{
message:
'CODEOWNERS file has a line starting with `- `. This is probably an autoformatting mistake, where your editor thinks this file is a markdown file and is trying to format a list item. Did you mean to use `*` instead?',
path: filename,
},
];
}

return [];
},
),
)
).flat();

// TODO: Use `toSorted` once we drop support for Node 18.
annotations.sort((a, b) => a.path.localeCompare(b.path));

return {
ok: annotations.length === 0,
fixable: false,
annotations,
};
};

export const tryDetectBadCodeowners = async (
_mode: 'format' | 'lint',
logger: Logger,
): Promise<InternalLintResult> => {
try {
return await detectBadCodeowners();
} catch (err) {
logger.warn('Failed to detect bad CODEOWNERS.');
logger.subtle(inspect(err));

return {
ok: false,
fixable: false,
annotations: [],
};
}
};
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ describe('tryMoveNpmrcOutOfIgnoreManagedSection', () => {
expect(writeFile.mock.calls.map((c) => c[0])).toEqual([
`~/project/${fileName}`,
]);
expect(writeFile.mock.calls.map((c) => c[1]).join('\n'))
expect((writeFile.mock.calls.map((c) => c[1]) as string[]).join('\n'))
.toMatchInlineSnapshot(`
"# managed by skuba
stuff
10 changes: 10 additions & 0 deletions src/cli/lint/internalLints/upgrade/patches/9.1.0/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Patches } from '../..';

import { tryUpgradeNode } from './upgradeNode';

export const patches: Patches = [
{
apply: tryUpgradeNode,
description: 'Upgrade Node.js to version 22',
},
];
37 changes: 37 additions & 0 deletions src/cli/lint/internalLints/upgrade/patches/9.1.0/upgradeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { inspect } from 'util';

import type { PatchFunction, PatchReturnType } from '../..';
import { log } from '../../../../../../utils/logging';
import { nodeVersionMigration } from '../../../../../migrate/nodeVersion';

const upgradeNode: PatchFunction = async ({
mode,
}): Promise<PatchReturnType> => {
if (process.env.SKIP_NODE_UPGRADE) {
return {
result: 'skip',
reason: 'SKIP_NODE_UPGRADE environment variable set',
};
}
if (mode === 'lint') {
return { result: 'apply' };
}

await nodeVersionMigration({
nodeVersion: 22,
ECMAScriptVersion: 'ES2024',
defaultNodeTypesVersion: '22.9.0',
});

return { result: 'apply' };
};

export const tryUpgradeNode: PatchFunction = async (config) => {
try {
return await upgradeNode(config);
} catch (err) {
log.warn('Failed to upgrade node version');
log.subtle(inspect(err));
return { result: 'skip', reason: 'due to an error' };
}
};
13 changes: 12 additions & 1 deletion src/cli/migrate/index.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,18 @@ import { log } from '../../utils/logging';
import { nodeVersionMigration } from './nodeVersion';

const migrations: Record<string, () => Promise<void>> = {
node20: () => nodeVersionMigration(20),
node20: () =>
nodeVersionMigration({
nodeVersion: 20,
ECMAScriptVersion: 'ES2023',
defaultNodeTypesVersion: '20.14.8',
}),
node22: () =>
nodeVersionMigration({
nodeVersion: 22,
ECMAScriptVersion: 'ES2024',
defaultNodeTypesVersion: '22.9.0',
}),
};

const logAvailableMigrations = () => {
286 changes: 286 additions & 0 deletions src/cli/migrate/nodeVersion/checks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import findUp from 'find-up';
import fs from 'fs-extra';

jest.mock('find-up');
jest.mock('fs-extra');

import { log } from '../../../utils/logging';

import {
isPatchableNodeVersion,
isPatchableServerlessVersion,
isPatchableSkubaType,
} from './checks';

jest.spyOn(log, 'warn');

afterEach(() => {
jest.clearAllMocks();
});

describe('isPatchableServerlessVersion', () => {
it('resolves as a noop when serverless version is supported', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
devDependencies: {
serverless: '4.0.0',
},
}) as never,
);
await expect(isPatchableServerlessVersion()).resolves.toBe(true);
});
it('should return false when the serverless version is below 4', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
devDependencies: {
serverless: '3.0.0',
},
}) as never,
);
await expect(isPatchableServerlessVersion()).resolves.toBe(false);
});
it('should return true when serverless version is not found', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
devDependencies: {
'something/else': '3.0.0',
},
}) as never,
);
await expect(isPatchableServerlessVersion()).resolves.toBe(true);
});
it('throws when no package.json is found', async () => {
jest.mocked(findUp).mockResolvedValueOnce(undefined);
await expect(isPatchableServerlessVersion()).rejects.toThrow(
'package.json not found, ensure it is in the correct location',
);
});
it('should return an error when the package.json is not valid json', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('invalid json' as never);
await expect(isPatchableServerlessVersion()).rejects.toThrow(
'package.json is not valid JSON',
);
});
});

describe('isPatchableSkubaType', () => {
it('should return true when skuba type is not "package"', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
skuba: {
type: 'application',
},
}) as never,
);
await expect(isPatchableSkubaType()).resolves.toBe(true);
});
it('should return false when skuba type is "package"', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
skuba: {
type: 'package',
},
}) as never,
);
await expect(isPatchableSkubaType()).resolves.toBe(false);
});
it('should return false when skuba type is not found', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
skuba: {
not_a_type: 'package',
},
}) as never,
);
await expect(isPatchableSkubaType()).resolves.toBe(false);
});
it('should throw when no package.json is not found', async () => {
jest.mocked(findUp).mockResolvedValueOnce(undefined);
await expect(isPatchableSkubaType()).rejects.toThrow(
'package.json not found, ensure it is in the correct location',
);
});
});

describe('isPatchableNodeVersion', () => {
it('should return true when the node version is supported', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('20' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(true);
});
it('should return false when the node version is greater than the target version', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('24' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the node version is not found', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(null as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the current node version is not a number', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('twenty' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the target node version is invalid', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('20' as never);
await expect(isPatchableNodeVersion(-1)).resolves.toBe(false);
});
it('should return true when the node version is equal to the target version', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('22' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(true);
});
it('should return true when the node version is found in .node-version', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.node-version');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('20' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(true);
});
it('should return true when the node version is found in package.json engines', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
engines: {
node: '20',
},
}) as never,
);
await expect(isPatchableNodeVersion(22)).resolves.toBe(true);
});
it('should return false when the node version in package.json engines is greater than the target version', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
engines: {
node: '24',
},
}) as never,
);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the node version in package.json engines is invalid', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
engines: {
node: 'invalid',
},
}) as never,
);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when no version is found in any file', async () => {
jest.mocked(findUp).mockResolvedValueOnce(undefined);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the version in .nvmrc is invalid', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('invalid' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the version in .node-version is invalid', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.node-version');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('invalid' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return false when the version in package.json engines is invalid', async () => {
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
engines: {
node: 'invalid',
},
}) as never,
);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
});
it('should return true when the version in .nvmrc and .node-version is invalid but a valid package.json engines version', async () => {
jest.mocked(findUp).mockResolvedValueOnce('.nvmrc');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue('invalid' as never);
await expect(isPatchableNodeVersion(22)).resolves.toBe(false);
jest.mocked(findUp).mockResolvedValueOnce('package.json');
jest
.spyOn(fs, 'readFile')
.mockImplementation()
.mockReturnValue(
JSON.stringify({
engines: {
node: '>=20',
},
}) as never,
);
await expect(isPatchableNodeVersion(22)).resolves.toBe(true);
});
});
151 changes: 151 additions & 0 deletions src/cli/migrate/nodeVersion/checks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import findUp from 'find-up';
import fs from 'fs-extra';
import { coerce, lte, satisfies } from 'semver';
import { type ZodRawShape, z } from 'zod';

import { log } from '../../../utils/logging';

const getParentFile = async (file: string) => {
const path = await findUp(file, { cwd: process.cwd() });
if (!path) {
return undefined;
}
return {
fileContent: await fs.readFile(path, 'utf-8'),
path,
};
};

export const extractFromParentPackageJson = async <T extends ZodRawShape>(
schema: z.ZodObject<T>,
) => {
const file = await getParentFile('package.json');
if (!file) {
return { packageJson: undefined, packageJsonRelativePath: undefined };
}
const { fileContent: packageJson, path } = file;
let rawJSON;
try {
rawJSON = JSON.parse(packageJson) as unknown;
} catch {
throw new Error(`${path} is not valid JSON`);
}
const result = schema.safeParse(rawJSON);
if (!result.success) {
return { packageJson: undefined, packageJsonRelativePath: path };
}

return { packageJson: result.data, packageJsonRelativePath: path };
};

export const isPatchableServerlessVersion = async (): Promise<boolean> => {
const { packageJson, packageJsonRelativePath } =
await extractFromParentPackageJson(
z.object({
devDependencies: z.object({
serverless: z.string().optional(),
}),
}),
);
if (!packageJson) {
throw new Error(
'package.json not found, ensure it is in the correct location',
);
}

const serverlessVersion = packageJson?.devDependencies.serverless;

if (!serverlessVersion) {
log.subtle(
`Serverless version not found in ${packageJsonRelativePath}, assuming it is not a dependency`,
);
return true;
}

if (!satisfies(serverlessVersion, '4.x.x')) {
log.warn(
`Serverless version ${serverlessVersion} cannot be migrated; use Serverless 4.x to automatically migrate Serverless files`,
);
return false;
}

log.ok(
`Proceeding with migration of Serverless version ${serverlessVersion}`,
);
return true;
};

export const isPatchableSkubaType = async (): Promise<boolean> => {
const { packageJson, packageJsonRelativePath } =
await extractFromParentPackageJson(
z.object({
skuba: z.object({
type: z.string().optional(),
}),
}),
);

if (!packageJson) {
throw new Error(
'package.json not found, ensure it is in the correct location',
);
}

const type = packageJson?.skuba.type;

if (!type) {
log.warn(
`skuba project type not found in ${packageJsonRelativePath}; add a package.json#/skuba/type to ensure the correct migration can be applied`,
);
return false;
}
if (type === 'package') {
log.warn(
'Migrations are not supported for packages; update manually to ensure major runtime deprecations are intended',
);
return false;
}

log.ok(`Proceeding with migration of skuba project type ${type}`);
return true;
};

export const isPatchableNodeVersion = async (
targetNodeVersion: number,
): Promise<boolean> => {
const nvmrcFile = await getParentFile('.nvmrc');
const nodeVersionFile = await getParentFile('.node-version');
const { packageJson } = await extractFromParentPackageJson(
z.object({
engines: z.object({
node: z.string(),
}),
}),
);

const nvmrcNodeVersion = nvmrcFile?.fileContent;
const nodeVersion = nodeVersionFile?.fileContent;
const engineVersion = packageJson?.engines.node;

const currentNodeVersion = nvmrcNodeVersion || nodeVersion || engineVersion;

const coercedTargetVersion = coerce(targetNodeVersion.toString())?.version;
const coercedCurrentVersion = coerce(currentNodeVersion)?.version;

const isNodeVersionValid =
coercedTargetVersion &&
coercedCurrentVersion &&
lte(coercedCurrentVersion, coercedTargetVersion);

if (!isNodeVersionValid) {
log.warn(
`Node.js version ${coercedCurrentVersion ?? 'unknown'} cannot be migrated to ${coercedTargetVersion}`,
);
return false;
}

log.ok(
`Proceeding with migration from Node.js ${coercedCurrentVersion} to ${coercedTargetVersion}`,
);
return true;
};
92 changes: 92 additions & 0 deletions src/cli/migrate/nodeVersion/getNodeTypesVersion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import npmFetch from 'npm-registry-fetch';

import { getNodeTypesVersion } from './getNodeTypesVersion';

const mockNpmFetch = jest.spyOn(npmFetch, 'json');

describe('getNodeTypesVersion', () => {
afterAll(() => {
jest.restoreAllMocks();
});

it.each([
[
'should return the default version if the response is not valid JSON',
() =>
Promise.resolve({
invalid: 'response',
}),
{
version: '22.9.0',
err: 'Failed to fetch latest @types/node version, using fallback version 22.9.0',
},
],
[
'should return the default version if the response is not ok',
() => Promise.reject(new Error('Not found')),
{
version: '22.9.0',
err: 'Failed to fetch latest @types/node version, using fallback version 22.9.0',
},
],
[
'should return default version if fetch fails',
() => Promise.reject(new Error('Network error')),
{
version: '22.9.0',
err: 'Failed to fetch latest @types/node version, using fallback version 22.9.0',
},
],
[
'should return default version if no matching version is found',
() =>
Promise.resolve({
versions: {},
}),
{
version: '22.9.0',
err: 'No matching @types/node versions for Node.js 22',
},
],
[
'should return the latest matching version filtering out invalid versions',
() =>
Promise.resolve({
versions: {
'22.1.0': {
name: '@types/node',
version: '22.1.0',
},
'not-a-version': {
name: '@types/node',
version: 'not-a-version',
},
'22.3.0': {
name: '@types/node',
version: '22.3.0',
},
'22.4.0': {
name: '@types/node',
version: '22.4.0',
deprecated: 'Warning this version is deprecated',
},
'22.2.0': {
name: '@types/node',
version: '22.2.0',
},
'32.2.0': {
name: '@types/node',
version: '32.2.0',
},
},
}),
{
version: '22.3.0',
},
],
])('%s', async (_, mockFetch, expected) => {
mockNpmFetch.mockImplementation(mockFetch);

await expect(getNodeTypesVersion(22, '22.9.0')).resolves.toEqual(expected);
});
});
48 changes: 48 additions & 0 deletions src/cli/migrate/nodeVersion/getNodeTypesVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { inspect } from 'util';

import { gt, satisfies, valid } from 'semver';

import { log } from '../../../utils/logging';
import { getNpmVersions } from '../../../utils/version';

type VersionResult = {
version: string;
err?: string;
};

export const getNodeTypesVersion = async (
major: number,
defaultVersion: string,
): Promise<VersionResult> => {
try {
const versions = await getNpmVersions('@types/node');

const matchingVersions = Object.values(versions ?? {}).filter(
(v) =>
valid(v.version) &&
satisfies(v.version, `${major}.x.x`) &&
!v.deprecated,
);

if (!matchingVersions.length) {
return {
version: defaultVersion,
err: `No matching @types/node versions for Node.js ${major}`,
};
}

const { version } = matchingVersions.reduce((a, b) =>
gt(a.version, b.version) ? a : b,
);

return {
version,
};
} catch (err) {
log.subtle(inspect(err));
return {
version: defaultVersion,
err: `Failed to fetch latest @types/node version, using fallback version ${defaultVersion}`,
};
}
};
180 changes: 151 additions & 29 deletions src/cli/migrate/nodeVersion/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import memfs, { vol } from 'memfs';

import * as checks from './checks';
import * as getNode22TypesVersionModule from './getNodeTypesVersion';

import { nodeVersionMigration } from '.';

jest
.spyOn(getNode22TypesVersionModule, 'getNodeTypesVersion')
.mockReturnValue(Promise.resolve({ version: '22.9.0' }));

jest.mock('fs-extra', () => memfs);
jest.mock('fast-glob', () => ({
glob: (pat: any, opts: any) =>
@@ -11,13 +18,17 @@ jest.mock('../../../utils/logging');

const volToJson = () => vol.toJSON(process.cwd(), undefined, true);

beforeEach(jest.clearAllMocks);
beforeEach(() => vol.reset());

afterEach(() => jest.clearAllMocks());

describe('nodeVersionMigration', () => {
const scenarios: Array<{
filesBefore: Record<string, string>;
filesAfter?: Record<string, string>;
isPatchableServerlessVersion?: boolean;
isPatchableSkubaType?: boolean;
isPatchableNodeVersion?: boolean;
scenario: string;
}> = [
{
@@ -34,29 +45,37 @@ describe('nodeVersionMigration', () => {
'serverless.yml':
'provider:\n logRetentionInDays: 30\n runtime: nodejs18.x\n region: ap-southeast-2',
'serverless.melb.yaml':
'provider:\n logRetentionInDays: 7\n runtime: nodejs16.x\n region: ap-southeast-4',
"provider:\n logRetentionInDays: 7\n runtime: nodejs16.x\n region: ap-southeast-4\n target: 'node20'",
'infra/myCoolStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_18_X,\n}`,
'infra/myCoolFolder/evenCoolerStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_16_X,\n}`,
'.buildkite/pipeline.yml':
'plugins:\n - docker#v3.0.0:\n image: node:18.1.2-slim\n',
'.buildkite/pipeline2.yml':
'plugins:\n - docker#v3.0.0:\n image: node:18\n',
'.buildkite/pipeline3.yml':
'plugins:\n - docker#v3.0.0:\n image: public.ecr.aws/docker/library/node:20-alpine\n',
'.node-version': '18.1.2\n',
'.node-version2': 'v20.15.0\n',
},
filesAfter: {
'.nvmrc': '20\n',
Dockerfile: 'FROM node:20\nRUN echo "hello"',
'.nvmrc': '22\n',
Dockerfile: 'FROM node:22\nRUN echo "hello"',
'Dockerfile.dev-deps':
'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"',
'serverless.yml':
'provider:\n logRetentionInDays: 30\n runtime: nodejs20.x\n region: ap-southeast-2',
'provider:\n logRetentionInDays: 30\n runtime: nodejs22.x\n region: ap-southeast-2',
'serverless.melb.yaml':
'provider:\n logRetentionInDays: 7\n runtime: nodejs20.x\n region: ap-southeast-4',
'infra/myCoolStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_20_X,\n}`,
'infra/myCoolFolder/evenCoolerStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_20_X,\n}`,
"provider:\n logRetentionInDays: 7\n runtime: nodejs22.x\n region: ap-southeast-4\n target: 'node22'",
'infra/myCoolStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_22_X,\n}`,
'infra/myCoolFolder/evenCoolerStack.ts': `const worker = new aws_lambda.Function(this, 'worker', {\n architecture: aws_lambda.Architecture[architecture],\n code: new aws_lambda.AssetCode('./lib'),\n runtime: aws_lambda.Runtime.NODEJS_22_X,\n}`,
'.buildkite/pipeline.yml':
'plugins:\n - docker#v3.0.0:\n image: node:20-slim\n',
'plugins:\n - docker#v3.0.0:\n image: node:22-slim\n',
'.buildkite/pipeline2.yml':
'plugins:\n - docker#v3.0.0:\n image: node:20\n',
'plugins:\n - docker#v3.0.0:\n image: node:22\n',
'.buildkite/pipeline3.yml':
'plugins:\n - docker#v3.0.0:\n image: public.ecr.aws/docker/library/node:22-alpine\n',
'.node-version': '22\n',
'.node-version2': 'v22\n',
},
},
{
@@ -79,36 +98,48 @@ describe('nodeVersionMigration', () => {
'FROM gcr.io/distroless/nodejs18-debian12\nRUN echo "hello"',
'Dockerfile.10':
'FROM --platform=linux/amd64 gcr.io/distroless/nodejs18-debian12 AS dev-deps\nRUN echo "hello"',
'Dockerfile.11':
'FROM --platform=${BUILDPLATFORM:-arm64} gcr.io/distroless/nodejs20-debian12@sha256:9f43117c3e33c3ed49d689e51287a246edef3af0afed51a54dc0a9095b2b3ef9 AS runtime',
'Dockerfile.12':
'# syntax=docker/dockerfile:1.10@sha256:865e5dd094beca432e8c0a1d5e1c465db5f998dca4e439981029b3b81fb39ed5\nFROM --platform=arm64 node:20@sha256:a5e0ed56f2c20b9689e0f7dd498cac7e08d2a3a283e92d9304e7b9b83e3c6ff3 AS dev-deps',
'Dockerfile.13':
'FROM public.ecr.aws/docker/library/node:20-alpine@sha256:c13b26e7e602ef2f1074aef304ce6e9b7dd284c419b35d89fcf3cc8e44a8def9 AS runtime',
},
filesAfter: {
'.nvmrc': '20\n',
'Dockerfile.1': 'FROM node:20\nRUN echo "hello"',
'Dockerfile.2': 'FROM node:20\nRUN echo "hello"',
'Dockerfile.3': 'FROM node:20-slim\nRUN echo "hello"',
'Dockerfile.4': 'FROM node:20-slim\nRUN echo "hello"',
'.nvmrc': '22\n',
'Dockerfile.1': 'FROM node:22\nRUN echo "hello"',
'Dockerfile.2': 'FROM node:22\nRUN echo "hello"',
'Dockerfile.3': 'FROM node:22-slim\nRUN echo "hello"',
'Dockerfile.4': 'FROM node:22-slim\nRUN echo "hello"',
'Dockerfile.5':
'FROM --platform=linux/amd64 node:20 AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 node:22 AS dev-deps\nRUN echo "hello"',
'Dockerfile.6':
'FROM --platform=linux/amd64 node:20 AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 node:22 AS dev-deps\nRUN echo "hello"',
'Dockerfile.7':
'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"',
'Dockerfile.8':
'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"',
'Dockerfile.9':
'FROM gcr.io/distroless/nodejs20-debian12\nRUN echo "hello"',
'FROM gcr.io/distroless/nodejs22-debian12\nRUN echo "hello"',
'Dockerfile.10':
'FROM --platform=linux/amd64 gcr.io/distroless/nodejs20-debian12 AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 gcr.io/distroless/nodejs22-debian12 AS dev-deps\nRUN echo "hello"',
'Dockerfile.11':
'FROM --platform=${BUILDPLATFORM:-arm64} gcr.io/distroless/nodejs22-debian12 AS runtime',
'Dockerfile.12':
'# syntax=docker/dockerfile:1.10@sha256:865e5dd094beca432e8c0a1d5e1c465db5f998dca4e439981029b3b81fb39ed5\nFROM --platform=arm64 node:22 AS dev-deps',
'Dockerfile.13':
'FROM public.ecr.aws/docker/library/node:22-alpine AS runtime',
},
},
{
scenario: 'already node 20',
scenario: 'already node 22',
filesBefore: {
'.nvmrc': '20\n',
Dockerfile: 'FROM node:20\nRUN echo "hello"',
'.nvmrc': '22\n',
Dockerfile: 'FROM node:22\nRUN echo "hello"',
'Dockerfile.dev-deps':
'FROM --platform=linux/amd64 node:20-slim AS dev-deps\nRUN echo "hello"',
'FROM --platform=linux/amd64 node:22-slim AS dev-deps\nRUN echo "hello"',
'serverless.yml':
'provider:\n logRetentionInDays: 30\n runtime: nodejs20.x\n region: ap-southeast-2',
'provider:\n logRetentionInDays: 30\n runtime: nodejs22.x\n region: ap-southeast-2',
},
},
{
@@ -117,14 +148,105 @@ describe('nodeVersionMigration', () => {
Dockerfile: 'FROM node:latest\nRUN echo "hello"',
},
},
{
scenario: 'node types',
filesBefore: {
'package.json': '"@types/node": "^14.0.0",',
'1/package.json': '"@types/node": "18.0.0"',
'2/package.json': `"engines": {\n"node": ">=18"\n},\n`,
'3/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "application"\n}`,
},
filesAfter: {
'package.json': '"@types/node": "^22.9.0",',
'1/package.json': '"@types/node": "22.9.0"',
'2/package.json': `"engines": {\n"node": ">=22"\n},\n`,
'3/package.json': `"engines": {\n"node": ">=22"\n},\n"skuba": {\n"type": "application"\n}`,
},
},
{
scenario: 'not patchable node types',
filesBefore: {
'1/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "package"\n}`,
},
filesAfter: {
'1/package.json': `"engines": {\n"node": ">=18"\n},\n"skuba": {\n"type": "package"\n}`,
},
isPatchableServerlessVersion: false,
},
{
scenario: 'tsconfig target',
filesBefore: {
'tsconfig.json': '"target": "ES2020"',
'1/tsconfig.json': '"target": "es2014"',
'2/tsconfig.json': '"target": "ESNext"',
'3/tsconfig.base.json': '"target": "ES2020"',
},
filesAfter: {
'tsconfig.json': '"target": "ES2024"',
'1/tsconfig.json': '"target": "ES2024"',
'2/tsconfig.json': '"target": "ESNext"',
'3/tsconfig.base.json': '"target": "ES2024"',
},
},
{
scenario: 'tsconfig lib',
filesBefore: {
'tsconfig.json': '"lib": ["ES2020"]',
'1/tsconfig.json': '"lib": ["es2014"]',
'2/tsconfig.json': '"lib": ["ESNext"]',
'3/tsconfig.json': '"lib": ["ESNext",\n"dom",\n"ES2020", "webworker"]',
'4/tsconfig.base.json': '"lib": ["ES2020"]',
},
filesAfter: {
'tsconfig.json': '"lib": ["ES2024"]',
'1/tsconfig.json': '"lib": ["ES2024"]',
'2/tsconfig.json': '"lib": ["ESNext"]',
'3/tsconfig.json': '"lib": ["ESNext",\n"dom",\n"ES2024", "webworker"]',
'4/tsconfig.base.json': '"lib": ["ES2024"]',
},
},
{
scenario: 'docker-compose.yml target',
filesBefore: {
'docker-compose.yml': 'image: node:18.1.2\n',
'docker-compose.dev.yml': 'image: node:18\n',
'docker-compose.prod.yml': 'image: node:18-slim\n',
},
filesAfter: {
'docker-compose.yml': 'image: node:22\n',
'docker-compose.dev.yml': 'image: node:22\n',
'docker-compose.prod.yml': 'image: node:22-slim\n',
},
},
];

it.each(scenarios)(
'handles $scenario',
async ({ filesBefore, filesAfter }) => {
async ({
filesBefore,
filesAfter,
isPatchableNodeVersion = true,
isPatchableServerlessVersion = true,
isPatchableSkubaType = true,
}) => {
jest
.spyOn(checks, 'isPatchableServerlessVersion')
.mockResolvedValue(isPatchableServerlessVersion);

jest
.spyOn(checks, 'isPatchableSkubaType')
.mockResolvedValue(isPatchableSkubaType);

jest
.spyOn(checks, 'isPatchableNodeVersion')
.mockResolvedValue(isPatchableNodeVersion);
vol.fromJSON(filesBefore, process.cwd());

await nodeVersionMigration(20);
await nodeVersionMigration({
nodeVersion: 22,
ECMAScriptVersion: 'ES2024',
defaultNodeTypesVersion: '22.9.0',
});

expect(volToJson()).toEqual(filesAfter ?? filesBefore);
},
198 changes: 159 additions & 39 deletions src/cli/migrate/nodeVersion/index.ts
Original file line number Diff line number Diff line change
@@ -6,90 +6,210 @@ import fs from 'fs-extra';
import { log } from '../../../utils/logging';
import { createDestinationFileReader } from '../../configure/analysis/project';

type SubPatch = (
import {
isPatchableNodeVersion,
isPatchableServerlessVersion,
isPatchableSkubaType,
} from './checks';
import { getNodeTypesVersion } from './getNodeTypesVersion';

type FileSelector =
| { files: string; file?: never }
| { file: string; files?: never }
) & {
test?: RegExp;
| { file: string; files?: never };

type SubPatch = FileSelector & {
tests?: Array<() => Promise<boolean>>;
regex?: RegExp;
replace: string;
};

const subPatches: SubPatch[] = [
{ file: '.nvmrc', replace: '<%- version %>\n' },
const subPatches = ({
nodeVersion,
nodeTypesVersion,
ECMAScriptVersion,
}: Versions): SubPatch[] => [
{ file: '.nvmrc', replace: `${nodeVersion}\n` },
{
files: '**/Dockerfile*',

regex:
/^FROM(.*) (public.ecr.aws\/docker\/library\/)?node:([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?)(-[a-z0-9]+)?(@sha256:[a-f0-9]{64})?( .*)?$/gm,
replace: `FROM$1 $2node:${nodeVersion}$4$6`,
},
{
files: '**/Dockerfile*',
regex:
/^FROM(.*) gcr.io\/distroless\/nodejs\d+-debian(\d+)(@sha256:[a-f0-9]{64})?(\.[^- \n]+)?(-[^ \n]+)?( .+|)$/gm,
replace: `FROM$1 gcr.io/distroless/nodejs${nodeVersion}-debian$2$4$5$6`,
},

{
files: 'Dockerfile*',
test: /^FROM(.*) node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?( .+|)$/gm,
replace: 'FROM$1 node:<%- version %>$3$4',
files: '**/serverless*.y*ml',
regex: /\bnodejs\d+.x\b/gm,
tests: [isPatchableServerlessVersion],
replace: `nodejs${nodeVersion}.x`,
},
{
files: 'Dockerfile*',
test: /^FROM(.*) gcr.io\/distroless\/nodejs\d+-debian(.+)$/gm,
replace: 'FROM$1 gcr.io/distroless/nodejs<%- version %>-debian$2',
files: '**/serverless*.y*ml',
regex: /\bnode\d+\b/gm,
tests: [isPatchableServerlessVersion],
replace: `node${nodeVersion}`,
},

{
files: 'serverless*.y*ml',
test: /nodejs\d+.x/gm,
replace: 'nodejs<%- version %>.x',
files: '**/infra/**/*.ts',
regex: /NODEJS_\d+_X/g,
replace: `NODEJS_${nodeVersion}_X`,
},
{
files: 'infra/**/*.ts',
test: /NODEJS_\d+_X/g,
replace: 'NODEJS_<%- version %>_X',
files: '**/infra/**/*.ts',
regex: /(target:\s*'node)(\d+)(.+)$/gm,
replace: `$1${nodeVersion}$3`,
},

{
files: '.buildkite/*',
test: /image: node:[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm,
replace: 'image: node:<%- version %>$2',
files: '**/.buildkite/*',
regex:
/(image: )(public.ecr.aws\/docker\/library\/)?(node:)[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm,
replace: `$1$2$3${nodeVersion}$5`,
},
{
files: '.node-version*',
regex: /(\d+(?:\.\d+)*)/g,
replace: `${nodeVersion}`,
},

{
files: '**/package.json',
regex: /("@types\/node":\s*")(\^)?(\d+\.\d+\.\d+)(")/gm,
tests: [isPatchableServerlessVersion],
replace: `$1$2${nodeTypesVersion}$4`,
},
{
files: '**/package.json',
regex:
/(["']engines["']:\s*{[\s\S]*?["']node["']:\s*["']>=)(\d+(?:\.\d+)*)(['"]\s*})/gm,
tests: [isPatchableServerlessVersion, isPatchableSkubaType],
replace: `$1${nodeVersion}$3`,
},

{
files: '**/tsconfig*.json',
regex: /("target":\s*")(ES\d+)"/gim,
tests: [isPatchableServerlessVersion, isPatchableSkubaType],
replace: `$1${ECMAScriptVersion}"`,
},
{
files: '**/tsconfig*.json',
regex: /("lib":\s*\[)([\S\s]*?)(ES\d+)([\S\s]*?)(\])/gim,
tests: [isPatchableServerlessVersion, isPatchableSkubaType],
replace: `$1$2${ECMAScriptVersion}$4$5`,
},

{
files: '**/docker-compose*.y*ml',
regex:
/(image: )(public.ecr.aws\/docker\/library\/)?(node:)[0-9.]+(\.[^- \n]+)?(-[^ \n]+)?$/gm,

replace: `$1$2$3${nodeVersion}$5`,
},
];

const runSubPatch = async (version: number, dir: string, patch: SubPatch) => {
type Versions = {
nodeVersion: number;
nodeTypesVersion: string;
ECMAScriptVersion: string;
};

const runSubPatch = async (dir: string, patch: SubPatch) => {
const readFile = createDestinationFileReader(dir);
const paths = patch.file
? [patch.file]
: await glob(patch.files ?? [], { cwd: dir });

await Promise.all(
paths.map(async (path) => {
if (path.includes('node_modules')) {
return;
}
const contents = await readFile(path);
if (!contents) {
return;
}

if (patch.test && !patch.test.test(contents)) {
if (patch.regex && !patch.regex.test(contents)) {
return;
}

const templated = patch.replace.replaceAll(
'<%- version %>',
version.toString(),
);
if (patch.tests) {
const results = await Promise.all(patch.tests.map((test) => test()));
if (!results.every(Boolean)) {
return;
}
}

await fs.promises.writeFile(
await writePatchedContents({
path,
patch.test ? contents.replaceAll(patch.test, templated) : templated,
);
contents,
templated: patch.replace,
regex: patch.regex,
});
}),
);
};

const upgrade = async (version: number, dir: string) => {
await Promise.all(
subPatches.map((subPatch) => runSubPatch(version, dir, subPatch)),
const writePatchedContents = async ({
path,
contents,
templated,
regex,
}: {
path: string;
contents: string;
templated: string;
regex?: RegExp;
}) =>
await fs.promises.writeFile(
path,
regex ? contents.replaceAll(regex, templated) : templated,
);

const upgrade = async (versions: Versions, dir: string) => {
for (const subPatch of subPatches(versions)) {
await runSubPatch(dir, subPatch);
}
};

export const nodeVersionMigration = async (
version: number,
{
nodeVersion,
ECMAScriptVersion,
defaultNodeTypesVersion,
}: {
nodeVersion: number;
ECMAScriptVersion: string;
defaultNodeTypesVersion: string;
},
dir = process.cwd(),
) => {
log.ok(`Upgrading to Node.js ${version}`);
log.ok(`Upgrading to Node.js ${nodeVersion}`);
try {
await upgrade(version, dir);
log.ok('Upgraded to Node.js', version);
} catch (err) {
if (!(await isPatchableNodeVersion(nodeVersion))) {
throw new Error('Node.js version is not patchable');
}

const { version: nodeTypesVersion, err } = await getNodeTypesVersion(
nodeVersion,
defaultNodeTypesVersion,
);
if (err) {
log.warn(err);
}
await upgrade({ nodeVersion, nodeTypesVersion, ECMAScriptVersion }, dir);
log.ok('Upgraded to Node.js', nodeVersion);
} catch (error) {
log.err('Failed to upgrade');
log.subtle(inspect(err));
log.subtle(inspect(error));
process.exitCode = 1;
}
};
28 changes: 25 additions & 3 deletions src/utils/copy.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { createEjsRenderer, createStringReplacer } from './copy';
import { log } from './logging';

jest.mock('./logging', () => ({
log: {
err: jest.fn(),
subtle: jest.fn(),
bold: (text: string) => text,
},
}));

afterEach(() => jest.clearAllMocks());

describe('createEjsRenderer', () => {
it('renders typical skuba placeholders', () => {
@@ -17,14 +28,25 @@ describe('createEjsRenderer', () => {

const render = createEjsRenderer(templateData);

const output = render(JSON.stringify(input));
const output = render('filename.txt', JSON.stringify(input));

expect(JSON.parse(output)).toEqual({
name: 'seek-koala',
repository: {
url: 'git+ssh://git@github.com/seek-oss/koala.git',
},
});
expect(log.err).not.toHaveBeenCalled();
});

it('does not crash on malformed files', () => {
const input = `<% really we should detect if something is textual or binary and skip if binary, but here we are`;
const render = createEjsRenderer({});

const output = render('filename.txt', input);

expect(output).toEqual(input);
expect(log.err).toHaveBeenCalledWith('Failed to render', 'filename.txt');
});
});

@@ -39,7 +61,7 @@ describe('createStringReplacer', () => {
},
]);

const output = replace(input);
const output = replace('filename.txt', input);

expect(output).toBe('red yellow blue red yellow blue red yellow');
});
@@ -62,7 +84,7 @@ describe('createStringReplacer', () => {
},
]);

const output = replace(input);
const output = replace('filename.txt', input);

expect(output).toBe('cyan magenta yellow');
});
17 changes: 12 additions & 5 deletions src/utils/copy.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import fs from 'fs-extra';
import { isErrorWithCode } from './error';
import { log } from './logging';

export type TextProcessor = (contents: string) => string;
export type TextProcessor = (sourcePath: string, contents: string) => string;

export const copyFile = async (
sourcePath: string,
@@ -19,7 +19,7 @@ export const copyFile = async (
const oldContents = await fs.promises.readFile(sourcePath, 'utf8');

const newContents = processors.reduce(
(contents, process) => process(contents),
(contents, process) => process(sourcePath, contents),
oldContents,
);

@@ -52,8 +52,15 @@ interface CopyFilesOptions {

export const createEjsRenderer =
(templateData: Record<string, unknown>): TextProcessor =>
(contents) =>
ejs.render(contents, templateData);
(sourcePath: string, contents) => {
try {
return ejs.render(contents, templateData, { strict: false });
} catch (err) {
log.err('Failed to render', log.bold(sourcePath));
log.subtle(err);
return contents;
}
};

export const createStringReplacer =
(
@@ -62,7 +69,7 @@ export const createStringReplacer =
output: string;
}>,
): TextProcessor =>
(contents) =>
(_sourcePath: string, contents) =>
replacements.reduce(
(newContents, { input, output }) => newContents.replace(input, output),
contents,
5 changes: 0 additions & 5 deletions src/utils/template.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ export const TEMPLATE_NAMES = [
'express-rest-api',
'greeter',
'koa-rest-api',
'lambda-sqs-worker',
'lambda-sqs-worker-cdk',
'oss-npm-package',
'private-npm-package',
@@ -52,10 +51,6 @@ export const TEMPLATE_DOCUMENTATION_CONFIG: Record<
added: '3.4.1',
filename: 'api.md',
},
'lambda-sqs-worker': {
added: '3.4.1',
filename: 'worker.md',
},
'lambda-sqs-worker-cdk': {
added: '3.13.0',
filename: 'worker.md',
71 changes: 55 additions & 16 deletions src/utils/version.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,71 @@
import searchNpm from 'libnpmsearch';
import validatePackageName from 'validate-npm-package-name';
import npmFetch from 'npm-registry-fetch';
import { z } from 'zod';

import { getSkubaManifest } from './manifest';
import { withTimeout } from './wait';

export const latestNpmVersion = async (
packageName: string,
): Promise<string> => {
const { validForNewPackages } = validatePackageName(packageName);
const NpmVersions = z.record(
z.string(),
z.object({
name: z.string(),
version: z.string(),
deprecated: z.string().optional(),
}),
);

if (!validForNewPackages) {
throw new Error(`Package "${packageName}" does not have a valid name`);
}
export type NpmVersions = z.infer<typeof NpmVersions>;

const PackageResponse = z.object({
'dist-tags': z.record(z.string(), z.string()).optional(),
versions: NpmVersions,
});

const [result] = await searchNpm(packageName, { limit: 1, timeout: 5_000 });
const getNpmPackage = async (packageName: string) => {
try {
const response = await npmFetch.json(packageName, {
headers: {
Accept:
'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
},
});

const parsedResponse = PackageResponse.safeParse(response);
if (!parsedResponse.success) {
throw new Error(
`Failed to parse package response from npm for package ${packageName}`,
);
}

if (result?.name !== packageName) {
throw new Error(
`Package "${packageName}" does not exist on the npm registry`,
);
return parsedResponse.data;
} catch (error) {
if (
error instanceof Error &&
'statusCode' in error &&
error.statusCode === 404
) {
return null;
}
throw error;
}
};

return result.version;
export const getNpmVersions = async (
packageName: string,
): Promise<NpmVersions | null> => {
const response = await getNpmPackage(packageName);
return response?.versions ?? null;
};

export const getLatestNpmVersion = async (
packageName: string,
): Promise<string | null> => {
const response = await getNpmPackage(packageName);
return response?.['dist-tags']?.latest ?? null;
};

const latestSkubaVersion = async (): Promise<string | null> => {
try {
const result = await withTimeout(latestNpmVersion('skuba'), { s: 2 });
const result = await withTimeout(getLatestNpmVersion('skuba'), { s: 2 });

return result.ok ? result.value : null;
} catch {
22 changes: 11 additions & 11 deletions src/wrapper/http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import http from 'http';
import type { AddressInfo } from 'net';

import { serializeError } from 'serialize-error';
import util from 'util';

import { log } from '../utils/logging';

@@ -14,17 +13,14 @@ import { log } from '../utils/logging';
export const createRequestListenerFromFunction =
(fn: (...args: unknown[]) => Promise<unknown>): http.RequestListener =>
async (req, res) => {
const writeJsonResponse = (statusCode: number, jsonResponse: unknown) => {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });

return new Promise<void>((resolve, reject) =>
jsonResponse === undefined
const writeResponse = (response: unknown) =>
new Promise<void>((resolve, reject) =>
response === undefined
? res.end(resolve)
: res.write(JSON.stringify(jsonResponse, null, 2), 'utf8', (err) =>
: res.write(response, 'utf8', (err) =>
err ? reject(err) : res.end(resolve),
),
);
};

try {
const requestBody = await new Promise<string>((resolve, reject) => {
@@ -46,9 +42,13 @@ export const createRequestListenerFromFunction =

const response: unknown = await fn(...args);

await writeJsonResponse(200, response);
res.writeHead(200, { 'Content-Type': 'application/json' });

await writeResponse(JSON.stringify(response, null, 2));
} catch (err) {
await writeJsonResponse(500, serializeError(err));
res.writeHead(500);

await writeResponse(util.inspect(err));
}
};

26 changes: 5 additions & 21 deletions src/wrapper/main.test.ts
Original file line number Diff line number Diff line change
@@ -49,17 +49,8 @@ test('asyncFunctionHandler', async () => {
.post('/')
.send([null, {}])
.expect(500)
.expect(({ body }) =>
expect(body).toMatchInlineSnapshot(
{ stack: expect.any(String) },
`
{
"message": "falsy event",
"name": "Error",
"stack": Any<String>,
}
`,
),
.expect(({ text }) =>
expect(text.split('\n')[0]).toBe('Error: falsy event'),
),
]);
});
@@ -167,16 +158,9 @@ test('syncFunctionHandler', async () => {
.post('/')
.send('Invalid JSON')
.expect(500)
.expect(({ body }) =>
expect(body).toMatchInlineSnapshot(
{ stack: expect.any(String) },
`
{
"message": "Unexpected token 'I', "Invalid JSON" is not valid JSON",
"name": "SyntaxError",
"stack": Any<String>,
}
`,
.expect(({ text }) =>
expect(text.split('\n')[0]).toBe(
`SyntaxError: Unexpected token 'I', "Invalid JSON" is not valid JSON`,
),
),
]);
6 changes: 4 additions & 2 deletions src/wrapper/testing/expressRequestListener.ts
Original file line number Diff line number Diff line change
@@ -2,10 +2,12 @@ import express from 'express';

const app = express().use((req, res) => {
if (req.path === '/express') {
return res.end('Express!');
res.end('Express!');
return;
}

return res.status(404).end();
res.status(404).end();
return;
});

Object.assign(app, {
4 changes: 2 additions & 2 deletions template/base/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"lib": ["ES2022"],
"lib": ["ES2024"],
"outDir": "lib",
"paths": {
"src": ["src"]
},
"target": "ES2022"
"target": "ES2024"
},
"exclude": ["lib*/**/*"],
"extends": "skuba/config/tsconfig.json"
14 changes: 7 additions & 7 deletions template/express-rest-api/.buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ configs:
NPM_READ_TOKEN: arn:aws:secretsmanager:ap-southeast-2:987872074697:secret:npm/npm-read-token

- &docker-ecr-cache
seek-oss/docker-ecr-cache#v2.2.0: &docker-ecr-cache-defaults
seek-oss/docker-ecr-cache#v2.2.1: &docker-ecr-cache-defaults
cache-on:
- .npmrc
- package.json#.packageManager
@@ -18,7 +18,7 @@ configs:
secrets: id=npm,src=/tmp/.npmrc

- &private-npm
seek-oss/private-npm#v1.2.0:
seek-oss/private-npm#v1.3.0:
env: NPM_READ_TOKEN
output-path: /tmp/

@@ -38,7 +38,7 @@ steps:
plugins:
- *aws-sm
- *private-npm
- seek-oss/docker-ecr-cache#v2.2.0:
- seek-oss/docker-ecr-cache#v2.2.1:
<<: *docker-ecr-cache-defaults
skip-pull-from-cache: true

@@ -57,7 +57,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
run: app
environment:
- GITHUB_API_TOKEN
@@ -70,7 +70,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: build
file: gantry.build.yml
region: <%- region %>
@@ -87,7 +87,7 @@ steps:
concurrency_group: <%- teamName %>/deploy/gantry/<%- devGantryEnvironmentName %>
key: deploy-dev
plugins:
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: apply
environment: <%- devGantryEnvironmentName %>
file: gantry.apply.yml
@@ -102,7 +102,7 @@ steps:
concurrency_group: <%- teamName %>/deploy/gantry/<%- prodGantryEnvironmentName %>
depends_on: deploy-dev
plugins:
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: apply
environment: <%- prodGantryEnvironmentName %>
file: gantry.apply.yml
4 changes: 1 addition & 3 deletions template/express-rest-api/.gantry/common.yml
Original file line number Diff line number Diff line change
@@ -9,6 +9,4 @@ image: '{{values "prodAccountId"}}.dkr.ecr.<%- region %>.amazonaws.com/{{values
# datadogSecretId: arn:aws:secretsmanager:<%- region %>:<aws-account-id>:secret:<secret-name>

tags:
seek:source:sha: '{{.CommitSHA}}'
seek:source:url: 'https://github.com/SEEK-Jobs/<%- repoName %>'
# seek:system:name: 'TODO: https://rfc.skinfra.xyz/RFC019-AWS-Tagging-Standard.html#required-tags'
# seek:system:name: 'TODO: https://rfc.skinfra.xyz/RFC051-AWS-Tagging-Standard.html#tagging-schema'
2 changes: 1 addition & 1 deletion template/express-rest-api/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
2 changes: 1 addition & 1 deletion template/express-rest-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ RUN pnpm install --offline --prod

###

FROM gcr.io/distroless/nodejs20-debian12 AS runtime
FROM gcr.io/distroless/nodejs22-debian12 AS runtime

WORKDIR /workdir

4 changes: 2 additions & 2 deletions template/express-rest-api/Dockerfile.dev-deps
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.14

FROM public.ecr.aws/docker/library/node:20-alpine AS dev-deps
FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps

RUN --mount=type=bind,source=package.json,target=package.json \
corepack enable pnpm && corepack install
3 changes: 0 additions & 3 deletions template/express-rest-api/gantry.build.yml
Original file line number Diff line number Diff line change
@@ -8,7 +8,4 @@ buildArgs:
# https://github.com/seek-oss/docker-ecr-cache-buildkite-plugin#building-on-the-resulting-image
BASE_IMAGE: '{{.Env.BUILDKITE_PLUGIN_DOCKER_ECR_CACHE_EXPORT_IMAGE}}:{{.Env.BUILDKITE_PLUGIN_DOCKER_ECR_CACHE_EXPORT_TAG}}'

# SEEK-Jobs/gantry#1661
failOnScanFindings: false

cpuArchitecture: <%- platformName %>
12 changes: 6 additions & 6 deletions template/express-rest-api/package.json
Original file line number Diff line number Diff line change
@@ -14,22 +14,22 @@
},
"dependencies": {
"@seek/logger": "^9.0.0",
"express": "^4.17.1",
"express": "^5.0.0",
"hot-shots": "^10.0.0",
"seek-datadog-custom-metrics": "^4.6.3",
"skuba-dive": "^2.0.0"
},
"devDependencies": {
"@types/express": "^4.17.13",
"@types/node": "^20.16.5",
"@types/express": "^5.0.0",
"@types/node": "^22.13.10",
"@types/supertest": "^6.0.0",
"mime": "^4.0.1",
"pino-pretty": "^11.0.0",
"pino-pretty": "^13.0.0",
"skuba": "*",
"supertest": "^7.0.0"
},
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@10.6.2",
"engines": {
"node": ">=20"
"node": ">=22"
}
}
6 changes: 3 additions & 3 deletions template/greeter/.buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -11,15 +11,15 @@ configs:
NPM_READ_TOKEN: arn:aws:secretsmanager:ap-southeast-2:987872074697:secret:npm/npm-read-token

- &docker-ecr-cache
seek-oss/docker-ecr-cache#v2.2.0:
seek-oss/docker-ecr-cache#v2.2.1:
cache-on:
- .npmrc
- package.json#.packageManager
- pnpm-lock.yaml
secrets: id=npm,src=/tmp/.npmrc

- &private-npm
seek-oss/private-npm#v1.2.0:
seek-oss/private-npm#v1.3.0:
env: NPM_READ_TOKEN
output-path: /tmp/

@@ -38,7 +38,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
run: app
environment:
- GITHUB_API_TOKEN
2 changes: 1 addition & 1 deletion template/greeter/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
4 changes: 2 additions & 2 deletions template/greeter/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.14

FROM public.ecr.aws/docker/library/node:20-alpine AS dev-deps
FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps

RUN --mount=type=bind,source=package.json,target=package.json \
corepack enable pnpm && corepack install
2 changes: 1 addition & 1 deletion template/greeter/README.md
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ It does not assume a deployment method or environment.
For inspiration in this space, check out:

- The `koa-rest-api` template for containerised deployments
- The `lambda-sqs-worker` template for Lambda deployments
- The `lambda-sqs-worker-cdk` template for Lambda deployments

## Support

6 changes: 3 additions & 3 deletions template/greeter/package.json
Original file line number Diff line number Diff line change
@@ -16,11 +16,11 @@
"skuba-dive": "^2.0.0"
},
"devDependencies": {
"@types/node": "^20.9.0",
"@types/node": "^22.13.10",
"skuba": "*"
},
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@10.6.2",
"engines": {
"node": ">=20"
"node": ">=22"
}
}
14 changes: 7 additions & 7 deletions template/koa-rest-api/.buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ configs:
NPM_READ_TOKEN: arn:aws:secretsmanager:ap-southeast-2:987872074697:secret:npm/npm-read-token

- &docker-ecr-cache
seek-oss/docker-ecr-cache#v2.2.0: &docker-ecr-cache-defaults
seek-oss/docker-ecr-cache#v2.2.1: &docker-ecr-cache-defaults
cache-on:
- .npmrc
- package.json#.packageManager
@@ -18,7 +18,7 @@ configs:
secrets: id=npm,src=/tmp/.npmrc

- &private-npm
seek-oss/private-npm#v1.2.0:
seek-oss/private-npm#v1.3.0:
env: NPM_READ_TOKEN
output-path: /tmp/

@@ -38,7 +38,7 @@ steps:
plugins:
- *aws-sm
- *private-npm
- seek-oss/docker-ecr-cache#v2.2.0:
- seek-oss/docker-ecr-cache#v2.2.1:
<<: *docker-ecr-cache-defaults
skip-pull-from-cache: true

@@ -57,7 +57,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
run: app
environment:
- GITHUB_API_TOKEN
@@ -70,7 +70,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: build
file: gantry.build.yml
region: <%- region %>
@@ -87,7 +87,7 @@ steps:
concurrency_group: <%- teamName %>/deploy/gantry/<%- devGantryEnvironmentName %>
key: deploy-dev
plugins:
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: apply
environment: <%- devGantryEnvironmentName %>
file: gantry.apply.yml
@@ -102,7 +102,7 @@ steps:
concurrency_group: <%- teamName %>/deploy/gantry/<%- prodGantryEnvironmentName %>
depends_on: deploy-dev
plugins:
- seek-jobs/gantry#v3.0.0:
- seek-jobs/gantry#v4.0.0:
command: apply
environment: <%- prodGantryEnvironmentName %>
file: gantry.apply.yml
4 changes: 1 addition & 3 deletions template/koa-rest-api/.gantry/common.yml
Original file line number Diff line number Diff line change
@@ -9,6 +9,4 @@ image: '{{values "prodAccountId"}}.dkr.ecr.<%- region %>.amazonaws.com/{{values
# datadogSecretId: arn:aws:secretsmanager:<%- region %>:<aws-account-id>:secret:<secret-name>

tags:
seek:source:sha: '{{.CommitSHA}}'
seek:source:url: 'https://github.com/SEEK-Jobs/<%- repoName %>'
# seek:system:name: 'TODO: https://rfc.skinfra.xyz/RFC019-AWS-Tagging-Standard.html#required-tags'
# seek:system:name: 'TODO: https://rfc.skinfra.xyz/RFC051-AWS-Tagging-Standard.html#tagging-schema'
2 changes: 1 addition & 1 deletion template/koa-rest-api/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
2 changes: 1 addition & 1 deletion template/koa-rest-api/Dockerfile
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ RUN pnpm install --offline --prod

###

FROM gcr.io/distroless/nodejs20-debian12 AS runtime
FROM gcr.io/distroless/nodejs22-debian12 AS runtime

WORKDIR /workdir

4 changes: 2 additions & 2 deletions template/koa-rest-api/Dockerfile.dev-deps
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.14

FROM public.ecr.aws/docker/library/node:20-alpine AS dev-deps
FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps

RUN --mount=type=bind,source=package.json,target=package.json \
corepack enable pnpm && corepack install
3 changes: 0 additions & 3 deletions template/koa-rest-api/gantry.build.yml
Original file line number Diff line number Diff line change
@@ -8,7 +8,4 @@ buildArgs:
# https://github.com/seek-oss/docker-ecr-cache-buildkite-plugin#building-on-the-resulting-image
BASE_IMAGE: '{{.Env.BUILDKITE_PLUGIN_DOCKER_ECR_CACHE_EXPORT_IMAGE}}:{{.Env.BUILDKITE_PLUGIN_DOCKER_ECR_CACHE_EXPORT_TAG}}'

# SEEK-Jobs/gantry#1661
failOnScanFindings: false

cpuArchitecture: <%- platformName %>
16 changes: 8 additions & 8 deletions template/koa-rest-api/package.json
Original file line number Diff line number Diff line change
@@ -17,11 +17,11 @@
"@koa/router": "^13.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/core": "^1.25.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.53.0",
"@opentelemetry/instrumentation-aws-sdk": "^0.44.0",
"@opentelemetry/instrumentation-http": "^0.53.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
"@opentelemetry/instrumentation-aws-sdk": "^0.49.0",
"@opentelemetry/instrumentation-http": "^0.57.0",
"@opentelemetry/propagator-b3": "^1.25.0",
"@opentelemetry/sdk-node": "^0.53.0",
"@opentelemetry/sdk-node": "^0.57.0",
"@seek/logger": "^9.0.0",
"hot-shots": "^10.0.0",
"koa": "^2.13.4",
@@ -36,16 +36,16 @@
"@types/co-body": "^6.1.3",
"@types/koa": "^2.13.4",
"@types/koa__router": "^12.0.0",
"@types/node": "^20.16.5",
"@types/node": "^22.13.10",
"@types/supertest": "^6.0.0",
"chance": "^1.1.8",
"mime": "^4.0.1",
"pino-pretty": "^11.0.0",
"pino-pretty": "^13.0.0",
"skuba": "*",
"supertest": "^7.0.0"
},
"packageManager": "pnpm@9.12.2",
"packageManager": "pnpm@10.6.2",
"engines": {
"node": ">=20"
"node": ">=22"
}
}
1 change: 0 additions & 1 deletion template/koa-rest-api/src/framework/server.test.ts
Original file line number Diff line number Diff line change
@@ -232,7 +232,6 @@ describe('createApp', () => {
const err = chance.sentence();

middleware.mockImplementation(() => {
// eslint-disable-next-line @typescript-eslint/only-throw-error
throw err;
});

4 changes: 2 additions & 2 deletions template/koa-rest-api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -5,13 +5,13 @@
// open-telemetry/opentelemetry-js#3580
"DOM",

"ES2022"
"ES2024"
],
"outDir": "lib",
"paths": {
"src": ["src"]
},
"target": "ES2022"
"target": "ES2024"
},
"exclude": ["lib*/**/*"],
"extends": "skuba/config/tsconfig.json"
10 changes: 5 additions & 5 deletions template/lambda-sqs-worker-cdk/.buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -9,15 +9,15 @@ configs:
NPM_READ_TOKEN: arn:aws:secretsmanager:ap-southeast-2:987872074697:secret:npm/npm-read-token

- &docker-ecr-cache
seek-oss/docker-ecr-cache#v2.2.0: &docker-ecr-cache-defaults
seek-oss/docker-ecr-cache#v2.2.1: &docker-ecr-cache-defaults
cache-on:
- .npmrc
- package.json#.packageManager
- pnpm-lock.yaml
secrets: id=npm,src=/tmp/.npmrc

- &private-npm
seek-oss/private-npm#v1.2.0:
seek-oss/private-npm#v1.3.0:
env: NPM_READ_TOKEN
output-path: /tmp/

@@ -33,7 +33,7 @@ configs:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
dependencies: false
run: app
environment:
@@ -63,7 +63,7 @@ steps:
- *aws-sm
- *private-npm
- *docker-ecr-cache
- docker-compose#v5.4.1:
- docker-compose#v5.6.0:
run: app
environment:
- GITHUB_API_TOKEN
@@ -78,7 +78,7 @@ steps:
plugins:
- *aws-sm
- *private-npm
- seek-oss/docker-ecr-cache#v2.2.0:
- seek-oss/docker-ecr-cache#v2.2.1:
<<: *docker-ecr-cache-defaults
skip-pull-from-cache: true

2 changes: 1 addition & 1 deletion template/lambda-sqs-worker-cdk/.nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20
22
6 changes: 3 additions & 3 deletions template/lambda-sqs-worker-cdk/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.14

FROM public.ecr.aws/docker/library/node:20-alpine AS dev-deps
FROM public.ecr.aws/docker/library/node:22-alpine AS dev-deps

# Needed for cdk
RUN apk add --no-cache bash
RUN apk add --no-cache bash git

RUN --mount=type=bind,source=package.json,target=package.json \
corepack enable pnpm && corepack install
7 changes: 4 additions & 3 deletions template/lambda-sqs-worker-cdk/README.md
Original file line number Diff line number Diff line change
@@ -14,9 +14,10 @@ Next steps:
3. [ ] Add the repository to BuildAgency;
see our internal [Buildkite Docs] for more information.
4. [ ] Add Datadog extension, deployment bucket configuration and data classification tags to [infra/config.ts](infra/config.ts).
5. [ ] Push local commits to the upstream GitHub branch.
6. [ ] Configure [GitHub repository settings].
7. [ ] Delete this checklist 😌.
5. [ ] For the smoke test, make sure Lambda has permissions to publish SNS message and configure `sourceSnsTopicArn` in [infra/config.ts](infra/config.ts).
6. [ ] Push local commits to the upstream GitHub branch.
7. [ ] Configure [GitHub repository settings].
8. [ ] Delete this checklist 😌.

[Buildkite Docs]: https://backstage.myseek.xyz/docs/default/component/buildkite-docs
[GitHub repository settings]: https://github.com/<%-orgName%>/<%-repoName%>/settings
Original file line number Diff line number Diff line change
@@ -187,7 +187,6 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
"DD_SERVERLESS_APPSEC_ENABLED": "false",
"DD_SERVERLESS_LOGS_ENABLED": "false",
"DD_SITE": "datadoghq.com",
"DD_TAGS": "git.commit.sha:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,git.repository_url:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"DD_TRACE_ENABLED": "true",
"DESTINATION_SNS_TOPIC_ARN": {
"Ref": "destinationtopicDCE2E0B8",
@@ -228,7 +227,7 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
"Arn",
],
},
"Runtime": "nodejs20.x",
"Runtime": "nodejs22.x",
"Tags": [
{
"Key": "aws-codedeploy-hooks",
@@ -604,6 +603,13 @@ exports[`returns expected CloudFormation stack for dev 1`] = `
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sns:Publish",
"Effect": "Allow",
"Resource": {
"Ref": "destinationtopicDCE2E0B8",
},
},
{
"Action": [
"secretsmanager:GetSecretValue",
@@ -907,7 +913,6 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
"DD_SERVERLESS_APPSEC_ENABLED": "false",
"DD_SERVERLESS_LOGS_ENABLED": "false",
"DD_SITE": "datadoghq.com",
"DD_TAGS": "git.commit.sha:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx,git.repository_url:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"DD_TRACE_ENABLED": "true",
"DESTINATION_SNS_TOPIC_ARN": {
"Ref": "destinationtopicDCE2E0B8",
@@ -948,7 +953,7 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
"Arn",
],
},
"Runtime": "nodejs20.x",
"Runtime": "nodejs22.x",
"Tags": [
{
"Key": "aws-codedeploy-hooks",
@@ -1324,6 +1329,13 @@ exports[`returns expected CloudFormation stack for prod 1`] = `
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sns:Publish",
"Effect": "Allow",
"Resource": {
"Ref": "destinationtopicDCE2E0B8",
},
},
{
"Action": [
"secretsmanager:GetSecretValue",
8 changes: 5 additions & 3 deletions template/lambda-sqs-worker-cdk/infra/appStack.test.ts
Original file line number Diff line number Diff line change
@@ -16,9 +16,11 @@ jest.useFakeTimers({
});

const originalEnv = process.env.ENVIRONMENT;
const originalVersion = process.env.VERSION;

afterAll(() => {
process.env.ENVIRONMENT = originalEnv;
process.env.VERSION = originalVersion;
});

afterEach(() => {
@@ -29,6 +31,7 @@ it.each(['dev', 'prod'])(
'returns expected CloudFormation stack for %s',
async (env) => {
process.env.ENVIRONMENT = env;
process.env.VERSION = 'local';

const { AppStack } = await import('./appStack');

@@ -63,9 +66,8 @@ it.each(['dev', 'prod'])(
)
.replaceAll(/"Value":"v\d+\.\d+\.\d+"/g, (_) => `"Value": "vx.x.x"`)
.replace(
/"DD_TAGS":"git.commit.sha:([0-9a-f]+),git.repository_url:([^\"]+)"/g,
(_, sha, url) =>
`"DD_TAGS":"git.commit.sha:${'x'.repeat(sha.length)},git.repository_url:${'x'.repeat(url.length)}"`,
/"DD_TAGS":"git.commit.sha:([0-9a-f]+),git.repository_url:([^\"]+)",/g,
'',
)
.replaceAll(
/(layer:Datadog-Extension-.+?:)\d+/g,
10 changes: 6 additions & 4 deletions template/lambda-sqs-worker-cdk/infra/appStack.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import {
aws_sqs,
} from 'aws-cdk-lib';
import type { Construct } from 'constructs';
import { Datadog } from 'datadog-cdk-constructs-v2';
import { DatadogLambda } from 'datadog-cdk-constructs-v2';

import { config } from './config';

@@ -80,7 +80,7 @@ export class AppStack extends Stack {

const worker = new aws_lambda_nodejs.NodejsFunction(this, 'worker', {
architecture: aws_lambda.Architecture[architecture],
runtime: aws_lambda.Runtime.NODEJS_20_X,
runtime: aws_lambda.Runtime.NODEJS_22_X,
environmentEncryption: kmsKey,
// aws-sdk-v3 sets this to true by default, so it is not necessary to set the environment variable
// https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/node-reusing-connections.html
@@ -89,7 +89,7 @@ export class AppStack extends Stack {
timeout: Duration.seconds(30),
bundling: {
sourceMap: true,
target: 'node20',
target: 'node22',
// aws-sdk-v3 is set as an external module by default, but we want it to be bundled with the function
externalModules: [],
nodeModules: ['datadog-lambda-js', 'dd-trace'],
@@ -109,13 +109,15 @@ export class AppStack extends Stack {
reservedConcurrentExecutions: config.workerLambda.reservedConcurrency,
});

destinationTopic.grantPublish(worker);

const datadogSecret = aws_secretsmanager.Secret.fromSecretPartialArn(
this,
'datadog-api-key-secret',
config.datadogApiKeySecretArn,
);

const datadog = new Datadog(this, 'datadog', {
const datadog = new DatadogLambda(this, 'datadog', {
apiKeySecret: datadogSecret,
addLayers: false,
enableDatadogLogs: false,
2 changes: 1 addition & 1 deletion template/lambda-sqs-worker-cdk/infra/config.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ const ENVIRONMENTS = ['dev', 'prod'] as const;

type Environment = (typeof ENVIRONMENTS)[number];

export const environment = Env.oneOf(ENVIRONMENTS)('ENVIRONMENT');
const environment = Env.oneOf(ENVIRONMENTS)('ENVIRONMENT');

interface Config {
appName: string;
8 changes: 3 additions & 5 deletions template/lambda-sqs-worker-cdk/infra/index.ts
Original file line number Diff line number Diff line change
@@ -2,17 +2,15 @@ import { HookStack } from '@seek/aws-codedeploy-infra';
import { App } from 'aws-cdk-lib';

import { AppStack } from './appStack';
import { config, environment } from './config';
import { config } from './config';

const app = new App();

const appStack = new AppStack(app, 'appStack', {
stackName: config.appName,
tags: {
'seek:env:label': environment,
'seek:source:sha': process.env.BUILDKITE_COMMIT ?? 'na',
// 'seek:source:url': 'TODO: add source URL',
// 'seek:system:name': 'TODO: add system name',
'seek:source:url': 'https://github.com/SEEK-Jobs/<%- repoName %>',
// 'seek:system:name': 'TODO: https://rfc.skinfra.xyz/RFC051-AWS-Tagging-Standard.html#tagging-schema',
},
});

Loading