@sap/cds-dk 9.4.3 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/bin/compile/index.js +1 -1
  3. package/bin/export.js +1 -1
  4. package/bin/help.js +0 -1
  5. package/bin/import.js +8 -3
  6. package/bin/repl.js +7 -11
  7. package/bin/up.js +150 -20
  8. package/bin/util/containerize.js +66 -25
  9. package/bin/version.js +4 -4
  10. package/lib/bind/k8s.js +1 -1
  11. package/lib/build/plugins/helm/files/templates/_deployment_helpers.tpl +1 -1
  12. package/lib/build/plugins/helm/index.js +1 -1
  13. package/lib/build/provider/hana/2migration.js +16 -24
  14. package/lib/build/provider/hana/index.js +1 -1
  15. package/lib/build/provider/hana/template/.hdiconfig-all +7 -1
  16. package/lib/build/provider/hana/template/package.json +1 -1
  17. package/lib/build/provider/internalBuildPlugin.js +15 -1
  18. package/lib/build/provider/java/index.js +12 -1
  19. package/lib/build/provider/mtx-sidecar/index.js +2 -1
  20. package/lib/build/provider/nodejs/index.js +2 -1
  21. package/lib/build/util.js +3 -3
  22. package/lib/compile/to_openapi.js +1 -5
  23. package/lib/import/common.js +2 -1
  24. package/lib/import/odata/ParserContext.js +1 -0
  25. package/lib/import/odata/v4parser.js +48 -3
  26. package/lib/import/openapi/importOpenAPI.js +6 -1
  27. package/lib/init/constants.js +1 -1
  28. package/lib/init/index.js +15 -6
  29. package/lib/init/merge.js +16 -0
  30. package/lib/init/projectReader.js +19 -4
  31. package/lib/init/registries/mta.js +2 -2
  32. package/lib/init/template/ams/index.js +1 -1
  33. package/lib/init/template/app-frontend/files/app-package.json.hbs +1 -7
  34. package/lib/init/template/app-frontend/files/ui5.yaml.hbs +20 -1
  35. package/lib/init/template/app-frontend/index.js +0 -4
  36. package/lib/init/template/application-logging/index.js +9 -14
  37. package/lib/init/template/approuter/files/mta.yaml.hbs +0 -2
  38. package/lib/init/template/approuter/index.js +6 -7
  39. package/lib/init/template/cloud-logging/index.js +2 -3
  40. package/lib/init/template/containerize/files/containerize.yaml.hbs +17 -2
  41. package/lib/init/template/containerize/index.js +0 -4
  42. package/lib/init/template/data/as-json.js +18 -20
  43. package/lib/init/template/dynatrace/index.js +0 -2
  44. package/lib/init/template/github-actions/files/workflows/cf.yaml.hbs +16 -1
  45. package/lib/init/template/github-actions/files/workflows/test.yaml.hbs +20 -0
  46. package/lib/init/template/github-actions/index.js +5 -8
  47. package/lib/init/template/hana/files/db/package.json +2 -2
  48. package/lib/init/template/hana/files/mta.yaml.hbs +0 -4
  49. package/lib/init/template/hana/index.js +3 -5
  50. package/lib/init/template/helm/index.js +2 -7
  51. package/lib/init/template/helm-unified-runtime/index.js +0 -4
  52. package/lib/init/template/html5-repo/files/app-package.json.hbs +1 -7
  53. package/lib/init/template/html5-repo/files/ui5.yaml.hbs +20 -1
  54. package/lib/init/template/html5-repo/index.js +6 -12
  55. package/lib/init/template/ias/files/values.yaml.hbs +6 -6
  56. package/lib/init/template/ias/index.js +12 -20
  57. package/lib/init/template/kyma/index.js +14 -1
  58. package/lib/init/template/malware-scanner/files/mta.yaml.hbs +3 -1
  59. package/lib/init/template/malware-scanner/files/values.yaml +3 -1
  60. package/lib/init/template/multitenancy/files/mta.yaml.hbs +1 -3
  61. package/lib/init/template/multitenancy/files/values.yaml.hbs +8 -0
  62. package/lib/init/template/multitenancy/index.js +8 -3
  63. package/lib/init/template/nodejs/files/package.json.hbs +1 -1
  64. package/lib/init/template/pipeline/files/app-package.json.hbs +1 -7
  65. package/lib/init/template/pipeline/files/ui5.yaml.hbs +20 -1
  66. package/lib/init/template/pipeline/index.js +1 -2
  67. package/lib/init/template/portal/files/mta.yaml.hbs +5 -3
  68. package/lib/init/template/portal/index.js +3 -6
  69. package/lib/init/template/sample/files/_i18n/i18n_de.properties +12 -0
  70. package/lib/init/template/sample/files/_i18n/i18n_en.properties +21 -0
  71. package/lib/init/template/sample/files/_i18n/i18n_fr.properties +20 -0
  72. package/lib/init/template/sample/files/_i18n/messages_de.properties +34 -0
  73. package/lib/init/template/sample/files/_i18n/messages_en.properties +34 -0
  74. package/lib/init/template/sample/files/_i18n/messages_fr.properties +34 -0
  75. package/lib/init/template/sample/files/app/_i18n/i18n_de.properties +0 -26
  76. package/lib/init/template/sample/files/app/_i18n/i18n_en.properties +8 -0
  77. package/lib/init/template/sample/files/app/admin-authors/fiori-service.cds +52 -0
  78. package/lib/init/template/sample/files/app/admin-authors/webapp/Component.js.hbs +3 -0
  79. package/lib/init/template/sample/files/app/admin-authors/webapp/i18n/i18n.properties +11 -0
  80. package/lib/init/template/sample/files/app/admin-authors/webapp/manifest.json +130 -0
  81. package/lib/init/template/sample/files/app/admin-books/fiori-service.cds +81 -88
  82. package/lib/init/template/sample/files/app/admin-books/webapp/Component.js.hbs +3 -0
  83. package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n.properties +10 -2
  84. package/lib/init/template/sample/files/app/admin-books/webapp/manifest.json +24 -48
  85. package/lib/init/template/sample/files/app/appconfig/fioriSandboxConfig.json +54 -3
  86. package/lib/init/template/sample/files/app/browse/fiori-service.cds +30 -30
  87. package/lib/init/template/sample/files/app/browse/webapp/Component.js.hbs +3 -0
  88. package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n.properties +10 -3
  89. package/lib/init/template/sample/files/app/browse/webapp/manifest.json +18 -23
  90. package/lib/init/template/sample/files/app/common.cds +126 -158
  91. package/lib/init/template/sample/files/app/fiori-apps.html +30 -0
  92. package/lib/init/template/sample/files/app/genres/fiori-service.cds +33 -0
  93. package/lib/init/template/sample/files/app/genres/tree-view.cds +42 -0
  94. package/lib/init/template/sample/files/app/genres/value-help.cds +6 -0
  95. package/lib/init/template/sample/files/app/genres/webapp/Component.js.hbs +3 -0
  96. package/lib/init/template/sample/files/app/genres/webapp/i18n/i18n.properties +4 -0
  97. package/lib/init/template/sample/files/app/genres/webapp/i18n/i18n_de.properties +2 -0
  98. package/lib/init/template/sample/files/app/genres/webapp/manifest.json +126 -0
  99. package/lib/init/template/sample/files/app/services.cds +6 -2
  100. package/lib/init/template/sample/files/db/currencies.cds +13 -0
  101. package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Books.csv +5 -5
  102. package/lib/init/template/sample/files/db/data/{sap.capire.bookshop-Books_texts.csv → sap.capire.bookshop-Books.texts.csv} +4 -4
  103. package/lib/init/template/sample/files/db/data/sap.capire.bookshop-Genres.csv +42 -15
  104. package/lib/init/template/sample/files/db/data/sap.common-Currencies.csv +12 -0
  105. package/lib/init/template/sample/files/db/data/sap.common-Currencies.texts.csv +15 -0
  106. package/lib/init/template/sample/files/db/hana/index.cds +10 -0
  107. package/lib/init/template/sample/files/db/schema.cds +35 -22
  108. package/lib/init/template/sample/files/db/sqlite/index.cds +10 -0
  109. package/lib/init/template/sample/files/package.json.hbs +14 -1
  110. package/lib/init/template/sample/files/srv/access-control.cds +3 -0
  111. package/lib/init/template/sample/files/srv/admin-service.cds +3 -2
  112. package/lib/init/template/sample/files/srv/cat-service.cds +7 -11
  113. package/lib/init/template/sample/index.js +26 -17
  114. package/lib/init/template/telemetry/index.js +1 -3
  115. package/lib/init/template/typescript/files/package.json +1 -1
  116. package/lib/init/template/workzone-standard/files/mta.yaml.hbs +0 -5
  117. package/lib/init/template/workzone-standard/index.js +0 -1
  118. package/lib/init/template/xsuaa/files/mta.yaml.hbs +4 -0
  119. package/lib/init/template/xsuaa/index.js +3 -5
  120. package/lib/lint/Linter.js +10 -4
  121. package/lib/lint/eslintHelp.json +1 -1
  122. package/lib/mtx/tenant_action.js +12 -4
  123. package/lib/util/checksum.js +32 -0
  124. package/lib/util/question.js +60 -7
  125. package/npm-shrinkwrap.json +59 -35
  126. package/package.json +2 -2
  127. package/lib/init/template/app-frontend/files/ui5-deploy.yaml.hbs +0 -25
  128. package/lib/init/template/github-actions/files/cf-setup/action.yaml +0 -53
  129. package/lib/init/template/html5-repo/files/ui5-deploy.yaml.hbs +0 -25
  130. package/lib/init/template/pipeline/files/ui5-deploy.yaml.hbs +0 -25
  131. package/lib/init/template/sample/files/app/_i18n/i18n.properties +0 -29
  132. package/lib/init/template/sample/files/app/admin-books/webapp/Component.js +0 -8
  133. package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n_de.properties +0 -3
  134. package/lib/init/template/sample/files/app/admin-books/webapp/i18n/i18n_en.properties +0 -3
  135. package/lib/init/template/sample/files/app/browse/webapp/Component.js +0 -7
  136. package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n_de.properties +0 -3
  137. package/lib/init/template/sample/files/app/browse/webapp/i18n/i18n_en.properties +0 -4
  138. package/lib/init/template/sample/files/app/index.html +0 -32
