pipeline.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { parse } from "yaml";
  2. import { readFileSync, readdirSync } from "fs";
  3. import { basename, resolve } from "path";
  4. import { execSync } from "child_process";
  5. import { BuildkitePipeline, BuildkiteStep, EsPipeline, EsPipelineConfig } from "./types";
  6. import { getBwcVersions, getSnapshotBwcVersions } from "./bwc-versions";
  7. const PROJECT_ROOT = resolve(`${import.meta.dir}/../../..`);
  8. const getArray = (strOrArray: string | string[] | undefined): string[] => {
  9. if (typeof strOrArray === "undefined") {
  10. return [];
  11. }
  12. return typeof strOrArray === "string" ? [strOrArray] : strOrArray;
  13. };
  14. const labelCheckAllow = (pipeline: EsPipeline, labels: string[]): boolean => {
  15. if (pipeline.config?.["allow-labels"]) {
  16. return getArray(pipeline.config["allow-labels"]).some((label) => labels.includes(label));
  17. }
  18. return true;
  19. };
  20. const labelCheckSkip = (pipeline: EsPipeline, labels: string[]): boolean => {
  21. if (pipeline.config?.["skip-labels"]) {
  22. return !getArray(pipeline.config["skip-labels"]).some((label) => labels.includes(label));
  23. }
  24. return true;
  25. };
  26. // Exclude the pipeline if all of the changed files in the PR are in at least one excluded region
  27. const changedFilesExcludedCheck = (pipeline: EsPipeline, changedFiles: string[]): boolean => {
  28. if (pipeline.config?.["excluded-regions"]) {
  29. return !changedFiles.every((file) =>
  30. getArray(pipeline.config?.["excluded-regions"]).some((region) => file.match(region))
  31. );
  32. }
  33. return true;
  34. };
  35. // Include the pipeline if all of the changed files in the PR are in at least one included region
  36. const changedFilesIncludedCheck = (pipeline: EsPipeline, changedFiles: string[]): boolean => {
  37. if (pipeline.config?.["included-regions"]) {
  38. return changedFiles.every((file) =>
  39. getArray(pipeline.config?.["included-regions"]).some((region) => file.match(region))
  40. );
  41. }
  42. return true;
  43. };
  44. const checkTargetBranch = (pipeline: EsPipeline, targetBranch: string | undefined) => {
  45. if (!targetBranch || !pipeline.config?.["skip-target-branches"]) {
  46. return true;
  47. }
  48. return !getArray(pipeline.config["skip-target-branches"]).some((branch) => branch === targetBranch);
  49. };
  50. const triggerCommentCheck = (pipeline: EsPipeline): boolean => {
  51. if (process.env["GITHUB_PR_TRIGGER_COMMENT"] && pipeline.config?.["trigger-phrase"]) {
  52. return !!process.env["GITHUB_PR_TRIGGER_COMMENT"].match(pipeline.config["trigger-phrase"]);
  53. }
  54. return false;
  55. };
  56. // There are so many BWC versions that we can't use the matrix feature in Buildkite, as it's limited to 20 elements per dimension
  57. // So we need to duplicate the steps instead
  58. // Recursively check for any steps that have a bwc_template attribute and expand them out into multiple steps, one for each BWC_VERSION
  59. const doBwcTransforms = (step: BuildkitePipeline | BuildkiteStep) => {
  60. const stepsToExpand = (step.steps || []).filter((s) => s.bwc_template);
  61. step.steps = (step.steps || []).filter((s) => !s.bwc_template);
  62. for (const s of step.steps) {
  63. if (s.steps?.length) {
  64. doBwcTransforms(s);
  65. }
  66. }
  67. for (const stepToExpand of stepsToExpand) {
  68. for (const bwcVersion of getBwcVersions()) {
  69. let newStepJson = JSON.stringify(stepToExpand).replaceAll("$BWC_VERSION_SNAKE", bwcVersion.replaceAll(".", "_"));
  70. newStepJson = newStepJson.replaceAll("$BWC_VERSION", bwcVersion);
  71. const newStep = JSON.parse(newStepJson);
  72. delete newStep.bwc_template;
  73. step.steps.push(newStep);
  74. }
  75. }
  76. };
  77. export const generatePipelines = (
  78. directory: string = `${PROJECT_ROOT}/.buildkite/pipelines/pull-request`,
  79. changedFiles: string[] = []
  80. ) => {
  81. let defaults: EsPipelineConfig = { config: {} };
  82. defaults = parse(readFileSync(`${directory}/.defaults.yml`, "utf-8"));
  83. defaults.config = defaults.config || {};
  84. let pipelines: EsPipeline[] = [];
  85. const files = readdirSync(directory);
  86. for (const file of files) {
  87. if (!file.endsWith(".yml") || file.endsWith(".defaults.yml")) {
  88. continue;
  89. }
  90. let yaml = readFileSync(`${directory}/${file}`, "utf-8");
  91. yaml = yaml.replaceAll("$SNAPSHOT_BWC_VERSIONS", JSON.stringify(getSnapshotBwcVersions()));
  92. const pipeline: EsPipeline = parse(yaml) || {};
  93. pipeline.config = { ...defaults.config, ...(pipeline.config || {}) };
  94. // '.../build-benchmark.yml' => 'build-benchmark'
  95. const name = basename(file).split(".", 2)[0];
  96. pipeline.name = name;
  97. pipeline.config["trigger-phrase"] = pipeline.config["trigger-phrase"] || `.*run\\W+elasticsearch-ci/${name}.*`;
  98. pipelines.push(pipeline);
  99. }
  100. const labels = (process.env["GITHUB_PR_LABELS"] || "")
  101. .split(",")
  102. .map((x) => x.trim())
  103. .filter((x) => x);
  104. if (!changedFiles?.length) {
  105. console.log("Doing git fetch and getting merge-base");
  106. const mergeBase = execSync(
  107. `git fetch origin ${process.env["GITHUB_PR_TARGET_BRANCH"]}; git merge-base origin/${process.env["GITHUB_PR_TARGET_BRANCH"]} HEAD`,
  108. { cwd: PROJECT_ROOT }
  109. )
  110. .toString()
  111. .trim();
  112. console.log(`Merge base: ${mergeBase}`);
  113. const changedFilesOutput = execSync(`git diff --name-only ${mergeBase}`, { cwd: PROJECT_ROOT }).toString().trim();
  114. changedFiles = changedFilesOutput
  115. .split("\n")
  116. .map((x) => x.trim())
  117. .filter((x) => x);
  118. console.log("Changed files (first 50):");
  119. console.log(changedFiles.slice(0, 50).join("\n"));
  120. }
  121. let filters: ((pipeline: EsPipeline) => boolean)[] = [
  122. (pipeline) => checkTargetBranch(pipeline, process.env["GITHUB_PR_TARGET_BRANCH"]),
  123. (pipeline) => labelCheckAllow(pipeline, labels),
  124. (pipeline) => labelCheckSkip(pipeline, labels),
  125. (pipeline) => changedFilesExcludedCheck(pipeline, changedFiles),
  126. (pipeline) => changedFilesIncludedCheck(pipeline, changedFiles),
  127. ];
  128. // When triggering via the "run elasticsearch-ci/step-name" comment, we ONLY want to run pipelines that match the trigger phrase, regardless of labels, etc
  129. // However, if we're using the overall CI trigger "[buildkite] test this [please]", we should use the regular filters above
  130. if (
  131. process.env["GITHUB_PR_TRIGGER_COMMENT"] &&
  132. !process.env["GITHUB_PR_TRIGGER_COMMENT"].match(
  133. /^\s*((@elastic(search)?machine|buildkite)\s*)?test\s+this(\s+please)?/i
  134. )
  135. ) {
  136. filters = [triggerCommentCheck];
  137. }
  138. for (const filter of filters) {
  139. pipelines = pipelines.filter(filter);
  140. }
  141. for (const pipeline of pipelines) {
  142. doBwcTransforms(pipeline);
  143. }
  144. pipelines.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
  145. const finalPipelines = pipelines.map((pipeline) => {
  146. const finalPipeline = { name: pipeline.name, pipeline: { ...pipeline } };
  147. delete finalPipeline.pipeline.config;
  148. delete finalPipeline.pipeline.name;
  149. return finalPipeline;
  150. });
  151. return finalPipelines;
  152. };