Skip to content

Commit 4e3ee82

Browse files
tsigouris007claude
andcommitted
fix: cleanup EPERM and shrink oversized step summary in post.js
The events file is written via `sudo tee` so it ends up root-owned, and /tmp's sticky bit blocks the unprivileged post-step from unlinking it directly — swap fs.unlinkSync for `sudo rm -f`, matching how we already write/signal via sudo elsewhere in this file. The step summary was also one markdown row per raw event, which blew past GitHub's 1MB limit on noisy runs with lots of repeated short-lived connections. Now it's one row per distinct connection (grouped by proto/ips/port/domain/process/uid) with a Count, a condensed source port list, and a first/last seen time range — keeps process attribution intact while cutting row count dramatically, with a size-based truncation fallback just in case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4b1fa21 commit 4e3ee82

1 file changed

Lines changed: 100 additions & 30 deletions

File tree

src/post.js

Lines changed: 100 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,68 @@ function parseEventsNDJSON(filePath) {
150150
}
151151
}
152152

153+
// Collapse events into one row per distinct connection (same protocol,
154+
// source/destination IP, destination port+domain, process, UID) — repeated
155+
// connections (e.g. keep-alive requests with a new ephemeral source port each
156+
// time) differ only in timestamp/source port, which is what's aggregated here.
157+
// Process/UID are kept as grouping keys (not collapsed) since which process
158+
// made a connection is the signal this tracker exists to surface.
159+
// Returns groups sorted by count descending.
160+
function groupEvents(events) {
161+
const groups = new Map();
162+
for (const e of events) {
163+
const protocol = e.network?.protocol || 'N/A';
164+
const sourceIp = e.source?.ip || 'N/A';
165+
const destIp = e.destination?.ip || 'N/A';
166+
const destPort = e.destination?.port ?? 'N/A';
167+
const destDomain = e.destination?.domain || 'N/A';
168+
const proc = e.process?.name || 'N/A';
169+
const uid = e.user?.id || 'N/A';
170+
const key = [protocol, sourceIp, destIp, destPort, destDomain, proc, uid].join(' ');
171+
172+
let g = groups.get(key);
173+
if (!g) {
174+
g = {
175+
protocol, sourceIp, destIp, destPort, destDomain, process: proc, uid,
176+
count: 0, srcPorts: new Set(), firstSeen: null, lastSeen: null,
177+
};
178+
groups.set(key, g);
179+
}
180+
g.count += 1;
181+
if (e.source?.port !== undefined && e.source?.port !== null) g.srcPorts.add(e.source.port);
182+
const ts = e.timestamp;
183+
if (ts) {
184+
if (g.firstSeen === null || ts < g.firstSeen) g.firstSeen = ts;
185+
if (g.lastSeen === null || ts > g.lastSeen) g.lastSeen = ts;
186+
}
187+
}
188+
return [...groups.values()].sort((a, b) => b.count - a.count);
189+
}
190+
191+
// Exact ports for low-cardinality groups; a count for the rest, so the cell
192+
// never becomes an unreadable pile of unrelated ephemeral port numbers.
193+
function formatSrcPorts(srcPorts) {
194+
if (srcPorts.size === 0) return 'N/A';
195+
if (srcPorts.size <= 3) return [...srcPorts].sort((a, b) => a - b).join(', ');
196+
return `${srcPorts.size} distinct`;
197+
}
198+
199+
// Leaves headroom under GitHub's 1024 KiB step-summary hard limit.
200+
const STEP_SUMMARY_BUDGET_BYTES = 900 * 1024;
201+
153202
// Write a markdown summary table to GITHUB_STEP_SUMMARY.
154-
function printStepSummary(events) {
203+
// `fullDataLocation` (an S3 URI or "the job log") is referenced if the table
204+
// still has to be cut down to fit the size limit.
205+
function printStepSummary(events, fullDataLocation) {
155206
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
156207
if (!summaryPath) return;
157208

158209
const repo = process.env.GITHUB_REPOSITORY || 'N/A';
159210
const workflow = process.env.GITHUB_WORKFLOW || 'N/A';
160211
const runId = process.env.GITHUB_RUN_ID || 'N/A';
161212

213+
const groups = groupEvents(events);
214+
162215
// Aggregate stats.
163216
const uniqueDsts = new Set(events.map((e) => e.destination?.ip)).size;
164217
const byProto = {};
@@ -178,44 +231,59 @@ function printStepSummary(events) {
178231
'| Repository | Workflow | Run ID |\n' +
179232
'| --- | --- | --- |\n' +
180233
`| ${repo} | ${workflow} | ${runId} |\n\n` +
181-
'| Total Events | Unique Destination IPs | Protocols |\n' +
182-
'| --- | --- | --- |\n' +
183-
`| ${events.length} | ${uniqueDsts} | ${protoLine} |\n\n` +
234+
'| Total Events | Unique Connections | Unique Destination IPs | Protocols |\n' +
235+
'| --- | --- | --- | --- |\n' +
236+
`| ${events.length} | ${groups.length} | ${uniqueDsts} | ${protoLine} |\n\n` +
184237
'---\n\n';
185238

186-
// Connection table.
239+
// Connection table: one row per distinct connection, not per raw event.
187240
const columns = [
188-
'Timestamp', 'Protocol',
189-
'Source IP', 'Src Port',
190-
'Destination IP', 'Dst Port', 'Dst Domain',
191-
'PID', 'Process', 'UID',
241+
'Protocol', 'Source IP', 'Destination IP', 'Dst Port', 'Dst Domain',
242+
'Process', 'UID', 'Count', 'Src Ports', 'First Seen (UTC)', 'Last Seen (UTC)',
192243
];
193-
194-
const mdRows = [];
195-
mdRows.push(`| ${columns.join(' | ')} |`);
196-
mdRows.push(`| ${columns.map(() => '---').join(' | ')} |`);
197-
198-
for (const e of events) {
244+
const headerLines = [
245+
`| ${columns.join(' | ')} |`,
246+
`| ${columns.map(() => '---').join(' | ')} |`,
247+
];
248+
const rowLines = groups.map((g) => {
199249
const row = [
200-
e.timestamp || 'N/A',
201-
e.network?.protocol || 'N/A',
202-
e.source?.ip || 'N/A',
203-
e.source?.port ?? 'N/A',
204-
e.destination?.ip || 'N/A',
205-
e.destination?.port ?? 'N/A',
206-
e.destination?.domain || 'N/A',
207-
e.process?.pid ?? 'N/A',
208-
e.process?.name || 'N/A',
209-
e.user?.id || 'N/A',
250+
g.protocol, g.sourceIp, g.destIp, g.destPort, g.destDomain,
251+
g.process, g.uid, g.count, formatSrcPorts(g.srcPorts),
252+
g.firstSeen || 'N/A', g.lastSeen || 'N/A',
210253
];
211-
mdRows.push(`| ${row.join(' | ')} |`);
254+
return `| ${row.join(' | ')} |`;
255+
});
256+
257+
const sectionHeader = '## Network Events\n\n';
258+
const footer = '\n\n---\n';
259+
260+
// Safety net: only trims in genuinely high-cardinality cases (e.g. many
261+
// one-off unique destinations), since grouping already collapses repeats.
262+
const fixedBytes = Buffer.byteLength(
263+
metadataMd + sectionHeader + headerLines.join('\n') + '\n' + footer, 'utf8',
264+
);
265+
const budgetForRows = STEP_SUMMARY_BUDGET_BYTES - fixedBytes;
266+
267+
let usedBytes = 0;
268+
const includedRows = [];
269+
for (const line of rowLines) {
270+
const lineBytes = Buffer.byteLength(line + '\n', 'utf8');
271+
if (usedBytes + lineBytes > budgetForRows) break;
272+
includedRows.push(line);
273+
usedBytes += lineBytes;
212274
}
213275

276+
const omittedCount = rowLines.length - includedRows.length;
277+
const truncationNote = omittedCount > 0
278+
? `\n> ${omittedCount} of ${rowLines.length} connections omitted to stay under the step summary size limit — full data is in ${fullDataLocation}.\n`
279+
: '';
280+
214281
try {
215282
fs.appendFileSync(summaryPath, metadataMd);
216-
fs.appendFileSync(summaryPath, '## Network Events\n\n');
217-
fs.appendFileSync(summaryPath, mdRows.join('\n'));
218-
fs.appendFileSync(summaryPath, '\n\n---\n');
283+
fs.appendFileSync(summaryPath, sectionHeader);
284+
fs.appendFileSync(summaryPath, [...headerLines, ...includedRows].join('\n'));
285+
fs.appendFileSync(summaryPath, truncationNote);
286+
fs.appendFileSync(summaryPath, footer);
219287
} catch (err) {
220288
core.warning(`Could not write step summary: ${err.message}`);
221289
}
@@ -307,7 +375,9 @@ async function run() {
307375
], { env: buildS3UploadEnv() });
308376
core.info('Upload complete');
309377
try {
310-
fs.unlinkSync(EVENTS_FILE);
378+
// EVENTS_FILE is root-owned (written via `sudo tee`) and /tmp has the
379+
// sticky bit set, so only root can unlink it
380+
await exec.exec('sudo', ['rm', '-f', EVENTS_FILE]);
311381
} catch (cleanupErr) {
312382
core.warning(`Could not delete ${EVENTS_FILE}: ${cleanupErr.message}`);
313383
}

0 commit comments

Comments
 (0)