agent-master-devsecops 3.9.0 → 5.1.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.
- package/README.md +205 -371
- package/agents/AGENT_DEVSECOPS.md +32 -417
- package/agents/AGENT_INFRA.md +31 -0
- package/agents/AGENT_ORCHESTRATOR.md +39 -264
- package/agents/AGENT_QA.md +58 -186
- package/agents/AGENT_SCRUM.md +23 -190
- package/agents/AGENT_SRE.md +22 -173
- package/agents/AGENT_UIUX.md +26 -162
- package/docs/ARCHITECTURE.md +152 -148
- package/docs/CALCULATIONS.md +17 -17
- package/docs/CHANGELOG.md +112 -49
- package/docs/ENGINEERING_RULES.md +56 -252
- package/docs/IDEAS.md +17 -17
- package/docs/LOCAL_BACKLOG.md +20 -0
- package/docs/STATE.md +111 -43
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -58
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -58
- package/package.json +59 -47
- package/scripts/auditors/a11y_auditor.js +462 -0
- package/scripts/auditors/code_quality_auditor.js +444 -0
- package/scripts/auditors/contract_test_validator.js +396 -0
- package/scripts/auditors/eval_harness.js +213 -0
- package/scripts/auditors/git_hygiene_linter.js +391 -0
- package/scripts/auditors/pr_reviewer.js +627 -0
- package/scripts/auditors/sast_scanner.js +442 -0
- package/scripts/auditors/sca_dependency_auditor.js +673 -0
- package/scripts/{bootstrap_cli.js → bin/bootstrap_cli.js} +597 -468
- package/scripts/bin/domain_idea_miner.js +658 -0
- package/scripts/bin/smart_onboarding.js +905 -0
- package/scripts/{web_inspector.js → bin/web_inspector.js} +578 -355
- package/scripts/core/architecture_advisor.js +378 -0
- package/scripts/core/ast_skeletonizer.js +413 -0
- package/scripts/core/autonomous_remediation_engine.js +305 -0
- package/scripts/{circuit_breaker.js → core/circuit_breaker.js} +546 -330
- package/scripts/core/context_compressor.js +237 -0
- package/scripts/core/fast_feature_scaffold.js +840 -0
- package/scripts/core/hot_state_cache.js +238 -0
- package/scripts/core/knowledge_gateway.js +888 -0
- package/scripts/{magic_prompt_generator.js → core/magic_prompt_generator.js} +250 -205
- package/scripts/{multi_agent_orchestrator.js → core/multi_agent_orchestrator.js} +421 -258
- package/scripts/{otel_tracer.js → core/otel_tracer.js} +335 -228
- package/scripts/{perf_benchmark.js → core/perf_benchmark.js} +212 -153
- package/scripts/core/sarif_engine.js +299 -0
- package/scripts/{sre_remediation_engine.js → core/sre_remediation_engine.js} +236 -174
- package/scripts/{universal_packager.js → core/universal_packager.js} +357 -313
- package/scripts/integrations/atlassian_client.js +600 -0
- package/scripts/integrations/atlassian_sync_action.js +576 -0
- package/scripts/integrations/confluence_sync.js +684 -0
- package/scripts/integrations/credentials_guard.js +500 -0
- package/scripts/integrations/jira_manager.js +722 -0
- package/scripts/mcp/handlers/atlassian_handlers.js +300 -0
- package/scripts/mcp/handlers/core_handlers.js +419 -0
- package/scripts/mcp/handlers/qa_handlers.js +502 -0
- package/scripts/mcp/handlers/rules_handlers.js +379 -0
- package/scripts/mcp/handlers/scaffold_handlers.js +334 -0
- package/scripts/mcp/handlers/sre_handlers.js +443 -0
- package/scripts/mcp/resources_registry.js +453 -0
- package/scripts/mcp/server.js +408 -0
- package/scripts/mcp/tool_handlers.js +444 -0
- package/scripts/mcp/tools_registry.js +1123 -0
- package/scripts/mcp_server.js +236 -1884
- package/.cursorignore +0 -23
- package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +0 -53
- package/scripts/a11y_auditor.js +0 -187
- package/scripts/ast_skeletonizer.js +0 -220
- package/scripts/atlassian_client.js +0 -238
- package/scripts/atlassian_sync_action.js +0 -269
- package/scripts/code_quality_auditor.js +0 -142
- package/scripts/confluence_sync.js +0 -337
- package/scripts/context_compressor.js +0 -139
- package/scripts/contract_test_validator.js +0 -166
- package/scripts/credentials_guard.js +0 -216
- package/scripts/domain_idea_miner.js +0 -364
- package/scripts/eval_harness.js +0 -100
- package/scripts/fast_feature_scaffold.js +0 -338
- package/scripts/git_hygiene_linter.js +0 -186
- package/scripts/hot_state_cache.js +0 -95
- package/scripts/jira_manager.js +0 -408
- package/scripts/pr_reviewer.js +0 -373
- package/scripts/sast_scanner.js +0 -180
- package/scripts/sca_dependency_auditor.js +0 -167
- package/scripts/smart_onboarding.js +0 -610
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* scripts/atlassian_client.js
|
|
4
|
-
*
|
|
5
|
-
* Cliente Unificado e Utilitários de Comunicação com APIs REST Atlassian (Jira e Confluence).
|
|
6
|
-
* Centraliza autenticação, sanitização de domínio, introspecção de git e execução de requisições HTTPS.
|
|
7
|
-
*
|
|
8
|
-
* Governança: Tríade Git + Jira + Confluence (Regra 11 e Regra 15).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('node:fs');
|
|
12
|
-
const path = require('node:path');
|
|
13
|
-
const https = require('node:https');
|
|
14
|
-
const { execSync, execFileSync } = require('node:child_process');
|
|
15
|
-
|
|
16
|
-
function sanitizeDomain(domainStr) {
|
|
17
|
-
if (!domainStr) return null;
|
|
18
|
-
return domainStr.replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function resolveMcpConfig() {
|
|
22
|
-
const homeDir = process.env.USERPROFILE || process.env.HOME || '';
|
|
23
|
-
const candidatePaths = [
|
|
24
|
-
path.join(homeDir, '.gemini', 'config', 'mcp_config.json'),
|
|
25
|
-
path.join(homeDir, '.antigravity', 'config', 'mcp_config.json'),
|
|
26
|
-
path.resolve(process.cwd(), '.mcp_config.json')
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
for (const p of candidatePaths) {
|
|
30
|
-
if (fs.existsSync(p)) {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
33
|
-
} catch (_) {}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getAtlassianCredentials(targetDir = process.cwd()) {
|
|
40
|
-
let token = process.env.ATLASSIAN_API_TOKEN || process.env.JIRA_API_TOKEN || process.env.CONFLUENCE_API_TOKEN || process.env.PAT;
|
|
41
|
-
let email = process.env.ATLASSIAN_EMAIL || process.env.JIRA_EMAIL || process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
|
|
42
|
-
let rawDomain = process.env.ATLASSIAN_DOMAIN || process.env.JIRA_DOMAIN || process.env.CONFLUENCE_DOMAIN || process.env.JIRA_BASE_URL || process.env.CONFLUENCE_BASE_URL;
|
|
43
|
-
let source = (token && email && rawDomain) ? 'environment_variables' : null;
|
|
44
|
-
|
|
45
|
-
if (!process.env.IGNORE_LOCAL_ENV && (!token || !email || !rawDomain)) {
|
|
46
|
-
const isLocalWorkspace = path.resolve(targetDir) === path.resolve(process.cwd());
|
|
47
|
-
const candidateFiles = isLocalWorkspace
|
|
48
|
-
? [
|
|
49
|
-
path.resolve(targetDir, '.env'),
|
|
50
|
-
path.resolve(targetDir, '..', '.env'),
|
|
51
|
-
path.resolve(process.cwd(), '.env'),
|
|
52
|
-
path.resolve(process.cwd(), '.env.local')
|
|
53
|
-
]
|
|
54
|
-
: [
|
|
55
|
-
path.resolve(targetDir, '.env'),
|
|
56
|
-
path.resolve(targetDir, '.env.local')
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
for (const envPath of candidateFiles) {
|
|
60
|
-
if (fs.existsSync(envPath)) {
|
|
61
|
-
try {
|
|
62
|
-
const raw = fs.readFileSync(envPath, 'utf8');
|
|
63
|
-
const dMatch = raw.match(/(?:ATLASSIAN_DOMAIN|JIRA_DOMAIN|CONFLUENCE_DOMAIN)=(.*)/);
|
|
64
|
-
const eMatch = raw.match(/(?:ATLASSIAN_EMAIL|JIRA_EMAIL|CONFLUENCE_EMAIL)=(.*)/);
|
|
65
|
-
const tMatch = raw.match(/(?:ATLASSIAN_API_TOKEN|JIRA_API_TOKEN|CONFLUENCE_API_TOKEN)=(.*)/);
|
|
66
|
-
if (dMatch && dMatch[1] && !rawDomain) rawDomain = dMatch[1].trim();
|
|
67
|
-
if (eMatch && eMatch[1] && !email) email = eMatch[1].trim();
|
|
68
|
-
if (tMatch && tMatch[1] && !token) token = tMatch[1].trim();
|
|
69
|
-
if (rawDomain && email && token) {
|
|
70
|
-
source = source || `local_file:${path.basename(envPath)}`;
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
} catch (_) {}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const isLocalTarget = path.resolve(targetDir) === path.resolve(process.cwd());
|
|
79
|
-
if ((!token || !email || !rawDomain) && isLocalTarget) {
|
|
80
|
-
const mcp = resolveMcpConfig();
|
|
81
|
-
if (mcp && mcp.mcpServers) {
|
|
82
|
-
if (mcp.mcpServers.jira && mcp.mcpServers.jira.env) {
|
|
83
|
-
token = token || mcp.mcpServers.jira.env.JIRA_API_TOKEN;
|
|
84
|
-
email = email || mcp.mcpServers.jira.env.JIRA_EMAIL;
|
|
85
|
-
rawDomain = rawDomain || mcp.mcpServers.jira.env.JIRA_BASE_URL;
|
|
86
|
-
if (token && email && rawDomain) source = source || 'mcp_config:jira';
|
|
87
|
-
}
|
|
88
|
-
if (mcp.mcpServers.confluence && mcp.mcpServers.confluence.env) {
|
|
89
|
-
token = token || mcp.mcpServers.confluence.env.PAT;
|
|
90
|
-
if (token) source = source || 'mcp_config:confluence';
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const cleanDomain = sanitizeDomain(rawDomain);
|
|
96
|
-
const isConfigured = Boolean(token && email && cleanDomain);
|
|
97
|
-
const auth = isConfigured ? Buffer.from(`${email}:${token}`).toString('base64') : null;
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
domain: cleanDomain,
|
|
101
|
-
email: email || null,
|
|
102
|
-
token: token || null,
|
|
103
|
-
auth,
|
|
104
|
-
authBase64: auth,
|
|
105
|
-
isConfigured,
|
|
106
|
-
source
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function checkGithubAuth(options = {}) {
|
|
111
|
-
const { globalStateCache } = require('./hot_state_cache.js');
|
|
112
|
-
const cacheKey = `gh_auth_${process.env.GITHUB_TOKEN || ''}_${process.env.GH_TOKEN || ''}`;
|
|
113
|
-
if (options && options.forceFresh) {
|
|
114
|
-
globalStateCache.invalidate(cacheKey);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return globalStateCache.wrap(cacheKey, () => {
|
|
118
|
-
let token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
119
|
-
let isAuthenticated = false;
|
|
120
|
-
let user = null;
|
|
121
|
-
let details = '';
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
const stdout = execFileSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
125
|
-
isAuthenticated = true;
|
|
126
|
-
details = stdout.trim();
|
|
127
|
-
const userMatch = stdout.match(/Logged in to [^\s]+ account ([^\s\(\)]+)/i) || stdout.match(/account ([^\s\(\)]+)/i);
|
|
128
|
-
if (userMatch) user = userMatch[1];
|
|
129
|
-
} catch (err) {
|
|
130
|
-
const output = (err.stdout || '') + (err.stderr || '');
|
|
131
|
-
if (/Logged in to/i.test(output)) {
|
|
132
|
-
isAuthenticated = true;
|
|
133
|
-
details = output.trim();
|
|
134
|
-
const userMatch = output.match(/account ([^\s\(\)]+)/i);
|
|
135
|
-
if (userMatch) user = userMatch[1];
|
|
136
|
-
} else if (token) {
|
|
137
|
-
isAuthenticated = true;
|
|
138
|
-
user = 'token-authenticated';
|
|
139
|
-
details = 'Autenticado via GITHUB_TOKEN.';
|
|
140
|
-
} else {
|
|
141
|
-
isAuthenticated = false;
|
|
142
|
-
details = output.trim() || 'Usuário não autenticado no GitHub CLI.';
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
installed: true,
|
|
148
|
-
authenticated: isAuthenticated,
|
|
149
|
-
isAuthenticated,
|
|
150
|
-
token: token || null,
|
|
151
|
-
user,
|
|
152
|
-
details
|
|
153
|
-
};
|
|
154
|
-
}, 10000); // 10s TTL
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function getGitRepositoryUrl() {
|
|
158
|
-
if (process.env.GITHUB_REPOSITORY) {
|
|
159
|
-
return `https://github.com/${process.env.GITHUB_REPOSITORY}`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const rawRemote = execFileSync('git', ['config', '--get', 'remote.origin.url'], { encoding: 'utf8' }).trim();
|
|
164
|
-
if (rawRemote.startsWith('git@github.com:')) {
|
|
165
|
-
return `https://github.com/${rawRemote.replace('git@github.com:', '').replace(/\.git$/, '')}`;
|
|
166
|
-
} else if (rawRemote.startsWith('https://github.com/')) {
|
|
167
|
-
return rawRemote.replace(/\.git$/, '');
|
|
168
|
-
}
|
|
169
|
-
} catch (_) {}
|
|
170
|
-
|
|
171
|
-
return 'https://github.com/ContalApps/agent-master-devsecops';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function getGitMetadata(options = {}) {
|
|
175
|
-
const { globalStateCache } = require('./hot_state_cache.js');
|
|
176
|
-
if (options && options.forceFresh) {
|
|
177
|
-
globalStateCache.invalidate('git_metadata');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return globalStateCache.wrap('git_metadata', () => {
|
|
181
|
-
let sha = '';
|
|
182
|
-
let branch = '';
|
|
183
|
-
let message = '';
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
sha = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
187
|
-
branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
188
|
-
message = execFileSync('git', ['log', '-1', '--format=%s', sha], { encoding: 'utf8' }).trim();
|
|
189
|
-
} catch (_) {}
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
sha,
|
|
193
|
-
branch,
|
|
194
|
-
message,
|
|
195
|
-
repoUrl: getGitRepositoryUrl()
|
|
196
|
-
};
|
|
197
|
-
}, 5000); // 5s TTL
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function requestAtlassian(credentials, method, endpointPath, bodyData = null) {
|
|
201
|
-
return new Promise((resolve, reject) => {
|
|
202
|
-
const postData = bodyData ? JSON.stringify(bodyData) : null;
|
|
203
|
-
const req = https.request({
|
|
204
|
-
hostname: credentials.domain,
|
|
205
|
-
path: endpointPath,
|
|
206
|
-
method,
|
|
207
|
-
headers: {
|
|
208
|
-
'Authorization': `Basic ${credentials.auth || credentials.authBase64}`,
|
|
209
|
-
'Content-Type': 'application/json',
|
|
210
|
-
'Accept': 'application/json',
|
|
211
|
-
...(postData ? { 'Content-Length': Buffer.byteLength(postData) } : {})
|
|
212
|
-
}
|
|
213
|
-
}, res => {
|
|
214
|
-
let data = '';
|
|
215
|
-
res.on('data', chunk => { data += chunk; });
|
|
216
|
-
res.on('end', () => {
|
|
217
|
-
try {
|
|
218
|
-
resolve({ status: res.statusCode, body: data ? JSON.parse(data) : {} });
|
|
219
|
-
} catch (_) {
|
|
220
|
-
resolve({ status: res.statusCode, body: data });
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
req.on('error', reject);
|
|
226
|
-
if (postData) req.write(postData);
|
|
227
|
-
req.end();
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
module.exports = {
|
|
232
|
-
sanitizeDomain,
|
|
233
|
-
getAtlassianCredentials,
|
|
234
|
-
checkGithubAuth,
|
|
235
|
-
getGitRepositoryUrl,
|
|
236
|
-
getGitMetadata,
|
|
237
|
-
requestAtlassian
|
|
238
|
-
};
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* scripts/atlassian_sync_action.js
|
|
4
|
-
*
|
|
5
|
-
* Sincronizador Automático Git ↔ Atlassian via Actions & Local (AGDEV-23 / AGDEV-35).
|
|
6
|
-
* Conecta automaticamente commits, branches e Pull Requests aos cards do Jira
|
|
7
|
-
* utilizando a API oficial de Remote Links do Jira Cloud (REST API v3).
|
|
8
|
-
*
|
|
9
|
-
* Blindado pela Regra 15: Imutabilidade de Requisitos & Zero Falhas Silenciosas.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const { execFileSync } = require('child_process');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const {
|
|
15
|
-
getAtlassianConfig,
|
|
16
|
-
getGitMetadata,
|
|
17
|
-
getGitRepositoryUrl,
|
|
18
|
-
validateRule15
|
|
19
|
-
} = require('./credentials_guard');
|
|
20
|
-
|
|
21
|
-
function getAuth() {
|
|
22
|
-
const cfg = getAtlassianConfig();
|
|
23
|
-
return cfg.authBase64;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function extractIssueKeys(text) {
|
|
27
|
-
if (!text) return [];
|
|
28
|
-
const matches = text.match(/\b[A-Z]{2,10}-\d+\b/g) || [];
|
|
29
|
-
return [...new Set(matches.map(k => k.toUpperCase()))];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function extractPrNumber(text) {
|
|
33
|
-
if (!text) return null;
|
|
34
|
-
const match = text.match(/#(\d+)\b/) || text.match(/pull request #(\d+)/i);
|
|
35
|
-
return match ? match[1] : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function formatRemoteLinkPayload(url, title, type = 'commit', globalId = null, summary = null) {
|
|
39
|
-
let iconUrl = 'https://github.githubassets.com/favicons/favicon.png';
|
|
40
|
-
let relationship = 'causes';
|
|
41
|
-
|
|
42
|
-
if (type === 'pr') {
|
|
43
|
-
relationship = 'relates to';
|
|
44
|
-
} else if (type === 'branch') {
|
|
45
|
-
relationship = 'builds';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const payload = {
|
|
49
|
-
relationship,
|
|
50
|
-
object: {
|
|
51
|
-
url,
|
|
52
|
-
title,
|
|
53
|
-
icon: {
|
|
54
|
-
url16x16: iconUrl,
|
|
55
|
-
title: `GitHub ${type.toUpperCase()}`
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
if (globalId) {
|
|
61
|
-
payload.globalId = globalId;
|
|
62
|
-
}
|
|
63
|
-
if (summary) {
|
|
64
|
-
payload.object.summary = summary;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return payload;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function linkUrlToJira(issueKey, url, title, type = 'commit', authOverride = null, globalId = null, summary = null) {
|
|
71
|
-
const atlassian = getAtlassianConfig();
|
|
72
|
-
const auth = authOverride || atlassian.authBase64;
|
|
73
|
-
if (!auth) {
|
|
74
|
-
return { status: 'FAILED', message: 'Credenciais Atlassian não configuradas no ambiente (Regra 15).' };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const cleanDomain = atlassian.domain;
|
|
78
|
-
if (!cleanDomain) {
|
|
79
|
-
return { status: 'FAILED', message: 'Domínio Atlassian não configurado para este projeto (Regra 15).' };
|
|
80
|
-
}
|
|
81
|
-
const payload = formatRemoteLinkPayload(url, title, type, globalId, summary);
|
|
82
|
-
const endpoint = `https://${cleanDomain}/rest/api/3/issue/${issueKey}/remotelink`;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const res = await fetch(endpoint, {
|
|
86
|
-
method: 'POST',
|
|
87
|
-
headers: {
|
|
88
|
-
'Authorization': `Basic ${auth}`,
|
|
89
|
-
'Content-Type': 'application/json',
|
|
90
|
-
'Accept': 'application/json'
|
|
91
|
-
},
|
|
92
|
-
body: JSON.stringify(payload)
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
if (res.ok || res.status === 201 || res.status === 200) {
|
|
96
|
-
return { status: 'SUCCESS', issueKey, url, type, httpStatus: res.status };
|
|
97
|
-
} else {
|
|
98
|
-
const errText = await res.text();
|
|
99
|
-
return { status: 'FAILED', issueKey, error: `HTTP ${res.status}: ${errText}` };
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
return { status: 'ERROR', issueKey, error: err.message };
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function backfillHistory(limit = 30) {
|
|
107
|
-
console.log(`[Atlassian Sync] Iniciando sincronização retroativa dos últimos ${limit} commits...`);
|
|
108
|
-
const atlassian = getAtlassianConfig();
|
|
109
|
-
if (!atlassian.isConfigured) {
|
|
110
|
-
throw new Error('Credenciais Atlassian ausentes para sincronização retroativa.');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const repoUrl = getGitRepositoryUrl();
|
|
114
|
-
const repoSlug = repoUrl.replace('https://github.com/', '');
|
|
115
|
-
|
|
116
|
-
let rawLog = '';
|
|
117
|
-
try {
|
|
118
|
-
rawLog = execFileSync('git', ['log', `-${limit}`, '--format=%H|%s'], { encoding: 'utf8' });
|
|
119
|
-
} catch (err) {
|
|
120
|
-
console.error('[Atlassian Sync] Falha ao extrair histórico git:', err.message);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const lines = rawLog.split('\n').filter(l => l.trim().length > 0);
|
|
125
|
-
const results = [];
|
|
126
|
-
|
|
127
|
-
for (const line of lines) {
|
|
128
|
-
const [sha, ...subjectParts] = line.split('|');
|
|
129
|
-
const subject = subjectParts.join('|');
|
|
130
|
-
const keys = extractIssueKeys(subject);
|
|
131
|
-
const prNumber = extractPrNumber(subject);
|
|
132
|
-
|
|
133
|
-
for (const key of keys) {
|
|
134
|
-
// 1. Link Commit
|
|
135
|
-
const commitGlobalId = `system=github&repo=${repoSlug}&type=commit&sha=${sha}`;
|
|
136
|
-
const commitRes = await linkUrlToJira(
|
|
137
|
-
key,
|
|
138
|
-
`${repoUrl}/commit/${sha}`,
|
|
139
|
-
`GitHub Commit: ${sha.slice(0, 7)} (${subject})`,
|
|
140
|
-
'commit',
|
|
141
|
-
atlassian.authBase64,
|
|
142
|
-
commitGlobalId
|
|
143
|
-
);
|
|
144
|
-
results.push({ key, type: 'commit', ref: sha.slice(0, 7), status: commitRes.status });
|
|
145
|
-
|
|
146
|
-
// 2. Link PR if present in commit title
|
|
147
|
-
if (prNumber) {
|
|
148
|
-
const prGlobalId = `system=github&repo=${repoSlug}&type=pr&id=${prNumber}`;
|
|
149
|
-
const prRes = await linkUrlToJira(
|
|
150
|
-
key,
|
|
151
|
-
`${repoUrl}/pull/${prNumber}`,
|
|
152
|
-
`GitHub Pull Request #${prNumber}`,
|
|
153
|
-
'pr',
|
|
154
|
-
atlassian.authBase64,
|
|
155
|
-
prGlobalId,
|
|
156
|
-
subject
|
|
157
|
-
);
|
|
158
|
-
results.push({ key, type: 'pr', ref: `#${prNumber}`, status: prRes.status });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const successCount = results.filter(r => r.status === 'SUCCESS').length;
|
|
164
|
-
console.log(`[Atlassian Sync] Sincronização retroativa concluída: ${successCount}/${results.length} links processados com sucesso.`);
|
|
165
|
-
return results;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function runCli() {
|
|
169
|
-
const isCI = process.env.GITHUB_ACTIONS === 'true';
|
|
170
|
-
|
|
171
|
-
// Validação Compulsória de Requisitos (Regra 15)
|
|
172
|
-
// Em CI, a ausência de secrets agora é ERRO CRÍTICO para impedir perda silenciosa
|
|
173
|
-
const validation = validateRule15({ failFast: isCI, requireGitHub: false, requireAtlassian: true });
|
|
174
|
-
if (!validation.valid) {
|
|
175
|
-
console.error(`[Atlassian Sync] Falha de validação da Regra 15:\n ${validation.missing.join(', ')}`);
|
|
176
|
-
console.error(` Remediação: ${validation.remediation.join('\n ')}`);
|
|
177
|
-
if (isCI) {
|
|
178
|
-
process.exit(1);
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const args = process.argv.slice(2);
|
|
184
|
-
if (args.includes('--retroactive') || args.includes('-r')) {
|
|
185
|
-
const limitIdx = args.findIndex(a => a === '--retroactive' || a === '-r');
|
|
186
|
-
const limitVal = parseInt(args[limitIdx + 1], 10) || 30;
|
|
187
|
-
await backfillHistory(limitVal);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const atlassian = getAtlassianConfig();
|
|
192
|
-
const gitMeta = getGitMetadata();
|
|
193
|
-
const repoUrl = gitMeta.repoUrl;
|
|
194
|
-
const repoSlug = repoUrl.replace('https://github.com/', '');
|
|
195
|
-
|
|
196
|
-
const sha = gitMeta.sha;
|
|
197
|
-
const branch = gitMeta.branch;
|
|
198
|
-
const message = gitMeta.message;
|
|
199
|
-
const prNumber = process.env.PR_NUMBER || extractPrNumber(process.env.PR_TITLE || '') || extractPrNumber(message);
|
|
200
|
-
|
|
201
|
-
const combined = `${branch} ${message} ${process.env.PR_TITLE || ''}`;
|
|
202
|
-
const keys = extractIssueKeys(combined);
|
|
203
|
-
|
|
204
|
-
if (keys.length === 0) {
|
|
205
|
-
console.log('[Atlassian Sync] Nenhuma chave Jira encontrada na branch ou mensagem de commit.');
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
console.log(`[Atlassian Sync] Chaves Jira identificadas: ${keys.join(', ')}`);
|
|
210
|
-
|
|
211
|
-
for (const key of keys) {
|
|
212
|
-
if (sha && repoUrl) {
|
|
213
|
-
const commitGlobalId = `system=github&repo=${repoSlug}&type=commit&sha=${sha}`;
|
|
214
|
-
const commitRes = await linkUrlToJira(
|
|
215
|
-
key,
|
|
216
|
-
`${repoUrl}/commit/${sha}`,
|
|
217
|
-
`GitHub Commit: ${sha.slice(0, 7)} (${message})`,
|
|
218
|
-
'commit',
|
|
219
|
-
atlassian.authBase64,
|
|
220
|
-
commitGlobalId
|
|
221
|
-
);
|
|
222
|
-
console.log(`[Atlassian Sync] Commit ${sha.slice(0, 7)} vinculado a ${key}: ${commitRes.status}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (branch && branch !== 'main' && branch !== 'HEAD' && repoUrl) {
|
|
226
|
-
const branchGlobalId = `system=github&repo=${repoSlug}&type=branch&name=${branch}`;
|
|
227
|
-
const branchRes = await linkUrlToJira(
|
|
228
|
-
key,
|
|
229
|
-
`${repoUrl}/tree/${branch}`,
|
|
230
|
-
`GitHub Branch: ${branch}`,
|
|
231
|
-
'branch',
|
|
232
|
-
atlassian.authBase64,
|
|
233
|
-
branchGlobalId
|
|
234
|
-
);
|
|
235
|
-
console.log(`[Atlassian Sync] Branch ${branch} vinculada a ${key}: ${branchRes.status}`);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (prNumber && repoUrl) {
|
|
239
|
-
const prGlobalId = `system=github&repo=${repoSlug}&type=pr&id=${prNumber}`;
|
|
240
|
-
const prRes = await linkUrlToJira(
|
|
241
|
-
key,
|
|
242
|
-
`${repoUrl}/pull/${prNumber}`,
|
|
243
|
-
`GitHub Pull Request #${prNumber}`,
|
|
244
|
-
'pr',
|
|
245
|
-
atlassian.authBase64,
|
|
246
|
-
prGlobalId,
|
|
247
|
-
process.env.PR_TITLE || message
|
|
248
|
-
);
|
|
249
|
-
console.log(`[Atlassian Sync] PR #${prNumber} vinculado a ${key}: ${prRes.status}`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (require.main === module) {
|
|
255
|
-
runCli().catch(err => {
|
|
256
|
-
console.error('[Atlassian Sync] Erro fatal na execução:', err);
|
|
257
|
-
process.exit(1);
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
module.exports = {
|
|
262
|
-
getAuth,
|
|
263
|
-
extractIssueKeys,
|
|
264
|
-
extractPrNumber,
|
|
265
|
-
formatRemoteLinkPayload,
|
|
266
|
-
linkUrlToJira,
|
|
267
|
-
backfillHistory,
|
|
268
|
-
runCli
|
|
269
|
-
};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Code Quality & Cyclomatic Complexity Auditor
|
|
3
|
-
* Auditoria independente de qualidade de software, complexidade ciclomática,
|
|
4
|
-
* profundidade de aninhamento e detecção de anti-patterns de código.
|
|
5
|
-
* Em conformidade com a Regra 13 (Estrutura de Código) e Matriz de Severidade.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('node:fs');
|
|
9
|
-
const path = require('node:path');
|
|
10
|
-
|
|
11
|
-
class CodeQualityAuditor {
|
|
12
|
-
constructor(options = {}) {
|
|
13
|
-
this.maxComplexity = options.maxComplexity || 15;
|
|
14
|
-
this.maxNesting = options.maxNesting || 4;
|
|
15
|
-
this.maxFileLines = options.maxFileLines || 300;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
auditCode(code, filename = 'snippet.js') {
|
|
19
|
-
const lines = code.split('\n');
|
|
20
|
-
const totalLines = lines.length;
|
|
21
|
-
let commentLines = 0;
|
|
22
|
-
let blankLines = 0;
|
|
23
|
-
let maxNestingObserved = 0;
|
|
24
|
-
let currentNesting = 0;
|
|
25
|
-
const issues = [];
|
|
26
|
-
|
|
27
|
-
// Contadores de decisão para complexidade ciclomática
|
|
28
|
-
let complexityScore = 1;
|
|
29
|
-
const decisionPatterns = [
|
|
30
|
-
/\bif\s*\(/g,
|
|
31
|
-
/\belse\s+if\s*\(/g,
|
|
32
|
-
/\bfor\s*\(/g,
|
|
33
|
-
/\bwhile\s*\(/g,
|
|
34
|
-
/\bcatch\s*\(/g,
|
|
35
|
-
/\bcase\s+[^:]+:/g,
|
|
36
|
-
/&&/g,
|
|
37
|
-
/\|\|/g,
|
|
38
|
-
/\?[^:]+:/g
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
lines.forEach((line, index) => {
|
|
42
|
-
const lineNum = index + 1;
|
|
43
|
-
const trimmed = line.trim();
|
|
44
|
-
|
|
45
|
-
if (!trimmed) {
|
|
46
|
-
blankLines++;
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
|
51
|
-
commentLines++;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Análise de aninhamento de chaves
|
|
56
|
-
for (const char of trimmed) {
|
|
57
|
-
if (char === '{') currentNesting++;
|
|
58
|
-
if (char === '}') currentNesting = Math.max(0, currentNesting - 1);
|
|
59
|
-
if (currentNesting > maxNestingObserved) {
|
|
60
|
-
maxNestingObserved = currentNesting;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Contagem de complexidade ciclomática
|
|
65
|
-
for (const pattern of decisionPatterns) {
|
|
66
|
-
const matches = trimmed.match(pattern);
|
|
67
|
-
if (matches) {
|
|
68
|
-
complexityScore += matches.length;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Detecção de empty catch block (anti-pattern de engolir exceções)
|
|
73
|
-
if (/catch\s*\([^\)]*\)\s*\{\s*\}/.test(trimmed)) {
|
|
74
|
-
issues.push({
|
|
75
|
-
type: 'EMPTY_CATCH',
|
|
76
|
-
line: lineNum,
|
|
77
|
-
severity: 'HIGH',
|
|
78
|
-
message: 'Bloco catch vazio detectado: erros não devem ser suprimidos silenciosamente (Regra 5).'
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Detecção de comentários TODO ou código comentado
|
|
83
|
-
if (/\b(?:TODO|FIXME|HACK)\b/i.test(trimmed)) {
|
|
84
|
-
issues.push({
|
|
85
|
-
type: 'PENDING_TODO',
|
|
86
|
-
line: lineNum,
|
|
87
|
-
severity: 'LOW',
|
|
88
|
-
message: `Marcação pendente detectada: "${trimmed}". Código pronto não deve deixar TODOs (Regra 7).`
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Verificação de limites
|
|
94
|
-
if (totalLines > this.maxFileLines) {
|
|
95
|
-
issues.push({
|
|
96
|
-
type: 'FILE_TOO_LONG',
|
|
97
|
-
line: totalLines,
|
|
98
|
-
severity: 'MEDIUM',
|
|
99
|
-
message: `Arquivo possui ${totalLines} linhas (limite canônico recomendado: ${this.maxFileLines} linhas - Regra 13).`
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (maxNestingObserved > this.maxNesting) {
|
|
104
|
-
issues.push({
|
|
105
|
-
type: 'DEEP_NESTING',
|
|
106
|
-
line: 1,
|
|
107
|
-
severity: 'MEDIUM',
|
|
108
|
-
message: `Nível máximo de aninhamento (${maxNestingObserved}) excede o limiar tolerado (${this.maxNesting}).`
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (complexityScore > this.maxComplexity) {
|
|
113
|
-
issues.push({
|
|
114
|
-
type: 'HIGH_CYCLOMATIC_COMPLEXITY',
|
|
115
|
-
line: 1,
|
|
116
|
-
severity: 'MEDIUM',
|
|
117
|
-
message: `Complexidade ciclomática (${complexityScore}) excede o limite aceitável de ${this.maxComplexity}.`
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const isClean = issues.filter(i => i.severity === 'HIGH' || i.severity === 'CRITICAL').length === 0;
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
filename,
|
|
125
|
-
status: isClean ? (issues.length === 0 ? 'OPTIMAL' : 'ACCEPTABLE_WITH_NOTES') : 'REFACTOR_REQUIRED',
|
|
126
|
-
metrics: {
|
|
127
|
-
totalLines,
|
|
128
|
-
codeLines: totalLines - commentLines - blankLines,
|
|
129
|
-
commentLines,
|
|
130
|
-
blankLines,
|
|
131
|
-
cyclomaticComplexity: complexityScore,
|
|
132
|
-
maxNestingDepth: maxNestingObserved
|
|
133
|
-
},
|
|
134
|
-
qualityScore: Math.max(0, 100 - (issues.length * 10) - (complexityScore > this.maxComplexity ? 15 : 0)),
|
|
135
|
-
issues
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
module.exports = {
|
|
141
|
-
CodeQualityAuditor
|
|
142
|
-
};
|