Skip to content

Commit 6fc6b0e

Browse files
feat: add testing framework (#45)
Use [act](https://github.com/nektos/act) to test the workflows. prepared with codex
1 parent 668168b commit 6fc6b0e

29 files changed

+774
-5
lines changed

.github/actions/splice-wf-run/action.yml

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ inputs:
55
source_workflow:
66
description: Name of the source workflow that emitted the bridge artifact
77
required: true
8+
bridge_override_json:
9+
description: Internal test-only JSON object used instead of consuming the bridge artifact; not part of the supported public API
10+
required: false
11+
default: ''
812
allow_pr_author:
913
description: Whether to always allow the PR author to trigger splice-bot
1014
required: false
@@ -90,7 +94,8 @@ runs:
9094
echo "branch-token source: ${{ steps.token_mode.outputs.branch_token_source }}"
9195
9296
- name: Consume bridge artifact
93-
id: bridge
97+
if: inputs.bridge_override_json == ''
98+
id: bridge_consume
9499
uses: leanprover-community/privilege-escalation-bridge/consume@v1
95100
with:
96101
token: ${{ inputs.token || github.token }}
@@ -113,6 +118,88 @@ runs:
113118
committer=outputs.committer
114119
author=outputs.author
115120
121+
- name: Load bridge override
122+
if: inputs.bridge_override_json != ''
123+
id: bridge_override
124+
shell: bash
125+
env:
126+
BRIDGE_OVERRIDE_JSON: ${{ inputs.bridge_override_json }}
127+
run: |
128+
set -euo pipefail
129+
130+
ruby <<'RUBY'
131+
require 'json'
132+
133+
raw = ENV.fetch('BRIDGE_OVERRIDE_JSON', '')
134+
output_path = ENV.fetch('GITHUB_OUTPUT')
135+
data = JSON.parse(raw)
136+
137+
unless data.is_a?(Hash)
138+
warn 'bridge_override_json must be a JSON object.'
139+
exit 1
140+
end
141+
142+
keys = %w[
143+
pr_number
144+
review_comment_id
145+
file_path
146+
commenter_login
147+
pr_author_login
148+
base_ref
149+
base_repo
150+
head_repo
151+
head_sha
152+
head_ref
153+
head_label
154+
committer
155+
author
156+
]
157+
158+
File.open(output_path, 'a') do |f|
159+
keys.each do |key|
160+
value = data[key]
161+
next if value.nil?
162+
f.puts("#{key}=#{value}")
163+
end
164+
end
165+
RUBY
166+
167+
- name: Resolve bridge data
168+
id: bridge
169+
shell: bash
170+
env:
171+
PR_NUMBER: ${{ steps.bridge_override.outputs.pr_number || steps.bridge_consume.outputs.pr_number }}
172+
REVIEW_COMMENT_ID: ${{ steps.bridge_override.outputs.review_comment_id || steps.bridge_consume.outputs.review_comment_id }}
173+
FILE_PATH: ${{ steps.bridge_override.outputs.file_path || steps.bridge_consume.outputs.file_path }}
174+
COMMENTER_LOGIN: ${{ steps.bridge_override.outputs.commenter_login || steps.bridge_consume.outputs.commenter_login }}
175+
PR_AUTHOR_LOGIN: ${{ steps.bridge_override.outputs.pr_author_login || steps.bridge_consume.outputs.pr_author_login }}
176+
BASE_REF: ${{ steps.bridge_override.outputs.base_ref || steps.bridge_consume.outputs.base_ref }}
177+
BASE_REPO: ${{ steps.bridge_override.outputs.base_repo || steps.bridge_consume.outputs.base_repo }}
178+
HEAD_REPO: ${{ steps.bridge_override.outputs.head_repo || steps.bridge_consume.outputs.head_repo }}
179+
HEAD_SHA: ${{ steps.bridge_override.outputs.head_sha || steps.bridge_consume.outputs.head_sha }}
180+
HEAD_REF: ${{ steps.bridge_override.outputs.head_ref || steps.bridge_consume.outputs.head_ref }}
181+
HEAD_LABEL: ${{ steps.bridge_override.outputs.head_label || steps.bridge_consume.outputs.head_label }}
182+
COMMITTER: ${{ steps.bridge_override.outputs.committer || steps.bridge_consume.outputs.committer }}
183+
AUTHOR: ${{ steps.bridge_override.outputs.author || steps.bridge_consume.outputs.author }}
184+
run: |
185+
set -euo pipefail
186+
187+
{
188+
echo "pr_number=${PR_NUMBER}"
189+
echo "review_comment_id=${REVIEW_COMMENT_ID}"
190+
echo "file_path=${FILE_PATH}"
191+
echo "commenter_login=${COMMENTER_LOGIN}"
192+
echo "pr_author_login=${PR_AUTHOR_LOGIN}"
193+
echo "base_ref=${BASE_REF}"
194+
echo "base_repo=${BASE_REPO}"
195+
echo "head_repo=${HEAD_REPO}"
196+
echo "head_sha=${HEAD_SHA}"
197+
echo "head_ref=${HEAD_REF}"
198+
echo "head_label=${HEAD_LABEL}"
199+
echo "committer=${COMMITTER}"
200+
echo "author=${AUTHOR}"
201+
} >> "$GITHUB_OUTPUT"
202+
116203
- name: Authorize commenter
117204
if: steps.bridge.outputs.file_path != ''
118205
id: authorize_commenter

.github/workflows/act_smoke.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Act Smoke Tests
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- ".github/workflows/*.yml"
7+
- ".github/workflows/*.yaml"
8+
- ".github/actions/**/*.yml"
9+
- ".github/actions/**/*.yaml"
10+
- "tests/actions/**"
11+
workflow_dispatch:
12+
13+
jobs:
14+
act-smoke:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
19+
20+
- name: Install act
21+
shell: bash
22+
run: |
23+
set -euo pipefail
24+
curl --silent --show-error --location \
25+
https://raw.githubusercontent.com/nektos/act/master/install.sh \
26+
| sudo bash -s -- -b /usr/local/bin
27+
28+
- name: Run act smoke suite
29+
shell: bash
30+
env:
31+
DOCKER_AUTH_CONFIG: '{}'
32+
GITHUB_TOKEN: ${{ github.token }}
33+
run: |
34+
set -euo pipefail
35+
tests/actions/run_act_smoke.sh

.github/workflows/splice.yaml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ on:
2222
required: false
2323
type: string
2424
default: '${{ github.event.pull_request.user.login }} <${{ github.event.pull_request.user.id }}+${{ github.event.pull_request.user.login }}@users.noreply.github.com>'
25+
emit_bridge_artifact:
26+
description: Whether to emit the bridge artifact; useful to disable in local smoke tests
27+
required: false
28+
type: boolean
29+
default: true
2530

2631
permissions: {}
2732

@@ -33,6 +38,7 @@ jobs:
3338
- name: Verify caller event is pull_request_review_comment
3439
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
3540
with:
41+
github-token: ${{ github.token }}
3642
script: |
3743
const eventName = context.eventName;
3844
const hasReviewCommentPayload = !!(context.payload?.comment && context.payload?.pull_request);
@@ -53,21 +59,28 @@ jobs:
5359
# Pass the reusable input down to the script and emit it as an output named base_ref
5460
BASE_REF_INPUT: ${{ inputs.base_ref }}
5561
with:
62+
github-token: ${{ github.token }}
5663
script: |
5764
// NOTE: In actions/github-script, `core`, `github`, and `context` are provided globally.
5865
const comment = context.payload?.comment;
5966
const pr = context.payload?.pull_request;
6067
const body = comment?.body || "";
68+
const triggerLine = body
69+
.split(/\r?\n/)
70+
.find((line) => /^splice-bot\b/i.test(line));
6171
6272
core.info(`Comment body:\n---\n${body}\n---`);
6373
6474
// Start-of-line match across lines; no leading whitespace allowed
65-
if (!/^splice-bot\b/im.test(body)) {
75+
if (!triggerLine) {
6676
core.info("No `splice-bot` found at the start of a line, skipping.");
6777
core.setOutput("skip", "true");
6878
return;
6979
}
7080
81+
const triggerKeyword = triggerLine.replace(/^splice-bot\b/i, "").trim().split(/\s+/)[0] || "";
82+
core.info(`Trigger keyword: ${triggerKeyword || "(none)"}`);
83+
7184
const path = comment?.path;
7285
if (!path) {
7386
core.info("This review comment is not on a file diff (no .path). Nothing to do.");
@@ -106,21 +119,24 @@ jobs:
106119
core.setOutput("skip", "false");
107120
// IMPORTANT: base_ref now comes from the reusable workflow input (default master)
108121
core.setOutput("base_ref", baseRefFromInput);
122+
core.setOutput("trigger_keyword", triggerKeyword);
109123
110-
- if: ${{ steps.extract.outputs.skip != 'true' }}
124+
- if: ${{ steps.extract.outputs.skip != 'true' && inputs.emit_bridge_artifact }}
111125
name: Prepare bridge outputs
112126
run: |
113127
jq -n \
114128
--arg base_ref "${{ steps.extract.outputs.base_ref }}" \
129+
--arg trigger_keyword "${{ steps.extract.outputs.trigger_keyword }}" \
115130
--arg committer "${{ inputs.committer }}" \
116131
--arg author "${{ inputs.author }}" \
117132
'{
118133
base_ref: $base_ref,
134+
trigger_keyword: $trigger_keyword,
119135
committer: $committer,
120136
author: $author,
121137
}' > bridge-outputs.json
122138
123-
- if: ${{ steps.extract.outputs.skip != 'true' }}
139+
- if: ${{ steps.extract.outputs.skip != 'true' && inputs.emit_bridge_artifact }}
124140
name: Emit bridge artifact
125141
uses: leanprover-community/privilege-escalation-bridge/emit@v1
126142
with:

.github/workflows/workflow_lint.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
paths:
66
- ".github/workflows/*.yml"
77
- ".github/workflows/*.yaml"
8+
- ".github/actions/**/*.yml"
9+
- ".github/actions/**/*.yaml"
810
workflow_dispatch:
911

1012
jobs:
@@ -15,4 +17,4 @@ jobs:
1517
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1618

1719
- name: Run actionlint
18-
uses: raven-actions/actionlint@e01d1ea33dd6a5ed517d95b4c0c357560ac6f518 # v2.1.1
20+
uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2.1.2

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,51 @@ jobs:
122122
# branch_token: ${{ secrets.SPLICE_BOT_FORK_TOKEN }}
123123
```
124124

125+
## Local `act` smoke tests
126+
127+
You can run the lightweight workflow smoke tests locally with [`act`](https://github.com/nektos/act).
128+
These tests exercise the reusable trigger workflow against canned review-comment payloads and intentionally skip bridge-artifact emission.
129+
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.
130+
131+
Prerequisites:
132+
133+
- `act` installed locally
134+
- Docker running
135+
- `GITHUB_TOKEN` available if the workflow under test needs it, for example:
136+
`export GITHUB_TOKEN="$(gh auth token)"`
137+
138+
Run all local `act` smoke tests:
139+
140+
```bash
141+
tests/actions/run_act_smoke.sh
142+
```
143+
144+
That script auto-discovers all reusable-workflow event fixtures under `tests/actions/events/` and then runs the composite-action smoke harness.
145+
If `GITHUB_TOKEN` is set in the environment, the script passes it through to `act` as a secret.
146+
147+
You can also run individual harnesses directly.
148+
149+
Example commands:
150+
151+
```bash
152+
act \
153+
--workflows tests/actions/workflows/splice_harness.yaml \
154+
--eventpath tests/actions/events/pr-review-comment-basic.json \
155+
pull_request_review_comment
156+
```
157+
158+
```bash
159+
act \
160+
--workflows tests/actions/workflows/splice_harness.yaml \
161+
--eventpath tests/actions/events/pr-review-comment-keyword.json \
162+
pull_request_review_comment
163+
```
164+
165+
The canned event payloads live under `tests/actions/events/`.
166+
The CI workflow that runs the same smoke harness is `.github/workflows/act_smoke.yaml`.
167+
The reusable trigger harness lives at `tests/actions/workflows/splice_harness.yaml`.
168+
There is also a composite-action harness at `tests/actions/workflows/splice_action_harness.yaml` for deterministic non-network failure cases.
169+
125170
## GitHub App token minting in caller job
126171

127172
```yaml
@@ -238,6 +283,7 @@ Permission mapping references:
238283
| Name | Type | Required | Default | Description |
239284
| ---- | ---- | -------- | ------- | ----------- |
240285
| `source_workflow` | string | Yes | - | Name of the source workflow that emitted the bridge artifact. |
286+
| `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. |
241287
| `allow_pr_author` | boolean | No | `true` | Always allow PR author to trigger. |
242288
| `min_repo_permission` | string | No | `anyone` | Minimum permission threshold: `anyone`, `triage`, `write`. |
243289
| `allowed_teams` | string | No | `''` | Comma/newline-separated team allowlist (`team-slug` or `org/team-slug`). |
@@ -249,6 +295,7 @@ Permission mapping references:
249295
| `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. |
250296

251297
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 }}`.
298+
Test-only internal inputs such as `bridge_override_json` are intentionally undocumented outside the local test harness context and may change without notice.
252299

253300
## Sensitive `splice-wf-run` action inputs
254301

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
log_file="$1"
6+
expected_marker="$2"
7+
8+
if ! grep -F -- "$expected_marker" "$log_file" >/dev/null 2>&1; then
9+
echo "Expected log marker not found: $expected_marker" >&2
10+
echo "--- begin log ---" >&2
11+
cat "$log_file" >&2
12+
echo "--- end log ---" >&2
13+
exit 1
14+
fi
15+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Trigger keyword: (none)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"comment": {
3+
"body": "Please split this out.\n\nsplice-bot",
4+
"id": 102,
5+
"path": "src/Foo.lean",
6+
"user": {
7+
"login": "reviewer"
8+
}
9+
},
10+
"pull_request": {
11+
"number": 43,
12+
"user": {
13+
"login": "author",
14+
"id": 12345
15+
},
16+
"base": {
17+
"sha": "3333333333333333333333333333333333333333",
18+
"repo": {
19+
"full_name": "leanprover-community/SpliceBot"
20+
}
21+
},
22+
"head": {
23+
"sha": "4444444444444444444444444444444444444444",
24+
"ref": "feature-branch",
25+
"label": "author:feature-branch",
26+
"repo": {
27+
"full_name": "leanprover-community/SpliceBot"
28+
}
29+
}
30+
}
31+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Trigger keyword: (none)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"comment": {
3+
"body": "Please split this.\r\n\r\nsplice-bot\r\n",
4+
"id": 109,
5+
"path": "src/Foo.lean",
6+
"user": {
7+
"login": "reviewer"
8+
}
9+
},
10+
"pull_request": {
11+
"number": 50,
12+
"user": {
13+
"login": "author",
14+
"id": 12345
15+
},
16+
"base": {
17+
"sha": "ffffffffffffffffffffffffffffffffffffffff",
18+
"repo": {
19+
"full_name": "leanprover-community/SpliceBot"
20+
}
21+
},
22+
"head": {
23+
"sha": "1212121212121212121212121212121212121212",
24+
"ref": "feature-branch",
25+
"label": "author:feature-branch",
26+
"repo": {
27+
"full_name": "leanprover-community/SpliceBot"
28+
}
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)