diff --git a/.sqlx/query-c4c9f02a27d364b0c5530c309c375bf678d6bce37014f2c29c52ee4c8e957480.json b/.sqlx/query-105c98ebfe5c66532279d410e4640bee41b639fc3c08443aa5ff47416f8b4082.json similarity index 75% rename from .sqlx/query-c4c9f02a27d364b0c5530c309c375bf678d6bce37014f2c29c52ee4c8e957480.json rename to .sqlx/query-105c98ebfe5c66532279d410e4640bee41b639fc3c08443aa5ff47416f8b4082.json index 7eb0d5ce..0659286e 100644 --- a/.sqlx/query-c4c9f02a27d364b0c5530c309c375bf678d6bce37014f2c29c52ee4c8e957480.json +++ b/.sqlx/query-105c98ebfe5c66532279d410e4640bee41b639fc3c08443aa5ff47416f8b4082.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: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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.try_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 ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n WHERE pr.repository = $1 AND\n pr.number = $2\n ", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -228,8 +285,9 @@ false, false, null, + null, null ] }, - "hash": "c4c9f02a27d364b0c5530c309c375bf678d6bce37014f2c29c52ee4c8e957480" + "hash": "105c98ebfe5c66532279d410e4640bee41b639fc3c08443aa5ff47416f8b4082" } diff --git a/.sqlx/query-f9ac851e2b883da0b3e27aa50a72f043d839132e7039b94d19bcbc6549f8313b.json b/.sqlx/query-56fa70f590fc119365eadaea51a53008de57714b2766ca4125b35805e1e0b38f.json similarity index 75% rename from .sqlx/query-f9ac851e2b883da0b3e27aa50a72f043d839132e7039b94d19bcbc6549f8313b.json rename to .sqlx/query-56fa70f590fc119365eadaea51a53008de57714b2766ca4125b35805e1e0b38f.json index e52c6f5a..38d1100c 100644 --- a/.sqlx/query-f9ac851e2b883da0b3e27aa50a72f043d839132e7039b94d19bcbc6549f8313b.json +++ b/.sqlx/query-56fa70f590fc119365eadaea51a53008de57714b2766ca4125b35805e1e0b38f.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: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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.try_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' OR pr.mergeable_state_is_stale = true OR pr.approved_by IS NOT NULL)\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: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n WHERE pr.repository = $1\n AND pr.status IN ('open', 'draft')\n ", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -227,8 +284,9 @@ false, false, null, + null, null ] }, - "hash": "f9ac851e2b883da0b3e27aa50a72f043d839132e7039b94d19bcbc6549f8313b" + "hash": "56fa70f590fc119365eadaea51a53008de57714b2766ca4125b35805e1e0b38f" } diff --git a/.sqlx/query-241e6fb6c4f9a53a2df1ca3381eea0797a444ca7293cf88721f1c8d0add07589.json b/.sqlx/query-58374bae861cc12ea4e357b74be19f87ad20eca76504e33807e6fe7ac0744a73.json similarity index 82% rename from .sqlx/query-241e6fb6c4f9a53a2df1ca3381eea0797a444ca7293cf88721f1c8d0add07589.json rename to .sqlx/query-58374bae861cc12ea4e357b74be19f87ad20eca76504e33807e6fe7ac0744a73.json index 4f15eeeb..435f208c 100644 --- a/.sqlx/query-241e6fb6c4f9a53a2df1ca3381eea0797a444ca7293cf88721f1c8d0add07589.json +++ b/.sqlx/query-58374bae861cc12ea4e357b74be19f87ad20eca76504e33807e6fe7ac0744a73.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH upserted_pr AS (\n INSERT INTO pull_request (\n repository,\n number,\n title,\n author,\n assignees,\n head_branch,\n base_branch,\n mergeable_state,\n mergeable_state_is_stale,\n status\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (repository, number)\n DO UPDATE SET\n title = $3,\n author = $4,\n assignees = $5,\n head_branch = $6,\n base_branch = $7,\n -- Note that we do NOT update mergeable_state here, it is updated explicitly elsewhere,\n -- to properly handle unmergeability notifications.\n mergeable_state_is_stale =\n -- Only set the stale flag here, but do not clear it!\n -- This is important for the mergeability queue to work properly, so that we can\n -- detect whether something is stale or not.\n CASE\n WHEN $9 = true THEN true\n ELSE pull_request.mergeable_state_is_stale\n END,\n -- The merged state is final, there is no going back from it.\n -- Sometimes, GitHub can return inconsistent data, and claim that a merged PR\n -- is open again.\n -- We do not ever want to revert a merged state in the DB, as that could\n -- break some invariants in the merge queue.\n status =\n CASE\n WHEN pull_request.status = 'merged' THEN pull_request.status\n ELSE $10\n END\n RETURNING *\n )\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n ", + "query": "\n WITH upserted_pr AS (\n INSERT INTO pull_request (\n repository,\n number,\n title,\n author,\n assignees,\n head_branch,\n base_branch,\n mergeable_state,\n mergeable_state_is_stale,\n status\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (repository, number)\n DO UPDATE SET\n title = $3,\n author = $4,\n assignees = $5,\n head_branch = $6,\n base_branch = $7,\n -- Note that we do NOT update mergeable_state here, it is updated explicitly elsewhere,\n -- to properly handle unmergeability notifications.\n mergeable_state_is_stale =\n -- Only set the stale flag here, but do not clear it!\n -- This is important for the mergeability queue to work properly, so that we can\n -- detect whether something is stale or not.\n CASE\n WHEN $9 = true THEN true\n ELSE pull_request.mergeable_state_is_stale\n END,\n -- The merged state is final, there is no going back from it.\n -- Sometimes, GitHub can return inconsistent data, and claim that a merged PR\n -- is open again.\n -- We do not ever want to revert a merged state in the DB, as that could\n -- break some invariants in the merge queue.\n status =\n CASE\n WHEN pull_request.status = 'merged' THEN pull_request.status\n ELSE $10\n END\n RETURNING *\n )\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM upserted_pr as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n ", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -236,8 +293,9 @@ false, false, true, + true, true ] }, - "hash": "241e6fb6c4f9a53a2df1ca3381eea0797a444ca7293cf88721f1c8d0add07589" + "hash": "58374bae861cc12ea4e357b74be19f87ad20eca76504e33807e6fe7ac0744a73" } diff --git a/.sqlx/query-5a69d813f84a2ec8f049ef20e56d89350c8fa09a845f05a5bba2779a3249b6e9.json b/.sqlx/query-5a69d813f84a2ec8f049ef20e56d89350c8fa09a845f05a5bba2779a3249b6e9.json new file mode 100644 index 00000000..19b39ec6 --- /dev/null +++ b/.sqlx/query-5a69d813f84a2ec8f049ef20e56d89350c8fa09a845f05a5bba2779a3249b6e9.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE pull_request SET unrolled_build_id = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "5a69d813f84a2ec8f049ef20e56d89350c8fa09a845f05a5bba2779a3249b6e9" +} diff --git a/.sqlx/query-5b2600a32eb97d04c1064a631d11f1f033e61ae412e6b1703a2e4be87cedb0dd.json b/.sqlx/query-5b2600a32eb97d04c1064a631d11f1f033e61ae412e6b1703a2e4be87cedb0dd.json new file mode 100644 index 00000000..0d98a511 --- /dev/null +++ b/.sqlx/query-5b2600a32eb97d04c1064a631d11f1f033e61ae412e6b1703a2e4be87cedb0dd.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE rollup_member\n SET unroll_state = $1\n WHERE rollup = $2 AND member = $3\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "5b2600a32eb97d04c1064a631d11f1f033e61ae412e6b1703a2e4be87cedb0dd" +} diff --git a/.sqlx/query-8285c6863a77e13e39094f029d265cb162e84a96793374c046eefa3fd45ec33f.json b/.sqlx/query-8285c6863a77e13e39094f029d265cb162e84a96793374c046eefa3fd45ec33f.json new file mode 100644 index 00000000..86e74551 --- /dev/null +++ b/.sqlx/query-8285c6863a77e13e39094f029d265cb162e84a96793374c046eefa3fd45ec33f.json @@ -0,0 +1,328 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\",\n rm.rollup AS rollup_id,\n rm.member,\n rm.rolled_up_sha,\n rm.rolled_up_merge_sha,\n rm.unroll_state AS \"unroll_state: UnrollState\",\n rm.position\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n JOIN rollup_member AS rm ON pr.id = rm.member\n WHERE pr.repository = $1 AND\n -- These states are \"active\" - we need to do something about them\n rm.unroll_state IN ('Waiting', 'Pending', 'Finished')\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "repository: GithubRepoName", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "number: PullRequestNumber", + "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": "status: PullRequestStatus", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "rollup: RollupMode", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "note", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "delegation!: DelegationStatus", + "type_info": "Record" + }, + { + "ordinal": 12, + "name": "head_branch", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "base_branch", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "mergeable_state: MergeableState", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "mergeable_state_is_stale", + "type_info": "Bool" + }, + { + "ordinal": 16, + "name": "created_at: DateTime", + "type_info": "Timestamptz" + }, + { + "ordinal": 17, + "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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } + }, + { + "ordinal": 18, + "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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } + }, + { + "ordinal": 20, + "name": "rollup_id", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "member", + "type_info": "Int4" + }, + { + "ordinal": 22, + "name": "rolled_up_sha", + "type_info": "Text" + }, + { + "ordinal": 23, + "name": "rolled_up_merge_sha", + "type_info": "Text" + }, + { + "ordinal": 24, + "name": "unroll_state: UnrollState", + "type_info": "Text" + }, + { + "ordinal": 25, + "name": "position", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + null, + false, + true, + true, + true, + null, + false, + false, + false, + false, + false, + null, + null, + null, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "8285c6863a77e13e39094f029d265cb162e84a96793374c046eefa3fd45ec33f" +} diff --git a/.sqlx/query-c8312763b3058646b5f315dbdd2f41d9848165c774aa2503ae8b06e1ec6fd3f9.json b/.sqlx/query-84cb79695f8a6a090518469ac3b44cb2cddc27928ae5052e25a60c50aae80f76.json similarity index 76% rename from .sqlx/query-c8312763b3058646b5f315dbdd2f41d9848165c774aa2503ae8b06e1ec6fd3f9.json rename to .sqlx/query-84cb79695f8a6a090518469ac3b44cb2cddc27928ae5052e25a60c50aae80f76.json index df66d5bd..0c1a3126 100644 --- a/.sqlx/query-c8312763b3058646b5f315dbdd2f41d9848165c774aa2503ae8b06e1ec6fd3f9.json +++ b/.sqlx/query-84cb79695f8a6a090518469ac3b44cb2cddc27928ae5052e25a60c50aae80f76.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: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n WHERE pr.id IN (\n SELECT rollup\n FROM rollup_member\n WHERE member = $1\n )\n ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n WHERE pr.id = $1\n ", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -227,8 +284,9 @@ false, false, null, + null, null ] }, - "hash": "c8312763b3058646b5f315dbdd2f41d9848165c774aa2503ae8b06e1ec6fd3f9" + "hash": "84cb79695f8a6a090518469ac3b44cb2cddc27928ae5052e25a60c50aae80f76" } diff --git a/.sqlx/query-3ff449594dc650ea1f2c0e595b22cc6cb637588ef89b76f23b19c2bf7e98a7a3.json b/.sqlx/query-8d85692619c56cd8a9e31d0f213e755c44f8d6487562c12c838451b0c55f0a69.json similarity index 76% rename from .sqlx/query-3ff449594dc650ea1f2c0e595b22cc6cb637588ef89b76f23b19c2bf7e98a7a3.json rename to .sqlx/query-8d85692619c56cd8a9e31d0f213e755c44f8d6487562c12c838451b0c55f0a69.json index 58659d9a..4cafe8df 100644 --- a/.sqlx/query-3ff449594dc650ea1f2c0e595b22cc6cb637588ef89b76f23b19c2bf7e98a7a3.json +++ b/.sqlx/query-8d85692619c56cd8a9e31d0f213e755c44f8d6487562c12c838451b0c55f0a69.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.priority,\n pr.note,\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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.try_build_id = try_build.id\nLEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\nWHERE try_build.id = $1 OR auto_build.id = $1\n", + "query": "\nSELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.priority,\n pr.note,\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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\",\n unrolled_build AS \"unrolled_build: BuildModel\"\nFROM pull_request as pr\nLEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\nLEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\nLEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\nWHERE try_build.id = $1 OR auto_build.id = $1\n", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -227,8 +284,9 @@ true, false, null, + null, null ] }, - "hash": "3ff449594dc650ea1f2c0e595b22cc6cb637588ef89b76f23b19c2bf7e98a7a3" + "hash": "8d85692619c56cd8a9e31d0f213e755c44f8d6487562c12c838451b0c55f0a69" } diff --git a/.sqlx/query-9480d20947705572f371701fffd24e6ba96ed0c6eca5e9e4826c51dbd8645abf.json b/.sqlx/query-9480d20947705572f371701fffd24e6ba96ed0c6eca5e9e4826c51dbd8645abf.json new file mode 100644 index 00000000..bf09d5ba --- /dev/null +++ b/.sqlx/query-9480d20947705572f371701fffd24e6ba96ed0c6eca5e9e4826c51dbd8645abf.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE rollup_member\n SET unroll_state = $1\n WHERE rollup = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "9480d20947705572f371701fffd24e6ba96ed0c6eca5e9e4826c51dbd8645abf" +} diff --git a/.sqlx/query-0d264cbe7a4125b6513fbfff45286a9aa1a355d9d21a9d70d9e2a430079d6989.json b/.sqlx/query-b9a802eccda698b315574cb5e51e60dee5654235f0604ddb8cc8f245091ce1be.json similarity index 51% rename from .sqlx/query-0d264cbe7a4125b6513fbfff45286a9aa1a355d9d21a9d70d9e2a430079d6989.json rename to .sqlx/query-b9a802eccda698b315574cb5e51e60dee5654235f0604ddb8cc8f245091ce1be.json index 1a80efb0..3c4db16a 100644 --- a/.sqlx/query-0d264cbe7a4125b6513fbfff45286a9aa1a355d9d21a9d70d9e2a430079d6989.json +++ b/.sqlx/query-b9a802eccda698b315574cb5e51e60dee5654235f0604ddb8cc8f245091ce1be.json @@ -1,16 +1,18 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO rollup_member (rollup, member, rolled_up_sha)\n VALUES ($1, $2, $3)\n ", + "query": "\n INSERT INTO rollup_member (rollup, member, rolled_up_sha, rolled_up_merge_sha, position)\n VALUES ($1, $2, $3, $4, $5)\n ", "describe": { "columns": [], "parameters": { "Left": [ "Int4", "Int4", - "Text" + "Text", + "Text", + "Int4" ] }, "nullable": [] }, - "hash": "0d264cbe7a4125b6513fbfff45286a9aa1a355d9d21a9d70d9e2a430079d6989" + "hash": "b9a802eccda698b315574cb5e51e60dee5654235f0604ddb8cc8f245091ce1be" } diff --git a/.sqlx/query-be9bded5cbfc40ef16f2e3ce0c745f33d94d81a160c524fdd8506fc5f1842a3d.json b/.sqlx/query-be9bded5cbfc40ef16f2e3ce0c745f33d94d81a160c524fdd8506fc5f1842a3d.json new file mode 100644 index 00000000..d411af98 --- /dev/null +++ b/.sqlx/query-be9bded5cbfc40ef16f2e3ce0c745f33d94d81a160c524fdd8506fc5f1842a3d.json @@ -0,0 +1,292 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n WHERE pr.id IN (\n SELECT rollup\n FROM rollup_member\n WHERE member = $1\n )\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "repository: GithubRepoName", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "number: PullRequestNumber", + "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": "status: PullRequestStatus", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "priority", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "rollup: RollupMode", + "type_info": "Text" + }, + { + "ordinal": 10, + "name": "note", + "type_info": "Text" + }, + { + "ordinal": 11, + "name": "delegation!: DelegationStatus", + "type_info": "Record" + }, + { + "ordinal": 12, + "name": "head_branch", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "base_branch", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "mergeable_state: MergeableState", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "mergeable_state_is_stale", + "type_info": "Bool" + }, + { + "ordinal": 16, + "name": "created_at: DateTime", + "type_info": "Timestamptz" + }, + { + "ordinal": 17, + "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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } + }, + { + "ordinal": 18, + "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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + null, + false, + true, + true, + true, + null, + false, + false, + false, + false, + false, + null, + null, + null + ] + }, + "hash": "be9bded5cbfc40ef16f2e3ce0c745f33d94d81a160c524fdd8506fc5f1842a3d" +} diff --git a/.sqlx/query-ca9ff1025fa5d9d92eb7a22b0134b636c18e87c8614f41d57b469afa1d9c012b.json b/.sqlx/query-ca9ff1025fa5d9d92eb7a22b0134b636c18e87c8614f41d57b469afa1d9c012b.json new file mode 100644 index 00000000..49aaecf1 --- /dev/null +++ b/.sqlx/query-ca9ff1025fa5d9d92eb7a22b0134b636c18e87c8614f41d57b469afa1d9c012b.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n rm.rollup,\n rm.member,\n pr.number AS number,\n rm.rolled_up_sha AS rolled_up_head_sha,\n rm.rolled_up_merge_sha AS rolled_up_merge_sha,\n rm.unroll_state as \"unroll_state: UnrollState\",\n rm.position as position\n FROM rollup_member rm\n JOIN pull_request AS pr ON pr.id = rm.member\n WHERE rm.rollup = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "rollup", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "member", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "number", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "rolled_up_head_sha", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "rolled_up_merge_sha", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "unroll_state: UnrollState", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "position", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "ca9ff1025fa5d9d92eb7a22b0134b636c18e87c8614f41d57b469afa1d9c012b" +} diff --git a/.sqlx/query-d6d03306575c3b113eb45cc94f535eccfda685ac21534c6be2648fb0552c1852.json b/.sqlx/query-d6d03306575c3b113eb45cc94f535eccfda685ac21534c6be2648fb0552c1852.json deleted file mode 100644 index 19489914..00000000 --- a/.sqlx/query-d6d03306575c3b113eb45cc94f535eccfda685ac21534c6be2648fb0552c1852.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT pr.number AS number, rm.rolled_up_sha AS sha\n FROM rollup_member rm\n JOIN pull_request AS pr ON pr.id = rm.member\n WHERE rm.rollup = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "number", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "sha", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "d6d03306575c3b113eb45cc94f535eccfda685ac21534c6be2648fb0552c1852" -} diff --git a/.sqlx/query-e1b584315cbf211a7fd69a24ad686c43114b3269a820cd0aa8677574da2929d0.json b/.sqlx/query-ebac6b162fdd9494b9113dc955f7bc5db26479d38e788f08403d64250506a33f.json similarity index 74% rename from .sqlx/query-e1b584315cbf211a7fd69a24ad686c43114b3269a820cd0aa8677574da2929d0.json rename to .sqlx/query-ebac6b162fdd9494b9113dc955f7bc5db26479d38e788f08403d64250506a33f.json index cc6b8623..dab09e83 100644 --- a/.sqlx/query-e1b584315cbf211a7fd69a24ad686c43114b3269a820cd0aa8677574da2929d0.json +++ b/.sqlx/query-ebac6b162fdd9494b9113dc955f7bc5db26479d38e788f08403d64250506a33f.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: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\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.try_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 ", + "query": "\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM pull_request as pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n WHERE pr.repository = $1\n AND (pr.mergeable_state = 'unknown' OR pr.mergeable_state_is_stale = true OR pr.approved_by IS NOT NULL)\n AND pr.status IN ('open', 'draft')\n ", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -227,8 +284,9 @@ false, false, null, + null, null ] }, - "hash": "e1b584315cbf211a7fd69a24ad686c43114b3269a820cd0aa8677574da2929d0" + "hash": "ebac6b162fdd9494b9113dc955f7bc5db26479d38e788f08403d64250506a33f" } diff --git a/.sqlx/query-93b2d0ebb73114b251b1c27deff42f3893a1b33bd8b28ed7f9255aea12a4581b.json b/.sqlx/query-f9051d24b6921cdde98d1de5057266589f71165091f82cf100048a06da4cdf60.json similarity index 78% rename from .sqlx/query-93b2d0ebb73114b251b1c27deff42f3893a1b33bd8b28ed7f9255aea12a4581b.json rename to .sqlx/query-f9051d24b6921cdde98d1de5057266589f71165091f82cf100048a06da4cdf60.json index ca278cf4..c5054b88 100644 --- a/.sqlx/query-93b2d0ebb73114b251b1c27deff42f3893a1b33bd8b28ed7f9255aea12a4581b.json +++ b/.sqlx/query-f9051d24b6921cdde98d1de5057266589f71165091f82cf100048a06da4cdf60.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH pr AS (\n UPDATE pull_request\n SET mergeable_state_is_stale = true\n WHERE pull_request.repository = $1\n AND pull_request.base_branch = $2\n AND pull_request.status IN ('open', 'draft')\n RETURNING pull_request.*\n )\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\"\n FROM pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n ", + "query": "\n WITH pr AS (\n UPDATE pull_request\n SET mergeable_state_is_stale = true\n WHERE pull_request.repository = $1\n AND pull_request.base_branch = $2\n AND pull_request.status IN ('open', 'draft')\n RETURNING pull_request.*\n )\n SELECT\n pr.id,\n pr.repository as \"repository: GithubRepoName\",\n pr.number as \"number: PullRequestNumber\",\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 \"status: PullRequestStatus\",\n pr.priority,\n pr.rollup as \"rollup: RollupMode\",\n pr.note,\n (\n pr.delegatee_id,\n pr.delegated_permission\n ) AS \"delegation!: DelegationStatus\",\n pr.head_branch,\n pr.base_branch,\n pr.mergeable_state as \"mergeable_state: MergeableState\",\n pr.mergeable_state_is_stale,\n pr.created_at as \"created_at: DateTime\",\n try_build AS \"try_build: BuildModel\",\n auto_build AS \"auto_build: BuildModel\",\n unrolled_build AS \"unrolled_build: BuildModel\"\n FROM pr\n LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id\n LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id\n LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id\n ", "describe": { "columns": [ { @@ -201,6 +201,63 @@ } } } + }, + { + "ordinal": 19, + "name": "unrolled_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" + ], + [ + "check_run_id", + "Int8" + ], + [ + "kind", + "Text" + ], + [ + "duration", + "Interval" + ], + [ + "pr_number", + "Int8" + ] + ] + } + } + } } ], "parameters": { @@ -228,8 +285,9 @@ false, false, true, + true, true ] }, - "hash": "93b2d0ebb73114b251b1c27deff42f3893a1b33bd8b28ed7f9255aea12a4581b" + "hash": "f9051d24b6921cdde98d1de5057266589f71165091f82cf100048a06da4cdf60" } diff --git a/README.md b/README.md index 0e9e6799..e118e610 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,13 @@ The bot uses the following branch names for its operations. - `automation/bors/auto` - This branch should be configured for CI workflows that need to run before merging to the base branch. +#### Unrolled builds +- `automation/bors/try-perf-merge` + - Used to prepare unrolled builds of rollup members, used for perf runs. + - Should not be configured for any CI workflows! +- `automation/bors/try-perf` + - This branch should be configured for CI workflows used to produce perf artifacts. + The merge and non-merge branches are needed because we cannot set branches to parent and merge them with a PR commit atomically using the GitHub API. @@ -69,7 +76,9 @@ describes the file can be found in `src/config.rs`. [Here](rust-bors.example.tom 4) Configure CI workflows on push to: - `automation/bors/try` branch (for try builds) - `automation/bors/auto` branch (for auto builds) -5) Give the bot permissions to push to `automation/bors/try`, `automation/bors/try-merge`, `automation/bors/auto`, and `automation/bors/auto-merge`. + - `automation/bors/try-perf` branch (for unrolled rollup perf builds) + - This is only needed if the `[unroll]` section is configured in the config file. +5) Give the bot permissions to push to `automation/bors/try`, `automation/bors/try-merge`, `automation/bors/auto`, `automation/bors/auto-merge`, `automation/bors/try-perf`, and `automation/bors/try-perf-merge`. ## Contributing diff --git a/docs/design.md b/docs/design.md index f360a7d4..e05448b6 100644 --- a/docs/design.md +++ b/docs/design.md @@ -52,6 +52,9 @@ repository: - Reload the mergeability status of open PRs from GitHub. - Sync the status of PRs between the DB and GitHub. - Run the merge queue. +- Terminate long-running EC2 instances. +- Start EC2 instances for jobs that have been queued for a long time. +- Process pending unrolled rollup member builds. ## Concurrency The bot is currently listening for GitHub webhooks concurrently, however it handles all commands serially, to avoid @@ -195,6 +198,26 @@ preventing the problem where two PRs pass tests independently but fail when comb Note that `automation/bors/auto-merge` should not have any CI workflows configured! These should be configured for the `automation/bors/auto` branch instead. +## Unrolled builds +When a rollup PR is merged, and the `[unroll]` section is enabled in the config, bors creates a separate unrolled build for each of its rollup member. + +The state of the unrolled builds is tracked via the `unroll_state` column of the `rollup_member` table. The individual states are: + +- `NULL`: the rollup member hasn't been merged (yet) in this rollup +- `Waiting`: the rollup is waiting for an unrolled build to be started by bors +- `Pending`: the rollup is waiting for an unrolled build to finish +- `Finished`: the unrolled build has finished, or it could not have been started +- `Reported`: the comment with the unrolling results has been posted to the rollup PR + +Once a rollup is merged, all its members are set to the `Waiting` state. Then, when the unroll queue runs, it will attempt to start an unrolled build for each member: +- It uses the `rolled_up_head_sha` captured when the rollup was created for the merge, and the `rolled_up_merge_sha` for the commit message. +- It attempts to merge the HEAD SHA onto the parent of the rollup's merge commit, in the `automation/bors/try-perf-merge` branch. +- If the merge succeeds, force-pushes the unrolled commit to `automation/bors/try-perf`. + +If any non-transient errors occur, the given rollup member is marked as `Finished`. + +Once all members are in the `Finished` state, bors will post a comment with a result tableto the merged rollup. + ## Recognizing that CI has succeeded/failed With [homu](https://github.com/rust-lang/homu) (the old bors implementation), GitHub actions CI running repositories had to use a "fake" job that marked the whole CI workflow as succeeded or failed, to signal to bors if it should consider diff --git a/docs/development.md b/docs/development.md index e2a7e3ce..ec082dc7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -139,7 +139,7 @@ Nevertheless, sometimes it might be easier to test it on your own repository. Th - `Workflow run` - Install your GitHub app on some test repository where you want to test bors. - Add `rust-bors.toml` in the root of the repository, and also add some example CI workflows. -- If you want to use custom permissions for PR approvals, create team data files for GitHub users in `data/team`. You can find examples in that directory, which you should copy and remove the `.example` suffix. +- If you want to use custom permissions for PR approvals, create team data files for GitHub users in `data/team`. You can find examples in that directory, which you should copy and remove the `.example` suffix. - Get your GitHub user `ID` `https://api.github.com/users/` - Edit both `bors.review.json` and `bors.try.json` files to include your GitHub `ID`: `{ "github_ids": [123] }` diff --git a/migrations/20260812140527_add_unrolled_build_to_pr.down.sql b/migrations/20260812140527_add_unrolled_build_to_pr.down.sql new file mode 100644 index 00000000..b998f87a --- /dev/null +++ b/migrations/20260812140527_add_unrolled_build_to_pr.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER TABLE pull_request + DROP COLUMN unrolled_build_id; diff --git a/migrations/20260812140527_add_unrolled_build_to_pr.up.sql b/migrations/20260812140527_add_unrolled_build_to_pr.up.sql new file mode 100644 index 00000000..702a1059 --- /dev/null +++ b/migrations/20260812140527_add_unrolled_build_to_pr.up.sql @@ -0,0 +1,3 @@ +-- Add up migration script here +ALTER TABLE pull_request + ADD COLUMN unrolled_build_id INTEGER REFERENCES build (id); diff --git a/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.down.sql b/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.down.sql new file mode 100644 index 00000000..a341224d --- /dev/null +++ b/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE rollup_member + DROP COLUMN IF EXISTS rolled_up_merge_sha, + DROP COLUMN IF EXISTS unroll_state, + DROP COLUMN IF EXISTS position; + +DROP INDEX IF EXISTS rollup_member_unroll_state_idx; diff --git a/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.up.sql b/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.up.sql new file mode 100644 index 00000000..0725ad38 --- /dev/null +++ b/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.up.sql @@ -0,0 +1,16 @@ +-- rolled_up_merge_sha is the SHA of the intermediate merge commit that added the PR to a rollup +-- unroll_state is the status of unrolling of the PR after its rollup has been merged +-- position is the numeric position of the member in the rollup (0 = first PR, 1 = second PR, etc.) +ALTER TABLE rollup_member + ADD COLUMN rolled_up_merge_sha TEXT NOT NULL DEFAULT '', + ADD COLUMN unroll_state TEXT NULL, + ADD COLUMN position INT NOT NULL DEFAULT 0; + +-- For finding unreported unrolled builds +CREATE INDEX IF NOT EXISTS rollup_member_unroll_state_idx ON rollup_member (unroll_state); + +-- Backfill existing rows from `rolled_up_sha` +-- This is not really correct, but it is better than leaving the column empty +UPDATE rollup_member rm +SET rolled_up_merge_sha = rolled_up_sha +WHERE rolled_up_merge_sha = '' diff --git a/rust-bors.example.toml b/rust-bors.example.toml index c5cfac9f..7cae1671 100644 --- a/rust-bors.example.toml +++ b/rust-bors.example.toml @@ -59,3 +59,6 @@ images = { jit_runner = "organization" # "repository" or "organization" # Allowed EC2 instance types that can be used in the `` field of the `os` label. allowed_instances = ["c8a.12xlarge"] + +# Enable unrolling of rollups +[unroll] diff --git a/src/bin/bors.rs b/src/bin/bors.rs index 0eadacf7..08e215d0 100644 --- a/src/bin/bors.rs +++ b/src/bin/bors.rs @@ -48,6 +48,9 @@ const EC2_INSTANCE_TERMINATE_INTERVAL: Duration = Duration::from_secs(60 * 10); /// long time. const EC2_INSTANCE_BACKFILL_TERMINAL: Duration = Duration::from_secs(60 * 15); +/// How often should the bot try to check unrolled builds? +const PROCESS_UNROLLED_MEMBER_BUILDS: Duration = Duration::from_secs(60 * 5); + #[derive(clap::Parser)] struct Opts { /// Github App ID. @@ -268,6 +271,7 @@ fn try_main(opts: Opts) -> anyhow::Result<()> { BorsGlobalEvent::ProcessMergeQueue, BorsGlobalEvent::TerminateOldEC2Instances, BorsGlobalEvent::ReloadWorkflowJobCache, + BorsGlobalEvent::ProcessUnrolledMemberBuilds, ]; for event in startup_events { refresh_tx.send(event).await?; @@ -281,6 +285,7 @@ fn try_main(opts: Opts) -> anyhow::Result<()> { let mut merge_queue_interval = make_interval(MERGE_QUEUE_CHECK_INTERVAL); let mut ec2_check_interval = make_interval(EC2_INSTANCE_TERMINATE_INTERVAL); let mut ec2_backfill_interval = make_interval(EC2_INSTANCE_BACKFILL_TERMINAL); + let mut unrolled_build_interval = make_interval(PROCESS_UNROLLED_MEMBER_BUILDS); loop { tokio::select! { _ = config_refresh.tick() => { @@ -307,6 +312,9 @@ fn try_main(opts: Opts) -> anyhow::Result<()> { _ = ec2_backfill_interval.tick() => { refresh_tx.send(BorsGlobalEvent::BackfillEC2Instances).await?; } + _ = unrolled_build_interval.tick() => { + refresh_tx.send(BorsGlobalEvent::ProcessUnrolledMemberBuilds).await?; + } } } }; diff --git a/src/bors/build.rs b/src/bors/build.rs index 91a59f8e..59c7d124 100644 --- a/src/bors/build.rs +++ b/src/bors/build.rs @@ -224,7 +224,7 @@ pub async fn start_build( proof: &ExclusiveLockProof, context: StartBuildContext, commit: StartBuildCommit, - check_run: StartBuildCheckRun, + check_run: Option, pr: &PullRequestModel, ) -> Result { let StartBuildContext { @@ -293,44 +293,55 @@ pub async fn start_build( ) .await } + BuildKind::UnrolledMember => { + db.attach_unrolled_build( + pr, + ci_branch.clone(), + build_commit_sha.clone(), + base_sha.clone(), + ) + .await + } } .map_err(StartBuildError::DatabaseError)?; - // Create a check run to track the build status in GitHub's UI. - // This gets added to the PR's head SHA so GitHub shows UI in the checks tab and - // the bottom of the PR. - let check_run_result = repo - .client - .create_check_run( - &check_run.name, - &head_sha, - CheckRunStatus::InProgress, - CheckRunOutput { - title: check_run.title, - summary: "".to_string(), - }, - &build_id.to_string(), - ) - .await; - match check_run_result { - Ok(check_run) => { - let check_run_id = check_run.id.into_inner() as i64; - if let Err(error) = db - .update_build( - build_id, - UpdateBuildParams::default().check_run_id(check_run_id), - ) - .await - { - tracing::error!( - "Failed to update build {build_id} with check run id {check_run_id}: {error:?}" - ); + if let Some(check_run) = check_run { + // Create a check run to track the build status in GitHub's UI. + // This gets added to the PR's head SHA so GitHub shows UI in the checks tab and + // the bottom of the PR. + let check_run_result = repo + .client + .create_check_run( + &check_run.name, + &head_sha, + CheckRunStatus::InProgress, + CheckRunOutput { + title: check_run.title, + summary: "".to_string(), + }, + &build_id.to_string(), + ) + .await; + match check_run_result { + Ok(check_run) => { + let check_run_id = check_run.id.into_inner() as i64; + if let Err(error) = db + .update_build( + build_id, + UpdateBuildParams::default().check_run_id(check_run_id), + ) + .await + { + tracing::error!( + "Failed to update build {build_id} with check run id {check_run_id}: {error:?}" + ); + } + } + Err(error) => { + // Check runs are non-critical; the build has already started, so do not block + // progress if they fail. + tracing::error!("Failed to create check run: {error:?}"); } - } - Err(error) => { - // Check runs are non-critical; the build has already started, so do not block - // progress if they fail. - tracing::error!("Failed to create check run: {error:?}"); } } diff --git a/src/bors/build_queue.rs b/src/bors/build_queue.rs index 7530fad1..d3d2c0f4 100644 --- a/src/bors/build_queue.rs +++ b/src/bors/build_queue.rs @@ -18,6 +18,7 @@ use crate::bors::comment::{ use crate::bors::event::WorkflowRunCompleted; use crate::bors::labels::handle_label_trigger; use crate::bors::merge_queue::MergeQueueSender; +use crate::bors::unroll_queue::UnrollQueueSender; use crate::bors::{ BuildKind, FailedWorkflowRun, RepositoryState, elapsed_time_since, hide_tagged_comments, }; @@ -96,6 +97,7 @@ pub async fn handle_build_queue_event( ctx: Arc, event: BuildQueueEvent, merge_queue_tx: MergeQueueSender, + unroll_queue_tx: UnrollQueueSender, ) -> anyhow::Result<()> { let db = &ctx.db; match event { @@ -111,8 +113,16 @@ pub async fn handle_build_queue_event( // First try to complete builds, and only then timeout then // Because if the bot was offline for some time, we want to first attempt to // actually finish the build, otherwise it might get instantly timeouted. - if !maybe_complete_build(&repo, db, &build, &pr, &merge_queue_tx, None) - .await? + if !maybe_complete_build( + &repo, + db, + &build, + &pr, + &merge_queue_tx, + &unroll_queue_tx, + None, + ) + .await? { maybe_timeout_build(&repo, db, &build, &pr, timeout).await?; } @@ -167,6 +177,7 @@ pub async fn handle_build_queue_event( &build, &pr, &merge_queue_tx, + &unroll_queue_tx, Some(CompletionTrigger { error_context }), ) .await?; @@ -211,6 +222,7 @@ async fn maybe_timeout_build( let trigger = match build.kind { BuildKind::Try => LabelTrigger::TryBuildFailed, BuildKind::Auto => LabelTrigger::AutoBuildFailed, + BuildKind::UnrolledMember => return Ok(()), }; let gh_pr = repo.client.get_pull_request(pr.number).await?; handle_label_trigger(repo, &gh_pr.into(), trigger).await?; @@ -247,6 +259,7 @@ async fn maybe_complete_build( build: &BuildModel, pr: &PullRequestModel, merge_queue_tx: &MergeQueueSender, + unroll_queue_tx: &UnrollQueueSender, completion_trigger: Option, ) -> anyhow::Result { assert_eq!( @@ -326,6 +339,7 @@ async fn maybe_complete_build( } else { LabelTrigger::AutoBuildFailed }), + BuildKind::UnrolledMember => None, }; let compute_duration = || { @@ -368,22 +382,35 @@ async fn maybe_complete_build( } // Trigger merge queue when an auto build completes - if build.kind == BuildKind::Auto { - merge_queue_tx.notify().await?; + match build.kind { + BuildKind::Auto => { + merge_queue_tx.notify().await?; + } + BuildKind::UnrolledMember => { + unroll_queue_tx + .process_unrolled_members(repo.repository()) + .await?; + } + BuildKind::Try => {} } let comment_opt = if build_succeeded { tracing::info!("Build succeeded for PR {pr_num}"); - if build.kind == BuildKind::Try { - Some(try_build_succeeded_comment( + match build.kind { + BuildKind::Try => Some(try_build_succeeded_comment( workflow_runs, CommitSha(build.commit_sha.clone()), CommitSha(build.parent.clone()), - )) - } else { - // Merge queue will post the build succeeded comment - None + )), + BuildKind::Auto => { + // Merge queue will post the build succeeded comment + None + } + BuildKind::UnrolledMember => { + // Unrolled perf builds do not send any comments on the rollup member PRs + None + } } } else { tracing::info!("Build failed for PR {pr_num}"); @@ -417,13 +444,16 @@ async fn maybe_complete_build( }; let tag = match build.kind { - BuildKind::Try => CommentTag::TryBuildStarted, - BuildKind::Auto => CommentTag::AutoBuildStarted, + BuildKind::Try => Some(CommentTag::TryBuildStarted), + BuildKind::Auto => Some(CommentTag::AutoBuildStarted), + BuildKind::UnrolledMember => None, }; - hide_tagged_comments(repo, db, pr, tag).await?; + if let Some(tag) = tag { + hide_tagged_comments(repo, db, pr, tag).await?; - if let Some(comment) = comment_opt { - repo.client.post_comment(pr_num, comment, db).await?; + if let Some(comment) = comment_opt { + repo.client.post_comment(pr_num, comment, db).await?; + } } Ok(true) diff --git a/src/bors/event.rs b/src/bors/event.rs index d3d51ddd..6ede056d 100644 --- a/src/bors/event.rs +++ b/src/bors/event.rs @@ -85,6 +85,8 @@ pub enum BorsGlobalEvent { BackfillEC2Instances, /// Reload jobs of pending workfows into the in-memory job cache. ReloadWorkflowJobCache, + /// Start or complete unrolled builds of rollups. + ProcessUnrolledMemberBuilds, } #[derive(Debug)] diff --git a/src/bors/handlers/mod.rs b/src/bors/handlers/mod.rs index cac813bc..03efd940 100644 --- a/src/bors/handlers/mod.rs +++ b/src/bors/handlers/mod.rs @@ -26,7 +26,7 @@ use crate::bors::mergeability_queue::set_pr_mergeability_based_on_user_action; use crate::bors::process::QueueSenders; use crate::bors::{ AUTO_BRANCH_NAME, BorsContext, BuildKind, CommandPrefix, Comment, PullRequestStatus, - RepositoryState, TRY_BRANCH_NAME, + RepositoryState, TRY_BRANCH_NAME, TRY_PERF_BRANCH_NAME, }; use crate::database::{DelegatedPermission, DelegationStatus, PullRequestModel}; use crate::ec2::{backfill_ec2_instances, terminate_old_ec2_instances}; @@ -366,6 +366,21 @@ pub async fn handle_bors_global_event( .instrument(span) .await?; } + BorsGlobalEvent::ProcessUnrolledMemberBuilds => { + tracing::info!("Process unrolled member builds"); + for_each_repo(&ctx, |repo| { + let repo = repo.clone(); + let senders = senders.clone(); + async move { + senders + .unroll_queue() + .process_unrolled_members(repo.repository()) + .await?; + anyhow::Ok(()) + } + }) + .await?; + } } Ok(()) } @@ -1169,13 +1184,14 @@ pub fn invalidation_comment( /// Is this branch interesting for the bot? fn is_bors_observed_branch(branch: &str) -> bool { - branch == TRY_BRANCH_NAME || branch == AUTO_BRANCH_NAME + get_build_kind_from_branch(branch).is_some() } fn get_build_kind_from_branch(branch: &str) -> Option { match branch { b if b == TRY_BRANCH_NAME => Some(BuildKind::Try), b if b == AUTO_BRANCH_NAME => Some(BuildKind::Auto), + b if b == TRY_PERF_BRANCH_NAME => Some(BuildKind::UnrolledMember), _ => None, } } diff --git a/src/bors/handlers/squash.rs b/src/bors/handlers/squash.rs index 1167d492..7c3ce764 100644 --- a/src/bors/handlers/squash.rs +++ b/src/bors/handlers/squash.rs @@ -851,7 +851,7 @@ mod tests { // Create fork let fork_repo = fork_repo(); let mut repo = Repo::new(pr_author.clone(), fork_repo.name()); - repo.fork = true; + repo.fork_of = Some(gh.default_repo()); // Set the default PR to be from the fork gh.default_repo().lock().get_pr_mut(1).head_repository = Some(repo.full_name()); diff --git a/src/bors/handlers/trybuild.rs b/src/bors/handlers/trybuild.rs index 5456fce4..25037763 100644 --- a/src/bors/handlers/trybuild.rs +++ b/src/bors/handlers/trybuild.rs @@ -88,7 +88,7 @@ pub(super) async fn command_try_build( }; let res = db - .ensure_not_concurrent(&format!("{}-try-build", repo.repository()), async |proof| { + .ensure_not_concurrent(BuildKind::Try, repo.repository(), async |proof| { // Try to cancel any previously running try build workflows let cancelled_workflow_urls = if let Some(build) = get_pending_try_build(pr.db) { let res = cancel_previous_try_build(repo, &db, build).await?; @@ -127,10 +127,10 @@ pub(super) async fn command_try_build( ), author: bors_commit_author(), }, - StartBuildCheckRun { + Some(StartBuildCheckRun { name: TRY_BUILD_CHECK_RUN_NAME.to_string(), - title: "Bors try build".to_string(), - }, + title: TRY_BUILD_CHECK_RUN_NAME.to_string(), + }), pr.db, ) .await @@ -302,7 +302,7 @@ mod tests { #[sqlx::test(migrator = "crate::MIGRATOR")] async fn try_success(pool: sqlx::PgPool) { - run_test(pool.clone(), async |ctx: &mut BorsTester| { + run_test(pool, async |ctx: &mut BorsTester| { ctx.post_comment("@bors try").await?; ctx.expect_comments((), 1).await; ctx.workflow_full_success(ctx.try_workflow()).await?; diff --git a/src/bors/handlers/workflow.rs b/src/bors/handlers/workflow.rs index 054133f3..7f83c680 100644 --- a/src/bors/handlers/workflow.rs +++ b/src/bors/handlers/workflow.rs @@ -81,6 +81,7 @@ async fn add_workflow_links_to_build_start_comment( let tag = match build.kind { BuildKind::Try => CommentTag::TryBuildStarted, BuildKind::Auto => CommentTag::AutoBuildStarted, + BuildKind::UnrolledMember => return Ok(()), }; let comments = db .get_tagged_bot_comments(&payload.repository, pr.number, tag) diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs index 817a2738..05d815f1 100644 --- a/src/bors/merge_queue.rs +++ b/src/bors/merge_queue.rs @@ -19,6 +19,7 @@ use crate::bors::handlers::{ InvalidationComment, InvalidationInfo, InvalidationReason, invalidate_pr, unapprove_pr, }; use crate::bors::mergeability_queue::{MergeabilityQueueSender, update_pr_with_known_mergeability}; +use crate::bors::unroll_queue::UnrollQueueSender; use crate::bors::{AUTO_BRANCH_NAME, BuildKind, PullRequestStatus, RepositoryState}; use crate::database::{ ApprovalInfo, BuildModel, BuildStatus, ExclusiveLockProof, ExclusiveOperationOutcome, @@ -91,6 +92,7 @@ pub(super) const AUTO_BUILD_CHECK_RUN_NAME: &str = "Bors auto build"; pub async fn merge_queue_tick( ctx: Arc, mergeability_sender: &MergeabilityQueueSender, + unroll_queue_sender: &UnrollQueueSender, ) -> anyhow::Result<()> { let repos: Vec> = ctx.repositories.repositories(); @@ -102,16 +104,14 @@ pub async fn merge_queue_tick( // from the DB). let res = ctx .db - .ensure_not_concurrent( - &format!("{}-auto-build", repo.repository()), - async |proof| { - if let Err(error) = - process_repository(&repo, &ctx, mergeability_sender, proof).await - { - tracing::error!("Error running merge queue for {repo_name}: {error:?}"); - } - }, - ) + .ensure_not_concurrent(BuildKind::Auto, repo.repository(), async |proof| { + if let Err(error) = + process_repository(&repo, &ctx, mergeability_sender, unroll_queue_sender, proof) + .await + { + tracing::error!("Error running merge queue for {repo_name}: {error:?}"); + } + }) .await .context("Merge lock failure")?; match res { @@ -134,6 +134,7 @@ async fn process_repository( repo: &RepositoryState, ctx: &BorsContext, mergeability_sender: &MergeabilityQueueSender, + unroll_queue_sender: &UnrollQueueSender, proof: ExclusiveLockProof, ) -> anyhow::Result<()> { if !repo.config.load().merge_queue_enabled { @@ -171,7 +172,16 @@ async fn process_repository( #[cfg(test)] crate::bors::WAIT_FOR_MERGE_QUEUE_MERGE_ATTEMPT.mark(); - handle_successful_build(repo, ctx, pr, auto_build, approval_info, pr_num).await?; + handle_successful_build( + repo, + ctx, + pr, + auto_build, + approval_info, + pr_num, + unroll_queue_sender, + ) + .await?; break; } QueueStatus::Approved(approval_info) => { @@ -257,6 +267,7 @@ async fn handle_successful_build( auto_build: &BuildModel, approval_info: &ApprovalInfo, pr_num: PullRequestNumber, + unroll_queue_sender: &UnrollQueueSender, ) -> anyhow::Result<()> { let commit_sha = CommitSha(auto_build.commit_sha.clone()); let workflow_runs = load_workflow_runs(repo, &ctx.db, auto_build) @@ -303,9 +314,21 @@ async fn handle_successful_build( } } else { tracing::info!("Auto build succeeded and merged for PR {pr_num}"); - ctx.db - .set_pr_status(&pr.repository, pr.number, PullRequestStatus::Merged) - .await?; + + if ctx.db.is_rollup(pr).await? { + tracing::info!("Recording waiting rollup member unroll state"); + // Rollup, also mark its members as waiting for an unroll + ctx.db.finish_rollup_merge(pr).await?; + // Trigger the unroll queue + unroll_queue_sender + .process_unrolled_members(repo.repository()) + .await?; + } else { + // Not a rollup, just mark it as merged + ctx.db + .set_pr_status(&pr.repository, pr.number, PullRequestStatus::Merged) + .await?; + } repo.client .post_comment(pr.number, comment, &ctx.db) .await?; @@ -617,10 +640,10 @@ async fn sanity_check_rollup( continue; } }; - if member_gh.head.sha != member.rolled_up_sha { + if member_gh.head.sha != member.rolled_up_head_sha { mismatches.push(RollupMemberMismatch { member: member.member, - expected: member.rolled_up_sha, + expected: member.rolled_up_head_sha, actual: member_gh.head.sha, }); } @@ -679,10 +702,10 @@ async fn start_auto_build( message: auto_merge_commit_message, author: bors_commit_author(), }, - StartBuildCheckRun { + Some(StartBuildCheckRun { name: AUTO_BUILD_CHECK_RUN_NAME.to_string(), title: AUTO_BUILD_CHECK_RUN_NAME.to_string(), - }, + }), pr, ) .await @@ -732,6 +755,7 @@ pub fn start_merge_queue( ctx: Arc, max_interval: chrono::Duration, mergeability_sender: MergeabilityQueueSender, + unroll_queue_sender: UnrollQueueSender, ) -> (MergeQueueSender, impl Future) { let (tx, mut rx) = mpsc::channel::(1024); let sender = MergeQueueSender { inner: tx }; @@ -745,15 +769,17 @@ pub fn start_merge_queue( notified: &mut bool, last_executed_at: &mut DateTime, mergeability_sender: &MergeabilityQueueSender, + unroll_queue_sender: &UnrollQueueSender, ) { *notified = false; *last_executed_at = Utc::now(); let span = tracing::info_span!("MergeQueue"); tracing::debug!("Processing merge queue"); - if let Err(error) = merge_queue_tick(ctx.clone(), mergeability_sender) - .instrument(span.clone()) - .await + if let Err(error) = + merge_queue_tick(ctx.clone(), mergeability_sender, unroll_queue_sender) + .instrument(span.clone()) + .await { // In tests, we want to panic on all errors. #[cfg(test)] @@ -777,6 +803,7 @@ pub fn start_merge_queue( &mut notified, &mut last_executed_at, &mergeability_sender, + &unroll_queue_sender, ) .await; } @@ -788,6 +815,7 @@ pub fn start_merge_queue( &mut notified, &mut last_executed_at, &mergeability_sender, + &unroll_queue_sender, ) .await; } diff --git a/src/bors/mod.rs b/src/bors/mod.rs index 85b0e57b..96ccf6ef 100644 --- a/src/bors/mod.rs +++ b/src/bors/mod.rs @@ -36,6 +36,7 @@ mod labels; pub mod merge_queue; pub mod mergeability_queue; pub mod process; +pub mod unroll_queue; use crate::PgDbClient; use crate::bors::command::BorsCommand; @@ -50,13 +51,22 @@ pub use job_cache::{WorkflowJobData, WorkflowJobStatus}; /// This branch should run CI checks. pub const AUTO_BRANCH_NAME: &str = "automation/bors/auto"; +/// Branch where CI checks run for try builds. /// This branch should run CI checks. pub const TRY_BRANCH_NAME: &str = "automation/bors/try"; +/// Branch where CI checks run for unrolled perf builds. +/// This branch should run CI checks. +pub const TRY_PERF_BRANCH_NAME: &str = "automation/bors/try-perf"; + #[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum BuildKind { + /// Try build on a PR, to test a set of CI jobs. Try, + /// Merge build, which attempts to merge a PR into the target branch. Auto, + /// Unrolled build of a rollup member, designed for compiler performance tests. + UnrolledMember, } /// Format the bors command help in Markdown format. diff --git a/src/bors/process.rs b/src/bors/process.rs index 69a39cc1..77772e69 100644 --- a/src/bors/process.rs +++ b/src/bors/process.rs @@ -1,13 +1,12 @@ use crate::bors::build_queue::{ BuildQueueReceiver, BuildQueueSender, create_build_queue, handle_build_queue_event, }; -#[cfg(not(test))] -use crate::bors::gitops_queue::handle_gitops_entry; use crate::bors::gitops_queue::{GitOpsQueueReceiver, GitOpsQueueSender, create_gitops_queue}; use crate::bors::merge_queue::{MergeQueueSender, start_merge_queue}; use crate::bors::mergeability_queue::{ MergeabilityQueueReceiver, MergeabilityQueueSender, create_mergeability_queue, }; +use crate::bors::unroll_queue::{UnrollQueueReceiver, UnrollQueueSender, create_unroll_queue}; use crate::bors::{handle_bors_global_event, handle_bors_repository_event}; use crate::{BorsContext, BorsGlobalEvent, BorsRepositoryEvent, TeamApiClient}; use anyhow::Error; @@ -22,14 +21,14 @@ pub struct BorsProcess { pub global_tx: mpsc::Sender, pub senders: QueueSenders, pub bors_process: Pin + Send>>, - // In tests, we want to run the mergeability queue manually, rather than it running in the + // In tests, we want to run the following queues manually, rather than it running in the // background, to have the ability to simulate various race conditions. #[cfg(test)] pub mergeability_queue_rx: MergeabilityQueueReceiver, - // In tests, we want to run gitops operations manually, to have explicit control over when - // expensive git operations are executed. #[cfg(test)] pub gitops_queue_rx: GitOpsQueueReceiver, + #[cfg(test)] + pub unroll_queue_rx: UnrollQueueReceiver, } /// Creates a future with a Bors process that continuously receives webhook events and reacts to @@ -45,19 +44,21 @@ pub fn create_bors_process( let (mergeability_queue_tx, mergeability_queue_rx) = create_mergeability_queue(); let (gitops_queue_tx, gitops_queue_rx) = create_gitops_queue(ctx.get_git()); + let (build_queue_tx, build_queue_rx) = create_build_queue(); + let (unroll_queue_tx, unroll_queue_rx) = create_unroll_queue(); let (merge_queue_tx, merge_queue_fut) = start_merge_queue( ctx.clone(), merge_queue_max_interval, mergeability_queue_tx.clone(), + unroll_queue_tx.clone(), ); - let (build_queue_tx, build_queue_rx) = create_build_queue(); - let senders = QueueSenders { merge_queue: merge_queue_tx.clone(), mergeability_queue: mergeability_queue_tx, build_queue: build_queue_tx, gitops_queue: gitops_queue_tx, + unroll_queue: unroll_queue_tx.clone(), }; let senders2 = senders.clone(); @@ -71,12 +72,20 @@ pub fn create_bors_process( let _ = tokio::join!( consume_repository_events(ctx.clone(), repository_rx, senders2.clone()), consume_global_events(ctx.clone(), global_rx, senders2, gh_client, team_api), - consume_build_queue_events(ctx.clone(), build_queue_rx, merge_queue_tx), + consume_build_queue_events( + ctx.clone(), + build_queue_rx, + merge_queue_tx, + unroll_queue_tx + ), merge_queue_fut ); - // Note that we do not run the mergeability queue or gitops queue automatically in - // tests, to have more control over them. Instead, we add them to the bors context - // below. + // Note that we do not run: + // - mergeability queue + // - unroll queue + // - gitops queue + // automatically in tests, to have more control over them. Instead, we add them to the + // bors context below. } // In real execution, the bot runs forever. If there is something that finishes // the futures early, it's essentially a bug. @@ -93,9 +102,12 @@ pub fn create_bors_process( _ = consume_mergeability_queue_events(ctx.clone(), mergeability_queue_rx) => { tracing::error!("Mergeability queue handling process has ended") } - _ = consume_build_queue_events(ctx.clone(), build_queue_rx, merge_queue_tx) => { + _ = consume_build_queue_events(ctx.clone(), build_queue_rx, merge_queue_tx, unroll_queue_tx) => { tracing::error!("Build queue handling process has ended") } + _ = consume_unroll_events(ctx.clone(), unroll_queue_rx) => { + tracing::error!("Unroll queue handling process has ended"); + } _ = consume_gitops_queue_events(gitops_queue_rx) => { tracing::error!("Gitops queue handling process has ended") } @@ -115,6 +127,8 @@ pub fn create_bors_process( mergeability_queue_rx, #[cfg(test)] gitops_queue_rx, + #[cfg(test)] + unroll_queue_rx, } } @@ -124,6 +138,7 @@ pub struct QueueSenders { merge_queue: MergeQueueSender, build_queue: BuildQueueSender, gitops_queue: GitOpsQueueSender, + unroll_queue: UnrollQueueSender, } impl QueueSenders { @@ -139,6 +154,9 @@ impl QueueSenders { pub fn gitops_queue(&self) -> &GitOpsQueueSender { &self.gitops_queue } + pub fn unroll_queue(&self) -> &UnrollQueueSender { + &self.unroll_queue + } } async fn consume_repository_events( @@ -210,14 +228,16 @@ async fn consume_build_queue_events( ctx: Arc, mut build_queue_rx: BuildQueueReceiver, merge_queue_tx: MergeQueueSender, + unroll_queue_tx: UnrollQueueSender, ) { while let Some(event) = build_queue_rx.recv().await { let ctx = ctx.clone(); let span = tracing::debug_span!("Build queue event", "{event:?}"); - if let Err(error) = handle_build_queue_event(ctx, event, merge_queue_tx.clone()) - .instrument(span.clone()) - .await + if let Err(error) = + handle_build_queue_event(ctx, event, merge_queue_tx.clone(), unroll_queue_tx.clone()) + .instrument(span.clone()) + .await { handle_root_error(span, error); } @@ -227,8 +247,27 @@ async fn consume_build_queue_events( } } +#[cfg(not(test))] +async fn consume_unroll_events(ctx: Arc, mut unroll_queue_rx: UnrollQueueReceiver) { + use crate::bors::unroll_queue::handle_unroll_queue_event; + + while let Some(event) = unroll_queue_rx.recv().await { + let ctx = ctx.clone(); + + let span = tracing::debug_span!("Unroll queue event", "{event:?}"); + if let Err(error) = handle_unroll_queue_event(ctx, event) + .instrument(span.clone()) + .await + { + handle_root_error(span, error); + } + } +} + #[cfg(not(test))] async fn consume_gitops_queue_events(mut gitops_queue_rx: GitOpsQueueReceiver) { + use crate::bors::gitops_queue::handle_gitops_entry; + while let Some(entry) = gitops_queue_rx.recv().await { let span = tracing::debug_span!("Gitops queue command", "{entry:?}"); if let Err(error) = handle_gitops_entry(&gitops_queue_rx, entry) diff --git a/src/bors/unroll_queue.rs b/src/bors/unroll_queue.rs new file mode 100644 index 00000000..a9cfe42a --- /dev/null +++ b/src/bors/unroll_queue.rs @@ -0,0 +1,748 @@ +use crate::bors::build::{ + StartBuildCommit, StartBuildContext, StartBuildError, StartBuildOutcome, start_build, +}; +use crate::bors::{BuildKind, Comment, RepositoryState, TRY_PERF_BRANCH_NAME, bors_commit_author}; +use crate::database::{ + BuildModel, BuildStatus, ExclusiveOperationOutcome, PullRequestModel, RollupMemberForUnrolling, + UnrollState, +}; +use crate::github::{CommitSha, GithubRepoName, PullRequestNumber}; +use crate::{BorsContext, PgDbClient}; +use anyhow::Context; +use std::collections::HashMap; +use std::fmt::Write; +use std::sync::Arc; +use tokio::sync::mpsc; +use tracing::{Instrument, info_span}; + +// This branch serves for preparing the final commit. +// It will be reset to master and merged with the branch that should be tested. +// Because this action (reset + merge) is not atomic, this branch should not run CI checks to avoid +// starting them twice. +const TRY_PERF_MERGE_BRANCH_NAME: &str = "automation/bors/try-perf-merge"; + +pub type UnrollQueueReceiver = mpsc::Receiver; + +#[derive(Debug)] +pub enum UnrollQueueEvent { + ProcessUnrolledMembers(GithubRepoName), +} + +#[derive(Clone)] +pub struct UnrollQueueSender { + inner: mpsc::Sender, +} + +impl UnrollQueueSender { + pub async fn process_unrolled_members( + &self, + repo: &GithubRepoName, + ) -> Result<(), mpsc::error::SendError> { + self.inner + .send(UnrollQueueEvent::ProcessUnrolledMembers(repo.clone())) + .await + } +} + +pub fn create_unroll_queue() -> (UnrollQueueSender, UnrollQueueReceiver) { + let (tx, rx) = tokio::sync::mpsc::channel(1024); + (UnrollQueueSender { inner: tx }, rx) +} + +/// This function starts unrolled builds for rollup members, +/// and sends a final comment to the rollup once all the builds are finished. +/// +/// In theory, this could be implemented within the build queue, but there are some reasons why +/// not to do that: +/// - Unrolling operations should not interfere with build queue operations, which are more +/// important. This is particularly important for starting the unrolled builds, which can take a +/// long time for large rollups. +/// - We can explicitly trigger it in tests without also triggering the build queue. +/// +/// We implement this as a separate unroll queue, so that it does not block other background sync +/// processes, and so that we can easily trigger it via a dedicated queue +pub async fn handle_unroll_queue_event( + ctx: Arc, + event: UnrollQueueEvent, +) -> anyhow::Result<()> { + let UnrollQueueEvent::ProcessUnrolledMembers(repo) = event; + let Ok(repo) = ctx.get_repo(&repo) else { + return Err(anyhow::anyhow!("Repo {repo} not found")); + }; + + if repo.config.load().unroll.is_none() { + return Ok(()); + } + + let db = &ctx.db; + let span = info_span!( + "Processing unrolled builds", + repo = repo.repository().to_string() + ); + process_unrolled_members(&repo, db).instrument(span).await?; + + Ok(()) +} + +async fn process_unrolled_members(repo: &RepositoryState, db: &PgDbClient) -> anyhow::Result<()> { + // Find all unrolled members that have not been processed yet + let members: Vec = db + .get_rollup_members_for_unrolling(repo.repository()) + .await?; + if members.is_empty() { + return Ok(()); + } + + tracing::info!( + "Found {} rollup members for unroll processing", + members.len() + ); + + // Group members by their rollup + let rollups: HashMap> = + members + .into_iter() + .fold(HashMap::new(), |mut rollups, member| { + rollups + .entry(member.member.rollup_id) + .or_default() + .push(member); + rollups + }); + + for (rollup_id, members) in rollups { + let Some(rollup) = db.get_pull_request_by_id(rollup_id).await? else { + tracing::error!("Rollup with ID {rollup_id} could not be found in the database"); + // The rollup is somehow missing in the DB, finish all its members + db.set_all_rollup_members_state(rollup_id, UnrollState::Reported) + .await?; + continue; + }; + let rollup_number = rollup.number; + let Some(rollup_auto_build) = &rollup.auto_build else { + tracing::error!("Rollup #{rollup_number} has no auto build attached"); + db.set_all_rollup_members_state(rollup_id, UnrollState::Reported) + .await?; + continue; + }; + + tracing::info!( + "Processing {} unrolled members of rollup #{rollup_number}", + members.len(), + ); + let span = info_span!("Rollup unrolling", rollup = rollup_number.0); + + process_rollup(db, repo, &rollup, rollup_auto_build, &members) + .instrument(span) + .await + .with_context(|| { + anyhow::anyhow!("Transient error occurred while unrolling rollup #{rollup_number}") + })?; + } + Ok(()) +} + +enum UnrollError { + CommitNotFound { sha: CommitSha }, + MergeConflict, + Transient(anyhow::Error), +} + +impl From for UnrollError { + fn from(error: anyhow::Error) -> Self { + Self::Transient(error) + } +} + +async fn process_rollup<'a>( + db: &'a PgDbClient, + repo: &'a RepositoryState, + rollup: &PullRequestModel, + rollup_auto_build: &BuildModel, + members: &'a [RollupMemberForUnrolling], +) -> anyhow::Result<()> { + let mut completed_members: HashMap = HashMap::new(); + + for member in members { + let unroll_state = member.member.unroll_state.as_ref().unwrap_or_else(|| panic!("get_rollup_members_for_unrolling returned a rollup member {member:?} with NULL unroll state")); + tracing::info!("Member #{} state: {unroll_state:?}", member.pr.number); + + match unroll_state { + UnrollState::Waiting => { + // No unrolled build started yet, start it + let build_result = start_unrolled_build(db, repo, rollup_auto_build, member).await; + let merge_sha = match build_result { + Ok(sha) => sha, + Err(UnrollError::CommitNotFound { sha }) => { + tracing::error!( + "Merge commit {sha} for member {member:?} could not be found" + ); + // Consider this member's unrolled build to be failed + db.set_rollup_member_state(&member.member, UnrollState::Finished) + .await?; + continue; + } + Err(UnrollError::MergeConflict) => { + tracing::error!( + "Merge conflict happened while creating an unrolled build for member {member:?}" + ); + // Consider this member's unrolled build to be failed + db.set_rollup_member_state(&member.member, UnrollState::Finished) + .await?; + continue; + } + Err(UnrollError::Transient(error)) => { + return Err(error.context(format!("Rollup member {member:?}"))); + } + }; + tracing::info!("Started unrolled build with SHA `{merge_sha}`"); + + // The build has been started, mark the member as pending + // If the member is pending, there must always be a build present for it! + db.set_rollup_member_state(&member.member, UnrollState::Pending) + .await?; + } + UnrollState::Pending => { + let unrolled_build = member.pr.unrolled_build.as_ref().unwrap_or_else(|| { + panic!( + "Rollup member {member:?} has unroll state pending, but no attached unrolled build" + ) + }); + + // If a pending build has finished in the meantime, mark it as such + match unrolled_build.status { + BuildStatus::Pending => {} + BuildStatus::Success + | BuildStatus::Failure + | BuildStatus::Cancelled + | BuildStatus::Timeouted => { + completed_members.insert( + member.pr.number, + CompletedMember { + member, + build: Some(unrolled_build), + }, + ); + // Mark the member as finished + db.set_rollup_member_state(&member.member, UnrollState::Finished) + .await?; + } + } + } + UnrollState::Finished => { + let unrolled_build = member.pr.unrolled_build.as_ref(); + + // This member was already previously completed + completed_members.insert( + member.pr.number, + CompletedMember { + member, + build: unrolled_build, + }, + ); + } + UnrollState::Reported => { + // This should not happen... + panic!( + "Encountered unprocessed rollup member {member:?} with unroll state reported" + ); + } + } + } + + // All members are completed, finish the unrolling process + if completed_members.len() == members.len() { + tracing::info!("All rollup members processed, sending the comment"); + + // Send comment + let members: Vec = completed_members.into_values().collect(); + let parent_sha = CommitSha(rollup_auto_build.parent.clone()); + let comment = + create_unroll_result_comment(repo.repository(), db, parent_sha, members).await; + repo.client.post_comment(rollup.number, comment, db).await?; + + // If that succeeded, mark all members as reported + // We cannot atomically send the comment and mark the members as reported. + // GitHub failures are more common than DB failures, so we prefer sending the comment twice + // in case of a DB error, rather than missing sending of the comment in case of a GitHub + // error. + db.set_all_rollup_members_state(rollup.id, UnrollState::Reported) + .await?; + + tracing::info!("Finished unrolling"); + + // At this point, the unrolling is finished and the given rollup and its members should + // never go to this function again + } + + Ok(()) +} + +async fn create_unroll_result_comment( + repo: &GithubRepoName, + db: &PgDbClient, + parent_sha: CommitSha, + mut members: Vec>, +) -> Comment { + // We want to sort the members by the order they occurred in the rollup + members.sort_by_key(|v| v.member.member.position); + + let mut unrolled_rows = String::new(); + for member in members { + let commit = match member.build { + Some(build) => match build.status { + BuildStatus::Success => { + let sha = &build.commit_sha; + format!("`{sha}`
([link](https://github.com/{repo}/commit/{sha}))",) + } + BuildStatus::Failure + | BuildStatus::Cancelled + | BuildStatus::Pending + | BuildStatus::Timeouted => { + // This is best effort, so we ignore errors + let workflow_url = db + .get_workflow_urls_for_build(build) + .await + .unwrap_or_default() + .into_iter() + .next(); + let status = if let Some(url) = workflow_url { + format!("[failed]({url})") + } else { + "failed".to_string() + }; + format!(":x: build {status} :x:") + } + }, + None => ":x: conflicts merging into previous parent commit :x:".to_string(), + }; + + let title = format_rollup_member_message(&member.member.pr.title).replace('|', "\\|"); + writeln!( + &mut unrolled_rows, + "|#{pr}|{title}|{commit}|", + pr = member.member.pr.number + ) + .unwrap(); + } + + let truncated = parent_sha.0.chars().take(10).collect::(); + let parent_sha_link = format!("[{truncated}](https://github.com/{repo}/commit/{parent_sha})"); + Comment::new(format!( + ":pushpin: Perf builds for each rolled up PR:\n\n\ + | PR# | Message | Perf Build Sha |\n|----|----|:-----:|\n\ + {unrolled_rows}\n\ + *parent commit*: {parent_sha_link}\n\nIn the case of a perf regression, \ + run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA`" + )) +} + +fn format_rollup_member_message(message: &str) -> String { + let truncated = message.chars().take(59).collect::(); + if message.chars().count() > 60 { + format!("{truncated}…") + } else { + message.to_string() + } +} + +struct CompletedMember<'a> { + member: &'a RollupMemberForUnrolling, + build: Option<&'a BuildModel>, +} + +/// Starts an unrolled build for the given rollup member and return its merge sha. +async fn start_unrolled_build( + db: &PgDbClient, + repo: &RepositoryState, + rollup_auto_build: &BuildModel, + member: &RollupMemberForUnrolling, +) -> Result { + // The SHA upon which we will base the merge + // This is the parent commit of the final merge SHA of the rollup + // We fetch it from its auto build + let base_sha = CommitSha(rollup_auto_build.parent.clone()); + + // The rollup HEAD SHA that we are merging + let head_sha = member.member.rolled_up_head_sha.clone(); + + // Commit message of the merge. We lookup the intermediate member rollup merge commit from + // GitHub. + let message = repo + .client + .get_commit_message(&member.member.rolled_up_merge_sha) + .await?; + let Some(message) = message else { + return Err(UnrollError::CommitNotFound { + sha: member.member.rolled_up_merge_sha.clone(), + }); + }; + let message = format!("Unrolled build for #{}\n{message}", member.pr.number); + + let res = db + .ensure_not_concurrent( + BuildKind::UnrolledMember, + repo.repository(), + async move |proof| { + let outcome = start_build( + db, + repo, + &proof, + StartBuildContext { + merge_branch: TRY_PERF_MERGE_BRANCH_NAME.to_string(), + ci_branch: TRY_PERF_BRANCH_NAME.to_string(), + base_sha, + head_sha, + build_kind: BuildKind::UnrolledMember, + }, + StartBuildCommit { + message, + author: bors_commit_author(), + }, + // Both the members and the rollup are merged, and the GitHub UI does not show + // check runs for merged PRs, so this is unnecessary + None, + &member.pr, + ) + .await + .map_err(|e| match e { + StartBuildError::GithubError(e) => e, + StartBuildError::DatabaseError(e) => e, + })?; + match outcome { + StartBuildOutcome::Success { + build_commit_sha, .. + } => Ok(build_commit_sha), + StartBuildOutcome::MergeConflict => Err(UnrollError::MergeConflict), + } + }, + ) + .await?; + match res { + ExclusiveOperationOutcome::Performed(res) => res, + ExclusiveOperationOutcome::Skipped => Err(UnrollError::Transient(anyhow::anyhow!( + "Cannot start unrolled build due to a concurrent bors instance." + ))), + } +} + +#[cfg(test)] +mod tests { + use crate::database::UnrollState; + use crate::github::{PullRequestNumber, make_rollup, rollup_state}; + use crate::tests::{ + BorsTester, Comment, MergeBehavior, PullRequest, default_repo_name, run_test, + }; + use http::StatusCode; + + #[sqlx::test] + async fn unroll_success(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + + let rollup = make_merged_rollup(ctx, &[&pr2, &pr3]).await?; + + // Ensure that waiting state has been set + ctx.get_rollup(rollup) + .await? + .expect_unroll_state_all(UnrollState::Waiting); + + // Unrolling should have been triggered by the merge queue after the rollup was merged + ctx.run_unroll_queue().await?; + + ctx.get_rollup(rollup) + .await? + .expect_unroll_state_all(UnrollState::Pending); + + for workflow in ctx.unrolled_workflows(2) { + ctx.workflow_full_success(workflow).await?; + } + ctx.run_unroll_queue().await?; + + let comment = ctx.get_next_comment_text(rollup).await?; + insta::assert_snapshot!(comment, @" + :pushpin: Perf builds for each rolled up PR: + + | PR# | Message | Perf Build Sha | + |----|----|:-----:| + |#2|Title of PR 2|`merge-0-pr-2-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-0-pr-2-d7d45f1f-reauthored-to-bors))| + |#3|Title of PR 3|`merge-1-pr-3-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-1-pr-3-d7d45f1f-reauthored-to-bors))| + + *parent commit*: [main-sha1](https://github.com/rust-lang/borstest/commit/main-sha1) + + In the case of a perf regression, run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA` + "); + + ctx.get_rollup(rollup) + .await? + .expect_unroll_state_all(UnrollState::Reported); + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_unrolled_build_commit_message(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + + make_merged_rollup(ctx, &[&pr2]).await?; + ctx.run_unroll_queue().await?; + + insta::assert_snapshot!(ctx.unrolled_branch().get_commit().message(), @" + Unrolled build for #2 + Rollup merge of #2 - default-user:pr/2, r=default-user + + Title of PR 2 + + Description of PR 2 + "); + + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_merge_conflict(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + + let rollup = make_merged_rollup(ctx, &[&pr2, &pr3]).await?; + + ctx.modify_repo(default_repo_name(), |repo| { + let mut n = 0; + repo.merge_behavior = MergeBehavior::Custom(Box::new(move || { + n += 1; + // Cause a conflict on the second member merge + (n == 2).then_some(StatusCode::CONFLICT) + })); + }); + + ctx.run_unroll_queue().await?; + for workflow in ctx.unrolled_workflows(1) { + ctx.workflow_full_success(workflow).await?; + } + ctx.run_unroll_queue().await?; + + let comment = ctx.get_next_comment_text(rollup).await?; + insta::assert_snapshot!(comment, @" + :pushpin: Perf builds for each rolled up PR: + + | PR# | Message | Perf Build Sha | + |----|----|:-----:| + |#2|Title of PR 2|`merge-0-pr-2-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-0-pr-2-d7d45f1f-reauthored-to-bors))| + |#3|Title of PR 3|:x: conflicts merging into previous parent commit :x:| + + *parent commit*: [main-sha1](https://github.com/rust-lang/borstest/commit/main-sha1) + + In the case of a perf regression, run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA` + "); + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_workflow_failure(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + + let rollup = make_merged_rollup(ctx, &[&pr2, &pr3]).await?; + ctx.run_unroll_queue().await?; + + let workflows = ctx.unrolled_workflows(2); + ctx.workflow_full_failure(workflows[0]).await?; + ctx.workflow_full_success(workflows[1]).await?; + ctx.run_unroll_queue().await?; + + let comment = ctx.get_next_comment_text(rollup).await?; + insta::assert_snapshot!(comment, @" + :pushpin: Perf builds for each rolled up PR: + + | PR# | Message | Perf Build Sha | + |----|----|:-----:| + |#2|Title of PR 2|:x: build [failed](https://github.com/rust-lang/borstest/actions/runs/2) :x:| + |#3|Title of PR 3|`merge-1-pr-3-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-1-pr-3-d7d45f1f-reauthored-to-bors))| + + *parent commit*: [main-sha1](https://github.com/rust-lang/borstest/commit/main-sha1) + + In the case of a perf regression, run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA` + "); + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_pr_order(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + let pr4 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + ctx.approve(pr4.id()).await?; + // This PR should be the first member of the rollup, due to the priority + ctx.post_comment(Comment::new(pr3.id(), "@bors p=5")).await?; + + let rollup = prepare_rollup(ctx, &[&pr2, &pr3, &pr4]).await?; + // This is needed so that the rollup is actually merged first + ctx.post_comment(Comment::new(rollup, "@bors p=10")).await?; + ctx.start_and_finish_auto_build(rollup).await?; + + ctx.run_unroll_queue().await?; + + for workflow in ctx.unrolled_workflows(3) { + ctx.workflow_full_success(workflow).await?; + } + ctx.run_unroll_queue().await?; + + let comment = ctx.get_next_comment_text(rollup).await?; + insta::assert_snapshot!(comment, @" + :pushpin: Perf builds for each rolled up PR: + + | PR# | Message | Perf Build Sha | + |----|----|:-----:| + |#3|Title of PR 3|`merge-1-pr-3-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-1-pr-3-d7d45f1f-reauthored-to-bors))| + |#2|Title of PR 2|`merge-0-pr-2-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-0-pr-2-d7d45f1f-reauthored-to-bors))| + |#4|Title of PR 4|`merge-2-pr-4-d7d45f1f-reauthored-to-bors`
([link](https://github.com/rust-lang/borstest/commit/merge-2-pr-4-d7d45f1f-reauthored-to-bors))| + + *parent commit*: [main-sha1](https://github.com/rust-lang/borstest/commit/main-sha1) + + In the case of a perf regression, run the following command for each PR you suspect might be the cause: `@rust-timer build $SHA` + "); + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_recover_transient_error(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + + let rollup = make_merged_rollup(ctx, &[&pr2, &pr3]).await?; + + ctx.modify_repo(default_repo_name(), |repo| { + repo.merge_behavior = + MergeBehavior::Custom(Box::new(|| Some(StatusCode::INTERNAL_SERVER_ERROR))); + }); + + // Transient error during merging + assert!(ctx.run_unroll_queue().await.is_err()); + ctx.get_rollup(rollup) + .await? + .expect_unroll_state_all(UnrollState::Waiting); + + ctx.modify_repo(default_repo_name(), |repo| { + repo.merge_behavior = MergeBehavior::default(); + }); + + ctx.trigger_and_run_unroll_queue().await?; + ctx.get_rollup(rollup) + .await? + .expect_unroll_state_all(UnrollState::Pending); + + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_recover_handle_all_members(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + + let rollup = make_merged_rollup(ctx, &[&pr2, &pr3]).await?; + + // Ensure that if one of the members has an error, we will continue processing + // the other ones. + ctx.modify_repo(default_repo_name(), |repo| { + let mut n = 0; + repo.merge_behavior = MergeBehavior::Custom(Box::new(move || { + n += 1; + // Cause a conflict only on the first member merge + (n == 1).then_some(StatusCode::CONFLICT) + })); + }); + + // Run the unrolling once, the second member should still be processed + // even if processing the first member fails + ctx.run_unroll_queue().await?; + ctx.get_rollup(rollup) + .await? + .expect_unroll_state(2, UnrollState::Finished) + .expect_unroll_state(3, UnrollState::Pending); + + Ok(()) + }) + .await; + } + + #[sqlx::test] + async fn unroll_concurrent_bors_instances(pool: sqlx::PgPool) { + run_test((pool, rollup_state()), async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + ctx.approve(pr3.id()).await?; + + make_merged_rollup(ctx, &[&pr2, &pr3]).await?; + + let concurrent_futs = (0..10) + .map(|_| async { ctx.run_unroll_queue_concurrent().await }) + .collect::>(); + futures::future::join_all(concurrent_futs).await; + + let branch = ctx.unrolled_branch(); + assert_eq!(branch.get_commit_history().len(), 2); + + Ok(()) + }) + .await; + } + + /// Creates and approves a rollup and waits until it is merged. + async fn make_merged_rollup( + ctx: &mut BorsTester, + prs: &[&PullRequest], + ) -> anyhow::Result { + let pr_number = prepare_rollup(ctx, prs).await?; + ctx.start_and_finish_auto_build(pr_number).await?; + Ok(pr_number) + } + + /// Creates and approves a rollup. + async fn prepare_rollup( + ctx: &mut BorsTester, + prs: &[&PullRequest], + ) -> anyhow::Result { + let response = make_rollup(ctx, prs).await?; + let location = response.get_header("location"); + let rollup: u64 = location + .rsplit('/') + .next() + .ok_or_else(|| anyhow::anyhow!("Invalid rollup redirect URL: {location}"))? + .parse()?; + let pr_number = PullRequestNumber(rollup); + ctx.approve(pr_number).await?; + Ok(pr_number) + } +} diff --git a/src/config.rs b/src/config.rs index 0a897a60..3ba88a50 100644 --- a/src/config.rs +++ b/src/config.rs @@ -46,8 +46,14 @@ pub struct RepositoryConfig { /// jobs in the repositories that bors manages. #[serde(default)] pub ec2_runners: Option, + /// Enable unrolling of rollups. + #[serde(default)] + pub unroll: Option, } +#[derive(serde::Deserialize, Debug)] +pub struct UnrollConfig {} + #[derive(serde::Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] pub enum JitRunnerKind { @@ -481,10 +487,33 @@ allowed_instances = ["c8a.12xlarge"] ], }, ), + unroll: None, } "#); } + #[test] + fn deserialize_unroll() { + let content = r#" +[unroll] +"#; + let config = load_config(content); + insta::assert_debug_snapshot!(config, @" + RepositoryConfig { + timeout: 3600s, + labels: {}, + labels_blocking_approval: [], + min_ci_time: None, + merge_queue_enabled: false, + report_merge_conflicts: false, + ec2_runners: None, + unroll: Some( + UnrollConfig, + ), + } + "); + } + #[test] fn load_example_config() { let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("rust-bors.example.toml"); diff --git a/src/database/client.rs b/src/database/client.rs index 7a23e326..43076c0e 100644 --- a/src/database/client.rs +++ b/src/database/client.rs @@ -2,17 +2,20 @@ use super::operations::{ approve_pull_request, clear_auto_build, create_build, create_workflow, delegate_pull_request, delete_tagged_bot_comment, find_build, find_pr_by_build, find_rollups_for_member_pr, get_last_n_successful_auto_builds, get_nonclosed_pull_requests, get_pending_builds, - get_prs_with_stale_mergeability_or_approved, get_pull_request, get_repository, - get_repository_by_name, get_rollup_members, get_tagged_bot_comments, - get_workflow_urls_for_build, get_workflows_for_build, insert_repo_if_not_exists, is_rollup, - record_tagged_bot_comment, set_pr_assignees, set_pr_mergeability_state, set_pr_priority, - set_pr_rollup_mode, set_pr_status, set_stale_mergeability_status_by_base_branch, - unapprove_pull_request, undelegate_pull_request, update_build, update_pr_try_build_id, - update_workflow_status, upsert_pull_request, upsert_repository, + get_prs_with_stale_mergeability_or_approved, get_pull_request, get_pull_request_by_id, + get_repository, get_repository_by_name, get_rollup_members, get_rollup_members_for_unrolling, + get_tagged_bot_comments, get_workflow_urls_for_build, get_workflows_for_build, + insert_repo_if_not_exists, is_rollup, record_tagged_bot_comment, set_pr_assignees, + set_pr_mergeability_state, set_pr_priority, set_pr_rollup_mode, set_pr_status, + set_rollup_member_unrolled_state, set_rollup_members_unrolled_state, + set_stale_mergeability_status_by_base_branch, unapprove_pull_request, undelegate_pull_request, + update_build, update_pr_try_build_id, update_pr_unrolled_build_id, update_workflow_status, + upsert_pull_request, upsert_repository, }; use super::{ ApprovalInfo, DelegatedPermission, MergeableState, PrimaryKey, RegisterRollupMemberParams, - RollupMember, RunId, UpdateBuildParams, UpsertPullRequestParams, + RollupMember, RollupMemberForUnrolling, RunId, UnrollState, UpdateBuildParams, + UpsertPullRequestParams, }; use std::collections::{HashMap, HashSet}; @@ -51,13 +54,25 @@ impl PgDbClient { /// **If it is not possible to take the lock, then `func` will NOT be called at all!** pub async fn ensure_not_concurrent( &self, - lock_name: &str, + build_kind: BuildKind, + repo: &GithubRepoName, func: Func, ) -> anyhow::Result> where Func: AsyncFnOnce(ExclusiveLockProof) -> R, { - let lock = PgAdvisoryLock::new(lock_name); + let lock_name = match build_kind { + BuildKind::Try => { + format!("{repo}-try-build") + } + BuildKind::Auto => { + format!("{repo}-auto-build") + } + BuildKind::UnrolledMember => { + format!("{repo}-unrolled-build") + } + }; + let lock = PgAdvisoryLock::new(&lock_name); // Try to acquire the lock let _guard = match lock @@ -178,6 +193,13 @@ impl PgDbClient { get_pull_request(&self.pool, repo, pr_number).await } + pub async fn get_pull_request_by_id( + &self, + id: PrimaryKey, + ) -> anyhow::Result> { + get_pull_request_by_id(&self.pool, id).await + } + /// Create or update a pull request in the database. /// Returns the updated PR state from the database. pub async fn upsert_pull_request( @@ -252,6 +274,29 @@ impl PgDbClient { Ok(build_id) } + /// Creates a new unrolled rollup member build and attaches it to a PR. + pub async fn attach_unrolled_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( + &self.pool, + pr, + &branch, + BuildKind::UnrolledMember, + &commit_sha, + &parent, + ) + .await?; + update_pr_unrolled_build_id(&mut *tx, pr.id, build_id).await?; + tx.commit().await?; + Ok(build_id) + } + pub async fn find_build( &self, repo: &GithubRepoName, @@ -389,15 +434,59 @@ impl PgDbClient { members: &[RegisterRollupMemberParams], ) -> anyhow::Result<()> { let mut tx = self.pool.begin().await?; - for member in members { + for (index, member) in members.iter().enumerate() { assert_ne!(rollup.id, member.member.id); - register_rollup_pr_member(&mut *tx, rollup, &member.member, &member.rolled_up_sha) - .await?; + register_rollup_pr_member(&mut *tx, rollup, member, index).await?; } tx.commit().await?; Ok(()) } + /// Mark the rollup PR as being merged, and set waiting unrolled state for all its members + /// inside a transaction. + pub async fn finish_rollup_merge(&self, rollup: &PullRequestModel) -> anyhow::Result<()> { + let mut tx = self.pool.begin().await?; + set_pr_status( + &mut *tx, + &rollup.repository, + rollup.number, + PullRequestStatus::Merged, + ) + .await?; + set_rollup_members_unrolled_state(&mut *tx, rollup.id, UnrollState::Waiting).await?; + tx.commit().await?; + Ok(()) + } + + /// Set the given unroll state to the given member of a rollup. + pub async fn set_rollup_member_state( + &self, + member: &RollupMember, + state: UnrollState, + ) -> anyhow::Result<()> { + set_rollup_member_unrolled_state(&self.pool, member.rollup_id, member.member_id, state) + .await + } + + /// Set the given unroll state to all members of this rollup. + pub async fn set_all_rollup_members_state( + &self, + rollup_id: i32, + state: UnrollState, + ) -> anyhow::Result<()> { + set_rollup_members_unrolled_state(&self.pool, rollup_id, state).await + } + + /// Mark the rollup PR as being merged, and set waiting unrolled state for all its members + /// inside a transaction. + pub async fn get_rollup_members_for_unrolling( + &self, + repo: &GithubRepoName, + ) -> anyhow::Result> { + let members = get_rollup_members_for_unrolling(&self.pool, repo).await?; + Ok(members) + } + /// Returns a map of rollup PR numbers to the set of member PR numbers that are part of that rollup. /// Only returns non-closed rollup PRs. pub async fn get_nonclosed_rollups( @@ -412,7 +501,7 @@ impl PgDbClient { is_rollup(&self.pool, pr.id).await } - /// Returns true if the given PR is a rollup. + /// If the given `pr` is a rollup, return its members. /// If the returned Vec is empty, the given pull request is not a rollup. pub async fn get_rollup_members( &self, diff --git a/src/database/mod.rs b/src/database/mod.rs index 78d20f6d..24e14476 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -436,6 +436,7 @@ impl sqlx::Encode<'_, sqlx::Postgres> for BuildKind { let tag = match self { Self::Try => "try", Self::Auto => "auto", + Self::UnrolledMember => "unrolled-member", }; <&str as sqlx::Encode>::encode(tag, buf) } @@ -446,6 +447,7 @@ impl sqlx::Decode<'_, sqlx::Postgres> for BuildKind { match <&str as sqlx::Decode>::decode(value)? { "try" => Ok(Self::Try), "auto" => Ok(Self::Auto), + "unrolled-member" => Ok(Self::UnrolledMember), kind => Err(format!("Unknown build kind: {kind}").into()), } } @@ -490,6 +492,9 @@ pub struct PullRequestModel { pub try_build: Option, /// The (latest) auto merge build associated with this PR, if any. pub auto_build: Option, + /// The (latest) unrolled build associated with this PR, if any. + /// This is only present for merged members of a rollup. + pub unrolled_build: Option, pub created_at: DateTime, } @@ -768,15 +773,85 @@ pub struct RegisterRollupMemberParams { /// Pull request model of the member PR. pub member: PullRequestModel, /// HEAD SHA of the member PR at rollup creation. - pub rolled_up_sha: CommitSha, + pub rolled_up_head_sha: CommitSha, + /// SHA of the intermediate merge commit that added this PR to the rollup. + /// It is used to fetch the rollup commit message when performing unrolling. + pub rolled_up_merge_sha: CommitSha, } -#[derive(Debug)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum UnrollState { + /// An unrolled build should be started for this rollup member. + Waiting, + /// An unrolled build has already been started for this rollup member, and is in progress. + Pending, + /// The unrolled build has finished (whether successfully or with a failure). + /// This state is also used if the build cannot be started for this member at all. + Finished, + /// All the unrolled builds for the rollup of this rollup member have been reported in a comment + /// on GitHub + /// + /// Invariant: if one member of a rollup has state reported, all the other members must also + /// have the state reported. + Reported, +} + +impl sqlx::Type for UnrollState { + fn type_info() -> sqlx::postgres::PgTypeInfo { + >::type_info() + } +} + +impl sqlx::Encode<'_, sqlx::Postgres> for UnrollState { + fn encode_by_ref( + &self, + buf: &mut sqlx::postgres::PgArgumentBuffer, + ) -> Result { + let tag = match self { + UnrollState::Waiting => "Waiting", + UnrollState::Pending => "Pending", + UnrollState::Finished => "Finished", + UnrollState::Reported => "Reported", + }; + <&str as sqlx::Encode>::encode(tag, buf) + } +} + +impl sqlx::Decode<'_, sqlx::Postgres> for UnrollState { + fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result { + match <&str as sqlx::Decode>::decode(value)? { + "Waiting" => Ok(Self::Waiting), + "Pending" => Ok(Self::Pending), + "Finished" => Ok(Self::Finished), + "Reported" => Ok(Self::Reported), + tag => Err(format!("Unknown unroll state: {tag}").into()), + } + } +} + +#[derive(Clone, Debug)] pub struct RollupMember { + /// Pull request DB ID of the rollup containing this member PR. + pub rollup_id: PrimaryKey, /// Pull request number of the member PR. pub member: PullRequestNumber, + /// Pull request DB ID of the member PR. + pub member_id: PrimaryKey, /// HEAD SHA of the member PR at rollup creation. - pub rolled_up_sha: CommitSha, + pub rolled_up_head_sha: CommitSha, + /// SHA of the intermediate merge of the PR into its containing rollup. + pub rolled_up_merge_sha: CommitSha, + /// Status of an unrolled commit corresponding to this rollup member. + pub unroll_state: Option, + /// Position of the member within the rollup. + /// Rollup members are merged sequentially, so each member has its specified index. + pub position: u32, +} + +#[derive(Debug)] +pub struct RollupMemberForUnrolling { + pub pr: PullRequestModel, + pub member: RollupMember, } /// Updates the build table with the given fields. @@ -887,6 +962,7 @@ pub fn pr_needs_update_in_db(db_pr: &PullRequestModel, gh_pr: &PullRequest) -> b note: _, try_build: _, auto_build: _, + unrolled_build: _, created_at: _, } = db_pr; let PullRequest { diff --git a/src/database/operations.rs b/src/database/operations.rs index a6ed0740..21623973 100644 --- a/src/database/operations.rs +++ b/src/database/operations.rs @@ -3,9 +3,6 @@ use chrono::Utc; use sqlx::postgres::PgExecutor; use std::collections::{HashMap, HashSet}; -use super::Assignees; -use super::BuildModel; -use super::CommentModel; use super::DelegatedPermission; use super::MergeableState; use super::PullRequestModel; @@ -16,6 +13,9 @@ use super::WorkflowStatus; use super::WorkflowType; use super::{ApprovalInfo, PrimaryKey, UpdateBuildParams}; use super::{ApprovalStatus, RollupMember}; +use super::{Assignees, RegisterRollupMemberParams}; +use super::{BuildModel, UnrollState}; +use super::{CommentModel, RollupMemberForUnrolling}; use crate::bors::PullRequestStatus; use crate::bors::RollupMode; use crate::bors::comment::CommentTag; @@ -65,10 +65,12 @@ pub(crate) async fn get_pull_request( pr.mergeable_state_is_stale, pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM pull_request as pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id WHERE pr.repository = $1 AND pr.number = $2 "#, @@ -83,6 +85,57 @@ pub(crate) async fn get_pull_request( .await } +pub(crate) async fn get_pull_request_by_id( + executor: impl PgExecutor<'_>, + id: PrimaryKey, +) -> anyhow::Result> { + measure_db_query("get_pull_request_by_id", || async { + let record = sqlx::query_as!( + PullRequestModel, + r#" + SELECT + pr.id, + pr.repository as "repository: GithubRepoName", + pr.number as "number: PullRequestNumber", + pr.title, + pr.author, + pr.assignees as "assignees: Assignees", + ( + pr.approved_by, + pr.approved_sha + ) AS "approval_status!: ApprovalStatus", + pr.status as "status: PullRequestStatus", + pr.priority, + pr.rollup as "rollup: RollupMode", + pr.note, + ( + pr.delegatee_id, + pr.delegated_permission + ) AS "delegation!: DelegationStatus", + pr.head_branch, + pr.base_branch, + pr.mergeable_state as "mergeable_state: MergeableState", + pr.mergeable_state_is_stale, + pr.created_at as "created_at: DateTime", + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" + FROM pull_request as pr + LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id + WHERE pr.id = $1 + "#, + id + ) + .fetch_optional(executor) + .await?; + + Ok(record) + }) + .await +} + pub(crate) async fn set_pr_status( executor: impl PgExecutor<'_>, repo: &GithubRepoName, @@ -193,10 +246,12 @@ pub(crate) async fn upsert_pull_request( pr.mergeable_state_is_stale, pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM upserted_pr as pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id "#, repo as &GithubRepoName, pr_number.0 as i64, @@ -249,10 +304,12 @@ pub(crate) async fn get_nonclosed_pull_requests( pr.mergeable_state_is_stale, pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM pull_request as pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id WHERE pr.repository = $1 AND pr.status IN ('open', 'draft') "#, @@ -334,10 +391,12 @@ pub(crate) async fn get_prs_with_stale_mergeability_or_approved( pr.mergeable_state_is_stale, pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM pull_request as pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id WHERE pr.repository = $1 AND (pr.mergeable_state = 'unknown' OR pr.mergeable_state_is_stale = true OR pr.approved_by IS NOT NULL) AND pr.status IN ('open', 'draft') @@ -394,10 +453,12 @@ pub(crate) async fn set_stale_mergeability_status_by_base_branch( pr.mergeable_state_is_stale, pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id "#, repo as &GithubRepoName, base_branch, @@ -545,10 +606,12 @@ SELECT pr.rollup as "rollup: RollupMode", pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM pull_request as pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id +LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id WHERE try_build.id = $1 OR auto_build.id = $1 "#, build_id @@ -597,6 +660,24 @@ pub(crate) async fn update_pr_auto_build_id( .await } +pub(crate) async fn update_pr_unrolled_build_id( + executor: impl PgExecutor<'_>, + pr_id: i32, + unrolled_build_id: i32, +) -> anyhow::Result<()> { + measure_db_query("update_pr_unrolled_build_id", || async { + sqlx::query!( + "UPDATE pull_request SET unrolled_build_id = $1 WHERE id = $2", + unrolled_build_id, + pr_id + ) + .execute(executor) + .await?; + Ok(()) + }) + .await +} + pub(crate) async fn create_build( executor: impl PgExecutor<'_>, pr: &PullRequestModel, @@ -1152,18 +1233,20 @@ pub(crate) async fn clear_auto_build( pub(crate) async fn register_rollup_pr_member( executor: impl PgExecutor<'_>, rollup: &PullRequestModel, - member: &PullRequestModel, - rolled_up_sha: &CommitSha, + params: &RegisterRollupMemberParams, + position: usize, ) -> anyhow::Result<()> { measure_db_query("register_rollup_pr_member", || async { sqlx::query!( r#" - INSERT INTO rollup_member (rollup, member, rolled_up_sha) - VALUES ($1, $2, $3) + INSERT INTO rollup_member (rollup, member, rolled_up_sha, rolled_up_merge_sha, position) + VALUES ($1, $2, $3, $4, $5) "#, rollup.id, - member.id, - rolled_up_sha.as_ref() + params.member.id, + params.rolled_up_head_sha.as_ref(), + params.rolled_up_merge_sha.as_ref(), + position as i32 ) .execute(executor) .await?; @@ -1227,31 +1310,182 @@ pub(crate) async fn is_rollup( pub(crate) async fn get_rollup_members( executor: impl PgExecutor<'_>, - pr_id: PrimaryKey, + rollup_pr_id: PrimaryKey, ) -> anyhow::Result> { measure_db_query("get_rollup_members", || async { let rows = sqlx::query!( r#" - SELECT pr.number AS number, rm.rolled_up_sha AS sha + SELECT + rm.rollup, + rm.member, + pr.number AS number, + rm.rolled_up_sha AS rolled_up_head_sha, + rm.rolled_up_merge_sha AS rolled_up_merge_sha, + rm.unroll_state as "unroll_state: UnrollState", + rm.position as position FROM rollup_member rm JOIN pull_request AS pr ON pr.id = rm.member WHERE rm.rollup = $1 "#, - pr_id + rollup_pr_id ) .fetch_all(executor) .await?; Ok(rows .into_iter() .map(|row| RollupMember { + rollup_id: row.rollup, member: PullRequestNumber(row.number as u64), - rolled_up_sha: CommitSha(row.sha), + member_id: row.member, + rolled_up_head_sha: CommitSha(row.rolled_up_head_sha), + rolled_up_merge_sha: CommitSha(row.rolled_up_merge_sha), + unroll_state: row.unroll_state, + position: row.position as u32, }) .collect()) }) .await } +pub(crate) async fn get_rollup_members_for_unrolling( + executor: impl PgExecutor<'_>, + repo: &GithubRepoName, +) -> anyhow::Result> { + measure_db_query("get_rollup_members_for_unrolling", || async { + let rows = sqlx::query!( + r#" + SELECT + pr.id, + pr.repository as "repository: GithubRepoName", + pr.number as "number: PullRequestNumber", + pr.title, + pr.author, + pr.assignees as "assignees: Assignees", + ( + pr.approved_by, + pr.approved_sha + ) AS "approval_status!: ApprovalStatus", + pr.status as "status: PullRequestStatus", + pr.priority, + pr.rollup as "rollup: RollupMode", + pr.note, + ( + pr.delegatee_id, + pr.delegated_permission + ) AS "delegation!: DelegationStatus", + pr.head_branch, + pr.base_branch, + pr.mergeable_state as "mergeable_state: MergeableState", + pr.mergeable_state_is_stale, + pr.created_at as "created_at: DateTime", + try_build AS "try_build: BuildModel", + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel", + rm.rollup AS rollup_id, + rm.member, + rm.rolled_up_sha, + rm.rolled_up_merge_sha, + rm.unroll_state AS "unroll_state: UnrollState", + rm.position + FROM pull_request as pr + LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id + LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id + JOIN rollup_member AS rm ON pr.id = rm.member + WHERE pr.repository = $1 AND + -- These states are "active" - we need to do something about them + rm.unroll_state IN ('Waiting', 'Pending', 'Finished') + "#, + repo as &GithubRepoName + ) + .fetch_all(executor) + .await?; + Ok(rows + .into_iter() + .map(|row| RollupMemberForUnrolling { + pr: PullRequestModel { + id: row.id, + repository: row.repository, + number: row.number, + title: row.title, + author: row.author, + assignees: row.assignees.into(), + status: row.status, + head_branch: row.head_branch, + base_branch: row.base_branch, + mergeable_state: row.mergeable_state, + mergeable_state_is_stale: row.mergeable_state_is_stale, + approval_status: row.approval_status, + delegation: row.delegation, + priority: row.priority, + rollup: row.rollup, + note: row.note, + try_build: row.try_build, + auto_build: row.auto_build, + unrolled_build: row.unrolled_build, + created_at: row.created_at, + }, + member: RollupMember { + rollup_id: row.rollup_id, + member: row.number, + member_id: row.member, + rolled_up_head_sha: CommitSha(row.rolled_up_sha), + rolled_up_merge_sha: CommitSha(row.rolled_up_merge_sha), + unroll_state: row.unroll_state, + position: row.position as u32, + }, + }) + .collect()) + }) + .await +} + +pub(crate) async fn set_rollup_member_unrolled_state( + executor: impl PgExecutor<'_>, + rollup_id: PrimaryKey, + member_id: PrimaryKey, + state: UnrollState, +) -> anyhow::Result<()> { + measure_db_query("set_rollup_member_unrolled_state", || async { + sqlx::query!( + r#" + UPDATE rollup_member + SET unroll_state = $1 + WHERE rollup = $2 AND member = $3 + "#, + state as UnrollState, + rollup_id, + member_id, + ) + .execute(executor) + .await?; + Ok(()) + }) + .await +} + +pub(crate) async fn set_rollup_members_unrolled_state( + executor: impl PgExecutor<'_>, + rollup_id: PrimaryKey, + state: UnrollState, +) -> anyhow::Result<()> { + measure_db_query("set_rollup_members_unrolled_state", || async { + sqlx::query!( + r#" + UPDATE rollup_member + SET unroll_state = $1 + WHERE rollup = $2 + "#, + state as UnrollState, + rollup_id + ) + .execute(executor) + .await?; + Ok(()) + }) + .await +} + pub(crate) async fn find_rollups_for_member_pr( executor: impl PgExecutor<'_>, rollup_id: PrimaryKey, @@ -1285,10 +1519,12 @@ pub(crate) async fn find_rollups_for_member_pr( pr.mergeable_state_is_stale, pr.created_at as "created_at: DateTime", try_build AS "try_build: BuildModel", - auto_build AS "auto_build: BuildModel" + auto_build AS "auto_build: BuildModel", + unrolled_build AS "unrolled_build: BuildModel" FROM pull_request as pr LEFT JOIN build AS try_build ON pr.try_build_id = try_build.id LEFT JOIN build AS auto_build ON pr.auto_build_id = auto_build.id + LEFT JOIN build AS unrolled_build ON pr.unrolled_build_id = unrolled_build.id WHERE pr.id IN ( SELECT rollup FROM rollup_member diff --git a/src/ec2/mod.rs b/src/ec2/mod.rs index e9ff488d..9f5de927 100644 --- a/src/ec2/mod.rs +++ b/src/ec2/mod.rs @@ -171,6 +171,7 @@ pub async fn start_ec2_github_runner( match data.build_kind { BuildKind::Try => "try", BuildKind::Auto => "auto", + BuildKind::UnrolledMember => "try-perf", } .to_string(), ), diff --git a/src/github/api/client.rs b/src/github/api/client.rs index c555652d..ea4eb474 100644 --- a/src/github/api/client.rs +++ b/src/github/api/client.rs @@ -25,6 +25,7 @@ use crate::github::{ }; use crate::utils::timing::{RetryMethod, RetryableOpError, ShouldRetry, perform_retryable}; use futures::TryStreamExt; +use http::StatusCode; use octocrab::models::actions::SelfHostedRunnerJitConfig; use octocrab::models::workflows::{Job, Run}; use serde::de::DeserializeOwned; @@ -377,6 +378,52 @@ impl GithubRepositoryClient { }) } + /// Get message of a commit with the given SHA. + pub async fn get_commit_message( + &self, + commit_sha: &CommitSha, + ) -> anyhow::Result> { + let message = perform_retryable("get_commit_message", RetryMethod::default(), || async { + #[derive(serde::Deserialize)] + struct CommitData { + message: String, + } + + #[derive(serde::Deserialize)] + struct CommitResponse { + commit: CommitData, + } + let response = self + .client + ._get(format!("/repos/{}/commits/{commit_sha}", self.repo_name)) + .await?; + match response.status() { + StatusCode::OK => { + let text = self + .client + .body_to_string(response) + .await + .unwrap_or_default(); + let commit = serde_json::from_str::(&text)?; + anyhow::Ok(Some(commit.commit.message)) + } + StatusCode::NOT_FOUND => Ok(None), + status => { + let text = self + .client + .body_to_string(response) + .await + .unwrap_or_default(); + Err(anyhow::anyhow!( + "Could not get commit `{commit_sha}` ({status}): {text}" + )) + } + } + }) + .await?; + Ok(message) + } + /// Create a new commit with the given author. pub async fn create_commit( &self, diff --git a/src/github/api/operations.rs b/src/github/api/operations.rs index db1b65cb..b313829b 100644 --- a/src/github/api/operations.rs +++ b/src/github/api/operations.rs @@ -484,6 +484,10 @@ pub async fn attempt_merge( tracing::warn!("Merge conflict"); Ok(MergeResult::Conflict) } + Err(MergeError::AlreadyMerged) => { + tracing::warn!("Branch was already merged"); + Ok(MergeResult::Conflict) + } Err(error) => Err(error.into()), } } diff --git a/src/github/mod.rs b/src/github/mod.rs index dae0f571..01f2db8a 100644 --- a/src/github/mod.rs +++ b/src/github/mod.rs @@ -24,6 +24,9 @@ pub use api::operations::{MergeResult, attempt_merge}; pub use error::AppError; pub use labels::{LabelModification, LabelTrigger}; +#[cfg(test)] +pub use rollup::tests::{make_rollup, rollup_state}; + use crate::bors::PullRequestStatus; /// Unique identifier of a GitHub repository diff --git a/src/github/rollup.rs b/src/github/rollup.rs index 5a9dab2c..e9478ce5 100644 --- a/src/github/rollup.rs +++ b/src/github/rollup.rs @@ -284,7 +284,7 @@ async fn create_rollup( anyhow::anyhow!("Could not create rollup branch {rollup_branch}: {error:?}") })?; - let mut successes = Vec::new(); + let mut successes: Vec = Vec::new(); let mut failures = Vec::new(); let mut github_prs = Vec::with_capacity(pr_nums.len()); @@ -339,8 +339,12 @@ async fn create_rollup( .await; match merge_attempt { - Ok(_) => { - successes.push((pr, head_sha)); + Ok(merge_commit) => { + successes.push(RegisterRollupMemberParams { + member: pr, + rolled_up_head_sha: head_sha, + rolled_up_merge_sha: merge_commit.sha, + }); } Err(error) => match error { MergeError::Conflict => { @@ -358,12 +362,12 @@ async fn create_rollup( } let mut body = "Successful merges:\n\n".to_string(); - for (pr, _) in &successes { + for successful_merge in &successes { body.push_str(&format!( " - {}#{} ({})\n", gh_client.repository(), - pr.number.0, - pr.title + successful_merge.member.number.0, + successful_merge.member.title )); } @@ -411,16 +415,8 @@ async fn create_rollup( db.set_rollup_mode(&rollup_db, RollupMode::Never, None) .await?; - let members = successes - .into_iter() - .map(|(member, rolled_up_sha)| RegisterRollupMemberParams { - member, - rolled_up_sha, - }) - .collect::>(); - // And register its rollup member PRs - db.register_rollup_members(&rollup_db, &members) + db.register_rollup_members(&rollup_db, &successes) .await .context("Cannot register rollup members")?; @@ -433,8 +429,10 @@ async fn create_rollup( } #[cfg(test)] -mod tests { +pub mod tests { use crate::bors::{PullRequestStatus, RollupMode}; + use crate::database::UnrollState; + use crate::database::operations::get_rollup_members; use crate::github::rollup::OAuthRollupState; use crate::github::{GithubRepoName, PullRequestNumber}; use crate::permissions::PermissionType; @@ -1204,7 +1202,44 @@ also include this pls" .await; } - async fn make_rollup( + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn rollup_merge_unrolled_state_waiting(pool: sqlx::PgPool) { + run_test( + (pool.clone(), rollup_state()), + async |ctx: &mut BorsTester| { + let pr2 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr2.id()).await?; + let pr3 = ctx.open_pr((), |_| {}).await?; + ctx.approve(pr3.id()).await?; + + make_rollup(ctx, &[&pr2, &pr3]) + .await? + .assert_status(StatusCode::SEE_OTHER); + ctx.approve(4).await?; + + ctx.start_and_finish_auto_build(4).await?; + ctx.pr(4).await.expect_status(PullRequestStatus::Merged); + + let pr_id = ctx.pr(4).await.get_db_pr().id; + + let members = get_rollup_members(&pool, pr_id).await.unwrap(); + assert_eq!(members.len(), 2); + for member in members { + assert_eq!( + member.unroll_state, + Some(UnrollState::Waiting), + "Rollup member {} has invalid unroll state", + member.member + ); + } + + Ok(()) + }, + ) + .await; + } + + pub async fn make_rollup( ctx: &mut BorsTester, prs: &[&PullRequest], ) -> anyhow::Result { @@ -1217,7 +1252,7 @@ also include this pls" .await } - fn rollup_state() -> GitHub { + pub fn rollup_state() -> GitHub { let mut gh = GitHub::default(); let rolluper = rollup_user(); gh.add_user(rolluper.clone()); @@ -1226,10 +1261,11 @@ also include this pls" .permissions .users .insert(rolluper.clone(), vec![PermissionType::Review]); + let gh = gh.append_to_default_config("[unroll]"); // Create fork let mut repo = Repo::new(rolluper, fork_repo().name()); - repo.fork = true; + repo.fork_of = Some(gh.default_repo()); gh.with_repo(repo) } diff --git a/src/lib.rs b/src/lib.rs index 98c25513..820c0af5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #![allow(async_fn_in_trait)] +#![allow(clippy::large_enum_variant)] +#![allow(clippy::result_large_err)] //! This is the library of the bors bot. mod bors; diff --git a/src/server/mod.rs b/src/server/mod.rs index 14a67ea2..5c0ddd35 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -569,6 +569,7 @@ pub async fn ec2_handler( let build_kind = |instance: &Ec2Instance| match instance.build_kind { BuildKind::Auto => 0, BuildKind::Try => 1, + BuildKind::UnrolledMember => 2, }; status(a) diff --git a/src/tests/github.rs b/src/tests/github.rs index 1097a028..1e10a3c0 100644 --- a/src/tests/github.rs +++ b/src/tests/github.rs @@ -7,6 +7,7 @@ use crate::permissions::PermissionType; use crate::tests::COMMENT_RECEIVE_TIMEOUT; use chrono::{DateTime, Utc}; use http::StatusCode; +use itertools::Itertools; use octocrab::models::pulls::MergeableState; use octocrab::models::workflows::Conclusion; use octocrab::models::{CheckSuiteId, JobId, RunId}; @@ -385,7 +386,7 @@ pub struct Repo { pub pull_request_error: bool, /// Push error failure/success behaviour. pub push_behaviour: BranchPushBehaviour, - pub fork: bool, + pub fork_of: Option>>, pub merge_behavior: MergeBehavior, } @@ -405,7 +406,7 @@ impl Repo { pull_request_error: false, check_runs: vec![], push_behaviour: BranchPushBehaviour::default(), - fork: false, + fork_of: None, merge_behavior: MergeBehavior::default(), }; repo.add_branch(Branch::default()); @@ -420,6 +421,10 @@ impl Repo { &self.owner } + pub fn is_fork(&self) -> bool { + self.fork_of.is_some() + } + pub fn branches(&self) -> &[Branch] { &self.branches } @@ -490,12 +495,20 @@ impl Repo { if let Some(old) = self.commits.insert(commit.commit_sha(), commit.clone()) { assert_eq!(old, commit); } + // Emulate commits being shared across forks on GitHub + if let Some(fork) = &self.fork_of { + fork.lock().create_commit(commit); + } } pub fn get_commit_by_sha(&self, sha: &str) -> Commit { self.commits .get(&CommitSha(sha.to_owned())) - .expect("Looking up non-existing commit SHA") + .unwrap_or_else(|| { + let commits: Vec<_> = self.commits.values().collect(); + let commits = commits.iter().map(|c| format!("{c:?}")).join("\n"); + panic!("Looking up non-existing commit SHA {sha}. Existing commits:\n{commits}") + }) .clone() } @@ -1162,6 +1175,9 @@ impl WorkflowRun { pub fn head_sha(&self) -> &str { &self.head_sha } + pub fn set_head_sha(&mut self, sha: &str) { + self.head_sha = sha.to_string(); + } pub fn duration(&self) -> Duration { self.duration diff --git a/src/tests/mock/permissions.rs b/src/tests/mock/permissions.rs index 27926d87..665af7d7 100644 --- a/src/tests/mock/permissions.rs +++ b/src/tests/mock/permissions.rs @@ -40,7 +40,7 @@ impl TeamApiMockServer { let repos: Vec<_> = github.lock().repos.values().cloned().collect(); for repo in repos { - if repo.lock().fork { + if repo.lock().is_fork() { continue; } diff --git a/src/tests/mock/pull_request.rs b/src/tests/mock/pull_request.rs index b8ceeccd..d861659a 100644 --- a/src/tests/mock/pull_request.rs +++ b/src/tests/mock/pull_request.rs @@ -88,7 +88,7 @@ async fn mock_pr_create( .get(&GithubRepoName::new(fork_owner, repo.full_name().name())) .expect("Fork not found") .clone(); - assert!(fork.lock().fork); + assert!(fork.lock().is_fork()); let commit = fork .lock() .get_branch_by_name(branch) diff --git a/src/tests/mock/repository.rs b/src/tests/mock/repository.rs index fc78b9ba..d31f72d8 100644 --- a/src/tests/mock/repository.rs +++ b/src/tests/mock/repository.rs @@ -77,7 +77,8 @@ async fn mock_branches_and_commits(repo: Arc>, mock_server: &MockSer mock_create_branch(repo.clone(), mock_server).await; mock_update_branch(repo.clone(), mock_server).await; mock_merge_branch(repo.clone(), mock_server).await; - mock_create_commit(repo.clone(), mock_server).await; + mock_get_commit(repo.clone(), mock_server).await; + mock_create_commit(repo, mock_server).await; } async fn mock_cancel_workflow(repo: Arc>, mock_server: &MockServer) { @@ -330,6 +331,29 @@ async fn mock_merge_branch(repo: Arc>, mock_server: &MockServer) { .await; } +async fn mock_get_commit(repo: Arc>, mock_server: &MockServer) { + let repo_name = repo.lock().full_name(); + dynamic_mock_req( + move |_: &Request, [sha]| { + let repo = repo.lock(); + + let commit = repo.get_commit_by_sha(sha); + + let response = serde_json::json!({ + "commit": { + "message": commit.message() + } + }); + + ResponseTemplate::new(200).set_body_json(response) + }, + "GET", + format!("^/repos/{repo_name}/commits/(.*)"), + ) + .mount(mock_server) + .await; +} + async fn mock_create_commit(repo: Arc>, mock_server: &MockServer) { Mock::given(method("POST")) .and(path(format!( diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 7e52dc82..bfa24fa0 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -5,7 +5,8 @@ use crate::bors::{ }; use crate::database::{ BuildModel, BuildStatus, DelegatedPermission, DelegationStatus, MergeableState, - OctocrabMergeableState, PullRequestModel, WorkflowStatus, + OctocrabMergeableState, PullRequestModel, RollupMemberForUnrolling, UnrollState, + WorkflowStatus, }; use crate::github::PullRequestNumber; use crate::{ @@ -40,6 +41,7 @@ use crate::bors::gitops_queue::{GitOpsQueueReceiver, handle_gitops_entry}; use crate::bors::merge_queue::merge_queue_tick; use crate::bors::mergeability_queue::{MergeabilityQueueReceiver, check_mergeability}; use crate::bors::process::QueueSenders; +use crate::bors::unroll_queue::{UnrollQueueEvent, UnrollQueueReceiver, handle_unroll_queue_event}; use crate::github::api::client::HideCommentReason; use crate::server::{ServerState, create_app}; use crate::tests::github::{ @@ -74,6 +76,8 @@ const TEST_TIMEOUT: Duration = Duration::from_secs(15); const TEST_CONDITION_TIMEOUT: Duration = Duration::from_secs(10); /// How long should we wait until a mergeability item is dequeued. const TEST_MERGEABILITY_TIMEOUT: Duration = Duration::from_secs(3); +/// How long should we wait until an unroll item is dequeued. +const TEST_UNROLL_TIMEOUT: Duration = Duration::from_secs(1); /// How often should we check whether a custom condition in a test is hit. const TEST_CONDITION_CHECK: Duration = Duration::from_millis(100); /// How long should we wait for a sync marker to be hit. @@ -83,6 +87,7 @@ const COMMENT_RECEIVE_TIMEOUT: Duration = TEST_CONDITION_TIMEOUT; const TRY_BRANCH: &str = "automation/bors/try"; const AUTO_BRANCH: &str = "automation/bors/auto"; +const UNROLLED_BRANCH: &str = "automation/bors/try-perf"; pub fn default_cmd_prefix() -> CommandPrefix { "@bors".to_string().into() @@ -194,6 +199,7 @@ pub struct BorsTester { senders: QueueSenders, mergeability_queue_rx: MergeabilityQueueReceiver, gitops_queue_rx: GitOpsQueueReceiver, + unroll_queue_rx: UnrollQueueReceiver, ctx: Arc, wait_for_markers: bool, } @@ -238,6 +244,7 @@ impl BorsTester { bors_process, mergeability_queue_rx, gitops_queue_rx, + unroll_queue_rx, } = create_bors_process( ctx.clone(), mock.github_client(), @@ -267,6 +274,7 @@ impl BorsTester { global_tx, mergeability_queue_rx, gitops_queue_rx, + unroll_queue_rx, ctx, wait_for_markers: true, }, @@ -306,6 +314,34 @@ impl BorsTester { self.create_workflow(default_repo_name(), AUTO_BRANCH) } + /// Creates N unrolled workflows, for the past N commits pushed to the unrolled branch. + /// This can be used to create workflows for unrolled builds, which are created in a batch, + /// and thus we cannot create a workflow for the latest SHA of the unrolled branch only. + /// + /// The workflows are returned from the oldest to the newest. + pub fn unrolled_workflows(&mut self, n: usize) -> Vec { + let repo_name = default_repo_name(); + let branch = self + .get_repo(&repo_name) + .lock() + .get_branch_by_name(UNROLLED_BRANCH) + .unwrap() + .clone(); + + assert!( + branch.get_commit_history().len() >= n, + "Branch doesn't have enough commits in its history.\n{branch:?}" + ); + let mut workflows = vec![]; + for commit in branch.get_commit_history().into_iter().rev().take(n).rev() { + let workflow = self.create_workflow(&repo_name, UNROLLED_BRANCH); + // Overwrite the SHA of the workflow + self.modify_workflow(workflow, |w| w.set_head_sha(commit.sha())); + workflows.push(workflow); + } + workflows + } + pub fn create_workflow>(&self, id: Id, branch: &str) -> RunId { let mut gh = self.github.lock(); gh.new_workflow(&id.into().0, branch) @@ -348,6 +384,26 @@ impl BorsTester { .await } + /// Get a rollup proxy that can be used to assert various things about it and its members. + pub async fn get_rollup>(&self, id: Id) -> anyhow::Result { + let rollup = self.pr(id).await; + let pr_db = rollup.require_db_pr(); + let members = self.db.get_rollup_members(pr_db).await?; + let mut members_enriched = vec![]; + for member in members { + let pr = self + .db + .get_pull_request(&pr_db.repository, member.member) + .await? + .expect("Rollup member PR not found in the DB"); + members_enriched.push(RollupMemberForUnrolling { member, pr }); + } + + Ok(RollupProxy { + members: members_enriched, + }) + } + pub fn try_branch(&self) -> Branch { self.repo() .lock() @@ -364,6 +420,14 @@ impl BorsTester { .clone() } + pub fn unrolled_branch(&self) -> Branch { + self.repo() + .lock() + .get_branch_by_name(UNROLLED_BRANCH) + .unwrap() + .clone() + } + /// Creates a branch and returns a **copy** of it. pub fn create_branch(&mut self, name: &str) -> Branch { let repo = self @@ -534,6 +598,31 @@ impl BorsTester { .unwrap(); } + /// Trigger and process a single unroll queue event. + pub async fn trigger_and_run_unroll_queue(&mut self) -> anyhow::Result<()> { + self.senders + .unroll_queue() + .process_unrolled_members(&default_repo_name()) + .await?; + self.run_unroll_queue().await + } + + /// Process a single unroll queue event. + pub async fn run_unroll_queue(&mut self) -> anyhow::Result<()> { + let ran = try_run_unroll_queue(self.ctx.clone(), &mut self.unroll_queue_rx).await?; + assert!(ran); + Ok(()) + } + + /// Directly run processing of unrolled members, without going through the queue. + pub async fn run_unroll_queue_concurrent(&self) -> anyhow::Result<()> { + handle_unroll_queue_event( + self.ctx.clone(), + UnrollQueueEvent::ProcessUnrolledMembers(default_repo_name()), + ) + .await + } + /// Enqueue PRs with stale/unknown mergeability into the mergeability queue. pub async fn refresh_mergeability_queue(&self) { // Wait until the refresh is fully handled @@ -593,9 +682,13 @@ impl BorsTester { pub async fn run_merge_queue_directly(&self) { wait_for_marker( async || { - merge_queue_tick(self.ctx.clone(), self.senders.mergeability_queue()) - .await - .unwrap(); + merge_queue_tick( + self.ctx.clone(), + self.senders.mergeability_queue(), + self.senders.unroll_queue(), + ) + .await + .unwrap(); Ok(()) }, self.wait_for_markers, @@ -1216,7 +1309,7 @@ impl BorsTester { res } - async fn finish(self, bors: JoinHandle<()>) -> anyhow::Result { + async fn finish(mut self, bors: JoinHandle<()>) -> anyhow::Result { // Tell the mergeability queue that it should shutdown once it has nothing else to do self.senders.mergeability_queue().shutdown(); @@ -1229,6 +1322,9 @@ impl BorsTester { // Drain the mergeability queue while try_run_mergeability_check(self.ctx.clone(), &self.mergeability_queue_rx).await? {} + // Drain unroll mergeability queue + while try_run_unroll_queue(self.ctx.clone(), &mut self.unroll_queue_rx).await? {} + // Wait until all events are handled in the bors service match tokio::time::timeout(Duration::from_secs(5), bors).await { Ok(Ok(_)) => {} @@ -1347,6 +1443,10 @@ impl PullRequestProxy { self.gh_pr.clone() } + pub fn get_db_pr(&self) -> &PullRequestModel { + self.require_db_pr() + } + /// Useful for debugging the GitHub and DB PR state. #[allow(unused)] pub fn dump(&self) -> &Self { @@ -1537,6 +1637,45 @@ impl PullRequestProxy { } } +/// A proxy object for checking assertions on rollup members. +pub struct RollupProxy { + members: Vec, +} + +impl RollupProxy { + /// Check that all members have this unroll state + #[track_caller] + pub fn expect_unroll_state_all(&self, state: UnrollState) -> &Self { + for member in &self.members { + assert_eq!( + member.member.unroll_state, + Some(state), + "Member {} has invalid unroll state", + member.member.member + ); + } + self + } + + /// Check if the given rollup memmber has the specified unroll state + #[track_caller] + pub fn expect_unroll_state(&self, member: u32, state: UnrollState) -> &Self { + let member = self + .members + .iter() + .find(|pr| pr.pr.number.0 == member as u64) + .expect("Cannot find rollup member"); + + assert_eq!( + member.member.unroll_state, + Some(state), + "Member {} has invalid unroll state", + member.member.member + ); + self + } +} + /// Start an async operation and wait until a specific [`TestSyncMarker`] /// is marked. async fn wait_for_marker( @@ -1582,3 +1721,22 @@ async fn try_run_mergeability_check( } } } + +/// Tries to pop a single unroll item from the unroll queue and handle it. +/// Returns true if there was some entry handled. +async fn try_run_unroll_queue( + ctx: Arc, + unroll_queue_rx: &mut UnrollQueueReceiver, +) -> anyhow::Result { + let fut = tokio::time::timeout(TEST_UNROLL_TIMEOUT, unroll_queue_rx.recv()); + match fut.await { + Ok(Some(event)) => { + handle_unroll_queue_event(ctx, event).await?; + Ok(true) + } + Ok(None) => Ok(false), + Err(_) => { + panic!("Timed out while waiting for an unroll item to be popped off the unroll queue"); + } + } +} diff --git a/tests/data/migrations/20260812140527_add_unrolled_build_to_pr.sql b/tests/data/migrations/20260812140527_add_unrolled_build_to_pr.sql new file mode 100644 index 00000000..bfd4801d --- /dev/null +++ b/tests/data/migrations/20260812140527_add_unrolled_build_to_pr.sql @@ -0,0 +1,3 @@ +UPDATE pull_request +SET unrolled_build_id = 1 +WHERE id = 3; diff --git a/tests/data/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.sql b/tests/data/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.sql new file mode 100644 index 00000000..aec387e2 --- /dev/null +++ b/tests/data/migrations/20260813085018_add_rolled_up_merge_sha_unroll_state_position_to_rollup_member.sql @@ -0,0 +1,2 @@ +-- Empty to satisfy migration tests +-- Data was backfilled during the migration itself. diff --git a/web/templates/ec2.html b/web/templates/ec2.html index c6c8e89d..8632e46d 100644 --- a/web/templates/ec2.html +++ b/web/templates/ec2.html @@ -60,6 +60,8 @@

EC2 instances of {{ repo_owner }}/{{ repo_name }}

{% match instance.build_kind %} {% when BuildKind::Try %} try + {% when BuildKind::UnrolledMember %} + unrolled-member {% when BuildKind::Auto %} auto {% endmatch %}