@freshworks/shiftleft-tools 1.1.8

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 (106) hide show
  1. package/README.md +351 -0
  2. package/bin/shiftleft.js +95 -0
  3. package/package.json +57 -0
  4. package/src/commands/doctor.js +208 -0
  5. package/src/commands/init-postman.js +298 -0
  6. package/src/commands/init-rules.js +78 -0
  7. package/src/commands/link.js +172 -0
  8. package/src/commands/protect.js +61 -0
  9. package/src/commands/run-tests.js +182 -0
  10. package/src/commands/setup-pipeline.js +209 -0
  11. package/src/commands/update.js +203 -0
  12. package/src/index.js +4 -0
  13. package/src/utils/copy-tree.js +98 -0
  14. package/src/utils/gitignore.js +26 -0
  15. package/src/utils/logger.js +9 -0
  16. package/src/utils/manifest.js +145 -0
  17. package/src/utils/stack.js +80 -0
  18. package/src/utils/template.js +135 -0
  19. package/templates/AGENTS.md +109 -0
  20. package/templates/CLAUDE.md +3 -0
  21. package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
  22. package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
  23. package/templates/postman/.husky/pre-commit +19 -0
  24. package/templates/postman/.prettierrc.json +5 -0
  25. package/templates/postman/README.md.ejs +147 -0
  26. package/templates/postman/collections/01-core.json.ejs +91 -0
  27. package/templates/postman/config/local.json.ejs +12 -0
  28. package/templates/postman/config/staging.json.ejs +26 -0
  29. package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
  30. package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
  31. package/templates/postman/gitignore +16 -0
  32. package/templates/postman/npmrc +31 -0
  33. package/templates/postman/package.json.ejs +66 -0
  34. package/templates/postman/run-all-shim.sh +16 -0
  35. package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
  36. package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
  37. package/templates/postman/scripts/infra/start-mocks.sh +138 -0
  38. package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
  39. package/templates/postman/scripts/lib/api_coverage.py +1122 -0
  40. package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
  41. package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
  42. package/templates/postman/scripts/lib/report_combined.py +527 -0
  43. package/templates/postman/scripts/lib/report_consolidated.py +363 -0
  44. package/templates/postman/scripts/lib/report_generator.py +121 -0
  45. package/templates/postman/scripts/lib/report_migration.py +156 -0
  46. package/templates/postman/scripts/lib/report_mutation.py +110 -0
  47. package/templates/postman/scripts/lib/report_unit.py +353 -0
  48. package/templates/postman/scripts/lib/report_utils.py +973 -0
  49. package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
  50. package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
  51. package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
  52. package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
  53. package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
  54. package/templates/postman/scripts/run-all.sh +452 -0
  55. package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
  56. package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
  57. package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
  58. package/templates/postman-node/README.md.ejs +26 -0
  59. package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
  60. package/templates/postman-node/config/local.json.ejs +46 -0
  61. package/templates/postman-node/config/staging.json.ejs +31 -0
  62. package/templates/postman-node/local.test.env.ejs +3 -0
  63. package/templates/postman-node/mocks/external.js +14 -0
  64. package/templates/postman-node/package.json.ejs +39 -0
  65. package/templates/postman-node/requirements.txt +1 -0
  66. package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
  67. package/templates/postman-node/scripts/database/run-migrations.js +29 -0
  68. package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
  69. package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
  70. package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
  71. package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
  72. package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
  73. package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
  74. package/templates/postman-node/scripts/lib/start-app.sh +48 -0
  75. package/templates/postman-node/scripts/lib/utils.sh +114 -0
  76. package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
  77. package/templates/postman-node/scripts/run-all.sh +303 -0
  78. package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
  79. package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
  80. package/templates/postman-node/stryker.config.js.ejs +51 -0
  81. package/templates/rules/local-test-setup.mdc +420 -0
  82. package/templates/rules/testing-node.mdc +66 -0
  83. package/templates/rules/testing.mdc +248 -0
  84. package/templates/skills/_shared/postman-standards.md +380 -0
  85. package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
  86. package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
  87. package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
  88. package/templates/skills/review-test-suite/SKILL-java.md +137 -0
  89. package/templates/skills/review-test-suite/SKILL-node.md +78 -0
  90. package/templates/skills/review-test-suite/SKILL.md +9 -0
  91. package/templates/skills/run-test-suite/SKILL-java.md +186 -0
  92. package/templates/skills/run-test-suite/SKILL-node.md +191 -0
  93. package/templates/skills/run-test-suite/SKILL.md +9 -0
  94. package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
  95. package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
  96. package/templates/skills/setup-api-tests/SKILL.md +9 -0
  97. package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
  98. package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
  99. package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
  100. package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
  101. package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
  102. package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
  103. package/templates/skills/write-api-tests/SKILL-java.md +115 -0
  104. package/templates/skills/write-api-tests/SKILL-node.md +83 -0
  105. package/templates/skills/write-api-tests/SKILL.md +9 -0
  106. package/templates/stryker.config.js +50 -0
