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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
{
|
|
2
|
+
"metadata": {
|
|
3
|
+
"generatedAt": "2025-06-14T16:09:07.455Z",
|
|
4
|
+
"methodology": "Real-time Electricity Maps API with industry standard PUE",
|
|
5
|
+
"availableZones": [
|
|
6
|
+
"US-CENT-SWPP"
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"regions": {
|
|
10
|
+
"us-central1": {
|
|
11
|
+
"zone": "US-CENT-SWPP",
|
|
12
|
+
"industryStandardPUE": 1.1,
|
|
13
|
+
"realTimeCarbonIntensity": 492,
|
|
14
|
+
"googleOfficialIntensity": 430,
|
|
15
|
+
"cfe": 0.95
|
|
16
|
+
},
|
|
17
|
+
"us-east1": {
|
|
18
|
+
"zone": "FALLBACK",
|
|
19
|
+
"industryStandardPUE": 1.1,
|
|
20
|
+
"error": "No matching zone found in available zones"
|
|
21
|
+
},
|
|
22
|
+
"us-east4": {
|
|
23
|
+
"zone": "FALLBACK",
|
|
24
|
+
"industryStandardPUE": 1.1,
|
|
25
|
+
"error": "No matching zone found in available zones"
|
|
26
|
+
},
|
|
27
|
+
"us-west1": {
|
|
28
|
+
"zone": "FALLBACK",
|
|
29
|
+
"industryStandardPUE": 1.1,
|
|
30
|
+
"error": "No matching zone found in available zones"
|
|
31
|
+
},
|
|
32
|
+
"us-west2": {
|
|
33
|
+
"zone": "FALLBACK",
|
|
34
|
+
"industryStandardPUE": 1.1,
|
|
35
|
+
"error": "No matching zone found in available zones"
|
|
36
|
+
},
|
|
37
|
+
"us-west3": {
|
|
38
|
+
"zone": "FALLBACK",
|
|
39
|
+
"industryStandardPUE": 1.1,
|
|
40
|
+
"error": "No matching zone found in available zones"
|
|
41
|
+
},
|
|
42
|
+
"us-west4": {
|
|
43
|
+
"zone": "FALLBACK",
|
|
44
|
+
"industryStandardPUE": 1.1,
|
|
45
|
+
"error": "No matching zone found in available zones"
|
|
46
|
+
},
|
|
47
|
+
"us-south1": {
|
|
48
|
+
"zone": "FALLBACK",
|
|
49
|
+
"industryStandardPUE": 1.1,
|
|
50
|
+
"error": "No matching zone found in available zones"
|
|
51
|
+
},
|
|
52
|
+
"europe-west1": {
|
|
53
|
+
"zone": "FALLBACK",
|
|
54
|
+
"industryStandardPUE": 1.1,
|
|
55
|
+
"error": "No matching zone found in available zones"
|
|
56
|
+
},
|
|
57
|
+
"europe-west2": {
|
|
58
|
+
"zone": "FALLBACK",
|
|
59
|
+
"industryStandardPUE": 1.1,
|
|
60
|
+
"error": "No matching zone found in available zones"
|
|
61
|
+
},
|
|
62
|
+
"europe-west3": {
|
|
63
|
+
"zone": "FALLBACK",
|
|
64
|
+
"industryStandardPUE": 1.1,
|
|
65
|
+
"error": "No matching zone found in available zones"
|
|
66
|
+
},
|
|
67
|
+
"europe-west4": {
|
|
68
|
+
"zone": "FALLBACK",
|
|
69
|
+
"industryStandardPUE": 1.1,
|
|
70
|
+
"error": "No matching zone found in available zones"
|
|
71
|
+
},
|
|
72
|
+
"europe-west6": {
|
|
73
|
+
"zone": "FALLBACK",
|
|
74
|
+
"industryStandardPUE": 1.1,
|
|
75
|
+
"error": "No matching zone found in available zones"
|
|
76
|
+
},
|
|
77
|
+
"europe-west8": {
|
|
78
|
+
"zone": "FALLBACK",
|
|
79
|
+
"industryStandardPUE": 1.1,
|
|
80
|
+
"error": "No matching zone found in available zones"
|
|
81
|
+
},
|
|
82
|
+
"europe-west9": {
|
|
83
|
+
"zone": "FALLBACK",
|
|
84
|
+
"industryStandardPUE": 1.1,
|
|
85
|
+
"error": "No matching zone found in available zones"
|
|
86
|
+
},
|
|
87
|
+
"europe-west10": {
|
|
88
|
+
"zone": "FALLBACK",
|
|
89
|
+
"industryStandardPUE": 1.1,
|
|
90
|
+
"error": "No matching zone found in available zones"
|
|
91
|
+
},
|
|
92
|
+
"europe-west12": {
|
|
93
|
+
"zone": "FALLBACK",
|
|
94
|
+
"industryStandardPUE": 1.1,
|
|
95
|
+
"error": "No matching zone found in available zones"
|
|
96
|
+
},
|
|
97
|
+
"europe-north1": {
|
|
98
|
+
"zone": "FALLBACK",
|
|
99
|
+
"industryStandardPUE": 1.1,
|
|
100
|
+
"error": "No matching zone found in available zones"
|
|
101
|
+
},
|
|
102
|
+
"europe-central2": {
|
|
103
|
+
"zone": "FALLBACK",
|
|
104
|
+
"industryStandardPUE": 1.1,
|
|
105
|
+
"error": "No matching zone found in available zones"
|
|
106
|
+
},
|
|
107
|
+
"europe-southwest1": {
|
|
108
|
+
"zone": "FALLBACK",
|
|
109
|
+
"industryStandardPUE": 1.1,
|
|
110
|
+
"error": "No matching zone found in available zones"
|
|
111
|
+
},
|
|
112
|
+
"asia-east1": {
|
|
113
|
+
"zone": "FALLBACK",
|
|
114
|
+
"industryStandardPUE": 1.1,
|
|
115
|
+
"error": "No matching zone found in available zones"
|
|
116
|
+
},
|
|
117
|
+
"asia-east2": {
|
|
118
|
+
"zone": "FALLBACK",
|
|
119
|
+
"industryStandardPUE": 1.1,
|
|
120
|
+
"error": "No matching zone found in available zones"
|
|
121
|
+
},
|
|
122
|
+
"asia-northeast1": {
|
|
123
|
+
"zone": "FALLBACK",
|
|
124
|
+
"industryStandardPUE": 1.1,
|
|
125
|
+
"error": "No matching zone found in available zones"
|
|
126
|
+
},
|
|
127
|
+
"asia-northeast2": {
|
|
128
|
+
"zone": "FALLBACK",
|
|
129
|
+
"industryStandardPUE": 1.1,
|
|
130
|
+
"error": "No matching zone found in available zones"
|
|
131
|
+
},
|
|
132
|
+
"asia-northeast3": {
|
|
133
|
+
"zone": "FALLBACK",
|
|
134
|
+
"industryStandardPUE": 1.1,
|
|
135
|
+
"error": "No matching zone found in available zones"
|
|
136
|
+
},
|
|
137
|
+
"asia-south1": {
|
|
138
|
+
"zone": "FALLBACK",
|
|
139
|
+
"industryStandardPUE": 1.1,
|
|
140
|
+
"error": "No matching zone found in available zones"
|
|
141
|
+
},
|
|
142
|
+
"asia-south2": {
|
|
143
|
+
"zone": "FALLBACK",
|
|
144
|
+
"industryStandardPUE": 1.1,
|
|
145
|
+
"error": "No matching zone found in available zones"
|
|
146
|
+
},
|
|
147
|
+
"asia-southeast1": {
|
|
148
|
+
"zone": "FALLBACK",
|
|
149
|
+
"industryStandardPUE": 1.1,
|
|
150
|
+
"error": "No matching zone found in available zones"
|
|
151
|
+
},
|
|
152
|
+
"asia-southeast2": {
|
|
153
|
+
"zone": "FALLBACK",
|
|
154
|
+
"industryStandardPUE": 1.1,
|
|
155
|
+
"error": "No matching zone found in available zones"
|
|
156
|
+
},
|
|
157
|
+
"australia-southeast1": {
|
|
158
|
+
"zone": "FALLBACK",
|
|
159
|
+
"industryStandardPUE": 1.1,
|
|
160
|
+
"error": "No matching zone found in available zones"
|
|
161
|
+
},
|
|
162
|
+
"australia-southeast2": {
|
|
163
|
+
"zone": "FALLBACK",
|
|
164
|
+
"industryStandardPUE": 1.1,
|
|
165
|
+
"error": "No matching zone found in available zones"
|
|
166
|
+
},
|
|
167
|
+
"southamerica-east1": {
|
|
168
|
+
"zone": "FALLBACK",
|
|
169
|
+
"industryStandardPUE": 1.1,
|
|
170
|
+
"error": "No matching zone found in available zones"
|
|
171
|
+
},
|
|
172
|
+
"northamerica-northeast1": {
|
|
173
|
+
"zone": "FALLBACK",
|
|
174
|
+
"industryStandardPUE": 1.1,
|
|
175
|
+
"error": "No matching zone found in available zones"
|
|
176
|
+
},
|
|
177
|
+
"northamerica-northeast2": {
|
|
178
|
+
"zone": "FALLBACK",
|
|
179
|
+
"industryStandardPUE": 1.1,
|
|
180
|
+
"error": "No matching zone found in available zones"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const kleur_1 = require("kleur");
|
|
10
|
+
const asciichart_1 = __importDefault(require("asciichart"));
|
|
11
|
+
const index_1 = require("./index");
|
|
12
|
+
const init_1 = require("./init");
|
|
13
|
+
const program = new commander_1.Command();
|
|
14
|
+
function parseGcpTimeseries(filePath) {
|
|
15
|
+
const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
|
|
16
|
+
const points = [];
|
|
17
|
+
for (const ts of raw.timeSeries || []) {
|
|
18
|
+
for (const p of ts.points || []) {
|
|
19
|
+
const timestamp = p.interval?.endTime || p.interval?.startTime;
|
|
20
|
+
let value;
|
|
21
|
+
if (p.value?.doubleValue !== undefined) {
|
|
22
|
+
value = p.value.doubleValue;
|
|
23
|
+
}
|
|
24
|
+
else if (p.value?.int64Value !== undefined) {
|
|
25
|
+
value = Number(p.value.int64Value);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
points.push({ timestamp, value });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return points;
|
|
34
|
+
}
|
|
35
|
+
const runCalculate = async (opts) => {
|
|
36
|
+
const gitlabDisabled = opts.gitlab === false;
|
|
37
|
+
if (!opts.cpuTimeseries || !opts.ramUsedTimeseries || !opts.ramSizeTimeseries) {
|
|
38
|
+
console.error((0, kleur_1.red)('Error: --cpu-timeseries, --ram-used-timeseries, and --ram-size-timeseries are required'));
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const cpuTimeseries = parseGcpTimeseries(opts.cpuTimeseries);
|
|
43
|
+
const ramUsedTimeseries = parseGcpTimeseries(opts.ramUsedTimeseries);
|
|
44
|
+
const ramSizeTimeseries = parseGcpTimeseries(opts.ramSizeTimeseries);
|
|
45
|
+
console.log((0, kleur_1.gray)(`Loaded ${cpuTimeseries.length} CPU points, ${ramUsedTimeseries.length} RAM points`));
|
|
46
|
+
// Sort by timestamp for charts
|
|
47
|
+
const cpuSorted = [...cpuTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
48
|
+
const ramSorted = [...ramUsedTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
49
|
+
const ramSizeSorted = [...ramSizeTimeseries].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
50
|
+
// CPU chart (only if more than 1 point)
|
|
51
|
+
const cpuValues = cpuSorted.map(p => p.value * 100);
|
|
52
|
+
if (cpuValues.length > 1) {
|
|
53
|
+
console.log((0, kleur_1.bold)('\nCPU Utilization (%)'));
|
|
54
|
+
console.log(asciichart_1.default.plot(cpuValues, { height: 8, format: (x) => x.toFixed(1).padStart(6) }));
|
|
55
|
+
}
|
|
56
|
+
// RAM chart (only if more than 1 point)
|
|
57
|
+
if (ramSorted.length > 1 && ramSizeSorted.length > 0) {
|
|
58
|
+
const totalRamGb = ramSizeSorted[0].value / (1024 * 1024 * 1024);
|
|
59
|
+
const ramPercent = ramSorted.map(p => (p.value / (1024 * 1024 * 1024) / totalRamGb) * 100);
|
|
60
|
+
console.log((0, kleur_1.bold)('\nRAM Utilization (%)'));
|
|
61
|
+
console.log(asciichart_1.default.plot(ramPercent, { height: 8, format: (x) => x.toFixed(1).padStart(6) }));
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const { result, budget, markdown } = await (0, index_1.calculate)({
|
|
65
|
+
provider: opts.provider,
|
|
66
|
+
machineType: opts.machine,
|
|
67
|
+
region: opts.region,
|
|
68
|
+
cpuTimeseries,
|
|
69
|
+
ramUsedTimeseries,
|
|
70
|
+
ramSizeTimeseries,
|
|
71
|
+
carbonBudgetGrams: opts.budget,
|
|
72
|
+
failOnBreach: opts.failOnBudget,
|
|
73
|
+
runnerTags: opts.runnerTags ? String(opts.runnerTags).split(/[,\s]+/).filter(Boolean) : (0, index_1.readRunnerTagsFromEnv)(),
|
|
74
|
+
gitlab: gitlabDisabled ? false : { postMergeRequestNote: Boolean(opts.postNote) },
|
|
75
|
+
writeMarkdownPath: opts.outMd,
|
|
76
|
+
writeJsonPath: opts.outJson
|
|
77
|
+
});
|
|
78
|
+
const statusColor = budget.overBudget ? kleur_1.red : kleur_1.green;
|
|
79
|
+
const totalEnergyWh = (result.cpuEnergyKwh + result.ramEnergyKwh) * 1000;
|
|
80
|
+
console.log((0, kleur_1.bold)('\nCarbon Report'));
|
|
81
|
+
console.log((0, kleur_1.gray)('------------------------------'));
|
|
82
|
+
console.log(`Provider: ${result.provider.toUpperCase()}`);
|
|
83
|
+
console.log(`Region: ${result.region}`);
|
|
84
|
+
console.log(`Machine: ${result.machineType}`);
|
|
85
|
+
console.log(`Runtime: ${(result.runtimeHours * 3600).toFixed(0)}s`);
|
|
86
|
+
console.log(`Energy: ${totalEnergyWh.toFixed(3)} Wh`);
|
|
87
|
+
console.log(`Total: ${statusColor(result.totalEmissions.toFixed(3) + ' gCO2e')}`);
|
|
88
|
+
if (budget.budgetConfigured && budget.limitGrams) {
|
|
89
|
+
console.log(`Budget: ${budget.limitGrams} gCO2e (${budget.overBudget ? 'over' : 'within'})`);
|
|
90
|
+
}
|
|
91
|
+
if (opts.outMd || opts.outJson) {
|
|
92
|
+
console.log((0, kleur_1.gray)('Artifacts:'));
|
|
93
|
+
if (opts.outMd)
|
|
94
|
+
console.log(`- markdown: ${opts.outMd}`);
|
|
95
|
+
if (opts.outJson)
|
|
96
|
+
console.log(`- json: ${opts.outJson}`);
|
|
97
|
+
}
|
|
98
|
+
if (!opts.outMd && !opts.outJson) {
|
|
99
|
+
console.log('\n' + markdown);
|
|
100
|
+
}
|
|
101
|
+
if (budget.overBudget && opts.failOnBudget) {
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error((0, kleur_1.red)('Error:'), err?.message || err);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
program
|
|
111
|
+
.name('gitgreen')
|
|
112
|
+
.description('GitGreen carbon calculator using real timeseries metrics')
|
|
113
|
+
.option('-p, --provider <provider>', 'Cloud provider (gcp)')
|
|
114
|
+
.option('-m, --machine <type>', 'Machine type (e.g., e2-standard-4)')
|
|
115
|
+
.option('-r, --region <region>', 'Region/zone (e.g., us-central1-a)')
|
|
116
|
+
.option('--cpu-timeseries <path>', 'Path to CPU timeseries JSON (GCP Monitoring format)')
|
|
117
|
+
.option('--ram-used-timeseries <path>', 'Path to RAM used timeseries JSON')
|
|
118
|
+
.option('--ram-size-timeseries <path>', 'Path to RAM size timeseries JSON')
|
|
119
|
+
.option('--budget <grams>', 'Carbon budget in grams CO2e', v => Number(v))
|
|
120
|
+
.option('--fail-on-budget', 'Exit non-zero when budget exceeded')
|
|
121
|
+
.option('--out-md <path>', 'Write markdown report')
|
|
122
|
+
.option('--out-json <path>', 'Write JSON report')
|
|
123
|
+
.option('--runner-tags <tags>', 'Comma-separated runner tags')
|
|
124
|
+
.option('--no-gitlab', 'Disable GitLab context auto-detection')
|
|
125
|
+
.option('--post-note', 'Post MR note when GitLab context is present')
|
|
126
|
+
.showHelpAfterError()
|
|
127
|
+
.action(async (opts) => {
|
|
128
|
+
await runCalculate(opts);
|
|
129
|
+
});
|
|
130
|
+
program
|
|
131
|
+
.command('init')
|
|
132
|
+
.description('Interactive setup for GitLab CI integration')
|
|
133
|
+
.option('--yes', 'Accept defaults when possible')
|
|
134
|
+
.action(async (cmdOpts) => {
|
|
135
|
+
await (0, init_1.runInit)({ assumeYes: Boolean(cmdOpts.yes) });
|
|
136
|
+
});
|
|
137
|
+
program.parseAsync(process.argv);
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
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.config = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
10
|
+
dotenv_1.default.config();
|
|
11
|
+
const numberFromEnv = (value, fallback) => {
|
|
12
|
+
if (!value)
|
|
13
|
+
return fallback;
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
16
|
+
};
|
|
17
|
+
const resolveDataDir = () => {
|
|
18
|
+
if (process.env.DATA_DIR) {
|
|
19
|
+
return path_1.default.resolve(process.env.DATA_DIR);
|
|
20
|
+
}
|
|
21
|
+
const bundled = path_1.default.join(__dirname, '..', 'data');
|
|
22
|
+
if (fs_1.default.existsSync(bundled)) {
|
|
23
|
+
return bundled;
|
|
24
|
+
}
|
|
25
|
+
const implData = path_1.default.join(__dirname, '..', '..', 'implementation', 'data');
|
|
26
|
+
if (fs_1.default.existsSync(implData)) {
|
|
27
|
+
return implData;
|
|
28
|
+
}
|
|
29
|
+
const cwdData = path_1.default.join(process.cwd(), 'data');
|
|
30
|
+
if (fs_1.default.existsSync(cwdData)) {
|
|
31
|
+
return cwdData;
|
|
32
|
+
}
|
|
33
|
+
return path_1.default.join(__dirname, '..', 'data');
|
|
34
|
+
};
|
|
35
|
+
exports.config = {
|
|
36
|
+
electricityMapsApiKey: process.env.ELECTRICITY_MAPS_API_KEY || '',
|
|
37
|
+
electricityMapsBaseUrl: process.env.ELECTRICITY_MAPS_BASE_URL || 'https://api.electricitymaps.com/v3',
|
|
38
|
+
gitlabBaseUrl: process.env.GITLAB_BASE_URL || 'https://gitlab.com',
|
|
39
|
+
defaultProvider: process.env.DEFAULT_PROVIDER || 'gcp',
|
|
40
|
+
fallbackPue: numberFromEnv(process.env.PUE_FALLBACK, 1.2),
|
|
41
|
+
gcpProjectId: process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT || '',
|
|
42
|
+
dataDir: resolveDataDir()
|
|
43
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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.readRunnerTagsFromEnv = exports.calculate = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const carbon_calculator_1 = require("./lib/carbon/carbon-calculator");
|
|
11
|
+
const intensity_provider_1 = require("./lib/carbon/intensity-provider");
|
|
12
|
+
const power_profile_repository_1 = require("./lib/carbon/power-profile-repository");
|
|
13
|
+
const zone_mapper_1 = require("./lib/carbon/zone-mapper");
|
|
14
|
+
const gitlab_client_1 = require("./lib/gitlab/gitlab-client");
|
|
15
|
+
const report_formatter_1 = require("./lib/gitlab/report-formatter");
|
|
16
|
+
const createDependencies = () => {
|
|
17
|
+
const powerProfileRepository = new power_profile_repository_1.PowerProfileRepository(config_1.config.dataDir);
|
|
18
|
+
const zoneMapper = new zone_mapper_1.ZoneMapper(config_1.config.dataDir, config_1.config.fallbackPue);
|
|
19
|
+
const intensityProvider = new intensity_provider_1.IntensityProvider(config_1.config.electricityMapsBaseUrl, config_1.config.electricityMapsApiKey);
|
|
20
|
+
const calculator = new carbon_calculator_1.CarbonCalculator(powerProfileRepository, zoneMapper, intensityProvider);
|
|
21
|
+
const gitlabClient = new gitlab_client_1.GitLabClient(config_1.config.gitlabBaseUrl);
|
|
22
|
+
return { calculator, gitlabClient };
|
|
23
|
+
};
|
|
24
|
+
const resolveGitLabContextFromEnv = (overrides) => {
|
|
25
|
+
const jobToken = overrides?.jobToken || process.env.CI_JOB_TOKEN;
|
|
26
|
+
const projectId = overrides?.projectId ?? (process.env.CI_PROJECT_ID ? Number(process.env.CI_PROJECT_ID) : undefined);
|
|
27
|
+
const jobId = overrides?.jobId ?? (process.env.CI_JOB_ID ? Number(process.env.CI_JOB_ID) : undefined);
|
|
28
|
+
const pipelineId = overrides?.pipelineId ?? (process.env.CI_PIPELINE_ID ? Number(process.env.CI_PIPELINE_ID) : undefined);
|
|
29
|
+
const mergeRequestIid = overrides?.mergeRequestIid ?? (process.env.CI_MERGE_REQUEST_IID ? Number(process.env.CI_MERGE_REQUEST_IID) : undefined);
|
|
30
|
+
const serverUrl = overrides?.serverUrl || process.env.CI_SERVER_URL || config_1.config.gitlabBaseUrl;
|
|
31
|
+
if (!jobToken || !projectId || !jobId)
|
|
32
|
+
return undefined;
|
|
33
|
+
return { projectId, jobId, pipelineId, mergeRequestIid, jobToken, serverUrl };
|
|
34
|
+
};
|
|
35
|
+
const toNumberIfSet = (value) => {
|
|
36
|
+
if (value === undefined || value === null || value === '')
|
|
37
|
+
return undefined;
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
40
|
+
};
|
|
41
|
+
const calculate = async (options) => {
|
|
42
|
+
const deps = createDependencies();
|
|
43
|
+
if (!['gcp', 'aws'].includes(options.provider)) {
|
|
44
|
+
throw new Error('provider must be gcp or aws');
|
|
45
|
+
}
|
|
46
|
+
if (!options.cpuTimeseries.length) {
|
|
47
|
+
throw new Error('cpuTimeseries is required and must have at least one data point');
|
|
48
|
+
}
|
|
49
|
+
const jobInput = {
|
|
50
|
+
provider: options.provider,
|
|
51
|
+
machineType: options.machineType,
|
|
52
|
+
region: options.region,
|
|
53
|
+
cpuTimeseries: options.cpuTimeseries,
|
|
54
|
+
ramUsedTimeseries: options.ramUsedTimeseries,
|
|
55
|
+
ramSizeTimeseries: options.ramSizeTimeseries
|
|
56
|
+
};
|
|
57
|
+
const budgetValue = toNumberIfSet(options.carbonBudgetGrams);
|
|
58
|
+
const budget = budgetValue !== undefined ? {
|
|
59
|
+
carbonBudgetGrams: budgetValue,
|
|
60
|
+
failOnBreach: Boolean(options.failOnBreach)
|
|
61
|
+
} : undefined;
|
|
62
|
+
const result = await deps.calculator.calculate(jobInput);
|
|
63
|
+
const markdown = (0, report_formatter_1.buildMarkdownReport)(result, jobInput, budget);
|
|
64
|
+
const jsonReport = (0, report_formatter_1.buildJsonReport)(result, jobInput, budget);
|
|
65
|
+
const budgetResult = (0, report_formatter_1.evaluateBudget)(result, budget);
|
|
66
|
+
if (options.writeMarkdownPath) {
|
|
67
|
+
fs_1.default.writeFileSync(path_1.default.resolve(options.writeMarkdownPath), markdown, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
if (options.writeJsonPath) {
|
|
70
|
+
fs_1.default.writeFileSync(path_1.default.resolve(options.writeJsonPath), JSON.stringify(jsonReport, null, 2), 'utf8');
|
|
71
|
+
}
|
|
72
|
+
const gitlabContext = options.gitlab === false ? undefined : resolveGitLabContextFromEnv(options.gitlab);
|
|
73
|
+
const gitlabOpts = options.gitlab !== false ? options.gitlab : undefined;
|
|
74
|
+
if (gitlabOpts?.postMergeRequestNote && gitlabContext?.jobToken) {
|
|
75
|
+
await deps.gitlabClient.postMergeRequestNote({
|
|
76
|
+
projectId: Number(gitlabContext.projectId),
|
|
77
|
+
jobId: Number(gitlabContext.jobId),
|
|
78
|
+
pipelineId: gitlabContext.pipelineId ? Number(gitlabContext.pipelineId) : undefined,
|
|
79
|
+
mergeRequestIid: gitlabContext.mergeRequestIid ? Number(gitlabContext.mergeRequestIid) : undefined,
|
|
80
|
+
jobToken: gitlabContext.jobToken,
|
|
81
|
+
serverUrl: gitlabContext.serverUrl || config_1.config.gitlabBaseUrl
|
|
82
|
+
}, markdown);
|
|
83
|
+
}
|
|
84
|
+
if (budgetResult.overBudget && budget?.failOnBreach) {
|
|
85
|
+
throw new Error(`Carbon budget exceeded (${budgetResult.limitGrams} gCO2e)`);
|
|
86
|
+
}
|
|
87
|
+
return { result, budget: budgetResult, markdown, jsonReport };
|
|
88
|
+
};
|
|
89
|
+
exports.calculate = calculate;
|
|
90
|
+
const readRunnerTagsFromEnv = () => {
|
|
91
|
+
const tagsEnv = process.env.CI_RUNNER_TAGS || process.env.RUNNER_TAGS || '';
|
|
92
|
+
return tagsEnv.split(/[,\s]+/).filter(Boolean);
|
|
93
|
+
};
|
|
94
|
+
exports.readRunnerTagsFromEnv = readRunnerTagsFromEnv;
|