-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add predecessors package logic ported from create-proposal-pr composite action #474
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
d5ed3f5
feat: add predecessors package logic ported from create-proposal-pr c…
ecPablo 0ee1d60
chore: changeset
ecPablo 9b53993
Merge branch 'main' into ecpablo/dx-1876-opcount-sorting-port
ecPablo 5ce38d0
fix: go mo tidy
ecPablo b7d95c3
fix: increase test coverage
ecPablo 48150d0
fix: increase test coverage
ecPablo b43354c
fix: rename to GetProposalPRViews
ecPablo 9931d35
fix: catch error if graph is not a DAG
ecPablo 12b75f6
fix: comments and func names
ecPablo a6111e9
Merge branch 'main' into ecpablo/dx-1876-opcount-sorting-port
ecPablo 01c1427
fix: comments and func names
ecPablo 06205f0
Update experimental/proposalutils/predecessors/github_ops.go
ecPablo 15ac954
Update experimental/proposalutils/predecessors/github_ops.go
ecPablo 1eb821c
Update experimental/proposalutils/predecessors/predecessor.go
ecPablo 2b3223e
Update experimental/proposalutils/predecessors/predecessor.go
ecPablo f80f362
fix: remove github module as dependency and move proposal pr finder t…
ecPablo dda1894
Merge branch 'main' into ecpablo/dx-1876-opcount-sorting-port
ecPablo d42dc83
fix: buildPRDependencyGraph should be public as will be used on the p…
ecPablo 4ad0cc7
Merge branch 'main' into ecpablo/dx-1876-opcount-sorting-port
ecPablo c91ddcc
fix: make PRHead type public as it will be used by the Github pr find…
ecPablo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "chainlink-deployments-framework": minor | ||
| --- | ||
|
|
||
| add predecessors and opcount calculation logic to proposalutils package. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package proposalutils | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "path/filepath" | ||
| "strings" | ||
| ) | ||
|
|
||
| // MatchesProposalPath is a simple filter for proposal JSON files in the expected dir. | ||
| func MatchesProposalPath(domain, environment, p string) bool { | ||
| p = filepath.ToSlash(p) | ||
| prefix := fmt.Sprintf("domains/%s/%s/proposals/", domain, environment) | ||
|
|
||
| return strings.HasPrefix(p, prefix) && strings.HasSuffix(p, ".json") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| package predecessors | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-common/pkg/logger" | ||
|
|
||
| "github.com/smartcontractkit/mcms" | ||
|
|
||
| mcmstypes "github.com/smartcontractkit/mcms/types" | ||
| ) | ||
|
|
||
| // ComputeHighestOpCountsFromPredecessors looks at the given predecessors, | ||
| // and for each chain present in 'newProposalData', computes the new starting | ||
| // op count as: current StartingOpCount (from the new proposal) + SUM of the | ||
| // number of ops across ALL predecessors that share the same chain selector | ||
| // AND the same MCM address. We assume that the predecessors are sorted | ||
| // from oldest to newest, so we can log them in that order. | ||
| // Returns: | ||
| // - newStartPerChain: map[chain] = baseline + sum(ops) | ||
| func ComputeHighestOpCountsFromPredecessors( | ||
| lggr logger.Logger, | ||
| newProposalData ProposalsOpData, | ||
| predecessors []PRView, | ||
| ) map[mcmstypes.ChainSelector]uint64 { | ||
| out := make(map[mcmstypes.ChainSelector]uint64, len(newProposalData)) | ||
|
|
||
| lggr.Infof("Computing new starting op counts from %d predecessors...", len(predecessors)) | ||
|
|
||
| for sel, cur := range newProposalData { | ||
| lggr.Infof("New proposal data - chain %d: MCM=%s start=%d ops=%d", uint64(sel), cur.MCMAddress, cur.StartingOpCount, cur.OpsCount) | ||
| baseline := cur.StartingOpCount | ||
| var extra uint64 | ||
|
|
||
| for _, pred := range predecessors { | ||
| other, ok := pred.ProposalData[sel] | ||
| if !ok || !sameMCM(cur.MCMAddress, other.MCMAddress) { | ||
| continue | ||
| } | ||
|
|
||
| start := other.StartingOpCount | ||
| if start < baseline { | ||
| // Stale predecessor: its proposed start is below current on-chain baseline. | ||
| // Ignore to avoid double-counting or lowering. | ||
| lggr.Warnf("Skipping stale predecessor PR#%d on chain %d: pred.start=%d < baseline (ie onchain-value)=%d", | ||
| pred.Number, uint64(sel), start, baseline) | ||
|
|
||
| continue | ||
| } | ||
|
|
||
| extra += other.OpsCount | ||
| lggr.Infof("Counting predecessor PR#%d on chain %d: baseline=%d start=%d +ops=%d", | ||
| pred.Number, uint64(sel), baseline, start, other.OpsCount) | ||
| } | ||
|
|
||
| out[sel] = baseline + extra | ||
| } | ||
|
|
||
| // Handle case where a predecessor PR is merged between PR creation and execution: | ||
| // If the latest predecessor's op count is higher than the computed sum, adjust to match. | ||
| if len(predecessors) > 0 { | ||
| for sel, cur := range newProposalData { | ||
| // get most recent predecessor with same MCM address | ||
| latestPredecessor := predecessors[len(predecessors)-1] | ||
| other, ok := latestPredecessor.ProposalData[sel] | ||
| if !ok || !sameMCM(cur.MCMAddress, other.MCMAddress) { | ||
| continue | ||
| } | ||
| newStart := other.StartingOpCount + other.OpsCount | ||
| if newStart > out[sel] { | ||
| lggr.Warnf("sum of predecessors' ops on chain %d is %d, but latest predecessor PR#%d has ops=%d, adjusting new start from %d to %d. This can happen when a predecessor gets merged while this action is running, but is not executed on-chain", uint64(sel), out[sel]-cur.StartingOpCount, latestPredecessor.Number, other.OpsCount, out[sel], newStart) | ||
| out[sel] = newStart | ||
| } | ||
| } | ||
| } | ||
ecPablo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| lggr.Infof("Computed new starting op counts: %v", out) | ||
|
|
||
| return out | ||
| } | ||
|
|
||
ecPablo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // ApplyHighestOpCountsToProposal updates the proposal file with the new starting op counts. | ||
| func ApplyHighestOpCountsToProposal( | ||
| lggr logger.Logger, | ||
| proposalPath string, | ||
| newOpCounts map[mcmstypes.ChainSelector]uint64, | ||
| ) error { | ||
| // 1) load via mcms | ||
| prop, err := mcms.LoadProposal(mcmstypes.KindTimelockProposal, proposalPath) | ||
| if err != nil { | ||
| return fmt.Errorf("load proposal: %w", err) | ||
| } | ||
|
|
||
| // 2) cast to concrete type to mutate ChainMetadata | ||
| tp, ok := prop.(*mcms.TimelockProposal) | ||
| if !ok { | ||
| return fmt.Errorf("expected *mcms.TimelockProposal, got %T", prop) | ||
| } | ||
|
|
||
| // 3) bump starting op counts | ||
| changed := false | ||
| for sel, end := range newOpCounts { | ||
| meta, ok := tp.ChainMetadata[sel] | ||
| if !ok { | ||
| continue | ||
| } | ||
| newStart := end | ||
| if meta.StartingOpCount < newStart { | ||
| lggr.Infof("Updated startingOpCount: chain %d → %d", uint64(sel), end) | ||
| meta.StartingOpCount = newStart | ||
| tp.ChainMetadata[sel] = meta | ||
| changed = true | ||
| } else { | ||
| lggr.Warnf("Not updating startingOpCount for chain %d: current=%d, proposed=%d", | ||
| uint64(sel), meta.StartingOpCount, newStart) | ||
| } | ||
| } | ||
|
|
||
| if !changed { | ||
| lggr.Infof("No startingOpCount changes needed for %s", proposalPath) | ||
| return nil | ||
| } | ||
|
|
||
| // 4) write back using mcms helper | ||
| f, err := os.OpenFile(proposalPath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o600) | ||
| if err != nil { | ||
| return fmt.Errorf("open for write: %w", err) | ||
| } | ||
| defer f.Close() | ||
|
|
||
| if err := mcms.WriteTimelockProposal(f, tp); err != nil { | ||
| return fmt.Errorf("write proposal: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // ParseProposalOpsData uses mcms for a local file path (current proposal) to get op counts. | ||
| func ParseProposalOpsData(ctx context.Context, filePath string) (ProposalsOpData, error) { | ||
| proposal, err := mcms.LoadProposal(mcmstypes.KindTimelockProposal, filePath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("load proposal from %s: %w", filePath, err) | ||
| } | ||
| // convert to timelock proposal | ||
| tp, ok := proposal.(*mcms.TimelockProposal) | ||
| if !ok { | ||
| return nil, fmt.Errorf("expected *mcms.TimelockProposal, got %T", proposal) | ||
| } | ||
|
|
||
| // Use conversion-aware counts | ||
| counts, err := tp.OperationCounts(ctx) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("converted operation counts: %w", err) | ||
| } | ||
|
|
||
| data := make(ProposalsOpData, len(proposal.ChainMetadatas())) | ||
| for chain, meta := range proposal.ChainMetadatas() { | ||
| data[chain] = McmOpData{ | ||
| MCMAddress: strings.TrimSpace(meta.MCMAddress), | ||
| StartingOpCount: meta.StartingOpCount, | ||
| OpsCount: counts[chain], | ||
| } | ||
| } | ||
|
|
||
| return data, nil | ||
| } | ||
|
|
||
| // sameMCM is a tiny helper to check mcm addresses are equal | ||
| func sameMCM(a, b string) bool { | ||
| return strings.EqualFold(strings.TrimSpace(a), strings.TrimSpace(b)) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.