@@ -0,0 +1,135 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import ejs from 'ejs';
5
+ import { fileState, recordFile, manifestKey } from './manifest.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ export const TEMPLATES_DIR = join(__dirname, '../../templates');
11
+
12
+ /**
13
+ * Get the path to a template file
14
+ */
15
+ export function getTemplatePath(relativePath) {
16
+ return join(TEMPLATES_DIR, relativePath);
17
+ }
18
+
19
+ /**
20
+ * Read and render a template with the given context
21
+ */
22
+ export function renderTemplate(templatePath, context = {}) {
23
+ const fullPath = getTemplatePath(templatePath);
24
+ const template = readFileSync(fullPath, 'utf8');
25
+ return ejs.render(template, context);
26
+ }
27
+
28
+ /**
29
+ * Resolve a template's final content (rendered if EJS, raw otherwise).
30
+ */
31
+ export function templateContent(templatePath, context = {}, render = false) {
32
+ return render
33
+ ? renderTemplate(templatePath, context)
34
+ : readFileSync(getTemplatePath(templatePath), 'utf8');
35
+ }
36
+
37
+ function writeFile(destPath, content) {
38
+ const destDir = dirname(destPath);
39
+ if (!existsSync(destDir)) {
40
+ mkdirSync(destDir, { recursive: true });
41
+ }
42
+ writeFileSync(destPath, content, 'utf8');
43
+ }
44
+
45
+ /**
46
+ * Copy a template to destination, optionally rendering EJS.
47
+ * "Create" semantics: skips an existing file unless `force`.
48
+ * Returns the resolved content so callers can record it in a manifest.
49
+ */
50
+ export function copyTemplate(templatePath, destPath, context = {}, options = {}) {
51
+ const { render = false, force = false } = options;
52
+ const content = templateContent(templatePath, context, render);
53
+
54
+ if (existsSync(destPath) && !force) {
55
+ return { skipped: true, path: destPath, content };
56
+ }
57
+
58
+ writeFile(destPath, content);
59
+ return { skipped: false, path: destPath, content };
60
+ }
61
+
62
+ /**
63
+ * Manifest-aware write. Branches on the three-way file state instead of
64
+ * blindly overwriting, so a locally-edited file is never silently clobbered.
65
+ *
66
+ * Returns { status, path, content } where status is one of:
67
+ * 'written' – file (re)written to match the template
68
+ * 'created' – file did not exist and was created
69
+ * 'uptodate' – already matches the template, left as-is
70
+ * 'skipped' – scaffold file the repo owns, left as-is
71
+ * 'conflict' – local edits diverged; wrote `${dest}.shiftleft-new` instead
72
+ */
73
+ export function syncTemplate(templatePath, destPath, context = {}, options = {}) {
74
+ const { render = false, force = false, manifest, cwd, source, scaffold = false, adopt = false } = options;
75
+ const content = templateContent(templatePath, context, render);
76
+
77
+ // Adopt-on-first-run: if we have no record of this file but it exists on
78
+ // disk, seed the baseline from the current disk content. An out-of-date
79
+ // file then reads as 'clean' and updates cleanly instead of 'local-edit'.
80
+ if (adopt) {
81
+ const key = manifestKey(cwd, destPath);
82
+ if (!manifest.managed[key] && existsSync(destPath)) {
83
+ recordFile(manifest, cwd, destPath, readFileSync(destPath, 'utf8'), {
84
+ source: 'adopted',
85
+ scaffold,
86
+ template: templatePath,
87
+ });
88
+ }
89
+ }
90
+
91
+ const state = fileState(cwd, destPath, manifest, content);
92
+
93
+ // Scaffold files are repo-owned: only create when missing, never overwrite
94
+ // (unless an explicit --force is given).
95
+ if (scaffold && !force && (state === 'clean' || state === 'local-edit' || state === 'uptodate')) {
96
+ if (state === 'uptodate') recordFile(manifest, cwd, destPath, content, { source, scaffold, template: templatePath });
97
+ return { status: state === 'uptodate' ? 'uptodate' : 'skipped', path: destPath, content };
98
+ }
99
+
100
+ switch (state) {
101
+ case 'uptodate':
102
+ recordFile(manifest, cwd, destPath, content, { source, scaffold, template: templatePath });
103
+ return { status: 'uptodate', path: destPath, content };
104
+
105
+ case 'new':
106
+ case 'missing':
107
+ writeFile(destPath, content);
108
+ recordFile(manifest, cwd, destPath, content, { source, scaffold, template: templatePath });
109
+ return { status: state === 'new' ? 'created' : 'written', path: destPath, content };
110
+
111
+ case 'clean':
112
+ writeFile(destPath, content);
113
+ recordFile(manifest, cwd, destPath, content, { source, scaffold, template: templatePath });
114
+ return { status: 'written', path: destPath, content };
115
+
116
+ case 'local-edit':
117
+ default:
118
+ if (force) {
119
+ writeFile(destPath, content);
120
+ recordFile(manifest, cwd, destPath, content, { source, scaffold, template: templatePath });
121
+ return { status: 'written', path: destPath, content };
122
+ }
123
+ writeFile(`${destPath}.shiftleft-new`, content);
124
+ return { status: 'conflict', path: destPath, content };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Ensure a directory exists
130
+ */
131
+ export function ensureDir(dirPath) {
132
+ if (!existsSync(dirPath)) {
133
+ mkdirSync(dirPath, { recursive: true });
134
+ }
135
+ }
@@ -0,0 +1,109 @@
1
+ # Agent Instructions — Freshworks Test Pipeline Dev Tools
2
+
3
+ This file is the agent entry point for AI assistants (Claude Code, Cursor, Codex) working in a Freshworks platform service repo. It routes to the right skill based on what needs to be done.
4
+
5
+ ## How to use skills
6
+
7
+ When the user invokes `/skill-name`:
8
+ 1. Read `.claude/skills/<skill-name>/SKILL.md` (Claude Code) or `.cursor/skills/<skill-name>/SKILL.md` (Cursor)
9
+ 2. Follow the instructions in that file completely — skills are step-by-step agent workflows, not static templates
10
+ 3. Skills inspect the repo first, reason about what's needed, confirm with the user, then execute
11
+
12
+ **Never copy-paste from templates without first reading the actual repo structure.** Every project has unique package names, module layouts, port numbers, and test patterns.
13
+
14
+ ---
15
+
16
+ ## Which skill to use
17
+
18
+ | Goal | Skill | Notes |
19
+ |---|---|---|
20
+ | New repo — set up everything from scratch | `/setup-test-pipeline` | Covers unit tests, JaCoCo/nyc, mutation, Postman, API coverage, quality report, Jenkins |
21
+ | Existing repo — find and fill gaps | `/enhance-test-pipeline` | Audits what's present, adds only what's missing, checks security guardrails |
22
+ | Add mutation tests only | `/setup-mutation-tests` | PIT for Java, Stryker for Node — discovers actual package scope before configuring |
23
+ | Set up Postman/Newman infrastructure | `/setup-api-tests` | Collections, environments, scripts (Java: WireMock; Node: nock) |
24
+ | Write tests for specific endpoints | `/write-api-tests [METHOD /path]` | e.g. `/write-api-tests POST /api/v3/installations` |
25
+ | Run tests | `/run-test-suite` | Knows all `run-all.sh` flags, CI stage-by-stage patterns |
26
+ | Review test quality | `/review-test-suite` | 20-point maturity check |
27
+
28
+ ---
29
+
30
+ ## Auto-detect: no skill specified
31
+
32
+ If the user asks to "set up tests", "improve the pipeline", or similar without specifying a skill, inspect the repo first and decide:
33
+
34
+ ```bash
35
+ # Is this Java or Node?
36
+ ls pom.xml 2>/dev/null && echo "Java" || echo "Node"
37
+
38
+ # What pipeline pieces already exist?
39
+ ls postman/scripts/run-all.sh 2>/dev/null && echo "run-all.sh present"
40
+ grep -l "pitest-maven" pom.xml 2>/dev/null && echo "PIT present" # Java
41
+ grep '"mutation-tests"' package.json 2>/dev/null && echo "Stryker present" # Node
42
+ ls postman/scripts/report-generators/java-api-coverage-matrix.sh 2>/dev/null && echo "Java API coverage present"
43
+ ls postman/scripts/report-generators/node-api-coverage-matrix.sh 2>/dev/null && echo "Node API coverage present"
44
+ ls postman/reports/ 2>/dev/null && echo "Reports present"
45
+ grep -l "Quality Report\|generateQualityReport" Jenkinsfile 2>/dev/null && echo "Jenkins quality report present"
46
+ ```
47
+
48
+ - **Nothing exists** → use `/setup-test-pipeline`
49
+ - **Some things exist** → use `/enhance-test-pipeline`
50
+ - **Specific component missing** → use the targeted skill
51
+
52
+ ---
53
+
54
+ ## Non-negotiables (always enforce, regardless of skill)
55
+
56
+ These apply to every repo this tooling touches. If any of these are violated, fix them as part of the work — do not skip even if the user didn't ask.
57
+
58
+ ### Node security guardrails
59
+
60
+ **`.npmrc` must exist** in the `postman/` directory with:
61
+ ```ini
62
+ ignore-scripts=true # blocks postinstall supply chain attacks
63
+ audit-level=high
64
+ audit-signatures=true # verifies package signing (npm 8.4+)
65
+ strict-ssl=true
66
+ registry=https://registry.npmjs.org/
67
+ ```
68
+
69
+ **`package-lock.json` must exist** — never `npm install` without a lockfile. In CI always use `npm ci --ignore-scripts`, never `npm install`.
70
+
71
+ **Secrets must never reach npm packages:**
72
+ - AWS credentials: fetch secret → generate JWT → `unset` the secret variable immediately before running Newman
73
+ - `GH_TOKEN` / `GITHUB_RUNWAYCI_TOKEN`: only inside `withCredentials` blocks, never in `environment {}` block
74
+ - Never pass secrets as environment variables to `npm run` commands
75
+
76
+ **npm audit in Jenkins:** Every ShiftLeft / staging test run must include:
77
+ ```bash
78
+ npm ci --ignore-scripts
79
+ npm audit --audit-level=critical 2>&1 || echo "WARNING: audit found issues"
80
+ npm audit signatures 2>&1 || echo "WARNING: signature verification failed"
81
+ npm audit --json > ../audit-report.json 2>&1 || true
82
+ archiveArtifacts artifacts: 'audit-report.json', allowEmptyArchive: true
83
+ ```
84
+
85
+ ### Java security guardrails
86
+
87
+ - Never commit secrets or credentials in `pom.xml` or `application.properties`
88
+ - `application-test.properties` / `application-integration.properties`: H2 only, never real DB credentials
89
+ - PIT mutation tests must be in a `<profile>` — never in the main build (prevents accidental slow runs)
90
+
91
+ ### Jenkins guardrails (both Java and Node)
92
+
93
+ - IRSA (pod IAM role) for AWS — never static `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in Jenkins env
94
+ - `withCredentials` scope: credentials exposed only for the specific `sh` block that needs them, not the whole stage
95
+ - `deleteDir()` in `post { always }` — clean workspace after every build
96
+
97
+ ---
98
+
99
+ ## Reference implementations
100
+
101
+ These repos are the canonical examples of a fully set-up pipeline:
102
+
103
+ | Repo | Type | What it demonstrates |
104
+ |---|---|---|
105
+ | `mp-installation` | Java/Spring Boot | PIT mutation, JaCoCo, multi-product Postman, API coverage matrix, quality report, Jenkins ShiftLeft |
106
+ | `freshapps_api_node` | Node/Express | Stryker since/full modes, nyc coverage, Newman staging, node-api-coverage-matrix, quality report |
107
+ | `dp-apps` | Java/Spring Boot | Same as mp-installation — the original reference |
108
+
109
+ When a skill needs a concrete example of a script, config, or Jenkinsfile stage, refer to these repos.
@@ -0,0 +1,3 @@
1
+ # Project AI Instructions
2
+
3
+ See [AGENTS.md](AGENTS.md) for full instructions — skill routing, auto-detection, security guardrails, and reference implementations.
@@ -0,0 +1,432 @@
1
+ pipeline {
2
+ agent {
3
+ kubernetes {
4
+ yaml k8sPodTemplate(pod: 'jnlp+java11+dind')
5
+ defaultContainer 'java-base'
6
+ }
7
+ }
8
+
9
+ // Artifacts will be retained 10 days or 10 builds
10
+ options {
11
+ buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '20'))
12
+ disableConcurrentBuilds()
13
+ }
14
+
15
+ parameters {
16
+ choice(choices: 'dev\nstaging\nproduction', description: '', name: 'deployTo')
17
+ string(name: 'image_version', defaultValue: '', description: 'Image version for Staging/Production deployment [Recommended]')
18
+ booleanParam(defaultValue: false, description: 'Selecting this option will only deploy the image and will not run any of the other stages', name: 'deployOnly')
19
+ }
20
+
21
+ stages {
22
+ stage('Unit tests') {
23
+ when {
24
+ expression {
25
+ params.deployTo == 'dev' && !params.deployOnly
26
+ }
27
+ }
28
+ steps {
29
+ script {
30
+ sh """
31
+ mvn clean install
32
+ """
33
+ }
34
+ }
35
+ }
36
+
37
+ stage('Mutation Tests') {
38
+ when {
39
+ expression {
40
+ params.deployTo == 'dev' && !params.deployOnly
41
+ }
42
+ }
43
+ steps {
44
+ script {
45
+ runMutationTests()
46
+ }
47
+ }
48
+ post {
49
+ always {
50
+ // Publish the PIT mutation report every build, regardless of the
51
+ // shiftleft-gated report stage. Requires <timestampedReports>false</timestampedReports>
52
+ // in the pitest config so the HTML lands at target/pit-reports.
53
+ publishHTML([
54
+ allowMissing: true,
55
+ alwaysLinkToLastBuild: true,
56
+ keepAll: true,
57
+ reportDir: 'target/pit-reports',
58
+ reportFiles: 'index.html',
59
+ reportName: 'Mutation Report (PIT)'
60
+ ])
61
+ archiveArtifacts artifacts: 'target/pit-reports/**', allowEmptyArchive: true
62
+ }
63
+ }
64
+ }
65
+
66
+ stage('SonarQube Analysis') {
67
+ when {
68
+ expression {
69
+ params.deployTo == 'dev' && !params.deployOnly
70
+ }
71
+ }
72
+ steps {
73
+ runJavaSonarQubeAnalysis()
74
+ }
75
+ }
76
+
77
+ stage('Shift Left Integration Test - Isolation') {
78
+ when {
79
+ expression {
80
+ params.deployTo == 'dev'
81
+ }
82
+ }
83
+ steps {
84
+ script {
85
+ runShiftLeftIntegration()
86
+ }
87
+ }
88
+ }
89
+
90
+ stage('Build Image') {
91
+ when {
92
+ branch 'main'
93
+ expression {
94
+ params.deployTo == 'dev' && params.image_version == ''
95
+ }
96
+ }
97
+ steps {
98
+ buildJavaProject()
99
+ }
100
+ }
101
+
102
+ stage('Promote Image') {
103
+ when {
104
+ expression {
105
+ params.deployTo == 'production' && !params.deployOnly
106
+ }
107
+ }
108
+ steps {
109
+ promoteJavaImage(params.image_version)
110
+ }
111
+ }
112
+
113
+ stage('Security scan') {
114
+ when {
115
+ branch 'main'
116
+ expression {
117
+ params.deployTo == 'dev' && !params.deployOnly
118
+ }
119
+ }
120
+ steps {
121
+ runSecurityTests('SERVICE_NAME')
122
+ }
123
+ }
124
+
125
+ stage('Deploy Image') {
126
+ when {
127
+ branch 'main'
128
+ }
129
+ steps {
130
+ deployArgo(params.deployTo, 'SERVICE_NAME', params.image_version)
131
+ }
132
+ }
133
+ }
134
+ post {
135
+ always {
136
+ script {
137
+ deleteDir()
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ def runShiftLeftIntegration() {
144
+ ensureShiftleftCli()
145
+ utilObj = new Utils()
146
+ def buildCause = ''
147
+ def latest_argo_sha = ''
148
+ def latest_stable_version = ''
149
+ def lsr_stage_result = ''
150
+
151
+ // Extract comment body safely without storing non-serializable object
152
+ try {
153
+ def causes = currentBuild.rawBuild.getCauses()
154
+ if (causes && causes.size() > 0) {
155
+ def cause = causes[0]
156
+ if (cause?.class?.toString()?.contains('GitHubPullRequestCommentCause')) {
157
+ buildCause = cause.getCommentBody() ?: ''
158
+ }
159
+ cause = null // Clear the non-serializable reference immediately
160
+ }
161
+ } catch (Exception e) {
162
+ echo "Could not get build cause: ${e.message}"
163
+ }
164
+
165
+ echo "Build Cause = ${buildCause}"
166
+ if (!buildCause.trim().toLowerCase().startsWith('shiftleft')) {
167
+ echo 'Branch build with no intention of shift left integration test.'
168
+ return
169
+ }
170
+
171
+ lock(resource: 'SERVICE_NAME-shiftleft-postman') {
172
+ def sl = shiftleftBuildLabels()
173
+ def branch_name_setup = "${sl.gitBranchBase}/SERVICE_NAME-apFeatureStack"
174
+ def branch_name_teardown = "${sl.gitBranchBase.replace('iso-setup-', 'iso-teardown-')}/SERVICE_NAME-apFeatureStack"
175
+
176
+ buildJavaProject(sl.imageTag)
177
+ def serviceName = utilObj.getK8ServiceName(utilObj.getRepoName())
178
+
179
+ echo "serviceName = ${serviceName}"
180
+
181
+ // SECURITY: GH_TOKEN only exposed to git/gh commands, not to npm packages
182
+ withCredentials([string(credentialsId: 'GITHUB_RUNWAYCI_TOKEN', variable: 'GH_TOKEN')]) {
183
+ sh """#!/bin/bash --login
184
+ git clone git@github.com:freshdesk/freshapps-k8s.git
185
+ cd freshapps-k8s
186
+ ls -la
187
+ isoforge version
188
+ git checkout -b ${branch_name_setup}
189
+
190
+ isoforge setup \
191
+ --service SERVICE_NAME \
192
+ --namespace shiftleft_feature_stack \
193
+ --imageTag ${sl.imageTag} \
194
+ --header ISO_HEADER \
195
+ --headerValue 'ISO_HEADER_VALUE' \
196
+ --accountHeader 'SERVICE_NAME-iso-account-id' \
197
+ --accountIdValue 'ACCOUNT_ID_VALUE'
198
+
199
+ if [[ -n "\$(git status --porcelain)" ]]; then
200
+ git add .
201
+ git commit -m "Added changes for SERVICE_NAME in apFeatureStack"
202
+ git push origin ${branch_name_setup}
203
+ else
204
+ echo "No changes to commit, check previous steps"
205
+ exit 1
206
+ fi
207
+
208
+ gh pr create \
209
+ --title "Isolation-${BUILD_NUMBER} Changes for SERVICE_NAME in apFeatureStack" \
210
+ --body "This PR includes changes for SERVICE_NAME in apFeatureStack" \
211
+ --base main \
212
+ --head ${branch_name_setup}
213
+
214
+ echo "PR created successfully"
215
+
216
+ gh pr merge ${branch_name_setup} \
217
+ --squash \
218
+ --admin
219
+
220
+ git checkout main
221
+ git pull origin main
222
+ """
223
+
224
+ // get the latest sha for SERVICE_NAME in apFeatureStack
225
+ latest_argo_sha = sh(returnStdout: true, script:'''
226
+ cd freshapps-k8s && git rev-parse HEAD
227
+ ''').trim()
228
+ }
229
+ echo "Latest SHA for SERVICE_NAME in apFeatureStack = ${latest_argo_sha}"
230
+
231
+ // wait for deployment to complete
232
+ withCredentials([string(credentialsId: 'ARGO_API_TOKEN', variable: 'ARGO_API_TOKEN')]) {
233
+ sh """
234
+ argoapi application --revision ${latest_argo_sha} --service SERVICE_NAME --namespace NAMESPACE
235
+ """
236
+ }
237
+
238
+ // run the integration tests
239
+ // NOTE: AWS credentials are automatically available via the Kubernetes pod's IAM role (IRSA)
240
+ updateShiftLeftStatus('pending')
241
+ try {
242
+ sh """#!/bin/bash --login
243
+ echo 'Running npm security audit...'
244
+ cd postman
245
+
246
+ # Install dependencies securely
247
+ if [ -f "package-lock.json" ]; then
248
+ npm ci --ignore-scripts
249
+ else
250
+ npm install --ignore-scripts
251
+ fi
252
+
253
+ # Run npm audit
254
+ echo "=== npm audit ==="
255
+ npm audit --audit-level=critical 2>&1 || {
256
+ AUDIT_EXIT_CODE=\$?
257
+ echo ""
258
+ echo "Warning: Security vulnerabilities detected (exit code: \$AUDIT_EXIT_CODE)"
259
+ echo "Continuing with tests..."
260
+ }
261
+
262
+ # Verify package signatures
263
+ npm audit signatures 2>&1 || echo "Warning: Signature verification not available"
264
+
265
+ # Save audit report
266
+ npm audit --json > ../audit-report.json 2>&1 || true
267
+
268
+ echo 'Running integration tests'
269
+ cd scripts
270
+ AWS_PROFILE=staging ./run-tests-staging.sh
271
+ """
272
+ echo "Integration tests completed"
273
+ updateShiftLeftStatus('success', '/QualityReport/')
274
+ } catch (Exception e) {
275
+ updateShiftLeftStatus('failure', '/QualityReport/')
276
+ echo "Integration tests failed: ${e.getMessage()}"
277
+ currentBuild.result = 'UNSTABLE'
278
+ }
279
+
280
+ // Archive audit report
281
+ if (fileExists('audit-report.json')) {
282
+ archiveArtifacts artifacts: 'audit-report.json', allowEmptyArchive: true
283
+ }
284
+
285
+ // Publish Postman staging report
286
+ if (fileExists('postman/reports')) {
287
+ publishHTML([
288
+ allowMissing: false,
289
+ alwaysLinkToLastBuild: true,
290
+ keepAll: true,
291
+ reportDir: 'postman/reports',
292
+ reportFiles: 'consolidated-staging-*.html',
293
+ includes: '**/*',
294
+ reportName: 'PostmanTestReport-ShiftLeft',
295
+ reportTitles: 'ShiftLeft Integration Tests'
296
+ ])
297
+ } else {
298
+ echo 'postman/reports folder does not exist, skipping HTML report publication'
299
+ }
300
+
301
+ // Generate API Coverage Matrix
302
+ echo "Generating API Coverage Matrix..."
303
+ try {
304
+ sh """#!/bin/bash --login
305
+ cd postman/scripts
306
+ ./run-all.sh --skip-unit --skip-mutation --skip-postman --skip-report --no-delay
307
+ """
308
+ } catch (Exception e) {
309
+ echo "API Coverage generation failed: ${e.getMessage()}"
310
+ currentBuild.result = 'UNSTABLE'
311
+ }
312
+
313
+ // Generate Combined Quality Report
314
+ echo "Generating Combined Quality Report..."
315
+ try {
316
+ sh """#!/bin/bash --login
317
+ cd postman/scripts
318
+ ./stage-report-artifacts.sh "\${WORKSPACE}" "\${WORKSPACE}/postman/reports" || true
319
+ ./run-all.sh --skip-unit --skip-mutation --skip-postman --skip-coverage --no-delay
320
+ """
321
+ } catch (Exception e) {
322
+ echo "Quality report generation failed: ${e.getMessage()}"
323
+ currentBuild.result = 'UNSTABLE'
324
+ }
325
+
326
+ // Publish Combined Quality Report
327
+ if (fileExists('postman/reports')) {
328
+ def qualityReport = sh(
329
+ returnStdout: true,
330
+ script: "ls -t postman/reports/quality-report-*.html 2>/dev/null | head -1 | xargs -r basename"
331
+ ).trim()
332
+ if (qualityReport) {
333
+ publishHTML([
334
+ allowMissing: true,
335
+ alwaysLinkToLastBuild: true,
336
+ keepAll: true,
337
+ reportDir: 'postman/reports',
338
+ reportFiles: qualityReport,
339
+ includes: '**/*',
340
+ reportName: 'QualityReport',
341
+ reportTitles: 'Combined Quality Report'
342
+ ])
343
+ }
344
+ archiveArtifacts artifacts: 'postman/reports/**', allowEmptyArchive: true
345
+ }
346
+
347
+ // teardown the isolation stack
348
+ withCredentials([string(credentialsId: 'GITHUB_RUNWAYCI_TOKEN', variable: 'GH_TOKEN')]) {
349
+ sh """#!/bin/bash --login
350
+ cd freshapps-k8s
351
+ git pull origin main
352
+ git checkout -b ${branch_name_teardown}
353
+
354
+ isoforge teardown \
355
+ --service SERVICE_NAME \
356
+ --namespace shiftleft_feature_stack
357
+
358
+ if [[ -n "\$(git status --porcelain)" ]]; then
359
+ git add .
360
+ git commit -m "Added changes for SERVICE_NAME in apFeatureStack"
361
+ git push origin ${branch_name_teardown}
362
+ else
363
+ echo "No changes to commit, check previous steps"
364
+ exit 1
365
+ fi
366
+
367
+ gh pr create \
368
+ --title "Isolation-Teardown-${BUILD_NUMBER} Changes for SERVICE_NAME in apFeatureStack" \
369
+ --body "This PR includes teardown changes for SERVICE_NAME in apFeatureStack" \
370
+ --base main \
371
+ --head ${branch_name_teardown}
372
+
373
+ echo "PR created successfully"
374
+
375
+ gh pr merge ${branch_name_teardown} \
376
+ --squash \
377
+ --admin
378
+ """
379
+ }
380
+
381
+ // argo sync to complete the teardown
382
+ withCredentials([string(credentialsId: 'ARGO_API_TOKEN', variable: 'ARGO_API_TOKEN')]) {
383
+ sh """
384
+ argoapi sync
385
+ """
386
+ }
387
+ }
388
+ }
389
+
390
+ def updateShiftLeftStatus(String state, String targetUrlPath = '') {
391
+ withCredentials([string(credentialsId: 'GITHUB_RUNWAYCI_TOKEN', variable: 'GITHUB_RUNWAYCI_TOKEN')]) {
392
+ def targetUrl = targetUrlPath ? "${env.BUILD_URL}${targetUrlPath}" : "${env.BUILD_URL}"
393
+ sh """#!/bin/bash --login
394
+ octocat status --context shiftleft-SERVICE_NAME-postman --description "ShiftLeft SERVICE_NAME Postman" --state ${state} --target_url "${targetUrl}"
395
+ octocat label --label shifted-left
396
+ """
397
+ }
398
+ }
399
+
400
+ // Ensure the shiftleft CLI is installed. run-all.sh execs `shiftleft test`,
401
+ // which stages the library scripts from the package — so CI agents need the CLI.
402
+ // Idempotent (skips if already present). Requires a Jenkins "Secret text"
403
+ // credential (id below) holding the Nexus npm basic-auth token, i.e. the value
404
+ // for //nexuscentral.runwayci.com/repository/npm-group/:_auth= (base64 of user:pass).
405
+ def ensureShiftleftCli() {
406
+ withCredentials([string(credentialsId: 'nexus-npm-auth', variable: 'NEXUS_NPM_AUTH')]) {
407
+ sh '''#!/bin/bash --login
408
+ set -e
409
+ if ! command -v shiftleft >/dev/null 2>&1; then
410
+ echo "//nexuscentral.runwayci.com/repository/npm-group/:_auth=${NEXUS_NPM_AUTH}" >> "$HOME/.npmrc"
411
+ npm install -g shiftleft-tools \
412
+ --registry=https://nexuscentral.runwayci.com/repository/npm-group/ --prefer-online
413
+ fi
414
+ '''
415
+ }
416
+ }
417
+
418
+ def runMutationTests() {
419
+ ensureShiftleftCli()
420
+ echo "Running PIT Mutation Tests..."
421
+ try {
422
+ sh """#!/bin/bash --login
423
+ cd postman/scripts
424
+
425
+ # Run only mutation tests (Phase 2: PIT), reusing unit test output from Unit Tests stage
426
+ ./run-all.sh --skip-unit --skip-postman --skip-coverage --skip-report --no-delay
427
+ """
428
+ } catch (Exception e) {
429
+ echo "Mutation tests failed: ${e.getMessage()}"
430
+ throw e
431
+ }
432
+ }