Skip to content

Commit 12e78dd

Browse files
feat(action): add a GitHub Action for cargo fallow (#18)
* feat(action): add a GitHub Action for cargo fallow action.yml at the repository root runs one cargo fallow command in a job. It writes the gates and the findings to the job summary, writes each finding as an annotation, can upload the SARIF log to code scanning, and fails the job when an enforced gate fails or the run has an error. - Inputs: the command (check, the single checks, and mutants with its own inputs), the format, fail-on-issues (the gate flag of each command), args, base, and the install method. - Install: the PATH, a release binary with a SHA-256 check, cargo install from crates.io, or a build of the action source. auto takes the first that works, so it builds from source until the first release. - Inputs reach the scripts only through environment variables, and args get no shell expansion. Each third-party action has a SHA pin. - The JSON run gives the verdict. mutants runs once, and a jq filter writes its SARIF log. A test compares that filter with a real run. github-action.yml tests the scripts and uses the action on this repository on Linux, macOS and Windows. Signed-off-by: Bart Waardenburg <bart@waardenburg.dev> * fix(action): download a release binary only over HTTPS curl now accepts only https (and file for the local test release), also after a redirect, with TLS 1.2 or later. A plain http URL fails before a connection. Signed-off-by: Bart Waardenburg <bart@waardenburg.dev> --------- Signed-off-by: Bart Waardenburg <bart@waardenburg.dev>
1 parent bae5271 commit 12e78dd

30 files changed

Lines changed: 2882 additions & 2 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ jobs:
294294
# fallow passes `--config .github/zizmor.yml` for its release and
295295
# Dependabot workflows. This repository has no such workflow, and
296296
# zizmor 1.26.1 reports no finding here, so it needs no config file.
297-
- run: uvx zizmor@1.26.1 --min-confidence medium --format plain .github/
297+
- run: uvx zizmor@1.26.1 --min-confidence medium --format plain .github/ action.yml
298298

299299
msrv:
300300
name: MSRV (1.98)
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
name: GitHub Action
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
# Deny all permissions by default, grant per-job as needed
10+
permissions: {}
11+
12+
concurrency:
13+
group: action-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
14+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15+
16+
env:
17+
CARGO_TERM_COLOR: always
18+
19+
jobs:
20+
scripts:
21+
name: Action scripts (${{ matrix.os }})
22+
runs-on: ${{ matrix.os }}
23+
timeout-minutes: 10
24+
permissions:
25+
contents: read
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
os: [ubuntu-26.04, macos-latest]
30+
steps:
31+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32+
with:
33+
persist-credentials: false
34+
- name: ShellCheck
35+
if: runner.os == 'Linux'
36+
run: |
37+
if ! command -v shellcheck > /dev/null 2>&1; then
38+
sudo apt-get update -q && sudo apt-get install -y -q shellcheck
39+
fi
40+
shellcheck -x action/scripts/*.sh action/tests/run.sh
41+
- name: Test the scripts and the jq filters
42+
run: bash action/tests/run.sh
43+
44+
# Run the action of this checkout on this repository. The first step builds
45+
# cargo-fallow from source (install: source). Each later step finds that
46+
# binary on the PATH (install: auto).
47+
self-test:
48+
name: Action self-test (${{ matrix.os }})
49+
runs-on: ${{ matrix.os }}
50+
timeout-minutes: 45
51+
permissions:
52+
contents: read
53+
# The sarif: true step checks code scanning and uploads when it can.
54+
security-events: write
55+
strategy:
56+
fail-fast: false
57+
matrix:
58+
os: [ubuntu-26.04, macos-latest, windows-latest]
59+
env:
60+
# `cargo install` builds in this folder, so the Rust cache keeps it.
61+
CARGO_TARGET_DIR: ${{ github.workspace }}/target
62+
steps:
63+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
64+
with:
65+
persist-credentials: false
66+
# `base: auto` compares with the base commit of the pull request.
67+
fetch-depth: 0
68+
- uses: ./.github/actions/setup-rust
69+
with:
70+
components: llvm-tools
71+
cache-key: action-self-test
72+
- uses: taiki-e/install-action@1e29709c0ec63babbe8f266397a465f29a825f91 # v2.87.31
73+
with:
74+
tool: cargo-nextest
75+
76+
- name: check (install from source)
77+
id: check
78+
uses: ./
79+
with:
80+
command: check
81+
install: source
82+
83+
- name: deps
84+
id: deps
85+
uses: ./
86+
with:
87+
command: deps
88+
format: json
89+
90+
- name: boundaries
91+
id: boundaries
92+
uses: ./
93+
with:
94+
command: boundaries
95+
96+
- name: size
97+
id: size
98+
uses: ./
99+
with:
100+
command: size
101+
base: auto
102+
103+
- name: visibility
104+
id: visibility
105+
uses: ./
106+
with:
107+
command: visibility
108+
fail-on-issues: 'false'
109+
110+
- name: complexity (report only, with SARIF)
111+
id: complexity
112+
uses: ./
113+
with:
114+
command: complexity
115+
fail-on-issues: 'false'
116+
format: sarif
117+
sarif: 'true'
118+
max-annotations: '5'
119+
120+
- name: dupes (the gate must fail)
121+
id: dupes
122+
continue-on-error: true
123+
uses: ./
124+
with:
125+
command: dupes
126+
annotations: 'false'
127+
128+
- name: An invalid manifest (exit code 2)
129+
id: broken
130+
continue-on-error: true
131+
uses: ./
132+
with:
133+
command: check
134+
manifest-path: does-not-exist/Cargo.toml
135+
136+
- name: mutants on a fixture crate
137+
id: mutants
138+
if: runner.os == 'Linux'
139+
uses: ./
140+
with:
141+
command: mutants
142+
working-directory: tests/fixtures/action-mutants
143+
fail-on-issues: 'false'
144+
mutants-jobs: '2'
145+
146+
- name: Check the outputs
147+
shell: bash
148+
env:
149+
CHECK_METHOD: ${{ steps.check.outputs.install-method }}
150+
CHECK_EXIT: ${{ steps.check.outputs.exit-code }}
151+
CHECK_ISSUES: ${{ steps.check.outputs.issues }}
152+
DEPS_METHOD: ${{ steps.deps.outputs.install-method }}
153+
DEPS_REPORT: ${{ steps.deps.outputs.report }}
154+
DEPS_RESULTS: ${{ steps.deps.outputs.results }}
155+
BOUNDARIES_EXIT: ${{ steps.boundaries.outputs.exit-code }}
156+
SIZE_EXIT: ${{ steps.size.outputs.exit-code }}
157+
VISIBILITY_EXIT: ${{ steps.visibility.outputs.exit-code }}
158+
COMPLEXITY_ISSUES: ${{ steps.complexity.outputs.issues }}
159+
COMPLEXITY_SARIF: ${{ steps.complexity.outputs.sarif }}
160+
COMPLEXITY_REPORT: ${{ steps.complexity.outputs.report }}
161+
COMPLEXITY_FAILED: ${{ steps.complexity.outputs.gates-failed }}
162+
DUPES_OUTCOME: ${{ steps.dupes.outcome }}
163+
DUPES_EXIT: ${{ steps.dupes.outputs.exit-code }}
164+
BROKEN_OUTCOME: ${{ steps.broken.outcome }}
165+
BROKEN_EXIT: ${{ steps.broken.outputs.exit-code }}
166+
BROKEN_CODE: ${{ steps.broken.outputs.error-code }}
167+
MUTANTS_SCORE: ${{ steps.mutants.outputs.mutation-score }}
168+
MUTANTS_SARIF: ${{ steps.mutants.outputs.sarif }}
169+
MUTANTS_FAILED: ${{ steps.mutants.outputs.gates-failed }}
170+
run: |
171+
failures=0
172+
expect() {
173+
if [ "$2" = "$3" ]; then
174+
echo "ok $1"
175+
else
176+
echo "::error::$1: expected '$3', got '$2'"
177+
failures=$((failures + 1))
178+
fi
179+
}
180+
expect "check: install method" "$CHECK_METHOD" "source"
181+
expect "check: exit code" "$CHECK_EXIT" "0"
182+
expect "check: issues" "$CHECK_ISSUES" "0"
183+
expect "deps: install method" "$DEPS_METHOD" "path"
184+
expect "deps: format json reports the JSON file" "$DEPS_REPORT" "$DEPS_RESULTS"
185+
expect "deps: kind" "$(jq -r .kind "$DEPS_RESULTS")" "deps"
186+
expect "boundaries: exit code" "$BOUNDARIES_EXIT" "0"
187+
expect "size: exit code" "$SIZE_EXIT" "0"
188+
expect "visibility: exit code" "$VISIBILITY_EXIT" "0"
189+
expect "complexity: report is the SARIF log" "$COMPLEXITY_REPORT" "$COMPLEXITY_SARIF"
190+
expect "complexity: SARIF version" "$(jq -r .version "$COMPLEXITY_SARIF")" "2.1.0"
191+
expect "complexity: SARIF results match issues" "$(jq '.runs[0].results | length' "$COMPLEXITY_SARIF")" "$COMPLEXITY_ISSUES"
192+
expect "complexity: failed gate" "$COMPLEXITY_FAILED" "health-findings"
193+
expect "dupes: the gate fails the step" "$DUPES_OUTCOME" "failure"
194+
expect "dupes: exit code" "$DUPES_EXIT" "1"
195+
expect "broken: the step fails" "$BROKEN_OUTCOME" "failure"
196+
expect "broken: exit code" "$BROKEN_EXIT" "2"
197+
expect "broken: error code" "$BROKEN_CODE" "CARGO_FALLOW_MANIFEST_NOT_FOUND"
198+
if [ "$RUNNER_OS" = "Linux" ]; then
199+
expect "mutants: score" "$MUTANTS_SCORE" "66.7"
200+
expect "mutants: failed gate" "$MUTANTS_FAILED" "surviving-mutants"
201+
expect "mutants: SARIF path from the repository root" \
202+
"$(jq -r '.runs[0].results[0].locations[0].physicalLocation.artifactLocation.uri' "$MUTANTS_SARIF")" \
203+
"tests/fixtures/action-mutants/src/lib.rs"
204+
fi
205+
exit "$failures"
206+
207+
action-success:
208+
name: Action checks passed
209+
if: always()
210+
needs: [scripts, self-test]
211+
runs-on: ubuntu-26.04
212+
timeout-minutes: 5
213+
permissions: {}
214+
steps:
215+
- name: All checks passed
216+
env:
217+
RESULTS: ${{ toJSON(needs.*.result) }}
218+
run: |
219+
for result in $(echo "$RESULTS" | jq -r '.[]'); do
220+
if [ "$result" != "success" ]; then
221+
echo "::error::Job failed with result: $result"
222+
exit 1
223+
fi
224+
done

‎CONTRIBUTING.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ hooks.
7373
| Security advisories | `cargo audit` | none | `ci.yml` |
7474
| Unused dependencies | `cargo shear` | pre-push | `ci.yml` |
7575
| Workflow syntax | `actionlint` | none | none |
76-
| Workflow security | `uvx zizmor@1.26.1 --min-confidence medium --format plain .github/` | none | `ci.yml` |
76+
| Workflow security | `uvx zizmor@1.26.1 --min-confidence medium --format plain .github/ action.yml` | none | `ci.yml` |
77+
| GitHub Action scripts | `shellcheck -x action/scripts/*.sh action/tests/run.sh` and `bash action/tests/run.sh` | none | `github-action.yml` |
78+
| GitHub Action self-test | The action of the checkout on this repository | none | `github-action.yml`, Linux, macOS and Windows |
7779
| MSRV | `cargo +1.98.0 check --locked` | none | `ci.yml` |
7880
| Commit messages | `commitlint` | commit-msg | `commitlint.yml` |
7981
| DCO sign-off | `node .github/scripts/check-dco.mjs` | commit-msg | `commitlint.yml` |

‎README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -739,6 +739,19 @@ The growth rule of `size` runs git with a clean environment: it removes
739739
git to another repository. `--base` must name a commit. A value that starts with
740740
`-` is an error. A changed path that is not UTF-8 text is in `skipped_files`.
741741

742+
## GitHub Action
743+
744+
The repository is also a GitHub Action. It runs one command, writes the job
745+
summary, shows each finding as an annotation, and can upload the SARIF log to
746+
code scanning. [action/README.md](action/README.md) lists the inputs and the
747+
outputs.
748+
749+
```yaml
750+
- uses: fallow-rs/cargo-fallow@<sha>
751+
with:
752+
command: check
753+
```
754+
742755
## Exit codes
743756
744757
| Code | Meaning |

0 commit comments

Comments
 (0)