diff --git a/.changeset/quick-icons-taste.md b/.changeset/quick-icons-taste.md new file mode 100644 index 000000000..95089a253 --- /dev/null +++ b/.changeset/quick-icons-taste.md @@ -0,0 +1,5 @@ +--- +'skuba': patch +--- + +api: Truncate Buildkite annotations over 1 MiB to resolve `buildkite-agent` crash diff --git a/package.json b/package.json index ac8ac3a3e..0fdb3864f 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,6 @@ "entryPoint": "src/index.ts", "template": null, "type": "package", - "version": "8.1.0" + "version": "8.2.1" } } diff --git a/src/api/buildkite/annotate.test.ts b/src/api/buildkite/annotate.test.ts index 294a841ca..07dfec2b5 100644 --- a/src/api/buildkite/annotate.test.ts +++ b/src/api/buildkite/annotate.test.ts @@ -1,9 +1,11 @@ import * as execModule from '../../utils/exec'; +import { log } from '../../utils/logging'; -import { annotate } from './annotate'; +import { MAX_SIZE, TRUNCATION_WARNING, annotate } from './annotate'; const exec = jest.spyOn(execModule, 'exec'); const hasCommand = jest.spyOn(execModule, 'hasCommand'); +const mockWarn = jest.spyOn(log, 'warn').mockImplementation(() => undefined); beforeEach(() => { jest.clearAllMocks(); @@ -28,6 +30,8 @@ const setEnvironmentVariables = () => { describe('annotate', () => { const markdown = '**Message**'; + const endOfMessage = 'EndMessage'; + const oversizeMarkdown = 'a'.repeat(MAX_SIZE).concat(endOfMessage); describe.each` description | opts @@ -54,6 +58,43 @@ describe('annotate', () => { expect(exec).not.toHaveBeenCalled(); }); + it('warns about truncation when annotation exceeds the maximum size', async () => { + setEnvironmentVariables(); + await annotate(oversizeMarkdown); + + const lastCall = exec.mock.calls[exec.mock.calls.length - 1]; + if (!lastCall) { + throw new Error('Expected exec to have been called at least once'); + } + + const lastArgument = lastCall[lastCall.length - 1]; + + if (typeof lastArgument !== 'string') { + throw new Error('Expected the last argument to be a string'); + } + + expect(lastArgument.endsWith(TRUNCATION_WARNING)).toBe(true); + }); + + it('logs the full message when annotation is truncated', async () => { + setEnvironmentVariables(); + await annotate(oversizeMarkdown); + + const lastCall = mockWarn.mock.calls[exec.mock.calls.length - 1]; + if (!lastCall) { + throw new Error('Expected log.warn to have been called at least once'); + } + + const lastArgument = lastCall[lastCall.length - 1]; + + if (typeof lastArgument !== 'string') { + throw new Error('Expected the last argument to be a string'); + } + + // Check for the end of message in case there's a failure, entire message isn't printed (it's too large) + expect(lastArgument.endsWith(endOfMessage)).toBe(true); + }); + it('skips when `buildkite-agent` is not present', async () => { hasCommand.mockResolvedValue(false); diff --git a/src/api/buildkite/annotate.ts b/src/api/buildkite/annotate.ts index d3567ff97..179609412 100644 --- a/src/api/buildkite/annotate.ts +++ b/src/api/buildkite/annotate.ts @@ -1,4 +1,5 @@ import { exec, hasCommand } from '../../utils/exec'; +import { log } from '../../utils/logging'; export type AnnotationStyle = 'success' | 'info' | 'warning' | 'error'; @@ -24,6 +25,10 @@ interface AnnotationOptions { style?: AnnotationStyle; } +// Buildkite annotation currently only supports 1MiB of data +export const MAX_SIZE = 1024 * 1024; // 1MiB in bytes +export const TRUNCATION_WARNING = '... [Truncated due to size limit]'; + /** * Asynchronously uploads a Buildkite annotation. * @@ -44,6 +49,16 @@ export const annotate = async ( return; } + // Check if the annotation exceeds the maximum size + let truncatedMarkdown = markdown; + if (markdown.length > MAX_SIZE) { + // Notify user of truncation, leave space for message + const remainingSpace = MAX_SIZE - TRUNCATION_WARNING.length; + truncatedMarkdown = markdown.slice(0, remainingSpace) + TRUNCATION_WARNING; + // Log full message to the build log + log.warn(`Annotation truncated, full message is: ${markdown}`); + } + // Always scope to the current Buildkite step. const context = [ opts.scopeContextToStep && process.env.BUILDKITE_STEP_ID, @@ -59,6 +74,6 @@ export const annotate = async ( 'annotate', ...(context ? ['--context', context] : []), ...(style ? ['--style', style] : []), - markdown, + truncatedMarkdown, ); };