diff --git a/.github/workflows/__shared-ci.yml b/.github/workflows/__shared-ci.yml index 261e5e7..eb04010 100644 --- a/.github/workflows/__shared-ci.yml +++ b/.github/workflows/__shared-ci.yml @@ -20,6 +20,18 @@ jobs: contents: read uses: ./.github/workflows/__test-action-matrix-outputs.yml + test-action-get-github-actions-bot-user: + needs: linter + permissions: + contents: read + uses: ./.github/workflows/__test-action-get-github-actions-bot-user.yml + + test-action-get-issue-number: + needs: linter + permissions: + contents: read + uses: ./.github/workflows/__test-action-get-issue-number.yml + test-action-parse-ci-reports: needs: linter permissions: @@ -41,3 +53,9 @@ jobs: permissions: contents: read uses: ./.github/workflows/__test-action-slugify.yml + + test-action-working-directory: + needs: linter + permissions: + contents: read + uses: ./.github/workflows/__test-action-working-directory.yml diff --git a/.github/workflows/__test-action-get-github-actions-bot-user.yml b/.github/workflows/__test-action-get-github-actions-bot-user.yml new file mode 100644 index 0000000..70df9a8 --- /dev/null +++ b/.github/workflows/__test-action-get-github-actions-bot-user.yml @@ -0,0 +1,36 @@ +name: Internal - Tests for get-github-actions-bot-user action + +on: + workflow_call: + +permissions: + contents: read + +jobs: + tests: + name: Tests for get-github-actions-bot-user action + runs-on: ubuntu-latest + steps: + - name: Arrange - Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Act - Run get-github-actions-bot-user action + id: get-github-actions-bot-user + uses: ./actions/get-github-actions-bot-user + + - name: Assert - Check get-github-actions-bot-user outputs + env: + STEPS_GET_GITHUB_ACTIONS_BOT_USER_OUTPUTS_NAME: ${{ steps.get-github-actions-bot-user.outputs.name }} + STEPS_GET_GITHUB_ACTIONS_BOT_USER_OUTPUTS_EMAIL: ${{ steps.get-github-actions-bot-user.outputs.email }} + run: | + if [ "${STEPS_GET_GITHUB_ACTIONS_BOT_USER_OUTPUTS_NAME}" != 'github-actions[bot]' ]; then + echo "get-github-actions-bot-user output name is not valid" + exit 1 + fi + + if [ "${STEPS_GET_GITHUB_ACTIONS_BOT_USER_OUTPUTS_EMAIL}" != '41898282+github-actions[bot]@users.noreply.github.com' ]; then + echo "get-github-actions-bot-user output email is not valid" + exit 1 + fi diff --git a/.github/workflows/__test-action-get-issue-number.yml b/.github/workflows/__test-action-get-issue-number.yml new file mode 100644 index 0000000..cbf016f --- /dev/null +++ b/.github/workflows/__test-action-get-issue-number.yml @@ -0,0 +1,43 @@ +name: Internal - Tests for get-issue-number action + +on: + workflow_call: + +permissions: + contents: read + +jobs: + tests: + name: Tests for get-issue-number action + runs-on: ubuntu-latest + steps: + - name: Arrange - Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Act - Run get-issue-number action + id: get-issue-number + continue-on-error: true + uses: ./actions/get-issue-number + + - name: Assert - Check get-issue-number behavior by event type + env: + STEPS_GET_ISSUE_NUMBER_OUTCOME: ${{ steps.get-issue-number.outcome }} + STEPS_GET_ISSUE_NUMBER_OUTPUTS_ISSUE_NUMBER: ${{ steps.get-issue-number.outputs.issue-number }} + EXPECTED_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} + run: | + if [ "${GITHUB_EVENT_NAME}" = 'pull_request' ]; then + if [ "${STEPS_GET_ISSUE_NUMBER_OUTCOME}" != 'success' ]; then + echo "get-issue-number should succeed for pull_request events" + exit 1 + fi + + if [ "${STEPS_GET_ISSUE_NUMBER_OUTPUTS_ISSUE_NUMBER}" != "${EXPECTED_PULL_REQUEST_NUMBER}" ]; then + echo "get-issue-number output is not valid for pull_request events" + exit 1 + fi + elif [ "${STEPS_GET_ISSUE_NUMBER_OUTCOME}" != 'failure' ]; then + echo "get-issue-number should fail when event is not pull_request" + exit 1 + fi diff --git a/.github/workflows/__test-action-parse-ci-reports.yml b/.github/workflows/__test-action-parse-ci-reports.yml index 2c3aacc..4a544ad 100644 --- a/.github/workflows/__test-action-parse-ci-reports.yml +++ b/.github/workflows/__test-action-parse-ci-reports.yml @@ -25,7 +25,8 @@ jobs: name: Tests for parse-ci-reports action runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Arrange - Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false @@ -71,7 +72,8 @@ jobs: ] EOF - - id: parse-ci-reports + - name: Act - Run parse-ci-reports action + id: parse-ci-reports uses: ./actions/parse-ci-reports with: report-paths: | @@ -81,7 +83,7 @@ jobs: report-name: "Test Reports" output-format: "summary,markdown" - - name: Check parse-ci-reports outputs + - name: Assert - Check parse-ci-reports outputs run: | # Check that markdown output exists if [ -z "${STEPS_PARSE_CI_REPORTS_OUTPUTS_MARKDOWN}" ]; then diff --git a/.github/workflows/__test-action-repository-owner-is-organization.yml b/.github/workflows/__test-action-repository-owner-is-organization.yml index 071b97b..0cac178 100644 --- a/.github/workflows/__test-action-repository-owner-is-organization.yml +++ b/.github/workflows/__test-action-repository-owner-is-organization.yml @@ -11,18 +11,20 @@ jobs: name: Tests for repository-owner-is-organization action runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Arrange - Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - id: repository-owner-is-organization + - name: Act - Run repository-owner-is-organization action + id: repository-owner-is-organization uses: ./actions/repository-owner-is-organization - - name: Check repository-owner-is-organization outputs + - name: Assert - Check repository-owner-is-organization outputs + env: + STEPS_REPOSITORY_OWNER_IS_ORGANIZATION_OUTPUTS_IS_ORGANIZATION: ${{ steps.repository-owner-is-organization.outputs.is-organization }} run: | if [ "${STEPS_REPOSITORY_OWNER_IS_ORGANIZATION_OUTPUTS_IS_ORGANIZATION}" != 'true' ]; then echo "repository-owner-is-organization outputs result is not valid" exit 1 fi - env: - STEPS_REPOSITORY_OWNER_IS_ORGANIZATION_OUTPUTS_IS_ORGANIZATION: ${{ steps.repository-owner-is-organization.outputs.is-organization }} diff --git a/.github/workflows/__test-action-slugify.yml b/.github/workflows/__test-action-slugify.yml index 80ddc47..d2ffd1d 100644 --- a/.github/workflows/__test-action-slugify.yml +++ b/.github/workflows/__test-action-slugify.yml @@ -11,16 +11,18 @@ jobs: name: Tests for slugify action runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Arrange - Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - id: slugify + - name: Act - Run slugify action + id: slugify uses: ./actions/slugify with: value: test content - - name: Check slugify outputs + - name: Assert - Check slugify outputs run: | if [ "${STEPS_SLUGIFY_OUTPUTS_RESULT}" != 'test-content' ]; then echo "Slugify outputs result is not valid" diff --git a/.github/workflows/__test-action-working-directory.yml b/.github/workflows/__test-action-working-directory.yml new file mode 100644 index 0000000..63f316a --- /dev/null +++ b/.github/workflows/__test-action-working-directory.yml @@ -0,0 +1,77 @@ +name: Internal - Tests for working-directory action + +on: + workflow_call: + +permissions: + contents: read + +jobs: + tests: + name: Tests for working-directory action + runs-on: ubuntu-latest + steps: + - name: Arrange - Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Act - Resolve workspace root directory + id: working-directory + uses: ./actions/working-directory + with: + working-directory: . + + - name: Assert - Check workspace root outputs + run: | + if [ "${STEPS_WORKING_DIRECTORY_OUTPUTS_ABSOLUTE_PATH}" != "${GITHUB_WORKSPACE}" ]; then + echo "working-directory absolute-path output is not valid" + exit 1 + fi + + if [ "${STEPS_WORKING_DIRECTORY_OUTPUTS_WORKSPACE_RELATIVE_PATH}" != "." ]; then + echo "working-directory workspace-relative-path output is not valid" + exit 1 + fi + env: + STEPS_WORKING_DIRECTORY_OUTPUTS_ABSOLUTE_PATH: ${{ steps.working-directory.outputs.absolute-path }} + STEPS_WORKING_DIRECTORY_OUTPUTS_WORKSPACE_RELATIVE_PATH: ${{ steps.working-directory.outputs.workspace-relative-path }} + + - name: Act - Run outside-workspace path with enforcement enabled + id: outside-workspace-disallowed + continue-on-error: true + uses: ./actions/working-directory + with: + working-directory: /tmp + enforce-path-in-workspace: true + + - name: Assert - Check enforcement failure outside workspace + run: | + if [ "${STEPS_OUTSIDE_WORKSPACE_DISALLOWED_OUTCOME}" != "failure" ]; then + echo "working-directory should fail when path is outside workspace and enforcement is enabled" + exit 1 + fi + env: + STEPS_OUTSIDE_WORKSPACE_DISALLOWED_OUTCOME: ${{ steps.outside-workspace-disallowed.outcome }} + + - name: Act - Run outside-workspace path with enforcement disabled + id: outside-workspace-allowed + uses: ./actions/working-directory + with: + working-directory: /tmp + enforce-path-in-workspace: false + + - name: Assert - Check enforcement opt-out outside workspace + run: | + if [ "${STEPS_OUTSIDE_WORKSPACE_ALLOWED_OUTPUTS_ABSOLUTE_PATH}" != "/tmp" ]; then + echo "working-directory absolute-path output is not valid when enforcement is disabled" + exit 1 + fi + + if [[ "${STEPS_OUTSIDE_WORKSPACE_ALLOWED_OUTPUTS_WORKSPACE_RELATIVE_PATH}" != ..* ]]; then + echo "working-directory workspace-relative-path should be outside workspace when enforcement is disabled" + exit 1 + fi + env: + STEPS_OUTSIDE_WORKSPACE_ALLOWED_OUTPUTS_ABSOLUTE_PATH: ${{ steps.outside-workspace-allowed.outputs.absolute-path }} + STEPS_OUTSIDE_WORKSPACE_ALLOWED_OUTPUTS_WORKSPACE_RELATIVE_PATH: ${{ steps.outside-workspace-allowed.outputs.workspace-relative-path }} diff --git a/README.md b/README.md index 5786223..8aa37ba 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Opinionated GitHub Actions and reusable workflows for foundational continuous-in - [Parse CI reports](actions/parse-ci-reports/README.md) - parses CI reports (tests, linting, coverage) into GitHub summaries and Markdown for PR comments. - [Repository owner is organization](actions/repository-owner-is-organization/README.md) - checks whether the repository owner is an organization. -- [Working directory](actions/working-directory/README.md) - resolves and validates a working directory path as an absolute path. +- [Working directory](actions/working-directory/README.md) - resolves and validates a working directory path, exposing absolute and workspace-relative outputs. - [Slugify](actions/slugify/README.md) - converts free-form strings into GitHub-friendly slugs. ## Reusable Workflows diff --git a/actions/get-issue-number/action.yml b/actions/get-issue-number/action.yml index a19834f..7081aa7 100644 --- a/actions/get-issue-number/action.yml +++ b/actions/get-issue-number/action.yml @@ -8,19 +8,22 @@ branding: outputs: issue-number: description: "The issue number." - value: ${{ steps.get-issue-number.outputs.result }} + value: ${{ steps.get-issue-number.outputs.issue-number }} runs: using: "composite" steps: - id: get-issue-number - shell: bash - run: | - if [ ! -z "${{ github.event.pull_request.number }}" ]; then - echo "result=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT - elif [ ! -z "${{ github.event.issue.number }}" ]; then - echo "result=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT - else - echo "No PR or issue number found for the current event" - exit 1 - fi + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const pullRequestNumber = context.payload.pull_request?.number; + const issueNumber = context.payload.issue?.number; + const result = pullRequestNumber ?? issueNumber; + + if (!result) { + core.setFailed('No PR or issue number found for the current event'); + return; + } + + core.setOutput('issue-number', result); diff --git a/actions/working-directory/README.md b/actions/working-directory/README.md index 2facfc1..9056d35 100644 --- a/actions/working-directory/README.md +++ b/actions/working-directory/README.md @@ -42,9 +42,10 @@ Action to resolve and validate a working directory path. ## Inputs -| **Input** | **Description** | **Required** | **Default** | -| ----------------------- | ------------------------------------------------------- | ------------ | ----------- | -| **`working-directory`** | Relative or absolute working directory path to resolve. | **false** | `.` | +| **Input** | **Description** | **Required** | **Default** | +| ------------------------------- | --------------------------------------------------------------------- | ------------ | ----------- | +| **`working-directory`** | Relative or absolute working directory path to resolve. | **false** | `.` | +| **`enforce-path-in-workspace`** | Whether to fail when the resolved path is outside `GITHUB_WORKSPACE`. | **false** | `true` | @@ -53,9 +54,10 @@ Action to resolve and validate a working directory path. ## Outputs -| **Output** | **Description** | -| ----------------------- | --------------------------------------------- | -| **`working-directory`** | The resolved absolute working directory path. | +| **Output** | **Description** | +| ----------------------------- | ------------------------------------------------------------------- | +| **`absolute-path`** | The resolved absolute working directory path. | +| **`workspace-relative-path`** | The resolved working directory path relative to `GITHUB_WORKSPACE`. | diff --git a/actions/working-directory/action.yml b/actions/working-directory/action.yml index b1b27c1..30c81fc 100644 --- a/actions/working-directory/action.yml +++ b/actions/working-directory/action.yml @@ -10,11 +10,18 @@ inputs: description: "Relative or absolute working directory path to resolve." required: false default: "." + enforce-path-in-workspace: + description: "Whether to fail when the resolved path is outside GITHUB_WORKSPACE." + required: false + default: "true" outputs: - working-directory: + absolute-path: description: "The resolved absolute working directory path." - value: ${{ steps.working-directory.outputs.working-directory }} + value: ${{ steps.working-directory.outputs.absolute-path }} + workspace-relative-path: + description: "The resolved working directory path relative to GITHUB_WORKSPACE." + value: ${{ steps.working-directory.outputs.workspace-relative-path }} runs: using: "composite" @@ -23,15 +30,18 @@ runs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: WORKING_DIRECTORY_INPUT: ${{ inputs.working-directory }} + ENFORCE_PATH_IN_WORKSPACE_INPUT: ${{ inputs.enforce-path-in-workspace }} with: script: | const fs = require('node:fs'); const path = require('node:path'); + const workspaceRoot = process.env.GITHUB_WORKSPACE || process.cwd(); + const enforcePathInWorkspace = (process.env.ENFORCE_PATH_IN_WORKSPACE_INPUT || 'true') !== 'false'; let workingDirectory = process.env.WORKING_DIRECTORY_INPUT || '.'; if (!path.isAbsolute(workingDirectory)) { - workingDirectory = path.join(process.env.GITHUB_WORKSPACE, workingDirectory); + workingDirectory = path.join(workspaceRoot, workingDirectory); } if (!fs.existsSync(workingDirectory)) { @@ -39,6 +49,22 @@ runs: return; } - workingDirectory = path.resolve(workingDirectory); - core.debug(`Running in working directory: ${workingDirectory}`); - core.setOutput('working-directory', workingDirectory); + if (!fs.statSync(workingDirectory).isDirectory()) { + core.setFailed(`The specified working directory is not a directory: ${workingDirectory}`); + return; + } + + const absolutePath = path.resolve(workingDirectory); + const workspaceRelativePath = path.relative(workspaceRoot, absolutePath) || '.'; + + if (enforcePathInWorkspace) { + const isOutsideWorkspace = workspaceRelativePath.startsWith('..') || path.isAbsolute(workspaceRelativePath); + + if (isOutsideWorkspace) { + core.setFailed(`The specified working directory is outside GITHUB_WORKSPACE: ${absolutePath}`); + return; + } + } + + core.setOutput('absolute-path', absolutePath); + core.setOutput('workspace-relative-path', workspaceRelativePath);