diff options
author | Francois Kritzinger <francois@codesynthesis.com> | 2024-10-29 16:54:26 +0200 |
---|---|---|
committer | Francois Kritzinger <francois@codesynthesis.com> | 2024-12-10 16:34:15 +0200 |
commit | e4447a19e8a58c16a9c31d13c5ed2c26b30a3550 (patch) | |
tree | 2b540df614c8a1ea7aa80c296df6129ffa7cf4ad | |
parent | 554d116a7b49eba5dd64ceb61b2f3b922f21c100 (diff) |
Implement build_unloaded_pre_check() and build_unloaded_load()
-rw-r--r-- | mod/mod-ci-github-gh.hxx | 2 | ||||
-rw-r--r-- | mod/mod-ci-github-gq.cxx | 260 | ||||
-rw-r--r-- | mod/mod-ci-github-gq.hxx | 48 | ||||
-rw-r--r-- | mod/mod-ci-github-service-data.cxx | 58 | ||||
-rw-r--r-- | mod/mod-ci-github-service-data.hxx | 19 | ||||
-rw-r--r-- | mod/mod-ci-github.cxx | 510 | ||||
-rw-r--r-- | mod/tenant-service.hxx | 9 |
7 files changed, 348 insertions, 558 deletions
diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx index 58714be..281d765 100644 --- a/mod/mod-ci-github-gh.hxx +++ b/mod/mod-ci-github-gh.hxx @@ -80,6 +80,8 @@ namespace brep // the merge commit. If false then `merge_commit_sha` is either empty or // no longer valid. // + // @@ TODO These appear to be unused. + // optional<bool> mergeable; string merge_commit_sha; diff --git a/mod/mod-ci-github-gq.cxx b/mod/mod-ci-github-gq.cxx index 1e9af74..17096eb 100644 --- a/mod/mod-ci-github-gq.cxx +++ b/mod/mod-ci-github-gq.cxx @@ -578,7 +578,10 @@ namespace brep os << "query {" << '\n' << " node(id:" << gq_str (nid) << ") {" << '\n' << " ... on PullRequest {" << '\n' - << " mergeable potentialMergeCommit { oid }" << '\n' + << " headRefOid" << '\n' + << " mergeStateStatus" << '\n' + << " mergeable" << '\n' + << " potentialMergeCommit { oid }" << '\n' << " }" << '\n' << " }" << '\n' << "}" << '\n'; @@ -586,10 +589,10 @@ namespace brep return os.str (); } - optional<string> - gq_pull_request_mergeable (const basic_mark& error, - const string& iat, - const string& nid) + optional<gq_pr_pre_check_info> + gq_fetch_pull_request_pre_check_info (const basic_mark& error, + const string& iat, + const string& nid) { string rq (gq_serialize_request (gq_query_pr_mergeability (nid))); @@ -610,7 +613,7 @@ namespace brep // The response value. Absent if the merge commit is still being // generated. // - optional<string> value; + optional<gq_pr_pre_check_info> r; resp (json::parser& p) { @@ -624,32 +627,42 @@ namespace brep { found = true; + string hs (p.next_expect_member_string ("headRefOid")); + string ms (p.next_expect_member_string ("mergeStateStatus")); string ma (p.next_expect_member_string ("mergeable")); - if (ma == "MERGEABLE") + if (ms == "BEHIND") + { + // The PR head branch is not up to date with the PR base + // branch. + // + // Note that we can only get here if the head-not-behind + // protection rule is active on the PR base branch. + // + r = {move (hs), true, nullopt}; + } + else if (ma == "MERGEABLE") { p.next_expect_member_object ("potentialMergeCommit"); string oid (p.next_expect_member_string ("oid")); p.next_expect (event::end_object); - value = move (oid); + r = {move (hs), false, move (oid)}; } else { if (ma == "CONFLICTING") - value = ""; + r = {move (hs), false, nullopt}; if (ma == "UNKNOWN") - ; // Still being generated; leave value absent. + ; // Still being generated; leave r absent. else - { - // @@ Let's throw invalid_json. - // - parse_error = "unexpected mergeable value '" + ma + '\''; - - // Carry on as if it were UNKNOWN. - } + throw_json (p, "unexpected mergeable value '" + ma + '\''); + } - // Skip the merge commit ID (which should be null). + if (!r || !r->merge_commit_sha) + { + // Skip the merge commit ID if it has not yet been extracted + // (in which case it should be null). // p.next_expect_name ("potentialMergeCommit"); p.next_expect_value_skip (); @@ -677,7 +690,7 @@ namespace brep else if (!rs.parse_error.empty ()) error << rs.parse_error; - return rs.value; + return rs.r; } else error << "failed to fetch pull request: error HTTP response status " @@ -711,215 +724,6 @@ namespace brep return nullopt; } - // Serialize a GraphQL query that fetches the last 100 (the maximum per - // page) open pull requests with the specified base branch from the - // repository with the specified node ID. - // - // @@ TMP Should we support more/less than 100? - // - // Doing more (or even 100) could waste a lot of CI resources on - // re-testing stale PRs. Maybe we should create a failed synthetic - // conclusion check run asking the user to re-run the CI manually if/when - // needed. - // - // Note that we cannot request more than 100 at a time (will need to - // do multiple requests with paging, etc). - // - // Also, maybe we should limit the result to "fresh" PRs, e.g., those - // that have been "touched" in the last week. - // - // Example query: - // - // query { - // node(id:"R_kgDOLc8CoA") - // { - // ... on Repository { - // pullRequests (last:100 states:OPEN baseRefName:"master") { - // edges { - // node { - // id - // number - // headRefOid - // } - // } - // } - // } - // } - // } - // - // - // pullRequests (last:50 states:[OPEN] - // orderBy:{field:UPDATED_AT direction:ASC} - // baseRefName:"master") { - // - // updatedAt field changes on PR close, conversion to draft PR, merge - // conflict resolution, etc. So seems like it changes with any kind of PR - // update. - // - static string - gq_query_fetch_open_pull_requests (const string& rid, const string& br) - { - ostringstream os; - - os << "query {" << '\n' - << " node(id:" << gq_str (rid) << ") {" << '\n' - << " ... on Repository {" << '\n' - << " pullRequests (last:100" << '\n' - << " states:" << gq_enum ("OPEN") << '\n' - << " baseRefName:" << gq_str (br) << '\n' - << " ) {" << '\n' - << " totalCount" << '\n' - << " edges { node { id number headRefOid } }" << '\n' - << " }" << '\n' - << " }" << '\n' - << " }" << '\n' - << "}" << '\n'; - - return os.str (); - } - - optional<vector<gh_pull_request>> - gq_fetch_open_pull_requests (const basic_mark& error, - const string& iat, - const string& nid, - const string& br) - { - string rq ( - gq_serialize_request (gq_query_fetch_open_pull_requests (nid, br))); - - try - { - // Response parser. - // - // Example response (only the part we need to parse here): - // - // { - // "node": { - // "pullRequests": { - // "totalCount": 2, - // "edges": [ - // { - // "node": { - // "id": "PR_kwDOLc8CoM5vRS0y", - // "number": 7, - // "headRefOid": "cf72888be9484d6946a1340264e7abf18d31cc92" - // } - // }, - // { - // "node": { - // "id": "PR_kwDOLc8CoM5vRzHs", - // "number": 8, - // "headRefOid": "626d25b318aad27bc0005277afefe3e8d6b2d434" - // } - // } - // ] - // } - // } - // } - // - struct resp - { - bool found = false; - - vector<gh_pull_request> pull_requests; - - resp (json::parser& p) - { - using event = json::event; - - auto parse_data = [this] (json::parser& p) - { - p.next_expect (event::begin_object); - - if (p.next_expect_member_object_null ("node")) - { - found = true; - - p.next_expect_member_object ("pullRequests"); - - uint16_t n (p.next_expect_member_number<uint16_t> ("totalCount")); - - p.next_expect_member_array ("edges"); - for (size_t i (0); i != n; ++i) - { - p.next_expect (event::begin_object); // edges[i] - - p.next_expect_member_object ("node"); - { - gh_pull_request pr; - pr.node_id = p.next_expect_member_string ("id"); - pr.number = p.next_expect_member_number<unsigned int> ("number"); - pr.head_sha = p.next_expect_member_string ("headRefOid"); - pull_requests.push_back (move (pr)); - } - p.next_expect (event::end_object); // node - - p.next_expect (event::end_object); // edges[i] - } - p.next_expect (event::end_array); // edges - - p.next_expect (event::end_object); // pullRequests - p.next_expect (event::end_object); // node - } - - p.next_expect (event::end_object); - }; - - gq_parse_response (p, move (parse_data)); - } - - resp () = default; - } rs; - - uint16_t sc (github_post (rs, - "graphql", // API Endpoint. - strings {"Authorization: Bearer " + iat}, - move (rq))); - - if (sc == 200) - { - if (!rs.found) - { - error << "repository '" << nid << "' not found"; - - return nullopt; - } - - return rs.pull_requests; - } - else - error << "failed to fetch repository pull requests: " - << "error HTTP response status " << sc; - } - catch (const json::invalid_json_input& e) - { - // Note: e.name is the GitHub API endpoint. - // - error << "malformed JSON in response from " << e.name << ", line: " - << e.line << ", column: " << e.column << ", byte offset: " - << e.position << ", error: " << e; - } - catch (const invalid_argument& e) - { - error << "malformed header(s) in response: " << e; - } - catch (const system_error& e) - { - error << "unable to fetch repository pull requests (errno=" << e.code () - << "): " << e.what (); - } - catch (const runtime_error& e) // From response type's parsing constructor. - { - // GitHub response contained error(s) (could be ours or theirs at this - // point). - // - error << "unable to fetch repository pull requests: " << e; - } - - return nullopt; - } - - // GraphQL serialization functions. // // The GraphQL spec: diff --git a/mod/mod-ci-github-gq.hxx b/mod/mod-ci-github-gq.hxx index ad9797a..72283ee 100644 --- a/mod/mod-ci-github-gq.hxx +++ b/mod/mod-ci-github-gq.hxx @@ -83,10 +83,15 @@ namespace brep build_state, optional<gq_built_result> = nullopt); - // Fetch a pull request's mergeability from GitHub. Return absent value if - // the merge commit is still being generated. Return empty string if the - // pull request is not auto-mergeable. Otherwise return the test merge - // commit id. + // Fetch pre-check information for a pull request from GitHub. This + // information is used to decide whether or not to CI the PR and is + // comprised of the PR's head commit SHA, whether its head branch is behind + // its base branch, and its test merge commit SHA. + // + // Return absent value if the merge commit is still being generated (which + // means PR head branch behindness is not yet known either). See the + // gq_pr_pre_check struct's member comments for non-absent return value + // semantics. // // Issue diagnostics and return absent if the request failed (which means it // will be treated by the caller as still being generated). @@ -95,22 +100,27 @@ namespace brep // merge commit. (For details see // https://docs.github.com/rest/guides/getting-started-with-the-git-database-api#checking-mergeability-of-pull-requests.) // - optional<string> - gq_pull_request_mergeable (const basic_mark& error, - const string& installation_access_token, - const string& node_id); + struct gq_pr_pre_check_info + { + // The PR head commit id. + // + string head_sha; - // Fetch the last 100 open pull requests with the specified base branch from - // the repository with the specified node ID. - // - // Issue diagnostics and return nullopt if the repository was not found or - // an error occurred. - // - optional<vector<gh_pull_request>> - gq_fetch_open_pull_requests (const basic_mark& error, - const string& installation_access_token, - const string& repository_node_id, - const string& base_branch); + // True if the PR's head branch is behind its base branch. + // + bool behind; + + // The commit id of the test merge commit. Absent if behind or the PR is + // not auto-mergeable. + // + optional<string> merge_commit_sha; + }; + + optional<gq_pr_pre_check_info> + gq_fetch_pull_request_pre_check_info ( + const basic_mark& error, + const string& installation_access_token, + const string& node_id); } #endif // MOD_MOD_CI_GITHUB_GQ_HXX diff --git a/mod/mod-ci-github-service-data.cxx b/mod/mod-ci-github-service-data.cxx index f93bc05..2a538a5 100644 --- a/mod/mod-ci-github-service-data.cxx +++ b/mod/mod-ci-github-service-data.cxx @@ -26,6 +26,22 @@ namespace brep to_string (version)); } + { + string v (p.next_expect_member_string ("kind")); + + if (v == "local") kind = local; + else if (v == "remote") kind = remote; + else + { + throw json::invalid_json_input ( + p.input_name, p.line (), p.column (), p.position (), + "invalid service data kind: '" + v + '\''); + } + } + + pre_check = p.next_expect_member_boolean<bool> ("pre_check"); + re_request = p.next_expect_member_boolean<bool> ("re_request"); + warning_success = p.next_expect_member_boolean<bool> ("warning_success"); // Installation access token. @@ -37,21 +53,17 @@ namespace brep p.next_expect_member_number<uint64_t> ("installation_id"); repository_node_id = p.next_expect_member_string ("repository_node_id"); + repository_clone_url = p.next_expect_member_string ("repository_clone_url"); { - string* s (p.next_expect_member_string_null ("repository_clone_url")); + string* s (p.next_expect_member_string_null ("pr_node_id")); if (s != nullptr) - repository_clone_url = *s; + pr_node_id = *s; } pr_number = p.next_expect_member_number_null<uint32_t> ("pr_number"); - { - string* s (p.next_expect_member_string_null ("merge_node_id")); - if (s != nullptr) - merge_node_id = *s; - } - + check_sha = p.next_expect_member_string ("check_sha"); report_sha = p.next_expect_member_string ("report_sha"); p.next_expect_member_array ("check_runs"); @@ -102,6 +114,7 @@ namespace brep timestamp iat_ea, uint64_t iid, string rid, + string rcu, kind_type k, bool rr, bool pc, @@ -112,6 +125,7 @@ namespace brep installation_access (move (iat_tok), iat_ea), installation_id (iid), repository_node_id (move (rid)), + repository_clone_url (move (rcu)), check_sha (move (cs)), report_sha (move (rs)) { @@ -125,12 +139,13 @@ namespace brep timestamp iat_ea, uint64_t iid, string rid, + string rcu, kind_type k, bool rr, bool pc, string cs, string rs, - string rcu, + string pid, uint32_t prn) : kind (k), pre_check (pc), re_request (rr), warning_success (ws), @@ -138,6 +153,7 @@ namespace brep installation_id (iid), repository_node_id (move (rid)), repository_clone_url (move (rcu)), + pr_node_id (move (pid)), pr_number (prn), check_sha (move (cs)), report_sha (move (rs)) @@ -154,6 +170,16 @@ namespace brep s.member ("version", 1); + s.member_name ("kind"); + switch (kind) + { + case local: s.value ("local"); break; + case remote: s.value ("remote"); break; + } + + s.member ("pre_check", pre_check); + s.member ("re_request", re_request); + s.member ("warning_success", warning_success); // Installation access token. @@ -165,10 +191,11 @@ namespace brep s.member ("installation_id", installation_id); s.member ("repository_node_id", repository_node_id); + s.member ("repository_clone_url", repository_clone_url); - s.member_name ("repository_clone_url"); - if (repository_clone_url) - s.value (*repository_clone_url); + s.member_name ("pr_node_id"); + if (pr_node_id) + s.value (*pr_node_id); else s.value (nullptr); @@ -178,12 +205,7 @@ namespace brep else s.value (nullptr); - s.member_name ("merge_node_id"); - if (merge_node_id) - s.value (*merge_node_id); - else - s.value (nullptr); - + s.member ("check_sha", check_sha); s.member ("report_sha", report_sha); s.member_begin_array ("check_runs"); diff --git a/mod/mod-ci-github-service-data.hxx b/mod/mod-ci-github-service-data.hxx index ae1506d..eb81ae4 100644 --- a/mod/mod-ci-github-service-data.hxx +++ b/mod/mod-ci-github-service-data.hxx @@ -74,8 +74,6 @@ namespace brep // Kind and phase. // - // @@ TODO Serialize these fields. - // enum kind_type {local, remote /*, queue */} kind; bool pre_check; bool re_request; // Re-requested (rebuild). @@ -92,20 +90,15 @@ namespace brep string repository_node_id; // GitHub-internal opaque repository id. + string repository_clone_url; + // The following two are only used for pull requests. // // @@ TODO/LATER: maybe put them in a struct? // - optional<string> repository_clone_url; + optional<string> pr_node_id; optional<uint32_t> pr_number; - // The GitHub ID of the synthetic PR merge check run or absent if it - // hasn't been created yet. - // - // @@ TODO Remove once merge check run code has been removed. - // - optional<string> merge_node_id; - // The commit ID the branch push or pull request (and its check runs) are // building. This will be the head commit for the branch push as well as // local pull requests and the test merge commit for remote pull requests. @@ -122,7 +115,7 @@ namespace brep vector<check_run> check_runs; // The GitHub ID of the synthetic conclusion check run or absent if it - // hasn't been created yet. See also merge_node_id above. + // hasn't been created yet. // optional<string> conclusion_node_id; @@ -149,6 +142,7 @@ namespace brep timestamp iat_expires_at, uint64_t installation_id, string repository_node_id, + string repository_clone_url, kind_type kind, bool pre_check, bool re_request, @@ -162,12 +156,13 @@ namespace brep timestamp iat_expires_at, uint64_t installation_id, string repository_node_id, + string repository_clone_url, kind_type kind, bool pre_check, bool re_request, string check_sha, string report_sha, - string repository_clone_url, + string pr_node_id, uint32_t pr_number); service_data () = default; diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx index f7480b6..60a6bae 100644 --- a/mod/mod-ci-github.cxx +++ b/mod/mod-ci-github.cxx @@ -389,10 +389,9 @@ namespace brep } } - // Let's capitalize the synthetic check run names to make them easier to - // distinguish from the regular ones. + // Let's capitalize the synthetic conclusion check run name to make it + // easier to distinguish from the regular ones. // - static string merge_check_run_name ("MERGE-COMMIT"); static string conclusion_check_run_name ("CONCLUSION"); // Return the colored circle corresponding to a result_status. @@ -469,7 +468,7 @@ namespace brep // protection rule for head behind is not enabled. In this case, it would // be good to complete the CI. So maybe/later. - // Service id that uniquely identifies the CI request. + // Service id that uniquely identifies the CI tenant. // string sid (cs.repository.node_id + ":" + cs.check_suite.head_sha); @@ -483,6 +482,12 @@ namespace brep // In this case we need to load the existing service data for this head // commit, extract the test merge commit, and restart the CI for that. // + // Note that it's possible the base branch has moved in the meantime and + // ideally we would want to re-request the test merge commit, etc. + // However, this will only be necessary if the user does not follow our + // recommendation of enabling the head-behind-base protection. And it + // seems all this extra complexity would not be warranted. + // string check_sha; service_data::kind_type kind; @@ -505,9 +510,9 @@ namespace brep } else { - error << "check suite for remote pull request " - << cs.check_suite.node_id - << ": re-requested but tenant_service with id " << sid + error << "check suite " << cs.check_suite.node_id + << " for remote pull request:" + << " re-requested but tenant_service with id " << sid << " did not exist"; return true; } @@ -525,12 +530,13 @@ namespace brep iat->expires_at, cs.installation.id, move (cs.repository.node_id), + move (cs.repository.clone_url), kind, false /* pre_check */, re_requested, move (check_sha), move (cs.check_suite.head_sha) /* report_sha */); - // If this check suite is being re-run, replace the existing CI request if - // it exists; otherwise create a new one, doing nothing if a request + // If this check suite is being re-run, replace the existing CI tenant if + // it exists; otherwise create a new one, doing nothing if a tenant // already exists (which could've been created by handle_pull_request()). // // Note that GitHub UI does not allow re-running the entire check suite @@ -540,7 +546,7 @@ namespace brep ? duplicate_tenant_mode::replace : duplicate_tenant_mode::ignore); - // Create an unloaded CI request. + // Create an unloaded CI tenant. // // Note: use no delay since we need to (re)create the synthetic conclusion // check run as soon as possible. @@ -549,7 +555,7 @@ namespace brep // management is not available in start(). // // After this call we will start getting the build_unloaded() - // notifications until (1) we load the request, (2) we cancel it, or (3) + // notifications until (1) we load the tenant, (2) we cancel it, or (3) // it gets archived after some timeout. // auto pr (create (error, @@ -746,9 +752,6 @@ namespace brep // job might actually still be relevant (in both local and remote PR // cases). - // @@ TODO Serialize the new service_data fields! - // - // Distinguish between local and remote PRs by comparing the head and base // repositories' paths. // @@ -771,13 +774,14 @@ namespace brep iat->expires_at, pr.installation.id, move (pr.repository.node_id), + move (pr.repository.clone_url), kind, true /* pre_check */, false /* re_request */, move (check_sha), move (pr.pull_request.head_sha) /* report_sha */, - move (pr.repository.clone_url), + pr.pull_request.node_id, pr.pull_request.number); - // Create an unloaded CI request for the pre-check phase (during which we + // Create an unloaded CI tenant for the pre-check phase (during which we // wait for the PR's merge commit and behindness to become available). // // Create with an empty service id so that the generated tenant id is used @@ -792,13 +796,13 @@ namespace brep // // After this call we will start getting the build_unloaded() // notifications -- which will be routed to build_unloaded_pre_check() -- - // until we cancel the request or it gets archived after some - // timeout. (Note that we never actually load this request, we always - // cancel it; see build_unloaded_pre_check() for details.) + // until we cancel the tenant or it gets archived after some timeout. + // (Note that we never actually load this request, we always cancel it; + // see build_unloaded_pre_check() for details.) // if (!create (error, warn, - &trace, + verb_ ? &trace : nullptr, *build_db_, move (ts), chrono::seconds (30) /* interval */, @@ -834,28 +838,140 @@ namespace brep } function<optional<string> (const tenant_service&)> ci_github:: - build_unloaded_pre_check (tenant_service&&, - service_data&&, + build_unloaded_pre_check (tenant_service&& ts, + service_data&& sd, const diag_epilogue& log_writer) const noexcept { NOTIFICATION_DIAG (log_writer); - // Note: PR only (but both local and remove). + // We get here for PRs only (but both local and remote). The overall + // plan is as follows: + // + // 1. Ask for the mergeability/behind status/test merge commit. + // + // 2. If not ready, get called again. + // + // 3. If not mergeable, behind, or different head (head changed while + // waiting for merge commit and thus differs from what's in the + // service_data), cancel the pre-check tenant and do nothing. + // + // 4. Otherwise, create an unloaded CI tenant and cancel ourselves. Note + // that all re-requested cases are handled elsewhere. // - // - Ask for test merge commit. - // - If not ready, get called again. - // - If not mergeable, behind, or different head (head changed while - // waiting for merge commit and thus differs from what's in the - // service_data), cancel itself and ignore. - // - Otherwise, create unloaded CI tenant (with proper duplicate mode - // based on re_request) and cancel itself. - // Note that in case of a mixed local/remote case, whether we CI the head // commit or test merge commit will be racy and there is nothing we can do // about (the purely local case can get "upgraded" to mixed after we have // started the CI job). // + // Request PR pre-check info (triggering the generation of the test merge + // commit on the GitHub's side). + // + optional<gq_pr_pre_check_info> pc ( + gq_fetch_pull_request_pre_check_info (error, + sd.installation_access.token, + *sd.pr_node_id)); + + if (!pc) + { + // Test merge commit not available yet: get called again to retry. + // + return nullptr; + } + + // Create the CI tenant if nothing is wrong, otherwise issue diagnostics. + // + if (pc->behind) + { + l3 ([&]{trace << "ignoring pull request " << *sd.pr_node_id + << ": head is behind base";}); + } + else if (!pc->merge_commit_sha) + { + l3 ([&]{trace << "ignoring pull request " << *sd.pr_node_id + << ": not auto-mergeable";}); + } + else if (pc->head_sha != sd.report_sha) + { + l3 ([&]{trace << "ignoring pull request " << *sd.pr_node_id + << ": head commit has changed";}); + } + else + { + // Create the CI tenant. + + // Update service data's check_sha if this is a remote PR. + // + if (sd.kind == service_data::remote) + sd.check_sha = *pc->merge_commit_sha; + + // Service id that will uniquely identify the CI tenant. + // + string sid (sd.repository_node_id + ":" + sd.report_sha); + + // Create an unloaded CI tenant, doing nothing if one already exists + // (which could've been created by a head branch push or another PR + // sharing the same head commit). + // + // Note: use no delay since we need to (re)create the synthetic + // conclusion check run as soon as possible. + // + // Note that we use the create() API instead of start() since duplicate + // management is not available in start(). + // + // After this call we will start getting the build_unloaded() + // notifications until (1) we load the tenant, (2) we cancel it, or (3) + // it gets archived after some timeout. + // + if (auto pr = create (error, warn, verb_ ? &trace : nullptr, + *build_db_, + tenant_service (sid, "ci-github", sd.json ()), + chrono::seconds (30) /* interval */, + chrono::seconds (0) /* delay */, + duplicate_tenant_mode::ignore)) + { + if (pr->second == duplicate_tenant_result::ignored) + { + // This PR is sharing a head commit with something else. + // + // If this is a local PR then it's probably the branch push, which + // is expected, so do nothing. + // + // If this is a remote PR then it could be anything (branch push, + // local PR, or another remote PR) which in turn means the CI result + // may end up being for head, not merge commit. There is nothing we + // can do about it on our side (the user can enable the head-behind- + // base protection on their side). + // + if (sd.kind == service_data::remote) + { + l3 ([&]{trace << "remote pull request " << *sd.pr_node_id + << ": CI tenant already exists for " << sid;}); + } + } + } + else + { + error << "pull request " << *sd.pr_node_id + << ": unable to create unloaded CI tenant " + << "with tenant_service id " << sid; + + // Fall through to cancel. + } + } + + // Cancel the pre-check tenant. + // + if (!cancel (error, warn, verb_ ? &trace : nullptr, + *build_db_, ts.type, ts.id)) + { + // Should never happen (no such tenant). + // + error << "pull request " << *sd.pr_node_id + << ": failed to cancel pre-check tenant with tenant_service id " + << ts.id; + } + return nullptr; } @@ -866,8 +982,15 @@ namespace brep { NOTIFICATION_DIAG (log_writer); - // @@@ TODO: load the tenant: should be the same for both branch push and - // PR. + // Load the tenant, which is essentially the same for both branch push and + // PR. The overall plan is as follows: + // + // - Create synthetic conclusion check run with the in-progress state. If + // unable to, get called again to re-try. + // + // - Load the tenant. If unable to, fail the conclusion check run. + // + // - Update service data. // // Get a new installation access token if the current one has expired. @@ -892,40 +1015,6 @@ namespace brep if (iat == nullptr) return nullptr; // Try again on the next call. - auto make_iat_updater = [&new_iat, &error] () - { - function<optional<string> (const tenant_service&)> r; - - if (new_iat) - { - r = [&error, - iat = move (new_iat)] (const tenant_service& ts) - -> optional<string> - { - // NOTE: this lambda may be called repeatedly (e.g., due to - // transaction being aborted) and so should not move out of its - // captures. - - service_data sd; - try - { - sd = service_data (*ts.data); - } - catch (const invalid_argument& e) - { - error << "failed to parse service data: " << e; - return nullopt; - } - - sd.installation_access = *iat; - - return sd.json (); - }; - } - - return r; - }; - // Create a synthetic check run with an in-progress state. Return the // check run on success or nullopt on failure. // @@ -980,221 +1069,82 @@ namespace brep move (br))) { assert (cr.state == build_state::built); - return cr; } else return nullopt; }; - // Synthetic merge check run node ID. Empty until created on the first - // call or retrieved from service data on subsequent calls. - // - string merge_node_id; + // Note that there is a window between receipt of a check_suite or + // pull_request event and the first bot/worker asking for a task, which + // could be substantial. We could probably (also) try to (re)create the + // conclusion checkrun in the webhook handler. @@ Maybe/later. - // True if this is the first call (or the merge commit couldn't be created - // on the first call, in which case we just re-try by treating it as a - // first call). + // (Re)create the synthetic conclusion check run first in order to convert + // a potentially completed check suite to building as early as possible. // - bool first (!sd.merge_node_id); + string conclusion_node_id; // Conclusion check run node ID. - // If this is the first call, (re)create the synthetic merge check run as - // soon as possible to make sure the previous check suite, if any, is no - // longer completed. - // - // Note that there is still a window between receipt of the pull_request - // event and the first bot/worker asking for a task, which could be - // substantial. We could probably (also) try to (re)create the merge - // checkrun in the webhook. @@ Maybe/later. - // - if (first) + if (auto cr = create_synthetic_cr (conclusion_check_run_name)) { - if (auto cr = create_synthetic_cr (merge_check_run_name)) - { - l3 ([&]{trace << "created check_run { " << *cr << " }";}); + l3 ([&]{trace << "created check_run { " << *cr << " }";}); - merge_node_id = move (*cr->node_id); - } - else - return make_iat_updater (); // Try again on the next call. + conclusion_node_id = move (*cr->node_id); } - else - merge_node_id = *sd.merge_node_id; - // Start/check PR mergeability. + // Load the CI tenant if the conclusion check run was created. // - optional<string> mc ( - gq_pull_request_mergeable (error, iat->token, ts.id)); // Merge commit. - - if (!mc || mc->empty ()) + if (!conclusion_node_id.empty ()) { - if (!mc) // No merge commit yet. - { - // If this is a subsequent notification and there is no merge commit, - // then there is nothing to do. - // - if (!first) - return make_iat_updater (); + string ru; // Repository URL. - // Fall through to update service data. - } - else // Not mergeable. + // CI the merge test commit for remote PRs and the head commit for + // everything else (branch push or local PRs). + // + if (sd.kind == service_data::remote) { - // If the commit is not mergeable, cancel the CI request and fail the - // merge check run. + // E.g. #pull/28/merge@1b6c9a361086ed93e6f1e67189e82d52de91c49b // - // Note that it feels like in this case we don't really need to create a - // failed synthetic conclusion check run since the PR cannot be merged - // anyway. - - if (auto cr = update_synthetic_cr ( - merge_node_id, - merge_check_run_name, - result_status::error, - "GitHub is unable to create test merge commit")) - { - l3 ([&]{trace << "updated check_run { " << *cr << " }";}); - - // Cancel the CI request. - // - // Ignore failure because this CI request may have been cancelled - // elsewhere due to an update to the PR base or head branches. - // - if (!cancel (error, warn, &trace, *build_db_, ts.type, ts.id)) - l3 ([&]{trace << "CI for PR " << ts.id << " already cancelled";}); - - return nullptr; // No need to update service data in this case. - } - else - { - // Don't cancel the CI request if the merge check run update failed - // so that we can try again on the next call. + ru = sd.repository_clone_url + "#pull/" + to_string (*sd.pr_number) + + "/merge@" + sd.check_sha; + } + else + ru = sd.repository_clone_url + '#' + sd.check_sha; - if (!first) - return make_iat_updater (); + repository_location rl (move (ru), repository_type::git); - // Fall through to update service data. - } - } + optional<start_result> r (load (error, warn, verb_ ? &trace : nullptr, + *build_db_, + move (ts), + move (rl))); - // This is a first notification, so record the merge check run in the - // service data. - // - return [&error, - iat = move (new_iat), - mni = move (merge_node_id)] (const tenant_service& ts) - -> optional<string> + if (!r || r->status != 200) { - // NOTE: this lambda may be called repeatedly (e.g., due to - // transaction being aborted) and so should not move out of its - // captures. - - service_data sd; - try - { - sd = service_data (*ts.data); - } - catch (const invalid_argument& e) + if (auto cr = update_synthetic_cr (conclusion_node_id, + conclusion_check_run_name, + result_status::error, + to_check_run_summary (r))) { - error << "failed to parse service data: " << e; - return nullopt; + l3 ([&]{trace << "updated check_run { " << *cr << " }";}); } - - if (iat) - sd.installation_access = *iat; - - sd.merge_node_id = mni; - - return sd.json (); - }; - } - - // If we are here, then it means we have a merge commit that we can load. - // - // Note that this can still be the first call (first=true). - // - - // As a first step, (re)create the synthetic conclusion check run and then - // change the merge check run state to success. Do it in this order so - // that the check suite does not become completed. - - // Synthetic conclusion check run node ID. Empty until created on the - // "second" call or retrieved from service data on subsequent calls. - // - string conclusion_node_id; - - // True if this is the first call after the merge commit became available, - // which we will call the "second" call (or we couldn't create the - // conclusion check run on the first such call, in which case we just - // re-try by treating it as a "second" call). - // - bool second (!sd.conclusion_node_id); - - if (second) - { - if (auto cr = create_synthetic_cr (conclusion_check_run_name)) - { - l3 ([&]{trace << "created check_run { " << *cr << " }";}); - - conclusion_node_id = move (*cr->node_id); - } - } - else - conclusion_node_id = *sd.conclusion_node_id; - - if (!conclusion_node_id.empty ()) // Conclusion check run was created. - { - // Update merge check run to successful. - // - if (auto cr = update_synthetic_cr (merge_node_id, - merge_check_run_name, - result_status::success, - "GitHub created test merge commit")) - { - l3 ([&]{trace << "updated check_run { " << *cr << " }";}); - - // Load the CI request. - // - // Example repository URL fragment: - // - // #pull/28/merge@1b6c9a361086ed93e6f1e67189e82d52de91c49b - // - repository_location rl (*sd.repository_clone_url + "#pull/" + - to_string (*sd.pr_number) + "/merge@" + *mc, - repository_type::git); - - optional<start_result> r ( - load (error, warn, &trace, *build_db_, move (ts), rl)); - - if (!r || r->status != 200) + else { - if (auto cr = update_synthetic_cr (conclusion_node_id, - conclusion_check_run_name, - result_status::error, - to_check_run_summary (r))) - { - l3 ([&]{trace << "updated check_run { " << *cr << " }";}); - } - else - { - // Nothing really we can do in this case since we will not receive - // any further notifications. - } + // Nothing really we can do in this case since we will not receive + // any further notifications. Log the error as a last resort. - return nullptr; // No need to update service data in this case. + error << "failed to load CI tenant " << ts.id + << " and unable to update conclusion"; } - } - else - { - // Don't load the CI request if the merge check run update failed so - // that we can try again on the next call. + + return nullptr; // No need to update service data in this case. } } + else if (!new_iat) + return nullptr; // Nothing to save (but retry on next call). return [&error, iat = move (new_iat), - mni = (first ? move (merge_node_id) : string ()), - cni = (second ? move (conclusion_node_id) : string ())] + cni = move (conclusion_node_id)] (const tenant_service& ts) -> optional<string> { // NOTE: this lambda may be called repeatedly (e.g., due to @@ -1215,9 +1165,6 @@ namespace brep if (iat) sd.installation_access = *iat; - if (!mni.empty ()) - sd.merge_node_id = mni; - if (!cni.empty ()) sd.conclusion_node_id = cni; @@ -1234,7 +1181,9 @@ namespace brep // them when notifying GitHub. The first is not important (we expect the // state to go back to building shortly). The second should normally not // happen and would mean that a completed check suite may go back on its - // conclusion (which would be pretty confusing for the user). + // conclusion (which would be pretty confusing for the user). @@@ This + // can/will happen on check run rebuild. Distinguish between internal + // and external rebuilds? // // So, for GitHub notifications, we only have the following linear // transition sequence: @@ -1340,7 +1289,7 @@ namespace brep // for (const build& b: builds) { - string bid (gh_check_run_name (b)); // Full build ID. + string bid (gh_check_run_name (b)); // Full build id. if (const check_run* scr = sd.find_check_run (bid)) { @@ -1362,6 +1311,8 @@ namespace brep else { // Ignore interrupted. + // + assert (*istate == build_state::building); } } else @@ -1406,7 +1357,7 @@ namespace brep // if (iat != nullptr) { - // Create a check_run for each build. + // Create a check_run for each build as a single request. // if (gq_create_check_runs (error, crs, @@ -1416,6 +1367,8 @@ namespace brep { for (const check_run& cr: crs) { + // We can only create a check run in the queued state. + // assert (cr.state == build_state::queued); l3 ([&]{trace << "created check_run { " << cr << " }";}); } @@ -1490,13 +1443,12 @@ namespace brep } optional<check_run> cr; // Updated check run. - string bid (gh_check_run_name (b)); // Full Build ID. + string bid (gh_check_run_name (b)); // Full build id. if (check_run* scr = sd.find_check_run (bid)) // Stored check run. { // Update the check run if it exists on GitHub and the queued - // notification succeeded and updated the service data, otherwise do - // nothing. + // notification updated the service data, otherwise do nothing. // if (scr->state == build_state::queued) { @@ -1561,12 +1513,10 @@ namespace brep if (cr->state == build_state::built) { warn << "check run " << bid << ": already in built state on GitHub"; - return nullptr; } assert (cr->state == build_state::building); - l3 ([&]{trace << "updated check_run { " << *cr << " }";}); } } @@ -1619,6 +1569,10 @@ namespace brep const build& b, const diag_epilogue& log_writer) const noexcept { + // @@ TODO Include service_data::event_node_id and perhaps ts.id in + // diagnostics? E.g. when failing to update check runs we print the + // build ID only. + // NOTIFICATION_DIAG (log_writer); service_data sd; @@ -1632,13 +1586,16 @@ namespace brep return nullptr; } - // Absent if have any unbuilt check runs. + // Here we need to update the state of this check run and, if there are no + // more unbuilt ones, update the synthetic conclusion check run. + // + // Absent means we still have unbuilt check runs. // optional<result_status> conclusion (*b.status); check_run cr; // Updated check run. { - string bid (gh_check_run_name (b)); // Full Build ID. + string bid (gh_check_run_name (b)); // Full build id. optional<check_run> scr; for (check_run& cr: sd.check_runs) @@ -1652,11 +1609,10 @@ namespace brep { if (cr.state == build_state::built) { + assert (cr.status); + if (conclusion) - { - assert (cr.status); *conclusion |= *cr.status; - } } else conclusion = nullopt; @@ -1686,6 +1642,9 @@ namespace brep warn << "check run " << bid << ": out of order built notification; " << "no check run state in service data"; + // Note that we have no hints here and so have to use the full build + // id for name. + // cr.build_id = move (bid); cr.name = cr.build_id; } @@ -1815,10 +1774,10 @@ namespace brep sm = os.str (); } - gq_built_result br (gh_to_conclusion (*b.status, sd.warning_success), - circle (*b.status) + ' ' + - ucase (to_string (*b.status)), - move (sm)); + gq_built_result br ( + gh_to_conclusion (*b.status, sd.warning_success), + circle (*b.status) + ' ' + ucase (to_string (*b.status)), + move (sm)); if (cr.node_id) { @@ -1834,7 +1793,6 @@ namespace brep move (br))) { assert (cr.state == build_state::built); - l3 ([&]{trace << "updated check_run { " << cr << " }";}); } } @@ -1843,7 +1801,7 @@ namespace brep // Create new check run. // // Note that we don't have build hints so will be creating this check - // run with the full build ID as name. In the unlikely event that an + // run with the full build id as name. In the unlikely event that an // out of order build_queued() were to run before we've saved this // check run to the service data it will create another check run with // the shortened name which will never get to the built state. @@ -1858,7 +1816,6 @@ namespace brep move (br))) { assert (cr.state == build_state::built); - l3 ([&]{trace << "created check_run { " << cr << " }";}); } } @@ -1902,8 +1859,6 @@ namespace brep { assert (sd.conclusion_node_id); - // Update the conclusion check run with success. - // result_status rs (*conclusion); optional<gq_built_result> br ( @@ -1928,14 +1883,13 @@ namespace brep move (br))) { assert (cr.state == build_state::built); - - l3 ([&]{trace << "updated check_run { " << cr << " }";}); + l3 ([&]{trace << "updated conclusion check_run { " << cr << " }";}); } else { // Nothing we can do here except log the error. // - error << "check suite " << ts.id + error << "tenant_service id " << ts.id << ": unable to update conclusion check run " << *sd.conclusion_node_id; } diff --git a/mod/tenant-service.hxx b/mod/tenant-service.hxx index 6a982ea..8ba199a 100644 --- a/mod/tenant-service.hxx +++ b/mod/tenant-service.hxx @@ -50,7 +50,9 @@ namespace brep // sense for the implementation to protect against overwriting later states // with earlier. For example, if it's possible to place a condition on a // notification, it makes sense to only set the state to queued if none of - // the later states (e.g., building) are already in effect. + // the later states (e.g., building) are already in effect. See also + // ci_start::rebuild() for additional details on the build->queued + // transition. // // Note also that it's possible for the build to get deleted at any stage // without any further notifications. This can happen, for example, due to @@ -131,8 +133,9 @@ namespace brep // returned callback should be NULL). // // Note: make sure the implementation of this notification does not take - // too long (currently 40 seconds) to avoid nested notifications. Note - // also that the first notification is delayed (currently 10 seconds). + // longer than the notification_interval argument of ci_start::create() to + // avoid nested notifications. The first notification can be delayed with + // the notify_delay argument. // class tenant_service_build_unloaded: public virtual tenant_service_base { |