@abgov/nx-oc 12.7.0 → 12.9.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 (30) hide show
  1. package/generators.json +5 -0
  2. package/package.json +1 -1
  3. package/src/adsp/environments.js +7 -6
  4. package/src/adsp/environments.js.map +1 -1
  5. package/src/generators/deployment/deployment.spec.ts +12 -0
  6. package/src/generators/deployment/dotnet-files/__projectName__.yml__tmpl__ +14 -0
  7. package/src/generators/deployment/frontend-files/__projectName__.yml__tmpl__ +14 -0
  8. package/src/generators/deployment/node-files/__projectName__.yml__tmpl__ +14 -0
  9. package/src/generators/pipeline/pipeline.js +36 -3
  10. package/src/generators/pipeline/pipeline.js.map +1 -1
  11. package/src/generators/pipeline/schema.d.ts +2 -1
  12. package/src/generators/pipeline/schema.json +3 -5
  13. package/src/generators/setup-secrets/schema.d.ts +3 -0
  14. package/src/generators/setup-secrets/schema.json +16 -0
  15. package/src/generators/setup-secrets/setup-secrets.d.ts +3 -0
  16. package/src/generators/setup-secrets/setup-secrets.js +80 -0
  17. package/src/generators/setup-secrets/setup-secrets.js.map +1 -0
  18. package/src/generators/setup-secrets/setup-secrets.spec.ts +78 -0
  19. package/src/utils/gh-utils.d.ts +2 -0
  20. package/src/utils/gh-utils.js +26 -0
  21. package/src/utils/gh-utils.js.map +1 -0
  22. package/src/utils/gh-utils.spec.ts +39 -0
  23. package/src/utils/git-utils.d.ts +2 -0
  24. package/src/utils/git-utils.js +25 -0
  25. package/src/utils/git-utils.js.map +1 -1
  26. package/src/utils/git-utils.spec.ts +51 -1
  27. package/src/utils/oc-utils.d.ts +4 -0
  28. package/src/utils/oc-utils.js +51 -0
  29. package/src/utils/oc-utils.js.map +1 -1
  30. package/src/utils/oc-utils.spec.ts +86 -1
package/generators.json CHANGED
@@ -13,6 +13,11 @@
13
13
  "schema": "./src/generators/apply-infra/schema.json",
14
14
  "description": "Generator that applies OpenShift manifests for pipeline."
15
15
  },
16
+ "setup-secrets": {
17
+ "factory": "./src/generators/setup-secrets/setup-secrets",
18
+ "schema": "./src/generators/setup-secrets/schema.json",
19
+ "description": "Sets GitHub Actions secrets and OpenShift GHCR pull secret for the pipeline."
20
+ },
16
21
  "deployment": {
17
22
  "factory": "./src/generators/deployment/deployment",
18
23
  "schema": "./src/generators/deployment/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-oc",
3
- "version": "12.7.0",
3
+ "version": "12.9.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for OpenShift.",
@@ -1,18 +1,19 @@
1
1
  "use strict";
2
+ var _a, _b, _c, _d, _e, _f;
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.environments = void 0;
4
5
  exports.environments = {
5
6
  dev: {
6
- accessServiceUrl: 'https://access.adsp-dev.gov.ab.ca',
7
- directoryServiceUrl: 'https://directory-service.adsp-dev.gov.ab.ca',
7
+ accessServiceUrl: (_a = process.env.ADSP_E2E_ACCESS_URL) !== null && _a !== void 0 ? _a : 'https://access.adsp-dev.gov.ab.ca',
8
+ directoryServiceUrl: (_b = process.env.ADSP_E2E_DIRECTORY_URL) !== null && _b !== void 0 ? _b : 'https://directory-service.adsp-dev.gov.ab.ca',
8
9
  },
9
10
  test: {
10
- accessServiceUrl: 'https://access-uat.alberta.ca',
11
- directoryServiceUrl: 'https://directory-service.adsp-uat.alberta.ca',
11
+ accessServiceUrl: (_c = process.env.ADSP_E2E_ACCESS_URL) !== null && _c !== void 0 ? _c : 'https://access-uat.alberta.ca',
12
+ directoryServiceUrl: (_d = process.env.ADSP_E2E_DIRECTORY_URL) !== null && _d !== void 0 ? _d : 'https://directory-service.adsp-uat.alberta.ca',
12
13
  },
13
14
  prod: {
14
- accessServiceUrl: 'https://access.alberta.ca',
15
- directoryServiceUrl: 'https://directory-service.adsp.alberta.ca',
15
+ accessServiceUrl: (_e = process.env.ADSP_E2E_ACCESS_URL) !== null && _e !== void 0 ? _e : 'https://access.alberta.ca',
16
+ directoryServiceUrl: (_f = process.env.ADSP_E2E_DIRECTORY_URL) !== null && _f !== void 0 ? _f : 'https://directory-service.adsp.alberta.ca',
16
17
  },
17
18
  };
18
19
  //# sourceMappingURL=environments.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"environments.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/adsp/environments.ts"],"names":[],"mappings":";;;AAMa,QAAA,YAAY,GAAyC;IAChE,GAAG,EAAE;QACH,gBAAgB,EAAE,mCAAmC;QACrD,mBAAmB,EAAE,8CAA8C;KACpE;IACD,IAAI,EAAE;QACJ,gBAAgB,EAAE,+BAA+B;QACjD,mBAAmB,EAAE,+CAA+C;KACrE;IACD,IAAI,EAAE;QACJ,gBAAgB,EAAE,2BAA2B;QAC7C,mBAAmB,EAAE,2CAA2C;KACjE;CACF,CAAC"}
1
+ {"version":3,"file":"environments.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/adsp/environments.ts"],"names":[],"mappings":";;;;AAMa,QAAA,YAAY,GAAyC;IAChE,GAAG,EAAE;QACH,gBAAgB,EAAE,MAAA,OAAO,CAAC,GAAG,CAAC,mBAAmB,mCAAI,mCAAmC;QACxF,mBAAmB,EAAE,MAAA,OAAO,CAAC,GAAG,CAAC,sBAAsB,mCAAI,8CAA8C;KAC1G;IACD,IAAI,EAAE;QACJ,gBAAgB,EAAE,MAAA,OAAO,CAAC,GAAG,CAAC,mBAAmB,mCAAI,+BAA+B;QACpF,mBAAmB,EAAE,MAAA,OAAO,CAAC,GAAG,CAAC,sBAAsB,mCAAI,+CAA+C;KAC3G;IACD,IAAI,EAAE;QACJ,gBAAgB,EAAE,MAAA,OAAO,CAAC,GAAG,CAAC,mBAAmB,mCAAI,2BAA2B;QAChF,mBAAmB,EAAE,MAAA,OAAO,CAAC,GAAG,CAAC,sBAAsB,mCAAI,2CAA2C;KACvG;CACF,CAAC"}
@@ -90,6 +90,10 @@ describe('Deployment Generator', () => {
90
90
 
91
91
  const dockerfile = host.read('.openshift/test/Dockerfile').toString();
92
92
  expect(dockerfile).toContain('nginx');
93
+
94
+ const manifest = host.read('.openshift/test/test.yml').toString();
95
+ expect(manifest).toContain('readinessProbe');
96
+ expect(manifest).toContain('livenessProbe');
93
97
  });
