diff --git a/config/prod_sys.config.src b/config/prod_sys.config.src index 19f3e97..fdfd00d 100644 --- a/config/prod_sys.config.src +++ b/config/prod_sys.config.src @@ -1,11 +1,14 @@ [ {kernel, [ - {logger_level, debug}, + {logger_level, info}, {logger, [ {handler, default, logger_std_h, #{ formatter => {nova_jsonlogger, #{ + schema => ecs, map_depth => 3, - term_depth => 50 + term_depth => 50, + max_term_size => 8192, + max_string_length => 8192 }} }} ]} diff --git a/rebar.config b/rebar.config index 0967e39..238cb45 100644 --- a/rebar.config +++ b/rebar.config @@ -3,7 +3,9 @@ {shell, [{config, "./config/dev_sys.config.src"}]}. {deps, [ - nova, + %% TODO: swap to a tag once novaframework/nova#385 (jsonlogger tier1) + %% lands and a release is cut. Pinned to the PR's fork branch for now. + {nova, {git, "https://github.com/Taure/nova.git", {branch, "feat/jsonlogger-tier1"}}}, {gakudan, {git, "https://github.com/Taure/gakudan.git", {tag, "v0.1.3"}}}, {gakudan_tickets, {git, "https://github.com/Taure/gakudan_tickets.git", {tag, "v0.1.0"}}}, {gakudan_tickets_github, diff --git a/rebar.lock b/rebar.lock index 3398dd7..8ab104a 100644 --- a/rebar.lock +++ b/rebar.lock @@ -21,7 +21,10 @@ {ref,"d17c09246b612328121946197ced9df3caf3a31a"}}, 0}, {<<"jhn_stdlib">>,{pkg,<<"jhn_stdlib">>,<<"5.11.2">>},1}, - {<<"nova">>,{pkg,<<"nova">>,<<"0.14.3">>},0}, + {<<"nova">>, + {git,"https://github.com/Taure/nova.git", + {ref,"6520435b03cb8de5a4be52fd844aef41164ae068"}}, + 0}, {<<"prometheus">>,{pkg,<<"prometheus">>,<<"6.1.2">>},1}, {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.2.0">>},1}, {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.15">>},2}, @@ -37,7 +40,6 @@ {<<"ddskerl">>, <<"BB97B90EEF1B5906520CEBDD38DA29B18A770B5567F6297927F53566EAE9BEBB">>}, {<<"erlydtl">>, <<"964B2DC84F8C17ACFAA69C59BA129EF26AC45D2BA898C3C6AD9B5BDC8BA13CED">>}, {<<"jhn_stdlib">>, <<"785074F3CA368EAA8E9AF1592BC19AE9EF1F7AF30B2CD6456A6083173A8F5CCB">>}, - {<<"nova">>, <<"2F08F11162A871CB1AA361BFEFE66D945F3284243F7EA6582AD8CA709BAFE2F3">>}, {<<"prometheus">>, <<"E5C3C567CD8B0994425920763405635211183C15BDC47D0CD524807DDE20CA1D">>}, {<<"prometheus_cowboy">>, <<"526F75D9850A9125496F78BCEECCA0F237BC7B403C976D44508543AE5967DAD9">>}, {<<"prometheus_httpd">>, <<"8F767D819A5D36275EAB9264AFF40D87279151646776069BF69FBDBBD562BD75">>}, @@ -52,7 +54,6 @@ {<<"ddskerl">>, <<"4E0F6047C6A002CE38F6EA155276DD918CD635FD0A5EDB9E0B46AEB6F7AFF2C2">>}, {<<"erlydtl">>, <<"D80EC044CD8F58809C19D29AC5605BE09E955040911B644505E31E9DD8143431">>}, {<<"jhn_stdlib">>, <<"2329CD16DEE46704AAB6184D09508E59DBA31C4D3255271DBB7D34D115ECA508">>}, - {<<"nova">>, <<"D35D0CEF073749958D2E7E446DED07354F5A3A6B96A31009A2E89D382BE72EF4">>}, {<<"prometheus">>, <<"109662001328112B860DE112D49B575310BE8FF33FE04BF0482EEC4B1E5A1278">>}, {<<"prometheus_cowboy">>, <<"2C7EB12F4B970D91E3B47BAAD0F138F6ADC34E53EEB0AE18068FF0AFAB441B24">>}, {<<"prometheus_httpd">>, <<"67736D000745184D5013C58A63E947821AB90CB9320BC2E6AE5D3061C6FFE039">>}, diff --git a/src/controllers/triagebot_webhook_controller.erl b/src/controllers/triagebot_webhook_controller.erl index 0b232a7..01481b4 100644 --- a/src/controllers/triagebot_webhook_controller.erl +++ b/src/controllers/triagebot_webhook_controller.erl @@ -28,45 +28,42 @@ github_event(Req) -> EventType = cowboy_req:header(~"x-github-event", Req1, ~""), Delivery = cowboy_req:header(~"x-github-delivery", Req1, ~""), Secret = triagebot_config:webhook_secret(), - - %% Echo to stdout via io:format so we are immune to whatever the logger - %% formatter might be dropping. This shows up in Clever Cloud's log - %% stream unconditionally. - io:format( - "[triagebot] webhook hit: delivery=~s event=~s sig_len=~p body_len=~p secret_len=~p~n", - [Delivery, EventType, byte_size(Sig), byte_size(Body), byte_size(Secret)] - ), - Valid = try gakudan_tickets_github:verify_signature(Secret, Sig, Body) catch - Class:Reason -> - io:format( - "[triagebot] verify_signature CRASHED: ~p:~p sig_len=~p body_len=~p secret_len=~p~n", - [Class, Reason, byte_size(Sig), byte_size(Body), byte_size(Secret)] - ), + Class:Reason:Stack -> + ?LOG_ERROR(#{ + event => verify_signature_crashed, + class => Class, + reason => Reason, + stacktrace => Stack, + delivery => Delivery, + github_event => EventType, + sig_byte_size => byte_size(Sig), + secret_byte_size => byte_size(Secret), + body_byte_size => byte_size(Body) + }), false end, case Valid of false -> - Expected = expected_signature_str(Secret, Body), - io:format( - "[triagebot] webhook 401: got_sig=~s expected_sig=~s body_fp=~s secret_fp=~s~n", - [ - Sig, - Expected, - fingerprint(Body), - fingerprint(Secret) - ] - ), + ?LOG_WARNING(#{ + event => webhook_signature_invalid, + delivery => Delivery, + github_event => EventType, + secret_byte_size => byte_size(Secret), + secret_fingerprint => fingerprint(Secret), + body_byte_size => byte_size(Body), + body_fingerprint => fingerprint(Body) + }), {status, 401}; true -> handle_verified(EventType, Body, Delivery) end. %% First 8 hex chars of SHA-256(Bin). Lets us compare two binaries for -%% equality without logging the underlying bytes. +%% equality without leaking the underlying bytes. fingerprint(B) when is_binary(B) -> H = crypto:hash(sha256, B), binary:part(hex(H), 0, 8). @@ -74,10 +71,6 @@ fingerprint(B) when is_binary(B) -> hex(Bin) -> iolist_to_binary([io_lib:format("~2.16.0b", [B]) || <> <= Bin]). -expected_signature_str(Secret, Body) when is_binary(Secret), is_binary(Body) -> - Hash = crypto:mac(hmac, sha256, Secret, Body), - <<"sha256=", (hex(Hash))/binary>>. - handle_verified(EventType, Body, Delivery) -> case gakudan_tickets_github:parse_webhook(EventType, Body) of {ok, {Event, Ticket}} -> diff --git a/src/triagebot_runner.erl b/src/triagebot_runner.erl index d65ba7d..63a11f8 100644 --- a/src/triagebot_runner.erl +++ b/src/triagebot_runner.erl @@ -28,6 +28,7 @@ and protects against the bot looping on its own label updates. ]). -define(TRIAGED_LABEL, ~"triaged"). +-define(CLAUDE_TRY_LABEL, ~"claude-try"). -spec dispatch(atom(), gakudan_tickets:ticket()) -> ok. dispatch(Event, Ticket) when Event =:= issue_opened; Event =:= issue_labeled -> @@ -100,6 +101,7 @@ apply_triage_output(Entries, Ticket, Source) -> Labels = extract_proposed_labels(Entries), _ = maybe_post_comment(Source, Id, Comment), _ = maybe_apply_labels(Source, Id, Labels), + _ = maybe_request_claude_implementation(Source, Id, Labels), ok. maybe_post_comment(_Source, _Id, <<>>) -> @@ -123,6 +125,32 @@ maybe_apply_labels(Source, Id, Labels) -> ?LOG_ERROR(#{event => apply_labels_failed, reason => Reason, ticket_id => Id}) end. +%% If the label_proposer asked for `claude-try`, leave a follow-up +%% comment summoning the claude-code-action workflow in the same repo. +%% The action listens on issues.labeled too, but posting an explicit +%% mention makes the chain visible in the issue thread. +maybe_request_claude_implementation(Source, Id, Labels) -> + case lists:member(?CLAUDE_TRY_LABEL, Labels) of + false -> + ok; + true -> + Body = + ~"@claude please implement this issue per the triage above. Open a PR closing this issue when done.", + case gakudan_tickets_github:post_comment(Source, Id, Body) of + ok -> + ?LOG_INFO(#{ + event => claude_implementation_requested, + ticket_id => Id + }); + {error, Reason} -> + ?LOG_ERROR(#{ + event => claude_request_comment_failed, + reason => Reason, + ticket_id => Id + }) + end + end. + format_input(Ticket) -> Id = maps:get(id, Ticket), Title = maps:get(title, Ticket, ~""),