diff --git a/.github/actions/splice-wf-run/action.yml b/.github/actions/splice-wf-run/action.yml index b73d09b..11a6b5f 100644 --- a/.github/actions/splice-wf-run/action.yml +++ b/.github/actions/splice-wf-run/action.yml @@ -5,6 +5,10 @@ inputs: source_workflow: description: Name of the source workflow that emitted the bridge artifact required: true + bridge_override_json: + description: Internal test-only JSON object used instead of consuming the bridge artifact; not part of the supported public API + required: false + default: '' allow_pr_author: description: Whether to always allow the PR author to trigger splice-bot required: false @@ -90,7 +94,8 @@ runs: echo "branch-token source: ${{ steps.token_mode.outputs.branch_token_source }}" - name: Consume bridge artifact - id: bridge + if: inputs.bridge_override_json == '' + id: bridge_consume uses: leanprover-community/privilege-escalation-bridge/consume@v1 with: token: ${{ inputs.token || github.token }} @@ -113,6 +118,88 @@ runs: committer=outputs.committer author=outputs.author + - name: Load bridge override + if: inputs.bridge_override_json != '' + id: bridge_override + shell: bash + env: + BRIDGE_OVERRIDE_JSON: ${{ inputs.bridge_override_json }} + run: | + set -euo pipefail + + ruby <<'RUBY' + require 'json' + + raw = ENV.fetch('BRIDGE_OVERRIDE_JSON', '') + output_path = ENV.fetch('GITHUB_OUTPUT') + data = JSON.parse(raw) + + unless data.is_a?(Hash) + warn 'bridge_override_json must be a JSON object.' + exit 1 + end + + keys = %w[ + pr_number + review_comment_id + file_path + commenter_login + pr_author_login + base_ref + base_repo + head_repo + head_sha + head_ref + head_label + committer + author + ] + + File.open(output_path, 'a') do |f| + keys.each do |key| + value = data[key] + next if value.nil? + f.puts("#{key}=#{value}") + end + end + RUBY + + - name: Resolve bridge data + id: bridge + shell: bash + env: + PR_NUMBER: ${{ steps.bridge_override.outputs.pr_number || steps.bridge_consume.outputs.pr_number }} + REVIEW_COMMENT_ID: ${{ steps.bridge_override.outputs.review_comment_id || steps.bridge_consume.outputs.review_comment_id }} + FILE_PATH: ${{ steps.bridge_override.outputs.file_path || steps.bridge_consume.outputs.file_path }} + COMMENTER_LOGIN: ${{ steps.bridge_override.outputs.commenter_login || steps.bridge_consume.outputs.commenter_login }} + PR_AUTHOR_LOGIN: ${{ steps.bridge_override.outputs.pr_author_login || steps.bridge_consume.outputs.pr_author_login }} + BASE_REF: ${{ steps.bridge_override.outputs.base_ref || steps.bridge_consume.outputs.base_ref }} + BASE_REPO: ${{ steps.bridge_override.outputs.base_repo || steps.bridge_consume.outputs.base_repo }} + HEAD_REPO: ${{ steps.bridge_override.outputs.head_repo || steps.bridge_consume.outputs.head_repo }} + HEAD_SHA: ${{ steps.bridge_override.outputs.head_sha || steps.bridge_consume.outputs.head_sha }} + HEAD_REF: ${{ steps.bridge_override.outputs.head_ref || steps.bridge_consume.outputs.head_ref }} + HEAD_LABEL: ${{ steps.bridge_override.outputs.head_label || steps.bridge_consume.outputs.head_label }} + COMMITTER: ${{ steps.bridge_override.outputs.committer || steps.bridge_consume.outputs.committer }} + AUTHOR: ${{ steps.bridge_override.outputs.author || steps.bridge_consume.outputs.author }} + run: | + set -euo pipefail + + { + echo "pr_number=${PR_NUMBER}" + echo "review_comment_id=${REVIEW_COMMENT_ID}" + echo "file_path=${FILE_PATH}" + echo "commenter_login=${COMMENTER_LOGIN}" + echo "pr_author_login=${PR_AUTHOR_LOGIN}" + echo "base_ref=${BASE_REF}" + echo "base_repo=${BASE_REPO}" + echo "head_repo=${HEAD_REPO}" + echo "head_sha=${HEAD_SHA}" + echo "head_ref=${HEAD_REF}" + echo "head_label=${HEAD_LABEL}" + echo "committer=${COMMITTER}" + echo "author=${AUTHOR}" + } >> "$GITHUB_OUTPUT" + - name: Authorize commenter if: steps.bridge.outputs.file_path != '' id: authorize_commenter diff --git a/.github/workflows/act_smoke.yaml b/.github/workflows/act_smoke.yaml new file mode 100644 index 0000000..6c947ae --- /dev/null +++ b/.github/workflows/act_smoke.yaml @@ -0,0 +1,35 @@ +name: Act Smoke Tests + +on: + pull_request: + paths: + - ".github/workflows/*.yml" + - ".github/workflows/*.yaml" + - ".github/actions/**/*.yml" + - ".github/actions/**/*.yaml" + - "tests/actions/**" + workflow_dispatch: + +jobs: + act-smoke: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install act + shell: bash + run: | + set -euo pipefail + curl --silent --show-error --location \ + https://raw.githubusercontent.com/nektos/act/master/install.sh \ + | sudo bash -s -- -b /usr/local/bin + + - name: Run act smoke suite + shell: bash + env: + DOCKER_AUTH_CONFIG: '{}' + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + tests/actions/run_act_smoke.sh diff --git a/.github/workflows/splice.yaml b/.github/workflows/splice.yaml index 1ae67db..cb643fe 100644 --- a/.github/workflows/splice.yaml +++ b/.github/workflows/splice.yaml @@ -22,6 +22,11 @@ on: required: false type: string default: '${{ github.event.pull_request.user.login }} <${{ github.event.pull_request.user.id }}+${{ github.event.pull_request.user.login }}@users.noreply.github.com>' + emit_bridge_artifact: + description: Whether to emit the bridge artifact; useful to disable in local smoke tests + required: false + type: boolean + default: true permissions: {} @@ -33,6 +38,7 @@ jobs: - name: Verify caller event is pull_request_review_comment uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: + github-token: ${{ github.token }} script: | const eventName = context.eventName; const hasReviewCommentPayload = !!(context.payload?.comment && context.payload?.pull_request); @@ -53,21 +59,28 @@ jobs: # Pass the reusable input down to the script and emit it as an output named base_ref BASE_REF_INPUT: ${{ inputs.base_ref }} with: + github-token: ${{ github.token }} script: | // NOTE: In actions/github-script, `core`, `github`, and `context` are provided globally. const comment = context.payload?.comment; const pr = context.payload?.pull_request; const body = comment?.body || ""; + const triggerLine = body + .split(/\r?\n/) + .find((line) => /^splice-bot\b/i.test(line)); core.info(`Comment body:\n---\n${body}\n---`); // Start-of-line match across lines; no leading whitespace allowed - if (!/^splice-bot\b/im.test(body)) { + if (!triggerLine) { core.info("No `splice-bot` found at the start of a line, skipping."); core.setOutput("skip", "true"); return; } + const triggerKeyword = triggerLine.replace(/^splice-bot\b/i, "").trim().split(/\s+/)[0] || ""; + core.info(`Trigger keyword: ${triggerKeyword || "(none)"}`); + const path = comment?.path; if (!path) { core.info("This review comment is not on a file diff (no .path). Nothing to do."); @@ -106,21 +119,24 @@ jobs: core.setOutput("skip", "false"); // IMPORTANT: base_ref now comes from the reusable workflow input (default master) core.setOutput("base_ref", baseRefFromInput); + core.setOutput("trigger_keyword", triggerKeyword); - - if: ${{ steps.extract.outputs.skip != 'true' }} + - if: ${{ steps.extract.outputs.skip != 'true' && inputs.emit_bridge_artifact }} name: Prepare bridge outputs run: | jq -n \ --arg base_ref "${{ steps.extract.outputs.base_ref }}" \ + --arg trigger_keyword "${{ steps.extract.outputs.trigger_keyword }}" \ --arg committer "${{ inputs.committer }}" \ --arg author "${{ inputs.author }}" \ '{ base_ref: $base_ref, + trigger_keyword: $trigger_keyword, committer: $committer, author: $author, }' > bridge-outputs.json - - if: ${{ steps.extract.outputs.skip != 'true' }} + - if: ${{ steps.extract.outputs.skip != 'true' && inputs.emit_bridge_artifact }} name: Emit bridge artifact uses: leanprover-community/privilege-escalation-bridge/emit@v1 with: diff --git a/.github/workflows/workflow_lint.yaml b/.github/workflows/workflow_lint.yaml index 7427a60..ec6e515 100644 --- a/.github/workflows/workflow_lint.yaml +++ b/.github/workflows/workflow_lint.yaml @@ -5,6 +5,8 @@ on: paths: - ".github/workflows/*.yml" - ".github/workflows/*.yaml" + - ".github/actions/**/*.yml" + - ".github/actions/**/*.yaml" workflow_dispatch: jobs: @@ -15,4 +17,4 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Run actionlint - uses: raven-actions/actionlint@e01d1ea33dd6a5ed517d95b4c0c357560ac6f518 # v2.1.1 + uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2 diff --git a/README.md b/README.md index 3dbd547..5beda9b 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,51 @@ jobs: # branch_token: ${{ secrets.SPLICE_BOT_FORK_TOKEN }} ``` +## Local `act` smoke tests + +You can run the lightweight workflow smoke tests locally with [`act`](https://github.com/nektos/act). +These tests exercise the reusable trigger workflow against canned review-comment payloads and intentionally skip bridge-artifact emission. +The composite-action smoke harness uses the internal test-only `bridge_override_json` input; that input exists only for local/CI testing and is not part of the supported public API. + +Prerequisites: + +- `act` installed locally +- Docker running +- `GITHUB_TOKEN` available if the workflow under test needs it, for example: + `export GITHUB_TOKEN="$(gh auth token)"` + +Run all local `act` smoke tests: + +```bash +tests/actions/run_act_smoke.sh +``` + +That script auto-discovers all reusable-workflow event fixtures under `tests/actions/events/` and then runs the composite-action smoke harness. +If `GITHUB_TOKEN` is set in the environment, the script passes it through to `act` as a secret. + +You can also run individual harnesses directly. + +Example commands: + +```bash +act \ + --workflows tests/actions/workflows/splice_harness.yaml \ + --eventpath tests/actions/events/pr-review-comment-basic.json \ + pull_request_review_comment +``` + +```bash +act \ + --workflows tests/actions/workflows/splice_harness.yaml \ + --eventpath tests/actions/events/pr-review-comment-keyword.json \ + pull_request_review_comment +``` + +The canned event payloads live under `tests/actions/events/`. +The CI workflow that runs the same smoke harness is `.github/workflows/act_smoke.yaml`. +The reusable trigger harness lives at `tests/actions/workflows/splice_harness.yaml`. +There is also a composite-action harness at `tests/actions/workflows/splice_action_harness.yaml` for deterministic non-network failure cases. + ## GitHub App token minting in caller job ```yaml @@ -238,6 +283,7 @@ Permission mapping references: | Name | Type | Required | Default | Description | | ---- | ---- | -------- | ------- | ----------- | | `source_workflow` | string | Yes | - | Name of the source workflow that emitted the bridge artifact. | +| `bridge_override_json` | string | No | `''` | Internal test-only override used by the local/CI `act` harness instead of consuming the bridge artifact. Do not rely on this in normal workflow usage; it is not part of the supported public API. | | `allow_pr_author` | boolean | No | `true` | Always allow PR author to trigger. | | `min_repo_permission` | string | No | `anyone` | Minimum permission threshold: `anyone`, `triage`, `write`. | | `allowed_teams` | string | No | `''` | Comma/newline-separated team allowlist (`team-slug` or `org/team-slug`). | @@ -249,6 +295,7 @@ Permission mapping references: | `branch_token` | string | No | `''` | Optional branch push token for `push_to_fork` mode. Falls back to `token`, then `github.token`, but fork mode should use an explicit token with write access to the fork. | Sensitive values should be passed through action inputs using workflow secrets when you do not want to rely on `github.token`, for example `token: ${{ secrets.SPLICE_BOT_TOKEN }}`. +Test-only internal inputs such as `bridge_override_json` are intentionally undocumented outside the local test harness context and may change without notice. ## Sensitive `splice-wf-run` action inputs diff --git a/tests/actions/assert/log_contains.sh b/tests/actions/assert/log_contains.sh new file mode 100755 index 0000000..ece05a0 --- /dev/null +++ b/tests/actions/assert/log_contains.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +log_file="$1" +expected_marker="$2" + +if ! grep -F -- "$expected_marker" "$log_file" >/dev/null 2>&1; then + echo "Expected log marker not found: $expected_marker" >&2 + echo "--- begin log ---" >&2 + cat "$log_file" >&2 + echo "--- end log ---" >&2 + exit 1 +fi + diff --git a/tests/actions/events/pr-review-comment-basic.expected b/tests/actions/events/pr-review-comment-basic.expected new file mode 100644 index 0000000..f389bbb --- /dev/null +++ b/tests/actions/events/pr-review-comment-basic.expected @@ -0,0 +1 @@ +Trigger keyword: (none) diff --git a/tests/actions/events/pr-review-comment-basic.json b/tests/actions/events/pr-review-comment-basic.json new file mode 100644 index 0000000..9cf3f27 --- /dev/null +++ b/tests/actions/events/pr-review-comment-basic.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Please split this out.\n\nsplice-bot", + "id": 102, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 43, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "3333333333333333333333333333333333333333", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "4444444444444444444444444444444444444444", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-carriage-return.expected b/tests/actions/events/pr-review-comment-carriage-return.expected new file mode 100644 index 0000000..f389bbb --- /dev/null +++ b/tests/actions/events/pr-review-comment-carriage-return.expected @@ -0,0 +1 @@ +Trigger keyword: (none) diff --git a/tests/actions/events/pr-review-comment-carriage-return.json b/tests/actions/events/pr-review-comment-carriage-return.json new file mode 100644 index 0000000..f8b3849 --- /dev/null +++ b/tests/actions/events/pr-review-comment-carriage-return.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Please split this.\r\n\r\nsplice-bot\r\n", + "id": 109, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 50, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "ffffffffffffffffffffffffffffffffffffffff", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "1212121212121212121212121212121212121212", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-empty-keyword-line.expected b/tests/actions/events/pr-review-comment-empty-keyword-line.expected new file mode 100644 index 0000000..f389bbb --- /dev/null +++ b/tests/actions/events/pr-review-comment-empty-keyword-line.expected @@ -0,0 +1 @@ +Trigger keyword: (none) diff --git a/tests/actions/events/pr-review-comment-empty-keyword-line.json b/tests/actions/events/pr-review-comment-empty-keyword-line.json new file mode 100644 index 0000000..6d160a1 --- /dev/null +++ b/tests/actions/events/pr-review-comment-empty-keyword-line.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "splice-bot \n\nmore discussion follows", + "id": 110, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 51, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "1313131313131313131313131313131313131313", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "1414141414141414141414141414141414141414", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-indented-trigger.expected b/tests/actions/events/pr-review-comment-indented-trigger.expected new file mode 100644 index 0000000..af5d331 --- /dev/null +++ b/tests/actions/events/pr-review-comment-indented-trigger.expected @@ -0,0 +1 @@ +No `splice-bot` found at the start of a line, skipping. diff --git a/tests/actions/events/pr-review-comment-indented-trigger.json b/tests/actions/events/pr-review-comment-indented-trigger.json new file mode 100644 index 0000000..d2be6de --- /dev/null +++ b/tests/actions/events/pr-review-comment-indented-trigger.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Looks good.\n\n splice-bot", + "id": 104, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 45, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "7777777777777777777777777777777777777777", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "8888888888888888888888888888888888888888", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-keyword-extra-text.expected b/tests/actions/events/pr-review-comment-keyword-extra-text.expected new file mode 100644 index 0000000..d5db01e --- /dev/null +++ b/tests/actions/events/pr-review-comment-keyword-extra-text.expected @@ -0,0 +1 @@ +Trigger keyword: ready diff --git a/tests/actions/events/pr-review-comment-keyword-extra-text.json b/tests/actions/events/pr-review-comment-keyword-extra-text.json new file mode 100644 index 0000000..2d632fa --- /dev/null +++ b/tests/actions/events/pr-review-comment-keyword-extra-text.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Please mark this ready.\n\nsplice-bot ready now", + "id": 106, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 47, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "cccccccccccccccccccccccccccccccccccccccc", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-keyword.expected b/tests/actions/events/pr-review-comment-keyword.expected new file mode 100644 index 0000000..d5db01e --- /dev/null +++ b/tests/actions/events/pr-review-comment-keyword.expected @@ -0,0 +1 @@ +Trigger keyword: ready diff --git a/tests/actions/events/pr-review-comment-keyword.json b/tests/actions/events/pr-review-comment-keyword.json new file mode 100644 index 0000000..97a80ae --- /dev/null +++ b/tests/actions/events/pr-review-comment-keyword.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Please mark this ready.\n\nsplice-bot ready", + "id": 103, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 44, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "5555555555555555555555555555555555555555", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "6666666666666666666666666666666666666666", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-missing-pr-fields.expected b/tests/actions/events/pr-review-comment-missing-pr-fields.expected new file mode 100644 index 0000000..34163fe --- /dev/null +++ b/tests/actions/events/pr-review-comment-missing-pr-fields.expected @@ -0,0 +1 @@ +Missing required PR details (base/head sha/repo). diff --git a/tests/actions/events/pr-review-comment-missing-pr-fields.json b/tests/actions/events/pr-review-comment-missing-pr-fields.json new file mode 100644 index 0000000..f91f265 --- /dev/null +++ b/tests/actions/events/pr-review-comment-missing-pr-fields.json @@ -0,0 +1,29 @@ +{ + "comment": { + "body": "splice-bot", + "id": 108, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 49, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-no-path.expected b/tests/actions/events/pr-review-comment-no-path.expected new file mode 100644 index 0000000..e0e5888 --- /dev/null +++ b/tests/actions/events/pr-review-comment-no-path.expected @@ -0,0 +1 @@ +This review comment is not on a file diff (no .path). Nothing to do. diff --git a/tests/actions/events/pr-review-comment-no-path.json b/tests/actions/events/pr-review-comment-no-path.json new file mode 100644 index 0000000..e2c0351 --- /dev/null +++ b/tests/actions/events/pr-review-comment-no-path.json @@ -0,0 +1,30 @@ +{ + "comment": { + "body": "splice-bot", + "id": 105, + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 46, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "9999999999999999999999999999999999999999", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-no-trigger.expected b/tests/actions/events/pr-review-comment-no-trigger.expected new file mode 100644 index 0000000..af5d331 --- /dev/null +++ b/tests/actions/events/pr-review-comment-no-trigger.expected @@ -0,0 +1 @@ +No `splice-bot` found at the start of a line, skipping. diff --git a/tests/actions/events/pr-review-comment-no-trigger.json b/tests/actions/events/pr-review-comment-no-trigger.json new file mode 100644 index 0000000..efd0b0e --- /dev/null +++ b/tests/actions/events/pr-review-comment-no-trigger.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Looks good to me.", + "id": 101, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 42, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "1111111111111111111111111111111111111111", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "2222222222222222222222222222222222222222", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/events/pr-review-comment-trigger-mid-line.expected b/tests/actions/events/pr-review-comment-trigger-mid-line.expected new file mode 100644 index 0000000..af5d331 --- /dev/null +++ b/tests/actions/events/pr-review-comment-trigger-mid-line.expected @@ -0,0 +1 @@ +No `splice-bot` found at the start of a line, skipping. diff --git a/tests/actions/events/pr-review-comment-trigger-mid-line.json b/tests/actions/events/pr-review-comment-trigger-mid-line.json new file mode 100644 index 0000000..93e7d07 --- /dev/null +++ b/tests/actions/events/pr-review-comment-trigger-mid-line.json @@ -0,0 +1,31 @@ +{ + "comment": { + "body": "Please run splice-bot here, thanks.", + "id": 107, + "path": "src/Foo.lean", + "user": { + "login": "reviewer" + } + }, + "pull_request": { + "number": 48, + "user": { + "login": "author", + "id": 12345 + }, + "base": { + "sha": "dddddddddddddddddddddddddddddddddddddddd", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + }, + "head": { + "sha": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "ref": "feature-branch", + "label": "author:feature-branch", + "repo": { + "full_name": "leanprover-community/SpliceBot" + } + } + } +} diff --git a/tests/actions/run_act_smoke.sh b/tests/actions/run_act_smoke.sh new file mode 100755 index 0000000..b3abb04 --- /dev/null +++ b/tests/actions/run_act_smoke.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +cd "$root_dir" + +common_args=( + --container-architecture linux/amd64 + --platform ubuntu-latest=catthehacker/ubuntu:act-latest + --no-cache-server +) +secret_args=() + +if [ -n "${GITHUB_TOKEN:-}" ]; then + secret_args+=(--secret GITHUB_TOKEN) +fi + +reusable_events=() +while IFS= read -r event_file; do + reusable_events+=("$event_file") +done < <(find tests/actions/events -maxdepth 1 -type f -name '*.json' | sort) + +if [ "${#reusable_events[@]}" -eq 0 ]; then + echo "No reusable workflow smoke fixtures found under tests/actions/events" >&2 + exit 1 +fi + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT +docker_config_dir="${tmp_dir}/docker-config" +mkdir -p "$docker_config_dir" +printf '{}\n' > "${docker_config_dir}/config.json" + +run_act_with_log() { + local log_file="$1" + shift + + if ! DOCKER_CONFIG="$docker_config_dir" DOCKER_AUTH_CONFIG='{}' "$@" > "$log_file" 2>&1; then + echo "act command failed; log follows:" >&2 + echo "--- begin act log ---" >&2 + cat "$log_file" >&2 + echo "--- end act log ---" >&2 + if grep -F "error getting credentials" "$log_file" >/dev/null 2>&1; then + echo "Hint: this looks like a Docker credential-helper problem while pulling the act runner image." >&2 + echo "Hint: try `docker pull catthehacker/ubuntu:act-latest` manually, or run with a clean Docker config." >&2 + fi + return 1 + fi +} + +for event_file in "${reusable_events[@]}"; do + expected_file="${event_file%.json}.expected" + if [ ! -f "$expected_file" ]; then + echo "Missing expected marker file for ${event_file}: ${expected_file}" >&2 + exit 1 + fi + + log_file="${tmp_dir}/$(basename "${event_file%.json}").log" + echo "==> Running reusable workflow smoke case: ${event_file}" + act_args=( + act + --workflows tests/actions/workflows/splice_harness.yaml + --eventpath "$event_file" + "${common_args[@]}" + ) + if [ -n "${GITHUB_TOKEN:-}" ]; then + act_args+=(--secret GITHUB_TOKEN) + fi + act_args+=(pull_request_review_comment) + run_act_with_log "$log_file" "${act_args[@]}" + + while IFS= read -r expected_marker || [ -n "$expected_marker" ]; do + [ -z "$expected_marker" ] && continue + if ! tests/actions/assert/log_contains.sh "$log_file" "$expected_marker"; then + echo "Reusable smoke case log:" >&2 + echo "--- begin act log ---" >&2 + cat "$log_file" >&2 + echo "--- end act log ---" >&2 + exit 1 + fi + done < "$expected_file" +done + +echo "==> Running composite action smoke harness" +composite_log_file="${tmp_dir}/splice_action_harness.log" +act_args=( + act + --workflows tests/actions/workflows/splice_action_harness.yaml + "${common_args[@]}" +) +if [ -n "${GITHUB_TOKEN:-}" ]; then + act_args+=(--secret GITHUB_TOKEN) +fi +act_args+=(workflow_dispatch) +run_act_with_log "$composite_log_file" "${act_args[@]}" diff --git a/tests/actions/workflows/splice_action_harness.yaml b/tests/actions/workflows/splice_action_harness.yaml new file mode 100644 index 0000000..f8f7cc3 --- /dev/null +++ b/tests/actions/workflows/splice_action_harness.yaml @@ -0,0 +1,136 @@ +name: Splice Action Act Harness + +on: + workflow_dispatch: + +permissions: {} + +jobs: + invalid-bridge-override-json: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run invalid bridge override case + id: invalid_bridge_override + continue-on-error: true + uses: ./.github/actions/splice-wf-run + with: + source_workflow: act-harness + bridge_override_json: >- + ["not","an","object"] + + - name: Assert invalid bridge override failed + shell: bash + run: | + set -euo pipefail + [ "${{ steps.invalid_bridge_override.outcome }}" = "failure" ] + + invalid-min-repo-permission: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run invalid permission case + id: invalid_permission + continue-on-error: true + uses: ./.github/actions/splice-wf-run + with: + source_workflow: act-harness + min_repo_permission: bogus + bridge_override_json: >- + {"file_path":"src/Foo.lean","commenter_login":"reviewer","pr_author_login":"author","base_ref":"master","base_repo":"leanprover-community/SpliceBot","head_repo":"leanprover-community/SpliceBot","head_sha":"1111111111111111111111111111111111111111","head_ref":"feature","head_label":"author:feature","committer":"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>","author":"author <12345+author@users.noreply.github.com>"} + + - name: Assert invalid permission failed + shell: bash + run: | + set -euo pipefail + [ "${{ steps.invalid_permission.outcome }}" = "failure" ] + + invalid-base-repo-format: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run invalid base repo format case + id: invalid_base_repo + continue-on-error: true + uses: ./.github/actions/splice-wf-run + with: + source_workflow: act-harness + bridge_override_json: >- + {"file_path":"src/Foo.lean","commenter_login":"reviewer","pr_author_login":"author","base_ref":"master","base_repo":"not-a-repo","head_repo":"leanprover-community/SpliceBot","head_sha":"1111111111111111111111111111111111111111","head_ref":"feature","head_label":"author:feature","committer":"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>","author":"author <12345+author@users.noreply.github.com>"} + + - name: Assert invalid base repo format failed + shell: bash + run: | + set -euo pipefail + [ "${{ steps.invalid_base_repo.outcome }}" = "failure" ] + + missing-bridge-data: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run missing bridge data case + id: missing_bridge + continue-on-error: true + uses: ./.github/actions/splice-wf-run + with: + source_workflow: act-harness + bridge_override_json: >- + {"file_path":"src/Foo.lean","commenter_login":"reviewer","base_ref":"master","base_repo":"leanprover-community/SpliceBot","head_repo":"leanprover-community/SpliceBot","head_sha":"1111111111111111111111111111111111111111","head_ref":"feature","head_label":"author:feature","committer":"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>","author":"author <12345+author@users.noreply.github.com>"} + + - name: Assert missing bridge data failed + shell: bash + run: | + set -euo pipefail + [ "${{ steps.missing_bridge.outcome }}" = "failure" ] + + authorized-pr-author-then-checkout-fails: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run pr-author authorization success case + id: pr_author_checkout_failure + continue-on-error: true + uses: ./.github/actions/splice-wf-run + with: + source_workflow: act-harness + allow_pr_author: 'true' + bridge_override_json: >- + {"file_path":"src/Foo.lean","commenter_login":"author","pr_author_login":"author","pr_number":"1","review_comment_id":"1","base_ref":"master","base_repo":"example/definitely-does-not-exist","head_repo":"example/definitely-does-not-exist","head_sha":"1111111111111111111111111111111111111111","head_ref":"feature","head_label":"author:feature","committer":"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>","author":"author <12345+author@users.noreply.github.com>"} + + - name: Assert pr-author auth succeeded before later failure + shell: bash + run: | + set -euo pipefail + [ "${{ steps.pr_author_checkout_failure.outcome }}" = "failure" ] + + allowed-user-then-checkout-fails: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Run allowed user authorization success case + id: allowed_user_checkout_failure + continue-on-error: true + uses: ./.github/actions/splice-wf-run + with: + source_workflow: act-harness + allowed_users: reviewer + bridge_override_json: >- + {"file_path":"src/Foo.lean","commenter_login":"reviewer","pr_author_login":"author","pr_number":"1","review_comment_id":"1","base_ref":"master","base_repo":"example/definitely-does-not-exist","head_repo":"example/definitely-does-not-exist","head_sha":"1111111111111111111111111111111111111111","head_ref":"feature","head_label":"author:feature","committer":"github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>","author":"author <12345+author@users.noreply.github.com>"} + + - name: Assert allowed user auth succeeded before later failure + shell: bash + run: | + set -euo pipefail + [ "${{ steps.allowed_user_checkout_failure.outcome }}" = "failure" ] diff --git a/tests/actions/workflows/splice_harness.yaml b/tests/actions/workflows/splice_harness.yaml new file mode 100644 index 0000000..cc47c3c --- /dev/null +++ b/tests/actions/workflows/splice_harness.yaml @@ -0,0 +1,17 @@ +name: Splice Act Harness + +on: + pull_request_review_comment: + types: + - created + - edited + +permissions: {} + +jobs: + run-reusable: + uses: ./.github/workflows/splice.yaml + with: + base_ref: master + emit_bridge_artifact: false +