Skip to content

Commit 47738ec

Browse files
committed
solve conflicts
2 parents b0a1036 + d8dca1d commit 47738ec

2,011 files changed

Lines changed: 39933 additions & 11183 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.buildkite/pipeline-utils/buildkite/client.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10+
import { execFileSync } from 'child_process';
1011
import { BuildkiteClient } from './client';
1112
import type { Build } from './types/build';
1213
import type { Job } from './types/job';
1314

15+
jest.mock('child_process', () => ({
16+
...jest.requireActual('child_process'),
17+
execFileSync: jest.fn(),
18+
}));
19+
20+
const execFileSyncMock = execFileSync as jest.MockedFunction<typeof execFileSync>;
21+
1422
describe('BuildkiteClient', () => {
1523
let buildkite: BuildkiteClient;
1624

@@ -256,4 +264,22 @@ describe('BuildkiteClient', () => {
256264
expect(result.success).toEqual(true);
257265
});
258266
});
267+
268+
describe('cancelStep', () => {
269+
afterEach(() => {
270+
execFileSyncMock.mockReset();
271+
});
272+
273+
it('calls buildkite-agent step cancel for the specified step', () => {
274+
buildkite.cancelStep('step-id-1');
275+
276+
expect(execFileSyncMock).toHaveBeenCalledWith(
277+
'buildkite-agent',
278+
['step', 'cancel', '--step', 'step-id-1'],
279+
{
280+
stdio: ['pipe', 'inherit', 'inherit'],
281+
}
282+
);
283+
});
284+
});
259285
});

.buildkite/pipeline-utils/buildkite/client.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import type { AxiosInstance } from 'axios';
1111
import axios from 'axios';
1212
import type { ExecSyncOptions } from 'child_process';
13-
import { execSync } from 'child_process';
13+
import { execFileSync, execSync } from 'child_process';
1414

1515
import { dump } from 'js-yaml';
1616

@@ -367,16 +367,35 @@ export class BuildkiteClient {
367367
return (await this.http.post(url, options)).data;
368368
};
369369

