Skip to content
This repository was archived by the owner on Dec 30, 2025. It is now read-only.

Commit b037869

Browse files
authored
Merge pull request #15 from numbata/report-workflows
Extract danger reporting infrastructure into reusable workflows and gem
2 parents 0622a37 + 6c9ab1b commit b037869

File tree

9 files changed

+537
-25
lines changed

9 files changed

+537
-25
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
name: Danger Comment
2+
on:
3+
workflow_run:
4+
workflows: [Danger]
5+
types: [completed]
6+
workflow_call:
7+
8+
permissions:
9+
actions: read
10+
contents: read
11+
issues: write
12+
pull-requests: write
13+
14+
jobs:
15+
comment:
16+
runs-on: ubuntu-latest
17+
if: |
18+
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
19+
|| github.event_name == 'workflow_call'
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 1
25+
- name: Download Danger Report (workflow_run)
26+
if: github.event_name == 'workflow_run'
27+
uses: actions/download-artifact@v4
28+
continue-on-error: true
29+
with:
30+
name: danger-report
31+
run-id: ${{ github.event.workflow_run.id }}
32+
repository: ${{ github.event.workflow_run.repository.full_name }}
33+
github-token: ${{ secrets.GITHUB_TOKEN }}
34+
- name: Download Danger Report (reusable call)
35+
if: github.event_name == 'workflow_call'
36+
uses: actions/download-artifact@v4
37+
continue-on-error: true
38+
with:
39+
name: danger-report
40+
- name: Post or Update PR Comment
41+
uses: actions/github-script@v7
42+
with:
43+
script: |
44+
const fs = require('fs');
45+
46+
const hasItems = (arr) => Array.isArray(arr) && arr.length > 0;
47+
48+
let report;
49+
try {
50+
report = JSON.parse(fs.readFileSync('danger_report.json', 'utf8'));
51+
} catch (e) {
52+
console.log('No danger report found, skipping comment');
53+
return;
54+
}
55+
56+
if (!report.pr_number) {
57+
console.log('No PR number found in report, skipping comment');
58+
return;
59+
}
60+
61+
let body = '## Danger Report\n\n';
62+
63+
if (hasItems(report.errors)) {
64+
body += '### ❌ Errors\n';
65+
report.errors.forEach(e => body += `- ${e}\n`);
66+
body += '\n';
67+
}
68+
69+
if (hasItems(report.warnings)) {
70+
body += '### ⚠️ Warnings\n';
71+
report.warnings.forEach(w => body += `- ${w}\n`);
72+
body += '\n';
73+
}
74+
75+
if (hasItems(report.messages)) {
76+
body += '### ℹ️ Messages\n';
77+
report.messages.forEach(m => body += `- ${m}\n`);
78+
body += '\n';
79+
}
80+
81+
if (hasItems(report.markdowns)) {
82+
report.markdowns.forEach(md => body += `${md}\n\n`);
83+
}
84+
85+
if (!hasItems(report.errors) &&
86+
!hasItems(report.warnings) &&
87+
!hasItems(report.messages) &&
88+
!hasItems(report.markdowns)) {
89+
body += '✅ All checks passed!';
90+
}
91+
92+
const { data: comments } = await github.rest.issues.listComments({
93+
owner: context.repo.owner,
94+
repo: context.repo.repo,
95+
issue_number: report.pr_number
96+
});
97+
98+
const botComment = comments.find(c =>
99+
c.user.login === 'github-actions[bot]' &&
100+
c.body.includes('## Danger Report')
101+
);
102+
103+
if (botComment) {
104+
await github.rest.issues.updateComment({
105+
owner: context.repo.owner,
106+
repo: context.repo.repo,
107+
comment_id: botComment.id,
108+
body: body
109+
});
110+
} else {
111+
await github.rest.issues.createComment({
112+
owner: context.repo.owner,
113+
repo: context.repo.repo,
114+
issue_number: report.pr_number,
115+
body: body
116+
});
117+
}
118+
119+
// Fail if there are errors
120+
if (report.errors && report.errors.length > 0) {
121+
core.setFailed('Danger found errors');
122+
}

