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
89 changes: 88 additions & 1 deletion .github/actions/splice-wf-run/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions .github/workflows/act_smoke.yaml
Original file line number Diff line number Diff line change
@@ -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
22 changes: 19 additions & 3 deletions .github/workflows/splice.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}

Expand All @@ -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);
Expand All @@ -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.");
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/workflow_lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
paths:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
- ".github/actions/**/*.yml"
- ".github/actions/**/*.yaml"
workflow_dispatch:

jobs:
Expand All @@ -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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`). |
Expand All @@ -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

Expand Down
15 changes: 15 additions & 0 deletions tests/actions/assert/log_contains.sh
Original file line number Diff line number Diff line change
@@ -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

1 change: 1 addition & 0 deletions tests/actions/events/pr-review-comment-basic.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Trigger keyword: (none)
31 changes: 31 additions & 0 deletions tests/actions/events/pr-review-comment-basic.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Trigger keyword: (none)
31 changes: 31 additions & 0 deletions tests/actions/events/pr-review-comment-carriage-return.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Trigger keyword: (none)
Loading