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 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
  [![CI](https://github.com/SalmonPlays/oss-signal/actions/workflows/ci.yml/badge.svg)](https://github.com/SalmonPlays/oss-signal/actions/workflows/ci.yml)
4
+ [![Repository health](https://github.com/SalmonPlays/oss-signal/actions/workflows/repository-health.yml/badge.svg)](https://github.com/SalmonPlays/oss-signal/actions/workflows/repository-health.yml)
4
5
  [![npm version](https://img.shields.io/npm/v/oss-signal.svg)](https://www.npmjs.com/package/oss-signal)
5
6
  [![npm downloads](https://img.shields.io/npm/dm/oss-signal.svg)](https://www.npmjs.com/package/oss-signal)
6
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](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
- ## CI
136
+ ## GitHub Action
124
137
 
125
- Add this to a GitHub Actions workflow:
138
+ Add `oss-signal` directly to a GitHub Actions workflow:
126
139
 
127
140
  ```yaml
128
- - run: npx oss-signal . --fail-under 80
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
+ ![oss-signal GitHub Actions summary](docs/assets/github-step-summary.svg)
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: actions/setup-node@v4
169
+ - uses: SalmonPlays/oss-signal@v0.3.0
170
+ id: oss-signal
147
171
  with:
148
- node-version: 22
149
- - run: npx oss-signal . --format markdown --output oss-signal-report.md --fail-under 80
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 inspects local files only; GitHub URL mode is on the roadmap.
159
- - It checks deterministic maintenance signals, not code quality.
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
+
@@ -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) |
@@ -1,7 +1,8 @@
1
1
  # OSS Signal Report
2
2
 
3
3
  Repository: `/Users/amon/Documents/Codex/2026-06-01/openai-s/outputs/oss-signal`
4
- Generated: 2026-06-02T01:40:56.294Z
4
+ Source: local
5
+ Generated: 2026-06-02T08:09:32.913Z
5
6
 
6
7
  Score: **100/100** (A)
7
8
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "oss-signal",
3
- "version": "0.1.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": "./src/cli.js"
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 { auditRepository, renderMarkdown } from "./index.js";
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 auditRepository(options.path, { maxFiles: options.maxFiles });
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
- const checks = [
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: "0.1.0",
163
- root: absoluteRoot,
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
+ }