package/CHANGELOG.md CHANGED
@@ -6,17 +6,58 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
8
8
 
9
+ ## Version 9.5.0 - 2025-12-01
10
+
11
+ ### Added
12
+
13
+ - `cds import` now accepts `--into-namespace <string>` to override the namespace entities are imported to, when importing OpenAPI definitions.
14
+
15
+ ### Changed
16
+
17
+ - `cds add portal` adds the correct service key configuration to the _mta.yaml_.
18
+ - `cds add sample` creates sample files aligned with recent versions of bookshop and bookstore in https://github.com/capire.
19
+ - `cds add sample` uses a matching ID in generated `manifest.json` and UI5 components.
20
+ - `cds build` uses `hdblogicalschema` and `hdblogicalschemaconfig` plugins in its default `.hdiconfig`.
21
+ - `cds add cloud-logging` adds required dependencies to _pom.xml_ for Java projects.
22
+ - `cds add kyma` is now the only suggested Kyma facet in `cds add help`.
23
+ - `cds add helm-unified-runtime` option is now deprecated; use `cds add kyma --unified-runtime` instead.
24
+ - `cds up --to k8s` now saves the checksum of buildpack images to determine if a rebuild is necessary.
25
+ - `cds up --to k8s` will interactively ask for registry credentials if they don't exist on the remote cluster.
26
+ - `cds build` automatically adds `contentLocalizedEdmx=true` to the java build task configuration if OData V2 is set.
27
+ - `cds add github-actions` has out-of-the-box support for Java projects.
28
+ - `cds add html5-repo` and `cds add app-frontend` do not create a separate _ui5-deploy.yaml_ but use the default _ui5.yaml_.
29
+ - `cds add` plugins allow merging into non-existing files with `cds.add.merge(<object>).into(<yaml-path>)`.
30
+ - `cds build` tasks that produce `csn.json` now emit the `compile.for.runtime` event, in alignment with other runtime CSN producers like `cds serve`. Plugins can use such events to hook into CSN creation.
31
+ - `cds add xsuaa` sets `credential-types: [ "binding-secret", "x509" ]` for XSUAA instances.
32
+ - `cds add hana` does not create an explicit `.hdiconfig` file any more as `cds build` provides defaults.
33
+ - `cds import` generates the on condition for a composition when the corresponding OData navigation property targets a collection, is contained (`$ContainsTarget` in OData EDM) and has a backlink (`$Partner` in OData EDM).
34
+ - `cds import` now considers OData contained navigation properties as compositions instead of associations.
35
+
36
+ ### Fixed
37
+
38
+ - `cds add ias` will always add a binding to the MTX sidecar or approuter if required.
39
+ - `cds add ias` has improved support for multitenancy scenarios on Kyma.
40
+ - `cds lint` no longer fails with error code 1 and no message when loading the config fails.
41
+ - `cds version` no longer prints an `undefined` value for `@sap/cds-dk`.
42
+ - `cds compile --flavor files` no longer crashes with a `TypeError`.
43
+ - `cds add workzone` does not add superfluous `destinations` configuration to the `srv-api` import.
44
+
9
45
  ## Version 9.4.3 - 2025-10-29
