Skip to content

Commit 549380d

Browse files
release: ci-doctor-action v1.0.0
0 parents  commit 549380d

6 files changed

Lines changed: 330 additions & 0 deletions

File tree

.github/workflows/self-test.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: self-test
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
permissions:
7+
contents: read
8+
pull-requests: write
9+
security-events: write
10+
jobs:
11+
smoke:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
15+
- name: Run self
16+
uses: ./
17+
with:
18+
fail-on: error
19+
upload-sarif: 'false'
20+
comment-on-pr: 'false'

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
*.log
3+
ci-doctor.md
4+
ci-doctor.json
5+
ci-doctor.sarif
6+
.npmrc-publish

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Changelog
2+
3+
## v1.0.0 — 2026-04-28
4+
5+
Initial release. Composite Action wrapping `ci-doctor@^0.5.0`.
6+
7+
- 16 audit rules (security, cost, reliability, hygiene)
8+
- Sticky PR comment (one comment per PR, updates in place)
9+
- SARIF upload to GitHub Code Scanning
10+
- `fail-on`, `only`, `disable`, `ci-doctor-version` inputs
11+
- Outputs: `sarif-path`, `markdown-path`, `finding-count`
12+
- Pinned dependencies (`upload-sarif@v3.27.0`, `github-script@v7.0.1`) by SHA

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 depmedic contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# ci-doctor-action
2+
3+
Audit your GitHub Actions workflows for **waste, cost, and security gaps** on every pull request.
4+
5+
Wraps the [`ci-doctor`](https://www.npmjs.com/package/ci-doctor) CLI (16 rules, 46 tests) and:
6+
7+
- posts a markdown summary as a sticky **PR comment**
8+
- uploads SARIF to **GitHub Code Scanning** (Security tab)
9+
- fails the build only on the severity threshold you choose
10+
11+
MIT, no telemetry, no auth required.
12+
13+
## Quick start
14+
15+
`.github/workflows/ci-doctor.yml`:
16+
17+
```yaml
18+
name: ci-doctor
19+
on:
20+
pull_request:
21+
paths:
22+
- '.github/workflows/**'
23+
permissions:
24+
contents: read
25+
pull-requests: write
26+
security-events: write
27+
jobs:
28+
audit:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: depmedicdev-byte/ci-doctor-action@v1
33+
```
34+
35+
That's it. Open a PR that touches `.github/workflows/`, you'll get a comment like:
36+
37+
| severity | rule | location | message |
38+
| --- | --- | --- | --- |
39+
| warn | docker-no-pin | release.yml | container image `node:22` is not pinned to @sha256 |
40+
| warn | service-no-healthcheck | test.yml | postgres service has no `--health-cmd` |
41+
| warn | expensive-runner | build.yml | uses `windows-latest` for `npm test` |
42+
43+
## Inputs
44+
45+
| input | default | description |
46+
| --- | --- | --- |
47+
| `path` | `.` | Path to scan (auto-discovers `.github/workflows`) |
48+
| `fail-on` | `error` | Fail the job on `info`, `warn`, or `error` |
49+
| `upload-sarif` | `true` | Upload to Code Scanning (needs `security-events: write`) |
50+
| `comment-on-pr` | `true` | Sticky PR comment (needs `pull-requests: write`) |
51+
| `ci-doctor-version` | `latest` | Pin to a specific npm version |
52+
| `only` | _empty_ | Comma-separated rule IDs to run exclusively |
53+
| `disable` | _empty_ | Comma-separated rule IDs to skip |
54+
55+
## Outputs
56+
57+
- `sarif-path` — `ci-doctor.sarif`
58+
- `markdown-path` — `ci-doctor.md`
59+
- `finding-count` — total findings (integer)
60+
61+
## Recipes
62+
63+
### Block PRs only on errors, warn on the rest
64+
65+
```yaml
66+
- uses: depmedicdev-byte/ci-doctor-action@v1
67+
with:
68+
fail-on: error
69+
```
70+
71+
### Hard mode: block any warning
72+
73+
```yaml
74+
- uses: depmedicdev-byte/ci-doctor-action@v1
75+
with:
76+
fail-on: warn
77+
```
78+
79+
### Disable a noisy rule
80+
81+
```yaml
82+
- uses: depmedicdev-byte/ci-doctor-action@v1
83+
with:
84+
disable: missing-concurrency,no-pin-actions
85+
```
86+
87+
### Run only the security rules
88+
89+
```yaml
90+
- uses: depmedicdev-byte/ci-doctor-action@v1
91+
with:
92+
only: docker-no-pin,no-pin-actions,after-script-leaks
93+
```
94+
95+
### Pin to a specific ci-doctor version (recommended)
96+
97+
```yaml
98+
- uses: depmedicdev-byte/ci-doctor-action@v1
99+
with:
100+
ci-doctor-version: '0.5.0'
101+
```
102+
103+
## What it checks (16 rules)
104+
105+
**security**: `no-pin-actions`, `pull-request-target-checkout`, `script-injection-context`, `insecure-checkout-token`, `docker-no-pin`, `after-script-leaks`
106+
107+
**cost**: `expensive-runner`, `missing-concurrency`, `missing-cache`, `wide-paths`, `cron-storm`, `service-no-healthcheck`
108+
109+
**reliability**: `flaky-retries`, `missing-timeout-minutes`, `legacy-actions-version`
110+
111+
**hygiene**: `actions-floating-tag`
112+
113+
Full descriptions: <https://depmedicdev-byte.github.io/rules.html>
114+
115+
## Related
116+
117+
- CLI: <https://www.npmjs.com/package/ci-doctor>
118+
- VS Code extension: `depmedic-vscode` (inline squigglies for the same 16 rules)
119+
- GitHub App: `depmedic-bot` (zero-config, Marketplace listing pending)
120+
- GitLab port: <https://www.npmjs.com/package/gitlab-ci-doctor>
121+
- Bitbucket port: <https://www.npmjs.com/package/bitbucket-ci-doctor>
122+
- Pin all `uses:` to SHAs: `npx pin-actions`
123+
124+
## License
125+
126+
MIT © depmedic

action.yml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
name: 'ci-doctor'
2+
description: 'Audit GitHub Actions workflows for waste, cost, and security gaps. 16 rules, SARIF + PR comment.'
3+
author: 'depmedic'
4+
5+
branding:
6+
icon: 'activity'
7+
color: 'green'
8+
9+
inputs:
10+
path:
11+
description: 'Path to scan. Default = repo root, which auto-discovers .github/workflows.'
12+
required: false
13+
default: '.'
14+
fail-on:
15+
description: 'Severity threshold for non-zero exit: info | warn | error. Default = error (only hard-fails on error-level findings).'
16+
required: false
17+
default: 'error'
18+
upload-sarif:
19+
description: 'Upload SARIF to GitHub Code Scanning so findings show under the Security tab. Requires security-events: write permission.'
20+
required: false
21+
default: 'true'
22+
comment-on-pr:
23+
description: 'Post a markdown table of findings as a PR comment. Requires pull-requests: write permission.'
24+
required: false
25+
default: 'true'
26+
ci-doctor-version:
27+
description: 'npm version of ci-doctor to run. Pin to a SHA-tag for reproducibility.'
28+
required: false
29+
default: 'latest'
30+
only:
31+
description: 'Comma-separated list of rule IDs to run exclusively.'
32+
required: false
33+
default: ''
34+
disable:
35+
description: 'Comma-separated list of rule IDs to skip.'
36+
required: false
37+
default: ''
38+
39+
outputs:
40+
sarif-path:
41+
description: 'Path to the SARIF file produced by ci-doctor.'
42+
value: ${{ steps.sarif.outputs.sarif-path }}
43+
markdown-path:
44+
description: 'Path to the markdown report produced by ci-doctor.'
45+
value: ${{ steps.markdown.outputs.markdown-path }}
46+
finding-count:
47+
description: 'Total number of findings (parsed from JSON report).'
48+
value: ${{ steps.json.outputs.finding-count }}
49+
50+
runs:
51+
using: 'composite'
52+
steps:
53+
- name: Resolve flags
54+
id: flags
55+
shell: bash
56+
run: |
57+
FLAGS=""
58+
if [ -n "${{ inputs.only }}" ]; then FLAGS="$FLAGS --only=${{ inputs.only }}"; fi
59+
if [ -n "${{ inputs.disable }}" ]; then FLAGS="$FLAGS --disable=${{ inputs.disable }}"; fi
60+
echo "flags=$FLAGS" >> "$GITHUB_OUTPUT"
61+
echo "ci-doctor flags: $FLAGS"
62+
63+
- name: Run ci-doctor (markdown report)
64+
id: markdown
65+
shell: bash
66+
run: |
67+
set +e
68+
npx --yes ci-doctor@${{ inputs.ci-doctor-version }} ${{ steps.flags.outputs.flags }} --markdown "${{ inputs.path }}" > ci-doctor.md
69+
echo "markdown-path=ci-doctor.md" >> "$GITHUB_OUTPUT"
70+
echo "::group::ci-doctor markdown"
71+
cat ci-doctor.md || true
72+
echo "::endgroup::"
73+
74+
- name: Run ci-doctor (SARIF for code scanning)
75+
id: sarif
76+
shell: bash
77+
run: |
78+
set +e
79+
npx --yes ci-doctor@${{ inputs.ci-doctor-version }} ${{ steps.flags.outputs.flags }} --sarif "${{ inputs.path }}" > ci-doctor.sarif
80+
echo "sarif-path=ci-doctor.sarif" >> "$GITHUB_OUTPUT"
81+
82+
- name: Run ci-doctor (JSON for finding-count output)
83+
id: json
84+
shell: bash
85+
run: |
86+
set +e
87+
npx --yes ci-doctor@${{ inputs.ci-doctor-version }} ${{ steps.flags.outputs.flags }} --json "${{ inputs.path }}" > ci-doctor.json
88+
if [ -s ci-doctor.json ]; then
89+
COUNT=$(node -e "try{const j=JSON.parse(require('fs').readFileSync('ci-doctor.json','utf8'));console.log(Array.isArray(j.findings)?j.findings.length:(j.summary&&j.summary.total||0))}catch(e){console.log(0)}")
90+
else
91+
COUNT=0
92+
fi
93+
echo "finding-count=$COUNT" >> "$GITHUB_OUTPUT"
94+
echo "Findings: $COUNT"
95+
96+
- name: Upload SARIF to GitHub Code Scanning
97+
if: ${{ inputs.upload-sarif == 'true' && hashFiles('ci-doctor.sarif') != '' }}
98+
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.27.0
99+
with:
100+
sarif_file: ci-doctor.sarif
101+
category: ci-doctor
102+
continue-on-error: true
103+
104+
- name: Comment on pull request
105+
if: ${{ inputs.comment-on-pr == 'true' && github.event_name == 'pull_request' && hashFiles('ci-doctor.md') != '' }}
106+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
107+
with:
108+
script: |
109+
const fs = require('fs');
110+
let body = '';
111+
try { body = fs.readFileSync('ci-doctor.md', 'utf8'); } catch { return; }
112+
body = body.trim();
113+
if (!body) return;
114+
if (body.length > 60000) body = body.slice(0, 60000) + '\n\n_truncated_';
115+
const marker = '<!-- ci-doctor-bot -->';
116+
const header = `${marker}\n\n## ci-doctor findings\n\n`;
117+
const full = header + body + '\n\n---\n_Powered by [ci-doctor](https://www.npmjs.com/package/ci-doctor) — [docs](https://depmedicdev-byte.github.io/)_';
118+
const { data: comments } = await github.rest.issues.listComments({
119+
owner: context.repo.owner,
120+
repo: context.repo.repo,
121+
issue_number: context.issue.number,
122+
per_page: 100,
123+
});
124+
const existing = comments.find((c) => c.body && c.body.includes(marker));
125+
if (existing) {
126+
await github.rest.issues.updateComment({
127+
owner: context.repo.owner,
128+
repo: context.repo.repo,
129+
comment_id: existing.id,
130+
body: full,
131+
});
132+
} else {
133+
await github.rest.issues.createComment({
134+
owner: context.repo.owner,
135+
repo: context.repo.repo,
136+
issue_number: context.issue.number,
137+
body: full,
138+
});
139+
}
140+
continue-on-error: true
141+
142+
- name: Enforce severity threshold
143+
shell: bash
144+
run: |
145+
npx --yes ci-doctor@${{ inputs.ci-doctor-version }} ${{ steps.flags.outputs.flags }} --severity=${{ inputs.fail-on }} "${{ inputs.path }}"

0 commit comments

Comments
 (0)