diff --git a/.github/test-dangerfile.js b/.github/test-dangerfile.js new file mode 100644 index 0000000..b125452 --- /dev/null +++ b/.github/test-dangerfile.js @@ -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'); +}; diff --git a/.github/workflows/danger-workflow-tests.yml b/.github/workflows/danger-workflow-tests.yml index 3d1b14e..bd24ebb 100644 --- a/.github/workflows/danger-workflow-tests.yml +++ b/.github/workflows/danger-workflow-tests.yml @@ -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!" diff --git a/danger/README.md b/danger/README.md index daaee7d..e979ab4 100644 --- a/danger/README.md +++ b/danger/README.md @@ -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`. @@ -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). \ No newline at end of file +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('...'); +} + +``` diff --git a/danger/action.yml b/danger/action.yml index 56d222b..3148668 100644 --- a/danger/action.yml +++ b/danger/action.yml @@ -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: @@ -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 diff --git a/danger/dangerfile.js b/danger/dangerfile.js index 997a9c0..d5feaa4 100644 --- a/danger/dangerfile.js +++ b/danger/dangerfile.js @@ -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);