agent-master-devsecops 0.0.0-stage → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursorignore +23 -0
- package/README.md +371 -3
- package/agents/AGENT_DEVSECOPS.md +417 -0
- package/agents/AGENT_ORCHESTRATOR.md +264 -0
- package/agents/AGENT_QA.md +186 -0
- package/agents/AGENT_SCRUM.md +190 -0
- package/agents/AGENT_SRE.md +173 -0
- package/agents/AGENT_UIUX.md +162 -0
- package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +53 -0
- package/docs/ARCHITECTURE.md +148 -0
- package/docs/CALCULATIONS.md +17 -0
- package/docs/CHANGELOG.md +49 -0
- package/docs/ENGINEERING_RULES.md +252 -0
- package/docs/IDEAS.md +17 -0
- package/docs/STATE.md +43 -0
- package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -0
- package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -0
- package/package.json +47 -6
- package/scripts/a11y_auditor.js +187 -0
- package/scripts/ast_skeletonizer.js +220 -0
- package/scripts/atlassian_client.js +238 -0
- package/scripts/atlassian_sync_action.js +269 -0
- package/scripts/bootstrap_cli.js +468 -0
- package/scripts/circuit_breaker.js +330 -0
- package/scripts/code_quality_auditor.js +142 -0
- package/scripts/confluence_sync.js +337 -0
- package/scripts/context_compressor.js +139 -0
- package/scripts/contract_test_validator.js +166 -0
- package/scripts/credentials_guard.js +216 -0
- package/scripts/domain_idea_miner.js +364 -0
- package/scripts/eval_harness.js +100 -0
- package/scripts/fast_feature_scaffold.js +338 -0
- package/scripts/git_hygiene_linter.js +186 -0
- package/scripts/hot_state_cache.js +95 -0
- package/scripts/jira_manager.js +408 -0
- package/scripts/magic_prompt_generator.js +205 -0
- package/scripts/mcp_server.js +1884 -0
- package/scripts/multi_agent_orchestrator.js +258 -0
- package/scripts/otel_tracer.js +228 -0
- package/scripts/perf_benchmark.js +153 -0
- package/scripts/pr_reviewer.js +373 -0
- package/scripts/sast_scanner.js +180 -0
- package/scripts/sca_dependency_auditor.js +167 -0
- package/scripts/smart_onboarding.js +610 -0
- package/scripts/sre_remediation_engine.js +174 -0
- package/scripts/universal_packager.js +313 -0
- package/scripts/web_inspector.js +355 -0
|
@@ -0,0 +1,238 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,269 @@
|
|
|
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
|
+
};
|