@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.
- package/README.md +351 -0
- package/bin/shiftleft.js +95 -0
- package/package.json +57 -0
- package/src/commands/doctor.js +208 -0
- package/src/commands/init-postman.js +298 -0
- package/src/commands/init-rules.js +78 -0
- package/src/commands/link.js +172 -0
- package/src/commands/protect.js +61 -0
- package/src/commands/run-tests.js +182 -0
- package/src/commands/setup-pipeline.js +209 -0
- package/src/commands/update.js +203 -0
- package/src/index.js +4 -0
- package/src/utils/copy-tree.js +98 -0
- package/src/utils/gitignore.js +26 -0
- package/src/utils/logger.js +9 -0
- package/src/utils/manifest.js +145 -0
- package/src/utils/stack.js +80 -0
- package/src/utils/template.js +135 -0
- package/templates/AGENTS.md +109 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/jenkins/Jenkinsfile-java.groovy +432 -0
- package/templates/jenkins/Jenkinsfile-node.groovy +450 -0
- package/templates/postman/.husky/pre-commit +19 -0
- package/templates/postman/.prettierrc.json +5 -0
- package/templates/postman/README.md.ejs +147 -0
- package/templates/postman/collections/01-core.json.ejs +91 -0
- package/templates/postman/config/local.json.ejs +12 -0
- package/templates/postman/config/staging.json.ejs +26 -0
- package/templates/postman/environments/local.postman_environment.json.ejs +31 -0
- package/templates/postman/environments/staging.postman_environment.json.ejs +31 -0
- package/templates/postman/gitignore +16 -0
- package/templates/postman/npmrc +31 -0
- package/templates/postman/package.json.ejs +66 -0
- package/templates/postman/run-all-shim.sh +16 -0
- package/templates/postman/scripts/auth/generate-jwt.sh +113 -0
- package/templates/postman/scripts/auth/get-issuer-secret.sh +140 -0
- package/templates/postman/scripts/infra/start-mocks.sh +138 -0
- package/templates/postman/scripts/infra/stop-mocks.sh +43 -0
- package/templates/postman/scripts/lib/api_coverage.py +1122 -0
- package/templates/postman/scripts/lib/cleanup-reports.sh +101 -0
- package/templates/postman/scripts/lib/cleanup-stryker.sh +44 -0
- package/templates/postman/scripts/lib/report_combined.py +527 -0
- package/templates/postman/scripts/lib/report_consolidated.py +363 -0
- package/templates/postman/scripts/lib/report_generator.py +121 -0
- package/templates/postman/scripts/lib/report_migration.py +156 -0
- package/templates/postman/scripts/lib/report_mutation.py +110 -0
- package/templates/postman/scripts/lib/report_unit.py +353 -0
- package/templates/postman/scripts/lib/report_utils.py +973 -0
- package/templates/postman/scripts/report-generators/generate-consolidated-report.sh +445 -0
- package/templates/postman/scripts/report-generators/java-api-coverage-matrix.sh +257 -0
- package/templates/postman/scripts/report-generators/mutation-report.sh +672 -0
- package/templates/postman/scripts/report-generators/node-api-coverage-matrix.sh +167 -0
- package/templates/postman/scripts/report-generators/stage-report-artifacts.sh +27 -0
- package/templates/postman/scripts/run-all.sh +452 -0
- package/templates/postman/scripts/runners/run-mutation-tests.sh +113 -0
- package/templates/postman/scripts/runners/run-tests-local.sh +936 -0
- package/templates/postman/scripts/runners/run-tests-staging.sh +741 -0
- package/templates/postman-node/README.md.ejs +26 -0
- package/templates/postman-node/collections/crud/01-bootstrap.json.ejs +34 -0
- package/templates/postman-node/config/local.json.ejs +46 -0
- package/templates/postman-node/config/staging.json.ejs +31 -0
- package/templates/postman-node/local.test.env.ejs +3 -0
- package/templates/postman-node/mocks/external.js +14 -0
- package/templates/postman-node/package.json.ejs +39 -0
- package/templates/postman-node/requirements.txt +1 -0
- package/templates/postman-node/scripts/database/cleanup-mysql.sh +12 -0
- package/templates/postman-node/scripts/database/run-migrations.js +29 -0
- package/templates/postman-node/scripts/database/start-mysql.sh +34 -0
- package/templates/postman-node/scripts/database/wait-for-mysql.sh +36 -0
- package/templates/postman-node/scripts/lib/api_coverage_node.py +1137 -0
- package/templates/postman-node/scripts/lib/fetch-jwt.sh +86 -0
- package/templates/postman-node/scripts/lib/run-newman.sh +104 -0
- package/templates/postman-node/scripts/lib/setup-database.sh +55 -0
- package/templates/postman-node/scripts/lib/start-app.sh +48 -0
- package/templates/postman-node/scripts/lib/utils.sh +114 -0
- package/templates/postman-node/scripts/report-generators/stage-report-artifacts.sh +26 -0
- package/templates/postman-node/scripts/run-all.sh +303 -0
- package/templates/postman-node/scripts/runners/run-tests.sh +123 -0
- package/templates/postman-node/scripts/setup-mocks.js.ejs +29 -0
- package/templates/postman-node/stryker.config.js.ejs +51 -0
- package/templates/rules/local-test-setup.mdc +420 -0
- package/templates/rules/testing-node.mdc +66 -0
- package/templates/rules/testing.mdc +248 -0
- package/templates/skills/_shared/postman-standards.md +380 -0
- package/templates/skills/enhance-test-pipeline/SKILL-java.md +483 -0
- package/templates/skills/enhance-test-pipeline/SKILL-node.md +431 -0
- package/templates/skills/enhance-test-pipeline/SKILL.md +9 -0
- package/templates/skills/review-test-suite/SKILL-java.md +137 -0
- package/templates/skills/review-test-suite/SKILL-node.md +78 -0
- package/templates/skills/review-test-suite/SKILL.md +9 -0
- package/templates/skills/run-test-suite/SKILL-java.md +186 -0
- package/templates/skills/run-test-suite/SKILL-node.md +191 -0
- package/templates/skills/run-test-suite/SKILL.md +9 -0
- package/templates/skills/setup-api-tests/SKILL-java.md +1094 -0
- package/templates/skills/setup-api-tests/SKILL-node.md +141 -0
- package/templates/skills/setup-api-tests/SKILL.md +9 -0
- package/templates/skills/setup-mutation-tests/SKILL-java.md +303 -0
- package/templates/skills/setup-mutation-tests/SKILL-node.md +408 -0
- package/templates/skills/setup-mutation-tests/SKILL.md +9 -0
- package/templates/skills/setup-test-pipeline/SKILL-java.md +454 -0
- package/templates/skills/setup-test-pipeline/SKILL-node.md +318 -0
- package/templates/skills/setup-test-pipeline/SKILL.md +9 -0
- package/templates/skills/write-api-tests/SKILL-java.md +115 -0
- package/templates/skills/write-api-tests/SKILL-node.md +83 -0
- package/templates/skills/write-api-tests/SKILL.md +9 -0
- 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,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
|
+
}
|