Skip to content

Conversation

gabrielsoltz
Copy link

@gabrielsoltz gabrielsoltz commented Sep 16, 2025

What kind of change does this PR introduce?

This PR introduces support for scanning multiple repositories in a single invocation, while preserving existing behavior by default. The only user-visible change for single-repo usage is a small improvement to the “Starting/Finished” banners (they now include the repo label).

New flags

  • --repos: comma-separated list of repositories to scan (e.g., --repos=owner1/repo1,github.com/owner2/repo2).
  • --org: GitHub organization handle (e.g., --org=github.com/ossf or ossf).
  • --combined: after scanning N repos, print a single combined table with all checks together with two new extra columns, REPO and AGGREGATED SCORE.

Precedence when multiple inputs are provided: --repos ➜ --org ➜ --local ➜ --repo (or repo resolved from package managers).

UX / output changes

Before (single repo):
Starting [License]
Starting [Code-Review]
...
Finished [License]
Finished [Code-Review]
...
Now (single or multi-repo):
Starting (owner/repo) [License]
Starting (owner/repo) [Code-Review]
...
Finished (owner/repo) [License]
Finished (owner/repo) [Code-Review]
...

This makes it obvious which repo each check belongs to—especially important when scanning many repos.
No other output changes occur unless --combined is used.

When --combined is set, a final COMBINED RESULTS section is emitted after all per-repo results, e.g.:

COMBINED RESULTS
----------------
| REPO                                             | AGGREGATED SCORE | SCORE   | NAME                 | REASON                      | DOCUMENTATION/REMEDIATION                                |
|--------------------------------------------------|------------------|---------|----------------------|-----------------------------|----------------------------------------------------------|
| github.com/ossf-tests/scorecard-check-...        | 3.3 / 10         | 10 / 10 | Binary-Artifacts     | no binaries found in repo   | https://github.com/ossf/scorecard/blob/main/docs/...     |
...

Implementation details

  • Introduce buildRepoURLs(ctx, *options.Options) ([]string, error):
  • Normalizes the input universe and always returns a slice of repo URIs.
  • Honors precedence --repos ➜ --org ➜ --local ➜ single --repo/pkg-manager.
  • Refactor rootCmd to iterate over the returned list and run the same scan pipeline for each repo.
  • ListOrgRepos for listing repositories from the organization, reusing the githubrepo code and logic.

Examples

Scan a list
scorecard --repos=ossf/scorecard,ossf-tests/scorecard-check-branch-protection-e2e
Scan all non-archived repos in an org
scorecard --org=github.com/ossf
Aggregate across many repos
scorecard --repos=ossf/scorecard,ossf-tests/scorecard-check-branch-protection-e2e --combined
  • PR title follows the guidelines defined in our pull request documentation

  • Tests for the changes have been added (for bug fixes/features)

Which issue(s) this PR fixes

Fixes #4792

Special notes for your reviewer

Does this PR introduce a user-facing change?

For user-facing changes, please add a concise, human-readable release note to
the release-note

(In particular, describe what changes users might need to make in their
application as a result of this pull request.)


@gabrielsoltz gabrielsoltz changed the title ✨ Implement --org and --repos for scanning organizations or multiple repositories ✨ Add multi-repo scanning: --repos, --org, and optional --combined output Sep 16, 2025
@gabrielsoltz gabrielsoltz marked this pull request as ready for review September 16, 2025 21:20
@gabrielsoltz gabrielsoltz requested a review from a team as a code owner September 16, 2025 21:20
@gabrielsoltz gabrielsoltz requested review from spencerschrock and raghavkaul and removed request for a team September 16, 2025 21:20
Copy link

codecov bot commented Sep 17, 2025

Codecov Report

❌ Patch coverage is 31.64179% with 229 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.48%. Comparing base (353ed60) to head (201ac1d).
⚠️ Report is 238 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4793      +/-   ##
==========================================
+ Coverage   66.80%   67.48%   +0.68%     
==========================================
  Files         230      250      +20     
  Lines       16602    19367    +2765     
==========================================
+ Hits        11091    13070    +1979     
- Misses       4808     5422     +614     
- Partials      703      875     +172     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@spencerschrock spencerschrock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a partial review of a few things before I ran out of time, I need to dive more into how you build the repo list, scan repos, and present the results. Feel free to address that feedback now or wait until I have more time for full review

Comment on lines 72 to 75
// TransportFactory is used to create an http.RoundTripper. It defaults to
// calling NewTransport but can be overridden in tests to provide a custom
// RoundTripper that redirects requests to a test server.
var TransportFactory = NewTransport
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we need this. Most mocking in this repo is done with dependency injection, so whatever uses the round tripper should have one injected in a test.

E.g.

