Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,11 @@ its ecosystem-specific value. A declaration PURL omits the version because the
raw requirement may be a range or property expression.

Parsers that do not preserve source locations leave `Declarations` empty.
The `pom.xml` parser populates parents, dependencies, dependency management,
plugins, plugin dependencies, plugin management, build extensions, and their
profile-scoped forms.
Declarations are available for `package.json`, Python requirements files,
`pyproject.toml`, GitHub Actions workflows, and `pom.xml`. The Maven parser
includes parents, dependencies, dependency management, plugins, plugin
dependencies, plugin management, build extensions, and their profile-scoped
forms.

### ParseResult

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/bazelbuild/buildtools v0.0.0-20260716142318-04cf7de1434f
github.com/bmatcuk/doublestar/v4 v4.10.0
github.com/git-pkgs/pom v0.1.7
github.com/git-pkgs/purl v0.1.16
github.com/git-pkgs/purl v0.1.17
gopkg.in/yaml.v3 v3.0.1
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/git-pkgs/pom v0.1.7 h1:4yKdtw6eyShtjul6bcZdyz7yLQ+jdrYeYkKbskDGi4c=
github.com/git-pkgs/pom v0.1.7/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
github.com/git-pkgs/purl v0.1.16 h1:VAX6tv0hhdTENbkrGMoPZbOAl1Y8U1/ZnzoCsYuNBYM=
github.com/git-pkgs/purl v0.1.16/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k=
github.com/git-pkgs/purl v0.1.17 h1:oRSd8tqllTLl74Wa4WnuqU500hXd9OdUnImOEswQUVE=
github.com/git-pkgs/purl v0.1.17/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k=
github.com/git-pkgs/vers v0.3.1 h1:jy/ht2wIRJI5zQrccm6GTeYr+hGFwe2z8LV1HOr4Wco=
github.com/git-pkgs/vers v0.3.1/go.mod h1:biTbSQK1qdbrsxDEKnqe3Jzclxz8vW6uDcwKjfUGcOo=
github.com/package-url/packageurl-go v0.1.6 h1:YO3p6u1XmCUliivUg/qWphaY8vI6hxSnnPv7Bfg3m5M=
Expand Down
39 changes: 33 additions & 6 deletions internal/github_actions/github_actions.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package github_actions

import (
"github.com/git-pkgs/manifests/internal/core"
"net/url"
"path/filepath"
"strconv"
"strings"

"github.com/git-pkgs/manifests/internal/core"

"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -59,19 +62,29 @@ func (p *githubWorkflowParser) Parse(filename string, content []byte) (*core.Res
}

var deps []core.Dependency
var declarations []core.Declaration
seen := make(map[string]bool)
locations := make(map[string]int)

for _, job := range workflow.Jobs {
deps = collectStepActions(job.Steps, deps, seen)
for jobName, job := range workflow.Jobs {
deps = collectStepActions(jobName, job.Steps, deps, &declarations, seen, locations)
deps = collectContainerImage(job.Container, deps, seen)
deps = collectServiceImages(job.Services, deps, seen)
}

return &core.Result{Dependencies: deps}, nil
return &core.Result{Dependencies: deps, Declarations: declarations}, nil
}

