diff --git a/.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json b/.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json new file mode 100644 index 00000000..d4166e22 --- /dev/null +++ b/.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE pull_request SET auto_build_id = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2" +} diff --git a/.sqlx/query-6cbee85c41cbecc631748d10ac63fc1ea310e39edb0e93456c30f914b1d07445.json b/.sqlx/query-662b16c5a8c1c4a708ba8f7602c3197542d6b73b6914670ab95984a306d311e1.json similarity index 72% rename from .sqlx/query-6cbee85c41cbecc631748d10ac63fc1ea310e39edb0e93456c30f914b1d07445.json rename to .sqlx/query-662b16c5a8c1c4a708ba8f7602c3197542d6b73b6914670ab95984a306d311e1.json index f25da3b4..36ad0f8c 100644 --- a/.sqlx/query-6cbee85c41cbecc631748d10ac63fc1ea310e39edb0e93456c30f914b1d07445.json +++ b/.sqlx/query-662b16c5a8c1c4a708ba8f7602c3197542d6b73b6914670ab95984a306d311e1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.priority,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.rollup as \"rollup: RollupMode\",\n pr.created_at as \"created_at: DateTime\",\n build AS \"try_build: BuildModel\"\nFROM pull_request as pr\nLEFT JOIN build ON pr.build_id = build.id\nWHERE build.id = $1\n", + "query": "\nSELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.priority,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.rollup as \"rollup: RollupMode\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\nFROM pull_request as pr\nLEFT JOIN build AS try_build ON pr.build_id = try_build.id\nLEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\nWHERE try_build.id = $1\n", "describe": { "columns": [ { @@ -113,6 +113,47 @@ } } } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } } ], "parameters": { @@ -135,8 +176,9 @@ false, true, false, + null, null ] }, - "hash": "6cbee85c41cbecc631748d10ac63fc1ea310e39edb0e93456c30f914b1d07445" + "hash": "662b16c5a8c1c4a708ba8f7602c3197542d6b73b6914670ab95984a306d311e1" } diff --git a/.sqlx/query-5dfd742dce68994fa90e7311ce0fa745fdd075768b9b2ef672ea36dafbcab620.json b/.sqlx/query-8c40e5fa6fe2717cd20ac5dffe9bf1021e414cdb7e218bb17916feb5dcc2e75f.json similarity index 76% rename from .sqlx/query-5dfd742dce68994fa90e7311ce0fa745fdd075768b9b2ef672ea36dafbcab620.json rename to .sqlx/query-8c40e5fa6fe2717cd20ac5dffe9bf1021e414cdb7e218bb17916feb5dcc2e75f.json index c852669c..0546da8e 100644 --- a/.sqlx/query-5dfd742dce68994fa90e7311ce0fa745fdd075768b9b2ef672ea36dafbcab620.json +++ b/.sqlx/query-8c40e5fa6fe2717cd20ac5dffe9bf1021e414cdb7e218bb17916feb5dcc2e75f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH upserted_pr AS (\n INSERT INTO pull_request (repository, number, title, author, assignees, base_branch, mergeable_state, status)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (repository, number)\n DO UPDATE SET\n title = $3,\n author = $4,\n assignees = $5,\n base_branch = $6,\n mergeable_state = $7,\n status = $8\n RETURNING *\n )\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n build AS \"try_build: BuildModel\"\n FROM upserted_pr as pr\n LEFT JOIN build ON pr.build_id = build.id\n ", + "query": "\n WITH upserted_pr AS (\n INSERT INTO pull_request (repository, number, title, author, assignees, base_branch, mergeable_state, status)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (repository, number)\n DO UPDATE SET\n title = $3,\n author = $4,\n assignees = $5,\n base_branch = $6,\n mergeable_state = $7,\n status = $8\n RETURNING *\n )\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM upserted_pr as pr\n LEFT JOIN build AS try_build ON pr.build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n ", "describe": { "columns": [ { @@ -113,6 +113,47 @@ } } } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } } ], "parameters": { @@ -142,8 +183,9 @@ false, false, false, + true, true ] }, - "hash": "5dfd742dce68994fa90e7311ce0fa745fdd075768b9b2ef672ea36dafbcab620" + "hash": "8c40e5fa6fe2717cd20ac5dffe9bf1021e414cdb7e218bb17916feb5dcc2e75f" } diff --git a/.sqlx/query-9174f0f76f57b4cdfcbc3b40310643169c56424f25df617bdd84739e1b6ec5e1.json b/.sqlx/query-97120f11a5bfe7280daa2d1393af11fb9430b377645c712c5bd1165d48577786.json similarity index 71% rename from .sqlx/query-9174f0f76f57b4cdfcbc3b40310643169c56424f25df617bdd84739e1b6ec5e1.json rename to .sqlx/query-97120f11a5bfe7280daa2d1393af11fb9430b377645c712c5bd1165d48577786.json index dacc31f2..77359526 100644 --- a/.sqlx/query-9174f0f76f57b4cdfcbc3b40310643169c56424f25df617bdd84739e1b6ec5e1.json +++ b/.sqlx/query-97120f11a5bfe7280daa2d1393af11fb9430b377645c712c5bd1165d48577786.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n build AS \"try_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build ON pr.build_id = build.id\n WHERE pr.repository = $1\n AND pr.base_branch = $2\n AND pr.status IN ('open', 'draft')\n ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n WHERE pr.repository = $1\n AND pr.status IN ('open', 'draft')\n ", "describe": { "columns": [ { @@ -113,11 +113,51 @@ } } } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } } ], "parameters": { "Left": [ - "Text", "Text" ] }, @@ -136,8 +176,9 @@ false, false, false, + null, null ] }, - "hash": "9174f0f76f57b4cdfcbc3b40310643169c56424f25df617bdd84739e1b6ec5e1" + "hash": "97120f11a5bfe7280daa2d1393af11fb9430b377645c712c5bd1165d48577786" } diff --git a/.sqlx/query-aa9f3483df0d2ab722603300368e19a9ae88785203c49bc4622a704d9bef220d.json b/.sqlx/query-aa9f3483df0d2ab722603300368e19a9ae88785203c49bc4622a704d9bef220d.json new file mode 100644 index 00000000..03d80317 --- /dev/null +++ b/.sqlx/query-aa9f3483df0d2ab722603300368e19a9ae88785203c49bc4622a704d9bef220d.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE pull_request SET auto_build_id = NULL WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + }, + "hash": "aa9f3483df0d2ab722603300368e19a9ae88785203c49bc4622a704d9bef220d" +} diff --git a/.sqlx/query-460e193af9b13cc12ec9146505df3e151700fcc5ec23e54aee5e8cbe530789b2.json b/.sqlx/query-acf2a1ece0d2b2de4944b63f091391fd9099b9a60d9b9205d785fb90bdcee9d0.json similarity index 70% rename from .sqlx/query-460e193af9b13cc12ec9146505df3e151700fcc5ec23e54aee5e8cbe530789b2.json rename to .sqlx/query-acf2a1ece0d2b2de4944b63f091391fd9099b9a60d9b9205d785fb90bdcee9d0.json index ce38be28..ccaa7469 100644 --- a/.sqlx/query-460e193af9b13cc12ec9146505df3e151700fcc5ec23e54aee5e8cbe530789b2.json +++ b/.sqlx/query-acf2a1ece0d2b2de4944b63f091391fd9099b9a60d9b9205d785fb90bdcee9d0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n build AS \"try_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build ON pr.build_id = build.id\n WHERE pr.repository = $1\n AND pr.mergeable_state = 'unknown'\n AND pr.status IN ('open', 'draft')\n ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n WHERE pr.repository = $1\n AND pr.mergeable_state = 'unknown'\n AND pr.status IN ('open', 'draft')\n ", "describe": { "columns": [ { @@ -113,6 +113,47 @@ } } } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } } ], "parameters": { @@ -135,8 +176,9 @@ false, false, false, + null, null ] }, - "hash": "460e193af9b13cc12ec9146505df3e151700fcc5ec23e54aee5e8cbe530789b2" + "hash": "acf2a1ece0d2b2de4944b63f091391fd9099b9a60d9b9205d785fb90bdcee9d0" } diff --git a/.sqlx/query-b17fe5f20832f55d485aa838e1b287fba9184d2b767dc9d8dd1c895c3172a4c0.json b/.sqlx/query-b17fe5f20832f55d485aa838e1b287fba9184d2b767dc9d8dd1c895c3172a4c0.json new file mode 100644 index 00000000..7ff7d25c --- /dev/null +++ b/.sqlx/query-b17fe5f20832f55d485aa838e1b287fba9184d2b767dc9d8dd1c895c3172a4c0.json @@ -0,0 +1,185 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n WHERE pr.repository = $1\n AND pr.status = 'open'\n AND pr.approved_by IS NOT NULL\n AND pr.mergeable_state = 'mergeable'\n -- Exclude PRs with failed auto builds\n AND (auto_build.status IS NULL OR auto_build.status NOT IN ('failure', 'cancelled', 'timeouted'))\n -- Tree closure check (if tree_priority is set)\n AND ($2::int IS NULL OR pr.priority >= $2)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "repository: GithubRepoName", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "number!: i64", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "title", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "author", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "assignees: Assignees", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "approval_status!: ApprovalStatus", + "type_info": "Record" + }, + { + "ordinal": 7, + "name": "pr_status: PullRequestStatus", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "rollup: RollupMode", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "delegated_permission: DelegatedPermission", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "base_branch", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "mergeable_state: MergeableState", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "created_at: DateTime", + "type_info": "Timestamptz" + }, + { + "ordinal": 14, + "name": "try_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + null, + false, + true, + true, + true, + false, + false, + false, + null, + null + ] + }, + "hash": "b17fe5f20832f55d485aa838e1b287fba9184d2b767dc9d8dd1c895c3172a4c0" +} diff --git a/.sqlx/query-0614a7f86fae0f18c6955ee322848955149b065a69b85b662b747687799bbc66.json b/.sqlx/query-c2e28346b7e7102194e0cf188b925e212f389fb9ed0f5f59f44524944b874bf6.json similarity index 72% rename from .sqlx/query-0614a7f86fae0f18c6955ee322848955149b065a69b85b662b747687799bbc66.json rename to .sqlx/query-c2e28346b7e7102194e0cf188b925e212f389fb9ed0f5f59f44524944b874bf6.json index 7e1a3b67..1704e394 100644 --- a/.sqlx/query-0614a7f86fae0f18c6955ee322848955149b065a69b85b662b747687799bbc66.json +++ b/.sqlx/query-c2e28346b7e7102194e0cf188b925e212f389fb9ed0f5f59f44524944b874bf6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n build AS \"try_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build ON pr.build_id = build.id\n WHERE pr.repository = $1 AND\n pr.number = $2\n ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n WHERE pr.repository = $1 AND\n pr.number = $2\n ", "describe": { "columns": [ { @@ -113,6 +113,47 @@ } } } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } } ], "parameters": { @@ -136,8 +177,9 @@ false, false, false, + null, null ] }, - "hash": "0614a7f86fae0f18c6955ee322848955149b065a69b85b662b747687799bbc66" + "hash": "c2e28346b7e7102194e0cf188b925e212f389fb9ed0f5f59f44524944b874bf6" } diff --git a/.sqlx/query-676d0b8475eebb8ccbfbd877b41b2a8b1f2f4f6d5e2662db9d5c2e02d613c769.json b/.sqlx/query-f29db2081e4deeadf82dccc8cae7f5fa13976b0bd65c655ecd32070a1942d4b9.json similarity index 70% rename from .sqlx/query-676d0b8475eebb8ccbfbd877b41b2a8b1f2f4f6d5e2662db9d5c2e02d613c769.json rename to .sqlx/query-f29db2081e4deeadf82dccc8cae7f5fa13976b0bd65c655ecd32070a1942d4b9.json index 52fc4d53..fa5d8094 100644 --- a/.sqlx/query-676d0b8475eebb8ccbfbd877b41b2a8b1f2f4f6d5e2662db9d5c2e02d613c769.json +++ b/.sqlx/query-f29db2081e4deeadf82dccc8cae7f5fa13976b0bd65c655ecd32070a1942d4b9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n build AS \"try_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build ON pr.build_id = build.id\n WHERE pr.repository = $1\n AND pr.status IN ('open', 'draft')\n ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number!: i64\",\n pr.title,\n pr.author,\n pr.assignees as \"assignees: Assignees\",\n (\n pr.approved_by,\n pr.approved_sha\n ) AS \"approval_status!: ApprovalStatus\",\n pr.status as \"pr_status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.delegated_permission as \"delegated_permission: DelegatedPermission\",\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n WHERE pr.repository = $1\n AND pr.base_branch = $2\n AND pr.status IN ('open', 'draft')\n ", "describe": { "columns": [ { @@ -113,10 +113,52 @@ } } } + }, + { + "ordinal": 15, + "name": "auto_build: BuildModel", + "type_info": { + "Custom": { + "name": "build", + "kind": { + "Composite": [ + [ + "id", + "Int4" + ], + [ + "repository", + "Text" + ], + [ + "branch", + "Text" + ], + [ + "commit_sha", + "Text" + ], + [ + "status", + "Text" + ], + [ + "parent", + "Text" + ], + [ + "created_at", + "Timestamptz" + ] + ] + } + } + } } ], "parameters": { "Left": [ + "Text", "Text" ] }, @@ -135,8 +177,9 @@ false, false, false, + null, null ] }, - "hash": "676d0b8475eebb8ccbfbd877b41b2a8b1f2f4f6d5e2662db9d5c2e02d613c769" + "hash": "f29db2081e4deeadf82dccc8cae7f5fa13976b0bd65c655ecd32070a1942d4b9" } diff --git a/migrations/20250617160331_add_auto_build_to_pull_request.down.sql b/migrations/20250617160331_add_auto_build_to_pull_request.down.sql new file mode 100644 index 00000000..47fd9146 --- /dev/null +++ b/migrations/20250617160331_add_auto_build_to_pull_request.down.sql @@ -0,0 +1,2 @@ +-- Add down migration script here +ALTER TABLE pull_request DROP COLUMN auto_build_id; diff --git a/migrations/20250617160331_add_auto_build_to_pull_request.up.sql b/migrations/20250617160331_add_auto_build_to_pull_request.up.sql new file mode 100644 index 00000000..0c81d679 --- /dev/null +++ b/migrations/20250617160331_add_auto_build_to_pull_request.up.sql @@ -0,0 +1,2 @@ +-- Add up migration script here +ALTER TABLE pull_request ADD COLUMN auto_build_id INTEGER REFERENCES build(id); diff --git a/rust-bors.example.toml b/rust-bors.example.toml index 61a14252..e4348688 100644 --- a/rust-bors.example.toml +++ b/rust-bors.example.toml @@ -8,9 +8,15 @@ timeout = 3600 # - try: Try build has started # - try_succeed: Try build has finished # - try_failed: Try build has failed +# - auto: Auto build has started +# - auto_succeed: Auto build has finished +# - auto_failed: Auto build has failed # (Optional) [labels] approve = ["+approved"] try = ["+foo", "-bar"] try_succeed = ["+foobar", "+foo", "+baz"] try_failed = [] +auto = ["+merge-testing"] +auto_succeed = ["+merged", "-merge-testing"] +auto_failed = ["+merge-failed", "-merge-testing"] diff --git a/src/bin/bors.rs b/src/bin/bors.rs index 75536df2..bccc9aed 100644 --- a/src/bin/bors.rs +++ b/src/bin/bors.rs @@ -32,6 +32,9 @@ const MERGEABILITY_STATUS_INTERVAL: Duration = Duration::from_secs(60 * 10); /// How often should the bot synchronize PR state. const PR_STATE_PERIODIC_REFRESH: Duration = Duration::from_secs(60 * 10); +/// How often should the bot process the merge queue. +const MERGE_QUEUE_INTERVAL: Duration = Duration::from_secs(30); + #[derive(clap::Parser)] struct Opts { /// Github App ID. @@ -157,6 +160,7 @@ fn try_main(opts: Opts) -> anyhow::Result<()> { let mut cancel_builds_refresh = make_interval(CANCEL_TIMED_OUT_BUILDS_INTERVAL); let mut mergeability_status_refresh = make_interval(MERGEABILITY_STATUS_INTERVAL); let mut prs_interval = make_interval(PR_STATE_PERIODIC_REFRESH); + let mut merge_queue_interval = make_interval(MERGE_QUEUE_INTERVAL); loop { tokio::select! { _ = config_refresh.tick() => { @@ -174,6 +178,9 @@ fn try_main(opts: Opts) -> anyhow::Result<()> { _ = prs_interval.tick() => { refresh_tx.send(BorsGlobalEvent::RefreshPullRequestState).await?; } + _ = merge_queue_interval.tick() => { + refresh_tx.send(BorsGlobalEvent::ProcessMergeQueue).await?; + } } } }; diff --git a/src/bors/comment.rs b/src/bors/comment.rs index 441c97e5..e1ca2d2f 100644 --- a/src/bors/comment.rs +++ b/src/bors/comment.rs @@ -181,3 +181,52 @@ fn list_workflows_status(workflows: &[WorkflowModel]) -> String { .collect::>() .join("\n") } + +pub fn auto_build_started_comment(head_sha: &CommitSha, merge_sha: &CommitSha) -> Comment { + Comment::new(format!( + ":hourglass: Testing commit {} with merge {}...", + head_sha, merge_sha + )) +} + +pub fn auto_build_succeeded_comment( + workflows: &[WorkflowModel], + approved_by: &str, + merge_sha: &CommitSha, + base_ref: &str, +) -> Comment { + let urls = workflows + .iter() + .map(|w| format!("[{}]({})", w.name, w.url)) + .collect::>() + .join(", "); + + Comment::new(format!( + ":sunny: Test successful - {}\nApproved by: {}\nPushing {} to {}...", + urls, approved_by, merge_sha, base_ref + )) +} + +pub fn auto_build_failed_comment(workflows: &[WorkflowModel]) -> Comment { + let failed_workflow = workflows + .iter() + .find(|w| w.status == WorkflowStatus::Failure); + + if let Some(workflow) = failed_workflow { + Comment::new(format!( + ":broken_heart: Test failed - [{}]({})", + workflow.name, workflow.url + )) + } else { + Comment::new(":broken_heart: Test failed".to_string()) + } +} + +pub fn auto_build_push_failed_comment(merge_sha: &CommitSha, base_branch: &str) -> Comment { + Comment::new(format!( + ":x: Failed to push commit `{}` to `{}` branch.\n\n\ +This may be a temporary GitHub issue and will be retried on the next queue run.\n\n\ +", + merge_sha, base_branch + )) +} diff --git a/src/bors/event.rs b/src/bors/event.rs index 24f764ab..26542039 100644 --- a/src/bors/event.rs +++ b/src/bors/event.rs @@ -75,6 +75,8 @@ pub enum BorsGlobalEvent { RefreshPullRequestMergeability, /// Periodic event that serves for synchronizing PR state. RefreshPullRequestState, + /// Process the merge queue. + ProcessMergeQueue, } #[derive(Debug)] diff --git a/src/bors/handlers/mod.rs b/src/bors/handlers/mod.rs index 92885fb4..c246effb 100644 --- a/src/bors/handlers/mod.rs +++ b/src/bors/handlers/mod.rs @@ -19,6 +19,7 @@ use crate::bors::handlers::trybuild::{TRY_BRANCH_NAME, command_try_build, comman use crate::bors::handlers::workflow::{ handle_check_suite_completed, handle_workflow_completed, handle_workflow_started, }; +use crate::bors::merge_queue::{AUTO_BRANCH_NAME, MergeQueueEvent}; use crate::bors::{BorsContext, Comment, RepositoryState}; use crate::database::{DelegatedPermission, PullRequestModel}; use crate::github::{GithubUser, PullRequest, PullRequestNumber}; @@ -33,13 +34,14 @@ use pr_events::{ }; use refresh::sync_pull_requests_state; use review::{command_delegate, command_set_priority, command_set_rollup, command_undelegate}; +use tokio::sync::mpsc; use tracing::Instrument; use super::mergeable_queue::MergeableQueueSender; mod help; mod info; -mod labels; +pub mod labels; mod ping; mod pr_events; mod refresh; @@ -51,6 +53,7 @@ mod workflow; pub async fn handle_bors_repository_event( event: BorsRepositoryEvent, ctx: Arc, + merge_queue_tx: mpsc::Sender, mergeable_queue_tx: MergeableQueueSender, ) -> anyhow::Result<()> { let db = Arc::clone(&ctx.db); @@ -81,7 +84,7 @@ pub async fn handle_bors_repository_event( author = comment.author.username ); let pr_number = comment.pr_number; - if let Err(error) = handle_comment(Arc::clone(&repo), db, ctx, comment) + if let Err(error) = handle_comment(Arc::clone(&repo), db, ctx, merge_queue_tx, comment) .instrument(span.clone()) .await { @@ -116,7 +119,7 @@ pub async fn handle_bors_repository_event( repo = payload.repository.to_string(), id = payload.run_id.into_inner() ); - handle_workflow_completed(repo, db, payload) + handle_workflow_completed(repo, db, merge_queue_tx, payload) .instrument(span.clone()) .await?; } @@ -125,7 +128,7 @@ pub async fn handle_bors_repository_event( "Check suite completed", repo = payload.repository.to_string(), ); - handle_check_suite_completed(repo, db, payload) + handle_check_suite_completed(repo, db, merge_queue_tx, payload) .instrument(span.clone()) .await?; } @@ -238,6 +241,7 @@ pub async fn handle_bors_global_event( gh_client: &Octocrab, team_api_client: &TeamApiClient, mergeable_queue_tx: MergeableQueueSender, + merge_queue_tx: mpsc::Sender, ) -> anyhow::Result<()> { let db = Arc::clone(&ctx.db); match event { @@ -313,6 +317,9 @@ pub async fn handle_bors_global_event( #[cfg(test)] crate::bors::WAIT_FOR_PR_STATUS_REFRESH.mark(); } + BorsGlobalEvent::ProcessMergeQueue => { + merge_queue_tx.send(()).await?; + } } Ok(()) } @@ -347,6 +354,7 @@ async fn handle_comment( repo: Arc, database: Arc, ctx: Arc, + merge_queue_tx: mpsc::Sender, comment: PullRequestComment, ) -> anyhow::Result<()> { let pr_number = comment.pr_number; @@ -367,6 +375,8 @@ async fn handle_comment( .with_context(|| format!("Cannot get information about PR {pr_number}"))?; for command in commands { + let merge_queue_tx = merge_queue_tx.clone(); + match command { Ok(command) => { // Reload the PR state from DB, because a previous command might have changed it. @@ -392,6 +402,7 @@ async fn handle_comment( command_approve( repo, database, + merge_queue_tx, &pr, &comment.author, &approver, @@ -514,6 +525,7 @@ async fn handle_comment( } } } + Ok(()) } @@ -549,7 +561,7 @@ async fn reload_repos( /// Is this branch interesting for the bot? fn is_bors_observed_branch(branch: &str) -> bool { - branch == TRY_BRANCH_NAME + matches!(branch, TRY_BRANCH_NAME | AUTO_BRANCH_NAME) } /// Deny permission for a request. diff --git a/src/bors/handlers/pr_events.rs b/src/bors/handlers/pr_events.rs index 2d8759bd..b1d412a1 100644 --- a/src/bors/handlers/pr_events.rs +++ b/src/bors/handlers/pr_events.rs @@ -5,9 +5,10 @@ use crate::bors::event::{ PullRequestReopened, PullRequestUnassigned, PushToBranch, }; use crate::bors::handlers::labels::handle_label_trigger; +use crate::bors::handlers::trybuild::cancel_build_workflows; use crate::bors::mergeable_queue::MergeableQueueSender; use crate::bors::{Comment, PullRequestStatus, RepositoryState}; -use crate::database::MergeableState; +use crate::database::{BuildStatus, MergeableState, PullRequestModel}; use crate::github::{CommitSha, LabelTrigger, PullRequestNumber}; use std::sync::Arc; @@ -53,6 +54,8 @@ pub(super) async fn handle_push_to_pull_request( mergeable_queue.enqueue(repo_state.repository().clone(), pr_number); + clear_auto_build_for_pr(&repo_state, &db, &pr_model).await?; + if !pr_model.is_approved() { return Ok(()); } @@ -261,6 +264,33 @@ PR will need to be re-approved."#, .await } +async fn clear_auto_build_for_pr( + repo_state: &RepositoryState, + db: &PgDbClient, + pr_model: &PullRequestModel, +) -> anyhow::Result<()> { + let Some(auto_build) = &pr_model.auto_build else { + return Ok(()); + }; + + if auto_build.status == BuildStatus::Pending { + tracing::info!( + "Cancelling auto build for PR {} due to push", + pr_model.number + ); + + cancel_build_workflows(&repo_state.client, db, auto_build).await?; + } + + tracing::info!( + "Clearing auto build reference for PR {} due to push", + pr_model.number + ); + db.clear_pr_auto_build(pr_model).await?; + + Ok(()) +} + #[cfg(test)] mod tests { use crate::bors::PullRequestStatus; diff --git a/src/bors/handlers/review.rs b/src/bors/handlers/review.rs index 1e7577d3..e061a576 100644 --- a/src/bors/handlers/review.rs +++ b/src/bors/handlers/review.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use tokio::sync::mpsc; + use crate::PgDbClient; use crate::bors::Comment; use crate::bors::RepositoryState; @@ -8,6 +10,7 @@ use crate::bors::command::RollupMode; use crate::bors::handlers::has_permission; use crate::bors::handlers::labels::handle_label_trigger; use crate::bors::handlers::{PullRequestData, deny_request}; +use crate::bors::merge_queue::MergeQueueEvent; use crate::database::ApprovalInfo; use crate::database::DelegatedPermission; use crate::database::TreeState; @@ -17,9 +20,11 @@ use crate::permissions::PermissionType; /// Approve a pull request. /// A pull request can only be approved by a user of sufficient authority. +#[allow(clippy::too_many_arguments)] pub(super) async fn command_approve( repo_state: Arc, db: Arc, + merge_queue_tx: mpsc::Sender, pr: &PullRequestData, author: &GithubUser, approver: &Approver, @@ -42,7 +47,9 @@ pub(super) async fn command_approve( db.approve(&pr.db, approval_info, priority, rollup).await?; handle_label_trigger(&repo_state, pr.number(), LabelTrigger::Approved).await?; - notify_of_approval(&repo_state, pr, approver.as_str()).await + notify_of_approval(&repo_state, pr, approver.as_str()).await?; + merge_queue_tx.send(()).await?; + Ok(()) } /// Unapprove a pull request. diff --git a/src/bors/handlers/workflow.rs b/src/bors/handlers/workflow.rs index 09adb4b4..dd87aea3 100644 --- a/src/bors/handlers/workflow.rs +++ b/src/bors/handlers/workflow.rs @@ -1,13 +1,24 @@ use std::sync::Arc; use std::time::Duration; +use octocrab::models::StatusState; +use tokio::sync::mpsc; + use crate::PgDbClient; use crate::bors::CheckSuiteStatus; use crate::bors::RepositoryState; +use crate::bors::comment::auto_build_failed_comment; +use crate::bors::comment::auto_build_succeeded_comment; use crate::bors::comment::{try_build_succeeded_comment, workflow_failed_comment}; use crate::bors::event::{CheckSuiteCompleted, WorkflowCompleted, WorkflowStarted}; +use crate::bors::handlers::TRY_BRANCH_NAME; use crate::bors::handlers::is_bors_observed_branch; use crate::bors::handlers::labels::handle_label_trigger; +use crate::bors::merge_queue::AUTO_BRANCH_NAME; +use crate::bors::merge_queue::MergeQueueEvent; +use crate::database::BuildModel; +use crate::database::PullRequestModel; +use crate::database::WorkflowModel; use crate::database::{BuildStatus, WorkflowStatus}; use crate::github::LabelTrigger; @@ -62,6 +73,7 @@ pub(super) async fn handle_workflow_started( pub(super) async fn handle_workflow_completed( repo: Arc, db: Arc, + merge_queue_tx: mpsc::Sender, mut payload: WorkflowCompleted, ) -> anyhow::Result<()> { if !is_bors_observed_branch(&payload.branch) { @@ -95,12 +107,13 @@ pub(super) async fn handle_workflow_completed( branch: payload.branch, commit_sha: payload.commit_sha, }; - try_complete_build(repo.as_ref(), db.as_ref(), event).await + try_complete_build(repo.as_ref(), db.as_ref(), merge_queue_tx, event).await } pub(super) async fn handle_check_suite_completed( repo: Arc, db: Arc, + merge_queue_tx: mpsc::Sender, payload: CheckSuiteCompleted, ) -> anyhow::Result<()> { if !is_bors_observed_branch(&payload.branch) { @@ -112,13 +125,15 @@ pub(super) async fn handle_check_suite_completed( payload.branch, payload.commit_sha ); - try_complete_build(repo.as_ref(), db.as_ref(), payload).await + + try_complete_build(repo.as_ref(), db.as_ref(), merge_queue_tx, payload).await } /// Try to complete a pending build. async fn try_complete_build( repo: &RepositoryState, db: &PgDbClient, + merge_queue_tx: mpsc::Sender, payload: CheckSuiteCompleted, ) -> anyhow::Result<()> { if !is_bors_observed_branch(&payload.branch) { @@ -184,15 +199,40 @@ async fn try_complete_build( return Ok(()); } - let (status, trigger) = if has_failure { - (BuildStatus::Failure, LabelTrigger::TryBuildFailed) - } else { - (BuildStatus::Success, LabelTrigger::TryBuildSucceeded) + let branch = payload.branch.as_str(); + let (status, trigger) = match (branch, has_failure) { + (TRY_BRANCH_NAME, true) => (BuildStatus::Failure, LabelTrigger::TryBuildFailed), + (TRY_BRANCH_NAME, false) => (BuildStatus::Success, LabelTrigger::TryBuildSucceeded), + (AUTO_BRANCH_NAME, true) => (BuildStatus::Failure, LabelTrigger::AutoBuildFailed), + (AUTO_BRANCH_NAME, false) => (BuildStatus::Success, LabelTrigger::AutoBuildSucceeded), + _ => unreachable!(), }; - db.update_build_status(&build, status).await?; + db.update_build_status(&build, status).await?; handle_label_trigger(repo, pr.number, trigger).await?; + match branch { + TRY_BRANCH_NAME => { + complete_try_build(repo, pr, build, workflows, has_failure, payload).await? + } + AUTO_BRANCH_NAME => { + complete_auto_build(repo, pr, workflows, has_failure, payload, merge_queue_tx).await?; + } + _ => unreachable!("Branch should only be bors-observed branch"), + } + + Ok(()) +} + +/// Complete the try build workflow. +async fn complete_try_build( + repo: &RepositoryState, + pr: PullRequestModel, + build: BuildModel, + workflows: Vec, + has_failure: bool, + payload: CheckSuiteCompleted, +) -> anyhow::Result<()> { let message = if !has_failure { tracing::info!("Workflow succeeded"); try_build_succeeded_comment(&workflows, payload.commit_sha, &build) @@ -205,6 +245,51 @@ async fn try_complete_build( Ok(()) } +/// Complete the auto build workflow. +async fn complete_auto_build( + repo: &RepositoryState, + pr: PullRequestModel, + workflows: Vec, + has_failure: bool, + payload: CheckSuiteCompleted, + merge_queue_tx: mpsc::Sender, +) -> anyhow::Result<()> { + let comment = if !has_failure { + tracing::info!("Auto build failed"); + auto_build_failed_comment(&workflows) + } else { + tracing::info!("Auto build succeeded"); + auto_build_succeeded_comment( + &workflows, + pr.approver().unwrap_or(""), + &payload.commit_sha, + &pr.base_branch, + ) + }; + repo.client.post_comment(pr.number, comment).await?; + + let (gh_status, gh_desc) = if !has_failure { + (StatusState::Failure, "Build failed") + } else { + (StatusState::Success, "Build succeeded") + }; + + let gh_pr = repo.client.get_pull_request(pr.number).await?; + repo.client + .create_commit_status( + &gh_pr.head.sha, + gh_status, + None, + Some(gh_desc), + Some("bors"), + ) + .await?; + + merge_queue_tx.send(()).await?; + + Ok(()) +} + #[cfg(test)] mod tests { use crate::bors::handlers::trybuild::TRY_BRANCH_NAME; diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs new file mode 100644 index 00000000..9dbbabc6 --- /dev/null +++ b/src/bors/merge_queue.rs @@ -0,0 +1,243 @@ +use anyhow::anyhow; +use octocrab::models::StatusState; + +use std::sync::Arc; + +use crate::{ + BorsContext, + bors::{ + PullRequestStatus, RepositoryState, + comment::{ + auto_build_push_failed_comment, auto_build_started_comment, merge_conflict_comment, + }, + handlers::labels::handle_label_trigger, + }, + database::{BuildStatus, MergeableState, PullRequestModel}, + github::{CommitSha, LabelTrigger, MergeError, api::client::GithubRepositoryClient}, + utils::sort_queue::sort_queue_prs, +}; + +/// Branch used for performing merge operations. +/// This branch should not run CI checks. +pub(super) const AUTO_MERGE_BRANCH_NAME: &str = "automation/bors/auto-merge"; + +/// Branch where CI checks run for merge builds. +/// This branch should run CI checks. +pub(super) const AUTO_BRANCH_NAME: &str = "automation/bors/auto"; + +pub type MergeQueueEvent = (); + +enum MergeResult { + Success(CommitSha), + Conflict, +} + +pub async fn handle_merge_queue(ctx: Arc) -> anyhow::Result<()> { + let repos: Vec> = + ctx.repositories.read().unwrap().values().cloned().collect(); + + for repo in repos { + let repo_name = repo.repository(); + let repo_db = match ctx.db.repo_db(repo_name).await? { + Some(repo) => repo, + None => { + tracing::error!("Repository {} not found", repo_name); + continue; + } + }; + let priority = repo_db.tree_state.priority(); + let prs = ctx.db.get_merge_queue_prs(repo_name, priority).await?; + + // Sort PRs according to merge queue priority rules. + // Pending PRs come first - this is important as we make sure to block the queue to + // prevent starting simultaneous auto-builds. + let prs = sort_queue_prs(prs); + + for pr in prs { + let pr_num = pr.number; + + if let Some(auto_build) = &pr.auto_build { + let commit_sha = CommitSha(auto_build.commit_sha.clone()); + + match auto_build.status { + // Build in progress - stop queue. We can only have one PR built at a time. + BuildStatus::Pending => { + tracing::debug!( + "PR {repo_name}/{pr_num} has a pending build - blocking queue" + ); + break; + } + // Build successful - point the base branch to the merged commit. + BuildStatus::Success => { + match repo + .client + .set_branch_to_sha(&pr.base_branch, &commit_sha) + .await + { + Ok(()) => { + tracing::info!("Auto build succeeded and merged"); + ctx.db + .set_pr_status( + &pr.repository, + pr.number, + PullRequestStatus::Merged, + ) + .await?; + } + Err(e) => { + tracing::error!("Failed to push to base branch: {:?}", e); + + let comment = + auto_build_push_failed_comment(&commit_sha, &pr.base_branch); + repo.client.post_comment(pr.number, comment).await?; + + // Continue to the next PR and keep it in the queue. + continue; + } + }; + } + BuildStatus::Failure | BuildStatus::Cancelled | BuildStatus::Timeouted => { + unreachable!("Failed auto builds should be filtered out by SQL query"); + } + } + } + + // No build exists for this PR - try to start a new merge build. + match start_auto_build(&repo, &ctx, pr).await { + Ok(true) => { + tracing::info!("Starting merge build for PR {pr_num}"); + // We can only have one PR being built at a time - block the queue. + break; + } + Ok(false) => { + // Failed due to issue with the PR (e.g. merge conflicts). + tracing::debug!("Failed to start merge build for PR {pr_num}"); + continue; + } + Err(error) => { + // Unexpected error - the PR will remain in the "queue" for a retry. + tracing::error!("Error starting merge build for PR {pr_num}: {:?}", error); + continue; + } + } + } + } + + Ok(()) +} + +/// Starts a new auto build for a pull request. +async fn start_auto_build( + repo: &Arc, + ctx: &Arc, + pr: PullRequestModel, +) -> anyhow::Result { + let client = &repo.client; + + let gh_pr = client.get_pull_request(pr.number).await?; + let base_sha = client.get_branch_sha(&pr.base_branch).await?; + + let auto_merge_commit_message = format!( + "Auto merge of #{} - {}, r={}\n\n{}\n\n{}", + pr.number, + gh_pr.head_label, + pr.approver().unwrap_or(""), + pr.title, + gh_pr.message + ); + + // 1. Merge PR head with base branch on `AUTO_MERGE_BRANCH_NAME` + match attempt_merge( + &repo.client, + &gh_pr.head.sha, + &base_sha, + &auto_merge_commit_message, + ) + .await? + { + MergeResult::Success(merge_sha) => { + // 2. Push merge commit to `AUTO_BRANCH_NAME` where CI runs + client + .set_branch_to_sha(AUTO_BRANCH_NAME, &merge_sha) + .await?; + + // 3. Record the build in the database + ctx.db + .attach_auto_build( + &pr, + AUTO_BRANCH_NAME.to_string(), + merge_sha.clone(), + base_sha, + ) + .await?; + + // 4. Update label + handle_label_trigger(repo, pr.number, LabelTrigger::AutoBuildStarted).await?; + + // 5. Post status comment + let comment = auto_build_started_comment(&gh_pr.head.sha, &merge_sha); + client.post_comment(pr.number, comment).await?; + + // 6. Set GitHub commit status to pending on PR head + let desc = format!( + "Testing commit {} with merge {}...", + gh_pr.head.sha, merge_sha + ); + client + .create_commit_status( + &gh_pr.head.sha, + StatusState::Pending, + None, + Some(&desc), + Some("bors"), + ) + .await?; + + Ok(true) + } + MergeResult::Conflict => { + repo.client + .post_comment(pr.number, merge_conflict_comment(&gh_pr.head.name)) + .await?; + ctx.db + .update_pr_mergeable_state(&pr, MergeableState::HasConflicts) + .await?; + + Ok(false) + } + } +} + +/// Attempts to merge the given head SHA with base SHA via `AUTO_MERGE_BRANCH_NAME`. +async fn attempt_merge( + client: &GithubRepositoryClient, + head_sha: &CommitSha, + base_sha: &CommitSha, + merge_message: &str, +) -> anyhow::Result { + tracing::debug!("Attempting to merge with base SHA {base_sha}"); + + // Reset auto merge branch to point to base branch + client + .set_branch_to_sha(AUTO_MERGE_BRANCH_NAME, base_sha) + .await + .map_err(|error| anyhow!("Cannot set auto merge branch to {}: {error:?}", base_sha.0))?; + + // then merge PR head commit into auto merge branch. + match client + .merge_branches(AUTO_MERGE_BRANCH_NAME, head_sha, merge_message) + .await + { + Ok(merge_sha) => { + tracing::debug!("Merge successful, SHA: {merge_sha}"); + + Ok(MergeResult::Success(merge_sha)) + } + Err(MergeError::Conflict) => { + tracing::warn!("Merge conflict"); + + Ok(MergeResult::Conflict) + } + Err(error) => Err(error.into()), + } +} diff --git a/src/bors/mod.rs b/src/bors/mod.rs index e94b6fba..39c3413c 100644 --- a/src/bors/mod.rs +++ b/src/bors/mod.rs @@ -22,6 +22,7 @@ pub mod comment; mod context; pub mod event; mod handlers; +pub mod merge_queue; pub mod mergeable_queue; #[cfg(test)] diff --git a/src/config.rs b/src/config.rs index ad31448a..81fc5ca8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,6 +58,9 @@ where Try, TrySucceed, TryFailed, + Auto, + AutoSucceed, + AutoFailed, } impl From for LabelTrigger { @@ -68,6 +71,9 @@ where Trigger::Try => LabelTrigger::TryBuildStarted, Trigger::TrySucceed => LabelTrigger::TryBuildSucceeded, Trigger::TryFailed => LabelTrigger::TryBuildFailed, + Trigger::Auto => LabelTrigger::AutoBuildStarted, + Trigger::AutoSucceed => LabelTrigger::AutoBuildSucceeded, + Trigger::AutoFailed => LabelTrigger::AutoBuildFailed, } } } diff --git a/src/database/client.rs b/src/database/client.rs index 008ebf43..abaaff77 100644 --- a/src/database/client.rs +++ b/src/database/client.rs @@ -9,15 +9,15 @@ use crate::github::PullRequestNumber; use crate::github::{CommitSha, GithubRepoName}; use super::operations::{ - approve_pull_request, create_build, create_pull_request, create_workflow, - delegate_pull_request, find_build, find_pr_by_build, get_nonclosed_pull_requests, - get_nonclosed_pull_requests_by_base_branch, get_prs_with_unknown_mergeable_state, - get_pull_request, get_repository, get_repository_by_name, get_running_builds, - get_workflow_urls_for_build, get_workflows_for_build, insert_repo_if_not_exists, - set_pr_assignees, set_pr_priority, set_pr_rollup, set_pr_status, unapprove_pull_request, - undelegate_pull_request, update_build_status, update_mergeable_states_by_base_branch, - update_pr_build_id, update_pr_mergeable_state, update_workflow_status, upsert_pull_request, - upsert_repository, + approve_pull_request, clear_pr_auto_build, create_build, create_pull_request, create_workflow, + delegate_pull_request, find_build, find_pr_by_build, get_merge_queue_prs, + get_nonclosed_pull_requests, get_nonclosed_pull_requests_by_base_branch, + get_prs_with_unknown_mergeable_state, get_pull_request, get_repository, get_repository_by_name, + get_running_builds, get_workflow_urls_for_build, get_workflows_for_build, + insert_repo_if_not_exists, set_pr_assignees, set_pr_priority, set_pr_rollup, set_pr_status, + unapprove_pull_request, undelegate_pull_request, update_build_status, + update_mergeable_states_by_base_branch, update_pr_auto_build_id, update_pr_build_id, + update_pr_mergeable_state, update_workflow_status, upsert_pull_request, upsert_repository, }; use super::{ApprovalInfo, DelegatedPermission, MergeableState, RunId, UpsertPullRequestParams}; @@ -190,6 +190,21 @@ impl PgDbClient { Ok(()) } + pub async fn attach_auto_build( + &self, + pr: &PullRequestModel, + branch: String, + commit_sha: CommitSha, + parent: CommitSha, + ) -> anyhow::Result<()> { + let mut tx = self.pool.begin().await?; + let build_id = + create_build(&mut *tx, &pr.repository, &branch, &commit_sha, &parent).await?; + update_pr_auto_build_id(&mut *tx, pr.id, build_id).await?; + tx.commit().await?; + Ok(()) + } + pub async fn find_build( &self, repo: &GithubRepoName, @@ -296,4 +311,16 @@ impl PgDbClient { ) -> anyhow::Result<()> { upsert_repository(&self.pool, repo, tree_state).await } + + pub async fn get_merge_queue_prs( + &self, + repo: &GithubRepoName, + tree_priority: Option, + ) -> anyhow::Result> { + get_merge_queue_prs(&self.pool, repo, tree_priority.map(|p| p as i32)).await + } + + pub async fn clear_pr_auto_build(&self, pr: &PullRequestModel) -> anyhow::Result<()> { + clear_pr_auto_build(&self.pool, pr.id).await + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index faef042a..906880ab 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -329,6 +329,7 @@ pub struct PullRequestModel { pub priority: Option, pub rollup: Option, pub try_build: Option, + pub auto_build: Option, pub created_at: DateTime, } diff --git a/src/database/operations.rs b/src/database/operations.rs index 0d27d8b3..f1b2641b 100644 --- a/src/database/operations.rs +++ b/src/database/operations.rs @@ -53,9 +53,11 @@ pub(crate) async fn get_pull_request( pr.base_branch, pr.mergeable_state as "mergeable_state: MergeableState", pr.created_at as "created_at: DateTime", - build AS "try_build: BuildModel" + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" FROM pull_request as pr - LEFT JOIN build ON pr.build_id = build.id + LEFT JOIN build AS try_build ON pr.build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id WHERE pr.repository = $1 AND pr.number = $2 "#, @@ -163,9 +165,11 @@ pub(crate) async fn upsert_pull_request( pr.base_branch, pr.mergeable_state as "mergeable_state: MergeableState", pr.created_at as "created_at: DateTime", - build AS "try_build: BuildModel" + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" FROM upserted_pr as pr - LEFT JOIN build ON pr.build_id = build.id + LEFT JOIN build AS try_build ON pr.build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id "#, repo as &GithubRepoName, params.pr_number.0 as i32, @@ -212,9 +216,11 @@ pub(crate) async fn get_nonclosed_pull_requests_by_base_branch( pr.base_branch, pr.mergeable_state as "mergeable_state: MergeableState", pr.created_at as "created_at: DateTime", - build AS "try_build: BuildModel" + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" FROM pull_request as pr - LEFT JOIN build ON pr.build_id = build.id + LEFT JOIN build AS try_build ON pr.build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id WHERE pr.repository = $1 AND pr.base_branch = $2 AND pr.status IN ('open', 'draft') @@ -256,9 +262,11 @@ pub(crate) async fn get_nonclosed_pull_requests( pr.base_branch, pr.mergeable_state as "mergeable_state: MergeableState", pr.created_at as "created_at: DateTime", - build AS "try_build: BuildModel" + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" FROM pull_request as pr - LEFT JOIN build ON pr.build_id = build.id + LEFT JOIN build AS try_build ON pr.build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id WHERE pr.repository = $1 AND pr.status IN ('open', 'draft') "#, @@ -318,9 +326,11 @@ pub(crate) async fn get_prs_with_unknown_mergeable_state( pr.base_branch, pr.mergeable_state as "mergeable_state: MergeableState", pr.created_at as "created_at: DateTime", - build AS "try_build: BuildModel" + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" FROM pull_request as pr - LEFT JOIN build ON pr.build_id = build.id + LEFT JOIN build AS try_build ON pr.build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id WHERE pr.repository = $1 AND pr.mergeable_state = 'unknown' AND pr.status IN ('open', 'draft') @@ -470,10 +480,12 @@ SELECT pr.mergeable_state as "mergeable_state: MergeableState", pr.rollup as "rollup: RollupMode", pr.created_at as "created_at: DateTime", - build AS "try_build: BuildModel" + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" FROM pull_request as pr -LEFT JOIN build ON pr.build_id = build.id -WHERE build.id = $1 +LEFT JOIN build AS try_build ON pr.build_id = try_build.id +LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id +WHERE try_build.id = $1 "#, build_id ) @@ -503,6 +515,40 @@ pub(crate) async fn update_pr_build_id( .await } +pub(crate) async fn update_pr_auto_build_id( + executor: impl PgExecutor<'_>, + pr_id: i32, + build_id: i32, +) -> anyhow::Result<()> { + measure_db_query("update_pr_auto_build_id", || async { + sqlx::query!( + "UPDATE pull_request SET auto_build_id = $1 WHERE id = $2", + build_id, + pr_id + ) + .execute(executor) + .await?; + Ok(()) + }) + .await +} + +pub(crate) async fn clear_pr_auto_build( + executor: impl PgExecutor<'_>, + pr_id: i32, +) -> anyhow::Result<()> { + measure_db_query("clear_pr_auto_build", || async { + sqlx::query!( + "UPDATE pull_request SET auto_build_id = NULL WHERE id = $1", + pr_id + ) + .execute(executor) + .await?; + Ok(()) + }) + .await +} + pub(crate) async fn create_build( executor: impl PgExecutor<'_>, repo: &GithubRepoName, @@ -929,3 +975,54 @@ pub(crate) async fn upsert_repository( }) .await } + +pub(crate) async fn get_merge_queue_prs( + executor: impl PgExecutor<'_>, + repo: &GithubRepoName, + tree_priority: Option, +) -> anyhow::Result> { + measure_db_query("get_merge_queue_prs", || async { + let records = sqlx::query_as!( + PullRequestModel, + r#" + SELECT + pr.id, + pr.repository as "repository: GithubRepoName", + pr.number as "number!: i64", + pr.title, + pr.author, + pr.assignees as "assignees: Assignees", + ( + pr.approved_by, + pr.approved_sha + ) AS "approval_status!: ApprovalStatus", + pr.status as "pr_status: PullRequestStatus", + pr.priority, + pr.rollup as "rollup: RollupMode", + pr.delegated_permission as "delegated_permission: DelegatedPermission", + pr.base_branch, + pr.mergeable_state as "mergeable_state: MergeableState", + pr.created_at as "created_at: DateTime", + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel" + FROM pull_request as pr + LEFT JOIN build AS try_build ON pr.build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + WHERE pr.repository = $1 + AND pr.status = 'open' + AND pr.approved_by IS NOT NULL + AND pr.mergeable_state = 'mergeable' + -- Exclude PRs with failed auto builds + AND (auto_build.status IS NULL OR auto_build.status NOT IN ('failure', 'cancelled', 'timeouted')) + -- Tree closure check (if tree_priority is set) + AND ($2::int IS NULL OR pr.priority >= $2) + "#, + repo as &GithubRepoName, + tree_priority + ) + .fetch_all(executor) + .await?; + Ok(records) + }) + .await +} diff --git a/src/github/api/client.rs b/src/github/api/client.rs index a9ceebd0..88172e9a 100644 --- a/src/github/api/client.rs +++ b/src/github/api/client.rs @@ -1,5 +1,6 @@ use anyhow::Context; use octocrab::Octocrab; +pub use octocrab::models::StatusState; use octocrab::models::{App, Repository}; use tracing::log; @@ -342,6 +343,40 @@ impl GithubRepositoryClient { Ok(prs) } + /// Create a commit status for the given SHA. + pub async fn create_commit_status( + &self, + sha: &CommitSha, + state: StatusState, + target_url: Option<&str>, + description: Option<&str>, + context: Option<&str>, + ) -> anyhow::Result<()> { + perform_network_request_with_retry("create_commit_status", || async { + let repos = self + .client + .repos(&self.repository().owner, &self.repository().name); + let mut builder = repos.create_status(sha.0.clone(), state); + + if let Some(url) = target_url { + builder = builder.target(url.to_string()); + } + if let Some(desc) = description { + builder = builder.description(desc.to_string()); + } + if let Some(ctx) = context { + builder = builder.context(ctx.to_string()); + } + + builder + .send() + .await + .context("Cannot create commit status")?; + Ok(()) + }) + .await? + } + fn format_pr(&self, pr: PullRequestNumber) -> String { format!("{}/{}", self.repository(), pr) } diff --git a/src/github/labels.rs b/src/github/labels.rs index 337bb33b..f8d8ca18 100644 --- a/src/github/labels.rs +++ b/src/github/labels.rs @@ -6,6 +6,9 @@ pub enum LabelTrigger { TryBuildStarted, TryBuildSucceeded, TryBuildFailed, + AutoBuildStarted, + AutoBuildSucceeded, + AutoBuildFailed, } #[derive(Debug, Eq, PartialEq)] diff --git a/src/github/server.rs b/src/github/server.rs index 241d1b48..2134683f 100644 --- a/src/github/server.rs +++ b/src/github/server.rs @@ -1,4 +1,5 @@ use crate::bors::event::BorsEvent; +use crate::bors::merge_queue::{MergeQueueEvent, handle_merge_queue}; use crate::bors::mergeable_queue::{ MergeableQueueReceiver, MergeableQueueSender, create_mergeable_queue, handle_mergeable_queue_item, @@ -12,6 +13,7 @@ use crate::github::webhook::WebhookSecret; use crate::templates::{ HelpTemplate, HtmlTemplate, NotFoundTemplate, PullRequestStats, QueueTemplate, RepositoryView, }; +use crate::utils::sort_queue::sort_queue_prs; use crate::{BorsGlobalEvent, BorsRepositoryEvent, PgDbClient, TeamApiClient}; use super::AppError; @@ -132,6 +134,7 @@ async fn queue_handler( }; let prs = state.db.get_nonclosed_pull_requests(&repo.name).await?; + let prs = sort_queue_prs(prs); // TODO: add failed count let (approved_count, rolled_up_count) = prs.iter().fold((0, 0), |(approved, rolled_up), pr| { @@ -184,6 +187,7 @@ pub async fn github_webhook_handler( pub struct BorsProcess { pub repository_tx: mpsc::Sender, pub global_tx: mpsc::Sender, + pub merge_queue_tx: mpsc::Sender, pub mergeable_queue_tx: MergeableQueueSender, pub bors_process: Pin + Send>>, } @@ -197,9 +201,11 @@ pub fn create_bors_process( ) -> BorsProcess { let (repository_tx, repository_rx) = mpsc::channel::(1024); let (global_tx, global_rx) = mpsc::channel::(1024); + let (merge_queue_tx, merge_queue_rx) = mpsc::channel::<()>(128); let (mergeable_queue_tx, mergeable_queue_rx) = create_mergeable_queue(); let mq_tx = mergeable_queue_tx.clone(); + let merge_queue_tx_clone = merge_queue_tx.clone(); let service = async move { let ctx = Arc::new(ctx); @@ -211,8 +217,21 @@ pub fn create_bors_process( #[cfg(test)] { tokio::join!( - consume_repository_events(ctx.clone(), repository_rx, mq_tx.clone()), - consume_global_events(ctx.clone(), global_rx, mq_tx, gh_client, team_api), + consume_repository_events( + ctx.clone(), + repository_rx, + merge_queue_tx.clone(), + mq_tx.clone() + ), + consume_global_events( + ctx.clone(), + global_rx, + mq_tx.clone(), + merge_queue_tx, + gh_client, + team_api + ), + consume_merge_queue(ctx.clone(), merge_queue_rx), consume_mergeable_queue(ctx, mergeable_queue_rx) ); } @@ -222,12 +241,15 @@ pub fn create_bors_process( #[cfg(not(test))] { tokio::select! { - _ = consume_repository_events(ctx.clone(), repository_rx, mq_tx.clone()) => { + _ = consume_repository_events(ctx.clone(), repository_rx, merge_queue_tx.clone(), mq_tx.clone()) => { tracing::error!("Repository event handling process has ended"); } - _ = consume_global_events(ctx.clone(), global_rx, mq_tx, gh_client, team_api) => { + _ = consume_global_events(ctx.clone(), global_rx, mq_tx, merge_queue_tx, gh_client, team_api) => { tracing::error!("Global event handling process has ended"); } + _ = consume_merge_queue(ctx.clone(), merge_queue_rx) => { + tracing::error!("Merge queue handling process has ended"); + } _ = consume_mergeable_queue(ctx, mergeable_queue_rx) => { tracing::error!("Mergeable queue handling process has ended") } @@ -238,6 +260,7 @@ pub fn create_bors_process( BorsProcess { repository_tx, global_tx, + merge_queue_tx: merge_queue_tx_clone, mergeable_queue_tx, bors_process: Box::pin(service), } @@ -246,17 +269,20 @@ pub fn create_bors_process( async fn consume_repository_events( ctx: Arc, mut repository_rx: mpsc::Receiver, + merge_queue_tx: mpsc::Sender, mergeable_queue_tx: MergeableQueueSender, ) { while let Some(event) = repository_rx.recv().await { let ctx = ctx.clone(); let mergeable_queue_tx = mergeable_queue_tx.clone(); + let merge_queue_tx = merge_queue_tx.clone(); let span = tracing::info_span!("RepositoryEvent"); tracing::debug!("Received repository event: {event:#?}"); - if let Err(error) = handle_bors_repository_event(event, ctx, mergeable_queue_tx) - .instrument(span.clone()) - .await + if let Err(error) = + handle_bors_repository_event(event, ctx, merge_queue_tx, mergeable_queue_tx) + .instrument(span.clone()) + .await { handle_root_error(span, error); } @@ -267,6 +293,7 @@ async fn consume_global_events( ctx: Arc, mut global_rx: mpsc::Receiver, mergeable_queue_tx: MergeableQueueSender, + merge_queue_tx: mpsc::Sender, gh_client: Octocrab, team_api: TeamApiClient, ) { @@ -276,16 +303,47 @@ async fn consume_global_events( let span = tracing::info_span!("GlobalEvent"); tracing::debug!("Received global event: {event:#?}"); - if let Err(error) = - handle_bors_global_event(event, ctx, &gh_client, &team_api, mergeable_queue_tx) - .instrument(span.clone()) - .await + if let Err(error) = handle_bors_global_event( + event, + ctx, + &gh_client, + &team_api, + mergeable_queue_tx, + merge_queue_tx.clone(), + ) + .instrument(span.clone()) + .await { handle_root_error(span, error); } } } +async fn consume_merge_queue( + ctx: Arc, + mut merge_queue_rx: mpsc::Receiver, +) { + while merge_queue_rx.recv().await.is_some() { + let mut drained_count = 0; + // Drain any extras that have arrived. We can do this as we know the state of + // the queue has changed and this single run should capture those changes. + while let Ok(()) = merge_queue_rx.try_recv() { + drained_count += 1; + } + + let ctx = ctx.clone(); + + let span = tracing::info_span!("MergeQueue"); + tracing::debug!( + "Processing merge queue, drained {} extra events", + drained_count + ); + if let Err(error) = handle_merge_queue(ctx).instrument(span.clone()).await { + handle_root_error(span, error); + } + } +} + async fn consume_mergeable_queue( ctx: Arc, mergeable_queue_rx: MergeableQueueReceiver, diff --git a/src/tests/mocks/bors.rs b/src/tests/mocks/bors.rs index 450120cb..bcaa4211 100644 --- a/src/tests/mocks/bors.rs +++ b/src/tests/mocks/bors.rs @@ -16,6 +16,7 @@ use super::pull_request::{ GitHubPullRequestEventPayload, GitHubPushEventPayload, PullRequestChangeEvent, }; use super::repository::PullRequest; +use crate::bors::merge_queue::MergeQueueEvent; use crate::bors::mergeable_queue::MergeableQueueSender; use crate::bors::{ RollupMode, WAIT_FOR_CANCEL_TIMED_OUT_BUILDS_REFRESH, WAIT_FOR_MERGEABILITY_STATUS_REFRESH, @@ -102,6 +103,7 @@ pub struct BorsTester { http_mock: ExternalHttpMock, github: GitHubState, db: Arc, + merge_queue_tx: Sender, mergeable_queue_tx: MergeableQueueSender, // Sender for bors global events global_tx: Sender, @@ -133,6 +135,7 @@ impl BorsTester { repository_tx, global_tx, mergeable_queue_tx, + merge_queue_tx, bors_process, } = create_bors_process(ctx, mock.github_client(), mock.team_api_client()); @@ -151,6 +154,7 @@ impl BorsTester { http_mock: mock, github, db, + merge_queue_tx, mergeable_queue_tx, global_tx, webhooks_active: true, @@ -771,6 +775,7 @@ impl BorsTester { // Make sure that the event channel senders are closed drop(self.app); drop(self.global_tx); + drop(self.merge_queue_tx); self.mergeable_queue_tx.shutdown(); // Wait until all events are handled in the bors service bors.await.unwrap(); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a33e94a2..9331c240 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod logging; +pub mod sort_queue; pub mod text; pub mod timing; diff --git a/src/utils/sort_queue.rs b/src/utils/sort_queue.rs new file mode 100644 index 00000000..edfa1761 --- /dev/null +++ b/src/utils/sort_queue.rs @@ -0,0 +1,61 @@ +use crate::bors::RollupMode; +use crate::database::{BuildStatus, MergeableState, PullRequestModel}; + +/// Sorts pull requests according to merge queue priority rules. +/// +/// Ordered by build status > mergeability > approval > +/// priority value > rollup > age. +pub fn sort_queue_prs(mut prs: Vec) -> Vec { + prs.sort_by(|a, b| { + // 1. Compare build status (lower value = higher priority) + get_status_priority(a) + .cmp(&get_status_priority(b)) + // 2. Compare mergeable state (0 = mergeable, 1 = conflicts/unknown) + .then_with(|| get_mergeable_priority(a).cmp(&get_mergeable_priority(b))) + // 3. Compare approval status (approved PRs should come first) + .then_with(|| a.is_approved().cmp(&b.is_approved()).reverse()) + // 4. Compare priority numbers (higher priority should come first) + .then_with(|| b.priority.unwrap_or(0).cmp(&a.priority.unwrap_or(0))) + // 5. Compare rollup mode (-1 = never/iffy, 0 = maybe, 1 = always) + .then_with(|| { + get_rollup_priority(a.rollup.as_ref()).cmp(&get_rollup_priority(b.rollup.as_ref())) + }) + // 6. Compare PR numbers (older first) + .then_with(|| a.number.cmp(&b.number)) + }); + prs +} + +fn get_status_priority(pr: &PullRequestModel) -> i32 { + match &pr.auto_build { + Some(build) => match build.status { + BuildStatus::Pending => 1, + BuildStatus::Success => 6, + BuildStatus::Failure => 5, + BuildStatus::Cancelled | BuildStatus::Timeouted => 4, + }, + None => { + if pr.is_approved() { + 2 // approved but no build + } else { + 3 // no status + } + } + } +} + +fn get_mergeable_priority(pr: &PullRequestModel) -> i32 { + match pr.mergeable_state { + MergeableState::Mergeable => 0, + MergeableState::HasConflicts => 1, + MergeableState::Unknown => 1, + } +} + +fn get_rollup_priority(rollup: Option<&RollupMode>) -> i32 { + match rollup { + Some(RollupMode::Always) => 1, + Some(RollupMode::Maybe) | None => 0, + Some(RollupMode::Iffy) | Some(RollupMode::Never) => -1, + } +} diff --git a/tests/data/migrations/20250617160331_add_auto_build_to_pull_request.sql b/tests/data/migrations/20250617160331_add_auto_build_to_pull_request.sql new file mode 100644 index 00000000..216b014e --- /dev/null +++ b/tests/data/migrations/20250617160331_add_auto_build_to_pull_request.sql @@ -0,0 +1,36 @@ +INSERT INTO + build (repository, branch, commit_sha, status, parent) +VALUES + ( + 'rust-lang/bors', + 'automation/bors/merge', + 'f2a8b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0', + 'success', + 'a7ec24743ca724dd4b164b3a76d29d0da9573617' + ), + ( + 'rust-lang/cargo', + 'automation/bors/merge', + '1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a1', + 'pending', + 'b3f987c12ee248ef21d37b59a40b17e93fac7c8a' + ), + ( + 'rust-lang/rust', + 'automation/bors/merge', + '9f8e7d6c5b4a3928374658291047382910473829', + 'failure', + '4ee5a1bfc10bc49f30a8f527557ac4a93a2b9d66' + ); + +UPDATE pull_request +SET + auto_build_id = 4 +WHERE + id = 1; + +UPDATE pull_request +SET + auto_build_id = 5 +WHERE + id = 2;