Skip to content

Commit 1602a8d

Browse files
CLD-522: Migrate Changeset Registry (#335)
## CLD-522: Migrate Changeset Registry - Move registry to framework - Rename instances of `migration` to `changeset` - Update testing requirements based on new package structure
1 parent 365c01c commit 1602a8d

File tree

3 files changed

+635
-0
lines changed

3 files changed

+635
-0
lines changed

.changeset/free-fans-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": minor
3+
---
4+
5+
Add changeset registry

engine/cld/changeset/registry.go

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package changeset
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"slices"
7+
"strconv"
8+
"strings"
9+
"sync"
10+
11+
cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment"
12+
13+
"github.com/smartcontractkit/chainlink-deployments-framework/operations"
14+
)
15+
16+
// RegistryProvider defines an interface for initializing and managing the changeset registry
17+
// for a domain environment. It provides methods to initialize the registry, archive changesets,
18+
// and retrieve the initialized ChangesetsRegistry.
19+
type RegistryProvider interface {
20+
// Init initializes the changeset registry by adding changesets specific to the domain
21+
// environment using the `Add` method on the ChangesetsRegistry.
22+
Init() error
23+
24+
// Archive archives a changeset in the registry. This is intended for changesets that have
25+
// already been applied and are retained only for historical purposes.
26+
Archive()
27+
28+
// Registry retrieves the initialized ChangesetsRegistry.
29+
Registry() *ChangesetsRegistry
30+
}
31+
32+
var _ RegistryProvider = (*BaseRegistryProvider)(nil)
33+
34+
// BaseRegistryProvider is a base implementation of a RegistryProvider that provides a
35+
// ChangesetsRegistry.
36+
type BaseRegistryProvider struct {
37+
registry *ChangesetsRegistry
38+
}
39+
40+
// NewBaseRegistryProvider is an implementation of a RegistryProvider that provides a struct that
41+
// can be embedded in domain-specific registry providers.
42+
func NewBaseRegistryProvider() *BaseRegistryProvider {
43+
return &BaseRegistryProvider{
44+
registry: NewChangesetsRegistry(),
45+
}
46+
}
47+
48+
// Registry returns the ChangesetsRegistry.
49+
func (p *BaseRegistryProvider) Registry() *ChangesetsRegistry {
50+
return p.registry
51+
}
52+
53+
// Init is an empty implementation of adding changesets to the registry.
54+
//
55+
// This should be overridden by the domain-specific registry provider.
56+
func (p *BaseRegistryProvider) Init() error {
57+
return nil
58+
}
59+
60+
// Archive is an empty implementation of archiving changesets in the registry.
61+
//
62+
// This should be overridden by the domain-specific registry provider.
63+
func (p *BaseRegistryProvider) Archive() {}
64+
65+
type registryEntry struct {
66+
// changeset is the changeset that is registered.
67+
changeset ChangeSet
68+
69+
// gitSHA is the git SHA of the buried changeset. This only applies to changesets that are
70+
// buried.
71+
gitSHA *string
72+
73+
// options contains the configuration options for this changeset
74+
options ChangesetConfig
75+
}
76+
77+
// newRegistryEntry creates a new registry entry for a changeset.
78+
func newRegistryEntry(c ChangeSet, opts ChangesetConfig) registryEntry {
79+
return registryEntry{changeset: c, options: opts}
80+
}
81+
82+
// newArchivedRegistryEntry creates a new registry entry for an archived changeset.
83+
func newArchivedRegistryEntry(gitSHA string) registryEntry {
84+
return registryEntry{gitSHA: &gitSHA}
85+
}
86+
87+
// IsArchived returns true if the changeset is archived.
88+
func (e registryEntry) IsArchived() bool {
89+
return e.gitSHA != nil
90+
}
91+
92+
// ChangesetsRegistry is a registry of changesets that can be applied to a domain environment.
93+
type ChangesetsRegistry struct {
94+
mu sync.Mutex
95+
96+
// entries is a map of changeset keys to registry entries.
97+
entries map[string]registryEntry
98+
99+
// keyHistory is a list of all changeset keys in the order they were added to the registry.
100+
keyHistory []string
101+
102+
// validate enables or disables changeset key validation.
103+
validate bool
104+
}
105+
106+
// NewChangesetsRegistry creates a new ChangesetsRegistry.
107+
func NewChangesetsRegistry() *ChangesetsRegistry {
108+
return &ChangesetsRegistry{
109+
entries: make(map[string]registryEntry),
110+
keyHistory: []string{},
111+
validate: true,
112+
}
113+
}
114+
115+
// SetValidate sets the validate flag for the registry. If set to true, changeset keys will be validated.
116+
func (r *ChangesetsRegistry) SetValidate(validate bool) {
117+
r.mu.Lock()
118+
defer r.mu.Unlock()
119+
120+
r.validate = validate
121+
}
122+
123+
// Apply applies a changeset.
124+
func (r *ChangesetsRegistry) Apply(
125+
key string, e cldf.Environment,
126+
) (cldf.ChangesetOutput, error) {
127+
r.mu.Lock()
128+
defer r.mu.Unlock()
129+
130+
entry, ok := r.entries[key]
131+
if !ok {
132+
return cldf.ChangesetOutput{}, fmt.Errorf("changeset '%s' not found", key)
133+
}
134+
135+
if entry.IsArchived() {
136+
return cldf.ChangesetOutput{}, fmt.Errorf("changeset '%s' is archived at SHA '%s'", key, *entry.gitSHA)
137+
}
138+
139+
return entry.changeset.Apply(e)
140+
}
141+
142+
// GetChangesetOptions retrieves the configuration options for a changeset.
143+
func (r *ChangesetsRegistry) GetChangesetOptions(key string) (ChangesetConfig, error) {
144+
entry, ok := r.entries[key]
145+
if !ok {
146+
return ChangesetConfig{}, fmt.Errorf("changeset '%s' not found", key)
147+
}
148+
149+
return entry.options, nil
150+
}
151+
152+
// GetConfigurations retrieves the configurations for a changeset.
153+
func (r *ChangesetsRegistry) GetConfigurations(key string) (Configurations, error) {
154+
entry, ok := r.entries[key]
155+
if !ok {
156+
return Configurations{}, fmt.Errorf("changeset '%s' not found", key)
157+
}
158+
159+
return entry.changeset.Configurations()
160+
}
161+
162+
// ChangesetOption defines an option for configuring a changeset
163+
type ChangesetOption func(*ChangesetConfig)
164+
165+
// ChangesetConfig holds configuration options for a changeset
166+
type ChangesetConfig struct {
167+
ChainsToLoad []uint64
168+
WithoutJD bool
169+
OperationRegistry *operations.OperationRegistry
170+
}
171+
172+
// OnlyLoadChainsFor will configure the environment to load only the specified chains.
173+
// By default, if option is not specified, all chains are loaded.
174+
// This is useful for changesets that are only applicable to a subset of chains.
175+
func OnlyLoadChainsFor(chainSelectors ...uint64) ChangesetOption {
176+
return func(o *ChangesetConfig) {
177+
o.ChainsToLoad = chainSelectors
178+
}
179+
}
180+
181+
// WithoutJD will configure the environment to not load Job Distributor.
182+
// By default, if option is not specified, Job Distributor is loaded.
183+
// This is useful for changesets that do not require Job Distributor to be loaded.
184+
func WithoutJD() ChangesetOption {
185+
return func(o *ChangesetConfig) {
186+
o.WithoutJD = true
187+
}
188+
}
189+
190+
// WithOperationRegistry will configure the changeset to use the specified operation registry.
191+
func WithOperationRegistry(registry *operations.OperationRegistry) ChangesetOption {
192+
return func(o *ChangesetConfig) {
193+
o.OperationRegistry = registry
194+
}
195+
}
196+
197+
// Add adds a changeset to the registry.
198+
// Options can be passed to configure the changeset.
199+
// - OnlyLoadChainsFor: will configure the environment to load only the specified chains.
200+
// - WithoutJD: will configure the environment to not load Job Distributor.
201+
// - WithOperationRegistry: will configure the changeset to use the specified operation registry.
202+
func (r *ChangesetsRegistry) Add(key string, cs ChangeSet, opts ...ChangesetOption) {
203+
r.mu.Lock()
204+
defer r.mu.Unlock()
205+
206+
// Validate the key format and index
207+
if r.validate {
208+
if err := r.validateKey(key); err != nil {
209+
panic(fmt.Errorf("invalid changeset key '%s': %w", key, err))
210+
}
211+
}
212+
213+
// Process the options
214+
options := ChangesetConfig{}
215+
for _, opt := range opts {
216+
opt(&options)
217+
}
218+
219+
r.entries[key] = newRegistryEntry(cs, options)
220+
r.keyHistory = append(r.keyHistory, key)
221+
}
222+
223+
func (r *ChangesetsRegistry) validateKey(key string) error {
224+
// Extract the numerical prefix from the new key
225+
currentIndex, err := extractIndexFromKey(key)
226+
if err != nil {
227+
return fmt.Errorf("invalid changeset key format '%s': %w", key, err)
228+
}
229+
230+
// If there are existing changesets, validate that the new index is greater than the last one
231+
if len(r.keyHistory) > 0 {
232+
lastKey := r.keyHistory[len(r.keyHistory)-1]
233+
lastIndex, err := extractIndexFromKey(lastKey)
234+
if err != nil {
235+
return fmt.Errorf("invalid previous changeset key format '%s': %w", lastKey, err)
236+
}
237+
238+
if currentIndex <= lastIndex {
239+
return fmt.Errorf("changeset index must be monotonically increasing: got %d, previous was %d",
240+
currentIndex, lastIndex)
241+
}
242+
}
243+
244+
return nil
245+
}
246+
247+
// extractIndexFromKey extracts the numerical index from a changeset key.
248+
// Expected format: "0001_changeset_name" where "0001" is the index.
249+
func extractIndexFromKey(key string) (int, error) {
250+
parts := strings.Split(key, "_")
251+
if len(parts) < 2 {
252+
return 0, fmt.Errorf("key '%s' does not follow the format 'index_name'", key)
253+
}
254+
255+
index, err := strconv.Atoi(parts[0])
256+
if err != nil {
257+
return 0, fmt.Errorf("could not parse index from key '%s': %w", key, err)
258+
}
259+
260+
return index, nil
261+
}
262+
263+
// Archive buries a changeset in the registry.
264+
func (r *ChangesetsRegistry) Archive(key string, gitSHA string) {
265+
r.mu.Lock()
266+
defer r.mu.Unlock()
267+
268+
r.entries[key] = newArchivedRegistryEntry(gitSHA)
269+
r.keyHistory = append(r.keyHistory, key)
270+
}
271+
272+
// LatestKey returns the most recent changeset key.
273+
func (r *ChangesetsRegistry) LatestKey() (string, error) {
274+
r.mu.Lock()
275+
defer r.mu.Unlock()
276+
277+
if len(r.keyHistory) == 0 {
278+
return "", errors.New("no changesets found")
279+
}
280+
281+
return r.keyHistory[len(r.keyHistory)-1], nil
282+
}
283+
284+
// ListKeys returns a copy of all registered changeset keys.
285+
func (r *ChangesetsRegistry) ListKeys() []string {
286+
r.mu.Lock()
287+
defer r.mu.Unlock()
288+
289+
return slices.Clone(r.keyHistory)
290+
}

0 commit comments

Comments
 (0)