-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoutdated.go
More file actions
186 lines (167 loc) · 4.83 KB
/
outdated.go
File metadata and controls
186 lines (167 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
package pin
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"github.com/git-pkgs/purl"
"github.com/git-pkgs/spdx"
"github.com/git-pkgs/vers"
)
// unmaintainedThresholdDays is informational, not a sync-blocker.
// 365 days is cold enough that actively-maintained libraries don't
// trip it while stable-on-purpose libraries (2-3 year cadence) do.
const unmaintainedThresholdDays = 365
type OutdatedOptions struct {
Dir string
Lock string
RegistryURL string
}
// OutdatedReport is one row of pin.Outdated. Severity reports the
// most-severe finding: ok / behind / deprecated / provenance-downgrade
// / yanked.
type OutdatedReport struct {
Name string
Locked string
Latest string
Behind bool
AgeDays int
LastPublish string
Deprecated string
Yanked bool
ProvenanceDowngrade bool // locked had provenance, latest doesn't
ProvenanceUpgrade bool // locked didn't, latest does
// LicenseLocked / LicenseLatest are SPDX-normalised. LicenseChange
// is true when both are non-empty and differ; bumping should
// re-evaluate license compatibility.
LicenseLocked string
LicenseLatest string
LicenseChange bool
// Unmaintained is informational; does not affect Severity or
// OutdatedExitCode.
Unmaintained bool
}
const (
SeverityOK = "ok"
SeverityBehind = "behind"
SeverityDeprecated = "deprecated"
SeverityYanked = "yanked"
SeverityProvenanceDowngrade = "provenance-downgrade"
)
func (r *OutdatedReport) Severity() string {
switch {
case r.Yanked:
return SeverityYanked
case r.ProvenanceDowngrade:
return SeverityProvenanceDowngrade
case r.Deprecated != "":
return SeverityDeprecated
case r.Behind:
return SeverityBehind
default:
return SeverityOK
}
}
func Outdated(ctx context.Context, opts OutdatedOptions) ([]OutdatedReport, error) {
return New(ClientOptions{RegistryURL: opts.RegistryURL}).Outdated(ctx, opts)
}
// Outdated reports each lockfile entry's status against the
// registry's current state.
func (c *Client) Outdated(ctx context.Context, opts OutdatedOptions) ([]OutdatedReport, error) {
if opts.Lock == "" {
opts.Lock = DefaultLock
}
l, err := readLock(filepath.Join(opts.Dir, opts.Lock))
if err != nil {
return nil, err
}
if l == nil {
return nil, fmt.Errorf("%w at %s", ErrNoLockfile, filepath.Join(opts.Dir, opts.Lock))
}
src := c.NPM
seen := map[string]bool{}
var reports []OutdatedReport
for _, a := range l.Assets {
if seen[a.Name] {
continue
}
seen[a.Name] = true
if !strings.HasPrefix(a.PURL, "pkg:npm/") {
reports = append(reports, OutdatedReport{
Name: a.Name,
Locked: a.Version,
})
continue
}
p, perr := purl.Parse(a.PURL)
if perr != nil {
return nil, fmt.Errorf("%s: parse purl %q: %w", a.Name, a.PURL, perr)
}
st, err := src.Status(ctx, p)
if err != nil {
return nil, fmt.Errorf("%s: %w", a.Name, err)
}
nLocked, nLatest := normaliseLicense(st.License), normaliseLicense(st.LatestLicense)
lastPublishAge := daysSince(st.LastPublish)
reports = append(reports, OutdatedReport{
Name: a.Name,
Locked: a.Version,
Latest: st.Latest,
Behind: st.Latest != "" && vers.Compare(a.Version, st.Latest) < 0,
AgeDays: daysSince(st.LatestTime),
LastPublish: st.LastPublish,
Deprecated: st.Deprecated,
Yanked: st.Yanked,
ProvenanceDowngrade: st.LockedHasProvenance && !st.LatestHasProvenance,
ProvenanceUpgrade: !st.LockedHasProvenance && st.LatestHasProvenance,
LicenseLocked: nLocked,
LicenseLatest: nLatest,
LicenseChange: nLocked != "" && nLatest != "" && nLocked != nLatest,
Unmaintained: lastPublishAge > unmaintainedThresholdDays,
})
}
sort.Slice(reports, func(i, j int) bool { return reports[i].Name < reports[j].Name })
return reports, nil
}
// normaliseLicense falls back to the raw string on parser error so
// two registry-supplied expressions still compare literally when
// SPDX normalisation can't help.
func normaliseLicense(s string) string {
if s == "" {
return ""
}
if out, err := spdx.NormalizeExpressionLax(s); err == nil {
return out
}
return s
}
func daysSince(iso string) int {
if iso == "" {
return -1
}
t, err := time.Parse(time.RFC3339, iso)
if err != nil {
return -1
}
return int(time.Since(t).Hours() / 24) //nolint:mnd
}
const (
ExitOutdated = 7
ExitYanked = 9
)
// OutdatedExitCode collapses reports into the CLI exit code:
// ExitYanked (9) > ExitOutdated (7) > 0.
func OutdatedExitCode(reports []OutdatedReport) int {
code := 0
for _, r := range reports {
switch {
case r.Yanked:
return ExitYanked
case r.Behind, r.Deprecated != "":
code = ExitOutdated
}
}
return code
}