.github/workflows/danger-run.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Danger
2+
on:
3+
pull_request:
4+
types: [ opened, reopened, edited, synchronize ]
5+
workflow_call:
6+
jobs:
7+
danger:
8+
name: Danger
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v3
13+
with:
14+
fetch-depth: 0
15+
- name: Set up Ruby
16+
uses: ruby/setup-ruby@v1
17+
with:
18+
ruby-version: 2.7
19+
bundler-cache: true
20+
- name: Run Danger
21+
# Note: We use 'dry_run' mode intentionally as part of a two-workflow pattern.
22+
# The actual commenting on GitHub is handled by the danger-comment.yml workflow.
23+
run: bundle exec danger dry_run --verbose
24+
env:
25+
DANGER_REPORT_PATH: danger_report.json
26+
- name: Upload Danger Report
27+
if: always()
28+
uses: actions/upload-artifact@v4
29+
with:
30+
name: danger-report
31+
path: danger_report.json
32+
retention-days: 1
33+
if-no-files-found: ignore

.github/workflows/danger.yml

Lines changed: 0 additions & 21 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 0.2.2 (Next)
44

55
* Your contribution here.
6+
* [#15](https://github.com/ruby-grape/danger/pull/15): Extract danger reporting infrastructure into reusable workflows and gem - [@numbata](https://github.com/numbata).
67

78
### 0.2.1 (2024/02/01)
89

Dangerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# frozen_string_literal: true
22

3+
require 'ruby-grape-danger'
4+
require 'English'
5+
6+
# This Dangerfile provides automatic danger report export and standard checks for Grape projects.
7+
# Other projects can import this via: danger.import_dangerfile(gem: 'ruby-grape-danger')
8+
# to get automatic reporting with their own custom checks.
9+
10+
# Register at_exit hook to export report when Dangerfile finishes
11+
at_exit do
12+
# Only skip if there's an actual exception (not SystemExit from danger calling exit)
13+
next if $ERROR_INFO && !$ERROR_INFO.is_a?(SystemExit)
14+
15+
# Find the Dangerfile instance and get its current status_report
16+
ObjectSpace.each_object(Danger::Dangerfile) do |df|
17+
reporter = RubyGrapeDanger::Reporter.new(df.status_report)
18+
reporter.export_json(
19+
ENV.fetch('DANGER_REPORT_PATH', nil),
20+
ENV.fetch('GITHUB_EVENT_PATH', nil)
21+
)
22+
break
23+
end
24+
end
25+
326
# --------------------------------------------------------------------------------------------------------------------
427
# Has any changes happened inside the actual library code?
528
# --------------------------------------------------------------------------------------------------------------------

README.md

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44

55
[![Build Status](https://travis-ci.org/ruby-grape/danger.svg?branch=master)](https://travis-ci.org/ruby-grape/danger)
66

7-
## Table of Contents
7+
# Table of Contents
88

99
- [Setup](#setup)
10-
- [Set DANGER_GITHUB_API_TOKEN in Travis-CI](#set-danger_github_api_token-in-travis-ci)
1110
- [Add Danger](#add-danger)
1211
- [Add Dangerfile](#add-dangerfile)
13-
- [Add Danger to Travis-CI](#add-danger-to-travis-ci)
12+
- [Add GitHub Actions Workflows](#add-github-actions-workflows)
1413
- [Commit via a Pull Request](#commit-via-a-pull-request)
14+
- [Reusable Workflows](#reusable-workflows)
15+
- [Architecture](#architecture)
16+
- [Benefits of Reusable Workflows](#benefits-of-reusable-workflows)
17+
- [How It Works](#how-it-works)
18+
- [Examples](#examples)
1519
- [License](#license)
1620

1721
## Setup
@@ -28,16 +32,106 @@ gem 'ruby-grape-danger', require: false
2832

2933
### Add Dangerfile
3034

31-
Commit a `Dangerfile`, eg. [Grape's Dangerfile](https://github.com/ruby-grape/grape/blob/master/Dangerfile).
35+
Create a `Dangerfile` in your project's root that imports `ruby-grape-danger` and adds your project-specific checks:
3236

3337
```ruby
3438
danger.import_dangerfile(gem: 'ruby-grape-danger')
39+
40+
# Your project-specific danger checks
41+
changelog.check!
42+
toc.check!
43+
```
44+
45+
The `ruby-grape-danger` Dangerfile automatically handles:
46+
- Setting up the reporting infrastructure
47+
- Exporting the danger report via `at_exit` hook when the Dangerfile finishes
48+
- Consistent output format for the workflow
49+
50+
### Add GitHub Actions Workflows
51+
52+
Create `.github/workflows/danger.yml`:
53+
54+
```yaml
55+
name: Danger
56+
on:
57+
pull_request:
58+
types: [ opened, reopened, edited, synchronize ]
59+
workflow_call:
60+
61+
jobs:
62+
danger:
63+
uses: ruby-grape/ruby-grape-danger/.github/workflows/danger-run.yml@main
64+
```
65+
66+
Create `.github/workflows/danger-comment.yml`:
67+
68+
```yaml
69+
name: Danger Comment
70+
on:
71+
workflow_run:
72+
workflows: [Danger]
73+
types: [completed]
74+
workflow_call:
75+
76+
jobs:
77+
comment:
78+
uses: ruby-grape/ruby-grape-danger/.github/workflows/danger-comment.yml@main
3579
```
3680

3781
### Commit via a Pull Request
3882

3983
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.
4084

85+
## Reusable Workflows
86+
87+
This gem provides **reusable GitHub Actions workflows** that can be referenced by any Grape project to implement standardized Danger checks with consistent reporting.
88+
89+
### Architecture
90+
91+
The workflows are separated into two stages:
92+
93+
1. **danger-run.yml**: Executes Danger checks and generates a report
94+
- Runs `bundle exec danger dry_run` with your project's Dangerfile
95+
- Generates a JSON report of check results
96+
- Uploads the report as an artifact
97+
98+
2. **danger-comment.yml**: Posts/updates PR comments with results
99+
- Downloads the Danger report artifact
100+
- Formats and posts results as a PR comment
101+
- Updates existing comment on subsequent runs
102+
103+
### Benefits of Reusable Workflows
104+
105+
✅ **DRY**: Define workflows once in `ruby-grape-danger`, reuse everywhere
106+
✅ **Consistent**: All Grape projects use the same reporting format and behavior
107+
✅ **Maintainable**: Fix a bug in the workflows once, all projects benefit automatically
108+
✅ **Scalable**: Add new checks to any project's Dangerfile without touching workflows
109+
110+
### How It Works
111+
112+
When you reference the reusable workflows:
113+
114+
```yaml
115+
uses: ruby-grape/ruby-grape-danger/.github/workflows/danger-run.yml@main
116+
```
117+
118+
GitHub Actions:
119+
1. Checks out **your project's repository** (not ruby-grape-danger)
120+
2. Installs dependencies from **your Gemfile**
121+
3. Runs danger using **your Dangerfile**
122+
- Your Dangerfile imports `ruby-grape-danger`'s Dangerfile via `danger.import_dangerfile(gem: 'ruby-grape-danger')`
123+
- The imported Dangerfile registers an `at_exit` hook for automatic reporting
124+
- Runs your project-specific checks (added after the import)
125+
- When Dangerfile finishes, the `at_exit` hook automatically exports the report
126+
4. The report is uploaded as an artifact for the commenting workflow
127+
128+
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.
129+
130+
### Examples
131+
132+
- [danger-changelog](https://github.com/ruby-grape/danger-changelog) - Validates CHANGELOG format
133+
- [grape](https://github.com/ruby-grape/grape) - Multi-check danger implementation
134+
41135
## License
42136

43137
MIT License. See [LICENSE](LICENSE) for details.

lib/ruby-grape-danger.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require 'ruby-grape-danger/version'
2+
require 'ruby-grape-danger/reporter'
3+
4+
module RubyGrapeDanger
5+
end

lib/ruby-grape-danger/reporter.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'json'
2+
3+
module RubyGrapeDanger
4+
class Reporter
5+
def initialize(status_report)
6+
@status_report = status_report
7+
end
8+
9+
def export_json(report_path, event_path)
10+
return unless report_path && event_path && File.exist?(event_path)
11+
12+
event = JSON.parse(File.read(event_path))
13+
pr_number = event.dig('pull_request', 'number')
14+
return unless pr_number
15+
16+
report = build_report(pr_number)
17+
File.write(report_path, JSON.pretty_generate(report))
18+
end
19+
20+
private
21+
22+
def build_report(pr_number)
23+
{
24+
pr_number: pr_number,
25+
errors: to_messages(@status_report[:errors]),
26+
warnings: to_messages(@status_report[:warnings]),
27+
messages: to_messages(@status_report[:messages]),
28+
markdowns: to_messages(@status_report[:markdowns])
29+
}
30+
end
31+
32+
def to_messages(items)
33+
Array(items).map do |item|
34+
item.respond_to?(:message) ? item.message : item.to_s
35+
end
36+
end
37+
end
38+
end

0 commit comments

Comments
 (0)