Skip to content

Commit d6ae7f7

Browse files
lucas-zimermanvaindseer-by-sentry[bot]claude
authored
Feat: Allow repo specific dangerfile. (#129)
* branch cleanup * Update danger/action.yml Co-authored-by: Ivan Dlugos <[email protected]> * Update danger/action.yml Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com> * Update danger/action.yml Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com> * reviewed changes * fix customPath ref * load path * code ref fix / simplified transversal check * use env fix reference use instefof instead of invalid contains use * Apply suggestions from code review Co-authored-by: Ivan Dlugos <[email protected]> * applied suggestion * missing dangerfile input definition * fix empty EXTRA_DANGERFILE_INPUT * fix env name * applied code suggestions * Apply suggestion from @vaind * Apply suggestion from @vaind * chore: minor refactoring * test(danger): Add comprehensive tests for extra-dangerfile and extra-install-packages features Add test coverage for the new extra-dangerfile and extra-install-packages inputs: - Create test-dangerfile.js demonstrating custom Danger checks - Add extra-dangerfile-test job to verify custom dangerfiles execute correctly - Add extra-packages-test job to verify package installation works - Tests validate that custom dangerfiles can access the Danger API and installed packages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Ivan Dlugos <[email protected]> Co-authored-by: seer-by-sentry[bot] <157164994+seer-by-sentry[bot]@users.noreply.github.com> Co-authored-by: Ivan Dlugos <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 0ae70c0 commit d6ae7f7

File tree

5 files changed

+227
-6
lines changed

5 files changed

+227
-6
lines changed

.github/test-dangerfile.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Test dangerfile for exercising extra-dangerfile feature
2+
// This demonstrates how repositories can add custom Danger checks
3+
4+
module.exports = async function ({ fail, warn, message, markdown, danger }) {
5+
console.log('::notice::Running custom dangerfile checks...');
6+
7+
// Test that we have access to the danger API
8+
if (!danger || !danger.github || !danger.github.pr) {
9+
fail('Custom dangerfile cannot access danger API');
10+
return;
11+
}
12+
13+
// Example check: Verify PR has a description
14+
const prBody = danger.github.pr.body;
15+
if (!prBody || prBody.trim().length === 0) {
16+
warn('PR description is empty. Consider adding a description to help reviewers.');
17+
} else {
18+
message('✅ Custom dangerfile check: PR has a description');
19+
}
20+
21+
// Example check: Verify PR title is not too short
22+
const prTitle = danger.github.pr.title;
23+
if (prTitle && prTitle.length < 10) {
24+
warn('PR title is quite short. Consider making it more descriptive.');
25+
} else {
26+
message('✅ Custom dangerfile check: PR title length is reasonable');
27+
}
28+
29+
// Show that we can access git information
30+
const modifiedFiles = danger.git.modified_files || [];
31+
const createdFiles = danger.git.created_files || [];
32+
const totalChangedFiles = modifiedFiles.length + createdFiles.length;
33+
34+
message(`📊 Custom check: This PR changes ${totalChangedFiles} file(s)`);
35+
36+
console.log('::notice::Custom dangerfile checks completed successfully');
37+
};

.github/workflows/danger-workflow-tests.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,70 @@ jobs:
3434
3535
Write-Host "✅ Danger PR analysis completed successfully!"
3636
Write-Host "ℹ️ Check the PR comments for any Danger findings"
37+
38+
# Test extra-dangerfile feature
39+
extra-dangerfile-test:
40+
runs-on: ubuntu-latest
41+
steps:
42+
- uses: actions/checkout@v4
43+
44+
- name: Run danger with extra dangerfile
45+
id: danger-extra
46+
uses: ./danger
47+
with:
48+
extra-dangerfile: '.github/test-dangerfile.js'
49+
50+
- name: Validate danger with extra-dangerfile outputs
51+
env:
52+
DANGER_OUTCOME: ${{ steps.danger-extra.outputs.outcome }}
53+
shell: pwsh
54+
run: |
55+
Write-Host "🔍 Validating Danger action with extra-dangerfile..."
56+
Write-Host "Danger Outcome: '$env:DANGER_OUTCOME'"
57+
58+
# Validate that Danger ran successfully
59+
$env:DANGER_OUTCOME | Should -Be "success"
60+
61+
Write-Host "✅ Danger with extra-dangerfile completed successfully!"
62+
63+
# Test extra-install-packages feature
64+
extra-packages-test:
65+
runs-on: ubuntu-latest
66+
steps:
67+
- uses: actions/checkout@v4
68+
69+
# Create a test dangerfile that requires curl
70+
- name: Create test dangerfile requiring curl
71+
shell: bash
72+
run: |
73+
cat > .github/test-dangerfile-curl.js << 'EOF'
74+
module.exports = async function ({ message, danger }) {
75+
const { execSync } = require('child_process');
76+
try {
77+
const curlVersion = execSync('curl --version', { encoding: 'utf-8' });
78+
message('✅ curl is available: ' + curlVersion.split('\n')[0]);
79+
} catch (err) {
80+
throw new Error('curl command not found - extra-install-packages failed');
81+
}
82+
};
83+
EOF
84+
85+
- name: Run danger with extra packages
86+
id: danger-packages
87+
uses: ./danger
88+
with:
89+
extra-dangerfile: '.github/test-dangerfile-curl.js'
90+
extra-install-packages: 'curl'
91+
92+
- name: Validate danger with extra-install-packages outputs
93+
env:
94+
DANGER_OUTCOME: ${{ steps.danger-packages.outputs.outcome }}
95+
shell: pwsh
96+
run: |
97+
Write-Host "🔍 Validating Danger action with extra-install-packages..."
98+
Write-Host "Danger Outcome: '$env:DANGER_OUTCOME'"
99+
100+
# Validate that Danger ran successfully
101+
$env:DANGER_OUTCOME | Should -Be "success"
102+
103+
Write-Host "✅ Danger with extra-install-packages completed successfully!"

danger/README.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ jobs:
3030
* required: false
3131
* default: `${{ github.token }}`
3232

33+
* `extra-dangerfile`: Path to an additional dangerfile to run custom checks.
34+
* type: string
35+
* required: false
36+
* default: ""
37+
38+
* `extra-install-packages`: Additional packages that are required by the extra-dangerfile, you can find a list of packages here: https://packages.debian.org/search?suite=bookworm&keywords=curl.
39+
* type: string
40+
* required: false
41+
* default: ""
42+
3343
## Outputs
3444

3545
* `outcome`: Whether the Danger run finished successfully. Possible values are `success`, `failure`, `cancelled`, or `skipped`.
@@ -52,4 +62,18 @@ The Danger action runs the following checks:
5262
- **Conventional commits**: Validates commit message format and PR title conventions
5363
- **Cross-repo links**: Checks for proper formatting of links in changelog entries
5464

55-
For detailed rule implementations, see [dangerfile.js](dangerfile.js).
65+
For detailed rule implementations, see [dangerfile.js](dangerfile.js).
66+
67+
## Extra Danger File
68+
69+
When using an extra dangerfile, the file must be inside the repository and written in CommonJS syntax. You can use the following snippet to export your dangerfile:
70+
71+
```JavaScript
72+
module.exports = async function ({ fail, warn, message, markdown, danger }) {
73+
...
74+
const gitUrl = danger.github.pr.head.repo.git_url;
75+
...
76+
warn('...');
77+
}
78+
79+
```

danger/action.yml

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ inputs:
77
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
88
required: false
99
default: ${{ github.token }}
10+
extra-dangerfile:
11+
description: 'Path to additional dangerfile to run after the main checks'
12+
type: string
13+
required: false
14+
extra-install-packages:
15+
description: 'Additional apt packages to install in the DangerJS container (space-separated package names)'
16+
type: string
17+
required: false
1018

1119
outputs:
1220
outcome:
@@ -28,19 +36,62 @@ runs:
2836
shell: pwsh
2937
run: Get-Content '${{ github.action_path }}/danger.properties' | Tee-Object $env:GITHUB_OUTPUT -Append
3038

39+
# Validate extra-install-packages to prevent code injection
40+
- name: Validate package names
41+
if: ${{ inputs.extra-install-packages }}
42+
shell: pwsh
43+
env:
44+
EXTRA_INSTALL_PACKAGES: ${{ inputs.extra-install-packages }}
45+
run: |
46+
# Validate against Debian package naming rules: must start with alphanumeric,
47+
# contain only lowercase letters, digits, hyphens, plus signs, periods
48+
# Package names cannot start with hyphen or period, and must be reasonable length
49+
foreach ($pkg in $env:EXTRA_INSTALL_PACKAGES -split '\s+') {
50+
if ($pkg -notmatch '^[a-z0-9][a-z0-9.+-]{0,100}$') {
51+
Write-Host "::error::Invalid package name '$pkg'. Debian packages must start with lowercase letter or digit and contain only lowercase letters, digits, hyphens, periods, and plus signs."
52+
exit 1
53+
}
54+
}
55+
3156
# Using a pre-built docker image in GitHub container registry instead of NPM to reduce possible attack vectors.
32-
- name: Run DangerJS
33-
id: danger
57+
- name: Setup container
3458
shell: bash
59+
env:
60+
GITHUB_TOKEN: ${{ inputs.api-token }}
61+
EXTRA_DANGERFILE_INPUT: ${{ inputs.extra-dangerfile }}
3562
run: |
36-
docker run \
63+
# Start a detached container with all necessary volumes and environment variables
64+
docker run -td --name danger \
65+
--entrypoint /bin/bash \
3766
--volume ${{ github.workspace }}:/github/workspace \
3867
--volume ${{ github.action_path }}:${{ github.action_path }} \
3968
--volume ${{ github.event_path }}:${{ github.event_path }} \
4069
--workdir /github/workspace \
4170
--user $(id -u) \
4271
-e "INPUT_ARGS" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e GITHUB_ACTIONS=true -e CI=true \
43-
-e GITHUB_TOKEN="${{ inputs.api-token }}" \
72+
-e "GITHUB_TOKEN" \
4473
-e DANGER_DISABLE_TRANSPILATION="true" \
74+
-e "EXTRA_DANGERFILE_INPUT" \
4575
ghcr.io/danger/danger-js:${{ steps.config.outputs.version }} \
46-
--failOnErrors --dangerfile ${{ github.action_path }}/dangerfile.js
76+
-c "sleep infinity"
77+
78+
- name: Setup additional packages
79+
if: ${{ inputs.extra-install-packages }}
80+
shell: bash
81+
env:
82+
EXTRA_INSTALL_PACKAGES: ${{ inputs.extra-install-packages }}
83+
run: |
84+
echo "Installing packages: $EXTRA_INSTALL_PACKAGES"
85+
docker exec --user root danger sh -c "set -e && apt-get update && apt-get install -y --no-install-recommends $EXTRA_INSTALL_PACKAGES"
86+
echo "All additional packages installed successfully."
87+
88+
- name: Run DangerJS
89+
id: danger
90+
shell: bash
91+
run: |
92+
docker exec --user $(id -u) danger danger ci --fail-on-errors --dangerfile ${{ github.action_path }}/dangerfile.js
93+
94+
- name: Cleanup container
95+
if: always()
96+
shell: bash
97+
run: docker rm -f danger || true

danger/dangerfile.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,52 @@ async function checkActionsArePinned() {
186186
}
187187
}
188188

