diff --git a/config-prod.yml b/config-prod.yml index 1ba1929..7c83e45 100644 --- a/config-prod.yml +++ b/config-prod.yml @@ -6,6 +6,8 @@ db: password: some_pw github-app-id: "87729" +github-client-id: "Iv1.8f11845b87ff14f0" +github-client-secret: "fooop" github-app-key-file: "rakudocibot.2020-11-06.private-key.pem" projects: rakudo: @@ -28,6 +30,7 @@ projects: install-id: 20243470 hook-url: https://cibot.rakudo.org/ +jwt-secret: "N#xTe4>IGSWfH#M&MYXxI~#w9yj@X8X/g8-N{;n)5D>kFh~AaW" obs-user: some_user obs-password: "some_pw" diff --git a/lib/CITestSetManager.rakumod b/lib/CITestSetManager.rakumod index eddb878..b0af867 100644 --- a/lib/CITestSetManager.rakumod +++ b/lib/CITestSetManager.rakumod @@ -26,8 +26,9 @@ method process-worklist() is serial-dedup { for DB::Command.^all.grep(*.status == DB::COMMAND_NEW) -> $command { given $command.command { when DB::RE_TEST { + my $ts; if $command.pr { - my $ts = DB::CITestSet.^all.sort(-*.id).first( *.pr.number == $command.pr.number ); + $ts = DB::CITestSet.^all.sort(-*.id).first( *.pr.number == $command.pr.number ); unless $ts { error "No TestSet for the PR of the given command found: " ~ $command.id; $command.status = DB::COMMAND_DONE; @@ -37,21 +38,24 @@ method process-worklist() is serial-dedup { $command.test-set = $ts; $command.^save; + } + else { + $ts = $command.ts; + } - debug "CITestSetManager: Starting re-test for command: " ~ $command.id; + debug "CITestSetManager: Starting re-test for command: " ~ $command.id; - for $!test-set-listeners.keys { - $_.re-test-test-set($ts) - } + for $!test-set-listeners.keys { + $_.re-test-test-set($ts) + } - $ts.status = DB::WAITING_FOR_TEST_RESULTS; - $ts.^save; + $ts.status = DB::WAITING_FOR_TEST_RESULTS; + $ts.^save; - $command.status = DB::COMMAND_DONE; - $command.^save; + $command.status = DB::COMMAND_DONE; + $command.^save; - $_.command-accepted($command) for $!status-listeners.keys; - } + $_.command-accepted($command) for $!status-listeners.keys; } } } @@ -109,6 +113,14 @@ method add-test-set(:$test-set!, :$source-spec!) { self.process-worklist; } +method re-test(:$test-set!) { + DB::Command.^create: + :command(DB::RE_TEST), + :$test-set, + ; + self.process-worklist; +} + method add-tests(*@tests) { $_.tests-queued(@tests) for $!status-listeners.keys; } diff --git a/lib/Config.rakumod b/lib/Config.rakumod index 804670a..d30fbb5 100644 --- a/lib/Config.rakumod +++ b/lib/Config.rakumod @@ -1,5 +1,11 @@ use YAMLish; +enum Project < + MOAR + NQP + RAKUDO +>; + class ConfigProject { has $.project; has $.repo; @@ -24,16 +30,27 @@ class ConfigProjects { has $.rakudo is required; has $.nqp is required; has $.moar is required; + + method for-id(Project $id) { + given $id { + when RAKUDO { return $!rakudo } + when NQP { return $!nqp } + when MOAR { return $!moar } + } + } } class Config { has %.db; has $.github-app-id; + has $.github-client-id; + has $.github-client-secret; has $.github-app-key-file; has $.projects; has $.hook-url; + has $.jwt-secret; has $.obs-user; has $.obs-password; @@ -70,15 +87,18 @@ class Config { Config.new: db => %config, - github-app-id => %config, - github-app-key-file => %config, - projects => ConfigProjects.new( + github-app-id => %config, + github-client-id => %config, + github-client-secret => %config, + github-app-key-file => %config, + projects => ConfigProjects.new( rakudo => ConfigProject.from-config(%config), nqp => ConfigProject.from-config(%config), moar => ConfigProject.from-config(%config), ), hook-url => %config, + jwt-secret => %config, obs-user => %config, obs-password => %config, diff --git a/lib/DB.rakumod b/lib/DB.rakumod index bbd3923..260c4f8 100644 --- a/lib/DB.rakumod +++ b/lib/DB.rakumod @@ -45,12 +45,6 @@ enum CIPlatformTestSetStatus < PLATFORM_DONE >; -enum Project < - MOAR - NQP - RAKUDO ->; - enum CITestSetStatus ( "NEW", # As created by the GitHubCITestRequester. SourceSpec has not been created yet. "UNPROCESSED", # SourceSpec has been created. Now to be processed by the CITestSetManager @@ -145,7 +139,7 @@ model CITestSet is rw is table { # Responsibility of the GitHubCITestRequester has DB::GitHubEventType $.event-type is column; - has DB::Project $.project is column; + has Project $.project is column; has Str $.git-url is column; has Str $.commit-sha is column; has Str $.user-url is column; @@ -205,7 +199,7 @@ model CITestSet is rw is table { model GitHubPullState is rw is table { has UInt $.id is serial; has DateTime $.creation is column .= now; - has DB::Project $.project is column; + has Project $.project is column; has Str $.last-default-branch-cursor is column{ :nullable }; has Str $.last-pr-cursor is column{ :nullable }; @@ -215,7 +209,7 @@ model GitHubPR is rw is table { has UInt $.id is serial; has DateTime $.creation is column .= now; has UInt $.number is column; - has DB::Project $.project is column; + has Project $.project is column; has Str $.base-url is column; has Str $.head-url is column; has Str $.head-branch is column; @@ -240,7 +234,7 @@ model Command is rw is table { has Str $.comment-id is column; has Str $.comment-url is column; - has DB::CommandStatus $.status is column; + has DB::CommandStatus $.status is column = DB::COMMAND_NEW; has DB::CommandEnum $.command is column; # If it's a re-test command, the test set we should re test diff --git a/lib/GitHubCITestRequester.rakumod b/lib/GitHubCITestRequester.rakumod index f228d4b..9a3ab89 100644 --- a/lib/GitHubCITestRequester.rakumod +++ b/lib/GitHubCITestRequester.rakumod @@ -156,7 +156,6 @@ method !process-pr-comment-task(PRCommentTask $comment) { comment-id => $comment.id, comment-url => $comment.user-url, :$command, - status => DB::COMMAND_NEW, ; if $command == DB::MERGE_ON_SUCCESS { $!github-interface.add-issue-comment: @@ -199,9 +198,9 @@ method !process-commit-task(CommitTask $commit) { method poll-for-changes() is serial-dedup { trace "GitHub: Polling for changes"; - for DB::RAKUDO, config.projects.rakudo, - DB::NQP, config.projects.nqp, - DB::MOAR, config.projects.moar -> $db-project, $project { + for RAKUDO, config.projects.rakudo, + NQP, config.projects.nqp, + MOAR, config.projects.moar -> $db-project, $project { my $state = DB::GitHubPullState.^all.first({ $_.project == $db-project }) // DB::GitHubPullState.^create(project => $db-project); # PRs @@ -338,12 +337,12 @@ method !determine-source-spec(:$project!, :$git-url!, :$commit-sha!, :$pr --> So my $did-things = False; with $pr { my %head-data = self!github-url-to-project-repo($pr.head-url); - if $project == DB::RAKUDO && %head-data eq config.projects.rakudo.repo || - $project == DB::NQP && %head-data eq config.projects.nqp.repo || - $project == DB::MOAR && %head-data eq config.projects.moar.repo { - my $uses-core-repos = %head-data eq ($project == DB::RAKUDO ?? config.projects.rakudo.slug !! - $project == DB::NQP ?? config.projects.nqp.slug !! - $project == DB::MOAR ?? config.projects.moar.slug !! + if $project == RAKUDO && %head-data eq config.projects.rakudo.repo || + $project == NQP && %head-data eq config.projects.nqp.repo || + $project == MOAR && %head-data eq config.projects.moar.repo { + my $uses-core-repos = %head-data eq ($project == RAKUDO ?? config.projects.rakudo.slug !! + $project == NQP ?? config.projects.nqp.slug !! + $project == MOAR ?? config.projects.moar.slug !! "should never happen"); my ($r-proj, $r-repo, $n-proj, $n-repo, $m-proj, $m-repo) = do if $uses-core-repos { config.projects.rakudo.project, config.projects.rakudo.repo, @@ -358,9 +357,9 @@ method !determine-source-spec(:$project!, :$git-url!, :$commit-sha!, :$pr --> So my $branch = $pr.head-branch; - for DB::RAKUDO, $rakudo-git-url, $rakudo-commit-sha, $rakudo-fetch-ref, $r-proj, $r-repo, - DB::NQP, $nqp-git-url, $nqp-commit-sha, $nqp-fetch-ref, $n-proj, $n-repo, - DB::MOAR, $moar-git-url, $moar-commit-sha, $moar-fetch-ref, $m-proj, $m-repo + for RAKUDO, $rakudo-git-url, $rakudo-commit-sha, $rakudo-fetch-ref, $r-proj, $r-repo, + NQP, $nqp-git-url, $nqp-commit-sha, $nqp-fetch-ref, $n-proj, $n-repo, + MOAR, $moar-git-url, $moar-commit-sha, $moar-fetch-ref, $m-proj, $m-repo -> $cur-proj, $out-url is rw, $out-commit-sha is rw, $out-fetch-ref is rw, $gh-project, $repo { if $cur-proj == $project { $out-url = $pr.base-url; @@ -388,17 +387,17 @@ method !determine-source-spec(:$project!, :$git-url!, :$commit-sha!, :$pr --> So if !$did-things { with $pr { given $project { - when DB::RAKUDO { + when RAKUDO { $rakudo-git-url = $pr.base-url; $rakudo-commit-sha = $commit-sha; $rakudo-fetch-ref = "pull/{$pr.number}/head"; } - when DB::NQP { + when NQP { $nqp-git-url = $pr.base-url; $nqp-commit-sha = $commit-sha; $nqp-fetch-ref = "pull/{$pr.number}/head"; } - when DB::MOAR { + when MOAR { $moar-git-url = $pr.base-url; $moar-commit-sha = $commit-sha; $moar-fetch-ref = "pull/{$pr.number}/head"; @@ -407,15 +406,15 @@ method !determine-source-spec(:$project!, :$git-url!, :$commit-sha!, :$pr --> So } else { given $project { - when DB::RAKUDO { + when RAKUDO { $rakudo-git-url = $git-url; $rakudo-commit-sha = $commit-sha; } - when DB::NQP { + when NQP { $nqp-git-url = $git-url; $nqp-commit-sha = $commit-sha; } - when DB::MOAR { + when MOAR { $moar-git-url = $git-url; $moar-commit-sha = $commit-sha; } @@ -467,18 +466,18 @@ method !command-to-enum($text is copy) { method !repo-to-project-repo($repo) { given $repo.lc { - when "rakudo" { { project => config.projects.rakudo.project, repo => config.projects.rakudo.repo, db-project => DB::RAKUDO } } - when "nqp" { { project => config.projects.nqp.project, repo => config.projects.nqp.repo, db-project => DB::NQP } } - when "moarvm" { { project => config.projects.moar.project, repo => config.projects.moar.repo, db-project => DB::MOAR } } + when "rakudo" { { project => config.projects.rakudo.project, repo => config.projects.rakudo.repo, db-project => RAKUDO } } + when "nqp" { { project => config.projects.nqp.project, repo => config.projects.nqp.repo, db-project => NQP } } + when "moarvm" { { project => config.projects.moar.project, repo => config.projects.moar.repo, db-project => MOAR } } default { die "unknown project"; } } } method !db-project-to-project-repo($db-project) { my $repo = do given $db-project { - when DB::RAKUDO { "rakudo" } - when DB::NQP { "nqp" } - when DB::MOAR { "moarvm" } + when RAKUDO { "rakudo" } + when NQP { "nqp" } + when MOAR { "moarvm" } } self!repo-to-project-repo($repo); } diff --git a/lib/GitHubInterface.rakumod b/lib/GitHubInterface.rakumod index e3b2876..6ceec1e 100644 --- a/lib/GitHubInterface.rakumod +++ b/lib/GitHubInterface.rakumod @@ -3,18 +3,51 @@ unit class GitHubInterface; use Config; use Log::Async; use WebService::GitHub::AppAuth; +use WebService::GitHub::OAuth; use WebService::GitHub; use GitHubCITestRequester; constant $gql-endpoint = "https://api.github.com/graphql"; has WebService::GitHub::AppAuth $!gh-auth; +has WebService::GitHub::OAuth $!gh-oauth; +has $!redirect-url; +has $!client-id; has WebService::GitHub $!gh; has GitHubCITestRequester $.processor is required; -submethod TWEAK(:$app-id!, :$pem!) { +method oauth-step-one-url($state) { + { + url => 'https://github.com/login/oauth/authorize', + query-params => [ + client_id => $!client-id, + redirect_uri => $!redirect-url, + state => $state, + allow_signups => 'false', + ], + } +} + +method oauth-code-to-token($code, $state) { + $!gh-oauth.step-two($state, $code, $state); +} + +method oauth-user-name($token) { + my WebService::GitHub $gh-oauth-client .= new: :pat($token); + my %user = self!validate($gh-oauth-client.users.get-authenticated().data); + return %user; +} + +submethod TWEAK(:$app-id!, :$!client-id!, :$client-secret!, :$pem!, :$!redirect-url!) { $!gh-auth .= new: :$app-id, - :$pem + :$pem, + ; + + $!gh-oauth .= new: + :$!client-id, + :$client-secret, + :$!redirect-url, + :$pem, ; $!gh .= new: diff --git a/lib/OBSInterface.rakumod b/lib/OBSInterface.rakumod index 00ecb45..ed3c9d9 100644 --- a/lib/OBSInterface.rakumod +++ b/lib/OBSInterface.rakumod @@ -28,7 +28,7 @@ submethod TWEAK() { headers => [ Accept => 'application/xml', Authorization => $!auth-str, - ]; + ] #`[ tls => { ssl-key-log-file => 'ssl-key-log-file', diff --git a/lib/RakudoCIBot.rakumod b/lib/RakudoCIBot.rakumod index c4f5c06..dd00ede 100644 --- a/lib/RakudoCIBot.rakumod +++ b/lib/RakudoCIBot.rakumod @@ -61,8 +61,11 @@ submethod TWEAK() { ; $!github-interface .= new: app-id => config.github-app-id, + client-id => config.github-client-id, + client-secret => config.github-client-secret, pem => $gh-pem, processor => $!requester, + redirect-url => config.hook-url ~ "gh-oauth-callback", ; $!requester.github-interface = $!github-interface; $!testset-manager.register-status-listener($!requester); @@ -127,7 +130,7 @@ method start() { http => <1.1>, host => config.web-host, port => config.web-port, - application => routes($!source-archive-creator, $!github-interface, $!obs), + application => routes($!testset-manager, $!source-archive-creator, $!github-interface, $!obs), after => [ Cro::HTTP::Log::File.new(logs => $*OUT, errors => $*ERR) ] diff --git a/lib/Routes.rakumod b/lib/Routes.rakumod index a67567a..db21bdc 100644 --- a/lib/Routes.rakumod +++ b/lib/Routes.rakumod @@ -1,16 +1,25 @@ use Cro::HTTP::Router; +use Cro::HTTP::Auth::WebToken::FromCookie; use Cro::WebApp::Template; +use Cro::Uri :encode-percents, :decode-percents; +use Red::Operators:api<2>; +use JSON::JWT; use Routes::home; use Routes::test; use Routes::testset; use Routes::source; use Routes::GitHubHook; +use Config; use SourceArchiveCreator; use GitHubInterface; +use CITestSetManager; use OBS; -sub routes(SourceArchiveCreator $sac, GitHubInterface $github-interface, OBS $obs) is export { +constant $jwt-gh-cookie-name = "jwt-gh-login"; +class JWT does Cro::HTTP::Auth::WebToken::FromCookie[$jwt-gh-cookie-name] {} + +sub routes(CITestSetManager $tsm, SourceArchiveCreator $sac, GitHubInterface $github-interface, OBS $obs) is export { template-location 'resources/templates/'; route { resources-from %?RESOURCES; @@ -29,9 +38,69 @@ sub routes(SourceArchiveCreator $sac, GitHubInterface $github-interface, OBS $ob resource "static/js", @path; } - include home-routes; - include test-routes; - include testset-routes($sac); + before JWT.new(secret => config.jwt-secret); + + template-part "login-part", -> Cro::HTTP::Auth $session { + \( + logged-in => True, + name => $session.token, + logout-url => "/logout", + ) + } + + template-part "login-part", -> { + \( + logged-in => False, + name => "", + logout-url => "", + ) + } + + sub gen-login-data($origin) { + my $url-data = $github-interface.oauth-step-one-url(encode-percents($origin)); + return $url-data; + } + + post -> "logout" { + set-cookie $jwt-gh-cookie-name, "", Max-Age => 0; + redirect :see-other, "/"; + } + + get -> "gh-oauth-callback", :$code, :$state { + # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app#using-the-web-application-flow-to-generate-a-user-access-token + + # 1. take code and generate an access-token + my $gh-token = $github-interface.oauth-code-to-token($code, $state); + my $gh-user = $github-interface.oauth-user-name($gh-token); + + # 2. stuff it into a web token saved in a cookie + my $time = DateTime.new(now).later(days => 7).posix(); + my %data = :token($gh-token), :username($gh-user), :exp($time); + my $token = JSON::JWT.encode(%data, :secret(config.jwt-secret), :alg('HS256')); + set-cookie $jwt-gh-cookie-name, $token; + + # 3. Extract the originating URL from $state and forward there + redirect decode-percents($state); + } + + post -> Cro::HTTP::Auth $session, "retest", $testset-id { + my $gh-token = $session.token; + my $gh-user = $session.token; + + my $ts = DB::CITestSet.^load: :id($testset-id); + my $conf = config.projects.for-id($ts.pr.project); + if $github-interface.can-user-merge-repo(owner => $conf.project, repo => $conf.repo, username => $gh-user) { + $tsm.re-test($ts); + created "/testset/$testset-id"; + } + else { + forbidden; + } + } + + include home-routes(&gen-login-data); + include test-routes(&gen-login-data); + include testset-routes($sac, &gen-login-data); include source-routes($sac); include github-hook-routes($github-interface); } diff --git a/lib/Routes/home.rakumod b/lib/Routes/home.rakumod index f288469..f13a977 100644 --- a/lib/Routes/home.rakumod +++ b/lib/Routes/home.rakumod @@ -5,10 +5,11 @@ use Cro::HTTP::Router; use Cro::WebApp::Template; use Red::Operators:api<2>; -sub home-routes() is export { +sub home-routes(&gen-login-data) is export { route { get -> { - my %data = test-sets => []; + my %data = login-data => gen-login-data("/"), + test-sets => []; for DB::CITestSet.^all.sort(-*.id).head(20) { %data.push: %( id => .id, diff --git a/lib/Routes/test.rakumod b/lib/Routes/test.rakumod index a44e6d0..6933614 100644 --- a/lib/Routes/test.rakumod +++ b/lib/Routes/test.rakumod @@ -4,11 +4,12 @@ use Cro::HTTP::Router; use Cro::WebApp::Template; use Red::Operators:api<2>; -sub test-routes() is export { +sub test-routes(&gen-login-data) is export { route { get -> "test", UInt $id { with DB::CITest.^load($id) { my %data = + login-url => gen-login-data("/test/$id"), id => .id, name => .name, backend => .platform-test-set.platform, diff --git a/lib/Routes/testset.rakumod b/lib/Routes/testset.rakumod index 28a5a90..6b42101 100644 --- a/lib/Routes/testset.rakumod +++ b/lib/Routes/testset.rakumod @@ -5,7 +5,7 @@ use Cro::HTTP::Router; use Cro::WebApp::Template; use Red::Operators:api<2>; -sub testset-routes($sac) is export { +sub testset-routes($sac, &gen-login-data) is export { route { get -> "testset", UInt $id { with DB::CITestSet.^load($id) { @@ -28,6 +28,7 @@ sub testset-routes($sac) is export { }; } my %data = + login-url => gen-login-data("/testset/$id"), id => .id, created-at => .creation, project => .project, diff --git a/resources/templates/home.crotmp b/resources/templates/home.crotmp index 54ff845..d21937c 100644 --- a/resources/templates/home.crotmp +++ b/resources/templates/home.crotmp @@ -1,5 +1,5 @@ <:use 'page.crotmp'> -<|page('Home')> +<|page('Home', .login-data)>

