Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eaf5785
Add Gitlab workflows that can be scheduled to notify Slack
zachmargolis Jun 18, 2024
bce9e64
Remove stage
zachmargolis Jun 18, 2024
15558af
Add specs for notify-slack
zachmargolis Jun 18, 2024
97d7164
Remove JSON matcher dependency
zachmargolis Jun 20, 2024
e187c4b
Fix lints
zachmargolis Jun 20, 2024
35d1fcd
Add changelog
zachmargolis Jun 20, 2024
0821841
lint
zachmargolis Jun 21, 2024
8b3d817
Make sure to run scheduled jobs on main branch only
zachmargolis Jun 21, 2024
8f98128
Revert "Make sure to run scheduled jobs on main branch only"
zachmargolis Jun 21, 2024
d90a80b
allow running scheduled jobs
Jun 21, 2024
345abfd
to revert: make pinpoint lint check fail
zachmargolis Jun 21, 2024
37a164d
Revert "to revert: make pinpoint lint check fail"
zachmargolis Jun 21, 2024
2c18c05
to revert: make pinpoint lint check fail
zachmargolis Jun 21, 2024
ba49e7a
change slack webhook syntax
Jun 21, 2024
e70eb8e
Try using single line for commands
zachmargolis Jun 21, 2024
00a091c
print debugging
zachmargolis Jun 21, 2024
3a3206a
Switch if syntax for bash
zachmargolis Jun 24, 2024
d1e0fde
Change newlines
zachmargolis Jun 24, 2024
005e7de
debug slack args
zachmargolis Jun 28, 2024
7c71af4
Remove backticks, they were being evaluated
zachmargolis Jun 28, 2024
376ca5f
add username
zachmargolis Jun 28, 2024
78dfeee
remove debuggin
zachmargolis Jun 28, 2024
b841abe
Clean up Slack message, href syntax didn't work
zachmargolis Jun 28, 2024
c55e92d
Remove broken-on-purpose yml change
zachmargolis Jun 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 51 additions & 3 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ stages:

workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: never
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "external_pull_request_event"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "external_pull_request_event" || $CI_PIPELINE_SOURCE == "schedule"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" || $CI_PIPELINE_SOURCE == "external_pull_request_event" || $CI_PIPELINE_SOURCE == "web"'
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "stages/prod"'
- if: '$CI_MERGE_REQUEST_IID || $CI_EXTERNAL_PULL_REQUEST_IID'
Expand Down Expand Up @@ -341,6 +339,7 @@ js_tests:
- *yarn_install
- yarn test

# Can be removed once pinpoint_check_scheduled is confirmed
pinpoint-check:
needs:
- job: install
Expand All @@ -353,6 +352,7 @@ pinpoint-check:
- *yarn_install
- make lint_country_dialing_codes

# Can be removed once audit_packages_scheduled is confirmed
audit_packages:
needs:
- job: install
Expand Down Expand Up @@ -838,3 +838,51 @@ ecr-scan-ci:
stage: scan
variables:
ecr_repo: idp/ci

pinpoint_check_scheduled:
needs:
- job: install
cache:
- <<: *ruby_cache
- <<: *yarn_cache
script:
- *bundle_install
- *yarn_install
- make lint_country_dialing_codes
after_script:
- |-
if [ "$CI_JOB_STATUS" != "success" ]; then
./scripts/notify-slack \
--icon ":gitlab:" \
--username "gitlab-notify" \
--channel "#login-appdev" \
--webhook "${SLACK_WEBHOOK}" \
--raise \
--text "Pinpoint supported countries check in GitLab failed.\nBuild Results: ${CI_JOB_URL}.\nCheck results locally with 'make lint_country_dialing_codes'"
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"

audit_packages_scheduled:
needs:
- job: install
cache:
- <<: *ruby_cache
- <<: *yarn_cache
script:
- *bundle_install
- *yarn_install
- make audit
after_script:
- |-
if [ "$CI_JOB_STATUS" != "success" ]; then
./scripts/notify-slack \
--icon ":gitlab:" \
--username "gitlab-notify" \
--channel "#login-appdev" \
--webhook "${SLACK_WEBHOOK}" \
--raise \
--text "Dependencies audit in GitLab failed.\nBuild Results: ${CI_JOB_URL}\nCheck results locally with 'make audit'"
fi
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
144 changes: 144 additions & 0 deletions scripts/notify-slack
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env ruby
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is imported from identity-devops where it's lived for a while as part of our infra notifications, I'll add some specs here

# frozen_string_literal: true

require 'json'
require 'net/http'
require 'optparse'

# Posts a message to Slack via webhook
class NotifySlack
def run(argv:, stdin:, stdout:)
channel = nil
username = nil
text = nil
webhook = nil
icon = ':login-gov:'
raise_on_failure = false

program_name = File.basename($PROGRAM_NAME)

parser = OptionParser.new do |opts|
opts.banner = <<~STR
#{program_name} [options]

Usage:

* Using arguments

#{program_name} --text "my *message*" \\
--channel "#some-channel" \\
--webhook https://example.com/webhook

