aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL111
-rw-r--r--etc/brep-module.conf8
-rw-r--r--etc/private/install/brep-module.conf8
-rw-r--r--libbrep/utility.hxx5
-rw-r--r--manifest6
-rw-r--r--mod/hmac.cxx13
-rw-r--r--mod/mod-build-configs.cxx6
-rw-r--r--mod/mod-build-result.cxx30
-rw-r--r--mod/mod-build-task.cxx27
-rw-r--r--mod/mod-builds.cxx6
-rw-r--r--mod/mod-ci-github-gq.cxx94
-rw-r--r--mod/mod-ci-github-gq.hxx36
-rw-r--r--mod/mod-ci-github-service-data.cxx10
-rw-r--r--mod/mod-ci-github-service-data.hxx13
-rw-r--r--mod/mod-ci-github.cxx460
-rw-r--r--mod/mod-ci-github.hxx15
-rw-r--r--mod/mod-ci.cxx55
-rw-r--r--mod/mod-ci.hxx4
-rw-r--r--mod/mod-submit.cxx10
-rw-r--r--mod/module.cli10
-rw-r--r--mod/tenant-service.cxx18
-rw-r--r--mod/tenant-service.hxx12
22 files changed, 699 insertions, 258 deletions
diff --git a/INSTALL b/INSTALL
index 94b73cf..7986b84 100644
--- a/INSTALL
+++ b/INSTALL
@@ -356,7 +356,116 @@ For sample CI request handler implementations see brep/handler/ci/.
6.4 Enabling GitHub CI integration
-@@ TODO
+6.4.1 Background
+
+The GitHub CI integration has one user-configurable setting:
+warning=<success|failure> (whether or not to fail on warnings).
+
+In order not to have to support repository configuration files, a deployment
+will consist of two registered GitHub Apps with the same webhook URL (i.e.,
+the same brep instance) but different query parameters: one with
+warning=success and the other with warning=failure. The App id is passed (as a
+query parameter) so that we know which private key to use (the key cannot be
+shared between Apps).
+
+We will call the warning=success App the "Default App" and the warning=failure
+App the "Werror App".
+
+6.4.2 Create the GitHub Apps
+
+To create a GitHub App under the <org> organization, visit
+https://github.com/organizations/<org>/settings/apps (Settings -> Developer
+settings -> GitHub Apps). Then click on New GitHub App.
+
+App names (note: 34 character limit):
+
+ Default App: "<org> CI"
+ Werror App: "<org> CI - warnings as errors"
+
+App description:
+
+ Default App: "Trigger <org> CI on branch push and pull request."
+ Werror App: "Trigger <org> CI on branch push and pull request. Warnings are
+ treated as errors".
+
+App homepage:
+
+ https://ci.<org>.org/
+
+Skip the "Identifying and authorizing users" and "Post installation" sections.
+
+Leave webhooks active.
+
+Webhook URL:
+
+ Default App: https://ci.<org>.org/?ci-github&app-id=XXX&warning=success
+ Werror App: https://ci.<org>.org/?ci-github&app-id=XXX&warning=failure
+
+Note that the App id only becomes available once the App has been registered
+so we update it later in both URLs.
+
+Webhook secret: Use the same random 64-character string for both Apps.
+
+ echo `tr -dc -- A-Za-z0-9 </dev/urandom | head -c 64`
+
+Note that GitHub says only that the secret should be "a random string with
+high entropy." However lots of sources say 32 bytes should be secure enough
+for HMAC-SHA256, while other sources recommend 64 bytes for maximal security
+at an insignificant performance cost. (Keys longer than 64 bytes are hashed to
+match the internal block size and are therefore not recommended.)
+
+Repository permissions:
+ - Checks: RW
+ - Contents: RO (for Push events)
+ - Metadata (mandatory): RO
+ - Pull requests: RO
+
+Subscribed events:
+ - Check suite
+ - Pull request
+ - Push
+
+Note that GitHub Apps with write access to the "Checks" permission are
+automatically subscribed to check_suite(requested|rerequested) and check_run
+events so no need to subscribe explicitly. However in order to receive
+check_suite(completed) events, which we need, one does have to subscribe to
+Check suite.
+
+Select "Any account" under "Where can this GitHub App be installed?".
+
+Click "Create GitHub App".
+
+When the page reloads (should be the General tab), note the App id and replace
+the XXX in the webhook URL with it.
+
+Still in the General tab, scroll to Private keys and generate a private key.
+The file will be downloaded by the browser.
+
+@@ TODO Logo
+@@ TODO Create Marketplace listing
+
+6.4.3 Configure brep
+
+Assume the following configuration values:
+
+- Webhook secret: abcdefg
+- Default App id: 12345
+- Werror App id: 67890
+
+In brep-module.conf:
+
+Set the webhook secret from the GitHub App settings:
+
+ ci-github-app-webhook-secret abcdefg
+
+Associate each GitHub App id with the App's private key:
+
+ ci-github-app-id-private-key 12345=path/to/default-app-private-key.pem
+ ci-github-app-id-private-key 67890=path/to/werror-app-private-key.pem
+
+Now brep should be ready to handle the webhook event requests triggered by
+branch pushes and pull requests in repositories into which one of these Apps
+has been installed.
7. Optimize CSS
diff --git a/etc/brep-module.conf b/etc/brep-module.conf
index fd6ba67..cdf028a 100644
--- a/etc/brep-module.conf
+++ b/etc/brep-module.conf
@@ -454,13 +454,15 @@ menu About=?about
# The GitHub App's configured webhook secret. If not set, then the GitHub CI
-# service is disabled. Note: make sure to choose a strong (random) secret.
+# service is disabled. Note that the path must be absolute. Note: make sure to
+# choose a strong (random) secret.
#
-# ci-github-app-webhook-secret
+# ci-github-app-webhook-secret <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.
+# GitHub App ID. Both vales are found in the GitHub App's settings. Note that
+# the paths must be absolute.
#
# ci-github-app-id-private-key <id>=<path>
diff --git a/etc/private/install/brep-module.conf b/etc/private/install/brep-module.conf
index 07db881..2545a87 100644
--- a/etc/private/install/brep-module.conf
+++ b/etc/private/install/brep-module.conf
@@ -462,13 +462,15 @@ submit-handler-timeout 120
# The GitHub App's configured webhook secret. If not set, then the GitHub CI
-# service is disabled. Note: make sure to choose a strong (random) secret.
+# service is disabled. Note that the path must be absolute. Note: make sure to
+# choose a strong (random) secret.
#
-# ci-github-app-webhook-secret
+# ci-github-app-webhook-secret <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.
+# GitHub App ID. Both vales are found in the GitHub App's settings. Note that
+# the paths must be absolute.
#
# ci-github-app-id-private-key <id>=<path>
diff --git a/libbrep/utility.hxx b/libbrep/utility.hxx
index fce8fb5..1925d01 100644
--- a/libbrep/utility.hxx
+++ b/libbrep/utility.hxx
@@ -12,7 +12,7 @@
#include <algorithm> // *
#include <libbutl/utility.hxx> // icasecmp(), reverse_iterate(),
- // operator<<(ostream, exception)
+ // operator<<(ostream, exception), etc
namespace brep
{
@@ -28,6 +28,9 @@ namespace brep
// <libbutl/utility.hxx>
//
using butl::utf8;
+ using butl::trim;
+ using butl::trim_left;
+ using butl::trim_right;
using butl::icasecmp;
using butl::reverse_iterate;
}
diff --git a/manifest b/manifest
index 720f35e..7681a23 100644
--- a/manifest
+++ b/manifest
@@ -24,9 +24,9 @@ depends: libapr1
depends: libapreq2
depends: libcmark-gfm == 0.29.0-a.4
depends: libcmark-gfm-extensions == 0.29.0-a.4
-depends: libstudxml ^1.1.0-b.10
-depends: libodb ^2.5.0-b.27
-depends: libodb-pgsql ^2.5.0-b.27
+depends: libstudxml ^1.1.0
+depends: libodb ^2.5.0
+depends: libodb-pgsql ^2.5.0
depends: libbutl [0.18.0-a.0.1 0.18.0-a.1)
depends: libbpkg [0.18.0-a.0.1 0.18.0-a.1)
depends: libbbot [0.18.0-a.0.1 0.18.0-a.1)
diff --git a/mod/hmac.cxx b/mod/hmac.cxx
index 1a78b4c..cfb0e23 100644
--- a/mod/hmac.cxx
+++ b/mod/hmac.cxx
@@ -16,6 +16,12 @@ compute_hmac (const options::openssl_options& o,
// To compute an HMAC over stdin with the key <secret>:
//
+ // openssl dgst -sha256 -hmac <secret>
+ //
+ // Note that since openssl 3.0 the `mac` command is the preferred method
+ // for generating HMACs. For future reference, the equivalent command
+ // would be:
+ //
// openssl mac -digest SHA256 -macopt "key:<secret>" HMAC
//
// Note that here we assume both output and diagnostics will fit into pipe
@@ -25,10 +31,9 @@ compute_hmac (const options::openssl_options& o,
path ("-"), // Write output to openssl::in.
process::pipe (errp.in.get (), move (errp.out)),
process_env (o.openssl (), o.openssl_envvar ()),
- "mac", o.openssl_option (),
- "-digest", "SHA256",
- "-macopt", string ("key:") + k,
- "HMAC");
+ "dgst", o.openssl_option (),
+ "-sha256",
+ "-hmac", k);
ifdstream err (move (errp.in));
diff --git a/mod/mod-build-configs.cxx b/mod/mod-build-configs.cxx
index ce79edb..2754f95 100644
--- a/mod/mod-build-configs.cxx
+++ b/mod/mod-build-configs.cxx
@@ -34,10 +34,12 @@ init (scanner& s)
s, unknown_mode::fail, unknown_mode::fail);
if (options_->build_config_specified ())
+ {
build_config_module::init (*options_);
- if (options_->root ().empty ())
- options_->root (dir_path ("/"));
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+ }
}
bool brep::build_configs::
diff --git a/mod/mod-build-result.cxx b/mod/mod-build-result.cxx
index cc058b5..b303386 100644
--- a/mod/mod-build-result.cxx
+++ b/mod/mod-build-result.cxx
@@ -565,7 +565,7 @@ handle (request& rq, response&)
{
assert (tss); // Wouldn't be here otherwise.
- const tenant_service& ss (tss->first);
+ tenant_service& ss (tss->first);
const build& b (*tss->second);
// Release the database connection since build_built() notification can
@@ -576,7 +576,33 @@ handle (request& rq, response&)
if (auto f = tsb->build_built (b.tenant, ss, b, log_writer_))
{
conn = build_db_->connection ();
- update_tenant_service_state (conn, ss.type, ss.id, f);
+
+ bool build_completed (false);
+
+ if (optional<string> data =
+ update_tenant_service_state (
+ conn, ss.type, ss.id,
+ [&f, &build_completed] (const string& tid,
+ const tenant_service& ts)
+ {
+ auto r (f (tid, ts));
+ build_completed = r.second;
+ return move (r.first);
+ }))
+ {
+ ss.data = move (data);
+ }
+
+ if (build_completed)
+ {
+ // Release the database connection since the build_completed()
+ // notification can potentially be time-consuming (e.g., it may
+ // perform an HTTP request).
+ //
+ conn.reset ();
+
+ tsb->build_completed (b.tenant, ss, log_writer_);
+ }
}
}
diff --git a/mod/mod-build-task.cxx b/mod/mod-build-task.cxx
index c8b1bb2..88e5618 100644
--- a/mod/mod-build-task.cxx
+++ b/mod/mod-build-task.cxx
@@ -2407,7 +2407,7 @@ handle (request& rq, response& rs)
}
// If a third-party service needs to be notified about the package
- // build, then call the tenant_service_build_built::build_building()
+ // build, then call the tenant_service_build_building::build_building()
// callback function and, if requested, update the tenant-associated
// service state.
//
@@ -2556,9 +2556,32 @@ handle (request& rq, response& rs)
{
conn = build_db_->connection ();
+ bool build_completed (false);
+
if (optional<string> data =
- update_tenant_service_state (conn, ss.type, ss.id, f))
+ update_tenant_service_state (
+ conn, ss.type, ss.id,
+ [&f, &build_completed] (const string& tid,
+ const tenant_service& ts)
+ {
+ auto r (f (tid, ts));
+ build_completed = r.second;
+ return move (r.first);
+ }))
+ {
ss.data = move (data);
+ }
+
+ if (build_completed)
+ {
+ // Release the database connection since the build_completed()
+ // notification can potentially be time-consuming (e.g., it may
+ // perform an HTTP request).
+ //
+ conn.reset ();
+
+ tsb->build_completed (b.tenant, ss, log_writer_);
+ }
}
}
}
diff --git a/mod/mod-builds.cxx b/mod/mod-builds.cxx
index 0155c2e..b11b3d7 100644
--- a/mod/mod-builds.cxx
+++ b/mod/mod-builds.cxx
@@ -58,10 +58,10 @@ init (scanner& s)
{
database_module::init (*options_, options_->build_db_retry ());
build_config_module::init (*options_);
- }
- if (options_->root ().empty ())
- options_->root (dir_path ("/"));
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+ }
}
template <typename T, typename C>
diff --git a/mod/mod-ci-github-gq.cxx b/mod/mod-ci-github-gq.cxx
index db69f0c..37d944c 100644
--- a/mod/mod-ci-github-gq.cxx
+++ b/mod/mod-ci-github-gq.cxx
@@ -380,9 +380,12 @@ namespace brep
assert (cr.state != build_state::built); // Not supported.
- // Ensure details URL is non-empty if present.
+ // Ensure details URL and output are non-empty if present.
//
assert (!cr.details_url || !cr.details_url->empty ());
+ assert (!cr.description ||
+ (!cr.description->title.empty () &&
+ !cr.description->summary.empty ()));
string al ("cr" + to_string (i)); // Field alias.
@@ -396,6 +399,13 @@ namespace brep
os << '\n';
os << " detailsUrl: " << gq_str (*cr.details_url);
}
+ if (cr.description)
+ {
+ os << " output: {" << '\n'
+ << " title: " << gq_str (cr.description->title) << '\n'
+ << " summary: " << gq_str (cr.description->summary) << '\n'
+ << " }";
+ }
os << "})" << '\n'
// Specify the selection set (fields to be returned). Note that we
// rename `id` to `node_id` (using a field alias) for consistency with
@@ -417,9 +427,9 @@ namespace brep
// Serialize a `createCheckRun` mutation for a build to GraphQL.
//
- // The build result argument (`br`) is required if the build_state is built
- // because GitHub does not allow a check run status of completed without a
- // conclusion.
+ // The conclusion argument (`co`) is required if the check run status is
+ // completed because GitHub does not allow a check run status of completed
+ // without a conclusion.
//
// The details URL argument (`du`) can be empty for queued but not for the
// other states.
@@ -433,12 +443,18 @@ namespace brep
const optional<string>& du, // Details URL.
const check_run& cr,
const string& st, // Check run status.
- optional<gq_built_result> br = nullopt)
+ const string& ti, // Output title.
+ const string& su, // Output summary.
+ optional<string> co = nullopt) // Conclusion.
{
// Ensure details URL is non-empty if present.
//
assert (!du || !du->empty ());
+ // Ensure we have conclusion if the status is completed.
+ //
+ assert (st != "COMPLETED" || co);
+
ostringstream os;
os << "mutation {" << '\n';
@@ -455,15 +471,13 @@ namespace brep
os << '\n';
os << " detailsUrl: " << gq_str (*du);
}
- if (br)
- {
- os << '\n';
- os << " conclusion: " << gq_enum (br->conclusion) << '\n'
- << " output: {" << '\n'
- << " title: " << gq_str (br->title) << '\n'
- << " summary: " << gq_str (br->summary) << '\n'
- << " }";
- }
+ os << '\n';
+ if (co)
+ os << " conclusion: " << gq_enum (*co) << '\n';
+ os << " output: {" << '\n'
+ << " title: " << gq_str (ti) << '\n'
+ << " summary: " << gq_str (su) << '\n'
+ << " }";
os << "})" << '\n'
// Specify the selection set (fields to be returned). Note that we
// rename `id` to `node_id` (using a field alias) for consistency with
@@ -485,7 +499,7 @@ namespace brep
// Serialize an `updateCheckRun` mutation for one build to GraphQL.
//
- // The `co` (conclusion) argument is required if the build_state is built
+ // The `br` argument is required if the check run status is completed
// because GitHub does not allow updating a check run to completed without a
// conclusion.
//
@@ -495,14 +509,11 @@ namespace brep
static string
gq_mutation_update_check_run (const string& ri, // Repository ID.
const string& ni, // Node ID.
- const optional<string>& du, // Details URL.
const string& st, // Check run status.
optional<timestamp> sa, // Started at.
optional<gq_built_result> br)
{
- // Ensure details URL is non-empty if present.
- //
- assert (!du || !du->empty ());
+ assert (st != "COMPLETED" || br);
ostringstream os;
@@ -527,11 +538,6 @@ namespace brep
": " + e.what ());
}
}
- if (du)
- {
- os << '\n';
- os << " detailsUrl: " << gq_str (*du);
- }
if (br)
{
os << '\n';
@@ -586,11 +592,11 @@ namespace brep
const string& hs,
const optional<string>& du,
build_state st,
- optional<gq_built_result> br)
+ string ti, string su)
{
- // Must have a result if state is built.
+ // State cannot be built without a conclusion.
//
- assert (st != build_state::built || br);
+ assert (st != build_state::built && !ti.empty () && !su.empty ());
string rq (
gq_serialize_request (
@@ -599,7 +605,8 @@ namespace brep
du,
cr,
gh_to_status (st),
- move (br))));
+ move (ti), move (su),
+ nullopt /* conclusion */)));
vector<check_run> crs {move (cr)};
crs[0].state = st;
@@ -612,12 +619,40 @@ namespace brep
}
bool
+ gq_create_check_run (const basic_mark& error,
+ check_run& cr,
+ const string& iat,
+ const string& rid,
+ const string& hs,
+ const optional<string>& du,
+ gq_built_result br)
+ {
+ string rq (
+ gq_serialize_request (
+ gq_mutation_create_check_run (rid,
+ hs,
+ du,
+ cr,
+ gh_to_status (build_state::built),
+ move (br.title), move (br.summary),
+ move (br.conclusion))));
+
+ vector<check_run> crs {move (cr)};
+ crs[0].state = build_state::built;
+
+ bool r (gq_mutate_check_runs (error, crs, iat, move (rq)));
+
+ cr = move (crs[0]);
+
+ return r;
+ }
+
+ bool
gq_update_check_run (const basic_mark& error,
check_run& cr,
const string& iat,
const string& rid,
const string& nid,
- const optional<string>& du,
build_state st,
optional<gq_built_result> br)
{
@@ -636,7 +671,6 @@ namespace brep
gq_serialize_request (
gq_mutation_update_check_run (rid,
nid,
- du,
gh_to_status (st),
sa,
move (br))));
diff --git a/mod/mod-ci-github-gq.hxx b/mod/mod-ci-github-gq.hxx
index 50950d4..0fc3817 100644
--- a/mod/mod-ci-github-gq.hxx
+++ b/mod/mod-ci-github-gq.hxx
@@ -20,7 +20,7 @@ namespace brep
//
// Create a new check run on GitHub for each build with the build state,
- // name, and details_url taken from each check_run object. Update
+ // name, details_url, and output taken from each check_run object. Update
// `check_runs` with the new data (node id and state_synced). Return false
// and issue diagnostics if the request failed.
//
@@ -39,18 +39,32 @@ namespace brep
const string& repository_id,
const string& head_sha);
- // Create a new check run on GitHub for a build. Update `cr` with the new
- // data (node id, state, and state_synced). Return false and issue
- // diagnostics if the request failed.
+ // Create a new check run on GitHub for a build in the queued or building
+ // state. Note that the state cannot be built because in that case a
+ // conclusion is required.
+ //
+ // Update `cr` with the new data (node id, state, and state_synced). Return
+ // false and issue diagnostics if the request failed.
//
// Throw invalid_argument if the passed data is invalid, missing, or
// inconsistent.
//
- // If the details_url is absent GitHub will use the app's homepage.
+ // If the details_url is absent GitHub will use the app's homepage. Title
+ // and summary are required and cannot be empty.
//
- // The gq_built_result is required if the build_state is built because
- // GitHub does not allow a check run status of `completed` without at least
- // a conclusion.
+ bool
+ gq_create_check_run (const basic_mark& error,
+ check_run& cr,
+ const string& installation_access_token,
+ const string& repository_id,
+ const string& head_sha,
+ const optional<string>& details_url,
+ build_state,
+ string title,
+ string summary);
+
+ // As above but create a check run in the built state (which requires a
+ // conclusion).
//
struct gq_built_result
{
@@ -66,8 +80,7 @@ namespace brep
const string& repository_id,
const string& head_sha,
const optional<string>& details_url,
- build_state,
- optional<gq_built_result> = nullopt);
+ gq_built_result);
// Update a check run on GitHub. Update `cr` with the new data (state and
// state_synced). Return false and issue diagnostics if the request failed.
@@ -79,8 +92,6 @@ namespace brep
// built to built is allowed). The latter case is signalled by setting the
// check_run state_synced member to false and the state member to built.
//
- // If the details_url is absent GitHub will use the app's homepage.
- //
// The gq_built_result is required if the build_state is built because
// GitHub does not allow a check run status of `completed` without at least
// a conclusion.
@@ -91,7 +102,6 @@ namespace brep
const string& installation_access_token,
const string& repository_id,
const string& node_id,
- const optional<string>& details_url,
build_state,
optional<gq_built_result> = nullopt);
diff --git a/mod/mod-ci-github-service-data.cxx b/mod/mod-ci-github-service-data.cxx
index 9f66a6c..c51f791 100644
--- a/mod/mod-ci-github-service-data.cxx
+++ b/mod/mod-ci-github-service-data.cxx
@@ -113,7 +113,15 @@ namespace brep
}
}
- check_runs.emplace_back (move (bid), move (nm), move (nid), s, ss, rs);
+ check_runs.push_back (
+ check_run {move (bid),
+ move (nm),
+ move (nid),
+ s,
+ ss,
+ rs,
+ nullopt, /* details_url */
+ nullopt /* description */});
p.next_expect (event::end_object);
}
diff --git a/mod/mod-ci-github-service-data.hxx b/mod/mod-ci-github-service-data.hxx
index 50bb49d..5d36696 100644
--- a/mod/mod-ci-github-service-data.hxx
+++ b/mod/mod-ci-github-service-data.hxx
@@ -33,10 +33,17 @@ namespace brep
optional<result_status> status; // Only if state is built & synced.
- // Note: never serialized (only used to pass information to the GraphQL
- // functions).
+ // Note: these are never serialized (only used to pass information to the
+ // GraphQL functions).
//
- optional<string> details_url;
+ struct description_type
+ {
+ string title;
+ string summary;
+ };
+
+ optional<string> details_url;
+ optional<description_type> description;
string
state_string () const
diff --git a/mod/mod-ci-github.cxx b/mod/mod-ci-github.cxx
index 25c238e..b995256 100644
--- a/mod/mod-ci-github.cxx
+++ b/mod/mod-ci-github.cxx
@@ -77,12 +77,45 @@ namespace brep
// Prepare for the CI requests handling, if configured.
//
- if (options_->build_config_specified () &&
- options_->ci_github_app_webhook_secret_specified ())
+ if (options_->ci_github_app_webhook_secret_specified ())
{
+ if (!options_->build_config_specified ())
+ fail << "package building functionality must be enabled";
+
if (!options_->ci_github_app_id_private_key_specified ())
fail << "no app id/private key mappings configured";
+ for (const auto& pr: options_->ci_github_app_id_private_key ())
+ {
+ if (pr.second.relative ())
+ fail << "ci-github-app-id-private-key path must be absolute";
+ }
+
+ // Read the webhook secret from the configured path.
+ //
+ {
+ const path& p (options_->ci_github_app_webhook_secret ());
+
+ if (p.relative ())
+ fail << "ci-github-app-webhook-secret path must be absolute";
+
+ try
+ {
+ ifdstream is (p);
+ getline (is, webhook_secret_, '\0');
+
+ // Trim leading/trailing whitespaces (presumably GitHub does the
+ // same in its web UI).
+ //
+ if (trim (webhook_secret_).empty ())
+ fail << "empty webhook secret in " << p;
+ }
+ catch (const io_error& e)
+ {
+ fail << "unable to read webhook secret from " << p << ": " << e;
+ }
+ }
+
ci_start::init (make_shared<options::ci_start> (*options_));
database_module::init (*options_, options_->build_db_retry ());
@@ -207,10 +240,10 @@ namespace brep
//
try
{
- string h (
- compute_hmac (*options_,
- body.data (), body.size (),
- options_->ci_github_app_webhook_secret ().c_str ()));
+ string h (compute_hmac (*options_,
+ body.data (),
+ body.size (),
+ webhook_secret_.c_str ()));
if (!icasecmp (h, hmac))
{
@@ -546,6 +579,10 @@ namespace brep
//
static string conclusion_check_run_name ("CONCLUSION");
+ static check_run::description_type conclusion_check_run_building_description {
+ "\U000026AA IN PROGRESS", // "Medium white" circle.
+ "Waiting for all builds to complete"};
+
bool ci_github::
handle_branch_push (gh_push_event ps, bool warning_success)
{
@@ -1124,8 +1161,14 @@ namespace brep
static gq_built_result
make_built_result (result_status rs, bool warning_success, string message)
{
+ string title (circle (rs == result_status::warning && !warning_success
+ ? result_status::error
+ : rs));
+ title += ' ';
+ title += ucase (to_string (rs));
+
return {gh_to_conclusion (rs, warning_success),
- circle (rs) + ' ' + ucase (to_string (rs)),
+ move (title),
move (message)};
}
@@ -1286,7 +1329,6 @@ namespace brep
if (gq_update_check_run (error, bcr, iat->token,
repo_node_id, cr.check_run.node_id,
- nullopt /* details_url */,
build_state::built, br))
{
l3 ([&]{trace << "updated check_run { " << bcr << " }";});
@@ -1300,7 +1342,6 @@ namespace brep
if (gq_update_check_run (error, ccr, iat->token,
repo_node_id, *sd.conclusion_node_id,
- nullopt /* details_url */,
build_state::built, move (br)))
{
l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";});
@@ -1349,7 +1390,6 @@ namespace brep
//
if (gq_update_check_run (error, ccr, iat->token,
repo_node_id, *sd.conclusion_node_id,
- nullopt /* details_url */,
build_state::built, move (br)))
{
l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";});
@@ -1430,6 +1470,8 @@ namespace brep
ccr.state = build_state::building;
ccr.state_synced = false;
+ ccr.details_url = details_url (tenant_id);
+ ccr.description = conclusion_check_run_building_description;
if (gq_create_check_runs (error, check_runs, iat->token,
repo_node_id, head_sha))
@@ -1580,7 +1622,6 @@ namespace brep
//
if (gq_update_check_run (error, bcr, iat->token,
repo_node_id, *bcr.node_id,
- nullopt /* details_url */,
build_state::built, br))
{
l3 ([&]{trace << "updated check_run { " << bcr << " }";});
@@ -1597,7 +1638,6 @@ namespace brep
//
if (gq_update_check_run (error, ccr, iat->token,
repo_node_id, *ccr.node_id,
- nullopt /* details_url */,
build_state::built, move (br)))
{
l3 ([&]{trace << "updated conclusion check_run { " << ccr << " }";});
@@ -1896,9 +1936,13 @@ namespace brep
// Create a synthetic check run with an in-progress state. Return the
// check run on success or nullopt on failure.
//
- auto create_synthetic_cr = [iat,
+ auto create_synthetic_cr = [&tenant_id,
+ iat,
&sd,
- &error] (string name) -> optional<check_run>
+ &error,
+ this] (string name,
+ const check_run::description_type& output)
+ -> optional<check_run>
{
check_run cr;
cr.name = move (name);
@@ -1910,8 +1954,9 @@ namespace brep
iat->token,
sd.repository_node_id,
sd.report_sha,
- nullopt /* details_url */,
- build_state::building))
+ details_url (tenant_id),
+ build_state::building,
+ output.title, output.summary))
{
return cr;
}
@@ -1946,7 +1991,6 @@ namespace brep
iat->token,
sd.repository_node_id,
node_id,
- nullopt /* details_url */,
build_state::built,
move (br)))
{
@@ -1969,7 +2013,9 @@ namespace brep
if (!sd.conclusion_node_id)
{
- if (auto cr = create_synthetic_cr (conclusion_check_run_name))
+ if (auto cr =
+ create_synthetic_cr (conclusion_check_run_name,
+ conclusion_check_run_building_description))
{
l3 ([&]{trace << "created check_run { " << *cr << " }";});
@@ -2263,11 +2309,15 @@ namespace brep
//
bs.push_back (b);
- crs.emplace_back (move (bid),
- gh_check_run_name (b, &hs),
- nullopt, /* node_id */
- build_state::queued,
- false /* state_synced */);
+ crs.push_back (
+ check_run {move (bid),
+ gh_check_run_name (b, &hs),
+ nullopt, /* node_id */
+ build_state::queued,
+ false /* state_synced */,
+ nullopt /* status */,
+ details_url (b),
+ nullopt /* description */});
}
}
@@ -2471,7 +2521,6 @@ namespace brep
iat->token,
sd.repository_node_id,
*cr->node_id,
- details_url (b),
build_state::building))
{
// Do nothing further if the state was already built on GitHub (note
@@ -2547,7 +2596,8 @@ namespace brep
return nullptr;
}
- function<optional<string> (const string&, const tenant_service&)> ci_github::
+ function<pair<optional<string>, bool> (const string&,
+ const tenant_service&)> ci_github::
build_built (const string& tenant_id,
const tenant_service& ts,
const build& b,
@@ -2578,44 +2628,17 @@ namespace brep
if (sd.completed)
return nullptr;
- // 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 and mark
- // the check suite as completed.
- //
- // Absent means we still have unbuilt check runs.
- //
- optional<result_status> conclusion (*b.status);
+ // Here we only update the state of this check run. If there are no more
+ // unbuilt ones, then the synthetic conclusion check run will be updated
+ // in build_completed(). Note that determining whether we have no more
+ // unbuilt would be racy here so instead we do it in the service data
+ // update function that we return.
check_run cr; // Updated check run.
{
string bid (gh_check_run_name (b)); // Full build id.
- optional<check_run> scr;
- for (check_run& cr: sd.check_runs)
- {
- if (cr.build_id == bid)
- {
- assert (!scr);
- scr = move (cr);
- }
- else
- {
- if (cr.state == build_state::built)
- {
- assert (cr.status);
-
- if (conclusion)
- *conclusion |= *cr.status;
- }
- else
- conclusion = nullopt;
- }
-
- if (scr && !conclusion.has_value ())
- break;
- }
-
- if (scr)
+ if (check_run* scr = sd.find_check_run (bid))
{
if (scr->state != build_state::building)
{
@@ -2664,8 +2687,6 @@ namespace brep
else
iat = &sd.installation_access;
- bool completed (false);
-
// Note: we treat the failure to obtain the installation access token the
// same as the failure to notify GitHub (state is updated but not marked
// synced).
@@ -2787,7 +2808,6 @@ namespace brep
iat->token,
sd.repository_node_id,
*cr.node_id,
- details_url (b),
build_state::built,
move (br)))
{
@@ -2811,7 +2831,6 @@ namespace brep
sd.repository_node_id,
sd.report_sha,
details_url (b),
- build_state::built,
move (br)))
{
assert (cr.state == build_state::built);
@@ -2821,71 +2840,27 @@ namespace brep
if (cr.state_synced)
{
- // Check run was created/updated successfully to built (with
- // status we specified).
+ // Check run was created/updated successfully to built (with status we
+ // specified).
//
cr.status = b.status;
-
- // Update the conclusion check run if all check runs are now built.
- //
- if (conclusion)
- {
- assert (sd.conclusion_node_id);
-
- result_status rs (*conclusion);
-
- gq_built_result br (
- make_built_result (rs, sd.warning_success,
- "All configurations are built"));
-
- check_run cr;
-
- // Set some fields for display purposes.
- //
- cr.node_id = *sd.conclusion_node_id;
- cr.name = conclusion_check_run_name;
-
- // Let unlikely invalid_argument propagate.
- //
- if (gq_update_check_run (error,
- cr,
- iat->token,
- sd.repository_node_id,
- *sd.conclusion_node_id,
- nullopt /* details_url */,
- build_state::built,
- move (br)))
- {
- assert (cr.state == build_state::built);
- l3 ([&]{trace << "updated conclusion check_run { " << cr << " }";});
- }
- else
- {
- // Nothing we can do here except log the error.
- //
- error << "tenant_service id " << ts.id
- << ": unable to update conclusion check run "
- << *sd.conclusion_node_id;
- }
-
- completed = true;
- }
}
}
return [tenant_id,
iat = move (new_iat),
cr = move (cr),
- completed = completed,
error = move (error),
warn = move (warn)] (const string& ti,
- const tenant_service& ts) -> optional<string>
+ const tenant_service& ts)
{
// NOTE: this lambda may be called repeatedly (e.g., due to transaction
// being aborted) and so should not move out of its captures.
+ // Do nothing if the tenant has been replaced.
+ //
if (tenant_id != ti)
- return nullopt; // Do nothing if the tenant has been replaced.
+ return make_pair (optional<string> (), false);
service_data sd;
try
@@ -2895,7 +2870,19 @@ namespace brep
catch (const invalid_argument& e)
{
error << "failed to parse service data: " << e;
- return nullopt;
+ return make_pair (optional<string> (), false);
+ }
+
+ // Feel like this could potentially happen in case of an out of order
+ // notification (see above).
+ //
+ if (sd.completed)
+ {
+ // @@ Perhaps this should be a warning but let's try error for now (we
+ // essentially missed a build, which could have failed).
+ //
+ error << "built notification for completed check suite";
+ return make_pair (optional<string> (), false);
}
if (iat)
@@ -2925,46 +2912,204 @@ namespace brep
else
sd.check_runs.push_back (cr);
- if (bool c = completed)
+ // Determine of this check suite is completed.
+ //
+ sd.completed = find_if (sd.check_runs.begin (), sd.check_runs.end (),
+ [] (const check_run& scr)
+ {
+ return scr.state != build_state::built;
+ }) == sd.check_runs.end ();
+ }
+
+ return make_pair (optional<string> (sd.json ()), sd.completed);
+ };
+ }
+ catch (const std::exception& e)
+ {
+ NOTIFICATION_DIAG (log_writer);
+
+ string bid (gh_check_run_name (b)); // Full build id.
+
+ error << "check run " << bid << ": unhandled exception: " << e.what ();
+
+ return nullptr;
+ }
+
+ void ci_github::
+ build_completed (const string& /* tenant_id */,
+ const tenant_service& ts,
+ const diag_epilogue& log_writer) const noexcept
+ try
+ {
+ // NOTE: this function is noexcept and should not throw.
+
+ NOTIFICATION_DIAG (log_writer);
+
+ service_data sd;
+ try
+ {
+ sd = service_data (*ts.data);
+ }
+ catch (const invalid_argument& e)
+ {
+ error << "failed to parse service data: " << e;
+ return;
+ }
+
+ // This could have been reset by handle_check_run_rerequest().
+ //
+ if (!sd.completed)
+ return;
+
+ assert (!sd.check_runs.empty ());
+
+ // Here we need to update the state of the synthetic conclusion check run.
+ //
+ result_status result (result_status::success);
+
+ // Conclusion check run summary. Will include the success/warning/failure
+ // count breakdown.
+ //
+ string summary;
+ {
+ // The success/warning/failure counts.
+ //
+ // Note that the warning count will be included in the success or
+ // failure count (depending on the value of sd.warning_success).
+ //
+ size_t succ_count (0), warn_count (0), fail_count (0);
+
+ // Count a result_status under the appropriate category.
+ //
+ auto count = [&succ_count,
+ &warn_count,
+ &fail_count,
+ ws = sd.warning_success] (result_status rs)
+ {
+ switch (rs)
{
- // Note that this can be racy: while we calculated the completed
- // value based on the snapshot of the service data, it could have
- // been changed (e.g., by handle_check_run_rerequest()). So we
- // re-calculate it based on the check run states and only update if
- // it matches. Otherwise, we log an error.
- //
- for (const check_run& scr: sd.check_runs)
+ case result_status::success: ++succ_count; break;
+
+ case result_status::error:
+ case result_status::abort:
+ case result_status::abnormal: ++fail_count; break;
+
+ case result_status::warning:
{
- if (scr.state != build_state::built)
- {
- string sid (sd.repository_node_id + ':' + sd.report_sha);
+ ++warn_count;
- error << "tenant_service id " << sid
- << ": out of order built notification service data update; "
- << "check suite is no longer complete";
+ if (ws)
+ ++succ_count;
+ else
+ ++fail_count;
- c = false;
- break;
- }
+ break;
}
- if (c)
- sd.completed = true;
+ case result_status::skip:
+ case result_status::interrupt:
+ {
+ assert (false);
+ }
}
+ };
+
+ for (const check_run& cr: sd.check_runs)
+ {
+ assert (cr.state == build_state::built && cr.status);
+
+ result |= *cr.status;
+ count (*cr.status);
}
- return sd.json ();
- };
+ // Construct the conclusion check run summary.
+ //
+ ostringstream os;
+
+ // Note: the warning count has already been included in the success or
+ // failure count.
+ //
+ os << fail_count << " failed";
+ if (!sd.warning_success && warn_count != 0)
+ os << " (" << warn_count << " due to warnings)";
+
+ os << ", " << succ_count << " succeeded";
+ if (sd.warning_success && warn_count != 0)
+ os << " (" << warn_count << " with warnings)";
+
+ os << ", " << (succ_count + fail_count) << " total";
+
+ summary = os.str ();
+ }
+
+ // Get a new installation access token if the current one has expired
+ // (unlikely since we just returned from build_built()). Note also that we
+ // are not saving the new token in the service data.
+ //
+ const gh_installation_access_token* iat (nullptr);
+ optional<gh_installation_access_token> new_iat;
+
+ if (system_clock::now () > sd.installation_access.expires_at)
+ {
+ if (optional<string> jwt = generate_jwt (sd.app_id, trace, error))
+ {
+ new_iat = obtain_installation_access_token (sd.installation_id,
+ move (*jwt),
+ error);
+ if (new_iat)
+ iat = &*new_iat;
+ }
+ }
+ else
+ iat = &sd.installation_access;
+
+ // Note: we treat the failure to obtain the installation access token the
+ // same as the failure to notify GitHub.
+ //
+ if (iat != nullptr)
+ {
+ // Update the conclusion check run if all check runs are now built.
+ //
+ assert (sd.conclusion_node_id);
+
+ gq_built_result br (
+ make_built_result (result, sd.warning_success, move (summary)));
+
+ check_run cr;
+
+ // Set some fields for display purposes.
+ //
+ cr.node_id = *sd.conclusion_node_id;
+ cr.name = conclusion_check_run_name;
+
+ // Let unlikely invalid_argument propagate.
+ //
+ if (gq_update_check_run (error,
+ cr,
+ iat->token,
+ sd.repository_node_id,
+ *sd.conclusion_node_id,
+ build_state::built,
+ move (br)))
+ {
+ assert (cr.state == build_state::built);
+ l3 ([&]{trace << "updated conclusion check_run { " << cr << " }";});
+ }
+ else
+ {
+ // Nothing we can do here except log the error.
+ //
+ error << "tenant_service id " << ts.id
+ << ": unable to update conclusion check run "
+ << *sd.conclusion_node_id;
+ }
+ }
}
catch (const std::exception& e)
{
NOTIFICATION_DIAG (log_writer);
- string bid (gh_check_run_name (b)); // Full build id.
-
- error << "check run " << bid << ": unhandled exception: " << e.what();
-
- return nullptr;
+ error << "unhandled exception: " << e.what ();
}
string ci_github::
@@ -2972,10 +3117,11 @@ namespace brep
{
// This code is based on build_force_url() in mod/build.cxx.
//
- return options_->host () +
- "/@" + b.tenant +
+ return
+ options_->host () +
+ tenant_dir (options_->root (), b.tenant).string () +
"?builds=" + mime_url_encode (b.package_name.string ()) +
- "&pv=" + b.package_version.string () +
+ "&pv=" + mime_url_encode (b.package_version.string ()) +
"&tg=" + mime_url_encode (b.target.string ()) +
"&tc=" + mime_url_encode (b.target_config_name) +
"&pc=" + mime_url_encode (b.package_config_name) +
@@ -2983,6 +3129,15 @@ namespace brep
b.toolchain_version.string ();
}
+ string ci_github::
+ details_url (const string& t) const
+ {
+ return
+ options_->host () +
+ tenant_dir (options_->root (), t).string () +
+ "?builds";
+ }
+
static optional<build_id>
parse_details_url (const string& details_url)
try
@@ -2995,12 +3150,21 @@ namespace brep
// Extract the tenant from the URL path.
//
- // Example path: @d2586f57-21dc-40b7-beb2-6517ad7917dd
+ // Example paths:
+ //
+ // @d2586f57-21dc-40b7-beb2-6517ad7917dd (37 characters)
+ // <brep-root>/@d2586f57-21dc-40b7-beb2-6517ad7917dd
//
- if (!u.path || u.path->size () != 37 || (*u.path)[0] != '@')
+ if (!u.path)
return nullopt;
- r.package.tenant = u.path->substr (1);
+ {
+ size_t p (u.path->find ('@'));
+ if (p == string::npos || u.path->size () - p != 37)
+ return nullopt; // Tenant not found or too short.
+
+ r.package.tenant = u.path->substr (p + 1);
+ }
// Extract the rest of the build_id members from the URL query.
//
@@ -3043,7 +3207,7 @@ namespace brep
};
if (c (pn, "builds")) r.package.name = package_name (decval ());
- else if (c (pv, "pv")) r.package.version = make_version (rawval ());
+ else if (c (pv, "pv")) r.package.version = make_version (decval ());
else if (c (tg, "tg")) r.target = target_triplet (decval ());
else if (c (tc, "tc")) r.target_config_name = decval ();
else if (c (pc, "pc")) r.package_config_name = decval ();
@@ -3055,8 +3219,8 @@ namespace brep
// Note: parsing code based on mod/mod-builds.cxx.
//
- size_t p (v.find_first_of ('-'));
- if (p >= v.size () - 1)
+ size_t p (v.find ('-'));
+ if (p == string::npos || p >= v.size () - 1)
return nullopt; // Invalid format.
r.toolchain_name = v.substr (0, p);
diff --git a/mod/mod-ci-github.hxx b/mod/mod-ci-github.hxx
index 4fcfa7e..4cedc94 100644
--- a/mod/mod-ci-github.hxx
+++ b/mod/mod-ci-github.hxx
@@ -72,12 +72,18 @@ namespace brep
const build&,
const diag_epilogue& log_writer) const noexcept override;
- virtual function<optional<string> (const string&, const tenant_service&)>
+ virtual function<pair<optional<string>, bool> (const string&,
+ const tenant_service&)>
build_built (const string& tenant_id,
const tenant_service&,
const build&,
const diag_epilogue& log_writer) const noexcept override;
+ virtual void
+ build_completed (const string& tenant_id,
+ const tenant_service& ts,
+ const diag_epilogue& log_writer) const noexcept override;
+
private:
virtual void
init (cli::scanner&);
@@ -127,6 +133,11 @@ namespace brep
string
details_url (const build&) const;
+ // Build a check run details_url for a tenant.
+ //
+ string
+ details_url (const string& tenant) const;
+
optional<string>
generate_jwt (const string& app_id,
const basic_mark& trace,
@@ -145,6 +156,8 @@ namespace brep
shared_ptr<options::ci_github> options_;
tenant_service_map& tenant_service_map_;
+
+ string webhook_secret_;
};
}
diff --git a/mod/mod-ci.cxx b/mod/mod-ci.cxx
index 46fbf6a..85c00c6 100644
--- a/mod/mod-ci.cxx
+++ b/mod/mod-ci.cxx
@@ -105,17 +105,17 @@ init (scanner& s)
fail << "unable to read ci-form file '" << ci_form << "': " << e;
}
}
- }
#ifdef BREP_CI_TENANT_SERVICE_UNLOADED
- if (!options_->build_config_specified ())
- fail << "package building functionality must be enabled";
+ if (!options_->build_config_specified ())
+ fail << "package building functionality must be enabled";
- database_module::init (*options_, options_->build_db_retry ());
+ database_module::init (*options_, options_->build_db_retry ());
#endif
- if (options_->root ().empty ())
- options_->root (dir_path ("/"));
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+ }
}
bool brep::ci::
@@ -131,8 +131,6 @@ handle (request& rq, response& rs)
HANDLER_DIAG;
- const dir_path& root (options_->root ());
-
// We will respond with the manifest to the CI request submission protocol
// violations and with a plain text message on the internal errors. In the
// latter case we will always respond with the same neutral message for
@@ -180,6 +178,8 @@ handle (request& rq, response& rs)
if (!options_->ci_data_specified ())
return respond_manifest (404, "CI request submission disabled");
+ const dir_path& root (options_->root ());
+
// Parse the request form data and verify the submission size limit.
//
// Note that the submission may include the overrides upload that we don't
@@ -387,18 +387,19 @@ handle (request& rq, response& rs)
optional<start_result> r;
- if (optional<string> ref = create (error,
- warn,
- verb_ ? &trace : nullptr,
- *build_db_,
- tenant_service ("", "ci", rl.string ()),
- chrono::seconds (40),
- chrono::seconds (10)))
+ if (optional<pair<string, duplicate_tenant_result>> ref =
+ create (error,
+ warn,
+ verb_ ? &trace : nullptr,
+ *build_db_, retry_,
+ tenant_service ("", "ci", rl.string ()),
+ chrono::seconds (40),
+ chrono::seconds (10)))
{
string msg ("unloaded CI request is created: " +
- options_->host () + tenant_dir (root, *ref).string ());
+ options_->host () + tenant_dir (root, ref->first).string ());
- r = start_result {200, move (msg), move (*ref), {}};
+ r = start_result {200, move (msg), move (ref->first), {}};
}
#endif
@@ -495,8 +496,8 @@ build_building (const string& /*tenant_id*/,
};
}
-function<optional<string> (const string& tenant_id,
- const brep::tenant_service&)> brep::ci::
+function<pair<optional<string>, bool> (const string& tenant_id,
+ const brep::tenant_service&)> brep::ci::
build_built (const string& /*tenant_id*/,
const tenant_service&,
const build& b,
@@ -514,13 +515,16 @@ build_built (const string& /*tenant_id*/,
b.toolchain_name + '/' +
b.toolchain_version.string ());
- return ts.data ? *ts.data + ", " + s : s;
+ return make_pair (
+ optional<string> (ts.data ? *ts.data + ", " + s : s), false);
};
}
#ifdef BREP_CI_TENANT_SERVICE_UNLOADED
-function<optional<string> (const brep::tenant_service&)> brep::ci::
-build_unloaded (tenant_service&& ts,
+function<optional<string> (const string& tenant_id,
+ const brep::tenant_service&)> brep::ci::
+build_unloaded (const string& /* tenant_id */,
+ tenant_service&& ts,
const diag_epilogue& log_writer) const noexcept
{
NOTIFICATION_DIAG (log_writer);
@@ -532,7 +536,7 @@ build_unloaded (tenant_service&& ts,
repository_location rl (*ts.data);
if (!load (error, warn, verb_ ? &trace : nullptr,
- *build_db_,
+ *build_db_, retry_,
move (ts),
rl))
return nullptr; // The diagnostics is already issued.
@@ -545,7 +549,10 @@ build_unloaded (tenant_service&& ts,
return nullptr;
}
- return [] (const tenant_service& ts) {return "loaded " + *ts.data;};
+ return [] (const string& tenant_id, const tenant_service& ts)
+ {
+ return "loaded " + tenant_id + ' ' + *ts.data;
+ };
}
#endif
#endif
diff --git a/mod/mod-ci.hxx b/mod/mod-ci.hxx
index 132b5b0..54532e6 100644
--- a/mod/mod-ci.hxx
+++ b/mod/mod-ci.hxx
@@ -87,8 +87,8 @@ namespace brep
const build&,
const diag_epilogue& log_writer) const noexcept override;
- virtual function<optional<string> (const string& tenant_id,
- const tenant_service&)>
+ virtual function<pair<optional<string>, bool> (const string& tenant_id,
+ const tenant_service&)>
build_built (const string& tenant_id,
const tenant_service&,
const build&,
diff --git a/mod/mod-submit.cxx b/mod/mod-submit.cxx
index 5ee358a..6c767cb 100644
--- a/mod/mod-submit.cxx
+++ b/mod/mod-submit.cxx
@@ -93,10 +93,10 @@ init (scanner& s)
if (options_->submit_handler_specified () &&
options_->submit_handler ().relative ())
fail << "submit-handler path must be absolute";
- }
- if (options_->root ().empty ())
- options_->root (dir_path ("/"));
+ if (options_->root ().empty ())
+ options_->root (dir_path ("/"));
+ }
}
bool brep::submit::
@@ -109,8 +109,6 @@ handle (request& rq, response& rs)
HANDLER_DIAG;
- const dir_path& root (options_->root ());
-
// We will respond with the manifest to the submission protocol violations
// and with a plain text message on the internal errors. In the latter case
// we will always respond with the same neutral message for security reason,
@@ -163,6 +161,8 @@ handle (request& rq, response& rs)
if (!options_->submit_data_specified ())
return respond_manifest (404, "submission disabled");
+ const dir_path& root (options_->root ());
+
// Parse the request form data and verify the submission size limit.
//
// Note that if it is exceeded, then there are parameters and this is the
diff --git a/mod/module.cli b/mod/module.cli
index 1273bf4..ba2b986 100644
--- a/mod/module.cli
+++ b/mod/module.cli
@@ -850,12 +850,12 @@ namespace brep
// GitHub CI-specific options.
//
- string ci-github-app-webhook-secret
+ path ci-github-app-webhook-secret
{
- "<secret>",
+ "<path>",
"The GitHub App's configured webhook secret. If not set, then the
- GitHub CI service is disabled. Note: make sure to choose a strong
- (random) secret."
+ GitHub CI service is disabled. Note that the path must be absolute.
+ Note: make sure to choose a strong (random) secret."
}
std::map<string, dir_path> ci-github-app-id-private-key
@@ -863,7 +863,7 @@ namespace brep
"<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."
+ settings. Note that the paths must be absolute."
}
uint16_t ci-github-jwt-validity-period = 600
diff --git a/mod/tenant-service.cxx b/mod/tenant-service.cxx
new file mode 100644
index 0000000..2c1f3bc
--- /dev/null
+++ b/mod/tenant-service.cxx
@@ -0,0 +1,18 @@
+// file : mod/tenant-service.cxx -*- C++ -*-
+// license : MIT; see accompanying LICENSE file
+
+#include <mod/tenant-service.hxx>
+
+namespace brep
+{
+ void tenant_service_build_built::
+ build_completed (const string& /* tenant_id */,
+ const tenant_service&,
+ const diag_epilogue& /* log_writer */) const noexcept
+ {
+ // If this notification is requested, then this function needs to be
+ // overridden by the tenant service implementation.
+ //
+ assert (false);
+ }
+}
diff --git a/mod/tenant-service.hxx b/mod/tenant-service.hxx
index 5564a56..d909eaa 100644
--- a/mod/tenant-service.hxx
+++ b/mod/tenant-service.hxx
@@ -127,12 +127,20 @@ namespace brep
class tenant_service_build_built: public virtual tenant_service_base
{
public:
- virtual function<optional<string> (const string& tenant_id,
- const tenant_service&)>
+ // The second half of the pair signals whether to call the
+ // build_completed() notification.
+ //
+ virtual function<pair<optional<string>, bool> (const string& tenant_id,
+ const tenant_service&)>
build_built (const string& tenant_id,
const tenant_service&,
const build&,
const diag_epilogue& log_writer) const noexcept = 0;
+
+ virtual void
+ build_completed (const string& tenant_id,
+ const tenant_service&,
+ const diag_epilogue& log_writer) const noexcept;
};
// This notification is only made on unloaded CI requests created with the