-
Notifications
You must be signed in to change notification settings - Fork 9
Extract danger reporting infrastructure into reusable workflows and gem #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b98d0fa
c1ebf15
fe0acfc
0b458cc
e89d519
5c00f2b
aa3026e
7ca183d
a40100a
6c9ab1b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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 | ||||||
|
||||||
| uses: actions/checkout@v3 | |
| uses: actions/checkout@v4 |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Ruby version 2.7 has reached end-of-life (EOL) in March 2023 and is no longer receiving security updates. Consider using a more recent Ruby version like 3.0 or later for better security and performance. This is especially important for a reusable workflow that will be used across multiple projects.
| ruby-version: 2.7 | |
| ruby-version: '3.1' |
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,28 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # frozen_string_literal: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+23
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 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) | |
| # 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 | |
| # Capture the Dangerfile instance evaluating this file so we don't need ObjectSpace traversal. | |
| dangerfile_instance = self if defined?(Danger::Dangerfile) && is_a?(Danger::Dangerfile) | |
| # 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) | |
| # Use the captured Dangerfile instance (if any) to get its current status_report | |
| df = dangerfile_instance | |
| next unless df | |
| reporter = RubyGrapeDanger::Reporter.new(df.status_report) | |
| reporter.export_json( | |
| ENV.fetch('DANGER_REPORT_PATH', nil), | |
| ENV.fetch('GITHUB_EVENT_PATH', nil) | |
| ) |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,14 +4,18 @@ | |||||
|
|
||||||
| [](https://travis-ci.org/ruby-grape/danger) | ||||||
|
|
||||||
| ## Table of Contents | ||||||
| # Table of Contents | ||||||
|
||||||
| # Table of Contents | |
| ## Table of Contents |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README examples reference the reusable Danger workflows using the mutable @main branch (in the uses: ruby-grape/ruby-grape-danger/.github/workflows/...@main lines). If downstream projects copy this configuration, a compromise of the ruby-grape-danger repository (or a malicious change to main) would immediately run attacker-controlled workflow code in their CI with access to their GITHUB_TOKEN and build artifacts. To avoid this supply-chain risk, update the examples to pin the reusable workflows to a specific commit SHA so consumers are not bound to a mutable branch reference.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| require 'ruby-grape-danger/version' | ||
| require 'ruby-grape-danger/reporter' | ||
|
|
||
| module RubyGrapeDanger | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,38 @@ | ||||||||||||||||||||||
| require 'json' | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| module RubyGrapeDanger | ||||||||||||||||||||||
| class Reporter | ||||||||||||||||||||||
| def initialize(status_report) | ||||||||||||||||||||||
| @status_report = status_report | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
Comment on lines
+4
to
+7
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def export_json(report_path, event_path) | ||||||||||||||||||||||
| return unless report_path && event_path && File.exist?(event_path) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| event = JSON.parse(File.read(event_path)) | ||||||||||||||||||||||
|
||||||||||||||||||||||
| event = JSON.parse(File.read(event_path)) | |
| begin | |
| event = JSON.parse(File.read(event_path)) | |
| rescue JSON::ParserError | |
| return | |
| end |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The to_messages method calls to_s on items that don't respond to :message. This means nil values will become the string "nil" rather than being filtered out. Consider whether empty or nil items should be excluded from the output, or if the current behavior is intentional for debugging purposes.
| Array(items).map do |item| | |
| item.respond_to?(:message) ? item.message : item.to_s | |
| Array(items).each_with_object([]) do |item, messages| | |
| msg = item.respond_to?(:message) ? item.message : item | |
| next if msg.nil? | |
| str = msg.to_s | |
| next if str.empty? | |
| messages << str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error checking is inconsistent. This checks for both existence and length of the errors array, but the hasItems function already performs both checks. Consider using hasItems(report.errors) here for consistency with the rest of the script.