diff --git a/.github/workflows/afternoon_seal_quotes.yml b/.github/workflows/afternoon_seal_quotes.yml index 724b4002..93f4183f 100644 --- a/.github/workflows/afternoon_seal_quotes.yml +++ b/.github/workflows/afternoon_seal_quotes.yml @@ -24,4 +24,4 @@ jobs: - name: Afternoon Seal Quotes id: afternoon_seal_quotes run: | - ./bin/seal_runner.rb afternoon_seal_quotes + ./bin/seal_runner.rb gems diff --git a/.github/workflows/gem_version_checker.yml b/.github/workflows/gem_version_checker.yml new file mode 100644 index 00000000..082b9f2b --- /dev/null +++ b/.github/workflows/gem_version_checker.yml @@ -0,0 +1,28 @@ +name: "Gem Version Checker" + +on: + workflow_dispatch: {} + schedule: + - cron: '00 13 * * 3' # Runs at 13:00 UTC, Every Wednesday. + +env: + SEAL_ORGANISATION: alphagov + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + +jobs: + gem-version-checker: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Gem Version Checker + id: gem_version_checker + run: | + ./bin/seal_runner.rb gems diff --git a/.github/workflows/morning_seal_prs.yml b/.github/workflows/seal_prs.yml similarity index 100% rename from .github/workflows/morning_seal_prs.yml rename to .github/workflows/seal_prs.yml diff --git a/bin/seal_runner.rb b/bin/seal_runner.rb index f579f27b..55f10c5d 100755 --- a/bin/seal_runner.rb +++ b/bin/seal_runner.rb @@ -1,5 +1,6 @@ #!/usr/bin/env ruby +require_relative '../lib/gem_version_checker' require_relative '../lib/seal' require_relative '../lib/team_builder' diff --git a/lib/gem_version_checker.rb b/lib/gem_version_checker.rb new file mode 100644 index 00000000..2af3084c --- /dev/null +++ b/lib/gem_version_checker.rb @@ -0,0 +1,43 @@ +require "uri" +require "net/http" +require "json" +require_relative "slack_poster" + +class GemVersionChecker + def print_version_discrepancies(gems) + gems.filter_map { |gem| + repo_name = gem["app_name"] + repo_url = gem["links"]["repo_url"] + rubygems_version = fetch_rubygems_version(repo_name) + if rubygems_version && change_is_significant?(repo_name, rubygems_version) + "<#{repo_url}|#{repo_name}> has unreleased changes since v#{rubygems_version}" + end + }.join("\n") + end + + def fetch_rubygems_version(gem_name) + uri = URI("https://rubygems.org/api/v1/gems/#{gem_name}.json") + res = Net::HTTP.get_response(uri) + + JSON.parse(res.body)["version"] if res.is_a?(Net::HTTPSuccess) + end + + def files_changed_since_tag(repo, tag) + Dir.mktmpdir do |path| + Dir.chdir(path) do + if system("git clone --recursive --depth 1 --shallow-submodules --no-single-branch https://github.com/alphagov/#{repo}.git > /dev/null 2>&1") + Dir.chdir(repo) { `git diff --name-only #{tag}`.split("\n") } + else + puts "Warning: Failed to clone #{repo}" + [] + end + end + end + end + + def change_is_significant?(repo_name, previous_version) + files_changed_since_tag(repo_name, "v#{previous_version}").any? do |path| + path.start_with?("app/", "lib/") || path == "CHANGELOG.md" + end + end +end diff --git a/lib/message_builder.rb b/lib/message_builder.rb index 90ff8da0..63dc1e9c 100644 --- a/lib/message_builder.rb +++ b/lib/message_builder.rb @@ -18,6 +18,8 @@ def build build_dependapanda_message when :ci build_ci_message + when :gems + build_gems_message else build_regular_message end @@ -57,6 +59,30 @@ def build_ci_message Message.new(ci_message, mood: "robot_face") end + def build_gems_message + Message.new(gems_message, mood: "gem") + end + + def gems_message + @message = all_govuk_gems.group_by { |gem| gem["team"] == team }.each do |team, gems| + GemVersionChecker.new.print_version_discrepancies(gems) + end + + template_file = Pathname.new("#{TEMPLATE_DIR}/gem_alerts.text.erb") + ERB.new(template_file.read, trim_mode: "-").result(binding).strip + end + + def all_govuk_gems + @all_govuk_gems ||= fetch_gems + end + + def fetch_gems + res = Net::HTTP.get_response("https://docs.publishing.service.gov.uk/gems.json") + raise "HTTP request failed: #{res.code}" unless res.is_a?(Net::HTTPSuccess) + + JSON.parse(res.body) + end + def ci_message @repos = check_team_repos_ci.reject { |_, v| v }.keys return nil if @repos.empty? diff --git a/lib/seal.rb b/lib/seal.rb index 9b6742a0..c32271b3 100755 --- a/lib/seal.rb +++ b/lib/seal.rb @@ -38,6 +38,8 @@ def bark_at(team, mode: nil) MessageBuilder.new(team, :ci).build if team.ci_checks when "seal_prs" MessageBuilder.new(team, :seal).build if team.seal_prs + when "gems" + MessageBuilder.new(team, :gems).build if team.gems end return if message.nil? || message.text.nil? diff --git a/lib/slack_poster.rb b/lib/slack_poster.rb index d10c2dc9..d6c9b7ec 100644 --- a/lib/slack_poster.rb +++ b/lib/slack_poster.rb @@ -60,6 +60,8 @@ def assign_poster_settings [":#{@season_symbol}angrier_seal:", "#{@season_name}Angry Seal"] when "robot_face" [":robot_face:", "Angry CI Robot"] + when "gem" + [":gem:", "Gem Release Robot"] when "tea" [":manatea:", "Tea Seal"] when "charter" @@ -103,6 +105,6 @@ def set_mood_from_team end def channel - @team_channel = "#bot-testing" if ENV["DEVELOPMENT"] + @team_channel = "#murilo-testing" end end diff --git a/lib/team.rb b/lib/team.rb index 306463ed..178facab 100644 --- a/lib/team.rb +++ b/lib/team.rb @@ -7,6 +7,7 @@ def initialize( afternoon_seal_quotes: nil, dependapanda: nil, ci_checks: nil, + gems: nil, compact: nil, exclude_labels: nil, exclude_titles: nil, @@ -22,6 +23,7 @@ def initialize( @afternoon_seal_quotes = (afternoon_seal_quotes.nil? ? false : afternoon_seal_quotes) @dependapanda = (dependapanda.nil? ? false : dependapanda) @ci_checks = (ci_checks.nil? ? false : ci_checks) + @gems = (gems.nil? ? false : gems) @compact = (compact.nil? ? false : compact) @quotes_days = quotes_days || [] @exclude_labels = exclude_labels || [] @@ -40,6 +42,7 @@ def initialize( afternoon_seal_quotes dependapanda ci_checks + gems compact exclude_labels exclude_titles diff --git a/lib/team_builder.rb b/lib/team_builder.rb index 2779ee16..95e67c1e 100644 --- a/lib/team_builder.rb +++ b/lib/team_builder.rb @@ -46,6 +46,7 @@ def build_all_teams raise "#{team_name} is a GOV.UK team and shouldn't list repos in ./config/alphagov.yml" end team_config["ci_checks"] = is_govuk_team?(team_name) + team_config["gems"] = is_govuk_team?(team_name) Team.new(**apply_env(team_config)) end end @@ -60,6 +61,7 @@ def apply_env(config) afternoon_seal_quotes: env["AFTERNOON_SEAL_QUOTES"] == "true" || config["afternoon_seal_quotes"], dependapanda: env["DEPENDAPANDA"] == "true" || config["dependapanda"], ci_checks: env["CI_CHECKS"] == "true" || config["ci_checks"], + gems: env["GEMS"] == "true" || config["gems"], compact: env["COMPACT"] == "true" || config["compact"], exclude_labels: env["GITHUB_EXCLUDE_LABELS"]&.split(",") || config["exclude_labels"], exclude_titles: env["GITHUB_EXCLUDE_TITLES"]&.split(",") || config["exclude_titles"], diff --git a/spec/gem_version_checker_spec.rb b/spec/gem_version_checker_spec.rb new file mode 100644 index 00000000..03565d5a --- /dev/null +++ b/spec/gem_version_checker_spec.rb @@ -0,0 +1,53 @@ +require "./lib/gem_version_checker" + +RSpec.describe GemVersionChecker do + subject(:gem_version_checker) { described_class.new } + + let(:slack_poster) { instance_double(SlackPoster, send_request: nil) } + + before do + allow(SlackPoster).to receive(:new).and_return(slack_poster) + end + + it "fetches version number of a gem from rubygems" do + stub_rubygems_call("6.0.1") + + expect(gem_version_checker.fetch_rubygems_version("example")).to eq("6.0.1") + end + + it "detects when there are no files changed since the last release that are built into the gem" do + stub_devdocs_call + stub_rubygems_call("1.2.3") + stub_files_changed_since_tag(["README.md"]) + + expect { gem_version_checker.print_version_discrepancies }.to output("team: #platform-security-reliability-team\n").to_stdout + end + + it "detects when there are files changed since the last release that are built into the gem" do + stub_devdocs_call + stub_rubygems_call("1.2.2") + stub_files_changed_since_tag(["lib/foo.rb"]) + + expect { gem_version_checker.print_version_discrepancies }.to output( + "team: #platform-security-reliability-team\n has unreleased changes since v1.2.2\n", + ).to_stdout + end + + def stub_files_changed_since_tag(files) + allow(gem_version_checker).to receive(:files_changed_since_tag) do + files + end + end + + def stub_rubygems_call(version) + repo = { "version": version } + stub_request(:get, "https://rubygems.org/api/v1/gems/example.json") + .to_return(status: 200, body: repo.to_json, headers: {}) + end + + def stub_devdocs_call + repo = [{ "app_name": "example", "team": "#platform-security-reliability-team", "links": { "repo_url": "https://example.com" } }] + stub_request(:get, "https://docs.publishing.service.gov.uk/gems.json") + .to_return(status: 200, body: repo.to_json, headers: {}) + end +end diff --git a/templates/gem_alerts.text.erb b/templates/gem_alerts.text.erb new file mode 100644 index 00000000..77a9d9e2 --- /dev/null +++ b/templates/gem_alerts.text.erb @@ -0,0 +1 @@ +<%= @message %>