Skip to content

Commit 4de56f8

Browse files
authored
Merge pull request github#1704 from github/brrygrdn/dg-11926-extract-github-step-summary
Extract a summary.md file when a Dependabot container completes successfully
2 parents ca4b065 + 8c1fb10 commit 4de56f8

4 files changed

Lines changed: 168 additions & 2 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as fs from 'fs'
2+
import * as os from 'os'
3+
import * as path from 'path'
4+
import {pack} from 'tar-stream'
5+
import {ContainerService} from '../src/container-service'
6+
7+
describe('ContainerService.extractJobSummary', () => {
8+
let stepSummaryPath: string
9+
let tmpDir: string
10+
11+
beforeEach(() => {
12+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dependabot-action-test-'))
13+
stepSummaryPath = path.join(tmpDir, 'step-summary.md')
14+
fs.writeFileSync(stepSummaryPath, '')
15+
process.env.GITHUB_STEP_SUMMARY = stepSummaryPath
16+
})
17+
18+
afterEach(() => {
19+
delete process.env.GITHUB_STEP_SUMMARY
20+
fs.rmSync(tmpDir, {recursive: true, force: true})
21+
})
22+
23+
test('extracts summary.md from container and appends to GITHUB_STEP_SUMMARY', async () => {
24+
const markdownContent =
25+
'## Dependency Graph Snapshot\n\n| Directory | Status |\n'
26+
27+
// Create a tar archive containing the summary file
28+
const tarStream = pack()
29+
tarStream.entry({name: 'summary.md'}, markdownContent)
30+
tarStream.finalize()
31+
32+
const mockContainer = {
33+
getArchive: jest.fn().mockResolvedValue(tarStream)
34+
} as any
35+
36+
await ContainerService.extractJobSummary(mockContainer)
37+
38+
const written = fs.readFileSync(stepSummaryPath, 'utf-8')
39+
expect(written).toEqual(markdownContent)
40+
expect(mockContainer.getArchive).toHaveBeenCalledWith({
41+
path: '/home/dependabot/dependabot-updater/output/summary.md'
42+
})
43+
})
44+
45+
test('gracefully skips when file does not exist in container', async () => {
46+
const mockContainer = {
47+
getArchive: jest.fn().mockRejectedValue(new Error('file not found: 404'))
48+
} as any
49+
50+
await ContainerService.extractJobSummary(mockContainer)
51+
52+
const written = fs.readFileSync(stepSummaryPath, 'utf-8')
53+
expect(written).toEqual('')
54+
})
55+
56+
test('does not write to GITHUB_STEP_SUMMARY when summary.md is empty', async () => {
57+
const tarStream = pack()
58+
tarStream.entry({name: 'summary.md'}, '')
59+
tarStream.finalize()
60+
61+
const mockContainer = {
62+
getArchive: jest.fn().mockResolvedValue(tarStream)
63+
} as any
64+
65+
await ContainerService.extractJobSummary(mockContainer)
66+
67+
const written = fs.readFileSync(stepSummaryPath, 'utf-8')
68+
expect(written).toEqual('')
69+
})
70+
71+
test('does nothing when GITHUB_STEP_SUMMARY is not set', async () => {
72+
delete process.env.GITHUB_STEP_SUMMARY
73+
74+
const mockContainer = {
75+
getArchive: jest.fn()
76+
} as any
77+
78+
await ContainerService.extractJobSummary(mockContainer)
79+
80+
expect(mockContainer.getArchive).not.toHaveBeenCalled()
81+
})
82+
})

dist/main/index.js

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/main/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/container-service.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as core from '@actions/core'
2+
import * as fs from 'fs'
23
import {Container} from 'dockerode'
3-
import {pack} from 'tar-stream'
4+
import {pack, extract} from 'tar-stream'
45
import {FileFetcherInput, FileUpdaterInput, ProxyConfig} from './config-types'
56
import {outStream, errStream} from './utils'
67

@@ -76,6 +77,12 @@ export const ContainerService = {
7677
'dependabot'
7778
)
7879
}
80+
81+
// Extract job summary only after all commands have succeeded.
82+
// This prevents malicious code executed during fetch_files from
83+
// injecting content — our updater overwrites the file at the end
84+
// of a successful run.
85+
await this.extractJobSummary(container)
7986
} else {
8087
// For test containers and other containers, just wait for completion
8188
const outcome = await container.wait()
@@ -140,5 +147,44 @@ export const ContainerService = {
140147
`Command failed with exit code ${inspection.ExitCode}: ${cmd.join(' ')}`
141148
)
142149
}
150+
},
151+
152+
async extractJobSummary(container: Container): Promise<void> {
153+
const summaryPath = '/home/dependabot/dependabot-updater/output/summary.md'
154+
const stepSummaryPath = process.env.GITHUB_STEP_SUMMARY
155+
156+
if (!stepSummaryPath) {
157+
return
158+
}
159+
160+
try {
161+
const archiveStream = await container.getArchive({path: summaryPath})
162+
163+
const content = await new Promise<string>((resolve, reject) => {
164+
const extractor = extract()
165+
let data = ''
166+
167+
extractor.on('entry', (header, stream, next) => {
168+
stream.on('data', chunk => {
169+
data += chunk.toString()
170+
})
171+
stream.on('end', () => next())
172+
stream.resume()
173+
})
174+
175+
extractor.on('finish', () => resolve(data))
176+
extractor.on('error', err => reject(err))
177+
178+
archiveStream.pipe(extractor)
179+
})
180+
181+
if (content.length > 0) {
182+
fs.appendFileSync(stepSummaryPath, content)
183+
core.info('Job summary written to GITHUB_STEP_SUMMARY')
184+
}
185+
} catch {
186+
// File doesn't exist in container (older updater image) — skip gracefully
187+
core.debug('No job summary file found in container')
188+
}
143189
}
144190
}

0 commit comments

Comments
 (0)