From b98d0faf5151b27938297666e4e753e0b752bcc7 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 01:11:27 +0100 Subject: [PATCH 01/10] Extract danger reporting infrastructure into reusable workflows and gem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves reporting responsibility from consuming gems into ruby-grape-danger, establishing it as the authoritative Danger integration framework for Grape projects. Key changes: - Add RubyGrapeDanger::Reporter class for consistent danger report generation - Implement automatic reporting via at_exit hook in gem's Dangerfile - Add reusable GitHub Actions workflows (danger-run.yml, danger-comment.yml) with embedded script for posting/updating PR comments - Consuming gems now just import ruby-grape-danger and add their checks Benefits: ✅ DRY: Workflows defined once, reused by all projects ✅ Consistent: All projects use same reporting format and behavior ✅ Maintainable: Fix bugs in workflows once, all projects benefit ✅ Scalable: New danger checks automatically get reporting infrastructure ✅ Simple API: Projects only need `danger.import_dangerfile(gem: 'ruby-grape-danger')` Example usage: danger.import_dangerfile(gem: 'ruby-grape-danger') changelog.check! # Reporting happens automatically via at_exit hook Includes comprehensive specs for Reporter class with 13 test cases covering: - JSON report generation and formatting - Message type handling (strings, objects with .message method) - Edge cases (missing files, missing PR number, empty arrays, nil values) - Multiline content preservation --- .github/workflows/danger-comment.yml | 122 +++++++++++++ .github/workflows/danger-run.yml | 33 ++++ Dangerfile | 18 ++ README.md | 96 ++++++++++- lib/ruby-grape-danger.rb | 5 + lib/ruby-grape-danger/reporter.rb | 38 +++++ spec/ruby-grape-danger/reporter_spec.rb | 217 ++++++++++++++++++++++++ 7 files changed, 526 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/danger-comment.yml create mode 100644 .github/workflows/danger-run.yml create mode 100644 lib/ruby-grape-danger.rb create mode 100644 lib/ruby-grape-danger/reporter.rb create mode 100644 spec/ruby-grape-danger/reporter_spec.rb diff --git a/.github/workflows/danger-comment.yml b/.github/workflows/danger-comment.yml new file mode 100644 index 0000000..50f3f72 --- /dev/null +++ b/.github/workflows/danger-comment.yml @@ -0,0 +1,122 @@ +name: Danger Comment +on: + workflow_run: + workflows: [Danger] + types: [completed] + workflow_call: + +permissions: + actions: read + contents: read + issues: write + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + if: | + (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') + || github.event_name == 'workflow_call' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 1 + - name: Download Danger Report (workflow_run) + if: github.event_name == 'workflow_run' + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: danger-report + run-id: ${{ github.event.workflow_run.id }} + repository: ${{ github.event.workflow_run.repository.full_name }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Download Danger Report (reusable call) + if: github.event_name == 'workflow_call' + uses: actions/download-artifact@v4 + continue-on-error: true + with: + name: danger-report + - name: Post or Update PR Comment + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const hasItems = (arr) => Array.isArray(arr) && arr.length > 0; + + let report; + try { + report = JSON.parse(fs.readFileSync('danger_report.json', 'utf8')); + } catch (e) { + console.log('No danger report found, skipping comment'); + return; + } + + if (!report.pr_number) { + console.log('No PR number found in report, skipping comment'); + return; + } + + let body = '## Danger Report\n\n'; + + if (hasItems(report.errors)) { + body += '### ❌ Errors\n'; + report.errors.forEach(e => body += `- ${e}\n`); + body += '\n'; + } + + if (hasItems(report.warnings)) { + body += '### ⚠️ Warnings\n'; + report.warnings.forEach(w => body += `- ${w}\n`); + body += '\n'; + } + + if (hasItems(report.messages)) { + body += '### ℹ️ Messages\n'; + report.messages.forEach(m => body += `- ${m}\n`); + body += '\n'; + } + + if (hasItems(report.markdowns)) { + report.markdowns.forEach(md => body += `${md}\n\n`); + } + + if (!hasItems(report.errors) && + !hasItems(report.warnings) && + !hasItems(report.messages) && + !hasItems(report.markdowns)) { + body += '✅ All checks passed!'; + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: report.pr_number + }); + + const botComment = comments.find(c => + c.user.login === 'github-actions[bot]' && + c.body.includes('## Danger Report') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: report.pr_number, + body: body + }); + } + + // Fail if there are errors + if (report.errors && report.errors.length > 0) { + core.setFailed('Danger found errors'); + } diff --git a/.github/workflows/danger-run.yml b/.github/workflows/danger-run.yml new file mode 100644 index 0000000..fc0d7ce --- /dev/null +++ b/.github/workflows/danger-run.yml @@ -0,0 +1,33 @@ +name: Danger +on: + pull_request: + types: [ opened, reopened, edited, synchronize ] + workflow_call: +jobs: + danger: + name: Danger + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + - name: Run Danger + # Note: We use 'dry_run' mode intentionally as part of a two-workflow pattern. + # The actual commenting on GitHub is handled by the danger-comment.yml workflow. + run: bundle exec danger dry_run --verbose + env: + DANGER_REPORT_PATH: danger_report.json + - name: Upload Danger Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: danger-report + path: danger_report.json + retention-days: 1 + if-no-files-found: ignore diff --git a/Dangerfile b/Dangerfile index c042e5f..bdaf195 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,5 +1,23 @@ # frozen_string_literal: true +require 'ruby-grape-danger' +require 'English' + +# -------------------------------------------------------------------------------------------------------------------- +# Automatically export danger report when Dangerfile finishes +# -------------------------------------------------------------------------------------------------------------------- +at_exit do + next if $ERROR_INFO # Skip if an exception occurred + + if defined?(status_report) + reporter = RubyGrapeDanger::Reporter.new(status_report) + reporter.export_json( + ENV.fetch('DANGER_REPORT_PATH', nil), + ENV.fetch('GITHUB_EVENT_PATH', nil) + ) + end +end + # -------------------------------------------------------------------------------------------------------------------- # Has any changes happened inside the actual library code? # -------------------------------------------------------------------------------------------------------------------- diff --git a/README.md b/README.md index e55b9b4..306ca51 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ ## Table of Contents - [Setup](#setup) - - [Set DANGER_GITHUB_API_TOKEN in Travis-CI](#set-danger_github_api_token-in-travis-ci) - [Add Danger](#add-danger) - [Add Dangerfile](#add-dangerfile) - - [Add Danger to Travis-CI](#add-danger-to-travis-ci) + - [Add GitHub Actions Workflows](#add-github-actions-workflows) - [Commit via a Pull Request](#commit-via-a-pull-request) +- [Reusable Workflows](#reusable-workflows) - [License](#license) ## Setup @@ -28,16 +28,106 @@ gem 'ruby-grape-danger', require: false ### Add Dangerfile -Commit a `Dangerfile`, eg. [Grape's Dangerfile](https://github.com/ruby-grape/grape/blob/master/Dangerfile). +Create a `Dangerfile` in your project's root that imports `ruby-grape-danger` and adds your project-specific checks: ```ruby danger.import_dangerfile(gem: 'ruby-grape-danger') + +# Your project-specific danger checks +changelog.check! +toc.check! +``` + +The `ruby-grape-danger` Dangerfile automatically handles: +- Setting up the reporting infrastructure +- Exporting the danger report via `at_exit` hook when the Dangerfile finishes +- Consistent output format for the workflow + +### Add GitHub Actions Workflows + +Create `.github/workflows/danger.yml`: + +```yaml +name: Danger +on: + pull_request: + types: [ opened, reopened, edited, synchronize ] + workflow_call: + +jobs: + danger: + uses: ruby-grape/ruby-grape-danger/.github/workflows/danger-run.yml@main +``` + +Create `.github/workflows/danger-comment.yml`: + +```yaml +name: Danger Comment +on: + workflow_run: + workflows: [Danger] + types: [completed] + workflow_call: + +jobs: + comment: + uses: ruby-grape/ruby-grape-danger/.github/workflows/danger-comment.yml@main ``` ### Commit via a Pull Request To test things out, make a dummy entry in `CHANGELOG.md` that doesn't match the standard format and make a pull request. Iterate until green. +## Reusable Workflows + +This gem provides **reusable GitHub Actions workflows** that can be referenced by any Grape project to implement standardized Danger checks with consistent reporting. + +### Architecture + +The workflows are separated into two stages: + +1. **danger-run.yml**: Executes Danger checks and generates a report + - Runs `bundle exec danger dry_run` with your project's Dangerfile + - Generates a JSON report of check results + - Uploads the report as an artifact + +2. **danger-comment.yml**: Posts/updates PR comments with results + - Downloads the Danger report artifact + - Formats and posts results as a PR comment + - Updates existing comment on subsequent runs + +### Benefits of Reusable Workflows + +✅ **DRY**: Define workflows once in `ruby-grape-danger`, reuse everywhere +✅ **Consistent**: All Grape projects use the same reporting format and behavior +✅ **Maintainable**: Fix a bug in the workflows once, all projects benefit automatically +✅ **Scalable**: Add new checks to any project's Dangerfile without touching workflows + +### How It Works + +When you reference the reusable workflows: + +```yaml +uses: ruby-grape/ruby-grape-danger/.github/workflows/danger-run.yml@main +``` + +GitHub Actions: +1. Checks out **your project's repository** (not ruby-grape-danger) +2. Installs dependencies from **your Gemfile** +3. Runs danger using **your Dangerfile** + - Your Dangerfile imports `ruby-grape-danger`'s Dangerfile via `danger.import_dangerfile(gem: 'ruby-grape-danger')` + - The imported Dangerfile registers an `at_exit` hook for automatic reporting + - Runs your project-specific checks (added after the import) + - When Dangerfile finishes, the `at_exit` hook automatically exports the report +4. The report is uploaded as an artifact for the commenting workflow + +Each project maintains its own Dangerfile with project-specific checks, while the `ruby-grape-danger` gem provides shared infrastructure for consistent reporting and workflow execution. + +### Examples + +- [danger-changelog](https://github.com/ruby-grape/danger-changelog) - Validates CHANGELOG format +- [grape](https://github.com/ruby-grape/grape) - Multi-check danger implementation + ## License MIT License. See [LICENSE](LICENSE) for details. diff --git a/lib/ruby-grape-danger.rb b/lib/ruby-grape-danger.rb new file mode 100644 index 0000000..1e758bd --- /dev/null +++ b/lib/ruby-grape-danger.rb @@ -0,0 +1,5 @@ +require 'ruby-grape-danger/version' +require 'ruby-grape-danger/reporter' + +module RubyGrapeDanger +end diff --git a/lib/ruby-grape-danger/reporter.rb b/lib/ruby-grape-danger/reporter.rb new file mode 100644 index 0000000..38f4686 --- /dev/null +++ b/lib/ruby-grape-danger/reporter.rb @@ -0,0 +1,38 @@ +require 'json' + +module RubyGrapeDanger + class Reporter + def initialize(status_report) + @status_report = status_report + end + + def export_json(report_path, event_path) + return unless report_path && event_path && File.exist?(event_path) + + event = JSON.parse(File.read(event_path)) + pr_number = event.dig('pull_request', 'number') + return unless pr_number + + report = build_report(pr_number) + File.write(report_path, JSON.pretty_generate(report)) + end + + private + + def build_report(pr_number) + { + pr_number: pr_number, + errors: to_messages(@status_report[:errors]), + warnings: to_messages(@status_report[:warnings]), + messages: to_messages(@status_report[:messages]), + markdowns: to_messages(@status_report[:markdowns]) + } + end + + def to_messages(items) + Array(items).map do |item| + item.respond_to?(:message) ? item.message : item.to_s + end + end + end +end diff --git a/spec/ruby-grape-danger/reporter_spec.rb b/spec/ruby-grape-danger/reporter_spec.rb new file mode 100644 index 0000000..0890903 --- /dev/null +++ b/spec/ruby-grape-danger/reporter_spec.rb @@ -0,0 +1,217 @@ +require 'spec_helper' +require 'json' +require 'tempfile' + +RSpec.describe RubyGrapeDanger::Reporter do + let(:status_report) do + { + errors: ['Error 1', 'Error 2'], + warnings: ['Warning 1'], + messages: ['Message 1', 'Message 2'], + markdowns: ['## Markdown 1'] + } + end + + let(:event_json) do + { + 'pull_request' => { + 'number' => 42 + } + } + end + + let(:reporter) { RubyGrapeDanger::Reporter.new(status_report) } + + describe '#initialize' do + it 'stores the status_report' do + expect(reporter.instance_variable_get(:@status_report)).to eq(status_report) + end + end + + describe '#export_json' do + let(:report_file) { Tempfile.new('danger_report.json') } + let(:event_file) { Tempfile.new('event.json') } + + before do + event_file.write(JSON.generate(event_json)) + event_file.close + end + + after do + report_file.unlink + event_file.unlink + end + + it 'creates a JSON report with all fields' do + reporter.export_json(report_file.path, event_file.path) + + report = JSON.parse(File.read(report_file.path)) + expect(report['pr_number']).to eq(42) + expect(report['errors']).to eq(['Error 1', 'Error 2']) + expect(report['warnings']).to eq(['Warning 1']) + expect(report['messages']).to eq(['Message 1', 'Message 2']) + expect(report['markdowns']).to eq(['## Markdown 1']) + end + + it 'formats the JSON nicely (pretty printed)' do + reporter.export_json(report_file.path, event_file.path) + + content = File.read(report_file.path) + expect(content).to include("\n") + expect(content).to include(" ") + end + + context 'with message objects (not strings)' do + let(:status_report) do + error_obj = double('error', message: 'Object error') + warning_obj = double('warning', message: 'Object warning') + + { + errors: [error_obj], + warnings: [warning_obj], + messages: [], + markdowns: [] + } + end + + it 'converts objects with message method to strings' do + reporter.export_json(report_file.path, event_file.path) + + report = JSON.parse(File.read(report_file.path)) + expect(report['errors']).to eq(['Object error']) + expect(report['warnings']).to eq(['Object warning']) + end + end + + context 'with mixed message types' do + let(:status_report) do + obj = double('mixed', message: 'Object message') + + { + errors: ['String error', obj], + warnings: [], + messages: [], + markdowns: [] + } + end + + it 'handles both strings and objects' do + reporter.export_json(report_file.path, event_file.path) + + report = JSON.parse(File.read(report_file.path)) + expect(report['errors']).to eq(['String error', 'Object message']) + end + end + + context 'with empty arrays' do + let(:status_report) do + { + errors: [], + warnings: [], + messages: [], + markdowns: [] + } + end + + it 'creates report with empty arrays' do + reporter.export_json(report_file.path, event_file.path) + + report = JSON.parse(File.read(report_file.path)) + expect(report['errors']).to eq([]) + expect(report['warnings']).to eq([]) + expect(report['messages']).to eq([]) + expect(report['markdowns']).to eq([]) + end + end + + context 'with nil values' do + let(:status_report) do + { + errors: nil, + warnings: nil, + messages: nil, + markdowns: nil + } + end + + it 'converts nil to empty array' do + reporter.export_json(report_file.path, event_file.path) + + report = JSON.parse(File.read(report_file.path)) + expect(report['errors']).to eq([]) + expect(report['warnings']).to eq([]) + expect(report['messages']).to eq([]) + expect(report['markdowns']).to eq([]) + end + end + + context 'when report_path is nil' do + it 'does not create a file' do + reporter.export_json(nil, event_file.path) + + # If file was created, we would have a different path + expect(File.exist?(report_file.path)).to be true + end + end + + context 'when event_path is nil' do + it 'does not create a file' do + reporter.export_json(report_file.path, nil) + + expect(File.size(report_file.path)).to eq(0) + end + end + + context 'when event file does not exist' do + it 'does not create a report file' do + reporter.export_json(report_file.path, '/nonexistent/path/event.json') + + expect(File.size(report_file.path)).to eq(0) + end + end + + context 'when event has no pull_request.number' do + let(:event_json) do + { + 'pull_request' => {} + } + end + + it 'does not create a report file' do + reporter.export_json(report_file.path, event_file.path) + + expect(File.size(report_file.path)).to eq(0) + end + end + + context 'when event has no pull_request key' do + let(:event_json) do + {} + end + + it 'does not create a report file' do + reporter.export_json(report_file.path, event_file.path) + + expect(File.size(report_file.path)).to eq(0) + end + end + + context 'with multiline markdown' do + let(:status_report) do + { + errors: [], + warnings: [], + messages: [], + markdowns: ["## Details\n\nSome content"] + } + end + + it 'preserves multiline markdown' do + reporter.export_json(report_file.path, event_file.path) + + report = JSON.parse(File.read(report_file.path)) + expect(report['markdowns']).to eq(["## Details\n\nSome content"]) + end + end + end +end From c1ebf1580068ffe733c038a3e45533d7499e083c Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 01:24:26 +0100 Subject: [PATCH 02/10] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eb938e..7e0677b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### 0.2.2 (Next) -* Your contribution here. +* [#15](https://github.com/ruby-grape/danger/pull/15): Extract danger reporting infrastructure into reusable workflows and gem - [@numbata](https://github.com/numbata). ### 0.2.1 (2024/02/01) From fe0acfc8b989eae79f4598fd37dc90157d1078ec Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 01:34:08 +0100 Subject: [PATCH 03/10] Document how to import ruby-grape-danger Dangerfile for reporting --- Dangerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dangerfile b/Dangerfile index bdaf195..d4866c7 100644 --- a/Dangerfile +++ b/Dangerfile @@ -3,6 +3,10 @@ require 'ruby-grape-danger' require 'English' +# This Dangerfile provides automatic danger report export and standard checks for Grape projects. +# Other projects can import this via: danger.import_dangerfile(gem: 'ruby-grape-danger') +# to get automatic reporting with their own custom checks. + # -------------------------------------------------------------------------------------------------------------------- # Automatically export danger report when Dangerfile finishes # -------------------------------------------------------------------------------------------------------------------- From 0b458cc300c9c8666aa96b365ec3f1f4e7b99cfa Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 01:46:47 +0100 Subject: [PATCH 04/10] Add debug logging to at_exit hook --- Dangerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dangerfile b/Dangerfile index d4866c7..164515f 100644 --- a/Dangerfile +++ b/Dangerfile @@ -11,14 +11,22 @@ require 'English' # Automatically export danger report when Dangerfile finishes # -------------------------------------------------------------------------------------------------------------------- at_exit do + puts "DEBUG: at_exit hook running" + puts "DEBUG: $ERROR_INFO = #{$ERROR_INFO.inspect}" + puts "DEBUG: status_report defined? = #{defined?(status_report).inspect}" + next if $ERROR_INFO # Skip if an exception occurred if defined?(status_report) + puts "DEBUG: Exporting danger report" reporter = RubyGrapeDanger::Reporter.new(status_report) reporter.export_json( ENV.fetch('DANGER_REPORT_PATH', nil), ENV.fetch('GITHUB_EVENT_PATH', nil) ) + puts "DEBUG: Danger report exported successfully" + else + puts "DEBUG: status_report not defined, skipping export" end end From e89d519e7d1b188d140ed7c93da037f58062f83e Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 01:51:10 +0100 Subject: [PATCH 05/10] Add comprehensive debugging to at_exit hook --- Dangerfile | 57 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/Dangerfile b/Dangerfile index 164515f..fab3d05 100644 --- a/Dangerfile +++ b/Dangerfile @@ -11,22 +11,47 @@ require 'English' # Automatically export danger report when Dangerfile finishes # -------------------------------------------------------------------------------------------------------------------- at_exit do - puts "DEBUG: at_exit hook running" - puts "DEBUG: $ERROR_INFO = #{$ERROR_INFO.inspect}" - puts "DEBUG: status_report defined? = #{defined?(status_report).inspect}" - - next if $ERROR_INFO # Skip if an exception occurred - - if defined?(status_report) - puts "DEBUG: Exporting danger report" - reporter = RubyGrapeDanger::Reporter.new(status_report) - reporter.export_json( - ENV.fetch('DANGER_REPORT_PATH', nil), - ENV.fetch('GITHUB_EVENT_PATH', nil) - ) - puts "DEBUG: Danger report exported successfully" - else - puts "DEBUG: status_report not defined, skipping export" + # Only skip if there's an actual exception (not SystemExit from danger calling exit) + next if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) + + # Try to export the danger report + begin + puts "DEBUG at_exit: Trying to find status_report" + puts "DEBUG at_exit: defined?(Danger) = #{defined?(Danger)}" + puts "DEBUG at_exit: Danger.class = #{Danger.class}" if defined?(Danger) + + # Try multiple ways to access the status report + danger_report = nil + + # Method 1: Try Danger.current_dangerfile + if defined?(Danger) && Danger.respond_to?(:current_dangerfile) + puts "DEBUG at_exit: Danger.current_dangerfile exists" + danger_report = Danger.current_dangerfile.status_report + end + + # Method 2: Try via ObjectSpace to find active dangerfile + unless danger_report + puts "DEBUG at_exit: Looking for Danger::Dangerfile via ObjectSpace" + ObjectSpace.each_object(Danger::Dangerfile) do |df| + danger_report = df.status_report + break + end + end + + if danger_report + puts "DEBUG at_exit: Found danger_report, exporting" + reporter = RubyGrapeDanger::Reporter.new(danger_report) + reporter.export_json( + ENV.fetch('DANGER_REPORT_PATH', nil), + ENV.fetch('GITHUB_EVENT_PATH', nil) + ) + else + puts "DEBUG at_exit: Could not find danger_report" + end + rescue => e + # Log any errors but don't fail the entire exit + puts "ERROR at_exit: #{e.class} - #{e.message}" + puts e.backtrace.join("\n") end end From 5c00f2b0bdeaf911b9789a638a49f6eaaaf197ce Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 01:54:46 +0100 Subject: [PATCH 06/10] Simplify at_exit hook - try direct status_report access --- Dangerfile | 46 ++++++++-------------------------------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/Dangerfile b/Dangerfile index fab3d05..edd560a 100644 --- a/Dangerfile +++ b/Dangerfile @@ -14,44 +14,14 @@ at_exit do # Only skip if there's an actual exception (not SystemExit from danger calling exit) next if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) - # Try to export the danger report - begin - puts "DEBUG at_exit: Trying to find status_report" - puts "DEBUG at_exit: defined?(Danger) = #{defined?(Danger)}" - puts "DEBUG at_exit: Danger.class = #{Danger.class}" if defined?(Danger) - - # Try multiple ways to access the status report - danger_report = nil - - # Method 1: Try Danger.current_dangerfile - if defined?(Danger) && Danger.respond_to?(:current_dangerfile) - puts "DEBUG at_exit: Danger.current_dangerfile exists" - danger_report = Danger.current_dangerfile.status_report - end - - # Method 2: Try via ObjectSpace to find active dangerfile - unless danger_report - puts "DEBUG at_exit: Looking for Danger::Dangerfile via ObjectSpace" - ObjectSpace.each_object(Danger::Dangerfile) do |df| - danger_report = df.status_report - break - end - end - - if danger_report - puts "DEBUG at_exit: Found danger_report, exporting" - reporter = RubyGrapeDanger::Reporter.new(danger_report) - reporter.export_json( - ENV.fetch('DANGER_REPORT_PATH', nil), - ENV.fetch('GITHUB_EVENT_PATH', nil) - ) - else - puts "DEBUG at_exit: Could not find danger_report" - end - rescue => e - # Log any errors but don't fail the entire exit - puts "ERROR at_exit: #{e.class} - #{e.message}" - puts e.backtrace.join("\n") + # Export the danger report + # The status_report method is available from the Dangerfile DSL + if defined?(Danger) && defined?(status_report) + reporter = RubyGrapeDanger::Reporter.new(status_report) + reporter.export_json( + ENV.fetch('DANGER_REPORT_PATH', nil), + ENV.fetch('GITHUB_EVENT_PATH', nil) + ) end end From aa3026e7807b523836f882f3eb364e0e1bf45a32 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 02:24:50 +0100 Subject: [PATCH 07/10] Simplify report capture - use closure to access status_report Just capture status_report as a local variable before at_exit block. Ruby closures allow the at_exit block to access the outer scope variable. Much simpler than module variables or separate classes. --- Dangerfile | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dangerfile b/Dangerfile index edd560a..bde9880 100644 --- a/Dangerfile +++ b/Dangerfile @@ -10,14 +10,16 @@ require 'English' # -------------------------------------------------------------------------------------------------------------------- # Automatically export danger report when Dangerfile finishes # -------------------------------------------------------------------------------------------------------------------- +# Capture status_report for use in at_exit block +report = status_report + at_exit do # Only skip if there's an actual exception (not SystemExit from danger calling exit) next if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) - # Export the danger report - # The status_report method is available from the Dangerfile DSL - if defined?(Danger) && defined?(status_report) - reporter = RubyGrapeDanger::Reporter.new(status_report) + # Export the danger report captured above + if report + reporter = RubyGrapeDanger::Reporter.new(report) reporter.export_json( ENV.fetch('DANGER_REPORT_PATH', nil), ENV.fetch('GITHUB_EVENT_PATH', nil) From 7ca183d48cf7150fa86c8cf8279eaee366286c20 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 02:37:38 +0100 Subject: [PATCH 08/10] Use ObjectSpace to access Dangerfile status_report in at_exit This is the most reliable way since status_report is a method that returns the mutable report object populated by danger checks. --- Dangerfile | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Dangerfile b/Dangerfile index bde9880..ee6cd4b 100644 --- a/Dangerfile +++ b/Dangerfile @@ -7,23 +7,19 @@ require 'English' # Other projects can import this via: danger.import_dangerfile(gem: 'ruby-grape-danger') # to get automatic reporting with their own custom checks. -# -------------------------------------------------------------------------------------------------------------------- -# Automatically export danger report when Dangerfile finishes -# -------------------------------------------------------------------------------------------------------------------- -# Capture status_report for use in at_exit block -report = status_report - +# Register at_exit hook to export report when Dangerfile finishes at_exit do # Only skip if there's an actual exception (not SystemExit from danger calling exit) next if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit) - # Export the danger report captured above - if report - reporter = RubyGrapeDanger::Reporter.new(report) + # Find the Dangerfile instance and get its current status_report + ObjectSpace.each_object(Danger::Dangerfile) do |df| + reporter = RubyGrapeDanger::Reporter.new(df.status_report) reporter.export_json( ENV.fetch('DANGER_REPORT_PATH', nil), ENV.fetch('GITHUB_EVENT_PATH', nil) ) + break end end From a40100aa1ec636c9ecc59cc2a4b060239e2f29e7 Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 02:43:56 +0100 Subject: [PATCH 09/10] Add reusable danger-run.yml and danger-comment.yml workflows These workflows provide the shared Danger integration infrastructure for Grape projects. The old single danger.yml is replaced by two-stage workflow pattern: - danger-run.yml: Execute danger checks and export report - danger-comment.yml: Post/update PR comment with results --- .github/workflows/danger.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/danger.yml diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml deleted file mode 100644 index 4d4e6ae..0000000 --- a/.github/workflows/danger.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: danger -on: pull_request - -jobs: - danger: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 100 - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.2 - bundler-cache: true - rubygems: latest - - name: Run Danger - run: | - # the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok - TOKEN=$(echo -n Z2hwX2lYb0dPNXNyejYzOFJyaTV3QUxUdkNiS1dtblFwZTFuRXpmMwo= | base64 --decode) - DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose From 6c9ab1bca542b9b25e03aa56c3b03b1696c8d68d Mon Sep 17 00:00:00 2001 From: Andrei Subbota Date: Wed, 17 Dec 2025 02:49:38 +0100 Subject: [PATCH 10/10] Update documentation for reusable workflows - Add subsections to Table of Contents for Reusable Workflows section - Promote Table of Contents to proper heading level (#) - Add placeholder entry in CHANGELOG for next contribution --- CHANGELOG.md | 1 + README.md | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e0677b..9adad5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### 0.2.2 (Next) +* Your contribution here. * [#15](https://github.com/ruby-grape/danger/pull/15): Extract danger reporting infrastructure into reusable workflows and gem - [@numbata](https://github.com/numbata). ### 0.2.1 (2024/02/01) diff --git a/README.md b/README.md index 306ca51..1608b96 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/ruby-grape/danger.svg?branch=master)](https://travis-ci.org/ruby-grape/danger) -## Table of Contents +# Table of Contents - [Setup](#setup) - [Add Danger](#add-danger) @@ -12,6 +12,10 @@ - [Add GitHub Actions Workflows](#add-github-actions-workflows) - [Commit via a Pull Request](#commit-via-a-pull-request) - [Reusable Workflows](#reusable-workflows) + - [Architecture](#architecture) + - [Benefits of Reusable Workflows](#benefits-of-reusable-workflows) + - [How It Works](#how-it-works) + - [Examples](#examples) - [License](#license) ## Setup