94
98
 
95
99
  it('can generate deployment for angular', async () => {
@@ -150,6 +154,10 @@ describe('Deployment Generator', () => {
150
154
 
151
155
  const dockerfile = host.read('.openshift/test/Dockerfile').toString();
152
156
  expect(dockerfile).toContain('node');
157
+
158
+ const manifest = host.read('.openshift/test/test.yml').toString();
159
+ expect(manifest).toContain('readinessProbe');
160
+ expect(manifest).toContain('livenessProbe');
153
161
  });
154
162
 
155
163
  it('can generate deployment for dotnet', async () => {
@@ -178,6 +186,10 @@ describe('Deployment Generator', () => {
178
186
 
179
187
  const dockerfile = host.read('.openshift/test/Dockerfile').toString();
180
188
  expect(dockerfile).toContain('dotnet');
189
+
190
+ const manifest = host.read('.openshift/test/test.yml').toString();
191
+ expect(manifest).toContain('readinessProbe');
192
+ expect(manifest).toContain('livenessProbe');
181
193
  });
182
194
 
183
195
  it('includes DATABASE_URL secretKeyRef and init container for postgres node deployment', async () => {
@@ -112,6 +112,20 @@ objects:
112
112
  requests:
113
113
  cpu: 20m
114
114
  memory: 200Mi
115
+ readinessProbe:
116
+ httpGet:
117
+ path: /health
118
+ port: 5000
119
+ initialDelaySeconds: 10
120
+ periodSeconds: 10
121
+ failureThreshold: 3
122
+ livenessProbe:
123
+ httpGet:
124
+ path: /health
125
+ port: 5000
126
+ initialDelaySeconds: 30
127
+ periodSeconds: 30
128
+ failureThreshold: 3
115
129
  volumeMounts:
116
130
  - mountPath: /opt/app-root/app/appsettings.json
117
131
  name: config-volume
@@ -96,6 +96,20 @@ objects:
96
96
  requests:
97
97
  cpu: 20m
98
98
  memory: 50Mi
99
+ readinessProbe:
100
+ httpGet:
101
+ path: /
102
+ port: 8080
103
+ initialDelaySeconds: 10
104
+ periodSeconds: 10
105
+ failureThreshold: 3
106
+ livenessProbe:
107
+ httpGet:
108
+ path: /
109
+ port: 8080
110
+ initialDelaySeconds: 30
111
+ periodSeconds: 30
112
+ failureThreshold: 3
99
113
  volumeMounts:
100
114
  - mountPath: /opt/app-root/src/config
101
115
  name: config-volume
@@ -128,6 +128,20 @@ objects:
128
128
  requests:
129
129
  cpu: 20m
130
130
  memory: 50Mi
131
+ readinessProbe:
132
+ httpGet:
133
+ path: /health
134
+ port: 3333
135
+ initialDelaySeconds: 10
136
+ periodSeconds: 10
137
+ failureThreshold: 3
138
+ livenessProbe:
139
+ httpGet:
140
+ path: /health
141
+ port: 3333
142
+ initialDelaySeconds: 30
143
+ periodSeconds: 30
144
+ failureThreshold: 3
131
145
  - apiVersion: v1
132
146
  kind: Service
133
147
  metadata:
@@ -7,8 +7,9 @@ const path = require("path");
7
7
  const pipeline_envs_1 = require("../../pipeline-envs");
8
8
  const git_utils_1 = require("../../utils/git-utils");
9
9
  const apply_infra_1 = require("../apply-infra/apply-infra");
10
+ const setup_secrets_1 = require("../setup-secrets/setup-secrets");
10
11
  function normalizeOptions(host, options) {
11
- var _a;
12
+ var _a, _b;
12
13
  const ocEnvProjects = ((_a = options.envs) === null || _a === void 0 ? void 0 : _a.split(' ')) || [options.infra];
13
14
  const envsProjectsSet = new Set(ocEnvProjects);
14
15
  if (envsProjectsSet.size !== ocEnvProjects.length) {
@@ -17,7 +18,28 @@ function normalizeOptions(host, options) {
17
18
  else if (ocEnvProjects.length > pipeline_envs_1.pipelineEnvs.length) {
18
19
  throw new Error(`Provided projects must correspond to ${pipeline_envs_1.pipelineEnvs.join(', ')} environments.`);
19
20
  }
20
- return Object.assign(Object.assign({}, options), { ocPipelineName: options.pipeline, ocInfraProject: options.infra, ocEnvProjects: ocEnvProjects, applyPipeline: !!options.apply, pipelineType: options.type === 'jenkins' ? 'jenkins' : 'actions' });
21
+ return Object.assign(Object.assign({}, options), { registry: (_b = options.registry) !== null && _b !== void 0 ? _b : '', ocPipelineName: options.pipeline, ocInfraProject: options.infra, ocEnvProjects: ocEnvProjects, applyPipeline: !!options.apply, pipelineType: options.type === 'jenkins' ? 'jenkins' : 'actions' });
22
+ }
23
+ // Resolves the container registry: CLI flag → derived from git remote → prompted.
24
+ function resolveRegistry(registry) {
25
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
26
+ var _a;
27
+ if (registry)
28
+ return registry;
29
+ const remoteUrl = (_a = (0, git_utils_1.getGitRemoteUrl)()) === null || _a === void 0 ? void 0 : _a.trim();
30
+ const derived = (0, git_utils_1.deriveRegistryFromRemote)(remoteUrl);
31
+ if (derived) {
32
+ console.log(`\n✓ Container registry: ${derived} (derived from git remote)\n`);
33
+ return derived;
34
+ }
35
+ const { prompt } = yield Promise.resolve().then(() => require('enquirer'));
36
+ const answer = yield prompt({
37
+ type: 'input',
38
+ name: 'registry',
39
+ message: 'What container registry should images be published to (e.g., ghcr.io/my-org)?',
40
+ });
41
+ return answer.registry;
42
+ });
21
43
  }
22
44
  function addFiles(host, options) {
23
45
  var _a;
@@ -34,11 +56,22 @@ function addFiles(host, options) {
34
56
  }
35
57
  function default_1(host, options) {
36
58
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
37
- const normalizedOptions = normalizeOptions(host, options);
59
+ var _a;
60
+ const registry = yield resolveRegistry(options.registry);
61
+ const normalizedOptions = normalizeOptions(host, Object.assign(Object.assign({}, options), { registry }));
38
62
  addFiles(host, normalizedOptions);
39
63
  yield (0, devkit_1.formatFiles)(host);
40
64
  if (normalizedOptions.applyPipeline) {
41
65
  yield (0, apply_infra_1.default)(host);
66
+ const remoteUrl = (_a = (0, git_utils_1.getGitRemoteUrl)()) === null || _a === void 0 ? void 0 : _a.trim();
67
+ if (remoteUrl) {
68
+ yield (0, setup_secrets_1.default)(host, { infra: normalizedOptions.ocInfraProject });
69
+ }
70
+ else {
71
+ console.log('\n⚠ No git remote found — skipping GitHub secrets setup.\n' +
72
+ ` Push to GitHub first then run:\n` +
73
+ ` npx nx g @abgov/nx-oc:setup-secrets --infra ${normalizedOptions.ocInfraProject}\n`);
74
+ }
42
75
  }
43
76
  });
44
77
  }
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/pipeline/pipeline.ts"],"names":[],"mappings":";;AAgEA,4BASC;;AAzED,uCAA8D;AAC9D,6BAA6B;AAC7B,uDAA2D;AAC3D,qDAAwD;AACxD,4DAA6D;AAG7D,SAAS,gBAAgB,CAAC,IAAU,EAAE,OAAe;;IACnD,MAAM,aAAa,GAAG,CAAA,MAAA,OAAO,CAAC,IAAI,0CAAE,KAAK,CAAC,GAAG,CAAC,KAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAElE,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;IAC/C,IAAI,eAAe,CAAC,IAAI,KAAK,aAAa,CAAC,MAAM,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;SAAM,IAAI,aAAa,CAAC,MAAM,GAAG,4BAAI,CAAC,MAAM,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,wCAAwC,4BAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CACxE,CAAC;IACJ,CAAC;IAED,uCACK,OAAO,KACV,cAAc,EAAE,OAAO,CAAC,QAAQ,EAChC,cAAc,EAAE,OAAO,CAAC,KAAK,EAC7B,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAC9B,YAAY,EAAE,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,IAChE;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,IAAU,EAAE,OAAyB;;IACrD,MAAM,eAAe,mCAChB,OAAO,KACV,mBAAmB,EAAE,MAAA,IAAA,2BAAe,GAAE,0CAAE,IAAI,EAAE,EAC9C,IAAI,EAAJ,4BAAI,EACJ,IAAI,EAAE,EAAE,GACT,CAAC;IAEF,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,CAAC,IAAI,CACV,iGAAiG;YAC/F,6BAA6B,CAChC,CAAC;QACF,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC/B,cAAc,EACd,eAAe,CAChB,CAAC;IACJ,CAAC;SAAM,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAC9C,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,EACzC,cAAc,EACd,eAAe,CAChB,CAAC;QACF,IAAA,sBAAa,EACX,IAAI,EACJ,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,EACzC,qBAAqB,EACrB,eAAe,CAChB,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAE1D,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC,CAAC;QAClC,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;QAExB,IAAI,iBAAiB,CAAC,aAAa,EAAE,CAAC;YACpC,MAAM,IAAA,qBAAmB,EAAC,IAAI,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;CAAA"}
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/pipeline/pipeline.ts"],"names":[],"mappings":"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"}
@@ -3,11 +3,12 @@ export interface Schema {
3
3
  type: string;
4
4
  infra: string;
5
5
  envs: string;
6
- registry: string;
6
+ registry?: string;
7
7
  apply?: boolean;
8
8
  }
9
9
 
10
10
  export interface NormalizedSchema extends Schema {
11
+ registry: string;
11
12
  ocPipelineName: string;
12
13
  ocInfraProject: string;
13
14
  ocEnvProjects: string[];
@@ -37,9 +37,8 @@
37
37
  },
38
38
  "registry": {
39
39
  "type": "string",
40
- "description": "Container registry to publish images to (e.g., ghcr.io/my-org).",
41
- "alias": "r",
42
- "x-prompt": "What container registry should images be published to (e.g., ghcr.io/my-org)?"
40
+ "description": "Container registry to publish images to (e.g., ghcr.io/my-org). Derived from the git remote when not provided.",
41
+ "alias": "r"
43
42
  },
44
43
  "apply": {
45
44
  "type": "boolean",
@@ -52,8 +51,7 @@
52
51
  "pipeline",
53
52
  "infra",
54
53
  "type",
55
- "envs",
56
- "registry"
54
+ "envs"
57
55
  ],
58
56
  "additionalProperties": false
59
57
  }
@@ -0,0 +1,3 @@
1
+ export interface Schema {
2
+ infra: string;
3
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "id": "NxOcSetupSecrets",
4
+ "title": "Setup Pipeline Secrets",
5
+ "type": "object",
6
+ "properties": {
7
+ "infra": {
8
+ "type": "string",
9
+ "description": "Name of the OpenShift infra project containing the github-actions service account.",
10
+ "alias": "i",
11
+ "x-prompt": "What is the OpenShift infra project?"
12
+ }
13
+ },
14
+ "required": ["infra"],
15
+ "additionalProperties": false
16
+ }
@@ -0,0 +1,3 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { Schema } from './schema';
3
+ export default function (host: Tree, options: Schema): Promise<void>;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const tslib_1 = require("tslib");
5
+ const oc_utils_1 = require("../../utils/oc-utils");
6
+ const gh_utils_1 = require("../../utils/gh-utils");
7
+ const git_utils_1 = require("../../utils/git-utils");
8
+ function default_1(host, options) {
9
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
10
+ var _a, _b;
11
+ try {
12
+ (0, oc_utils_1.ensureOcLogin)();
13
+ }
14
+ catch (e) {
15
+ console.log(e instanceof Error ? e.message : String(e));
16
+ return;
17
+ }
18
+ const remoteUrl = (_a = (0, git_utils_1.getGitRemoteUrl)()) === null || _a === void 0 ? void 0 : _a.trim();
19
+ const repo = (0, git_utils_1.getGitHubRepo)(remoteUrl);
20
+ if (!repo) {
21
+ console.log('\n⚠ No GitHub remote found — skipping GitHub Actions secrets setup.\n' +
22
+ ' Push to GitHub first then re-run this generator.');
23
+ return;
24
+ }
25
+ try {
26
+ (0, gh_utils_1.checkGhCli)();
27
+ }
28
+ catch (e) {
29
+ console.log(e instanceof Error ? e.message : String(e));
30
+ return;
31
+ }
32
+ console.log('\nSetting up pipeline secrets...');
33
+ const serverUrl = (0, oc_utils_1.getOcServerUrl)();
34
+ if (serverUrl) {
35
+ const ok = (0, gh_utils_1.setGhSecret)('OPENSHIFT_SERVER', serverUrl, repo);
36
+ console.log(ok ? '✓ OPENSHIFT_SERVER set' : '✗ Failed to set OPENSHIFT_SERVER');
37
+ }
38
+ else {
39
+ console.log('⚠ Could not determine OpenShift server URL — set OPENSHIFT_SERVER manually.');
40
+ }
41
+ const saToken = (0, oc_utils_1.getSaToken)('github-actions', options.infra);
42
+ if (saToken) {
43
+ const ok = (0, gh_utils_1.setGhSecret)('OPENSHIFT_TOKEN', saToken, repo);
44
+ console.log(ok ? '✓ OPENSHIFT_TOKEN set' : '✗ Failed to set OPENSHIFT_TOKEN');
45
+ }
46
+ else {
47
+ console.log('⚠ Could not retrieve github-actions token — set OPENSHIFT_TOKEN manually.');
48
+ }
49
+ const org = (_b = (0, git_utils_1.deriveRegistryFromRemote)(remoteUrl)) === null || _b === void 0 ? void 0 : _b.replace('ghcr.io/', '');
50
+ if (!org) {
51
+ console.log('⚠ Could not derive GitHub org from remote — create ghcr-pull-secret manually.');
52
+ return;
53
+ }
54
+ const { prompt } = yield Promise.resolve().then(() => require('enquirer'));
55
+ let pat;
56
+ try {
57
+ const answer = yield prompt({
58
+ type: 'password',
59
+ name: 'pat',
60
+ message: 'Enter a GitHub PAT with read:packages scope (for OpenShift image import):',
61
+ });
62
+ pat = answer.pat;
63
+ }
64
+ catch (_c) {
65
+ return;
66
+ }
67
+ if (!pat)
68
+ return;
69
+ const secretCreated = (0, oc_utils_1.createDockerRegistrySecret)('ghcr-pull-secret', 'ghcr.io', org, pat, options.infra);
70
+ if (secretCreated) {
71
+ console.log(`✓ ghcr-pull-secret created in ${options.infra}`);
72
+ const linked = (0, oc_utils_1.linkSecretToServiceAccount)('ghcr-pull-secret', 'github-actions', options.infra);
73
+ console.log(linked ? '✓ ghcr-pull-secret linked to github-actions service account' : '✗ Failed to link secret');
74
+ }
75
+ else {
76
+ console.log(`✗ Failed to create ghcr-pull-secret in ${options.infra}`);
77
+ }
78
+ });
79
+ }
80
+ //# sourceMappingURL=setup-secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup-secrets.js","sourceRoot":"","sources":["../../../../../../packages/nx-oc/src/generators/setup-secrets/setup-secrets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
2
+ import * as ocUtils from '../../utils/oc-utils';
3
+ import * as ghUtils from '../../utils/gh-utils';
4
+ import * as gitUtils from '../../utils/git-utils';
5
+ import { Schema } from './schema';
6
+ import generator from './setup-secrets';
7
+
8
+ jest.mock('../../utils/oc-utils');
9
+ jest.mock('../../utils/gh-utils');
10
+ jest.mock('../../utils/git-utils');
11
+ jest.mock('enquirer', () => ({
12
+ prompt: jest.fn().mockResolvedValue({ pat: 'test-pat' }),
13
+ }));
14
+
15
+ const ocMock = ocUtils as jest.Mocked<typeof ocUtils>;
16
+ const ghMock = ghUtils as jest.Mocked<typeof ghUtils>;
17
+ const gitMock = gitUtils as jest.Mocked<typeof gitUtils>;
18
+
19
+ describe('Setup Secrets Generator', () => {
20
+ const options: Schema = { infra: 'my-infra' };
21
+
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ ocMock.ensureOcLogin.mockImplementation(() => undefined);
25
+ ocMock.getOcServerUrl.mockReturnValue('https://api.example.com:6443');
26
+ ocMock.getSaToken.mockReturnValue('test-sa-token');
27
+ ocMock.createDockerRegistrySecret.mockReturnValue(true);
28
+ ocMock.linkSecretToServiceAccount.mockReturnValue(true);
29
+ ghMock.checkGhCli.mockImplementation(() => undefined);
30
+ ghMock.setGhSecret.mockReturnValue(true);
31
+ gitMock.getGitRemoteUrl.mockReturnValue('https://github.com/GovAlta/nx-tools.git');
32
+ gitMock.getGitHubRepo.mockReturnValue('GovAlta/nx-tools');
33
+ gitMock.deriveRegistryFromRemote.mockReturnValue('ghcr.io/GovAlta');
34
+ });
35
+
36
+ it('sets GitHub secrets and creates GHCR pull secret', async () => {
37
+ const host = createTreeWithEmptyWorkspace();
38
+ await generator(host, options);
39
+
40
+ expect(ghMock.setGhSecret).toHaveBeenCalledWith('OPENSHIFT_SERVER', 'https://api.example.com:6443', 'GovAlta/nx-tools');
41
+ expect(ghMock.setGhSecret).toHaveBeenCalledWith('OPENSHIFT_TOKEN', 'test-sa-token', 'GovAlta/nx-tools');
42
+ expect(ocMock.createDockerRegistrySecret).toHaveBeenCalledWith(
43
+ 'ghcr-pull-secret', 'ghcr.io', 'GovAlta', 'test-pat', 'my-infra'
44
+ );
45
+ expect(ocMock.linkSecretToServiceAccount).toHaveBeenCalledWith(
46
+ 'ghcr-pull-secret', 'github-actions', 'my-infra'
47
+ );
48
+ });
49
+
50
+ it('skips secrets when no GitHub remote is found', async () => {
51
+ gitMock.getGitRemoteUrl.mockReturnValue(undefined);
52
+ gitMock.getGitHubRepo.mockReturnValue(undefined);
53
+
54
+ const host = createTreeWithEmptyWorkspace();
55
+ await generator(host, options);
56
+
57
+ expect(ghMock.setGhSecret).not.toHaveBeenCalled();
58
+ expect(ocMock.createDockerRegistrySecret).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it('skips secrets when gh CLI is not available', async () => {
62
+ ghMock.checkGhCli.mockImplementation(() => { throw new Error('gh: not found'); });
63
+
64
+ const host = createTreeWithEmptyWorkspace();
65
+ await generator(host, options);
66
+
67
+ expect(ghMock.setGhSecret).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it('skips GHCR secret when SA token cannot be retrieved', async () => {
71
+ ocMock.getSaToken.mockReturnValue(undefined);
72
+
73
+ const host = createTreeWithEmptyWorkspace();
74
+ await generator(host, options);
75
+
76
+ expect(ocMock.createDockerRegistrySecret).toHaveBeenCalled();
77
+ });
78
+ });
@@ -0,0 +1,2 @@
1
+ export declare function checkGhCli(): void;
2
+ export declare function setGhSecret(name: string, value: string, repo: string): boolean;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkGhCli = checkGhCli;
4
+ exports.setGhSecret = setGhSecret;
5
+ const child_process_1 = require("child_process");
6
+ function checkGhCli() {
7
+ try {
8
+ (0, child_process_1.execFileSync)('gh', ['auth', 'status'], { stdio: 'pipe' });
9
+ }
10
+ catch (_a) {
11
+ throw new Error('gh CLI is not installed or not authenticated. Run `gh auth login` then re-try.');
12
+ }
13
+ }
14
+ function setGhSecret(name, value, repo) {
15
+ try {
16
+ (0, child_process_1.execFileSync)('gh', ['secret', 'set', name, '--repo', repo], {
17
+ input: Buffer.from(value),
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ });
20
+ return true;
21
+ }
22
+ catch (_a) {
23
+ return false;
24
+ }
25
+ }
26
+ //# sourceMappingURL=gh-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gh-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/gh-utils.ts"],"names":[],"mappings":";;AAEA,gCAQC;AAED,kCAUC;AAtBD,iDAA6C;AAE7C,SAAgB,UAAU;IACxB,IAAI,CAAC;QACH,IAAA,4BAAY,EAAC,IAAI,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5D,CAAC;IAAC,WAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,gFAAgF,CACjF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAgB,WAAW,CAAC,IAAY,EAAE,KAAa,EAAE,IAAY;IACnE,IAAI,CAAC;QACH,IAAA,4BAAY,EAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE;YAC1D,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;YACzB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;SAChC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { mocked } from 'jest-mock';
3
+ import { checkGhCli, setGhSecret } from './gh-utils';
4
+
5
+ jest.mock('child_process');
6
+ const mockedExecFileSync = mocked(execFileSync);
7
+
8
+ describe('checkGhCli', () => {
9
+ beforeEach(() => mockedExecFileSync.mockReset());
10
+
11
+ it('does not throw when gh is authenticated', () => {
12
+ mockedExecFileSync.mockReturnValue(Buffer.from('Logged in to github.com'));
13
+ expect(() => checkGhCli()).not.toThrow();
14
+ });
15
+
16
+ it('throws a clear error when gh CLI is unavailable or unauthenticated', () => {
17
+ mockedExecFileSync.mockImplementation(() => { throw new Error('gh: command not found'); });
18
+ expect(() => checkGhCli()).toThrow('gh CLI is not installed or not authenticated');
19
+ });
20
+ });
21
+
22
+ describe('setGhSecret', () => {
23
+ beforeEach(() => mockedExecFileSync.mockReset());
24
+
25
+ it('returns true on success', () => {
26
+ mockedExecFileSync.mockReturnValue(Buffer.from(''));
27
+ expect(setGhSecret('OPENSHIFT_TOKEN', 'tok', 'GovAlta/nx-tools')).toBe(true);
28
+ expect(mockedExecFileSync).toHaveBeenCalledWith(
29
+ 'gh',
30
+ ['secret', 'set', 'OPENSHIFT_TOKEN', '--repo', 'GovAlta/nx-tools'],
31
+ expect.objectContaining({ input: Buffer.from('tok') })
32
+ );
33
+ });
34
+
35
+ it('returns false when gh command fails', () => {
36
+ mockedExecFileSync.mockImplementation(() => { throw new Error('gh: not found'); });
37
+ expect(setGhSecret('OPENSHIFT_TOKEN', 'tok', 'GovAlta/nx-tools')).toBe(false);
38
+ });
39
+ });
@@ -1 +1,3 @@
1
1
  export declare function getGitRemoteUrl(): string | undefined;
2
+ export declare function deriveRegistryFromRemote(remoteUrl?: string): string | undefined;
3
+ export declare function getGitHubRepo(remoteUrl?: string): string | undefined;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getGitRemoteUrl = getGitRemoteUrl;
4
+ exports.deriveRegistryFromRemote = deriveRegistryFromRemote;
5
+ exports.getGitHubRepo = getGitHubRepo;
4
6
  const child_process_1 = require("child_process");
5
7
  function getGitRemoteUrl() {
6
8
  try {
@@ -11,4 +13,27 @@ function getGitRemoteUrl() {
11
13
  console.log(`Failed to execute git`, e);
12
14
  }
13
15
  }
16
+ // Parses a GitHub remote URL and returns the ghcr.io registry for the org.
17
+ // Handles both HTTPS (https://github.com/ORG/REPO.git) and
18
+ // SSH (git@github.com:ORG/REPO.git) formats.
19
+ function deriveRegistryFromRemote(remoteUrl) {
20
+ var _a;
21
+ if (!remoteUrl)
22
+ return undefined;
23
+ const url = remoteUrl.trim();
24
+ const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\//);
25
+ const sshMatch = url.match(/git@github\.com:([^/]+)\//);
26
+ const org = (_a = httpsMatch === null || httpsMatch === void 0 ? void 0 : httpsMatch[1]) !== null && _a !== void 0 ? _a : sshMatch === null || sshMatch === void 0 ? void 0 : sshMatch[1];
27
+ return org ? `ghcr.io/${org}` : undefined;
28
+ }
29
+ // Returns the "owner/repo" slug for use with `gh secret set --repo`.
30
+ function getGitHubRepo(remoteUrl) {
31
+ var _a;
32
+ if (!remoteUrl)
33
+ return undefined;
34
+ const url = remoteUrl.trim();
35
+ const httpsMatch = url.match(/https:\/\/github\.com\/(.+?)(?:\.git)?\s*$/);
36
+ const sshMatch = url.match(/git@github\.com:(.+?)(?:\.git)?\s*$/);
37
+ return (_a = httpsMatch === null || httpsMatch === void 0 ? void 0 : httpsMatch[1]) !== null && _a !== void 0 ? _a : sshMatch === null || sshMatch === void 0 ? void 0 : sshMatch[1];
38
+ }
14
39
  //# sourceMappingURL=git-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"git-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/git-utils.ts"],"names":[],"mappings":";;AAEA,0CAWC;AAbD,iDAAwC;AAExC,SAAgB,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,wBAAQ,EACrB,oCAAoC,EACpC,EAAE,KAAK,EAAE,MAAM,EAAE,CAClB,CAAC,QAAQ,EAAE,CAAA;QAEZ,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAAA;IACzC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"git-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/git-utils.ts"],"names":[],"mappings":";;AAEA,0CAWC;AAKD,4DAOC;AAGD,sCAMC;AAlCD,iDAAwC;AAExC,SAAgB,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,wBAAQ,EACrB,oCAAoC,EACpC,EAAE,KAAK,EAAE,MAAM,EAAE,CAClB,CAAC,QAAQ,EAAE,CAAA;QAEZ,OAAO,MAAM,CAAA;IACf,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,GAAG,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAAA;IACzC,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,2DAA2D;AAC3D,6CAA6C;AAC7C,SAAgB,wBAAwB,CAAC,SAAkB;;IACzD,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAK,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,MAAA,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAG,CAAC,CAAC,mCAAI,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAG,CAAC,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED,qEAAqE;AACrE,SAAgB,aAAa,CAAC,SAAkB;;IAC9C,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAC3E,MAAM,QAAQ,GAAK,GAAG,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACpE,OAAO,MAAA,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAG,CAAC,CAAC,mCAAI,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAG,CAAC,CAAC,CAAC;AAC1C,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { mocked } from 'jest-mock';
3
- import { getGitRemoteUrl } from './git-utils';
3
+ import { getGitRemoteUrl, deriveRegistryFromRemote, getGitHubRepo } from './git-utils';
4
4
 
5
5
  jest.mock('child_process');
6
6
  const mockedExecSync = mocked(execSync);
@@ -32,3 +32,53 @@ describe('getGitRemoteUrl', () => {
32
32
  expect(url).toBeUndefined();
33
33
  });
34
34
  });
35
+
36
+ describe('deriveRegistryFromRemote', () => {
37
+ it('derives registry from HTTPS remote', () => {
38
+ expect(deriveRegistryFromRemote('https://github.com/GovAlta/nx-tools.git'))
39
+ .toBe('ghcr.io/GovAlta');
40
+ });
41
+
42
+ it('derives registry from SSH remote', () => {
43
+ expect(deriveRegistryFromRemote('git@github.com:GovAlta/nx-tools.git'))
44
+ .toBe('ghcr.io/GovAlta');
45
+ });
46
+
47
+ it('returns undefined for non-GitHub remote', () => {
48
+ expect(deriveRegistryFromRemote('https://gitlab.com/org/repo.git')).toBeUndefined();
49
+ });
50
+
51
+ it('returns undefined when remoteUrl is undefined', () => {
52
+ expect(deriveRegistryFromRemote(undefined)).toBeUndefined();
53
+ });
54
+
55
+ it('handles trailing newline from git output', () => {
56
+ expect(deriveRegistryFromRemote('https://github.com/GovAlta/nx-tools.git\n'))
57
+ .toBe('ghcr.io/GovAlta');
58
+ });
59
+ });
60
+
61
+ describe('getGitHubRepo', () => {
62
+ it('returns owner/repo from HTTPS remote', () => {
63
+ expect(getGitHubRepo('https://github.com/GovAlta/nx-tools.git'))
64
+ .toBe('GovAlta/nx-tools');
65
+ });
66
+
67
+ it('returns owner/repo from SSH remote', () => {
68
+ expect(getGitHubRepo('git@github.com:GovAlta/nx-tools.git'))
69
+ .toBe('GovAlta/nx-tools');
70
+ });
71
+
72
+ it('handles trailing newline from git output', () => {
73
+ expect(getGitHubRepo('https://github.com/GovAlta/nx-tools.git\n'))
74
+ .toBe('GovAlta/nx-tools');
75
+ });
76
+
77
+ it('returns undefined for non-GitHub remote', () => {
78
+ expect(getGitHubRepo('https://gitlab.com/org/repo.git')).toBeUndefined();
79
+ });
80
+
81
+ it('returns undefined when remoteUrl is undefined', () => {
82
+ expect(getGitHubRepo(undefined)).toBeUndefined();
83
+ });
84
+ });
@@ -1,3 +1,7 @@
1
+ export declare function getOcServerUrl(): string | undefined;
2
+ export declare function getSaToken(saName: string, namespace: string): string | undefined;
3
+ export declare function createDockerRegistrySecret(name: string, server: string, username: string, password: string, namespace: string): boolean;
4
+ export declare function linkSecretToServiceAccount(secretName: string, saName: string, namespace: string): boolean;
1
5
  export declare function runOcCommand(command: 'project' | 'start-build' | 'process' | 'apply', params: string[], input?: Buffer): {
2
6
  success: boolean;
3
7
  stdout?: Buffer;
@@ -1,8 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getOcServerUrl = getOcServerUrl;
4
+ exports.getSaToken = getSaToken;
5
+ exports.createDockerRegistrySecret = createDockerRegistrySecret;
6
+ exports.linkSecretToServiceAccount = linkSecretToServiceAccount;
3
7
  exports.runOcCommand = runOcCommand;
4
8
  exports.ensureOcLogin = ensureOcLogin;
5
9
  const child_process_1 = require("child_process");
10
+ function getOcServerUrl() {
11
+ try {
12
+ return (0, child_process_1.execFileSync)('oc', ['config', 'view', '--minify', '-o', 'jsonpath={.clusters[0].cluster.server}'], { stdio: 'pipe' }).toString().trim() || undefined;
13
+ }
14
+ catch (_a) {
15
+ return undefined;
16
+ }
17
+ }
18
+ // Creates a bounded SA token valid for one year (OCP 4.11+ TokenRequest API).
19
+ // Falls back to the legacy `oc sa get-token` on older clusters.
20
+ function getSaToken(saName, namespace) {
21
+ try {
22
+ return (0, child_process_1.execFileSync)('oc', ['create', 'token', saName, '-n', namespace, '--duration=8760h'], { stdio: 'pipe' }).toString().trim() || undefined;
23
+ }
24
+ catch (_a) {
25
+ try {
26
+ return (0, child_process_1.execFileSync)('oc', ['sa', 'get-token', saName, '-n', namespace], { stdio: 'pipe' }).toString().trim() || undefined;
27
+ }
28
+ catch (_b) {
29
+ return undefined;
30
+ }
31
+ }
32
+ }
33
+ function createDockerRegistrySecret(name, server, username, password, namespace) {
34
+ try {
35
+ (0, child_process_1.execFileSync)('oc', [
36
+ 'create', 'secret', 'docker-registry', name,
37
+ `--docker-server=${server}`,
38
+ `--docker-username=${username}`,
39
+ `--docker-password=${password}`,
40
+ '-n', namespace,
41
+ ], { stdio: 'pipe' });
42
+ return true;
43
+ }
44
+ catch (_a) {
45
+ return false;
46
+ }
47
+ }
48
+ function linkSecretToServiceAccount(secretName, saName, namespace) {
49
+ try {
50
+ (0, child_process_1.execFileSync)('oc', ['secrets', 'link', saName, secretName, '--for=pull', '-n', namespace], { stdio: 'pipe' });
51
+ return true;
52
+ }
53
+ catch (_a) {
54
+ return false;
55
+ }
56
+ }
6
57
  function runOcCommand(command, params, input) {
7
58
  const args = input
8
59
  ? [command, '-f', '-', ...params]
@@ -1 +1 @@
1
- {"version":3,"file":"oc-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/oc-utils.ts"],"names":[],"mappings":";;AAEA,oCAiBC;AAED,sCAmCC;AAxDD,iDAAwD;AAExD,SAAgB,YAAY,CAC1B,OAAwD,EACxD,MAAgB,EAChB,KAAc;IAEd,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;QACjC,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,CAAC;IAEzB,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvD,MAAM,MAAM,GAAG,IAAA,4BAAY,EAAC,IAAI,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAClE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,GAAG,CAAC,iCAAiC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAClE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;AACH,CAAC;AAED,SAAgB,aAAa;IAC3B,IAAI,CAAC;QACH,IAAA,4BAAY,EAAC,IAAI,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IAAC,WAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wIAAwI,CACzI,CAAC;IACJ,CAAC;IAED,IAAI,CAAC;QACH,IAAA,4BAAY,EAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAClD,OAAO;IACT,CAAC;IAAC,WAAM,CAAC;QACP,oDAAoD;IACtD,CAAC;IAED,IAAI,MAAM,GAAkB,IAAI,CAAC;IACjC,IAAI,CAAC;QACH,MAAM;YACJ,IAAA,4BAAY,EAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,wCAAwC,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;iBAClH,QAAQ,EAAE;iBACV,IAAI,EAAE,IAAI,IAAI,CAAC;IACtB,CAAC;IAAC,WAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,2DAA2D,CAAC,CAAC;IACzE,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,IAAI,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAEhE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,6FAA6F,CAC9F,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"oc-utils.js","sourceRoot":"","sources":["../../../../../packages/nx-oc/src/utils/oc-utils.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,13 @@
1
1
  import { execFileSync, spawnSync } from 'child_process';
2
2
  import { mocked } from 'jest-mock';
3
- import { ensureOcLogin, runOcCommand } from './oc-utils';
3
+ import {
4
+ ensureOcLogin,
5
+ runOcCommand,
6
+ getOcServerUrl,
7
+ getSaToken,
8
+ createDockerRegistrySecret,
9
+ linkSecretToServiceAccount,
10
+ } from './oc-utils';
4
11
 
5
12
  jest.mock('child_process');
6
13
  const mockedExecFileSync = mocked(execFileSync);
@@ -126,3 +133,81 @@ describe('ensureOcLogin', () => {
126
133
  expect(() => ensureOcLogin()).toThrow('OpenShift login failed or was cancelled');
127
134
  });
128
135
  });
136
+
137
+ describe('getOcServerUrl', () => {
138
+ beforeEach(() => mockedExecFileSync.mockReset());
139
+
140
+ it('returns the server URL from kubeconfig', () => {
141
+ mockedExecFileSync.mockReturnValue(Buffer.from('https://api.example.com:6443'));
142
+ expect(getOcServerUrl()).toBe('https://api.example.com:6443');
143
+ });
144
+
145
+ it('returns undefined when oc command fails', () => {
146
+ mockedExecFileSync.mockImplementation(() => { throw new Error('no config'); });
147
+ expect(getOcServerUrl()).toBeUndefined();
148
+ });
149
+ });
150
+
151
+ describe('getSaToken', () => {
152
+ beforeEach(() => mockedExecFileSync.mockReset());
153
+
154
+ it('returns token from oc create token', () => {
155
+ mockedExecFileSync.mockReturnValue(Buffer.from('eyJhbGciOi...'));
156
+ expect(getSaToken('github-actions', 'my-infra')).toBe('eyJhbGciOi...');
157
+ expect(mockedExecFileSync).toHaveBeenCalledWith(
158
+ 'oc', ['create', 'token', 'github-actions', '-n', 'my-infra', '--duration=8760h'],
159
+ expect.objectContaining({ stdio: 'pipe' })
160
+ );
161
+ });
162
+
163
+ it('falls back to oc sa get-token when create token fails', () => {
164
+ mockedExecFileSync.mockImplementationOnce(() => { throw new Error('unsupported'); }); // oc create token
165
+ mockedExecFileSync.mockReturnValueOnce(Buffer.from('legacy-token')); // oc sa get-token
166
+ expect(getSaToken('github-actions', 'my-infra')).toBe('legacy-token');
167
+ });
168
+
169
+ it('returns undefined when both commands fail', () => {
170
+ mockedExecFileSync.mockImplementation(() => { throw new Error('not found'); });
171
+ expect(getSaToken('github-actions', 'my-infra')).toBeUndefined();
172
+ });
173
+ });
174
+
175
+ describe('createDockerRegistrySecret', () => {
176
+ beforeEach(() => mockedExecFileSync.mockReset());
177
+
178
+ it('returns true on success', () => {
179
+ mockedExecFileSync.mockReturnValue(Buffer.from('secret/ghcr-pull-secret created'));
180
+ expect(createDockerRegistrySecret('ghcr-pull-secret', 'ghcr.io', 'org', 'pat', 'my-infra')).toBe(true);
181
+ expect(mockedExecFileSync).toHaveBeenCalledWith(
182
+ 'oc',
183
+ ['create', 'secret', 'docker-registry', 'ghcr-pull-secret',
184
+ '--docker-server=ghcr.io', '--docker-username=org', '--docker-password=pat',
185
+ '-n', 'my-infra'],
186
+ expect.objectContaining({ stdio: 'pipe' })
187
+ );
188
+ });
189
+
190
+ it('returns false when oc command fails', () => {
191
+ mockedExecFileSync.mockImplementation(() => { throw new Error('already exists'); });
192
+ expect(createDockerRegistrySecret('ghcr-pull-secret', 'ghcr.io', 'org', 'pat', 'my-infra')).toBe(false);
193
+ });
194
+ });
195
+
196
+ describe('linkSecretToServiceAccount', () => {
197
+ beforeEach(() => mockedExecFileSync.mockReset());
198
+
199
+ it('returns true on success', () => {
200
+ mockedExecFileSync.mockReturnValue(Buffer.from(''));
201
+ expect(linkSecretToServiceAccount('ghcr-pull-secret', 'github-actions', 'my-infra')).toBe(true);
202
+ expect(mockedExecFileSync).toHaveBeenCalledWith(
203
+ 'oc',
204
+ ['secrets', 'link', 'github-actions', 'ghcr-pull-secret', '--for=pull', '-n', 'my-infra'],
205
+ expect.objectContaining({ stdio: 'pipe' })
206
+ );
207
+ });
208
+
209
+ it('returns false when oc command fails', () => {
210
+ mockedExecFileSync.mockImplementation(() => { throw new Error('not found'); });
211
+ expect(linkSecretToServiceAccount('ghcr-pull-secret', 'github-actions', 'my-infra')).toBe(false);
212
+ });
213
+ });