oss-signal 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +44 -10
- package/action.yml +45 -0
- package/docs/adoption-evidence.md +61 -0
- package/docs/assets/github-step-summary.svg +24 -0
- package/docs/codex-for-oss-application.md +76 -0
- package/docs/examples/github-action-workflow.yml +23 -0
- package/docs/examples/github-url-report.md +41 -0
- package/docs/outreach/README.md +5 -5
- package/docs/self-audit.md +2 -1
- package/package.json +4 -2
- package/src/action.js +153 -0
- package/src/cli.js +17 -5
- package/src/index.js +227 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Added GitHub Actions step summary output for readable workflow reports.
|
|
6
|
+
- Added a `summary` Action input for turning step summary output on or off.
|
|
7
|
+
|
|
8
|
+
## 0.2.0
|
|
9
|
+
|
|
10
|
+
- Added direct GitHub repository audits for public repositories.
|
|
11
|
+
- Added `owner/repo` shorthand and `--ref` support.
|
|
12
|
+
- Added GitHub community profile evidence for shared maintainer files.
|
|
13
|
+
- Added a zero-dependency GitHub Action wrapper with score outputs.
|
|
14
|
+
- Updated the CLI help output and package metadata for npm 11.
|
|
15
|
+
|
|
3
16
|
## 0.1.0
|
|
4
17
|
|
|
5
18
|
- Initial CLI with Markdown and JSON output.
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# oss-signal
|
|
2
2
|
|
|
3
3
|
[](https://github.com/SalmonPlays/oss-signal/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/SalmonPlays/oss-signal/actions/workflows/repository-health.yml)
|
|
4
5
|
[](https://www.npmjs.com/package/oss-signal)
|
|
5
6
|
[](https://www.npmjs.com/package/oss-signal)
|
|
6
7
|
[](LICENSE)
|
|
@@ -21,6 +22,7 @@ Open-source projects often fail quietly because the maintainer workflow is undoc
|
|
|
21
22
|
- Contributors can attach a report to a cleanup issue or pull request.
|
|
22
23
|
- Teams can gate release readiness with `--fail-under`.
|
|
23
24
|
- Foundations and working groups can compare repository hygiene across many projects.
|
|
25
|
+
- CI maintainers can add it as a GitHub Action, show the score in the workflow summary, and publish the report as an artifact.
|
|
24
26
|
|
|
25
27
|
## Install
|
|
26
28
|
|
|
@@ -45,6 +47,13 @@ Audit the current directory:
|
|
|
45
47
|
oss-signal
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
Audit a public GitHub repository without cloning it:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
oss-signal https://github.com/SalmonPlays/oss-signal
|
|
54
|
+
oss-signal platformatic/massimo --format json
|
|
55
|
+
```
|
|
56
|
+
|
|
48
57
|
Write a Markdown report:
|
|
49
58
|
|
|
50
59
|
```bash
|
|
@@ -73,6 +82,8 @@ oss-signal . --format markdown --output docs/maintainer-readiness.md
|
|
|
73
82
|
|
|
74
83
|
See [docs/rules.md](docs/rules.md) for rule details and scoring weights.
|
|
75
84
|
|
|
85
|
+
For GitHub URL audits, `oss-signal` reads the repository file tree through the GitHub API and also uses GitHub's community profile signal when available. This lets it detect organization-level files such as a shared code of conduct.
|
|
86
|
+
|
|
76
87
|
## Real Output
|
|
77
88
|
|
|
78
89
|
This repository audits itself at **100/100 (A)**:
|
|
@@ -86,7 +97,7 @@ Summary:
|
|
|
86
97
|
- Total checks: 15
|
|
87
98
|
```
|
|
88
99
|
|
|
89
|
-
See [docs/self-audit.md](docs/self-audit.md) for the full self-audit report.
|
|
100
|
+
See [docs/self-audit.md](docs/self-audit.md) for the full local self-audit report and [docs/examples/github-url-report.md](docs/examples/github-url-report.md) for the GitHub URL audit output.
|
|
90
101
|
|
|
91
102
|
## Field Audits
|
|
92
103
|
|
|
@@ -98,6 +109,8 @@ See [docs/self-audit.md](docs/self-audit.md) for the full self-audit report.
|
|
|
98
109
|
|
|
99
110
|
See [docs/outreach](docs/outreach) for the reports and draft issue text. Drafts are not posted automatically; maintainers should only receive specific, useful, and respectful suggestions.
|
|
100
111
|
|
|
112
|
+
For a compact maintainer/adoption summary, see [docs/adoption-evidence.md](docs/adoption-evidence.md).
|
|
113
|
+
|
|
101
114
|
## Example Recommendation Output
|
|
102
115
|
|
|
103
116
|
```text
|
|
@@ -120,14 +133,24 @@ When `--fail-under <score>` is provided, it exits with `1` if the score is below
|
|
|
120
133
|
oss-signal . --fail-under 80
|
|
121
134
|
```
|
|
122
135
|
|
|
123
|
-
##
|
|
136
|
+
## GitHub Action
|
|
124
137
|
|
|
125
|
-
Add
|
|
138
|
+
Add `oss-signal` directly to a GitHub Actions workflow:
|
|
126
139
|
|
|
127
140
|
```yaml
|
|
128
|
-
-
|
|
141
|
+
- uses: SalmonPlays/oss-signal@v0.3.0
|
|
142
|
+
id: oss-signal
|
|
143
|
+
with:
|
|
144
|
+
fail-under: "80"
|
|
145
|
+
output: oss-signal-report.md
|
|
146
|
+
summary: "true"
|
|
147
|
+
- run: echo "score ${{ steps.oss-signal.outputs.score }} (${{ steps.oss-signal.outputs.grade }})"
|
|
129
148
|
```
|
|
130
149
|
|
|
150
|
+
The Action writes a concise GitHub Actions step summary by default, so reviewers can see the score and recommended next steps without downloading an artifact. Set `summary: "false"` to disable it.
|
|
151
|
+
|
|
152
|
+

|
|
153
|
+
|
|
131
154
|
Full workflow example:
|
|
132
155
|
|
|
133
156
|
```yaml
|
|
@@ -143,25 +166,36 @@ jobs:
|
|
|
143
166
|
runs-on: ubuntu-latest
|
|
144
167
|
steps:
|
|
145
168
|
- uses: actions/checkout@v4
|
|
146
|
-
- uses:
|
|
169
|
+
- uses: SalmonPlays/oss-signal@v0.3.0
|
|
170
|
+
id: oss-signal
|
|
147
171
|
with:
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
fail-under: "80"
|
|
173
|
+
output: oss-signal-report.md
|
|
174
|
+
summary: "true"
|
|
150
175
|
- uses: actions/upload-artifact@v4
|
|
151
176
|
with:
|
|
152
177
|
name: oss-signal-report
|
|
153
178
|
path: oss-signal-report.md
|
|
154
179
|
```
|
|
155
180
|
|
|
181
|
+
See [docs/examples/github-action-workflow.yml](docs/examples/github-action-workflow.yml) for a copyable workflow.
|
|
182
|
+
|
|
183
|
+
This repository dogfoods the public Action tag in [Repository health](.github/workflows/repository-health.yml), which runs `SalmonPlays/oss-signal@v0.3.0` against the repository and uploads the Markdown report artifact.
|
|
184
|
+
|
|
185
|
+
You can also run the CLI directly in CI:
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
- run: npx oss-signal . --format markdown --output oss-signal-report.md --fail-under 80
|
|
189
|
+
```
|
|
190
|
+
|
|
156
191
|
## Current Limitations
|
|
157
192
|
|
|
158
|
-
- It
|
|
159
|
-
-
|
|
193
|
+
- It checks deterministic maintenance signals, not code quality or project importance.
|
|
194
|
+
- GitHub URL mode uses unauthenticated API requests unless `GITHUB_TOKEN` is set, so very heavy usage may hit GitHub rate limits.
|
|
160
195
|
- A high score does not prove a project is important. It proves the maintainer workflow is documented and automatable.
|
|
161
196
|
|
|
162
197
|
## Roadmap
|
|
163
198
|
|
|
164
|
-
- GitHub API mode for public repository URLs
|
|
165
199
|
- Ecosystem-specific profiles for Python, Rust, Go, and JavaScript packages
|
|
166
200
|
- SARIF output for code scanning dashboards
|
|
167
201
|
- Rules for release automation and provenance metadata
|
package/action.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: oss-signal
|
|
2
|
+
description: Audit open-source repository maintenance readiness and produce actionable maintainer next steps.
|
|
3
|
+
author: SalmonPlays
|
|
4
|
+
branding:
|
|
5
|
+
icon: activity
|
|
6
|
+
color: blue
|
|
7
|
+
inputs:
|
|
8
|
+
path:
|
|
9
|
+
description: Local repository path, GitHub URL, or owner/repo shorthand to audit.
|
|
10
|
+
required: false
|
|
11
|
+
default: "."
|
|
12
|
+
format:
|
|
13
|
+
description: Output format, either markdown or json.
|
|
14
|
+
required: false
|
|
15
|
+
default: markdown
|
|
16
|
+
output:
|
|
17
|
+
description: Report file path.
|
|
18
|
+
required: false
|
|
19
|
+
default: oss-signal-report.md
|
|
20
|
+
summary:
|
|
21
|
+
description: Write a concise report to the GitHub Actions step summary.
|
|
22
|
+
required: false
|
|
23
|
+
default: "true"
|
|
24
|
+
fail-under:
|
|
25
|
+
description: Fail the action when the score is below this number.
|
|
26
|
+
required: false
|
|
27
|
+
max-files:
|
|
28
|
+
description: Maximum files to inspect.
|
|
29
|
+
required: false
|
|
30
|
+
default: "20000"
|
|
31
|
+
ref:
|
|
32
|
+
description: Git ref for GitHub URL audits.
|
|
33
|
+
required: false
|
|
34
|
+
outputs:
|
|
35
|
+
score:
|
|
36
|
+
description: Numeric maintainer-readiness score.
|
|
37
|
+
grade:
|
|
38
|
+
description: Letter grade for the maintainer-readiness score.
|
|
39
|
+
failed:
|
|
40
|
+
description: Number of failed checks.
|
|
41
|
+
report-path:
|
|
42
|
+
description: Path to the generated report file, when output is enabled.
|
|
43
|
+
runs:
|
|
44
|
+
using: node20
|
|
45
|
+
main: src/action.js
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Adoption Evidence
|
|
2
|
+
|
|
3
|
+
This page collects the public evidence that `oss-signal` is built for real open-source maintainer workflows.
|
|
4
|
+
|
|
5
|
+
## Project Links
|
|
6
|
+
|
|
7
|
+
- Repository: https://github.com/SalmonPlays/oss-signal
|
|
8
|
+
- npm package: https://www.npmjs.com/package/oss-signal
|
|
9
|
+
- GitHub Action tag: https://github.com/SalmonPlays/oss-signal/tree/v0.3.0
|
|
10
|
+
- GitHub Action metadata: [action.yml](../action.yml)
|
|
11
|
+
- Public dogfood workflow: [.github/workflows/repository-health.yml](../.github/workflows/repository-health.yml)
|
|
12
|
+
- Self-audit report: [docs/self-audit.md](self-audit.md)
|
|
13
|
+
- GitHub URL audit report: [docs/examples/github-url-report.md](examples/github-url-report.md)
|
|
14
|
+
- GitHub Action workflow example: [docs/examples/github-action-workflow.yml](examples/github-action-workflow.yml)
|
|
15
|
+
- Codex for Open Source application brief: [docs/codex-for-oss-application.md](codex-for-oss-application.md)
|
|
16
|
+
- Rule reference: [docs/rules.md](rules.md)
|
|
17
|
+
|
|
18
|
+
## Maintainer Use Case
|
|
19
|
+
|
|
20
|
+
`oss-signal` audits repository maintenance readiness and returns a score with concrete next steps. It is aimed at work maintainers actually do: documenting contribution paths, setting support boundaries, keeping CI visible, collecting useful issue context, and making security reporting easier.
|
|
21
|
+
|
|
22
|
+
The CLI supports two practical modes:
|
|
23
|
+
|
|
24
|
+
- Local repository audit for maintainers working in a clone.
|
|
25
|
+
- Public GitHub repository audit for quick triage without cloning.
|
|
26
|
+
|
|
27
|
+
It also ships as a GitHub Action, so maintainers can gate repository hygiene in CI, show the result in the GitHub Actions step summary, and upload a Markdown report as a workflow artifact. This repository dogfoods the public Action tag through the Repository health workflow.
|
|
28
|
+
|
|
29
|
+
## Public Field Audits And PRs
|
|
30
|
+
|
|
31
|
+
The tool has been used to generate maintainer-readiness reports for public repositories and convert them into respectful cleanup issues:
|
|
32
|
+
|
|
33
|
+
| Repository | Report | Posted issue | Follow-up PR |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| `platformatic/massimo` | [report](outreach/platformatic-massimo-report.md) | https://github.com/platformatic/massimo/issues/159 | https://github.com/platformatic/massimo/pull/160 |
|
|
36
|
+
| `supermarkt/checkjebon` | [report](outreach/supermarkt-checkjebon-report.md) | https://github.com/supermarkt/checkjebon/issues/22 | https://github.com/supermarkt/checkjebon/pull/23 |
|
|
37
|
+
| `sammorrisdesign/interactive-feed` | [report](outreach/sammorrisdesign-interactive-feed-report.md) | https://github.com/sammorrisdesign/interactive-feed/issues/14 | https://github.com/sammorrisdesign/interactive-feed/pull/15 |
|
|
38
|
+
|
|
39
|
+
These issues and pull requests are evidence of the intended maintainer workflow: run a deterministic audit, explain the missing signals, and give maintainers a small set of actionable improvements. Each PR is intentionally limited to documentation or GitHub templates.
|
|
40
|
+
|
|
41
|
+
## Verification Commands
|
|
42
|
+
|
|
43
|
+
From this repository:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run check
|
|
47
|
+
npm run audit:github
|
|
48
|
+
node src/cli.js platformatic/massimo --format json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The current repository self-audit score is 100/100, the GitHub community profile health score is 100, and CI verifies the local GitHub Action wrapper.
|
|
52
|
+
|
|
53
|
+
Public CI evidence:
|
|
54
|
+
|
|
55
|
+
- CI workflow: https://github.com/SalmonPlays/oss-signal/actions/workflows/ci.yml
|
|
56
|
+
- Repository health workflow: https://github.com/SalmonPlays/oss-signal/actions/workflows/repository-health.yml
|
|
57
|
+
- CodeQL workflow: https://github.com/SalmonPlays/oss-signal/actions/workflows/codeql.yml
|
|
58
|
+
|
|
59
|
+
## Boundaries
|
|
60
|
+
|
|
61
|
+
`oss-signal` does not claim that a repository is high quality or widely adopted. It measures maintainability signals that are visible in repository files and GitHub community profile metadata.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="470" viewBox="0 0 920 470" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title id="title">oss-signal GitHub Actions step summary</title>
|
|
3
|
+
<desc id="desc">Example GitHub Actions step summary showing an oss-signal score of 100 out of 100.</desc>
|
|
4
|
+
<rect width="920" height="470" rx="18" fill="#ffffff"/>
|
|
5
|
+
<rect x="1" y="1" width="918" height="468" rx="18" fill="none" stroke="#d0d7de" stroke-width="2"/>
|
|
6
|
+
<rect x="0" y="0" width="920" height="58" rx="18" fill="#f6f8fa"/>
|
|
7
|
+
<text x="32" y="37" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="18" font-weight="700">GitHub Actions step summary</text>
|
|
8
|
+
<text x="32" y="106" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="30" font-weight="700">oss-signal</text>
|
|
9
|
+
<text x="32" y="154" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="20">Score: </text>
|
|
10
|
+
<text x="94" y="154" fill="#1a7f37" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="20" font-weight="700">100/100 (A)</text>
|
|
11
|
+
<rect x="32" y="190" width="520" height="152" rx="8" fill="#ffffff" stroke="#d0d7de"/>
|
|
12
|
+
<line x1="32" y1="238" x2="552" y2="238" stroke="#d0d7de"/>
|
|
13
|
+
<line x1="32" y1="286" x2="552" y2="286" stroke="#d0d7de"/>
|
|
14
|
+
<line x1="32" y1="342" x2="552" y2="342" stroke="#d0d7de"/>
|
|
15
|
+
<line x1="388" y1="190" x2="388" y2="342" stroke="#d0d7de"/>
|
|
16
|
+
<text x="54" y="222" fill="#57606a" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="16" font-weight="700">Result</text>
|
|
17
|
+
<text x="444" y="222" fill="#57606a" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="16" font-weight="700">Count</text>
|
|
18
|
+
<text x="54" y="270" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="16">Passed</text>
|
|
19
|
+
<text x="478" y="270" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="16">15</text>
|
|
20
|
+
<text x="54" y="318" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="16">Failed</text>
|
|
21
|
+
<text x="486" y="318" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="16">0</text>
|
|
22
|
+
<text x="32" y="390" fill="#24292f" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="21" font-weight="700">Recommended next steps</text>
|
|
23
|
+
<text x="32" y="428" fill="#57606a" font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="17">No missing maintainer-readiness checks found.</text>
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Codex for Open Source Application Brief
|
|
2
|
+
|
|
3
|
+
Snapshot: 2026-06-02T11:20:40Z
|
|
4
|
+
|
|
5
|
+
This document summarizes why `oss-signal` is a fit for OpenAI's Codex for Open Source program. The official program page says open-source maintainers can apply, with emphasis on core maintainers, widely used public projects, and projects that play an important ecosystem role: https://developers.openai.com/community/codex-for-oss
|
|
6
|
+
|
|
7
|
+
## Project
|
|
8
|
+
|
|
9
|
+
- Repository: https://github.com/SalmonPlays/oss-signal
|
|
10
|
+
- npm package: https://www.npmjs.com/package/oss-signal
|
|
11
|
+
- GitHub Action tag: https://github.com/SalmonPlays/oss-signal/tree/v0.3.0
|
|
12
|
+
- CI workflow: https://github.com/SalmonPlays/oss-signal/actions/workflows/ci.yml
|
|
13
|
+
- Repository health workflow: https://github.com/SalmonPlays/oss-signal/actions/workflows/repository-health.yml
|
|
14
|
+
- CodeQL workflow: https://github.com/SalmonPlays/oss-signal/actions/workflows/codeql.yml
|
|
15
|
+
- Maintainer evidence: [adoption-evidence.md](adoption-evidence.md)
|
|
16
|
+
|
|
17
|
+
## What `oss-signal` Does
|
|
18
|
+
|
|
19
|
+
`oss-signal` is a dependency-light CLI and GitHub Action for OSS maintainers. It audits maintainer-readiness signals that lower recurring maintainer load:
|
|
20
|
+
|
|
21
|
+
- README, license, contribution, support, security, code of conduct, and changelog files.
|
|
22
|
+
- CI, tests, issue templates, pull request templates, Dependabot, and CodeQL-style security workflow.
|
|
23
|
+
- Package metadata and lockfile hygiene.
|
|
24
|
+
|
|
25
|
+
The output is a deterministic score plus actionable next steps in Markdown or JSON. The GitHub Action also writes a workflow step summary so maintainers and reviewers can see the result without downloading an artifact.
|
|
26
|
+
|
|
27
|
+
## Why Codex Helps
|
|
28
|
+
|
|
29
|
+
This project is designed around repeatable maintainer workflows where Codex is useful:
|
|
30
|
+
|
|
31
|
+
- Run audits against public repositories without cloning.
|
|
32
|
+
- Convert findings into focused cleanup issues or pull requests.
|
|
33
|
+
- Keep repository hygiene visible in CI.
|
|
34
|
+
- Generate small contributor-facing files that maintainers can review quickly.
|
|
35
|
+
- Use Codex to turn audit findings into scoped documentation and workflow improvements.
|
|
36
|
+
|
|
37
|
+
## Public Evidence
|
|
38
|
+
|
|
39
|
+
The repository currently has:
|
|
40
|
+
|
|
41
|
+
- A published npm package.
|
|
42
|
+
- A reusable GitHub Action with `score`, `grade`, `failed`, and `report-path` outputs.
|
|
43
|
+
- A v0.3.0 GitHub Action tag with step summary support.
|
|
44
|
+
- A public dogfood workflow that runs `SalmonPlays/oss-signal@v0.3.0` against the repository.
|
|
45
|
+
- CI and CodeQL workflows passing on `main`.
|
|
46
|
+
- A local self-audit score of 100/100.
|
|
47
|
+
- Public reports, issues, and PRs created from real repository audits.
|
|
48
|
+
|
|
49
|
+
## Field Audits And Follow-Up PRs
|
|
50
|
+
|
|
51
|
+
| Repository | Report | Issue | PR | Status |
|
|
52
|
+
| --- | --- | --- | --- | --- |
|
|
53
|
+
| `platformatic/massimo` | [report](outreach/platformatic-massimo-report.md) | https://github.com/platformatic/massimo/issues/159 | https://github.com/platformatic/massimo/pull/160 | open, mergeable |
|
|
54
|
+
| `supermarkt/checkjebon` | [report](outreach/supermarkt-checkjebon-report.md) | https://github.com/supermarkt/checkjebon/issues/22 | https://github.com/supermarkt/checkjebon/pull/23 | open, mergeable |
|
|
55
|
+
| `sammorrisdesign/interactive-feed` | [report](outreach/sammorrisdesign-interactive-feed-report.md) | https://github.com/sammorrisdesign/interactive-feed/issues/14 | https://github.com/sammorrisdesign/interactive-feed/pull/15 | open, mergeable |
|
|
56
|
+
|
|
57
|
+
These PRs are intentionally small and maintainer-friendly. They add documentation or GitHub templates rather than changing product code.
|
|
58
|
+
|
|
59
|
+
## Application Positioning
|
|
60
|
+
|
|
61
|
+
Recommended application angle:
|
|
62
|
+
|
|
63
|
+
`oss-signal` is not yet a widely adopted project, but it is a public OSS maintainer tool built specifically for repeatable Codex-assisted maintenance. The project already has a working CLI, npm distribution, GitHub Action, passing CI/CodeQL, self-audit evidence, and three public field-audit PRs. Codex support would be used to continue auditing repositories, prepare focused maintainer PRs, improve Action automation, and document repeatable OSS maintenance workflows.
|
|
64
|
+
|
|
65
|
+
## Current Gaps
|
|
66
|
+
|
|
67
|
+
- External PRs are open but not yet merged.
|
|
68
|
+
- npm download metrics are still early because the package is newly published.
|
|
69
|
+
- The project needs more real maintainers using the Action in their own repositories.
|
|
70
|
+
|
|
71
|
+
## Next Evidence To Collect
|
|
72
|
+
|
|
73
|
+
- One or more merged external PRs.
|
|
74
|
+
- A GitHub Release for v0.3.0 with release notes.
|
|
75
|
+
- A public workflow run in another repository using `SalmonPlays/oss-signal@v0.3.0`.
|
|
76
|
+
- npm download data once the registry starts reporting weekly/monthly counts.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Repository health
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
oss-signal:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: SalmonPlays/oss-signal@v0.3.0
|
|
14
|
+
id: oss-signal
|
|
15
|
+
with:
|
|
16
|
+
fail-under: "80"
|
|
17
|
+
output: oss-signal-report.md
|
|
18
|
+
summary: "true"
|
|
19
|
+
- uses: actions/upload-artifact@v4
|
|
20
|
+
with:
|
|
21
|
+
name: oss-signal-report
|
|
22
|
+
path: oss-signal-report.md
|
|
23
|
+
- run: echo "oss-signal score ${{ steps.oss-signal.outputs.score }} (${{ steps.oss-signal.outputs.grade }})"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# OSS Signal Report
|
|
2
|
+
|
|
3
|
+
Repository: `https://github.com/SalmonPlays/oss-signal`
|
|
4
|
+
Source: GitHub (SalmonPlays/oss-signal@main)
|
|
5
|
+
Generated: 2026-06-02T08:09:34.957Z
|
|
6
|
+
|
|
7
|
+
Score: **100/100** (A)
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
- Passed: 15
|
|
12
|
+
- Failed: 0
|
|
13
|
+
- Total checks: 15
|
|
14
|
+
- Default branch: main
|
|
15
|
+
- GitHub stars: 0
|
|
16
|
+
- GitHub community health: 100
|
|
17
|
+
|
|
18
|
+
## Checks
|
|
19
|
+
|
|
20
|
+
| Status | Check | Why it matters |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| PASS | README | A clear README is the front door for users and contributors. |
|
|
23
|
+
| PASS | License | A license tells downstream users what they may legally do with the code. |
|
|
24
|
+
| PASS | Contributing guide | Maintainers get better issues and pull requests when expectations are documented. |
|
|
25
|
+
| PASS | Security policy | Responsible disclosure needs a private, documented path. |
|
|
26
|
+
| PASS | Code of conduct | Community norms reduce ambiguity during difficult interactions. |
|
|
27
|
+
| PASS | Changelog | Users need a durable place to understand release impact. |
|
|
28
|
+
| PASS | Support policy | Support boundaries help maintainers avoid turning every request into unpaid consulting. |
|
|
29
|
+
| PASS | Continuous integration | CI catches regressions before maintainers merge changes. |
|
|
30
|
+
| PASS | Tests | Tests make review safer and lower the cost of outside contributions. |
|
|
31
|
+
| PASS | Issue templates | Issue templates collect the facts maintainers need to reproduce and triage. |
|
|
32
|
+
| PASS | Pull request template | PR templates nudge contributors to include tests, docs, and review context. |
|
|
33
|
+
| PASS | Dependency update automation | Automated dependency updates reduce security and compatibility drift. |
|
|
34
|
+
| PASS | Static security analysis | Static analysis finds common vulnerability patterns before releases. |
|
|
35
|
+
| PASS | Node package metadata | Package metadata makes installation, testing, and release automation discoverable. |
|
|
36
|
+
| PASS | Dependency lockfile | Lockfiles make CI and contributor setup reproducible. |
|
|
37
|
+
|
|
38
|
+
## Recommended Next Steps
|
|
39
|
+
|
|
40
|
+
No missing checks. Keep the report current as the repository evolves.
|
|
41
|
+
|
package/docs/outreach/README.md
CHANGED
|
@@ -13,8 +13,8 @@ Important notes:
|
|
|
13
13
|
|
|
14
14
|
## Audited Repositories
|
|
15
15
|
|
|
16
|
-
| Repository | Local score | Draft | Posted issue |
|
|
17
|
-
| --- | ---: | --- | --- |
|
|
18
|
-
| `platformatic/massimo` | 58/100 | [issue draft](platformatic-massimo-issue-draft.md) | [#159](https://github.com/platformatic/massimo/issues/159) |
|
|
19
|
-
| `supermarkt/checkjebon` | 21/100 | [issue draft](supermarkt-checkjebon-issue-draft.md) | [#22](https://github.com/supermarkt/checkjebon/issues/22) |
|
|
20
|
-
| `sammorrisdesign/interactive-feed` | 31/100 | [issue draft](sammorrisdesign-interactive-feed-issue-draft.md) | [#14](https://github.com/sammorrisdesign/interactive-feed/issues/14) |
|
|
16
|
+
| Repository | Local score | Draft | Posted issue | Follow-up PR |
|
|
17
|
+
| --- | ---: | --- | --- | --- |
|
|
18
|
+
| `platformatic/massimo` | 58/100 | [issue draft](platformatic-massimo-issue-draft.md) | [#159](https://github.com/platformatic/massimo/issues/159) | [#160](https://github.com/platformatic/massimo/pull/160) |
|
|
19
|
+
| `supermarkt/checkjebon` | 21/100 | [issue draft](supermarkt-checkjebon-issue-draft.md) | [#22](https://github.com/supermarkt/checkjebon/issues/22) | [#23](https://github.com/supermarkt/checkjebon/pull/23) |
|
|
20
|
+
| `sammorrisdesign/interactive-feed` | 31/100 | [issue draft](sammorrisdesign-interactive-feed-issue-draft.md) | [#14](https://github.com/sammorrisdesign/interactive-feed/issues/14) | [#15](https://github.com/sammorrisdesign/interactive-feed/pull/15) |
|
package/docs/self-audit.md
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oss-signal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A dependency-light CLI that audits open-source repository maintenance readiness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"oss-signal": "
|
|
7
|
+
"oss-signal": "src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"src",
|
|
19
19
|
"docs",
|
|
20
|
+
"action.yml",
|
|
20
21
|
"README.md",
|
|
21
22
|
"LICENSE",
|
|
22
23
|
"CHANGELOG.md"
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"test": "node --test",
|
|
26
27
|
"lint": "node --check src/*.js test/*.test.js",
|
|
27
28
|
"audit:self": "node src/cli.js . --format markdown --output docs/self-audit.md",
|
|
29
|
+
"audit:github": "node src/cli.js SalmonPlays/oss-signal --format markdown --output docs/examples/github-url-report.md",
|
|
28
30
|
"audit:check": "node src/cli.js . --format json --fail-under 100 > /dev/null",
|
|
29
31
|
"check": "npm run lint && npm test && npm run audit:check",
|
|
30
32
|
"prepublishOnly": "npm run check"
|
package/src/action.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { auditTarget, renderMarkdown } from "./index.js";
|
|
6
|
+
|
|
7
|
+
const OUTPUT_DELIMITER = "oss_signal_output";
|
|
8
|
+
|
|
9
|
+
export async function runAction(env = process.env, stdout = process.stdout, stderr = process.stderr) {
|
|
10
|
+
const options = parseActionInputs(env);
|
|
11
|
+
const report = await auditTarget(options.path, {
|
|
12
|
+
maxFiles: options.maxFiles,
|
|
13
|
+
ref: options.ref
|
|
14
|
+
});
|
|
15
|
+
const body = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderMarkdown(report);
|
|
16
|
+
|
|
17
|
+
if (options.output) {
|
|
18
|
+
await fs.mkdir(path.dirname(path.resolve(options.output)), { recursive: true });
|
|
19
|
+
await fs.writeFile(options.output, body, "utf8");
|
|
20
|
+
} else {
|
|
21
|
+
stdout.write(body);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await writeGitHubOutput(env.GITHUB_OUTPUT, {
|
|
25
|
+
score: report.score,
|
|
26
|
+
grade: report.grade,
|
|
27
|
+
failed: report.summary.failed,
|
|
28
|
+
"report-path": options.output ?? ""
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (options.summary) {
|
|
32
|
+
await writeGitHubStepSummary(env.GITHUB_STEP_SUMMARY, report);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof options.failUnder === "number" && report.score < options.failUnder) {
|
|
36
|
+
stderr.write(`oss-signal: score ${report.score} is below fail-under ${options.failUnder}\n`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseActionInputs(env = process.env) {
|
|
44
|
+
const format = getInput(env, "format") || "markdown";
|
|
45
|
+
if (!["markdown", "json"].includes(format)) {
|
|
46
|
+
throw new Error("format must be either markdown or json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
path: getInput(env, "path") || ".",
|
|
51
|
+
format,
|
|
52
|
+
output: emptyToUndefined(getInput(env, "output")) ?? "oss-signal-report.md",
|
|
53
|
+
failUnder: parseOptionalNumber(getInput(env, "fail-under"), "fail-under"),
|
|
54
|
+
maxFiles: parseOptionalNumber(getInput(env, "max-files"), "max-files") ?? 20000,
|
|
55
|
+
ref: emptyToUndefined(getInput(env, "ref")),
|
|
56
|
+
summary: parseOptionalBoolean(getInput(env, "summary"), "summary") ?? true
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function writeGitHubOutput(outputFile, values) {
|
|
61
|
+
if (!outputFile) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body = Object.entries(values)
|
|
66
|
+
.map(([name, value]) => `${name}<<${OUTPUT_DELIMITER}\n${value}\n${OUTPUT_DELIMITER}`)
|
|
67
|
+
.join("\n");
|
|
68
|
+
await fs.appendFile(outputFile, `${body}\n`, "utf8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function writeGitHubStepSummary(summaryFile, report) {
|
|
72
|
+
if (!summaryFile) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const failedChecks = report.checks.filter((check) => !check.passed);
|
|
77
|
+
const nextSteps = failedChecks.length > 0
|
|
78
|
+
? failedChecks.map((check) => `- **${check.label}:** ${check.fix}`).join("\n")
|
|
79
|
+
: "- No missing maintainer-readiness checks found.";
|
|
80
|
+
|
|
81
|
+
const body = [
|
|
82
|
+
"# oss-signal",
|
|
83
|
+
"",
|
|
84
|
+
`Score: **${report.score}/100 (${report.grade})**`,
|
|
85
|
+
"",
|
|
86
|
+
"| Result | Count |",
|
|
87
|
+
"| --- | ---: |",
|
|
88
|
+
`| Passed | ${report.summary.passed} |`,
|
|
89
|
+
`| Failed | ${report.summary.failed} |`,
|
|
90
|
+
`| Total checks | ${report.summary.total} |`,
|
|
91
|
+
"",
|
|
92
|
+
"## Recommended next steps",
|
|
93
|
+
"",
|
|
94
|
+
nextSteps,
|
|
95
|
+
""
|
|
96
|
+
].join("\n");
|
|
97
|
+
|
|
98
|
+
await fs.appendFile(summaryFile, body, "utf8");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getInput(env, name) {
|
|
102
|
+
const directKey = `INPUT_${name.toUpperCase()}`;
|
|
103
|
+
const normalizedKey = `INPUT_${name.toUpperCase().replaceAll("-", "_")}`;
|
|
104
|
+
return env[directKey]?.trim() || env[normalizedKey]?.trim() || "";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseOptionalNumber(value, name) {
|
|
108
|
+
const normalized = emptyToUndefined(value);
|
|
109
|
+
if (normalized === undefined) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parsed = Number(normalized);
|
|
114
|
+
if (!Number.isFinite(parsed)) {
|
|
115
|
+
throw new Error(`${name} must be a number`);
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseOptionalBoolean(value, name) {
|
|
121
|
+
const normalized = emptyToUndefined(value)?.toLowerCase();
|
|
122
|
+
if (normalized === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw new Error(`${name} must be a boolean`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function emptyToUndefined(value) {
|
|
137
|
+
return value === undefined || value === "" ? undefined : value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function escapeWorkflowCommand(value) {
|
|
141
|
+
return String(value).replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isMainModule() {
|
|
145
|
+
return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (isMainModule()) {
|
|
149
|
+
runAction().catch((error) => {
|
|
150
|
+
process.stdout.write(`::error::${escapeWorkflowCommand(error.message)}\n`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
});
|
|
153
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
const VERSION = "0.1.0";
|
|
3
|
+
import { auditTarget, renderMarkdown, VERSION } from "./index.js";
|
|
6
4
|
|
|
7
5
|
async function main(argv) {
|
|
8
6
|
const options = parseArgs(argv);
|
|
@@ -16,7 +14,10 @@ async function main(argv) {
|
|
|
16
14
|
return;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
const report = await
|
|
17
|
+
const report = await auditTarget(options.path, {
|
|
18
|
+
maxFiles: options.maxFiles,
|
|
19
|
+
ref: options.ref
|
|
20
|
+
});
|
|
20
21
|
const body = options.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderMarkdown(report);
|
|
21
22
|
|
|
22
23
|
if (options.output) {
|
|
@@ -38,6 +39,7 @@ function parseArgs(argv) {
|
|
|
38
39
|
output: undefined,
|
|
39
40
|
failUnder: undefined,
|
|
40
41
|
maxFiles: 20000,
|
|
42
|
+
ref: undefined,
|
|
41
43
|
help: false,
|
|
42
44
|
version: false
|
|
43
45
|
};
|
|
@@ -66,6 +68,10 @@ function parseArgs(argv) {
|
|
|
66
68
|
options.maxFiles = parseNumber(requireValue(argv, ++index, "--max-files"), "--max-files");
|
|
67
69
|
} else if (arg.startsWith("--max-files=")) {
|
|
68
70
|
options.maxFiles = parseNumber(arg.slice("--max-files=".length), "--max-files");
|
|
71
|
+
} else if (arg === "--ref") {
|
|
72
|
+
options.ref = requireValue(argv, ++index, "--ref");
|
|
73
|
+
} else if (arg.startsWith("--ref=")) {
|
|
74
|
+
options.ref = arg.slice("--ref=".length);
|
|
69
75
|
} else if (arg.startsWith("-")) {
|
|
70
76
|
throw new Error(`Unknown option: ${arg}`);
|
|
71
77
|
} else {
|
|
@@ -105,13 +111,19 @@ function helpText() {
|
|
|
105
111
|
return `oss-signal audits open-source repository maintenance readiness.
|
|
106
112
|
|
|
107
113
|
Usage:
|
|
108
|
-
oss-signal [path] [--format markdown|json] [--output file] [--fail-under score]
|
|
114
|
+
oss-signal [path-or-github-url] [--format markdown|json] [--output file] [--fail-under score]
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
oss-signal .
|
|
118
|
+
oss-signal https://github.com/SalmonPlays/oss-signal
|
|
119
|
+
oss-signal platformatic/massimo --format json
|
|
109
120
|
|
|
110
121
|
Options:
|
|
111
122
|
--format Output format. Defaults to markdown.
|
|
112
123
|
--output, -o Write the report to a file instead of stdout.
|
|
113
124
|
--fail-under Exit with code 1 when the score is below this value.
|
|
114
125
|
--max-files Maximum files to inspect. Defaults to 20000.
|
|
126
|
+
--ref Git ref for GitHub URL audits. Defaults to the repository default branch.
|
|
115
127
|
--version, -v Show the CLI version.
|
|
116
128
|
--help, -h Show this help message.
|
|
117
129
|
`;
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
|
+
import https from "node:https";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
5
|
+
export const VERSION = "0.3.0";
|
|
6
|
+
|
|
4
7
|
const COMMUNITY_FILES = [
|
|
5
8
|
{
|
|
6
9
|
id: "readme",
|
|
@@ -146,12 +149,61 @@ const DEFAULT_IGNORE_DIRS = new Set([
|
|
|
146
149
|
export async function auditRepository(root, options = {}) {
|
|
147
150
|
const absoluteRoot = path.resolve(root ?? ".");
|
|
148
151
|
const tree = await listRepositoryFiles(absoluteRoot, options);
|
|
152
|
+
return createReportFromTree(tree, {
|
|
153
|
+
root: absoluteRoot,
|
|
154
|
+
source: {
|
|
155
|
+
type: "local",
|
|
156
|
+
location: absoluteRoot
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function auditTarget(target = ".", options = {}) {
|
|
162
|
+
const normalizedTarget = target ?? ".";
|
|
163
|
+
if (!isGitHubUrl(normalizedTarget) && await pathExists(normalizedTarget)) {
|
|
164
|
+
return auditRepository(normalizedTarget, options);
|
|
165
|
+
}
|
|
166
|
+
if (isGitHubTarget(normalizedTarget)) {
|
|
167
|
+
return auditGitHubRepository(normalizedTarget, options);
|
|
168
|
+
}
|
|
169
|
+
return auditRepository(normalizedTarget, options);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function auditGitHubRepository(target, options = {}) {
|
|
173
|
+
const parsed = parseGitHubTarget(target);
|
|
174
|
+
const fetchImpl = options.fetchImpl;
|
|
175
|
+
const headers = githubHeaders(options.githubToken ?? process.env.GITHUB_TOKEN);
|
|
176
|
+
const repo = await fetchJson(fetchImpl, `https://api.github.com/repos/${parsed.owner}/${parsed.repo}`, headers);
|
|
177
|
+
const ref = options.ref ?? repo.default_branch;
|
|
178
|
+
const tree = await listGitHubRepositoryFiles(fetchImpl, parsed.owner, parsed.repo, ref, headers, options);
|
|
179
|
+
const communityProfile = await fetchCommunityProfile(fetchImpl, parsed.owner, parsed.repo, headers);
|
|
180
|
+
|
|
181
|
+
return createReportFromTree(tree, {
|
|
182
|
+
root: repo.html_url,
|
|
183
|
+
source: {
|
|
184
|
+
type: "github",
|
|
185
|
+
location: repo.html_url,
|
|
186
|
+
owner: parsed.owner,
|
|
187
|
+
repo: parsed.repo,
|
|
188
|
+
ref,
|
|
189
|
+
defaultBranch: repo.default_branch,
|
|
190
|
+
stars: repo.stargazers_count,
|
|
191
|
+
forks: repo.forks_count,
|
|
192
|
+
openIssues: repo.open_issues_count,
|
|
193
|
+
healthPercentage: communityProfile?.health_percentage
|
|
194
|
+
},
|
|
195
|
+
communityProfile
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function createReportFromTree(tree, metadata) {
|
|
149
200
|
const fileSet = new Set(tree);
|
|
150
|
-
|
|
201
|
+
let checks = [
|
|
151
202
|
...COMMUNITY_FILES.map((rule) => checkPathRule(rule, fileSet)),
|
|
152
203
|
...AUTOMATION_FILES.map((rule) => checkMatcherRule(rule, tree)),
|
|
153
204
|
...PACKAGE_FILES.map((rule) => checkMatcherRule(rule, tree))
|
|
154
205
|
];
|
|
206
|
+
checks = applyCommunityProfileEvidence(checks, metadata.communityProfile);
|
|
155
207
|
|
|
156
208
|
const totalWeight = checks.reduce((sum, check) => sum + check.weight, 0);
|
|
157
209
|
const earnedWeight = checks.filter((check) => check.passed).reduce((sum, check) => sum + check.weight, 0);
|
|
@@ -159,8 +211,9 @@ export async function auditRepository(root, options = {}) {
|
|
|
159
211
|
|
|
160
212
|
return {
|
|
161
213
|
tool: "oss-signal",
|
|
162
|
-
version:
|
|
163
|
-
root:
|
|
214
|
+
version: VERSION,
|
|
215
|
+
root: metadata.root,
|
|
216
|
+
source: metadata.source,
|
|
164
217
|
generatedAt: new Date().toISOString(),
|
|
165
218
|
score,
|
|
166
219
|
grade: gradeForScore(score),
|
|
@@ -178,6 +231,7 @@ export function renderMarkdown(report) {
|
|
|
178
231
|
"# OSS Signal Report",
|
|
179
232
|
"",
|
|
180
233
|
`Repository: \`${report.root}\``,
|
|
234
|
+
`Source: ${sourceSummary(report.source)}`,
|
|
181
235
|
`Generated: ${report.generatedAt}`,
|
|
182
236
|
"",
|
|
183
237
|
`Score: **${report.score}/100** (${report.grade})`,
|
|
@@ -186,13 +240,24 @@ export function renderMarkdown(report) {
|
|
|
186
240
|
"",
|
|
187
241
|
`- Passed: ${report.summary.passed}`,
|
|
188
242
|
`- Failed: ${report.summary.failed}`,
|
|
189
|
-
`- Total checks: ${report.summary.total}
|
|
243
|
+
`- Total checks: ${report.summary.total}`
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
if (report.source?.type === "github") {
|
|
247
|
+
lines.push(
|
|
248
|
+
`- Default branch: ${report.source.defaultBranch}`,
|
|
249
|
+
`- GitHub stars: ${report.source.stars ?? 0}`,
|
|
250
|
+
`- GitHub community health: ${report.source.healthPercentage ?? "unknown"}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
lines.push(
|
|
190
255
|
"",
|
|
191
256
|
"## Checks",
|
|
192
257
|
"",
|
|
193
258
|
"| Status | Check | Why it matters |",
|
|
194
259
|
"| --- | --- | --- |"
|
|
195
|
-
|
|
260
|
+
);
|
|
196
261
|
|
|
197
262
|
for (const check of report.checks) {
|
|
198
263
|
lines.push(`| ${check.passed ? "PASS" : "FAIL"} | ${escapeTable(check.label)} | ${escapeTable(check.why)} |`);
|
|
@@ -253,6 +318,153 @@ export async function listRepositoryFiles(root, options = {}) {
|
|
|
253
318
|
return files;
|
|
254
319
|
}
|
|
255
320
|
|
|
321
|
+
export function parseGitHubTarget(target) {
|
|
322
|
+
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(target)) {
|
|
323
|
+
const [owner, repo] = target.split("/");
|
|
324
|
+
return { owner, repo: repo.replace(/\.git$/, "") };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let url;
|
|
328
|
+
try {
|
|
329
|
+
url = new URL(target);
|
|
330
|
+
} catch {
|
|
331
|
+
throw new Error(`Invalid GitHub target: ${target}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (url.hostname !== "github.com" && url.hostname !== "www.github.com") {
|
|
335
|
+
throw new Error(`Only github.com URLs are supported for remote audits: ${target}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const [owner, repo] = url.pathname.split("/").filter(Boolean);
|
|
339
|
+
if (!owner || !repo) {
|
|
340
|
+
throw new Error(`GitHub URL must include owner and repository: ${target}`);
|
|
341
|
+
}
|
|
342
|
+
return { owner, repo: repo.replace(/\.git$/, "") };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isGitHubTarget(target) {
|
|
346
|
+
return isGitHubUrl(target) || /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(target);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isGitHubUrl(target) {
|
|
350
|
+
return /^https?:\/\/(www\.)?github\.com\//.test(target);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function pathExists(target) {
|
|
354
|
+
try {
|
|
355
|
+
await fs.stat(path.resolve(target));
|
|
356
|
+
return true;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error.code === "ENOENT") {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function listGitHubRepositoryFiles(fetchImpl, owner, repo, ref, headers, options = {}) {
|
|
366
|
+
const maxFiles = options.maxFiles ?? 20000;
|
|
367
|
+
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(ref)}?recursive=1`;
|
|
368
|
+
const data = await fetchJson(fetchImpl, treeUrl, headers);
|
|
369
|
+
return (data.tree ?? [])
|
|
370
|
+
.filter((entry) => entry.type === "blob" && typeof entry.path === "string")
|
|
371
|
+
.map((entry) => entry.path)
|
|
372
|
+
.slice(0, maxFiles);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function fetchCommunityProfile(fetchImpl, owner, repo, headers) {
|
|
376
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/community/profile`;
|
|
377
|
+
try {
|
|
378
|
+
return await fetchJson(fetchImpl, url, headers);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (error.status === 404) {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async function fetchJson(fetchImpl, url, headers) {
|
|
388
|
+
if (!fetchImpl) {
|
|
389
|
+
return requestJson(url, headers);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const response = await fetchImpl(url, { headers });
|
|
393
|
+
if (!response.ok) {
|
|
394
|
+
const error = new Error(`GitHub API request failed with ${response.status}: ${url}`);
|
|
395
|
+
error.status = response.status;
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
return response.json();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function requestJson(url, headers) {
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
const request = https.get(url, { headers }, (response) => {
|
|
404
|
+
let body = "";
|
|
405
|
+
response.setEncoding("utf8");
|
|
406
|
+
response.on("data", (chunk) => {
|
|
407
|
+
body += chunk;
|
|
408
|
+
});
|
|
409
|
+
response.on("end", () => {
|
|
410
|
+
if ((response.statusCode ?? 500) < 200 || (response.statusCode ?? 500) >= 300) {
|
|
411
|
+
const error = new Error(`GitHub API request failed with ${response.statusCode}: ${url}`);
|
|
412
|
+
error.status = response.statusCode;
|
|
413
|
+
reject(error);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
resolve(JSON.parse(body));
|
|
418
|
+
} catch (error) {
|
|
419
|
+
reject(error);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
request.on("error", reject);
|
|
424
|
+
request.end();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function githubHeaders(token) {
|
|
429
|
+
const headers = {
|
|
430
|
+
Accept: "application/vnd.github+json",
|
|
431
|
+
"User-Agent": "oss-signal"
|
|
432
|
+
};
|
|
433
|
+
if (token) {
|
|
434
|
+
headers.Authorization = `Bearer ${token}`;
|
|
435
|
+
}
|
|
436
|
+
return headers;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function applyCommunityProfileEvidence(checks, profile) {
|
|
440
|
+
if (!profile?.files) {
|
|
441
|
+
return checks;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const profileEvidenceByCheck = {
|
|
445
|
+
readme: profile.files.readme,
|
|
446
|
+
license: profile.files.license,
|
|
447
|
+
contributing: profile.files.contributing,
|
|
448
|
+
security: profile.files.security_policy,
|
|
449
|
+
"code-of-conduct": profile.files.code_of_conduct_file ?? profile.files.code_of_conduct,
|
|
450
|
+
"issue-templates": profile.files.issue_template,
|
|
451
|
+
"pull-request-template": profile.files.pull_request_template
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return checks.map((check) => {
|
|
455
|
+
const evidence = profileEvidenceByCheck[check.id];
|
|
456
|
+
if (check.passed || !evidence) {
|
|
457
|
+
return check;
|
|
458
|
+
}
|
|
459
|
+
const evidenceUrl = evidence.html_url ?? evidence.url ?? "GitHub community profile";
|
|
460
|
+
return {
|
|
461
|
+
...check,
|
|
462
|
+
passed: true,
|
|
463
|
+
evidence: [`GitHub community profile: ${evidenceUrl}`]
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
256
468
|
function checkPathRule(rule, fileSet) {
|
|
257
469
|
const matchedPath = rule.paths.find((candidate) => fileSet.has(candidate));
|
|
258
470
|
return {
|
|
@@ -324,3 +536,13 @@ function gradeForScore(score) {
|
|
|
324
536
|
function escapeTable(value) {
|
|
325
537
|
return String(value).replaceAll("|", "\\|");
|
|
326
538
|
}
|
|
539
|
+
|
|
540
|
+
function sourceSummary(source) {
|
|
541
|
+
if (!source) {
|
|
542
|
+
return "local";
|
|
543
|
+
}
|
|
544
|
+
if (source.type === "github") {
|
|
545
|
+
return `GitHub (${source.owner}/${source.repo}@${source.ref})`;
|
|
546
|
+
}
|
|
547
|
+
return "local";
|
|
548
|
+
}
|