type stubTripper struct {
responsePath string
}
func (s stubTripper) RoundTrip(_ *http.Request) (*http.Response, error) {
f, err := os.Open(s.responsePath)
if err != nil {
return nil, err
}
return &http.Response{
Status: "200 OK",
StatusCode: http.StatusOK,
Body: f,
}, nil
}

httpClient := &http.Client{
Transport: stubTripper{
responsePath: tt.responsePath,
},
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then when you make a client you can still call this by default, but if the test gives you a round tripper you use it instead.

func NewRepoClient(ctx context.Context, opts ...Option) (clients.RepoClient, error) {
var config repoClientConfig
for _, option := range opts {
if err := option(&config); err != nil {
return nil, err
}
}
if config.rt == nil {
logger := log.NewLogger(log.DefaultLevel)
config.rt = roundtripper.NewTransport(ctx, logger)
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was also changed; still not sure if my implementation is the expected one.

go.mod Outdated
Comment on lines 43 to 44
github.com/google/go-github/v53 v53.2.0
github.com/google/go-github/v60 v60.0.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why introduce a second version when the function you need is available in the version we already use?
https://pkg.go.dev/github.com/google/go-github/v53/github#RepositoriesService.ListByOrg

Copy link
Author

@gabrielsoltz gabrielsoltz Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Changed. ✅

Comment on lines 28 to 29
// ListOrgRepos lists all non-archived repositories for a GitHub organization.
func ListOrgRepos(ctx context.Context, org string) ([]string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we move org.go and org_test.go to cmd/internal/org/org.go and cmd/internal/org/org_test.go.

Any code we can keep out of the public is good.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure I understand this. But if you think that's the correct place for it, I'll move it... My logic was to keep all related github code together....

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved ✅

Comment on lines 72 to 75
// parseOrgName extracts the organization name from a GitHub URL or returns the input if already an org name.
func parseOrgName(input string) string {
// Remove "github.com/" prefix if present
const prefix = "github.com/"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other Scorecard arguments accept URLs with schmes. So I would want you to test this works with something like:

http://github.com/gabrielsoltz
https://github.com/gabrielsoltz

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improved and added test cases ✅

// See the License for the specific language governing permissions and
// limitations under the License.

// Package cmd implements Scorecard command-line.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please dont delete comments unrelated to your change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restored ✅

cmd/root.go Outdated
Comment on lines 129 to 130
// Shared setup (unchanged)
pol, err := policy.ParseFromFile(o.PolicyFile)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unchanged from what?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the previous behavior before my change, I'll remove that comment now because I see it's confusing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted that part of the comment ✅

Comment on lines 134 to 148

// Read docs.
checkDocs, err := docs.Read()
if err != nil {
return fmt.Errorf("cannot read yaml file: %w", err)
}

var requiredRequestTypes []checker.RequestType
// if local option not set add file based
if o.Local != "" {
requiredRequestTypes = append(requiredRequestTypes, checker.FileBased)
}
// if commit option set to anything other than HEAD add commit based
if !strings.EqualFold(o.Commit, clients.HeadSHA) {
requiredRequestTypes = append(requiredRequestTypes, checker.CommitBased)
}
// this call to policy is different from the one in scorecard.Run
// this one is concerned with a policy file, while the scorecard.Run call is
// more concerned with the supported request types

enabledChecks, err := policy.GetEnabled(pol, o.Checks(), requiredRequestTypes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please dont delete comments unrelated to your change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for this, I restored them ✅

@spencerschrock
Copy link
Member

Also, this repo requires DCO.

> Additionally the Linux Foundation (LF) requires all contributions include per-commit sign-offs.
> Ensure you use the `-s` or `--signoff` flag for every commit.
>
> For more details, see the [LF DCO wiki](https://wiki.linuxfoundation.org/dco)
> or [this Pi-hole signoff guide](https://docs.pi-hole.net/guides/github/how-to-signoff/).

For instructions on how to fix it:
https://github.com/ossf/scorecard/pull/4793/checks?check_run_id=50569515731

Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
@gabrielsoltz gabrielsoltz force-pushed the feat-support-org-and-repos branch from 3ab90d2 to 75c4984 Compare September 19, 2025 08:57
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
@justaugustus justaugustus mentioned this pull request Sep 20, 2025
2 tasks
@justaugustus justaugustus moved this to In Progress in OpenSSF Scorecard Sep 20, 2025
@justaugustus justaugustus moved this from In Progress to Review in progress in OpenSSF Scorecard Sep 20, 2025
Signed-off-by: Gabriel Alejandro Soltz <[email protected]>
@spencerschrock
Copy link
Member

Just wanted to update you that this is still on my radar! We are trying to cut a 5.3.0 release this week, so my attention has been on that. But once that's cut this week, I will make time to finish. my review, and happy to cut a 5.4.0 or 5.3.1 shortly after

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Review in progress
Development

Successfully merging this pull request may close these issues.

Feature: Support Scanning Github Organizations and Multiple Repositories
2 participants