Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/test-dangerfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Test dangerfile for exercising extra-dangerfile feature
// This demonstrates how repositories can add custom Danger checks

module.exports = async function ({ fail, warn, message, markdown, danger }) {
console.log('::notice::Running custom dangerfile checks...');

// Test that we have access to the danger API
if (!danger || !danger.github || !danger.github.pr) {
fail('Custom dangerfile cannot access danger API');
return;
}

// Example check: Verify PR has a description
const prBody = danger.github.pr.body;
if (!prBody || prBody.trim().length === 0) {
warn('PR description is empty. Consider adding a description to help reviewers.');
} else {
message('✅ Custom dangerfile check: PR has a description');
}

// Example check: Verify PR title is not too short
const prTitle = danger.github.pr.title;
if (prTitle && prTitle.length < 10) {
warn('PR title is quite short. Consider making it more descriptive.');
} else {
message('✅ Custom dangerfile check: PR title length is reasonable');
}

// Show that we can access git information
const modifiedFiles = danger.git.modified_files || [];
const createdFiles = danger.git.created_files || [];
const totalChangedFiles = modifiedFiles.length + createdFiles.length;

message(`📊 Custom check: This PR changes ${totalChangedFiles} file(s)`);

console.log('::notice::Custom dangerfile checks completed successfully');
};
67 changes: 67 additions & 0 deletions .github/workflows/danger-workflow-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,70 @@ jobs:

Write-Host "✅ Danger PR analysis completed successfully!"
Write-Host "ℹ️ Check the PR comments for any Danger findings"

# Test extra-dangerfile feature
extra-dangerfile-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run danger with extra dangerfile
id: danger-extra
uses: ./danger
with:
extra-dangerfile: '.github/test-dangerfile.js'

- name: Validate danger with extra-dangerfile outputs
env:
DANGER_OUTCOME: ${{ steps.danger-extra.outputs.outcome }}
shell: pwsh
run: |
Write-Host "🔍 Validating Danger action with extra-dangerfile..."
Write-Host "Danger Outcome: '$env:DANGER_OUTCOME'"

# Validate that Danger ran successfully
$env:DANGER_OUTCOME | Should -Be "success"

Write-Host "✅ Danger with extra-dangerfile completed successfully!"

# Test extra-install-packages feature
extra-packages-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Create a test dangerfile that requires curl
- name: Create test dangerfile requiring curl
shell: bash
run: |
cat > .github/test-dangerfile-curl.js << 'EOF'
module.exports = async function ({ message, danger }) {
const { execSync } = require('child_process');
try {
const curlVersion = execSync('curl --version', { encoding: 'utf-8' });
message('✅ curl is available: ' + curlVersion.split('\n')[0]);
} catch (err) {
throw new Error('curl command not found - extra-install-packages failed');
}
};
EOF

- name: Run danger with extra packages
id: danger-packages
uses: ./danger
with:
extra-dangerfile: '.github/test-dangerfile-curl.js'
extra-install-packages: 'curl'

- name: Validate danger with extra-install-packages outputs
env:
DANGER_OUTCOME: ${{ steps.danger-packages.outputs.outcome }}
shell: pwsh
run: |
Write-Host "🔍 Validating Danger action with extra-install-packages..."
Write-Host "Danger Outcome: '$env:DANGER_OUTCOME'"

# Validate that Danger ran successfully
$env:DANGER_OUTCOME | Should -Be "success"

Write-Host "✅ Danger with extra-install-packages completed successfully!"
26 changes: 25 additions & 1 deletion danger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ jobs:
* required: false
* default: `${{ github.token }}`

* `extra-dangerfile`: Path to an additional dangerfile to run custom checks.
* type: string
* required: false
* default: ""

* `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.
* type: string
* required: false
* default: ""

## Outputs

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

For detailed rule implementations, see [dangerfile.js](dangerfile.js).
For detailed rule implementations, see [dangerfile.js](dangerfile.js).

## Extra Danger File

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:

```JavaScript
module.exports = async function ({ fail, warn, message, markdown, danger }) {
...
const gitUrl = danger.github.pr.head.repo.git_url;
...
warn('...');
}