* Passing text over STDIN

echo "my *message*" | #{program_name} --text - \\
--channel "#some-channel" \\
--webhook https://example.com/webhook

Options:
STR

opts.on('--channel CHANNEL', 'which channel to notify') do |channel_v|
channel = channel_v
end

opts.on('--webhook WEBHOOK', 'Slack webhook URL') do |webhook_v|
webhook = webhook_v
end

opts.on('--username USERNAME', 'which username to notify as') do |username_v|
username = username_v
end

opts.on('--text TEXT', 'text of notification, pass - to read from STDIN') do |text_v|
if text_v == '-'
if stdin.tty?
stdout.print 'please enter text of message: '
text = stdin.gets
else
text = stdin.read
end
else
text = text_v
end
end

opts.on('--icon ICON', 'slack emoji to use as icon (optional)') do |icon_v|
icon = icon_v
end

opts.on('--[no-]raise', <<~EOS) do |raise_v|
raise errors/exit uncleanly if the webhook fails. defaults to not raising
EOS
raise_on_failure = raise_v
end

opts.on('--help') do
puts opts
exit 0
end
end

parser.parse!(argv)

if !channel || !username || !text || !webhook
stdout.puts parser
exit 1
end

notify(
webhook: webhook,
channel: channel,
username: username,
text: text,
icon: format_icon(icon),
)
stdout.puts 'OK'
rescue Net::HTTPClientException => err
stdout.puts "#{program_name} HTTP ERROR: #{err.response.code}"
raise err if raise_on_failure
rescue => err
stdout.puts "#{program_name} ERROR: #{err.message}"
raise err if raise_on_failure
end

# @raise [Net::HTTPClientException] throws an error for non-successful response
# @return [Net::HTTPResponse]
def notify(webhook:, channel:, username:, text:, icon:)
url = URI(webhook)

req = Net::HTTP::Post.new(url)
req.form_data = {
'payload' => {
channel: channel,
username: username,
text: text,
icon_emoji: icon,
}.to_json,
}

Net::HTTP.start(
url.hostname,
url.port,
use_ssl: url.scheme == 'https',
open_timeout: 1,
read_timeout: 1,
write_timeout: 1,
ssl_timeout: 1,
) do |http|
http.request(req)
end.value
end

def format_icon(icon)
if icon.start_with?(':') && icon.end_with?(':')
icon
else
":#{icon}:"
end
end
end

if $PROGRAM_NAME == __FILE__
NotifySlack.new.run(argv: ARGV, stdin: STDIN, stdout: STDOUT)
end
120 changes: 120 additions & 0 deletions spec/scripts/notify-slack_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'spec_helper'
require 'rack/utils'
load File.expand_path('../../scripts/notify-slack', __dir__)

RSpec.describe NotifySlack do
subject(:notifier) { described_class.new }

let(:webhook) { 'https://slack.example.com/abcdef/ghijkl' }
let(:channel) { '#fun-channel' }
let(:username) { 'notifier-bot' }
let(:text) { 'my message' }
let(:icon) { ':red_circle:' }

describe '#run' do
let(:argv) do
[
'--webhook',
webhook,
'--channel',
channel,
'--username',
username,
'--text',
text,
'--icon', icon
]
end
let(:stdin) { StringIO.new }
let(:stdout) { StringIO.new }

subject(:run) do
notifier.run(argv:, stdin:, stdout:)
end

before do
allow(notifier).to receive(:exit)
end

context 'missing required argument' do
before do
argv.delete('--webhook')
argv.delete(webhook)
end

it 'prints help and exits uncleanly' do
expect(notifier).to receive(:exit).with(1)

run

expect(stdout.string).to include('Usage')
end
end

it 'notifies' do
post_request = stub_request(:post, webhook)

run

expect(post_request).to have_been_made
end

context 'network error' do
before do
stub_request(:post, webhook).to_return(status: 500)
end

it 'prints an error and exits cleanly' do
expect(notifier).to_not receive(:exit)

run

expect(stdout.string).to include('ERROR: 500')
end

context 'with --raise' do
before { argv << '--raise' }

it 'raises an error' do
expect { run }.to raise_error(Net::HTTPExceptions)
end
end
end
end

describe '#notify' do
subject(:notify) do
notifier.notify(webhook:, channel:, username:, text:, icon:)
end

it 'POSTs JSON inside of form encoding to the webhook' do
post_request = stub_request(:post, webhook).with(
headers: {
content_type: 'application/x-www-form-urlencoded',
},
) do |req|
form = Rack::Utils.parse_query(req.body)
expect(JSON.parse(form['payload'], symbolize_names: true)).to eq(
channel:,
username:,
text:,
icon_emoji: icon,
)
end

notify

expect(post_request).to have_been_made
end
end

describe '#format_icon' do
it 'adds colons around icon names if missing' do
expect(notifier.format_icon('joy')).to eq(':joy:')
end

it 'leaves colons around icon names if present' do
expect(notifier.format_icon(':sob:')).to eq(':sob:')
end
end
end