Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions __tests__/extract-job-summary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import {pack} from 'tar-stream'
import {ContainerService} from '../src/container-service'

describe('ContainerService.extractJobSummary', () => {
let stepSummaryPath: string
let tmpDir: string

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dependabot-action-test-'))
stepSummaryPath = path.join(tmpDir, 'step-summary.md')
fs.writeFileSync(stepSummaryPath, '')
process.env.GITHUB_STEP_SUMMARY = stepSummaryPath
})

afterEach(() => {
delete process.env.GITHUB_STEP_SUMMARY
fs.rmSync(tmpDir, {recursive: true, force: true})
})

test('extracts summary.md from container and appends to GITHUB_STEP_SUMMARY', async () => {
const markdownContent =
'## Dependency Graph Snapshot\n\n| Directory | Status |\n'

// Create a tar archive containing the summary file
const tarStream = pack()
tarStream.entry({name: 'summary.md'}, markdownContent)
tarStream.finalize()

const mockContainer = {
getArchive: jest.fn().mockResolvedValue(tarStream)
} as any

await ContainerService.extractJobSummary(mockContainer)

const written = fs.readFileSync(stepSummaryPath, 'utf-8')
expect(written).toEqual(markdownContent)
expect(mockContainer.getArchive).toHaveBeenCalledWith({
path: '/home/dependabot/dependabot-updater/output/summary.md'
})
})

test('gracefully skips when file does not exist in container', async () => {
const mockContainer = {
getArchive: jest.fn().mockRejectedValue(new Error('file not found: 404'))
} as any

await ContainerService.extractJobSummary(mockContainer)

const written = fs.readFileSync(stepSummaryPath, 'utf-8')
expect(written).toEqual('')
})

test('does not write to GITHUB_STEP_SUMMARY when summary.md is empty', async () => {
const tarStream = pack()
tarStream.entry({name: 'summary.md'}, '')
tarStream.finalize()

const mockContainer = {
getArchive: jest.fn().mockResolvedValue(tarStream)
} as any

await ContainerService.extractJobSummary(mockContainer)

const written = fs.readFileSync(stepSummaryPath, 'utf-8')
expect(written).toEqual('')
})

test('does nothing when GITHUB_STEP_SUMMARY is not set', async () => {
delete process.env.GITHUB_STEP_SUMMARY

const mockContainer = {
getArchive: jest.fn()
} as any

await ContainerService.extractJobSummary(mockContainer)

expect(mockContainer.getArchive).not.toHaveBeenCalled()
})
})
38 changes: 38 additions & 0 deletions dist/main/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/main/index.js.map

Large diffs are not rendered by default.

48 changes: 47 additions & 1 deletion src/container-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import {Container} from 'dockerode'
import {pack} from 'tar-stream'
import {pack, extract} from 'tar-stream'
import {FileFetcherInput, FileUpdaterInput, ProxyConfig} from './config-types'
import {outStream, errStream} from './utils'

Expand Down Expand Up @@ -76,6 +77,12 @@ export const ContainerService = {
'dependabot'
)
}

// Extract job summary only after all commands have succeeded.
// This prevents malicious code executed during fetch_files from
// injecting content — our updater overwrites the file at the end
// of a successful run.
await this.extractJobSummary(container)
} else {
// For test containers and other containers, just wait for completion
const outcome = await container.wait()
Expand Down Expand Up @@ -140,5 +147,44 @@ export const ContainerService = {
`Command failed with exit code ${inspection.ExitCode}: ${cmd.join(' ')}`
)
}
},

async extractJobSummary(container: Container): Promise<void> {
const summaryPath = '/home/dependabot/dependabot-updater/output/summary.md'
const stepSummaryPath = process.env.GITHUB_STEP_SUMMARY

if (!stepSummaryPath) {
return
}

try {
const archiveStream = await container.getArchive({path: summaryPath})

const content = await new Promise<string>((resolve, reject) => {
const extractor = extract()
let data = ''

extractor.on('entry', (header, stream, next) => {
stream.on('data', chunk => {
data += chunk.toString()
})
stream.on('end', () => next())
stream.resume()
})

extractor.on('finish', () => resolve(data))
extractor.on('error', err => reject(err))

archiveStream.pipe(extractor)
})
Comment on lines +163 to +179

if (content.length > 0) {
fs.appendFileSync(stepSummaryPath, content)
core.info('Job summary written to GITHUB_STEP_SUMMARY')
}
} catch {
// File doesn't exist in container (older updater image) — skip gracefully
core.debug('No job summary file found in container')
}
Comment on lines +185 to +188
}
}
Loading