gitgreen 0.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 +77 -0
- package/data/aws_machine_power_profiles.json +54 -0
- package/data/cpu_power_profiles.json +275 -0
- package/data/gcp_machine_power_profiles.json +1802 -0
- package/data/runtime-pue-mappings.json +183 -0
- package/dist/cli.js +137 -0
- package/dist/config.js +43 -0
- package/dist/index.js +94 -0
- package/dist/init.js +773 -0
- package/dist/lib/carbon/carbon-calculator.js +105 -0
- package/dist/lib/carbon/intensity-provider.js +29 -0
- package/dist/lib/carbon/power-profile-repository.js +90 -0
- package/dist/lib/carbon/zone-mapper.js +107 -0
- package/dist/lib/gitlab/gitlab-client.js +28 -0
- package/dist/lib/gitlab/report-formatter.js +93 -0
- package/dist/lib/telemetry/gitlab-job-loader.js +23 -0
- package/dist/lib/telemetry/runner-parser.js +63 -0
- package/dist/types.js +2 -0
- package/package.json +56 -0
package/dist/init.js
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInit = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const axios_1 = __importDefault(require("axios"));
|
|
11
|
+
const prompts_1 = __importDefault(require("prompts"));
|
|
12
|
+
const kleur_1 = require("kleur");
|
|
13
|
+
const hasGlab = () => {
|
|
14
|
+
try {
|
|
15
|
+
(0, child_process_1.execSync)('glab --version', { stdio: 'ignore' });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const glabAuthenticated = () => {
|
|
23
|
+
try {
|
|
24
|
+
(0, child_process_1.execSync)('glab auth status', { stdio: 'ignore' });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const getGitRemoteProject = () => {
|
|
32
|
+
try {
|
|
33
|
+
const remote = (0, child_process_1.execSync)('git remote get-url origin', { encoding: 'utf8' }).trim();
|
|
34
|
+
const match = remote.match(/gitlab\.com[:/](.+?)(\.git)?$/);
|
|
35
|
+
return match ? match[1] : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const setVariableGlab = (project, key, value, masked) => {
|
|
42
|
+
try {
|
|
43
|
+
(0, child_process_1.execSync)(`glab variable delete ${key} -R "${project}" 2>/dev/null || true`, { stdio: 'ignore' });
|
|
44
|
+
const maskFlag = masked ? '--masked' : '';
|
|
45
|
+
(0, child_process_1.execSync)(`echo "${value}" | glab variable set ${key} -R "${project}" ${maskFlag}`, { stdio: 'pipe' });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const setVariableApi = async (baseUrl, pat, projectPath, key, value, masked) => {
|
|
53
|
+
const projectEncoded = encodeURIComponent(projectPath);
|
|
54
|
+
const apiUrl = `${baseUrl}/api/v4/projects/${projectEncoded}/variables`;
|
|
55
|
+
try {
|
|
56
|
+
// Delete existing
|
|
57
|
+
await axios_1.default.delete(`${apiUrl}/${key}`, {
|
|
58
|
+
headers: { 'PRIVATE-TOKEN': pat }
|
|
59
|
+
}).catch(() => { });
|
|
60
|
+
// Create new
|
|
61
|
+
await axios_1.default.post(apiUrl, {
|
|
62
|
+
key,
|
|
63
|
+
value,
|
|
64
|
+
masked,
|
|
65
|
+
protected: false
|
|
66
|
+
}, {
|
|
67
|
+
headers: { 'PRIVATE-TOKEN': pat }
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const setVariable = async (auth, project, key, value, masked = false) => {
|
|
76
|
+
if (auth.type === 'glab') {
|
|
77
|
+
return setVariableGlab(project, key, value, masked);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
return setVariableApi(auth.baseUrl, auth.pat, project, key, value, masked);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const generateCiJob = (runnerTag) => {
|
|
84
|
+
const tagsSection = runnerTag ? `\n tags:\n - ${runnerTag}` : '';
|
|
85
|
+
return `# GitGreen carbon analysis (generated by gitgreen init)
|
|
86
|
+
# Fetches real CPU and RAM metrics from GCP Monitoring API
|
|
87
|
+
|
|
88
|
+
carbon_analysis:
|
|
89
|
+
stage: test${tagsSection}
|
|
90
|
+
before_script:
|
|
91
|
+
- echo "$GCP_SA_KEY_BASE64" | base64 -d > /tmp/gcp-key.json
|
|
92
|
+
- gcloud auth activate-service-account --key-file=/tmp/gcp-key.json
|
|
93
|
+
- npm install ./gitgreen-cli-0.1.0.tgz
|
|
94
|
+
script:
|
|
95
|
+
- |
|
|
96
|
+
TOKEN=$(gcloud auth print-access-token)
|
|
97
|
+
START_TIME=$(date -u -d "$CI_PIPELINE_CREATED_AT" +%Y-%m-%dT%H:%M:%SZ)
|
|
98
|
+
END_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
99
|
+
|
|
100
|
+
CPU_FILTER="resource.type=\\"gce_instance\\" AND resource.labels.instance_id=\\"$GCP_INSTANCE_ID\\" AND resource.labels.zone=\\"$GCP_ZONE\\" AND metric.type=\\"compute.googleapis.com/instance/cpu/utilization\\""
|
|
101
|
+
CPU_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$CPU_FILTER'''))")
|
|
102
|
+
|
|
103
|
+
RAM_USED_FILTER="resource.type=\\"gce_instance\\" AND resource.labels.instance_id=\\"$GCP_INSTANCE_ID\\" AND metric.type=\\"compute.googleapis.com/instance/memory/balloon/ram_used\\""
|
|
104
|
+
RAM_USED_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$RAM_USED_FILTER'''))")
|
|
105
|
+
|
|
106
|
+
RAM_SIZE_FILTER="resource.type=\\"gce_instance\\" AND resource.labels.instance_id=\\"$GCP_INSTANCE_ID\\" AND metric.type=\\"compute.googleapis.com/instance/memory/balloon/ram_size\\""
|
|
107
|
+
RAM_SIZE_ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('''$RAM_SIZE_FILTER'''))")
|
|
108
|
+
|
|
109
|
+
# Calculate expected data points (GCP reports every 60s)
|
|
110
|
+
START_SEC=$(date -d "$START_TIME" +%s)
|
|
111
|
+
NOW_SEC=$(date +%s)
|
|
112
|
+
DURATION_SEC=$((NOW_SEC - START_SEC))
|
|
113
|
+
EXPECTED_POINTS=$((DURATION_SEC / 60))
|
|
114
|
+
[ "$EXPECTED_POINTS" -lt 1 ] && EXPECTED_POINTS=1
|
|
115
|
+
|
|
116
|
+
echo "Pipeline duration: \${DURATION_SEC}s, expecting ~\$EXPECTED_POINTS data points"
|
|
117
|
+
echo "Waiting for metrics data (GCP has ~3 min lag)..."
|
|
118
|
+
|
|
119
|
+
for i in $(seq 1 30); do
|
|
120
|
+
END_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
121
|
+
CPU_DATA=$(curl -s -H "Authorization: Bearer $TOKEN" \\
|
|
122
|
+
"https://monitoring.googleapis.com/v3/projects/$GCP_PROJECT_ID/timeSeries?filter=$CPU_ENCODED&interval.startTime=$START_TIME&interval.endTime=$END_TIME")
|
|
123
|
+
|
|
124
|
+
POINTS=$(echo "$CPU_DATA" | python3 -c "import sys,json; d=json.load(sys.stdin); print(sum(len(ts.get('points',[])) for ts in d.get('timeSeries',[])))" 2>/dev/null || echo "0")
|
|
125
|
+
|
|
126
|
+
if [ "$POINTS" -ge "$EXPECTED_POINTS" ]; then
|
|
127
|
+
echo "Got $POINTS CPU data points (expected $EXPECTED_POINTS)"
|
|
128
|
+
break
|
|
129
|
+
fi
|
|
130
|
+
echo "Waiting for data... got $POINTS/$EXPECTED_POINTS (attempt $i/30)"
|
|
131
|
+
sleep 10
|
|
132
|
+
done
|
|
133
|
+
|
|
134
|
+
# Fetch RAM data
|
|
135
|
+
RAM_USED_DATA=$(curl -s -H "Authorization: Bearer $TOKEN" \\
|
|
136
|
+
"https://monitoring.googleapis.com/v3/projects/$GCP_PROJECT_ID/timeSeries?filter=$RAM_USED_ENCODED&interval.startTime=$START_TIME&interval.endTime=$END_TIME")
|
|
137
|
+
|
|
138
|
+
RAM_SIZE_DATA=$(curl -s -H "Authorization: Bearer $TOKEN" \\
|
|
139
|
+
"https://monitoring.googleapis.com/v3/projects/$GCP_PROJECT_ID/timeSeries?filter=$RAM_SIZE_ENCODED&interval.startTime=$START_TIME&interval.endTime=$END_TIME")
|
|
140
|
+
|
|
141
|
+
# Save timeseries to files
|
|
142
|
+
echo "$CPU_DATA" > /tmp/cpu_timeseries.json
|
|
143
|
+
echo "$RAM_USED_DATA" > /tmp/ram_used_timeseries.json
|
|
144
|
+
echo "$RAM_SIZE_DATA" > /tmp/ram_size_timeseries.json
|
|
145
|
+
|
|
146
|
+
CMD="./node_modules/.bin/gitgreen --provider gcp --machine $MACHINE_TYPE --region $GCP_ZONE --cpu-timeseries /tmp/cpu_timeseries.json --ram-used-timeseries /tmp/ram_used_timeseries.json --ram-size-timeseries /tmp/ram_size_timeseries.json --out-md carbon-report.md --out-json carbon-report.json --no-gitlab"
|
|
147
|
+
[ -n "\${CARBON_BUDGET_GRAMS:-}" ] && CMD="$CMD --budget $CARBON_BUDGET_GRAMS"
|
|
148
|
+
[ "\${FAIL_ON_BUDGET:-}" = "true" ] && CMD="$CMD --fail-on-budget"
|
|
149
|
+
|
|
150
|
+
eval "$CMD"
|
|
151
|
+
cat carbon-report.md
|
|
152
|
+
artifacts:
|
|
153
|
+
paths:
|
|
154
|
+
- carbon-report.md
|
|
155
|
+
- carbon-report.json
|
|
156
|
+
expire_in: 30 days
|
|
157
|
+
`;
|
|
158
|
+
};
|
|
159
|
+
const runInit = async (opts = {}) => {
|
|
160
|
+
console.log((0, kleur_1.bold)('\nGitGreen Setup\n'));
|
|
161
|
+
// Step 1: GitLab Authentication
|
|
162
|
+
console.log((0, kleur_1.gray)('Step 1: GitLab Authentication'));
|
|
163
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
164
|
+
let auth;
|
|
165
|
+
const useGlab = hasGlab() && glabAuthenticated();
|
|
166
|
+
if (useGlab) {
|
|
167
|
+
console.log((0, kleur_1.green)('Using glab CLI (authenticated)'));
|
|
168
|
+
auth = { type: 'glab', baseUrl: 'https://gitlab.com' };
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
if (hasGlab()) {
|
|
172
|
+
console.log('glab found but not authenticated. Run: glab auth login');
|
|
173
|
+
console.log('Or provide a Personal Access Token.\n');
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.log('glab CLI not found. Using GitLab API.\n');
|
|
177
|
+
}
|
|
178
|
+
const { pat } = await (0, prompts_1.default)({
|
|
179
|
+
type: 'password',
|
|
180
|
+
name: 'pat',
|
|
181
|
+
message: 'GitLab Personal Access Token (api scope)'
|
|
182
|
+
});
|
|
183
|
+
if (!pat) {
|
|
184
|
+
console.log((0, kleur_1.red)('PAT required'));
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const { baseUrl } = await (0, prompts_1.default)({
|
|
188
|
+
type: 'text',
|
|
189
|
+
name: 'baseUrl',
|
|
190
|
+
message: 'GitLab URL',
|
|
191
|
+
initial: 'https://gitlab.com'
|
|
192
|
+
});
|
|
193
|
+
auth = { type: 'pat', pat, baseUrl };
|
|
194
|
+
}
|
|
195
|
+
// Step 2: Project
|
|
196
|
+
console.log((0, kleur_1.gray)('\nStep 2: GitLab Project'));
|
|
197
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
198
|
+
const detectedProject = getGitRemoteProject();
|
|
199
|
+
const { projectPath } = await (0, prompts_1.default)({
|
|
200
|
+
type: 'text',
|
|
201
|
+
name: 'projectPath',
|
|
202
|
+
message: 'Project (owner/repo)',
|
|
203
|
+
initial: detectedProject || ''
|
|
204
|
+
});
|
|
205
|
+
if (!projectPath) {
|
|
206
|
+
console.log((0, kleur_1.red)('Project required'));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
// Step 3: Cloud Provider
|
|
210
|
+
console.log((0, kleur_1.gray)('\nStep 3: Cloud Provider'));
|
|
211
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
212
|
+
const { provider } = await (0, prompts_1.default)({
|
|
213
|
+
type: 'select',
|
|
214
|
+
name: 'provider',
|
|
215
|
+
message: 'Runner cloud provider',
|
|
216
|
+
choices: [
|
|
217
|
+
{ title: 'GCP (Google Cloud)', value: 'gcp' },
|
|
218
|
+
{ title: 'AWS', value: 'aws' },
|
|
219
|
+
{ title: 'Manual configuration', value: 'manual' }
|
|
220
|
+
]
|
|
221
|
+
});
|
|
222
|
+
if (provider === 'aws') {
|
|
223
|
+
console.log((0, kleur_1.red)('\nAWS support coming soon.'));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
if (provider === 'manual') {
|
|
227
|
+
console.log((0, kleur_1.red)('\nManual configuration coming soon.'));
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
// Check if gcloud is available
|
|
231
|
+
let hasGcloud = false;
|
|
232
|
+
try {
|
|
233
|
+
(0, child_process_1.execSync)('gcloud --version', { stdio: 'ignore' });
|
|
234
|
+
hasGcloud = true;
|
|
235
|
+
}
|
|
236
|
+
catch { }
|
|
237
|
+
if (!hasGcloud) {
|
|
238
|
+
console.log((0, kleur_1.red)('gcloud CLI not found. Install it from https://cloud.google.com/sdk/docs/install'));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
// GCP Project Selection (needed for both runner provisioning and config)
|
|
242
|
+
console.log((0, kleur_1.gray)('\nStep 4: GCP Project'));
|
|
243
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
244
|
+
let gcpProjectId;
|
|
245
|
+
console.log((0, kleur_1.gray)('Fetching GCP projects...'));
|
|
246
|
+
let projects = [];
|
|
247
|
+
try {
|
|
248
|
+
const projectsJson = (0, child_process_1.execSync)('gcloud projects list --format="json"', { encoding: 'utf8' });
|
|
249
|
+
projects = JSON.parse(projectsJson).map((p) => ({ id: p.projectId, name: p.name }));
|
|
250
|
+
}
|
|
251
|
+
catch { }
|
|
252
|
+
if (projects.length > 0) {
|
|
253
|
+
const projectChoices = [
|
|
254
|
+
...projects.map(p => ({ title: `${p.id} (${p.name})`, value: p.id })),
|
|
255
|
+
{ title: 'Enter manually', value: '_manual_' }
|
|
256
|
+
];
|
|
257
|
+
const { selectedProject } = await (0, prompts_1.default)({
|
|
258
|
+
type: 'select',
|
|
259
|
+
name: 'selectedProject',
|
|
260
|
+
message: 'GCP Project',
|
|
261
|
+
choices: projectChoices
|
|
262
|
+
});
|
|
263
|
+
if (selectedProject === '_manual_') {
|
|
264
|
+
const { manualProject } = await (0, prompts_1.default)({
|
|
265
|
+
type: 'text',
|
|
266
|
+
name: 'manualProject',
|
|
267
|
+
message: 'GCP Project ID'
|
|
268
|
+
});
|
|
269
|
+
gcpProjectId = manualProject;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
gcpProjectId = selectedProject;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const { manualProject } = await (0, prompts_1.default)({
|
|
277
|
+
type: 'text',
|
|
278
|
+
name: 'manualProject',
|
|
279
|
+
message: 'GCP Project ID'
|
|
280
|
+
});
|
|
281
|
+
gcpProjectId = manualProject;
|
|
282
|
+
}
|
|
283
|
+
// Step 5: Runner Setup
|
|
284
|
+
console.log((0, kleur_1.gray)('\nStep 5: Runner Setup'));
|
|
285
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
286
|
+
const { runnerSetup } = await (0, prompts_1.default)({
|
|
287
|
+
type: 'select',
|
|
288
|
+
name: 'runnerSetup',
|
|
289
|
+
message: 'GitLab Runner',
|
|
290
|
+
choices: [
|
|
291
|
+
{ title: 'Use existing runner', value: 'existing' },
|
|
292
|
+
{ title: 'Provision new GCP runner VM', value: 'provision-gcp' },
|
|
293
|
+
{ title: 'Provision new AWS runner (coming soon)', value: 'provision-aws' }
|
|
294
|
+
]
|
|
295
|
+
});
|
|
296
|
+
if (runnerSetup === 'provision-aws') {
|
|
297
|
+
console.log((0, kleur_1.red)('\nAWS runner provisioning coming soon.'));
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
let gcpZone = '';
|
|
301
|
+
let gcpInstanceId = '';
|
|
302
|
+
let machineType = '';
|
|
303
|
+
let runnerTag = '';
|
|
304
|
+
if (runnerSetup === 'provision-gcp') {
|
|
305
|
+
console.log((0, kleur_1.gray)('\nProvisioning GCP Runner VM...'));
|
|
306
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
307
|
+
const { vmZone } = await (0, prompts_1.default)({
|
|
308
|
+
type: 'text',
|
|
309
|
+
name: 'vmZone',
|
|
310
|
+
message: 'GCP zone for runner VM',
|
|
311
|
+
initial: 'us-central1-a'
|
|
312
|
+
});
|
|
313
|
+
gcpZone = vmZone;
|
|
314
|
+
const { vmMachineType } = await (0, prompts_1.default)({
|
|
315
|
+
type: 'text',
|
|
316
|
+
name: 'vmMachineType',
|
|
317
|
+
message: 'Machine type',
|
|
318
|
+
initial: 'e2-standard-4'
|
|
319
|
+
});
|
|
320
|
+
machineType = vmMachineType;
|
|
321
|
+
const { vmName } = await (0, prompts_1.default)({
|
|
322
|
+
type: 'text',
|
|
323
|
+
name: 'vmName',
|
|
324
|
+
message: 'Runner VM name',
|
|
325
|
+
initial: 'gcp-carbon-runner'
|
|
326
|
+
});
|
|
327
|
+
const { vmTag } = await (0, prompts_1.default)({
|
|
328
|
+
type: 'text',
|
|
329
|
+
name: 'vmTag',
|
|
330
|
+
message: 'Runner tag (for routing CI jobs)',
|
|
331
|
+
initial: 'gcp'
|
|
332
|
+
});
|
|
333
|
+
runnerTag = vmTag;
|
|
334
|
+
// Create runner token via GitLab API
|
|
335
|
+
let runnerToken = '';
|
|
336
|
+
if (auth.type === 'glab') {
|
|
337
|
+
console.log((0, kleur_1.gray)('Creating runner token via GitLab API...'));
|
|
338
|
+
try {
|
|
339
|
+
const projectEncoded = encodeURIComponent(projectPath);
|
|
340
|
+
// Get project ID first
|
|
341
|
+
const projectInfoJson = (0, child_process_1.execSync)(`glab api "projects/${projectEncoded}"`, { encoding: 'utf8' });
|
|
342
|
+
const projectInfo = JSON.parse(projectInfoJson);
|
|
343
|
+
const projectId = projectInfo.id;
|
|
344
|
+
const createResult = (0, child_process_1.execSync)(`glab api --method POST "user/runners" -f runner_type=project_type -f project_id=${projectId} -f description="${vmName}" -f tag_list="${runnerTag}" -f run_untagged=true`, { encoding: 'utf8' });
|
|
345
|
+
const runnerData = JSON.parse(createResult);
|
|
346
|
+
runnerToken = runnerData.token;
|
|
347
|
+
console.log((0, kleur_1.green)(`Runner token created (ID: ${runnerData.id})`));
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
console.log((0, kleur_1.gray)('Could not create token via API, falling back to manual...'));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Fallback: ask user for token
|
|
354
|
+
if (!runnerToken) {
|
|
355
|
+
console.log((0, kleur_1.gray)('\nCreate a runner token in GitLab (check "Run untagged jobs"):'));
|
|
356
|
+
console.log((0, kleur_1.green)(` https://gitlab.com/${projectPath}/-/runners/new`));
|
|
357
|
+
const { manualToken } = await (0, prompts_1.default)({
|
|
358
|
+
type: 'password',
|
|
359
|
+
name: 'manualToken',
|
|
360
|
+
message: 'GitLab runner token (glrt-...)'
|
|
361
|
+
});
|
|
362
|
+
runnerToken = manualToken;
|
|
363
|
+
}
|
|
364
|
+
if (!runnerToken) {
|
|
365
|
+
console.log((0, kleur_1.red)('Runner token required'));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
// Create service account
|
|
369
|
+
const saName = 'gitlab-runner';
|
|
370
|
+
const saEmail = `${saName}@${gcpProjectId}.iam.gserviceaccount.com`;
|
|
371
|
+
console.log((0, kleur_1.gray)(`Creating service account: ${saEmail}`));
|
|
372
|
+
try {
|
|
373
|
+
(0, child_process_1.execSync)(`gcloud iam service-accounts create ${saName} --project=${gcpProjectId} --display-name="GitLab Runner" 2>/dev/null || true`, { stdio: 'pipe' });
|
|
374
|
+
}
|
|
375
|
+
catch { }
|
|
376
|
+
// Grant monitoring role
|
|
377
|
+
console.log((0, kleur_1.gray)('Granting Monitoring Viewer role...'));
|
|
378
|
+
try {
|
|
379
|
+
(0, child_process_1.execSync)(`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:${saEmail}" --role="roles/monitoring.viewer" --quiet 2>/dev/null`, { stdio: 'pipe' });
|
|
380
|
+
}
|
|
381
|
+
catch { }
|
|
382
|
+
// Check if VM exists
|
|
383
|
+
let vmExists = false;
|
|
384
|
+
try {
|
|
385
|
+
(0, child_process_1.execSync)(`gcloud compute instances describe ${vmName} --project=${gcpProjectId} --zone=${gcpZone} 2>/dev/null`, { stdio: 'pipe' });
|
|
386
|
+
vmExists = true;
|
|
387
|
+
}
|
|
388
|
+
catch { }
|
|
389
|
+
const startupScript = `#!/usr/bin/env bash
|
|
390
|
+
set -euo pipefail
|
|
391
|
+
apt-get update
|
|
392
|
+
apt-get install -y curl ca-certificates python3
|
|
393
|
+
|
|
394
|
+
# Install Node.js 20.x
|
|
395
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
|
396
|
+
apt-get install -y nodejs
|
|
397
|
+
|
|
398
|
+
# Install GitLab Runner
|
|
399
|
+
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | bash
|
|
400
|
+
apt-get install -y gitlab-runner
|
|
401
|
+
|
|
402
|
+
INSTANCE_ID=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/id)
|
|
403
|
+
TOKEN=$(curl -s -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/attributes/runner-token)
|
|
404
|
+
|
|
405
|
+
gitlab-runner register \\
|
|
406
|
+
--non-interactive \\
|
|
407
|
+
--url https://gitlab.com/ \\
|
|
408
|
+
--token "$TOKEN" \\
|
|
409
|
+
--executor shell \\
|
|
410
|
+
--description "${vmName}"
|
|
411
|
+
|
|
412
|
+
echo "$INSTANCE_ID" > /opt/gitlab-runner-instance-id.txt
|
|
413
|
+
|
|
414
|
+
systemctl enable gitlab-runner
|
|
415
|
+
systemctl start gitlab-runner
|
|
416
|
+
`;
|
|
417
|
+
const tmpFile = `/tmp/gitgreen-startup-${Date.now()}.sh`;
|
|
418
|
+
fs_1.default.writeFileSync(tmpFile, startupScript);
|
|
419
|
+
if (vmExists) {
|
|
420
|
+
console.log((0, kleur_1.gray)(`VM ${vmName} exists, re-registering runner...`));
|
|
421
|
+
try {
|
|
422
|
+
(0, child_process_1.execSync)(`gcloud compute instances add-metadata ${vmName} --project=${gcpProjectId} --zone=${gcpZone} --metadata=runner-token="${runnerToken}"`, { stdio: 'pipe' });
|
|
423
|
+
(0, child_process_1.execSync)(`gcloud compute ssh ${vmName} --project=${gcpProjectId} --zone=${gcpZone} --command="sudo gitlab-runner unregister --all-runners 2>/dev/null || true; INSTANCE_ID=\\$(curl -s -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/id); TOKEN=\\$(curl -s -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/runner-token); sudo gitlab-runner register --non-interactive --url https://gitlab.com/ --token \\$TOKEN --executor shell --description '${vmName}'; echo \\$INSTANCE_ID | sudo tee /opt/gitlab-runner-instance-id.txt; sudo systemctl restart gitlab-runner"`, { stdio: 'inherit' });
|
|
424
|
+
// Get instance ID
|
|
425
|
+
gcpInstanceId = (0, child_process_1.execSync)(`gcloud compute ssh ${vmName} --project=${gcpProjectId} --zone=${gcpZone} --command="cat /opt/gitlab-runner-instance-id.txt"`, { encoding: 'utf8' }).trim();
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
console.log((0, kleur_1.red)('Failed to re-register runner: ' + err.message));
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
console.log((0, kleur_1.gray)(`Creating VM ${vmName}...`));
|
|
434
|
+
try {
|
|
435
|
+
(0, child_process_1.execSync)(`gcloud compute instances create ${vmName} --project=${gcpProjectId} --zone=${gcpZone} --machine-type=${vmMachineType} --service-account=${saEmail} --scopes=https://www.googleapis.com/auth/cloud-platform --metadata=runner-token="${runnerToken}" --metadata-from-file=startup-script=${tmpFile} --image-family=debian-12 --image-project=debian-cloud`, { stdio: 'inherit' });
|
|
436
|
+
console.log((0, kleur_1.green)(`VM ${vmName} created. Runner will register in 2-3 minutes.`));
|
|
437
|
+
console.log((0, kleur_1.gray)('Waiting for runner to register...'));
|
|
438
|
+
// Wait for instance ID file
|
|
439
|
+
for (let i = 0; i < 30; i++) {
|
|
440
|
+
await new Promise(r => setTimeout(r, 10000));
|
|
441
|
+
try {
|
|
442
|
+
gcpInstanceId = (0, child_process_1.execSync)(`gcloud compute ssh ${vmName} --project=${gcpProjectId} --zone=${gcpZone} --command="cat /opt/gitlab-runner-instance-id.txt 2>/dev/null"`, { encoding: 'utf8' }).trim();
|
|
443
|
+
if (gcpInstanceId) {
|
|
444
|
+
console.log((0, kleur_1.green)(`Runner registered. Instance ID: ${gcpInstanceId}`));
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch { }
|
|
449
|
+
console.log((0, kleur_1.gray)(`Waiting... (${i + 1}/30)`));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
console.log((0, kleur_1.red)('Failed to create VM: ' + err.message));
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
fs_1.default.unlinkSync(tmpFile);
|
|
458
|
+
console.log((0, kleur_1.green)('Runner provisioning complete!'));
|
|
459
|
+
}
|
|
460
|
+
// Step 6: GCP Instance Configuration (if using existing runner)
|
|
461
|
+
console.log((0, kleur_1.gray)('\nStep 6: GCP Instance Configuration'));
|
|
462
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
463
|
+
// If we provisioned a runner, we already have the values
|
|
464
|
+
if (runnerSetup === 'provision-gcp' && gcpInstanceId && gcpZone && machineType) {
|
|
465
|
+
console.log((0, kleur_1.green)(`Using provisioned runner: ${gcpZone}, ${machineType}, instance-${gcpInstanceId}`));
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
// List instances in project
|
|
469
|
+
console.log((0, kleur_1.gray)('Fetching instances...'));
|
|
470
|
+
let instances = [];
|
|
471
|
+
try {
|
|
472
|
+
const instancesJson = (0, child_process_1.execSync)(`gcloud compute instances list --project=${gcpProjectId} --format="json"`, { encoding: 'utf8' });
|
|
473
|
+
instances = JSON.parse(instancesJson).map((i) => ({
|
|
474
|
+
name: i.name,
|
|
475
|
+
zone: i.zone.split('/').pop(),
|
|
476
|
+
id: i.id,
|
|
477
|
+
machineType: i.machineType.split('/').pop()
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
catch { }
|
|
481
|
+
if (instances.length > 0) {
|
|
482
|
+
const instanceChoices = [
|
|
483
|
+
...instances.map(i => ({
|
|
484
|
+
title: `${i.name} (${i.zone}, ${i.machineType})`,
|
|
485
|
+
value: i
|
|
486
|
+
})),
|
|
487
|
+
{ title: 'Enter manually', value: '_manual_' }
|
|
488
|
+
];
|
|
489
|
+
const { selectedInstance } = await (0, prompts_1.default)({
|
|
490
|
+
type: 'select',
|
|
491
|
+
name: 'selectedInstance',
|
|
492
|
+
message: 'GCP Instance',
|
|
493
|
+
choices: instanceChoices
|
|
494
|
+
});
|
|
495
|
+
if (selectedInstance === '_manual_') {
|
|
496
|
+
const { manualZone } = await (0, prompts_1.default)({
|
|
497
|
+
type: 'text',
|
|
498
|
+
name: 'manualZone',
|
|
499
|
+
message: 'GCP Zone (e.g., us-central1-a)'
|
|
500
|
+
});
|
|
501
|
+
gcpZone = manualZone;
|
|
502
|
+
const { manualInstanceId } = await (0, prompts_1.default)({
|
|
503
|
+
type: 'text',
|
|
504
|
+
name: 'manualInstanceId',
|
|
505
|
+
message: 'GCP Instance ID (numeric)'
|
|
506
|
+
});
|
|
507
|
+
gcpInstanceId = manualInstanceId;
|
|
508
|
+
const { manualMachineType } = await (0, prompts_1.default)({
|
|
509
|
+
type: 'text',
|
|
510
|
+
name: 'manualMachineType',
|
|
511
|
+
message: 'Machine type',
|
|
512
|
+
initial: 'e2-standard-4'
|
|
513
|
+
});
|
|
514
|
+
machineType = manualMachineType;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
gcpZone = selectedInstance.zone;
|
|
518
|
+
gcpInstanceId = selectedInstance.id;
|
|
519
|
+
machineType = selectedInstance.machineType;
|
|
520
|
+
const selectedVmName = selectedInstance.name;
|
|
521
|
+
console.log((0, kleur_1.green)(` Selected: ${selectedVmName}`));
|
|
522
|
+
console.log((0, kleur_1.gray)(` Zone: ${gcpZone}`));
|
|
523
|
+
console.log((0, kleur_1.gray)(` Instance ID: ${gcpInstanceId}`));
|
|
524
|
+
console.log((0, kleur_1.gray)(` Machine: ${machineType}`));
|
|
525
|
+
// Ask if they want to register this VM as a runner for this project
|
|
526
|
+
const { registerRunner } = await (0, prompts_1.default)({
|
|
527
|
+
type: 'confirm',
|
|
528
|
+
name: 'registerRunner',
|
|
529
|
+
message: 'Register this VM as a runner for this GitLab project?',
|
|
530
|
+
initial: true
|
|
531
|
+
});
|
|
532
|
+
if (registerRunner) {
|
|
533
|
+
const { existingRunnerTag } = await (0, prompts_1.default)({
|
|
534
|
+
type: 'text',
|
|
535
|
+
name: 'existingRunnerTag',
|
|
536
|
+
message: 'Runner tag (for routing CI jobs)',
|
|
537
|
+
initial: 'gcp'
|
|
538
|
+
});
|
|
539
|
+
runnerTag = existingRunnerTag;
|
|
540
|
+
let runnerToken = '';
|
|
541
|
+
// Try to create runner token via glab API first
|
|
542
|
+
if (auth.type === 'glab') {
|
|
543
|
+
console.log((0, kleur_1.gray)('Creating runner token via GitLab API...'));
|
|
544
|
+
try {
|
|
545
|
+
const projectEncoded = encodeURIComponent(projectPath);
|
|
546
|
+
// Get project ID first
|
|
547
|
+
const projectInfoJson = (0, child_process_1.execSync)(`glab api "projects/${projectEncoded}"`, { encoding: 'utf8' });
|
|
548
|
+
const projectInfo = JSON.parse(projectInfoJson);
|
|
549
|
+
const projectId = projectInfo.id;
|
|
550
|
+
const createResult = (0, child_process_1.execSync)(`glab api --method POST "user/runners" -f runner_type=project_type -f project_id=${projectId} -f description="${selectedVmName}" -f tag_list="${runnerTag}" -f run_untagged=true`, { encoding: 'utf8' });
|
|
551
|
+
const runnerData = JSON.parse(createResult);
|
|
552
|
+
runnerToken = runnerData.token;
|
|
553
|
+
console.log((0, kleur_1.green)(`Runner token created (ID: ${runnerData.id})`));
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
console.log((0, kleur_1.gray)('Could not create token via API, falling back to manual...'));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Fallback: ask user for token
|
|
560
|
+
if (!runnerToken) {
|
|
561
|
+
console.log((0, kleur_1.gray)('\nCreate a runner token in GitLab (check "Run untagged jobs"):'));
|
|
562
|
+
console.log((0, kleur_1.green)(` https://gitlab.com/${projectPath}/-/runners/new`));
|
|
563
|
+
const { manualToken } = await (0, prompts_1.default)({
|
|
564
|
+
type: 'password',
|
|
565
|
+
name: 'manualToken',
|
|
566
|
+
message: 'GitLab runner token (glrt-...)'
|
|
567
|
+
});
|
|
568
|
+
runnerToken = manualToken;
|
|
569
|
+
}
|
|
570
|
+
if (runnerToken) {
|
|
571
|
+
console.log((0, kleur_1.gray)('Registering runner on VM...'));
|
|
572
|
+
try {
|
|
573
|
+
// Update runner token metadata
|
|
574
|
+
(0, child_process_1.execSync)(`gcloud compute instances add-metadata ${selectedVmName} --project=${gcpProjectId} --zone=${gcpZone} --metadata=runner-token="${runnerToken}"`, { stdio: 'pipe' });
|
|
575
|
+
// SSH and re-register
|
|
576
|
+
(0, child_process_1.execSync)(`gcloud compute ssh ${selectedVmName} --project=${gcpProjectId} --zone=${gcpZone} --command="sudo gitlab-runner unregister --all-runners 2>/dev/null || true; TOKEN=\\$(curl -s -H 'Metadata-Flavor: Google' http://metadata.google.internal/computeMetadata/v1/instance/attributes/runner-token); sudo gitlab-runner register --non-interactive --url https://gitlab.com/ --token \\$TOKEN --executor shell --description '${selectedVmName}'; sudo systemctl restart gitlab-runner"`, { stdio: 'inherit' });
|
|
577
|
+
console.log((0, kleur_1.green)('Runner registered!'));
|
|
578
|
+
}
|
|
579
|
+
catch (err) {
|
|
580
|
+
console.log((0, kleur_1.red)('Failed to register runner: ' + err.message));
|
|
581
|
+
console.log((0, kleur_1.gray)('You may need to register manually or check VM access.'));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
console.log((0, kleur_1.gray)('No instances found in project'));
|
|
589
|
+
const { manualZone } = await (0, prompts_1.default)({
|
|
590
|
+
type: 'text',
|
|
591
|
+
name: 'manualZone',
|
|
592
|
+
message: 'GCP Zone (e.g., us-central1-a)'
|
|
593
|
+
});
|
|
594
|
+
gcpZone = manualZone;
|
|
595
|
+
const { manualInstanceId } = await (0, prompts_1.default)({
|
|
596
|
+
type: 'text',
|
|
597
|
+
name: 'manualInstanceId',
|
|
598
|
+
message: 'GCP Instance ID (numeric)'
|
|
599
|
+
});
|
|
600
|
+
gcpInstanceId = manualInstanceId;
|
|
601
|
+
const { manualMachineType } = await (0, prompts_1.default)({
|
|
602
|
+
type: 'text',
|
|
603
|
+
name: 'manualMachineType',
|
|
604
|
+
message: 'Machine type',
|
|
605
|
+
initial: 'e2-standard-4'
|
|
606
|
+
});
|
|
607
|
+
machineType = manualMachineType;
|
|
608
|
+
}
|
|
609
|
+
} // end else (use existing runner)
|
|
610
|
+
// Step 7: Service Account
|
|
611
|
+
console.log((0, kleur_1.gray)('\nStep 5: Service Account'));
|
|
612
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
613
|
+
let saKeyBase64;
|
|
614
|
+
const { keyMethod } = await (0, prompts_1.default)({
|
|
615
|
+
type: 'select',
|
|
616
|
+
name: 'keyMethod',
|
|
617
|
+
message: 'Service account key',
|
|
618
|
+
choices: [
|
|
619
|
+
{ title: 'Create with gcloud (recommended)', value: 'gcloud' },
|
|
620
|
+
{ title: 'Use existing key file', value: 'file' }
|
|
621
|
+
]
|
|
622
|
+
});
|
|
623
|
+
if (keyMethod === 'gcloud') {
|
|
624
|
+
const defaultSa = `gitlab-runner@${gcpProjectId}.iam.gserviceaccount.com`;
|
|
625
|
+
const { saEmail } = await (0, prompts_1.default)({
|
|
626
|
+
type: 'text',
|
|
627
|
+
name: 'saEmail',
|
|
628
|
+
message: 'Service account email',
|
|
629
|
+
initial: defaultSa
|
|
630
|
+
});
|
|
631
|
+
// Check if SA exists, create if not
|
|
632
|
+
console.log((0, kleur_1.gray)('Checking service account...'));
|
|
633
|
+
try {
|
|
634
|
+
(0, child_process_1.execSync)(`gcloud iam service-accounts describe ${saEmail} --project=${gcpProjectId}`, { stdio: 'ignore' });
|
|
635
|
+
console.log((0, kleur_1.gray)('Service account exists'));
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
console.log((0, kleur_1.gray)('Creating service account...'));
|
|
639
|
+
const saName = saEmail.split('@')[0];
|
|
640
|
+
(0, child_process_1.execSync)(`gcloud iam service-accounts create ${saName} --project=${gcpProjectId} --display-name="GitGreen Runner"`, { stdio: 'inherit' });
|
|
641
|
+
}
|
|
642
|
+
// Grant monitoring viewer role
|
|
643
|
+
console.log((0, kleur_1.gray)('Granting Monitoring Viewer role...'));
|
|
644
|
+
try {
|
|
645
|
+
(0, child_process_1.execSync)(`gcloud projects add-iam-policy-binding ${gcpProjectId} --member="serviceAccount:${saEmail}" --role="roles/monitoring.viewer" --quiet`, { stdio: 'ignore' });
|
|
646
|
+
}
|
|
647
|
+
catch { }
|
|
648
|
+
// Create key
|
|
649
|
+
console.log((0, kleur_1.gray)('Creating key...'));
|
|
650
|
+
const tmpKeyPath = `/tmp/gitgreen-sa-key-${Date.now()}.json`;
|
|
651
|
+
(0, child_process_1.execSync)(`gcloud iam service-accounts keys create ${tmpKeyPath} --iam-account=${saEmail}`, { stdio: 'inherit' });
|
|
652
|
+
saKeyBase64 = fs_1.default.readFileSync(tmpKeyPath).toString('base64');
|
|
653
|
+
fs_1.default.unlinkSync(tmpKeyPath);
|
|
654
|
+
console.log((0, kleur_1.green)('Service account key created'));
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
const { saKeyPath } = await (0, prompts_1.default)({
|
|
658
|
+
type: 'text',
|
|
659
|
+
name: 'saKeyPath',
|
|
660
|
+
message: 'Path to service account JSON key'
|
|
661
|
+
});
|
|
662
|
+
if (!fs_1.default.existsSync(saKeyPath)) {
|
|
663
|
+
console.log((0, kleur_1.red)('File not found: ' + saKeyPath));
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
saKeyBase64 = fs_1.default.readFileSync(saKeyPath).toString('base64');
|
|
667
|
+
}
|
|
668
|
+
// Step 6: Electricity Maps API
|
|
669
|
+
console.log((0, kleur_1.gray)('\nStep 6: Electricity Maps API'));
|
|
670
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
671
|
+
console.log((0, kleur_1.gray)('Get free key: https://api-portal.electricitymaps.com'));
|
|
672
|
+
const { electricityMapsKey } = await (0, prompts_1.default)({
|
|
673
|
+
type: 'password',
|
|
674
|
+
name: 'electricityMapsKey',
|
|
675
|
+
message: 'Electricity Maps API Key'
|
|
676
|
+
});
|
|
677
|
+
if (!electricityMapsKey) {
|
|
678
|
+
console.log((0, kleur_1.red)('API key required'));
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
// Step 7: Optional
|
|
682
|
+
console.log((0, kleur_1.gray)('\nStep 7: Optional Settings'));
|
|
683
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
684
|
+
console.log((0, kleur_1.gray)('Set a carbon budget to track emissions against a limit.'));
|
|
685
|
+
console.log((0, kleur_1.gray)('Example: 10 grams CO2e per job. Leave empty to skip.\n'));
|
|
686
|
+
const { carbonBudget } = await (0, prompts_1.default)({
|
|
687
|
+
type: 'number',
|
|
688
|
+
name: 'carbonBudget',
|
|
689
|
+
message: 'Carbon budget (grams CO2e)',
|
|
690
|
+
initial: undefined
|
|
691
|
+
});
|
|
692
|
+
let failOnBudget = false;
|
|
693
|
+
if (carbonBudget) {
|
|
694
|
+
const { shouldFail } = await (0, prompts_1.default)({
|
|
695
|
+
type: 'confirm',
|
|
696
|
+
name: 'shouldFail',
|
|
697
|
+
message: 'Fail CI job if over budget?',
|
|
698
|
+
initial: false
|
|
699
|
+
});
|
|
700
|
+
failOnBudget = shouldFail;
|
|
701
|
+
}
|
|
702
|
+
// Step 8: Set Variables
|
|
703
|
+
console.log((0, kleur_1.gray)('\nStep 8: Setting CI/CD Variables'));
|
|
704
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
705
|
+
const variables = [
|
|
706
|
+
{ key: 'GCP_PROJECT_ID', value: gcpProjectId, masked: false },
|
|
707
|
+
{ key: 'GCP_ZONE', value: gcpZone, masked: false },
|
|
708
|
+
{ key: 'GCP_INSTANCE_ID', value: gcpInstanceId, masked: false },
|
|
709
|
+
{ key: 'MACHINE_TYPE', value: machineType, masked: false },
|
|
710
|
+
{ key: 'GCP_SA_KEY_BASE64', value: saKeyBase64, masked: true },
|
|
711
|
+
{ key: 'ELECTRICITY_MAPS_API_KEY', value: electricityMapsKey, masked: true }
|
|
712
|
+
];
|
|
713
|
+
if (carbonBudget) {
|
|
714
|
+
variables.push({ key: 'CARBON_BUDGET_GRAMS', value: String(carbonBudget), masked: false });
|
|
715
|
+
}
|
|
716
|
+
if (failOnBudget) {
|
|
717
|
+
variables.push({ key: 'FAIL_ON_BUDGET', value: 'true', masked: false });
|
|
718
|
+
}
|
|
719
|
+
for (const v of variables) {
|
|
720
|
+
const ok = await setVariable(auth, projectPath, v.key, v.value, v.masked);
|
|
721
|
+
if (ok) {
|
|
722
|
+
console.log((0, kleur_1.green)(' Set ' + v.key));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log((0, kleur_1.red)(' Failed: ' + v.key));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// Step 9: Generate CI job
|
|
729
|
+
console.log((0, kleur_1.gray)('\nStep 9: CI Configuration'));
|
|
730
|
+
console.log((0, kleur_1.gray)('─'.repeat(40)));
|
|
731
|
+
// Only prompt for runner tag if not already set from provisioning
|
|
732
|
+
if (!runnerTag) {
|
|
733
|
+
const { inputRunnerTag } = await (0, prompts_1.default)({
|
|
734
|
+
type: 'text',
|
|
735
|
+
name: 'inputRunnerTag',
|
|
736
|
+
message: 'Runner tag (leave empty for any runner)',
|
|
737
|
+
initial: ''
|
|
738
|
+
});
|
|
739
|
+
runnerTag = inputRunnerTag || '';
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
console.log((0, kleur_1.gray)(`Using runner tag: ${runnerTag}`));
|
|
743
|
+
}
|
|
744
|
+
const { addCiJob } = await (0, prompts_1.default)({
|
|
745
|
+
type: 'confirm',
|
|
746
|
+
name: 'addCiJob',
|
|
747
|
+
message: 'Add job to .gitlab-ci.yml?',
|
|
748
|
+
initial: true
|
|
749
|
+
});
|
|
750
|
+
const ciJob = generateCiJob(runnerTag || undefined);
|
|
751
|
+
if (addCiJob) {
|
|
752
|
+
const ciPath = path_1.default.join(process.cwd(), '.gitlab-ci.yml');
|
|
753
|
+
if (fs_1.default.existsSync(ciPath)) {
|
|
754
|
+
// Backup existing file before modifying
|
|
755
|
+
const backupPath = `/tmp/gitlab-ci-backup-${Date.now()}.yml`;
|
|
756
|
+
const existing = fs_1.default.readFileSync(ciPath, 'utf8');
|
|
757
|
+
fs_1.default.writeFileSync(backupPath, existing);
|
|
758
|
+
console.log((0, kleur_1.gray)(`Backup saved to: ${backupPath}`));
|
|
759
|
+
fs_1.default.writeFileSync(ciPath, existing + '\n' + ciJob);
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
fs_1.default.writeFileSync(ciPath, 'stages:\n - test\n\n' + ciJob);
|
|
763
|
+
}
|
|
764
|
+
console.log((0, kleur_1.green)('Updated .gitlab-ci.yml'));
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
console.log('\nCI job snippet:\n');
|
|
768
|
+
console.log(ciJob);
|
|
769
|
+
}
|
|
770
|
+
console.log((0, kleur_1.bold)('\nDone'));
|
|
771
|
+
console.log('Commit and push to trigger the pipeline.\n');
|
|
772
|
+
};
|
|
773
|
+
exports.runInit = runInit;
|