carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Version-candidate selection: which of an app's shipped versions are worth
2// reading manifests for, decided from the listing alone. Shared by the
3// console's app page and `carlos releases` direct mode, so the two surfaces
4// agree about what "the latest releases" are.
5package release
6
7import (
8 "context"
9 "sort"
10 "strings"
11 "time"
12)
13
14// VersionCandidates lists the distinct versions under an app's releases
15// prefix, in the order manifests should be read.
16//
17// With a ModLister (both real stores), the answer is RANKED: the newest max
18// versions by each manifest.json object's own last-modified stamp, newest
19// first. The stamp is written at ship time, so it is a best-effort stand-in
20// for the manifest's built_at — best-effort meaning: S3 stamps are
21// second-granularity (same-second ships tie, broken deterministically by
22// version name descending, the final-sort tie-break every caller already
23// uses), and `accounts migrate` re-stamps the whole pre-migration backlog at
24// migration time. A caller then reads only the manifests it will render —
25// fetch-then-sort read every manifest ever shipped, and a CI-shipped app
26// (tito's titogo, 318 versions) took ~8s of sequential bucket GETs per page
27// render that way.
28//
29// A version with no manifest.json ranks by its newest object instead — an
30// in-flight ship (artifacts land before the manifest, Ship's write order)
31// briefly occupies a slot as the error row its fetch produces, and a ship
32// that died mid-write stays visible as one. Deliberate: the old sort-last
33// behavior silently hid an aborted ship forever once an app had max readable
34// versions, and an operator whose ship died wants the wreckage on the page,
35// not vanished.
36//
37// With a plain Store (test fakes; no production caller — cmd/console and the
38// CLI both hand over a bare *S3Store or *DirStore), the answer is every
39// version, unranked and uncapped, in listing order: the caller keeps its
40// fetch-everything path, byte-identical to the pre-ModLister behavior.
41func VersionCandidates(ctx context.Context, s Store, acct, app string, max int) (versions []string, ranked bool, err error) {
42 prefix := ReleasesPrefix(acct, app)
43 var cands []versionStamp
44 ml, ranked := s.(ModLister)
45 if ranked {
46 cands, err = rankedVersionStamps(ctx, ml, prefix)
47 if err != nil {
48 return nil, false, err
49 }
50 if len(cands) > max {
51 cands = cands[:max]
52 }
53 } else {
54 keys, err := s.List(ctx, prefix)
55 if err != nil {
56 return nil, false, err
57 }
58 index := map[string]int{}
59 for _, k := range keys {
60 cands = noteKey(cands, index, prefix, k, time.Time{})
61 }
62 }
63 versions = make([]string, len(cands))
64 for i, c := range cands {
65 versions[i] = c.version
66 }
67 return versions, ranked, nil
68}
69
70// versionStamp is one distinct version under a releases prefix with the
71// stamp candidate ranking uses: manifest.json's own mod time once seen,
72// else the newest object's.
73type versionStamp struct {
74 version string
75 mod time.Time
76 manifest bool
77}
78
79// noteKey folds one listed key into the per-version aggregation — the
80// manifest-stamp-wins rule VersionCandidates has always applied, shared so
81// prune's selector and the render path can never disagree about a
82// version's age.
83func noteKey(cands []versionStamp, index map[string]int, prefix, key string, mod time.Time) []versionStamp {
84 v, rest, ok := strings.Cut(strings.TrimPrefix(key, prefix), "/")
85 if !ok || v == "" {
86 return cands
87 }
88 isManifest := rest == "manifest.json"
89 i, seen := index[v]
90 if !seen {
91 index[v] = len(cands)
92 return append(cands, versionStamp{version: v, mod: mod, manifest: isManifest})
93 }
94 switch {
95 case isManifest:
96 cands[i].mod, cands[i].manifest = mod, true
97 case !cands[i].manifest && mod.After(cands[i].mod):
98 cands[i].mod = mod
99 }
100 return cands
101}
102
103// rankedVersionStamps lists EVERY distinct version under prefix from a
104// mod-aware store, newest first by the manifest-stamp-wins rule, ties
105// broken by version name descending. Uncapped: VersionCandidates caps for
106// rendering; PruneEligible needs the whole history.
107func rankedVersionStamps(ctx context.Context, ml ModLister, prefix string) ([]versionStamp, error) {
108 keys, err := ml.ListMod(ctx, prefix)
109 if err != nil {
110 return nil, err
111 }
112 var cands []versionStamp
113 index := map[string]int{}
114 for _, km := range keys {
115 cands = noteKey(cands, index, prefix, km.Key, km.Mod)
116 }
117 sort.SliceStable(cands, func(i, j int) bool {
118 if !cands[i].mod.Equal(cands[j].mod) {
119 return cands[i].mod.After(cands[j].mod)
120 }
121 return cands[i].version > cands[j].version
122 })
123 return cands, nil
124}
125