```
61 changes: 56 additions & 5 deletions danger/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ inputs:
description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
required: false
default: ${{ github.token }}
extra-dangerfile:
description: 'Path to additional dangerfile to run after the main checks'
type: string
required: false
extra-install-packages:
description: 'Additional apt packages to install in the DangerJS container (space-separated package names)'
type: string
required: false

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

# Validate extra-install-packages to prevent code injection
- name: Validate package names
if: ${{ inputs.extra-install-packages }}
shell: pwsh
env:
EXTRA_INSTALL_PACKAGES: ${{ inputs.extra-install-packages }}
run: |
# Validate against Debian package naming rules: must start with alphanumeric,
# contain only lowercase letters, digits, hyphens, plus signs, periods
# Package names cannot start with hyphen or period, and must be reasonable length
foreach ($pkg in $env:EXTRA_INSTALL_PACKAGES -split '\s+') {
if ($pkg -notmatch '^[a-z0-9][a-z0-9.+-]{0,100}$') {
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."
exit 1
}
}

# Using a pre-built docker image in GitHub container registry instead of NPM to reduce possible attack vectors.
- name: Run DangerJS
id: danger
- name: Setup container
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.api-token }}
EXTRA_DANGERFILE_INPUT: ${{ inputs.extra-dangerfile }}
run: |
docker run \
# Start a detached container with all necessary volumes and environment variables
docker run -td --name danger \
--entrypoint /bin/bash \
--volume ${{ github.workspace }}:/github/workspace \
--volume ${{ github.action_path }}:${{ github.action_path }} \
--volume ${{ github.event_path }}:${{ github.event_path }} \
--workdir /github/workspace \
--user $(id -u) \
-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 \
-e GITHUB_TOKEN="${{ inputs.api-token }}" \
-e "GITHUB_TOKEN" \
-e DANGER_DISABLE_TRANSPILATION="true" \
-e "EXTRA_DANGERFILE_INPUT" \
ghcr.io/danger/danger-js:${{ steps.config.outputs.version }} \
--failOnErrors --dangerfile ${{ github.action_path }}/dangerfile.js
-c "sleep infinity"

- name: Setup additional packages
if: ${{ inputs.extra-install-packages }}
shell: bash
env:
EXTRA_INSTALL_PACKAGES: ${{ inputs.extra-install-packages }}
run: |
echo "Installing packages: $EXTRA_INSTALL_PACKAGES"
docker exec --user root danger sh -c "set -e && apt-get update && apt-get install -y --no-install-recommends $EXTRA_INSTALL_PACKAGES"
echo "All additional packages installed successfully."

- name: Run DangerJS
id: danger
shell: bash
run: |
docker exec --user $(id -u) danger danger ci --fail-on-errors --dangerfile ${{ github.action_path }}/dangerfile.js

- name: Cleanup container
if: always()
shell: bash
run: docker rm -f danger || true
42 changes: 42 additions & 0 deletions danger/dangerfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,52 @@ async function checkActionsArePinned() {
}
}

async function checkFromExternalChecks() {
// Get the external dangerfile path from environment variable (passed via workflow input)
// Priority: EXTRA_DANGERFILE (absolute path) -> EXTRA_DANGERFILE_INPUT (relative path)
const extraDangerFilePath = process.env.EXTRA_DANGERFILE || process.env.EXTRA_DANGERFILE_INPUT;
console.log(`::debug:: Checking from external checks: ${extraDangerFilePath}`);
if (extraDangerFilePath) {
try {
const workspaceDir = '/github/workspace';

const path = require('path');
const fs = require('fs');
const customPath = path.join(workspaceDir, extraDangerFilePath);
// Ensure the resolved path is within workspace
const resolvedPath = fs.realpathSync(customPath);
if (!resolvedPath.startsWith(workspaceDir)) {
fail(`Invalid dangerfile path: ${extraDangerFilePath}. Must be within workspace.`);
throw new Error('Security violation: dangerfile path outside workspace');
}

const extraModule = require(customPath);
if (typeof extraModule !== 'function') {
warn(`EXTRA_DANGERFILE must export a function at ${customPath}`);
return;
}
await extraModule({
fail: fail,
warn: warn,
message: message,
markdown: markdown,
danger: danger,
});
} catch (err) {
if (err.message && err.message.includes('Cannot use import statement outside a module')) {
warn(`External dangerfile uses ES6 imports. Please convert to CommonJS syntax (require/module.exports) or use .mjs extension with proper module configuration.\nFile: ${extraDangerFilePath}`);
} else {
warn(`Could not load custom Dangerfile: ${extraDangerFilePath}\n${err}`);
}
}
}
}

async function checkAll() {
await checkDocs();
await checkChangelog();
await checkActionsArePinned();
await checkFromExternalChecks();
}

schedule(checkAll);
Loading