// collectStepActions extracts action dependencies from job steps.
func collectStepActions(steps []githubStep, deps []core.Dependency, seen map[string]bool) []core.Dependency {
// collectStepActions extracts action dependencies and source declarations
// from job steps.
func collectStepActions(
jobName string,
steps []githubStep,
deps []core.Dependency,
declarations *[]core.Declaration,
seen map[string]bool,
locations map[string]int,
) []core.Dependency {
for _, step := range steps {
if step.Uses == "" {
continue
Expand All @@ -81,6 +94,20 @@ func collectStepActions(steps []githubStep, deps []core.Dependency, seen map[str
if name == "" {
continue
}
if !strings.HasPrefix(name, "docker://") {
base := "jobs/" + url.PathEscape(jobName) + "/steps/" + url.PathEscape(name)
locations[base]++
location := base
if locations[base] > 1 {
location += "/" + strconv.Itoa(locations[base])
}
*declarations = append(*declarations, core.Declaration{
Name: name,
Version: version,
Scope: core.Runtime,
Location: location,
})
}

key := name + "@" + version
if seen[key] {
Expand Down
44 changes: 44 additions & 0 deletions internal/github_actions/github_actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,47 @@ func TestGitHubWorkflow(t *testing.T) {
t.Error("expected docker://node to have version")
}
}

func TestGitHubWorkflowDeclarations(t *testing.T) {
content := []byte(`jobs:
build:
steps:
- uses: actions/checkout@v4
- uses: actions/cache/restore@v3
- uses: actions/checkout@v3
- uses: docker://alpine:3.20
- uses: ./local-action
test:
steps:
- uses: actions/checkout@main
`)

result, err := (&githubWorkflowParser{}).Parse("workflow.yml", content)
if err != nil {
t.Fatalf("Parse: %v", err)
}

want := map[string]struct {
name string
version string
}{
"jobs/build/steps/actions%2Fcheckout": {"actions/checkout", "v4"},
"jobs/build/steps/actions%2Fcache%2Frestore": {"actions/cache/restore", "v3"},
"jobs/build/steps/actions%2Fcheckout/2": {"actions/checkout", "v3"},
"jobs/test/steps/actions%2Fcheckout": {"actions/checkout", "main"},
}
if len(result.Declarations) != len(want) {
t.Fatalf("Declarations has %d entries, want %d: %+v", len(result.Declarations), len(want), result.Declarations)
}
for _, declaration := range result.Declarations {
expected, ok := want[declaration.Location]
if !ok {
t.Errorf("unexpected declaration at %q: %+v", declaration.Location, declaration)
continue
}
if declaration.Name != expected.name || declaration.Version != expected.version || declaration.Scope != core.Runtime {
t.Errorf("declaration at %q = %+v, want name %q and version %q",
declaration.Location, declaration, expected.name, expected.version)
}
}
}
90 changes: 31 additions & 59 deletions internal/npm/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package npm
import (
"bytes"
"encoding/json"
"net/url"
"strings"

"github.com/git-pkgs/manifests/internal/core"
Expand Down Expand Up @@ -44,42 +45,31 @@ func (p *npmPackageJSONParser) Parse(filename string, content []byte) (*core.Res
}

var deps []core.Dependency
var declarations []core.Declaration
collectNpmDeclarations(&deps, &declarations, "dependencies", pkg.Dependencies, core.Runtime)
collectNpmDeclarations(&deps, &declarations, "devDependencies", pkg.DevDependencies, core.Development)
collectNpmDeclarations(&deps, &declarations, "optionalDependencies", pkg.OptionalDependencies, core.Optional)
collectNpmDeclarations(&deps, &declarations, "peerDependencies", pkg.PeerDependencies, core.Runtime)

for name, value := range pkg.Dependencies {
if isNpmComment(name) {
continue
}
version, ok := value.(string)
if !ok {
continue
}
realName, realVersion := parseNpmAlias(name, version)
deps = append(deps, core.Dependency{
Name: realName,
Version: realVersion,
Scope: core.Runtime,
Direct: true,
})
}

for name, value := range pkg.DevDependencies {
if isNpmComment(name) {
continue
}
version, ok := value.(string)
if !ok {
continue
}
realName, realVersion := parseNpmAlias(name, version)
deps = append(deps, core.Dependency{
Name: realName,
Version: realVersion,
Scope: core.Development,
Direct: true,
})
}
return &core.Result{
Name: pkg.Name,
Version: pkg.Version,
Licenses: npmLicenses(pkg.License, pkg.Licenses),
Dependencies: deps,
Declarations: declarations,
}, nil
}

for name, value := range pkg.OptionalDependencies {
// collectNpmDeclarations appends dependencies and source declarations from
// one package.json dependency block.
func collectNpmDeclarations(
dependencies *[]core.Dependency,
declarations *[]core.Declaration,
location string,
values map[string]any,
scope core.Scope,
) {
for name, value := range values {
if isNpmComment(name) {
continue
}
Expand All @@ -88,37 +78,19 @@ func (p *npmPackageJSONParser) Parse(filename string, content []byte) (*core.Res
continue
}
realName, realVersion := parseNpmAlias(name, version)
deps = append(deps, core.Dependency{
*dependencies = append(*dependencies, core.Dependency{
Name: realName,
Version: realVersion,
Scope: core.Optional,
Scope: scope,
Direct: true,
})
}

for name, value := range pkg.PeerDependencies {
if isNpmComment(name) {
continue
}
version, ok := value.(string)
if !ok {
continue
}
realName, realVersion := parseNpmAlias(name, version)
deps = append(deps, core.Dependency{
Name: realName,
Version: realVersion,
Scope: core.Runtime, // peer dependencies are runtime requirements
Direct: true,
*declarations = append(*declarations, core.Declaration{
Name: realName,
Version: realVersion,
Scope: scope,
Location: location + "/" + url.PathEscape(name),
})
}

return &core.Result{
Name: pkg.Name,
Version: pkg.Version,
Licenses: npmLicenses(pkg.License, pkg.Licenses),
Dependencies: deps,
}, nil
}

func npmLicenses(license any, legacy []npmLicense) []string {
Expand Down
46 changes: 46 additions & 0 deletions internal/npm/npm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,52 @@ func TestNpmPackageJSON(t *testing.T) {
}
}

func TestNpmPackageJSONDeclarations(t *testing.T) {
content := []byte(`{
"dependencies": {
"plain": "1.2.3",
"alias": "npm:@scope/actual-package@^2.0.0",
"@scope/package": "~3.0.0",
"// note": "ignored"
},
"devDependencies": {"plain": "2.0.0"},
"optionalDependencies": {"optional": "4.0.0"},
"peerDependencies": {"peer": ">=5.0.0"}
}`)

result, err := (&npmPackageJSONParser{}).Parse("package.json", content)
if err != nil {
t.Fatalf("Parse: %v", err)
}

want := map[string]struct {
name string
version string
scope core.Scope
}{
"dependencies/plain": {"plain", "1.2.3", core.Runtime},
"dependencies/alias": {"@scope/actual-package", "^2.0.0", core.Runtime},
"dependencies/@scope%2Fpackage": {"@scope/package", "~3.0.0", core.Runtime},
"devDependencies/plain": {"plain", "2.0.0", core.Development},
"optionalDependencies/optional": {"optional", "4.0.0", core.Optional},
"peerDependencies/peer": {"peer", ">=5.0.0", core.Runtime},
}
if len(result.Declarations) != len(want) {
t.Fatalf("Declarations has %d entries, want %d: %+v", len(result.Declarations), len(want), result.Declarations)
}
for _, declaration := range result.Declarations {
expected, ok := want[declaration.Location]
if !ok {
t.Errorf("unexpected declaration at %q: %+v", declaration.Location, declaration)
continue
}
if declaration.Name != expected.name || declaration.Version != expected.version || declaration.Scope != expected.scope {
t.Errorf("declaration at %q = %+v, want name %q, version %q, scope %q",
declaration.Location, declaration, expected.name, expected.version, expected.scope)
}
}
}

func TestNpmPackageLock(t *testing.T) {
content, err := os.ReadFile("../../testdata/npm/package-lock.json")
if err != nil {
Expand Down
Loading