aboutsummaryrefslogtreecommitdiff
path: root/mod
diff options
context:
space:
mode:
authorBoris Kolpackov <boris@codesynthesis.com>2024-12-04 12:09:19 +0200
committerBoris Kolpackov <boris@codesynthesis.com>2024-12-10 16:44:55 +0200
commit87588e0916f7e5d4c3709c14157615a08656cbdd (patch)
tree7e920eb60b029e8558f24c231e0e5e254fa19492 /mod
parente28291b10fa2fbe12d33eba5acfc7de62b0f1dcc (diff)
Support multiple GitHub app instances
Diffstat (limited to 'mod')
-rw-r--r--mod/mod-ci-github-gh.cxx131
-rw-r--r--mod/mod-ci-github-gh.hxx117
-rw-r--r--mod/mod-ci-github-service-data.cxx17
-rw-r--r--mod/mod-ci-github-service-data.hxx9
-rw-r--r--mod/mod-ci-github.cxx113
-rw-r--r--mod/mod-ci-github.hxx6
-rw-r--r--mod/module.cli15
7 files changed, 306 insertions, 102 deletions
diff --git a/mod/mod-ci-github-gh.cxx b/mod/mod-ci-github-gh.cxx
index a25e52c..021ff6b 100644
--- a/mod/mod-ci-github-gh.cxx
+++ b/mod/mod-ci-github-gh.cxx
@@ -123,7 +123,52 @@ namespace brep
{
p.next_expect (event::begin_object);
- bool ni (false), hb (false), hs (false), cc (false), co (false);
+ bool ni (false), hb (false), hs (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ auto c = [&p] (bool& v, const char* s)
+ {
+ return p.name () == s ? (v = true) : false;
+ };
+
+ if (c (ni, "node_id")) node_id = p.next_expect_string ();
+ else if (c (hb, "head_branch"))
+ {
+ string* v (p.next_expect_string_null ());
+ if (v != nullptr)
+ head_branch = *v;
+ }
+ else if (c (hs, "head_sha")) head_sha = p.next_expect_string ();
+ else p.next_expect_value_skip ();
+ }
+
+ if (!ni) missing_member (p, "gh_check_suite", "node_id");
+ if (!hb) missing_member (p, "gh_check_suite", "head_branch");
+ if (!hs) missing_member (p, "gh_check_suite", "head_sha");
+ }
+
+ ostream&
+ operator<< (ostream& os, const gh_check_suite& cs)
+ {
+ os << "node_id: " << cs.node_id
+ << ", head_branch: " << (cs.head_branch ? *cs.head_branch : "null")
+ << ", head_sha: " << cs.head_sha;
+
+ return os;
+ }
+
+ // gh_check_suite_ex
+ //
+ gh_check_suite_ex::
+ gh_check_suite_ex (json::parser& p)
+ {
+ p.next_expect (event::begin_object);
+
+ bool ni (false), hb (false), hs (false), cc (false), co (false),
+ ap (false);
// Skip unknown/uninteresting members.
//
@@ -150,24 +195,54 @@ namespace brep
if (v != nullptr)
conclusion = *v;
}
+ else if (c (ap, "app"))
+ {
+ p.next_expect (event::begin_object);
+
+ bool ai (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ if (c (ai, "id"))
+ {
+ // Note: unlike the check_run webhook's app.id, the check_suite
+ // one can be null. It's unclear under what circumstances, but it
+ // shouldn't happen unless something is broken.
+ //
+ string* v (p.next_expect_number_null ());
+
+ if (v == nullptr)
+ throw_json (p, "check_suite.app.id is null");
+
+ app_id = *v;
+ }
+ else p.next_expect_value_skip ();
+ }
+
+ if (!ai) missing_member (p, "gh_check_suite_ex.app", "id");
+ }
else p.next_expect_value_skip ();
}
- if (!ni) missing_member (p, "gh_check_suite", "node_id");
- if (!hb) missing_member (p, "gh_check_suite", "head_branch");
- if (!hs) missing_member (p, "gh_check_suite", "head_sha");
- if (!cc) missing_member (p, "gh_check_suite", "latest_check_runs_count");
- if (!co) missing_member (p, "gh_check_suite", "conclusion");
+ if (!ni) missing_member (p, "gh_check_suite_ex", "node_id");
+ if (!hb) missing_member (p, "gh_check_suite_ex", "head_branch");
+ if (!hs) missing_member (p, "gh_check_suite_ex", "head_sha");
+ if (!cc) missing_member (p, "gh_check_suite_ex", "latest_check_runs_count");
+ if (!co) missing_member (p, "gh_check_suite_ex", "conclusion");
+ if (!ap) missing_member (p, "gh_check_suite_ex", "app");
}
ostream&
- operator<< (ostream& os, const gh_check_suite& cs)
+ operator<< (ostream& os, const gh_check_suite_ex& cs)
{
os << "node_id: " << cs.node_id
<< ", head_branch: " << (cs.head_branch ? *cs.head_branch : "null")
<< ", head_sha: " << cs.head_sha
<< ", latest_check_runs_count: " << cs.check_runs_count
- << ", conclusion: " << (cs.conclusion ? *cs.conclusion : "null");
+ << ", conclusion: " << (cs.conclusion ? *cs.conclusion : "null")
+ << ", app_id: " << cs.app_id;
return os;
}
@@ -208,7 +283,8 @@ namespace brep
{
p.next_expect (event::begin_object);
- bool ni (false), nm (false), st (false), du (false), cs (false);
+ bool ni (false), nm (false), st (false), du (false), cs (false),
+ ap (false);
// Skip unknown/uninteresting members.
//
@@ -224,14 +300,31 @@ namespace brep
else if (c (st, "status")) status = p.next_expect_string ();
else if (c (du, "details_url")) details_url = p.next_expect_string ();
else if (c (cs, "check_suite")) check_suite = gh_check_suite (p);
+ else if (c (ap, "app"))
+ {
+ p.next_expect (event::begin_object);
+
+ bool ai (false);
+
+ // Skip unknown/uninteresting members.
+ //
+ while (p.next_expect (event::name, event::end_object))
+ {
+ if (c (ai, "id")) app_id = p.next_expect_number ();
+ else p.next_expect_value_skip ();
+ }
+
+ if (!ai) missing_member (p, "gh_check_run_ex.app", "id");
+ }
else p.next_expect_value_skip ();
}
- if (!ni) missing_member (p, "gh_check_run", "node_id");
- if (!nm) missing_member (p, "gh_check_run", "name");
- if (!st) missing_member (p, "gh_check_run", "status");
- if (!du) missing_member (p, "gh_check_run", "details_url");
- if (!cs) missing_member (p, "gh_check_run", "check_suite");
+ if (!ni) missing_member (p, "gh_check_run_ex", "node_id");
+ if (!nm) missing_member (p, "gh_check_run_ex", "name");
+ if (!st) missing_member (p, "gh_check_run_ex", "status");
+ if (!du) missing_member (p, "gh_check_run_ex", "details_url");
+ if (!cs) missing_member (p, "gh_check_run_ex", "check_suite");
+ if (!ap) missing_member (p, "gh_check_run_ex", "app");
}
@@ -250,7 +343,8 @@ namespace brep
{
os << static_cast<const gh_check_run&> (cr)
<< ", details_url: " << cr.details_url
- << ", check_suite: { " << cr.check_suite << " }";
+ << ", check_suite: { " << cr.check_suite << " }"
+ << ", app_id: " << cr.app_id;
return os;
}
@@ -356,7 +450,8 @@ namespace brep
<< "path: " << pr.head_path
<< ", ref: " << pr.head_ref
<< ", sha: " << pr.head_sha
- << " }";
+ << " }"
+ << ", app_id: " << pr.app_id;
return os;
}
@@ -418,7 +513,7 @@ namespace brep
return p.name () == s ? (v = true) : false;
};
- if (c (i, "id")) id = p.next_expect_number<uint64_t> ();
+ if (c (i, "id")) id = p.next_expect_number ();
else p.next_expect_value_skip ();
}
@@ -452,7 +547,7 @@ namespace brep
};
if (c (ac, "action")) action = p.next_expect_string ();
- else if (c (cs, "check_suite")) check_suite = gh_check_suite (p);
+ else if (c (cs, "check_suite")) check_suite = gh_check_suite_ex (p);
else if (c (rp, "repository")) repository = gh_repository (p);
else if (c (in, "installation")) installation = gh_installation (p);
else p.next_expect_value_skip ();
diff --git a/mod/mod-ci-github-gh.hxx b/mod/mod-ci-github-gh.hxx
index 05c289e..ab6dbaa 100644
--- a/mod/mod-ci-github-gh.hxx
+++ b/mod/mod-ci-github-gh.hxx
@@ -26,9 +26,10 @@ namespace brep
// GitHub request/response types (all start with gh_).
//
// Note that the GitHub REST and GraphQL APIs use different id types and
- // values. In the REST API they are usually integers (but sometimes
- // strings!) whereas in GraphQL they are always strings (note:
- // base64-encoded and opaque, not just the REST id value as a string).
+ // values. In the REST API they are usually integers (but check the API
+ // reference for the object in question) whereas in GraphQL they are always
+ // strings (note: base64-encoded and opaque, not just the REST id value as a
+ // string).
//
// In both APIs the id field is called `id`, but REST responses and webhook
// events also contain the corresponding GraphQL object's id in the
@@ -43,7 +44,7 @@ namespace brep
//
namespace json = butl::json;
- // The "check_suite" object within a check_suite webhook event request.
+ // The check_suite member of a check_run webhook event (gh_check_run_event).
//
struct gh_check_suite
{
@@ -51,15 +52,32 @@ namespace brep
optional<string> head_branch;
string head_sha;
+ explicit
+ gh_check_suite (json::parser&);
+
+ gh_check_suite () = default;
+ };
+
+ // The check_suite member of a check_suite webhook event
+ // (gh_check_suite_event).
+ //
+ struct gh_check_suite_ex: gh_check_suite
+ {
size_t check_runs_count;
optional<string> conclusion;
+ string app_id;
+
explicit
- gh_check_suite (json::parser&);
+ gh_check_suite_ex (json::parser&);
- gh_check_suite () = default;
+ gh_check_suite_ex () = default;
};
+ // The check_run object returned in response to GraphQL requests
+ // (e.g. create or update check run). Note that we specifiy the set of
+ // members to return in the GraphQL request.
+ //
struct gh_check_run
{
string node_id;
@@ -72,17 +90,24 @@ namespace brep
gh_check_run () = default;
};
+ // The check_run member of a check_run webhook event (gh_check_run_event).
+ //
struct gh_check_run_ex: gh_check_run
{
string details_url;
gh_check_suite check_suite;
+ string app_id;
+
explicit
gh_check_run_ex (json::parser&);
gh_check_run_ex () = default;
};
+ // The pull_request member of a pull_request webhook event
+ // (gh_pull_request_event).
+ //
struct gh_pull_request
{
string node_id;
@@ -96,38 +121,24 @@ namespace brep
string head_ref;
string head_sha;
+ // Note: not received from GitHub but set from the app-id webhook query
+ // parameter instead.
+ //
+ // For some reason, unlike the check_suite and check_run webhooks, the
+ // pull_request webhook does not contain the app id. For the sake of
+ // simplicity we emulate check_suite and check_run by storing the app-id
+ // webhook query parameter here.
+ //
+ string app_id;
+
explicit
gh_pull_request (json::parser&);
gh_pull_request () = default;
};
- // Return the GitHub check run status corresponding to a build_state.
- //
- string
- gh_to_status (build_state);
-
- // Return the build_state corresponding to a GitHub check run status
- // string. Throw invalid_argument if the passed status was invalid.
- //
- build_state
- gh_from_status (const string&);
-
- // If warning_success is true, then map result_status::warning to `SUCCESS`
- // and to `FAILURE` otherwise.
- //
- // Throw invalid_argument in case of unsupported result_status value
- // (currently skip, interrupt).
- //
- string
- gh_to_conclusion (result_status, bool warning_success);
-
- // Create a check_run name from a build. If the second argument is not
- // NULL, return an abbreviated id if possible.
+ // The repository member of various webhook events.
//
- string
- gh_check_run_name (const build&, const build_queued_hints* = nullptr);
-
struct gh_repository
{
string node_id;
@@ -140,9 +151,11 @@ namespace brep
gh_repository () = default;
};
+ // The installation member of various webhook events.
+ //
struct gh_installation
{
- uint64_t id; // Note: used for installation access token (REST API).
+ string id; // Note: used for installation access token (REST API).
explicit
gh_installation (json::parser&);
@@ -150,12 +163,12 @@ namespace brep
gh_installation () = default;
};
- // The check_suite webhook event request.
+ // The check_suite webhook event.
//
struct gh_check_suite_event
{
string action;
- gh_check_suite check_suite;
+ gh_check_suite_ex check_suite;
gh_repository repository;
gh_installation installation;
@@ -165,6 +178,8 @@ namespace brep
gh_check_suite_event () = default;
};
+ // The check_run webhook event.
+ //
struct gh_check_run_event
{
string action;
@@ -178,6 +193,8 @@ namespace brep
gh_check_run_event () = default;
};
+ // The pull_request webhook event.
+ //
struct gh_pull_request_event
{
string action;
@@ -198,6 +215,9 @@ namespace brep
gh_pull_request_event () = default;
};
+ // Installation access token (IAT) returned when we authenticate as a GitHub
+ // app installation.
+ //
struct gh_installation_access_token
{
string token;
@@ -211,6 +231,32 @@ namespace brep
gh_installation_access_token () = default;
};
+ // Return the GitHub check run status corresponding to a build_state.
+ //
+ string
+ gh_to_status (build_state);
+
+ // Return the build_state corresponding to a GitHub check run status
+ // string. Throw invalid_argument if the passed status was invalid.
+ //
+ build_state
+ gh_from_status (const string&);
+
+ // If warning_success is true, then map result_status::warning to `SUCCESS`
+ // and to `FAILURE` otherwise.
+ //
+ // Throw invalid_argument in case of unsupported result_status value
+ // (currently skip, interrupt).
+ //
+ string
+ gh_to_conclusion (result_status, bool warning_success);
+
+ // Create a check_run name from a build. If the second argument is not
+ // NULL, return an abbreviated id if possible.
+ //
+ string
+ gh_check_run_name (const build&, const build_queued_hints* = nullptr);
+
// Throw system_error if the conversion fails due to underlying operating
// system errors.
//
@@ -227,6 +273,9 @@ namespace brep
operator<< (ostream&, const gh_check_suite&);
ostream&
+ operator<< (ostream&, const gh_check_suite_ex&);
+
+ ostream&
operator<< (ostream&, const gh_check_run&);
ostream&
diff --git a/mod/mod-ci-github-service-data.cxx b/mod/mod-ci-github-service-data.cxx
index 31a556d..9f66a6c 100644
--- a/mod/mod-ci-github-service-data.cxx
+++ b/mod/mod-ci-github-service-data.cxx
@@ -54,8 +54,8 @@ namespace brep
p.next_expect_name ("installation_access");
installation_access = gh_installation_access_token (p);
- installation_id =
- p.next_expect_member_number<uint64_t> ("installation_id");
+ app_id = p.next_expect_member_string ("app_id");
+ installation_id = p.next_expect_member_string ("installation_id");
repository_node_id = p.next_expect_member_string ("repository_node_id");
repository_clone_url = p.next_expect_member_string ("repository_clone_url");
@@ -135,7 +135,8 @@ namespace brep
service_data (bool ws,
string iat_tok,
timestamp iat_ea,
- uint64_t iid,
+ string aid,
+ string iid,
string rid,
string rcu,
kind_type k,
@@ -146,7 +147,8 @@ namespace brep
: kind (k), pre_check (pc), re_request (rr),
warning_success (ws),
installation_access (move (iat_tok), iat_ea),
- installation_id (iid),
+ app_id (move (aid)),
+ installation_id (move (iid)),
repository_node_id (move (rid)),
repository_clone_url (move (rcu)),
check_sha (move (cs)),
@@ -161,7 +163,8 @@ namespace brep
service_data (bool ws,
string iat_tok,
timestamp iat_ea,
- uint64_t iid,
+ string aid,
+ string iid,
string rid,
string rcu,
kind_type k,
@@ -174,7 +177,8 @@ namespace brep
: kind (k), pre_check (pc), re_request (rr),
warning_success (ws),
installation_access (move (iat_tok), iat_ea),
- installation_id (iid),
+ app_id (move (aid)),
+ installation_id (move (iid)),
repository_node_id (move (rid)),
repository_clone_url (move (rcu)),
pr_node_id (move (pid)),
@@ -233,6 +237,7 @@ namespace brep
s.end_object ();
+ s.member ("app_id", app_id);
s.member ("installation_id", installation_id);
s.member ("repository_node_id", repository_node_id);
s.member ("repository_clone_url", repository_clone_url);
diff --git a/mod/mod-ci-github-service-data.hxx b/mod/mod-ci-github-service-data.hxx
index 0f4c760..50bb49d 100644
--- a/mod/mod-ci-github-service-data.hxx
+++ b/mod/mod-ci-github-service-data.hxx
@@ -89,7 +89,8 @@ namespace brep
//
gh_installation_access_token installation_access;
- uint64_t installation_id;
+ string app_id;
+ string installation_id;
string repository_node_id; // GitHub-internal opaque repository id.
@@ -151,7 +152,8 @@ namespace brep
service_data (bool warning_success,
string iat_token,
timestamp iat_expires_at,
- uint64_t installation_id,
+ string app_id,
+ string installation_id,
string repository_node_id,
string repository_clone_url,
kind_type kind,
@@ -165,7 +167,8 @@ namespace brep
service_data (bool warning_success,
string iat_token,
timestamp iat_expires_at,
- uint64_t installation_id,
+ string app_id,
+ string installation_id,
string repository_node_id,
string repository_clone_url,
kind_type kind,
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 14b3c00..5bcec98 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -61,6 +61,8 @@ namespace brep
void ci_github::
init (scanner& s)
{
+ HANDLER_DIAG;
+
{
shared_ptr<tenant_service_base> ts (
dynamic_pointer_cast<tenant_service_base> (shared_from_this ()));
@@ -78,6 +80,9 @@ namespace brep
if (options_->build_config_specified () &&
options_->ci_github_app_webhook_secret_specified ())
{
+ if (!options_->ci_github_app_id_private_key_specified ())
+ fail << "no app id/private key mappings configured";
+
ci_start::init (make_shared<options::ci_start> (*options_));
database_module::init (*options_, options_->build_db_retry ());
@@ -221,33 +226,48 @@ namespace brep
fail << "unable to compute request HMAC: " << e;
}
- // Process the `warning` webhook request query parameter.
+ // Process the `app-id` and `warning` webhook request query parameters.
//
+ string app_id;
bool warning_success;
{
const name_values& rps (rq.parameters (1024, true /* url_only */));
- auto i (find_if (rps.begin (), rps.end (),
- [] (auto&& rp) {return rp.name == "warning";}));
+ bool ai (false), wa (false);
- if (i == rps.end ())
- throw invalid_request (400,
- "missing 'warning' webhook query parameter");
+ auto badreq = [] (const string& m)
+ {
+ throw invalid_request (400, m);
+ };
- if (!i->value)
- throw invalid_request (
- 400, "missing 'warning' webhook query parameter value");
+ for (const name_value& rp: rps)
+ {
+ if (rp.name == "app-id")
+ {
+ if (!rp.value)
+ badreq ("missing 'app-id' webhook query parameter value");
- const string& v (*i->value);
+ ai = true;
+ app_id = *rp.value;
+ }
+ else if (rp.name == "warning")
+ {
+ if (!rp.value)
+ badreq ("missing 'warning' webhook query parameter value");
- if (v == "success") warning_success = true;
- else if (v == "failure") warning_success = false;
- else
- {
- throw invalid_request (
- 400,
- "invalid 'warning' webhook query parameter value: '" + v + '\'');
+ wa = true;
+ const string& v (*rp.value);
+
+ if (v == "success") warning_success = true;
+ else if (v == "failure") warning_success = false;
+ else
+ badreq ("invalid 'warning' webhook query parameter value: '" + v +
+ '\'');
+ }
}
+
+ if (!ai) badreq ("missing 'app-id' webhook query parameter");
+ if (!wa) badreq ("missing 'warning' webhook query parameter");
}
// There is a webhook event (specified in the x-github-event header) and
@@ -279,6 +299,12 @@ namespace brep
throw invalid_request (400, move (m));
}
+ if (cs.check_suite.app_id != app_id)
+ {
+ fail << "webhook check_suite app.id " << cs.check_suite.app_id
+ << " does not match app-id query parameter " << app_id;
+ }
+
if (cs.action == "requested")
{
return handle_check_suite_request (move (cs), warning_success);
@@ -327,6 +353,12 @@ namespace brep
throw invalid_request (400, move (m));
}
+ if (cr.check_run.app_id != app_id)
+ {
+ fail << "webhook check_run app.id " << cr.check_run.app_id
+ << " does not match app-id query parameter " << app_id;
+ }
+
if (cr.action == "rerequested")
{
// Someone manually requested to re-run a specific check run.
@@ -372,6 +404,14 @@ namespace brep
throw invalid_request (400, move (m));
}
+ // Store the app-id webhook query parameter in the gh_pull_request
+ // object (see gh_pull_request for an explanation).
+ //
+ // When we receive the other webhooks we do check that the app ids in
+ // the payload and query match but here we have to assume it is valid.
+ //
+ pr.pull_request.app_id = app_id;
+
if (pr.action == "opened" ||
pr.action == "synchronize")
{
@@ -519,7 +559,7 @@ namespace brep
// let's obtain it to flush out any permission issues early. Also, it is
// valid for an hour so we will most likely make use of it.
//
- optional<string> jwt (generate_jwt (trace, error));
+ optional<string> jwt (generate_jwt (cs.check_suite.app_id, trace, error));
if (!jwt)
throw server_error ();
@@ -603,6 +643,7 @@ namespace brep
service_data sd (warning_success,
iat->token,
iat->expires_at,
+ cs.check_suite.app_id,
cs.installation.id,
move (cs.repository.node_id),
move (cs.repository.clone_url),
@@ -851,7 +892,7 @@ namespace brep
auto get_iat = [this, &trace, &error, &cr] ()
-> optional<gh_installation_access_token>
{
- optional<string> jwt (generate_jwt (trace, error));
+ optional<string> jwt (generate_jwt (cr.check_run.app_id, trace, error));
if (!jwt)
return nullopt;
@@ -1298,7 +1339,7 @@ namespace brep
// let's obtain it to flush out any permission issues early. Also, it is
// valid for an hour so we will most likely make use of it.
//
- optional<string> jwt (generate_jwt (trace, error));
+ optional<string> jwt (generate_jwt (pr.pull_request.app_id, trace, error));
if (!jwt)
throw server_error ();
@@ -1379,6 +1420,7 @@ namespace brep
service_data sd (warning_success,
move (iat->token),
iat->expires_at,
+ pr.pull_request.app_id,
pr.installation.id,
move (pr.repository.node_id),
move (pr.repository.clone_url),
@@ -1682,7 +1724,7 @@ namespace brep
if (system_clock::now () > sd.installation_access.expires_at)
{
- if (optional<string> jwt = generate_jwt (trace, error))
+ if (optional<string> jwt = generate_jwt (sd.app_id, trace, error))
{
new_iat = obtain_installation_access_token (sd.installation_id,
move (*jwt),
@@ -2085,7 +2127,7 @@ namespace brep
if (system_clock::now () > sd.installation_access.expires_at)
{
- if (optional<string> jwt = generate_jwt (trace, error))
+ if (optional<string> jwt = generate_jwt (sd.app_id, trace, error))
{
new_iat = obtain_installation_access_token (sd.installation_id,
move (*jwt),
@@ -2250,7 +2292,7 @@ namespace brep
if (system_clock::now () > sd.installation_access.expires_at)
{
- if (optional<string> jwt = generate_jwt (trace, error))
+ if (optional<string> jwt = generate_jwt (sd.app_id, trace, error))
{
new_iat = obtain_installation_access_token (sd.installation_id,
move (*jwt),
@@ -2456,7 +2498,7 @@ namespace brep
if (system_clock::now () > sd.installation_access.expires_at)
{
- if (optional<string> jwt = generate_jwt (trace, error))
+ if (optional<string> jwt = generate_jwt (sd.app_id, trace, error))
{
new_iat = obtain_installation_access_token (sd.installation_id,
move (*jwt),
@@ -2881,19 +2923,32 @@ namespace brep
}
optional<string> ci_github::
- generate_jwt (const basic_mark& trace,
+ generate_jwt (const string& app_id,
+ const basic_mark& trace,
const basic_mark& error) const
{
string jwt;
try
{
+ // Look up the private key path for the app id and fail if not found.
+ //
+ const map<string, dir_path>& pks (
+ options_->ci_github_app_id_private_key ());
+
+ auto pk (pks.find (app_id));
+ if (pk == pks.end ())
+ {
+ error << "unable to generate JWT: "
+ << "no private key configured for app id " << app_id;
+ return nullopt;
+ }
+
// Set token's "issued at" time 60 seconds in the past to combat clock
// drift (as recommended by GitHub).
//
jwt = brep::generate_jwt (
*options_,
- options_->ci_github_app_private_key (),
- to_string (options_->ci_github_app_id ()),
+ pk->second, app_id,
chrono::seconds (options_->ci_github_jwt_validity_period ()),
chrono::seconds (60));
@@ -2949,7 +3004,7 @@ namespace brep
// example.
//
optional<gh_installation_access_token> ci_github::
- obtain_installation_access_token (uint64_t iid,
+ obtain_installation_access_token (const string& iid,
string jwt,
const basic_mark& error) const
{
@@ -2958,7 +3013,7 @@ namespace brep
{
// API endpoint.
//
- string ep ("app/installations/" + to_string (iid) + "/access_tokens");
+ string ep ("app/installations/" + iid + "/access_tokens");
uint16_t sc (
github_post (iat, ep, strings {"Authorization: Bearer " + jwt}));
diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx
index f97bf05..059801a 100644
--- a/mod/mod-ci-github.hxx
+++ b/mod/mod-ci-github.hxx
@@ -120,14 +120,16 @@ namespace brep
details_url (const build&) const;
optional<string>
- generate_jwt (const basic_mark& trace, const basic_mark& error) const;
+ generate_jwt (const string& app_id,
+ const basic_mark& trace,
+ const basic_mark& error) const;
// Authenticate to GitHub as an app installation. Return the installation
// access token (IAT). Issue diagnostics and return nullopt if something
// goes wrong.
//
optional<gh_installation_access_token>
- obtain_installation_access_token (uint64_t install_id,
+ obtain_installation_access_token (const string& install_id,
string jwt,
const basic_mark& error) const;
diff --git a/mod/module.cli b/mod/module.cli
index 274ecd4..1273bf4 100644
--- a/mod/module.cli
+++ b/mod/module.cli
@@ -850,12 +850,6 @@ namespace brep
// GitHub CI-specific options.
//
- size_t ci-github-app-id
- {
- "<id>",
- "The GitHub App ID. Found in the app's settings on GitHub."
- }
-
string ci-github-app-webhook-secret
{
"<secret>",
@@ -864,11 +858,12 @@ namespace brep
(random) secret."
}
- path ci-github-app-private-key
+ std::map<string, dir_path> ci-github-app-id-private-key
{
- "<path>",
- "The private key used during GitHub API authentication. Created in
- the GitHub App's settings."
+ "<id>=<path>",
+ "The private key used during GitHub API authentication for the
+ specified GitHub App ID. Both vales are found in the GitHub App's
+ settings."
}
uint16_t ci-github-jwt-validity-period = 600