189+
async function checkFromExternalChecks() {
190+
// Get the external dangerfile path from environment variable (passed via workflow input)
191+
// Priority: EXTRA_DANGERFILE (absolute path) -> EXTRA_DANGERFILE_INPUT (relative path)
192+
const extraDangerFilePath = process.env.EXTRA_DANGERFILE || process.env.EXTRA_DANGERFILE_INPUT;
193+
console.log(`::debug:: Checking from external checks: ${extraDangerFilePath}`);
194+
if (extraDangerFilePath) {
195+
try {
196+
const workspaceDir = '/github/workspace';
197+
198+
const path = require('path');
199+
const fs = require('fs');
200+
const customPath = path.join(workspaceDir, extraDangerFilePath);
201+
// Ensure the resolved path is within workspace
202+
const resolvedPath = fs.realpathSync(customPath);
203+
if (!resolvedPath.startsWith(workspaceDir)) {
204+
fail(`Invalid dangerfile path: ${extraDangerFilePath}. Must be within workspace.`);
205+
throw new Error('Security violation: dangerfile path outside workspace');
206+
}
207+
208+
const extraModule = require(customPath);
209+
if (typeof extraModule !== 'function') {
210+
warn(`EXTRA_DANGERFILE must export a function at ${customPath}`);
211+
return;
212+
}
213+
await extraModule({
214+
fail: fail,
215+
warn: warn,
216+
message: message,
217+
markdown: markdown,
218+
danger: danger,
219+
});
220+
} catch (err) {
221+
if (err.message && err.message.includes('Cannot use import statement outside a module')) {
222+
warn(`External dangerfile uses ES6 imports. Please convert to CommonJS syntax (require/module.exports) or use .mjs extension with proper module configuration.\nFile: ${extraDangerFilePath}`);
223+
} else {
224+
warn(`Could not load custom Dangerfile: ${extraDangerFilePath}\n${err}`);
225+
}
226+
}
227+
}
228+
}
229+
189230
async function checkAll() {
190231
await checkDocs();
191232
await checkChangelog();
192233
await checkActionsArePinned();
234+
await checkFromExternalChecks();
193235
}
194236

195237
schedule(checkAll);

0 commit comments

Comments
 (0)