Rakudo CI Bot

Recent test runs

diff --git a/resources/templates/login.crotmp b/resources/templates/login.crotmp new file mode 100644 index 0000000..d22d661 --- /dev/null +++ b/resources/templates/login.crotmp @@ -0,0 +1,18 @@ +<:sub login($login-data)> +<:part login-part(:$logged-in, :$name, :$logout-url)> + + <$name> +
+ +
+ + +
+ <@$login-data.query-params> + + + +
+ + + diff --git a/resources/templates/page.crotmp b/resources/templates/page.crotmp index 1f2aa8d..03c8fed 100644 --- a/resources/templates/page.crotmp +++ b/resources/templates/page.crotmp @@ -1,4 +1,5 @@ -<:macro page($title)> +<:use 'login.crotmp'> +<:macro page($title, $login-data)> @@ -21,6 +22,7 @@ +<&login($login-data)> <:body> diff --git a/resources/templates/test.crotmp b/resources/templates/test.crotmp index 56ab5c6..4fde28d 100644 --- a/resources/templates/test.crotmp +++ b/resources/templates/test.crotmp @@ -1,5 +1,5 @@ <:use 'page.crotmp'> -<|page('Test')> +<|page('Test', .login-url)>

Test <.id>

    diff --git a/resources/templates/testset.crotmp b/resources/templates/testset.crotmp index 43fc853..0d044dc 100644 --- a/resources/templates/testset.crotmp +++ b/resources/templates/testset.crotmp @@ -1,5 +1,5 @@ <:use 'page.crotmp'> -<|page('Test Set')> +<|page('Test Set', .login-url)>

    Test Set <.id>

    diff --git a/t/CITestSetManager.rakutest b/t/CITestSetManager.rakutest index 635c6e7..2eef688 100644 --- a/t/CITestSetManager.rakutest +++ b/t/CITestSetManager.rakutest @@ -88,7 +88,7 @@ moar-commit-sha => '0123456789012345678901234567890123456789', ### Adding a new test set my $test-set = DB::CITestSet.^create: event-type => DB::MAIN_BRANCH, - project => DB::RAKUDO, + project => RAKUDO, git-url => "https://github.com/rakudo/rakudo.git", user-url => "https://github.com/rakudo/rakudo/commit/0123456789012345678901234567890123456789", commit-sha => '0123456789012345678901234567890123456789', diff --git a/t/GitHubInterface.rakutest b/t/GitHubInterface.rakutest index 0326994..88c3d36 100644 --- a/t/GitHubInterface.rakutest +++ b/t/GitHubInterface.rakutest @@ -12,7 +12,10 @@ my $data-dir = $*PROGRAM.parent.add('data'); my $processor = mocked(GitHubCITestRequester); -my GitHubInterface $parser .= new(:app-id(123456), :$processor, :pem($data-dir.add("dummy.pem").slurp)); +my GitHubInterface $parser .= new(:app-id(123456), :client-id('Iv1.1234567890abcdef'), + :client-secret('some-secret'), :redirect-url('https://some-example.org/gh-oauth-callback'), + :$processor, :pem($data-dir.add("dummy.pem").slurp)); + $parser.parse-hook-request('pull_request', from-json $data-dir.add("webhook-github-create-pull.body").slurp); check-mock($processor, diff --git a/t/OBS.rakutest b/t/OBS.rakutest index 8953ff2..d7d4dd4 100644 --- a/t/OBS.rakutest +++ b/t/OBS.rakutest @@ -124,7 +124,7 @@ my OBS $obs .= new: ### Testing starts here my $testset = DB::CITestSet.^create: event-type => DB::MAIN_BRANCH, - project => DB::RAKUDO, + project => RAKUDO, git-url => "https://github.com/rakudo/rakudo.git", commit-sha => '0123456789012345678901234567890123456789', user-url => "https://github.com/rakudo/rakudo/commit/0123456789012345678901234567890123456789", diff --git a/t/OBS_retest.rakutest b/t/OBS_retest.rakutest index d7234cc..ebc933a 100644 --- a/t/OBS_retest.rakutest +++ b/t/OBS_retest.rakutest @@ -145,7 +145,7 @@ my OBS $obs .= new: ### Testing starts here my $testset = DB::CITestSet.^create: event-type => DB::MAIN_BRANCH, - project => DB::RAKUDO, + project => RAKUDO, git-url => "https://github.com/rakudo/rakudo.git", commit-sha => '0123456789012345678901234567890123456789', user-url => "https://github.com/rakudo/rakudo/commit/0123456789012345678901234567890123456789",