Skip to content

Commit f7113a2

Browse files
committed
Remove API token requirement from Danger workflow
Use secure two-workflow pattern from dblock's blog post: - First workflow runs on pull_request, executes danger dry_run without write permissions, uploads JSON report as artifact - Second workflow runs on workflow_run after first completes, downloads artifact and posts PR comment with write access This is secure because untrusted fork code never has access to write permissions. The comment workflow runs trusted code from the base branch. Inlined Dangerfile checks from ruby-grape-danger to avoid plugins that require GitHub API methods not available in dry_run mode.
1 parent d4244ab commit f7113a2

File tree

3 files changed

+143
-7
lines changed

3 files changed

+143
-7
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
name: danger-comment
3+
on:
4+
workflow_run:
5+
workflows: [danger]
6+
types: [completed]
7+
8+
permissions:
9+
contents: read
10+
pull-requests: write
11+
12+
jobs:
13+
comment:
14+
runs-on: ubuntu-latest
15+
if: >
16+
github.event.workflow_run.event == 'pull_request' &&
17+
github.event.workflow_run.pull_requests[0]
18+
steps:
19+
- name: Download Danger report
20+
uses: actions/download-artifact@v6
21+
with:
22+
name: danger-report
23+
github-token: ${{ github.token }}
24+
run-id: ${{ github.event.workflow_run.id }}
25+
- name: Post or update PR comment
26+
uses: actions/github-script@v8
27+
with:
28+
script: |
29+
const fs = require('fs');
30+
const report = JSON.parse(fs.readFileSync('danger_report.json', 'utf8'));
31+
32+
let body = '## Danger Report\n\n';
33+
34+
if (report.errors && report.errors.length > 0) {
35+
body += '### Errors\n';
36+
report.errors.forEach(e => body += `- :no_entry_sign: ${e}\n`);
37+
body += '\n';
38+
}
39+
40+
if (report.warnings && report.warnings.length > 0) {
41+
body += '### Warnings\n';
42+
report.warnings.forEach(w => body += `- :warning: ${w}\n`);
43+
body += '\n';
44+
}
45+
46+
if (report.messages && report.messages.length > 0) {
47+
body += '### Messages\n';
48+
report.messages.forEach(m => body += `- :book: ${m}\n`);
49+
body += '\n';
50+
}
51+
52+
if ((!report.errors || report.errors.length === 0) &&
53+
(!report.warnings || report.warnings.length === 0) &&
54+
(!report.messages || report.messages.length === 0)) {
55+
body += ':white_check_mark: All checks passed!';
56+
}
57+
58+
const prNumber = ${{ github.event.workflow_run.pull_requests[0].number }};
59+
const { data: comments } = await github.rest.issues.listComments({
60+
owner: context.repo.owner,
61+
repo: context.repo.repo,
62+
issue_number: prNumber
63+
});
64+
65+
const botComment = comments.find(c =>
66+
c.user.login === 'github-actions[bot]' &&
67+
c.body.includes('## Danger Report')
68+
);
69+
70+
if (botComment) {
71+
await github.rest.issues.updateComment({
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
comment_id: botComment.id,
75+
body: body
76+
});
77+
} else {
78+
await github.rest.issues.createComment({
79+
owner: context.repo.owner,
80+
repo: context.repo.repo,
81+
issue_number: prNumber,
82+
body: body
83+
});
84+
}
85+
86+
// Fail if there are errors
87+
if (report.errors && report.errors.length > 0) {
88+
core.setFailed('Danger found errors');
89+
}

.github/workflows/danger.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,22 @@ jobs:
66
danger:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v4
9+
- uses: actions/checkout@v6
1010
with:
11-
fetch-depth: 100
11+
fetch-depth: 0
1212
- name: Set up Ruby
1313
uses: ruby/setup-ruby@v1
1414
with:
1515
ruby-version: 3.4
1616
bundler-cache: true
1717
- name: Run Danger
18-
run: |
19-
# the token is public, has public_repo scope and belongs to the grape-bot user owned by @dblock, this is ok
20-
TOKEN=$(echo -n Z2hwX2lYb0dPNXNyejYzOFJyaTV3QUxUdkNiS1dtblFwZTFuRXpmMwo= | base64 --decode)
21-
DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose
18+
env:
19+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
20+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
21+
DANGER_REPORT_PATH: danger_report.json
22+
run: bundle exec danger dry_run --base=$BASE_SHA --head=$HEAD_SHA --verbose
23+
- name: Upload Danger report
24+
uses: actions/upload-artifact@v5
25+
with:
26+
name: danger-report
27+
path: danger_report.json

Dangerfile

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,44 @@
11
# frozen_string_literal: true
22

3-
danger.import_dangerfile(gem: 'ruby-grape-danger')
3+
# Inline checks from ruby-grape-danger (avoids plugins requiring GitHub API token)
4+
5+
has_app_changes = !git.modified_files.grep(/lib/).empty?
6+
has_spec_changes = !git.modified_files.grep(/spec/).empty?
7+
8+
if has_app_changes && !has_spec_changes
9+
warn("There're library changes, but not tests. That's OK as long as you're refactoring existing code.", sticky: false)
10+
end
11+
12+
if !has_app_changes && has_spec_changes
13+
message('We really appreciate pull requests that demonstrate issues, even without a fix. That said, the next step is to try and fix the failing tests!', sticky: false)
14+
end
15+
16+
# Simplified changelog check (replaces danger-changelog plugin which requires github.* methods)
17+
# Note: toc.check! from danger-toc plugin removed (not essential for CI)
18+
has_changelog_changes = git.modified_files.include?('CHANGELOG.md') || git.added_files.include?('CHANGELOG.md')
19+
if has_app_changes && !has_changelog_changes
20+
warn('Please update CHANGELOG.md with a description of your changes.', sticky: false)
21+
end
22+
23+
(git.modified_files + git.added_files - %w[Dangerfile]).each do |file|
24+
next unless File.file?(file)
25+
26+
contents = File.read(file)
27+
if file.start_with?('spec')
28+
fail("`xit` or `fit` left in tests (#{file})") if contents =~ /^\w*[xf]it/
29+
fail("`fdescribe` left in tests (#{file})") if contents =~ /^\w*fdescribe/
30+
end
31+
end
32+
33+
# Output JSON report for GitHub Actions workflow_run to post as PR comment
34+
if ENV['DANGER_REPORT_PATH']
35+
require 'json'
36+
37+
report = {
38+
errors: violation_report[:errors].map(&:message),
39+
warnings: violation_report[:warnings].map(&:message),
40+
messages: violation_report[:messages].map(&:message)
41+
}
42+
43+
File.write(ENV['DANGER_REPORT_PATH'], JSON.pretty_generate(report))
44+
end

0 commit comments

Comments
 (0)