chat_notifier 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -7
- data/README.md +17 -9
- data/lib/chat_notifier/chatter/slack.rb +62 -0
- data/lib/chat_notifier/chatter.rb +5 -5
- data/lib/chat_notifier/failure_groups.rb +63 -0
- data/lib/chat_notifier/messenger.rb +1 -1
- data/lib/chat_notifier/version.rb +1 -1
- data/lib/chat_notifier.rb +23 -18
- data/lib/minitest/chat_notifier_plugin.rb +2 -2
- metadata +20 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62958f54e981fe03f20e8e7b954b18b0cc7f404542698d58db36a3fd7911b3ed
|
|
4
|
+
data.tar.gz: 42760f3c980906d660c601a984719ea551d137e6db064545ef9fcc446442a6d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c37610777e3d4cc791791339e8b39ea7cacc386fb4032290a29040c0ef0184ee87d0d5ae26d3d072b16e9ed85d6abbd668e37b46adbaff98faeab1aff2a9396
|
|
7
|
+
data.tar.gz: d70529078358faeb2d419bae2951958b0ca94fdcaec00bfe5d820092dd4e366e784b5825ea1f0541eb041d91758427265256f30efd1f487e9427b6022e19c382
|
data/CHANGELOG.md
CHANGED
|
@@ -5,15 +5,23 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.3.0] - 2026-07-24
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Threaded Slack failure notifications via a Slack bot token (NOTIFY_SLACK_BOT_TOKEN), grouping failing files into batched thread replies (e453cd0)
|
|
13
|
+
|
|
14
|
+
## [0.2.5] - 2026-02-02
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Dependabot configuration with weekly updates
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
- Moved ENV variable setting `NOTIFY_APP_NAME` into the test helper
|
|
23
|
+
- Refactored method signatures for easier testing
|
|
16
24
|
|
|
17
|
-
###
|
|
25
|
+
### Removed
|
|
18
26
|
|
|
19
|
-
-
|
|
27
|
+
- OpenStruct dependency
|
data/README.md
CHANGED
|
@@ -51,6 +51,22 @@ If you are _not_ using Rails, you will need to add this ENV variable:
|
|
|
51
51
|
NOTIFY_APP_NAME
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
### Threaded failure notifications (optional)
|
|
55
|
+
|
|
56
|
+
Incoming webhooks post a single message. If you instead provide a Slack **bot
|
|
57
|
+
token**, ChatNotifier uses the Slack Web API and threads failures: it posts the
|
|
58
|
+
branch/run summary as a parent message, then posts the failing files — grouped
|
|
59
|
+
into reasonably sized batches — as threaded replies.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
NOTIFY_SLACK_BOT_TOKEN # xoxb-… bot token; enables threading
|
|
63
|
+
NOTIFY_SLACK_NOTIFY_CHANNEL # channel id or name to post to
|
|
64
|
+
NOTIFY_SLACK_THREAD_GROUP_SIZE # optional, files per reply (default 10)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
When both `NOTIFY_SLACK_BOT_TOKEN` and `NOTIFY_SLACK_WEBHOOK_URL` are set, the
|
|
68
|
+
bot token is preferred. The bot needs the `chat:write` scope.
|
|
69
|
+
|
|
54
70
|
### Debug your Slack setup
|
|
55
71
|
|
|
56
72
|
Create rake task to test the connection to your Slack channel
|
|
@@ -79,12 +95,4 @@ end
|
|
|
79
95
|
|
|
80
96
|
## Contributing
|
|
81
97
|
|
|
82
|
-
This gem is managed with [Reissue](https://github.com/SOFware/reissue).
|
|
83
|
-
|
|
84
|
-
Releasing a new version:
|
|
85
|
-
|
|
86
|
-
```sh
|
|
87
|
-
rake build:checksum
|
|
88
|
-
rake release
|
|
89
|
-
git push
|
|
90
|
-
```
|
|
98
|
+
This gem is managed with [Reissue](https://github.com/SOFware/reissue). Releases are automated via the [shared release workflow](https://github.com/SOFware/reissue/blob/main/.github/workflows/SHARED_WORKFLOW_README.md). Trigger a release by running the "Release gem to RubyGems.org" workflow from the Actions tab.
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../failure_groups"
|
|
4
|
+
|
|
3
5
|
module ChatNotifier
|
|
4
6
|
class Chatter
|
|
5
7
|
class Slack < self
|
|
6
8
|
Chatter.register self
|
|
7
9
|
|
|
10
|
+
API_URL = "https://slack.com/api/chat.postMessage"
|
|
11
|
+
DEFAULT_THREAD_GROUP_SIZE = 10
|
|
12
|
+
|
|
8
13
|
class << self
|
|
9
14
|
def handles?(settings)
|
|
10
15
|
!settings.keys.grep(/NOTIFY_SLACK/).empty?
|
|
@@ -15,14 +20,71 @@ module ChatNotifier
|
|
|
15
20
|
settings.fetch("NOTIFY_SLACK_WEBHOOK_URL", nil)
|
|
16
21
|
end
|
|
17
22
|
|
|
23
|
+
def bot_token
|
|
24
|
+
settings.fetch("NOTIFY_SLACK_BOT_TOKEN", nil)
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def channel
|
|
19
28
|
settings.fetch("NOTIFY_SLACK_NOTIFY_CHANNEL", nil)
|
|
20
29
|
end
|
|
21
30
|
|
|
31
|
+
def thread_group_size
|
|
32
|
+
Integer(settings.fetch("NOTIFY_SLACK_THREAD_GROUP_SIZE", DEFAULT_THREAD_GROUP_SIZE))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# When a bot token is configured we use the Slack Web API, which can
|
|
36
|
+
# thread failures. The token path is preferred over an incoming webhook.
|
|
37
|
+
def post(messenger, process: Net::HTTP.method(:post))
|
|
38
|
+
return super unless bot_token
|
|
39
|
+
|
|
40
|
+
post_via_api(messenger, process:)
|
|
41
|
+
end
|
|
42
|
+
|
|
22
43
|
def payload(data)
|
|
23
44
|
super(Configuration.for(data, self).to_h)
|
|
24
45
|
end
|
|
25
46
|
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def post_via_api(messenger, process:)
|
|
50
|
+
parent_text = messenger.failure? ? messenger.lede : messenger.message
|
|
51
|
+
parent = post_message(text: parent_text, process:)
|
|
52
|
+
|
|
53
|
+
return parent unless messenger.failure? && ok?(parent)
|
|
54
|
+
|
|
55
|
+
thread_ts = response_ts(parent)
|
|
56
|
+
FailureGroups.new(messenger.failures, group_size: thread_group_size).reply_texts.each do |text|
|
|
57
|
+
post_message(text:, thread_ts:, process:)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def post_message(text:, process:, thread_ts: nil)
|
|
62
|
+
body = {channel: channel, text: text}
|
|
63
|
+
body[:thread_ts] = thread_ts if thread_ts
|
|
64
|
+
process.call(URI(API_URL), body.to_json, api_headers)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def api_headers
|
|
68
|
+
{
|
|
69
|
+
"Content-Type" => "application/json; charset=utf-8",
|
|
70
|
+
"Authorization" => "Bearer #{bot_token}"
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def ok?(response)
|
|
75
|
+
parsed_response(response)["ok"] == true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def response_ts(response)
|
|
79
|
+
parsed_response(response)["ts"]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def parsed_response(response)
|
|
83
|
+
JSON.parse(response.body.to_s)
|
|
84
|
+
rescue JSON::ParserError
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
|
|
26
88
|
class Configuration
|
|
27
89
|
def self.for(messenger, communicator)
|
|
28
90
|
if messenger.failure?
|
|
@@ -13,7 +13,7 @@ module ChatNotifier
|
|
|
13
13
|
@environment = environment
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
attr_reader :settings, :repository, :environment
|
|
17
17
|
|
|
18
18
|
class << self
|
|
19
19
|
def handlers
|
|
@@ -45,16 +45,16 @@ module ChatNotifier
|
|
|
45
45
|
def body
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def post(messenger)
|
|
48
|
+
def post(messenger, process: Net::HTTP.method(:post))
|
|
49
49
|
uri = URI(webhook_url)
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
process.call(uri, payload(messenger))
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def conditional_post(messenger)
|
|
54
|
+
def conditional_post(messenger, process: Net::HTTP.method(:post))
|
|
55
55
|
return if messenger.success? && !verbose?
|
|
56
56
|
|
|
57
|
-
post(messenger)
|
|
57
|
+
post(messenger, process:)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def verbose?
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ChatNotifier
|
|
4
|
+
# Groups test failures by file and packs them into reasonably sized
|
|
5
|
+
# batches suitable for posting as individual Slack thread replies.
|
|
6
|
+
class FailureGroups
|
|
7
|
+
def initialize(failures, group_size: 10, char_limit: 3000)
|
|
8
|
+
@failures = failures
|
|
9
|
+
@group_size = group_size
|
|
10
|
+
@char_limit = char_limit
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :failures, :group_size, :char_limit
|
|
14
|
+
|
|
15
|
+
# One rendered string per thread reply: file blocks packed into batches
|
|
16
|
+
# bounded by group_size (file count) and char_limit (rendered length).
|
|
17
|
+
def reply_texts
|
|
18
|
+
batches.map { |blocks| blocks.join("\n") }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def batches
|
|
24
|
+
file_blocks.each_with_object([]) do |block, packed|
|
|
25
|
+
current = packed.last
|
|
26
|
+
if current.nil? ||
|
|
27
|
+
current.size >= group_size ||
|
|
28
|
+
(current.join("\n").length + 1 + block.length) > char_limit
|
|
29
|
+
packed << [block]
|
|
30
|
+
else
|
|
31
|
+
current << block
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Render each file (with its failing lines) into a text block.
|
|
37
|
+
def file_blocks
|
|
38
|
+
by_file.map do |file, lines|
|
|
39
|
+
bullets = lines.map { |line| " • #{File.basename(file)}:#{line}" }
|
|
40
|
+
([file] + bullets).join("\n")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# { "path/to/file.rb" => ["42", "88"], ... } preserving first-seen order.
|
|
45
|
+
def by_file
|
|
46
|
+
failures.each_with_object({}) do |failure, groups|
|
|
47
|
+
file, line = normalize(failure.location)
|
|
48
|
+
(groups[file] ||= []) << line
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# RSpec locations are "path:line" strings; Minitest locations are
|
|
53
|
+
# [file, line] arrays. Normalize both to [file, line].
|
|
54
|
+
def normalize(location)
|
|
55
|
+
if location.is_a?(Array)
|
|
56
|
+
[location[0], location[1]]
|
|
57
|
+
else
|
|
58
|
+
file, _, line = location.to_s.rpartition(":")
|
|
59
|
+
[file, line]
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/chat_notifier.rb
CHANGED
|
@@ -16,25 +16,32 @@ module ChatNotifier
|
|
|
16
16
|
class << self
|
|
17
17
|
attr_accessor :logger
|
|
18
18
|
|
|
19
|
-
def app
|
|
20
|
-
if defined?(::Rails) && Rails.respond_to?(:application)
|
|
19
|
+
def app(env: ENV, name: env.fetch("NOTIFY_APP_NAME"))
|
|
20
|
+
name ||= if defined?(::Rails) && Rails.respond_to?(:application)
|
|
21
21
|
Rails.application.class.module_parent
|
|
22
22
|
else
|
|
23
|
-
|
|
23
|
+
raise "No app name provided and Rails is not defined"
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
STANDARDS = {
|
|
28
|
+
repository: Repository,
|
|
29
|
+
environment: TestEnvironment,
|
|
30
|
+
chatter: Chatter,
|
|
31
|
+
messenger: Messenger
|
|
32
|
+
}
|
|
33
|
+
|
|
27
34
|
# In order to test this locally see `rake chat_notifier:debug`
|
|
28
|
-
def debug!(env, summary:, notifier: :Debug)
|
|
29
|
-
repository =
|
|
30
|
-
environment =
|
|
35
|
+
def debug!(env, summary:, notifier: :Debug, **kwargs)
|
|
36
|
+
repository = (kwargs[:repository] || STANDARDS[:repository]).for(env)
|
|
37
|
+
environment = (kwargs[:environment] || STANDARDS[:environment]).for(env)
|
|
31
38
|
|
|
32
|
-
chatter =
|
|
39
|
+
chatter = (kwargs[:chatter] || STANDARDS[:chatter]).const_get(notifier).new(
|
|
33
40
|
settings: env,
|
|
34
41
|
repository: repository,
|
|
35
42
|
environment: environment
|
|
36
43
|
)
|
|
37
|
-
messenger =
|
|
44
|
+
messenger = (kwargs[:messenger] || STANDARDS[:messenger]).for(
|
|
38
45
|
summary,
|
|
39
46
|
app:,
|
|
40
47
|
repository:,
|
|
@@ -44,16 +51,16 @@ module ChatNotifier
|
|
|
44
51
|
chatter.post(messenger)
|
|
45
52
|
end
|
|
46
53
|
|
|
47
|
-
def call(summary
|
|
48
|
-
repository =
|
|
49
|
-
environment =
|
|
50
|
-
chatter =
|
|
54
|
+
def call(summary:, **kwargs)
|
|
55
|
+
repository = (kwargs[:repository] || STANDARDS[:repository]).for(ENV)
|
|
56
|
+
environment = (kwargs[:environment] || STANDARDS[:environment]).for(ENV)
|
|
57
|
+
chatter = (kwargs[:chatter] || STANDARDS[:chatter]).handling(
|
|
51
58
|
ENV,
|
|
52
59
|
repository:,
|
|
53
60
|
environment:
|
|
54
61
|
)
|
|
55
62
|
|
|
56
|
-
messenger =
|
|
63
|
+
messenger = (kwargs[:messenger] || STANDARDS[:messenger]).for(
|
|
57
64
|
summary,
|
|
58
65
|
app:,
|
|
59
66
|
repository:,
|
|
@@ -61,11 +68,9 @@ module ChatNotifier
|
|
|
61
68
|
)
|
|
62
69
|
|
|
63
70
|
chatter.each do |box|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
logger.error("ChatNotifier: #{box.webhook_url} #{exception.class}: #{exception.message}")
|
|
68
|
-
end
|
|
71
|
+
box.conditional_post(messenger)
|
|
72
|
+
rescue => exception
|
|
73
|
+
logger.error("ChatNotifier: #{box.webhook_url} #{exception.class}: #{exception.message}")
|
|
69
74
|
end
|
|
70
75
|
end
|
|
71
76
|
end
|
|
@@ -8,8 +8,8 @@ module Minitest
|
|
|
8
8
|
ExceptionLocation = Data.define(:location)
|
|
9
9
|
Summary = Data.define(:failed_examples)
|
|
10
10
|
def report
|
|
11
|
-
summary = Summary[(results.map{ |result| ExceptionLocation[result.source_location] })]
|
|
11
|
+
summary = Summary[(results.map { |result| ExceptionLocation[result.source_location] })]
|
|
12
12
|
::ChatNotifier.call(summary:)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
|
-
end
|
|
15
|
+
end
|
metadata
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chat_notifier
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Gay
|
|
8
8
|
- Savannah Albanez
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: exe
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
13
|
-
dependencies:
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: logger
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
14
27
|
description: Send test results to chat
|
|
15
28
|
email:
|
|
16
29
|
- jim@saturnflyer.com
|
|
@@ -26,6 +39,7 @@ files:
|
|
|
26
39
|
- lib/chat_notifier/chatter.rb
|
|
27
40
|
- lib/chat_notifier/chatter/debug.rb
|
|
28
41
|
- lib/chat_notifier/chatter/slack.rb
|
|
42
|
+
- lib/chat_notifier/failure_groups.rb
|
|
29
43
|
- lib/chat_notifier/messenger.rb
|
|
30
44
|
- lib/chat_notifier/repository.rb
|
|
31
45
|
- lib/chat_notifier/repository/debug.rb
|
|
@@ -36,12 +50,10 @@ files:
|
|
|
36
50
|
- lib/chat_notifier/test_environment/github.rb
|
|
37
51
|
- lib/chat_notifier/version.rb
|
|
38
52
|
- lib/minitest/chat_notifier_plugin.rb
|
|
39
|
-
homepage:
|
|
40
53
|
licenses: []
|
|
41
54
|
metadata:
|
|
42
55
|
source_code_uri: https://github.com/SOFware/chat_notifier.git
|
|
43
56
|
changelog_uri: https://github.com/SOFWare/chat_notifier/blob/master/CHANGELOG.md
|
|
44
|
-
post_install_message:
|
|
45
57
|
rdoc_options: []
|
|
46
58
|
require_paths:
|
|
47
59
|
- lib
|
|
@@ -49,15 +61,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
49
61
|
requirements:
|
|
50
62
|
- - ">="
|
|
51
63
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: 3.
|
|
64
|
+
version: 3.3.0
|
|
53
65
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
66
|
requirements:
|
|
55
67
|
- - ">="
|
|
56
68
|
- !ruby/object:Gem::Version
|
|
57
69
|
version: '0'
|
|
58
70
|
requirements: []
|
|
59
|
-
rubygems_version: 3.
|
|
60
|
-
signing_key:
|
|
71
|
+
rubygems_version: 3.6.9
|
|
61
72
|
specification_version: 4
|
|
62
73
|
summary: Notify chat of test results
|
|
63
74
|
test_files: []
|