10
46
 
47
+ ### Added
48
+
11
49
  ### Changed
12
50
 
51
+ - `cds add github-actions` will use the `cap-js/cf-setup` action from GitHub marketplace instead of creating a local copy.
13
52
  - `cds add containerize` omits the Unix-based `before_all` scripts in `containerize.yaml` in favor of a cross-platform compatible `cds up --to k8s` integration.
14
53
  - `cds up --to k8s` now uses built-in containerization, eliminating the hard dependency on the `ctz` library.
15
54
  - `cds up --to k8s` will interactively ask for a domain if it can't be determined from the current Kubernetes configuration.
16
55
  - `cds build --for hana` no longer requires `.hdbgrants` and `.hdbrevokes` to be specified as a plugin in the `.hdiconfig` file.
56
+ - `cds import` no longer emits usage info when invoked with the dry option.
17
57
 
18
58
  ### Fixed
19
59
 
60
+ - `cds add data` fixes an issue where randomly generated `Decimal` and `Integer` values could exceed their defined precision or scale.
20
61
  - `cds add pipeline` does not add UI5 template files in some cases.
21
62
  - `cds add ias` does not write to the `mta.yaml` for non-MTA projects.
22
63
  - `cds add ias` automatically adds the `authenticationType` to `xs-app.json` if required.
@@ -40,6 +81,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
40
81
  - `cds add test` creates correct OData URLs with unqualified entity names in service paths.
41
82
  - `cds add http` skips contained composition targets.
42
83
  - `cds add sample` no longer comes with an error when creating a Book through Fiori.
84
+ - `cds build` now places the `i18n` folder in the generated output root directory instead of nested locations.
43
85
 
44
86
  ## Version 9.4.1 - 2025-10-09
45
87
 
@@ -330,6 +372,41 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
330
372
  - `cds add ams` for Java adds a custom builder to the `mta.yaml` to circumvent the missing `srv/src/gen/policies`.
331
373
  - `cds add multitenancy` requires the `srv-api` instead of the `mtx-api` for Java projects.
332
374
 
375
+ ## Version 8.9.11 - 2025-11-21
376
+
377
+ ### Fixed
378
+
379
+ - Bump dependencies in shrinkwrap.
380
+
381
+ ## Version 8.9.10 - 2025-09-19
382
+
383
+ ### Fixed
384
+
385
+ - Updated shrinkwrap to include `@sap/cds-compiler` version 5.9.12.
386
+
387
+ ## Version 8.9.9 - 2025-09-16
388
+
389
+ ### Fixed
390
+
391
+ - CVE-2025-58754: vulnerability with `axios` versions <1.12.0.
392
+
393
+ ## Version 8.9.8 - 2025-07-24
394
+
395
+ ### Fixed
396
+
397
+ - CVE-2025-7783: vulnerability with `form-data` versions <4.0.4.
398
+
399
+ ## Version 8.9.7 - 2025-07-17
400
+
401
+ ### Fixed
402
+
403
+ - Bump dependencies in shrinkwrap.
404
+
405
+ ## Version 8.9.6 - 2025-06-05
406
+
407
+ ### Fixed
408
+
409
+ - Bump dependencies in shrinkwrap.
333
410
 