370+
cancelStep = (stepIdOrKey: string): void => {
371+
execFileSync('buildkite-agent', ['step', 'cancel', '--step', stepIdOrKey], {
372+
stdio: ['pipe', 'inherit', 'inherit'],
373+
});
374+
};
375+
376+
getMetadataKeys = (): string[] => {
377+
const stdout = execFileSync('buildkite-agent', ['meta-data', 'keys'], {
378+
stdio: ['pipe', 'pipe', 'inherit'],
379+
});
380+
381+
const output = stdout?.toString().trim() ?? '';
382+
if (!output) {
383+
return [];
384+
}
385+
386+
return output.split('\n').filter(Boolean);
387+
};
388+
370389
setMetadata = (key: string, value: string) => {
371-
this.exec(`buildkite-agent meta-data set '${key}'`, {
390+
execFileSync('buildkite-agent', ['meta-data', 'set', key], {
372391
input: value,
373392
stdio: ['pipe', 'inherit', 'inherit'],
374393
});
375394
};
376395

377396
getMetadata(key: string, defaultValue: string | null = null): string | null {
378397
try {
379-
const stdout = this.exec(`buildkite-agent meta-data get '${key}'`, {
398+
const stdout = execFileSync('buildkite-agent', ['meta-data', 'get', key], {
380399
stdio: ['pipe'],
381400
});
382401
return stdout?.toString().trim() || defaultValue;

.buildkite/pipeline-utils/buildkite/types/job.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,21 @@ export interface Job {
3636
id: string;
3737
type: string;
3838
name: string;
39-
step_key: string;
39+
step_key: string | null;
40+
/**
41+
* A job is an execution of a step. When a step is run with `parallelism`,
42+
* multiple jobs will share the same `step.id`.
43+
*
44+
* This is present in the Buildkite Builds API response for jobs.
45+
*/
46+
step?: {
47+
id: string;
48+
signature?: string | null;
49+
};
4050
state: JobState;
4151
logs_url: string;
4252
raw_log_url: string;
43-
command: string;
53+
command: string | null;
4454
exit_status: null | number;
4555
artifact_paths: string;
4656
artifacts_url: string;
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { execFileSync } from 'child_process';
11+
import fs from 'fs';
12+
import os from 'os';
13+
import path from 'path';
14+
import { load as loadYaml } from 'js-yaml';
15+
16+
jest.mock('child_process', () => ({
17+
execFileSync: jest.fn(),
18+
}));
19+
20+
import { getPipeline } from './utils';
21+
22+
const execFileSyncMock = execFileSync as jest.MockedFunction<typeof execFileSync>;
23+
24+
describe('getPipeline', () => {
25+
let tempDir: string;
26+
27+
beforeEach(() => {
28+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'buildkite-utils-'));
29+
execFileSyncMock.mockReset();
30+
});
31+
32+
afterEach(() => {
33+
fs.rmSync(tempDir, { recursive: true, force: true });
34+
});
35+
36+
const writePipeline = (contents: string): string => {
37+
const filename = path.join(tempDir, 'pipeline.yml');
38+
fs.writeFileSync(filename, contents);
39+
return filename;
40+
};
41+
42+
it('ignores comment-only steps when registering cancel-on-gate-failure metadata', () => {
43+
const filename = writePipeline(`steps:
44+
# - command: .buildkite/scripts/steps/checks/api_contracts.sh
45+
# key: check_api_contracts
46+
`);
47+
48+
expect(() => getPipeline(filename, { cancelOnGateFailure: true })).not.toThrow();
49+
expect(execFileSyncMock).not.toHaveBeenCalled();
50+
});
51+
52+
it('ignores non-command steps when registering cancel-on-gate-failure metadata', () => {
53+
const filename = writePipeline(`steps:
54+
- wait: ~
55+
`);
56+
57+
expect(() => getPipeline(filename, { cancelOnGateFailure: true })).not.toThrow();
58+
expect(execFileSyncMock).not.toHaveBeenCalled();
59+
});
60+
61+
it('registers metadata for keyed steps', () => {
62+
const filename = writePipeline(`steps:
63+
- command: echo test
64+
key: test_step
65+
`);
66+
67+
getPipeline(filename, { cancelOnGateFailure: true });
68+
69+
expect(execFileSyncMock).toHaveBeenCalledWith('buildkite-agent', [
70+
'meta-data',
71+
'set',
72+
'cancel_on_gate_failure:test_step',
73+
'true',
74+
]);
75+
});
76+
77+
it('still throws when an active step is missing a key', () => {
78+
const filename = writePipeline(`steps:
79+
- command: echo test
80+
`);
81+
82+
expect(() => getPipeline(filename, { cancelOnGateFailure: true })).toThrow(
83+
'is missing a "key"'
84+
);
85+
});
86+
87+
it('verifies manually registered base.yml step keys exist', () => {
88+
const repoRoot = path.resolve(__dirname, '../../..');
89+
const pipelineSource = fs.readFileSync(
90+
path.resolve(__dirname, '../../scripts/pipelines/pull_request/pipeline.ts'),
91+
'utf8'
92+
);
93+
94+
// Extract step keys from the manual registration loop in pipeline.ts
95+
const manualKeysMatch = pipelineSource.match(/for \(const stepKey of \[([^\]]+)\]\)/s);
96+
expect(manualKeysMatch).not.toBeNull();
97+
98+
const manualKeys = [...manualKeysMatch![1].matchAll(/'([^']+)'/g)].map(([, key]) => key);
99+
expect(manualKeys.length).toBeGreaterThan(0);
100+
101+
// Parse base.yml and collect all step keys
102+
const baseYml = fs.readFileSync(
103+
path.resolve(repoRoot, '.buildkite/pipelines/pull_request/base.yml'),
104+
'utf8'
105+
);
106+
const baseDoc = loadYaml(baseYml) as { steps: Array<{ key?: string }> };
107+
const baseKeys = new Set(
108+
baseDoc.steps
109+
.filter((s: Record<string, unknown>) => typeof s.key === 'string')
110+
.map((s: Record<string, unknown>) => s.key)
111+
);
112+
113+
for (const key of manualKeys) {
114+
expect(baseKeys).toContain(key);
115+
}
116+
});
117+
118+
it('accepts every cancelable pull request pipeline fragment', () => {
119+
const repoRoot = path.resolve(__dirname, '../../..');
120+
const pullRequestPipeline = path.resolve(
121+
__dirname,
122+
'../../scripts/pipelines/pull_request/pipeline.ts'
123+
);
124+
const pipelineSource = fs.readFileSync(pullRequestPipeline, 'utf8');
125+
const cancelablePipelines = [
126+
...new Set(
127+
[...pipelineSource.matchAll(/getPipeline\(\s*'([^']+\.yml)'\s*,\s*cancelable\s*\)/gs)].map(
128+
([, filename]) => path.resolve(repoRoot, filename)
129+
)
130+
),
131+
];
132+
133+
expect(cancelablePipelines.length).toBeGreaterThan(0);
134+
135+
for (const filename of cancelablePipelines) {
136+
expect(() => getPipeline(filename, { cancelOnGateFailure: true })).not.toThrow();
137+
}
138+
});
139+
140+
it('has no duplicate step keys across cancelable pipeline files and base.yml', () => {
141+
const repoRoot = path.resolve(__dirname, '../../..');
142+
const pullRequestPipeline = path.resolve(
143+
__dirname,
144+
'../../scripts/pipelines/pull_request/pipeline.ts'
145+
);
146+
const pipelineSource = fs.readFileSync(pullRequestPipeline, 'utf8');
147+
148+
// Collect all cancelable pipeline files
149+
const cancelablePipelines = [
150+
...new Set(
151+
[...pipelineSource.matchAll(/getPipeline\(\s*'([^']+\.yml)'\s*,\s*cancelable\s*\)/gs)].map(
152+
([, filename]) => path.resolve(repoRoot, filename)
153+
)
154+
),
155+
];
156+
157+
// Also include base.yml
158+
cancelablePipelines.push(path.resolve(repoRoot, '.buildkite/pipelines/pull_request/base.yml'));
159+
160+
// These pipeline pairs are in if/else branches in pipeline.ts and can never
161+
// both be uploaded in the same build, so shared keys are safe.
162+
const mutuallyExclusivePairs = new Set([
163+
'build_project.yml:deploy_project.yml',
164+
'deploy_project.yml:build_project.yml',
165+
]);
166+
167+
const allKeys = new Map<string, string>();
168+
const duplicates: string[] = [];
169+
170+
for (const filename of cancelablePipelines) {
171+
const doc = loadYaml(fs.readFileSync(filename, 'utf8'));
172+
if (!doc || !Array.isArray(doc.steps)) continue;
173+
174+
const collectKeys = (steps: Array<Record<string, unknown>>) => {
175+
for (const step of steps) {
176+
if (typeof step !== 'object' || step === null) continue;
177+
if (typeof step.key === 'string') {
178+
const rel = path.relative(repoRoot, filename);
179+
if (allKeys.has(step.key as string)) {
180+
const existingFile = allKeys.get(step.key as string)!;
181+
const pair = `${path.basename(existingFile)}:${path.basename(rel)}`;
182+
if (!mutuallyExclusivePairs.has(pair)) {
183+
duplicates.push(`key "${step.key}" in ${rel} conflicts with ${existingFile}`);
184+
}
185+
} else {
186+
allKeys.set(step.key as string, rel);
187+
}
188+
}
189+
if (Array.isArray(step.steps)) {
190+
collectKeys(step.steps as Array<Record<string, unknown>>);
191+
}
192+
}
193+
};
194+
195+
collectKeys(doc.steps);
196+
}
197+
198+
expect(duplicates).toEqual([]);
199+
});
200+
});

0 commit comments

Comments
 (0)