334
411
  ## Version 8.9.5 - 2025-06-05
335
412
 
@@ -297,7 +297,7 @@ async function compile (models, options={}) {
297
297
  throw `You must specify a model to compile.\nRun 'cds c -?' to learn more.`
298
298
  }
299
299
 
300
- let chain = model.then (m => model=m)
300
+ let chain = model.then?. (m => model=m) ?? Promise.resolve(model) // model could be a sync. value
301
301
 
302
302
  // add processors for compiling
303
303
  if (options.for) for (let each of options.for.split(',')) {
package/bin/export.js CHANGED
@@ -111,7 +111,7 @@ class Export extends Minify {
111
111
  async export (minified,o) {
112
112
  if (this.options.to === '-') return super.print (minified)
113
113
  if (this.options.to.endsWith('/*')) this.options.to = this.export_to()
114
- console.log(`\n${this.name}ing APIs to`, this.options.to, '...\n')
114
+ console.log(`\n${this.name}ing APIs to`, cds.utils.local(this.options.to), '...\n')
115
115
  const exports = []; exports.push(...this.export_models (minified))
116
116
  if (o.texts) exports.push (...this.export_texts (minified))
117
117
  if (o.data) exports.push (this.export_data (minified))
package/bin/help.js CHANGED
@@ -12,7 +12,6 @@ module.exports = exports = Object.assign (help, {formatHelp, handleCompletion, h
12
12
 
13
13
  *i* | *init* jump-start cds-based projects
14
14
  *a* | *add* add a feature to an existing project
15
- * * | *gen* generate models/data using a descriptive prompt [beta]
16
15
  *m* | *import* add models from external sources
17
16
  *c* | *compile* compile cds models to different outputs
18
17
  *p* | *parse* parses given cds models
package/bin/import.js CHANGED
@@ -1,7 +1,7 @@
1
1
  module.exports = Object.assign(_import, {
2
2
  _name: 'import',
3
- options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group', '--data-product' ],
4
- shortcuts: [ '-s', '-4', '-o', '-as', '-n', '-ns', '-d', undefined, '-c', undefined, undefined, '-', '-f'],
3
+ options: [ '--services', '--for', '--out', '--as', '--name', '--include-namespaces', '--dependencies', '--from', '--config', '--destination', '--group', '--into-namespace', '--data-product' ],
4
+ shortcuts: [ '-s', '-4', '-o', '-as', '-n', '-ns', '-d',undefined, '-c', undefined, undefined, '-2', '-', '-f'],
5
5
  flags: [ '--dry', '--force', '--no-copy', '--no-save', '--keep-namespace', '--data-product', '--edmx', '--rfc', '--odata', '--openapi', '--asyncapi' ],
6
6
  help: `
7
7
  # SYNOPSIS
@@ -56,10 +56,15 @@ module.exports = Object.assign(_import, {
56
56
  Not supported by import types "edmx", "openapi" or "asyncapi".
57
57
 
58
58
  *--data-product*
59
- Tells cds import to import a data product. The source must be a valid
59
+ Tells cds import to import a data product. The source must be a valid
60
60
  data product CSN file, e.g. as found on http://api.sap.com/dataproducts.
61
61
  (Not released with general availability yet)
62
62
 
63
+ *-2 | --into-namespace* <string>
64
+ Specifies a custom namespace to be used for the imported service.
65
+ This will override the namespace that was specified via the the
66
+ @title annotation of the original service.
67
+
63
68
  *Below options are valid for OData only*
64
69
 
65
70
  *--keep-namespace*
package/bin/repl.js CHANGED
@@ -35,7 +35,7 @@ module.exports = Object.assign (repl, {
35
35
  *cds eval* to evaluate and execute JavaScript.
36
36
  `})
37
37
 
38
- async function repl (_, options={}) {
38
+ async function repl ([project], options={}) {
39
39
 
40
40
  const GREEN = '\x1b[32m', RESET = '\x1b[0m'
41
41
  const cds = require('../lib/cds')
@@ -46,9 +46,9 @@ async function repl (_, options={}) {
46
46
  inspect.defaultOptions.depth = 11
47
47
 
48
48
  Object.defineProperty (cds,'repl',{value:true})
49
- await cds.service.bindings
49
+ await cds.service.bindings // REVISIT: this pre-populates many of the cds facade's lazy properties
50
50
  let context={}, repl = { context, displayPrompt(){} }
51
- if (options.run) await _run (options.run)
51
+ if ((options.run ??= project)) await _run (options.run)
52
52
  if (options.use) _export (...options.use.split(','))
53
53
 
54
54
  repl = require('repl').start ({
@@ -94,14 +94,10 @@ async function repl (_, options={}) {
94
94
  process.on = (event, listener) => event !== 'uncaughtException' && event !== 'unhandledRejection' && process.addListener(event, listener)
95
95
 
96
96
  async function _run (project) {
97
- try {
98
- if (require.resolve('@cap-js/cds-test', {paths: [cds.root, cds.home]})) {
99
- await cds.test (project)
100
- }
101
- } catch (e) {
102
- if (e.code !== 'MODULE_NOT_FOUND') throw e
103
- await cds.exec ('--project', project, '--port', '0', '--in-memory?')
104
- }
97
+ await require('../bin/serve') ([project], { project, port: 0,
98
+ 'in-memory?': true,
99
+ 'with-mocks': true
100
+ })
105
101
  _export ('entities', 'services')
106
102
  }
107
103
 
package/bin/up.js CHANGED
@@ -28,28 +28,77 @@ module.exports = Object.assign(up, {
28
28
  `})
29
29
 
30
30
  const { execSync } = require('node:child_process')
31
+ const { tmpdir } = require('node:os')
32
+ const crypto = require('node:crypto')
31
33
  const cds = require('../lib/cds')
32
- const { highlight } = require('../lib/util/term')
33
- const { exists, fs, path, path: { join } } = cds.utils
34
+ const { highlight, warn, dim } = require('../lib/util/term')
35
+ const { exists, fs, path, path: { join }, read, write } = cds.utils
36
+ const { ask4 } = require('../lib/util/question')
34
37
  const { readProject } = require('../lib/init/projectReader')
35
38
  const { mkdirp, copy } = cds.utils
36
39
  const { containerize } = require('../bin/util/containerize')
40
+ const { calcDirChecksum } = require('../lib/util/checksum')
37
41
 
38
- const run = cmd => {
39
- console.log(highlight(cmd))
42
+ const DEBUG = /\b(y|all|cli)\b/.test(process.env.DEBUG) ? console.debug : undefined
43
+
44
+ const run = (cmd, { continueOnFail = false, silent = false } = {}) => {
45
+ const redactedCmd = cmd.replace(/(--docker-password=)(['"]?)[^'"\s]+(['"]?)/, '$1[REDACTED]$3')
46
+ console.log(highlight(redactedCmd))
40
47
  try {
41
48
  return execSync(cmd, {
42
- stdio: 'inherit',
49
+ stdio: silent ? 'pipe' : 'inherit',
43
50
  env: { ...process.env, FORCE_COLOR: cds.utils.colors.enabled }
44
51
  })
45
52
  } catch (error) {
53
+ if (continueOnFail) {
54
+ if (!silent)
55
+ console.error(error.message)
56
+ return
57
+ }
46
58
  throw error.message
47
59
  }
48
60
  }
49
61
 
62
+ /**
63
+ * Get or create checksum file path in temp directory for UI5 apps
64
+ * @param {string} appName - The name of the app
65
+ * @returns {string} - Path to checksum file in temp directory
66
+ */
67
+ function getChecksumFilePath(appName) {
68
+ const projectHash = crypto.createHash('md5').update(cds.root).digest('hex').substring(0, 8)
69
+ return join(tmpdir(), `cds-ui5-checksum-${projectHash}-${appName}.txt`)
70
+ }
71
+
72
+ /**
73
+ * Read stored checksum for an app from temp directory
74
+ * @param {string} appName - The name of the app
75
+ * @returns {string|null} - The stored checksum or null if not found
76
+ */
77
+ function readStoredChecksum(appName) {
78
+ const checksumFile = getChecksumFilePath(appName)
79
+ if (exists(checksumFile)) {
80
+ try {
81
+ return fs.readFileSync(checksumFile, 'utf-8').trim()
82
+ } catch (e) {
83
+ // Ignore read errors and assume no checksum is stored
84
+ }
85
+ }
86
+ return null
87
+ }
88
+
89
+ /**
90
+ * Write checksum for an app to temp directory
91
+ * @param {string} appName - The name of the app
92
+ * @param {string} checksum - The checksum to store
93
+ */
94
+ function writeStoredChecksum(appName, checksum) {
95
+ const checksumFile = getChecksumFilePath(appName)
96
+ fs.writeFileSync(checksumFile, checksum, 'utf-8')
97
+ }
98
+
50
99
  async function up() {
51
100
  const project = readProject()
52
- const { apps, appName, hasApprouter, hasXsuaa, isMonorepoMicroservice } = project
101
+ const { apps, appName, hasApprouter, hasXsuaa, isMonorepoMicroservice, k8s } = project
53
102
 
54
103
  const html5DeployerPath = join(cds.root, cds.env.folders.app, 'html5-deployer', 'resources')
55
104
 
@@ -82,18 +131,37 @@ async function up() {
82
131
  }
83
132
 
84
133
  if (cds.cli.options.to === 'k8s') {
85
- if (!exists('chart')) run('cds add helm')
86
- if (apps.length > 0) {
87
- await mkdirp(html5DeployerPath)
88
- }
134
+ if (!exists('chart') || !exists('containerize.yaml')) run('cds add kyma')
89
135
 
90
- for (const { app } of apps) {
91
- const appPath = path.join(cds.root, cds.env.folders.app, app)
92
- run(`npm run build --prefix ${appPath}`)
93
- await copy(path.join(appPath, `dist/${app}.zip`)).to(html5DeployerPath, `${app}.zip`)
94
- }
136
+ if (project.hasHtml5Repo) {
137
+ if (apps.length > 0) {
138
+ await mkdirp(html5DeployerPath)
139
+ }
140
+
141
+ for (const { app } of apps) {
142
+ const appPath = path.join(cds.root, cds.env.folders.app, app)
143
+ const zipPath = path.join(appPath, `dist/${app}.zip`)
144
+
145
+ // Calculate checksum for the app source folder (excluding node_modules and dist)
146
+ const currentChecksum = await calcDirChecksum(
147
+ appPath,
148
+ (dir) => !dir.includes('node_modules') && !dir.includes('dist')
149
+ )
150
+
151
+ const storedChecksum = readStoredChecksum(app)
152
+ const needsBuild = currentChecksum !== storedChecksum || !exists(zipPath)
153
+
154
+ if (needsBuild) {
155
+ DEBUG?.(dim(`build required for '${app}' – checksums`, { old: storedChecksum, new: currentChecksum }))
156
+ run(`npm run build --prefix ${appPath}`)
157
+ writeStoredChecksum(app, currentChecksum)
158
+ } else {
159
+ console.log(highlight(`Skipping build for UI5 app '${app}' - no changes detected (checksum: ${currentChecksum.substring(0, 8)}...)`))
160
+ }
95
161
 
96
- if (!exists('containerize.yaml')) run('cds add containerize')
162
+ await copy(zipPath).to(html5DeployerPath, `${app}.zip`)
163
+ }
164
+ }
97
165
  } else {
98
166
  if (!exists('mta.yaml')) {
99
167
  run('cds add mta')
@@ -114,16 +182,78 @@ async function up() {
114
182
 
115
183
  if (cds.cli.options.to === 'k8s') {
116
184
  const ns = cds.cli.options.namespace
117
- const nsHelm = ns ? `--namespace ${ns} --create-namespace` : ''
185
+ const nsHelm = ns ? `--namespace ${ns}` : ''
118
186
  const nsKubectl = ns ? `-n ${ns}` : ''
119
187
 
120
188
  run('npm i --package-lock')
121
189
  run('cds build --production')
122
- await containerize('containerize.yaml', { push: true, clearCache: !!process.env.CLEAR_CACHE })
123
- console.log('Containerization completed')
190
+
191
+ if (ns) {
192
+ const k8sNamespaceResult = run(`kubectl get namespace ${ns} -o jsonpath="{.metadata.name}"`, { continueOnFail: true, silent: true })
193
+ if (!k8sNamespaceResult) {
194
+ run(`kubectl create namespace ${ns}`)
195
+ }
196
+ }
197
+
198
+ const isValidDomain = str => /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(str)
199
+ const { image, imagePullSecret } = k8s.values.global ?? {}
200
+ let registry = image?.registry
201
+ if (!process.env.CI) {
202
+ if (!registry || !isValidDomain(registry)) {
203
+ const valuesPath = 'chart/values.yaml'
204
+ const [yn] = await ask4([warn(`invalid registry server '${registry}' detected, override in ${valuesPath}? (Y/n)`)])
205
+ if (yn?.toLowerCase() !== 'y' || process.env.CI) {
206
+ throw `registry parameter is invalid, please specify it in ${valuesPath}`
207
+ } else {
208
+ registry = image.registry = await ask4(['registry server (e.g. index.docker.io): '])
209
+ const values = (await read(valuesPath)).replace(/(registry:\s*).*/, `$1${registry}`)
210
+ await write(values).to(valuesPath)
211
+
212
+ const containerize = (await read('containerize.yaml')).replace(/(repository:\s*).*/, `$1${registry}`)
213
+ await write(containerize).to('containerize.yaml')
214
+ }
215
+ }
216
+ if (imagePullSecret?.name) {
217
+ const imagePullSecretResult = run(`kubectl get secret ${imagePullSecret.name} ${nsKubectl} -o jsonpath="{.metadata.name}"`, { continueOnFail: true, silent: true })
218
+ if (!imagePullSecretResult) {
219
+ const yn = await ask4([`image pull secret '${imagePullSecret.name}' not found in namespace '${ns || 'default'}' – do you want to create it? (Y/n) `])
220
+ if (yn?.toLowerCase() !== 'y' || process.env.CI) {
221
+ throw `image pull secret '${imagePullSecret.name}' is required to pull images from the registry`
222
+ } else {
223
+ const [username, email] = await ask4([
224
+ 'registry username: ',
225
+ 'registry email: ',
226
+ ])
227
+ const password = await ask4(['registry password: '], { redacted: true })
228
+ run(`kubectl create secret docker-registry ${imagePullSecret.name} ${nsKubectl} --docker-server=${registry} --docker-username=${username} --docker-password='${password}' --docker-email=${email}`)
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ const modules = await containerize()
235
+ const helmArgs = []
236
+ modules?.forEach(m => {
237
+ if (m.path === 'gen/srv') {
238
+ helmArgs.push(`--set srv.annotations.deployment.deployment-checksum=${m.checksum}`)
239
+ }
240
+ if (m.path === 'app/router') {
241
+ helmArgs.push(`--set approuter.annotations.deployment.deployment-checksum=${m.checksum}`)
242
+ }
243
+ if (m.path === 'app/html5-deployer') {
244
+ helmArgs.push(`--set html5-apps-deployer.annotations.job.job-checksum=${m.checksum}`)
245
+ }
246
+ if (m.path === 'gen/db') {
247
+ helmArgs.push(`--set hana-deployer.annotations.job.job-checksum=${m.checksum}`)
248
+ }
249
+ if (m.path === 'gen/mtx/sidecar') {
250
+ helmArgs.push(`--set sidecar.annotations.deployment.deployment-checksum=${m.checksum}`)
251
+ }
252
+ })
124
253
 
125
254
  let helmUpgrade = `helm upgrade --install ${appName} ./gen/chart ${nsHelm} --wait --wait-for-jobs --timeout=10m`
126
- if (hasXsuaa) helmUpgrade += ' --set-file xsuaa.jsonParameters=xs-security.json'
255
+ if (hasXsuaa) helmArgs.push(' --set-file xsuaa.jsonParameters=xs-security.json')
256
+ if (helmArgs.length > 0) helmUpgrade += ` ${helmArgs.join(' ')}`
127
257
  run(helmUpgrade)
128
258
  run(`kubectl rollout status deployment ${appName}-srv ${nsKubectl} --timeout=8m`)
129
259
  if (hasApprouter) run(`kubectl rollout status deployment ${appName}-approuter ${nsKubectl} --timeout=8m`)
@@ -2,7 +2,8 @@ const cds = require('../../lib/cds')
2
2
  const { path } = cds.utils
3
3
  const { join } = path
4
4
  const { execSync } = require('node:child_process')
5
- const { highlight } = require('../../lib/util/term')
5
+ const { highlight, info } = require('../../lib/util/term')
6
+ const { calcDirChecksum } = require('../../lib/util/checksum')
6
7
 
7
8
  const buildpacks = new Set([
8
9
  'java', 'nodejs', 'sap-machine', 'executable-jar', 'spring-boot', 'syft'
@@ -17,15 +18,20 @@ const keyProcessorMap = {
17
18
  buildpack: processBuildpack
18
19
  }
19
20
 
20
- const run = cmd => {
21
+ const run = (cmd, { continueOnFail = false, silent = false } = {}) => {
21
22
  cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd
22
23
  console.log(highlight(cmd))
23
24
  try {
24
25
  return execSync(cmd, {
25
- stdio: 'inherit',
26
+ stdio: silent ? 'pipe' : 'inherit',
26
27
  env: { ...process.env, FORCE_COLOR: cds.utils.colors.enabled }
27
28
  })
28
29
  } catch (error) {
30
+ if (continueOnFail) {
31
+ if (!silent)
32
+ console.error(error.message)
33
+ return
34
+ }
29
35
  throw error.message
30
36
  }
31
37
  }
@@ -72,41 +78,57 @@ function processBuildpack(bp, name, tag, clearCache = false) {
72
78
  * @param {string} filename - The path to the YAML file to parse.
73
79
  * @param {string} [repoOpt=''] - Optional repository override. If not provided, uses the repository defined in the YAML file.
74
80
  * @param {boolean} [clearCache=false] - Whether to clear cache for buildpack builds.
75
- * @returns {{
81
+ * @returns {Promise<{
76
82
  * commands: Array<{
77
83
  * name: string,
78
84
  * tag: string,
79
85
  * buildCmd: Array<string>,
80
86
  * tagCmd: Array<string>,
81
- * pushCmd: Array<string>
87
+ * pushCmd: Array<string>,
88
+ * image: string,
89
+ * checksum: string|null
90
+ * path: string
82
91
  * }>,
83
92
  * before_all: Array<Array<string>>,
84
93
  * repository: string
85
- * }} An object containing the commands for each module, any global pre-commands, and the repository name.
94
+ * }>} An object containing the commands for each module, any global pre-commands, and the repository name.
86
95
  * @throws {Error} If modules are not defined in the YAML file or if build-parameters are invalid.
87
96
  */
88
- function parseYAML(filename, repoOpt = '', options) {
97
+ async function parseYAML(filename, repoOpt = '', options) {
89
98
  const file = cds.load.yaml(join(cds.root, filename))
90
99
  const { repository: repoFile, modules, tag: globalTag = 'latest' } = file
91
100
  const repository = repoOpt || repoFile
92
101
  const before_all = file['before-all'] ? file['before-all'].map(c => c.trim().split(' ')) : []
93
- if (!modules) throw new Error('Modules not defined')
94
- console.log(`Using repository: ${repository}`)
95
- const commands = modules.map(m => {
102
+ if (!modules) throw `no modules defined in ${filename}`
103
+ const commands = await Promise.all(modules.map(async (m) => {
96
104
  const name = m.name, tag = m.tag || globalTag, bp = m['build-parameters']
97
- const source = `${name}:${tag}`, repoImg = `${repository}/${source}`
105
+ const rawImg = `${repository}/${name}`, source = `${name}:${tag}`, repoImg = `${repository}/${source}`
98
106
  const key = Object.keys(bp)[0]
107
+ let checksum = null
108
+ if (bp.buildpack?.path)
109
+ checksum = await calcDirChecksum(
110
+ join(cds.root, bp.buildpack.path),
111
+ (dir) => !dir.includes('node_modules'),
112
+ 'sha256',
113
+ [
114
+ join(cds.root, filename),
115
+ join(cds.root, 'package.json')
116
+ ]
117
+ )
99
118
  const buildType = keyProcessorMap[key]
100
- if (!buildType) throw new Error(`build-parameters ${Object.keys(bp)} is invalid.`)
119
+
120
+ if (!buildType) throw `'build-parameters' ${Object.keys(bp)} in ${filename} is invalid`
101
121
  return {
102
- name, tag,
103
- buildCmd: key === 'buildpack'
104
- ? buildType(bp, name, tag, options.clearCache)
105
- : buildType(bp, name, tag),
106
- tagCmd: ['docker', 'tag', source, repoImg],
107
- pushCmd: ['docker', 'push', repoImg]
122
+ name,
123
+ tag,
124
+ buildCmd: buildType(bp, name, tag, key === 'buildpack' && options.clearCache),
125
+ tagCmd: [['docker', 'tag', source, repoImg]].concat(checksum ? [['docker', 'tag', source, `${rawImg}:${checksum}`]] : []),
126
+ pushCmd: ['docker', 'push', repoImg],
127
+ image: rawImg,
128
+ checksum,
129
+ path: bp.buildpack?.path
108
130
  }
109
- })
131
+ }))
110
132
  return { commands, before_all, repository }
111
133
  }
112
134
 
@@ -117,10 +139,19 @@ function parseYAML(filename, repoOpt = '', options) {
117
139
  */
118
140
  async function executeModules(modules, options) {
119
141
  for (const m of modules) {
120
- console.log(`Containerising module ${m.name}`)
142
+ if (m.checksum) {
143
+ try {
144
+ run(`docker inspect --format='{{index .RepoTags 0}}' ${m.image}:${m.checksum}`, { silent: true })
145
+ console.log(info(`image ${m.image}:${m.checksum} already exists – skipping build and push`))
146
+ continue
147
+ } catch (error) {
148
+ // ignore
149
+ }
150
+ }
151
+
121
152
  for (const cmd of m.buildCmd) run(cmd)
122
153
  if (options.push) {
123
- run(m.tagCmd)
154
+ for (const cmd of m.tagCmd) run(cmd)
124
155
  run(m.pushCmd)
125
156
  }
126
157
  }
@@ -130,13 +161,23 @@ async function executeModules(modules, options) {
130
161
  * Main function to containerize modules based on a YAML configuration file.
131
162
  * @param {string} filename - The path to the YAML configuration file.
132
163
  * @param {{ push: boolean, clearCache: boolean }} options - Options object, e.g., { push: true } to push images after building.
133
- * @returns {Promise<void>}
164
+ * @returns {Promise<Array<{
165
+ * name: string,
166
+ * tag: string,
167
+ * buildCmd: Array<string>,
168
+ * tagCmd: Array<string>,
169
+ * pushCmd: Array<string>,
170
+ * image: string,
171
+ * checksum: string|null
172
+ * path: string
173
+ * }>} - The list of modules that were containerized.
134
174
  */
135
- async function containerize(filename, options = { push: false, clearCache: false }) {
136
- const { repository, before_all, commands: modules } = parseYAML(filename, '', options)
175
+ async function containerize(filename = 'containerize.yaml', options = { clearCache: !!process.env.CLEAR_CACHE, push: true }) {
176
+ const { repository, before_all, commands: modules } = await parseYAML(filename, '', options)
137
177
  if (!repository || !modules) return
138
- for (const cmd of before_all) await run(cmd)
178
+ for (const cmd of before_all) run(cmd)
139
179
  await executeModules(modules, options)
180
+ return modules
140
181
  }
141
182
 
142
183
  module.exports = { containerize }
package/bin/version.js CHANGED
@@ -52,7 +52,7 @@ function list_versions (args, options) {
52
52
 
53
53
  process.stdout.isTTY && console.log()
54
54
  if (options.markdown || options.info) {
55
- _markdown (versions)
55
+ _markdown (versions, options)
56
56
  } else {
57
57
  const mark = !colors ? s => s : require('../lib/util/term').info
58
58
  for (let each of Object.keys(versions)) console.log(`${mark(each)}: ${versions[each]}`)
@@ -81,7 +81,7 @@ function info({ root: cwd = process.cwd() }) {
81
81
 
82
82
  // Sort entries, with @sap/cds packages on top, followed by @cap-js packages
83
83
  const keys = Object.keys(versions).sort(), sorted={}
84
- for (let k of ['@sap/cds','@sap/cds-dk']) if (!(k in sorted)) sorted[k] = versions[k]
84
+ for (let k of ['@sap/cds']) if (!(k in sorted)) sorted[k] = versions[k]
85
85
  for (let k of keys) if (!(k in sorted) && k.startsWith('@sap/cds')) sorted[k] = versions[k]
86
86
  for (let k of keys) if (!(k in sorted) && k.startsWith('@cap-js')) sorted[k] = versions[k]
87
87
  for (let k of keys) if (!(k in sorted)) sorted[k] = versions[k]
@@ -115,9 +115,9 @@ function _versions_from(pkg_path, pkg_label) {
115
115
  return versions
116
116
  }
117
117
 
118
- function _markdown (versions) {
118
+ function _markdown (versions, { root: cwd = process.cwd() }) {
119
119
  const pkg = { name:'', repository:'', version:'' }; try {
120
- Object.assign (pkg, require (resolve('package.json')))
120
+ Object.assign (pkg, require (require.resolve('./package.json', { paths:[cwd] })))
121
121
  } catch (e) {/* ignored */}
122
122
  const repo = pkg.repository.url || pkg.repository || 'https://github.com/<your/repo>'
123
123
  const valLength = Math.max(repo.length, versions.home.length)
package/lib/bind/k8s.js CHANGED
@@ -32,7 +32,7 @@ module.exports = new class K8SBind extends require('./plugin') {
32
32
 
33
33
  services4(app) {
34
34
  const { execSync } = require('node:child_process')
35
- const json = cmd => JSON.parse(execSync(cmd, { encoding: 'utf8' }))
35
+ const json = cmd => JSON.parse(execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }))
36
36
 
37
37
  const bindings = json('kubectl get servicebindings -o json').items
38
38
  .filter(b => b.metadata.name.startsWith(app))
@@ -23,7 +23,7 @@ copied from web-application: web-application.fullname
23
23
  */}}
24
24
  {{- define "cap.deploymentHost" -}}
25
25
  {{- if .deployment.expose.host }}
26
- {{- .deployment.expose.host }}
26
+ {{- tpl .deployment.expose.host . }}
27
27
  {{- else }}
28
28
  {{- $name := (include "cap.web-application.fullname" .) }}
29
29
  {{- if hasPrefix $name .Release.Namespace }}
@@ -27,7 +27,7 @@ module.exports = class HelmBuildPlugin extends Plugin {
27
27
 
28
28
  async build() {
29
29
  if (!HelmBuildPlugin.hasTask()) {
30
- this.pushMessage("Helm Chart not found. Execute `cds add helm` to add the helm chart.", Plugin.WARNING)
30
+ this.pushMessage("Helm Chart not found. Execute `cds add kyma` to add the helm chart.", Plugin.WARNING)
31
31
  return
32
32
  }
33
33