-
Notifications
You must be signed in to change notification settings - Fork 81
Fix dependency manager update prompts and filesystem actions #2226
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
Changes from all commits
6c95655
209f5bc
b80d6e9
96838db
d8a7b70
b01830d
6c775c7
f03072e
9858706
475cfa3
49e771c
9c28e1f
bcbe97a
8b93bac
fb48b32
51cc6f1
4a58bf0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,12 +59,23 @@ type categorizedLogs struct { | |||||||||||||||||
| type pendingPrompt struct { | ||||||||||||||||||
| contractName string | ||||||||||||||||||
| networkName string | ||||||||||||||||||
| contractAddr string | ||||||||||||||||||
| contractData string | ||||||||||||||||||
| needsDeployment bool | ||||||||||||||||||
| needsAlias bool | ||||||||||||||||||
| needsUpdate bool | ||||||||||||||||||
| updateHash string | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (pp *pendingPrompt) matches(name, network string) bool { | ||||||||||||||||||
| return pp.contractName == name && pp.networkName == network | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (di *DependencyInstaller) logFileSystemAction(message string) { | ||||||||||||||||||
| msg := util.MessageWithEmojiPrefix("✅", message) | ||||||||||||||||||
| di.logs.fileSystemActions = append(di.logs.fileSystemActions, msg) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (cl *categorizedLogs) LogAll(logger output.Logger) { | ||||||||||||||||||
| logger.Info(util.MessageWithEmojiPrefix("📝", "Dependency Manager Actions Summary")) | ||||||||||||||||||
| logger.Info("") // Add a line break after the section | ||||||||||||||||||
|
|
@@ -102,6 +113,7 @@ type DependencyFlags struct { | |||||||||||||||||
| skipDeployments bool `default:"false" flag:"skip-deployments" info:"Skip adding the dependency to deployments"` | ||||||||||||||||||
| skipAlias bool `default:"false" flag:"skip-alias" info:"Skip prompting for an alias"` | ||||||||||||||||||
| skipUpdatePrompts bool `default:"false" flag:"skip-update-prompts" info:"Skip prompting to update existing dependencies"` | ||||||||||||||||||
| update bool `default:"false" flag:"update" info:"Automatically accept all dependency updates"` | ||||||||||||||||||
| deploymentAccount string `default:"" flag:"deployment-account,d" info:"Account name to use for deployments (skips deployment account prompt)"` | ||||||||||||||||||
| name string `default:"" flag:"name" info:"Import alias name for the dependency (sets canonical field for Cadence import aliasing)"` | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -125,17 +137,35 @@ type DependencyInstaller struct { | |||||||||||||||||
| TargetDir string | ||||||||||||||||||
| SkipDeployments bool | ||||||||||||||||||
| SkipAlias bool | ||||||||||||||||||
| SkipUpdatePrompts bool | ||||||||||||||||||
| Update bool | ||||||||||||||||||
| DeploymentAccount string | ||||||||||||||||||
| Name string | ||||||||||||||||||
| logs categorizedLogs | ||||||||||||||||||
| dependencies map[string]config.Dependency | ||||||||||||||||||
| accountAliases map[string]map[string]flowsdk.Address // network -> account -> alias | ||||||||||||||||||
| installCount int // Track number of dependencies installed | ||||||||||||||||||
| pendingPrompts []pendingPrompt // Dependencies that need prompts after tree display | ||||||||||||||||||
| prompter Prompter // Optional: for testing. If nil, uses real prompts | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| type Prompter interface { | ||||||||||||||||||
| GenericBoolPrompt(msg string) (bool, error) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| type prompter struct{} | ||||||||||||||||||
|
|
||||||||||||||||||
| func (prompter) GenericBoolPrompt(msg string) (bool, error) { | ||||||||||||||||||
| return prompt.GenericBoolPrompt(msg) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // NewDependencyInstaller creates a new instance of DependencyInstaller | ||||||||||||||||||
| func NewDependencyInstaller(logger output.Logger, state *flowkit.State, saveState bool, targetDir string, flags DependencyFlags) (*DependencyInstaller, error) { | ||||||||||||||||||
| // Validate flags: --update and --skip-update-prompts are mutually exclusive | ||||||||||||||||||
| if flags.update && flags.skipUpdatePrompts { | ||||||||||||||||||
| return nil, fmt.Errorf("cannot use both --update and --skip-update-prompts flags together") | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| emulatorGateway, err := gateway.NewGrpcGateway(config.EmulatorNetwork) | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| return nil, fmt.Errorf("error creating emulator gateway: %v", err) | ||||||||||||||||||
|
|
@@ -165,12 +195,15 @@ func NewDependencyInstaller(logger output.Logger, state *flowkit.State, saveStat | |||||||||||||||||
| TargetDir: targetDir, | ||||||||||||||||||
| SkipDeployments: flags.skipDeployments, | ||||||||||||||||||
| SkipAlias: flags.skipAlias, | ||||||||||||||||||
| SkipUpdatePrompts: flags.skipUpdatePrompts, | ||||||||||||||||||
| Update: flags.update, | ||||||||||||||||||
| DeploymentAccount: flags.deploymentAccount, | ||||||||||||||||||
| Name: flags.name, | ||||||||||||||||||
| dependencies: make(map[string]config.Dependency), | ||||||||||||||||||
| logs: categorizedLogs{}, | ||||||||||||||||||
| accountAliases: make(map[string]map[string]flowsdk.Address), | ||||||||||||||||||
| pendingPrompts: make([]pendingPrompt, 0), | ||||||||||||||||||
| prompter: prompter{}, | ||||||||||||||||||
| }, nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -190,7 +223,6 @@ func (di *DependencyInstaller) Install() error { | |||||||||||||||||
| // Phase 1: Process all dependencies and display tree (no prompts) | ||||||||||||||||||
| for _, dependency := range *di.State.Dependencies() { | ||||||||||||||||||
| if err := di.processDependency(dependency); err != nil { | ||||||||||||||||||
| di.Logger.Error(fmt.Sprintf("Error processing dependency: %v", err)) | ||||||||||||||||||
| return err | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -556,18 +588,20 @@ func (di *DependencyInstaller) fetchDependenciesWithDepth(dependency config.Depe | |||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (di *DependencyInstaller) contractFileExists(address, contractName string) bool { | ||||||||||||||||||
| func (di *DependencyInstaller) getContractFilePath(address, contractName string) string { | ||||||||||||||||||
| fileName := fmt.Sprintf("%s.cdc", contractName) | ||||||||||||||||||
| path := filepath.Join("imports", address, fileName) | ||||||||||||||||||
| return filepath.Join("imports", address, fileName) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (di *DependencyInstaller) contractFileExists(address, contractName string) bool { | ||||||||||||||||||
| path := di.getContractFilePath(address, contractName) | ||||||||||||||||||
| _, err := di.State.ReaderWriter().Stat(path) | ||||||||||||||||||
|
|
||||||||||||||||||
| return err == nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (di *DependencyInstaller) createContractFile(address, contractName, data string) error { | ||||||||||||||||||
| fileName := fmt.Sprintf("%s.cdc", contractName) | ||||||||||||||||||
| path := filepath.Join(di.TargetDir, "imports", address, fileName) | ||||||||||||||||||
| relativePath := di.getContractFilePath(address, contractName) | ||||||||||||||||||
| path := filepath.Join(di.TargetDir, relativePath) | ||||||||||||||||||
| dir := filepath.Dir(path) | ||||||||||||||||||
|
|
||||||||||||||||||
| if err := di.State.ReaderWriter().MkdirAll(dir, 0755); err != nil { | ||||||||||||||||||
|
|
@@ -581,14 +615,31 @@ func (di *DependencyInstaller) createContractFile(address, contractName, data st | |||||||||||||||||
| return nil | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func (di *DependencyInstaller) handleFileSystem(contractAddr, contractName, contractData, networkName string) error { | ||||||||||||||||||
| // verifyLocalFileIntegrity checks if the local file matches the expected hash in flow.json | ||||||||||||||||||
| func (di *DependencyInstaller) verifyLocalFileIntegrity(contractAddr, contractName, expectedHash string) error { | ||||||||||||||||||
| if !di.contractFileExists(contractAddr, contractName) { | ||||||||||||||||||
| if err := di.createContractFile(contractAddr, contractName, contractData); err != nil { | ||||||||||||||||||
| return fmt.Errorf("failed to create contract file: %w", err) | ||||||||||||||||||
| } | ||||||||||||||||||
| return nil // File doesn't exist, nothing to verify | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| filePath := di.getContractFilePath(contractAddr, contractName) | ||||||||||||||||||
| fileContent, err := di.State.ReaderWriter().ReadFile(filePath) | ||||||||||||||||||
| if err != nil { | ||||||||||||||||||
| return fmt.Errorf("failed to read file for integrity check: %w", err) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Calculate hash of existing file | ||||||||||||||||||
| fileHash := sha256.New() | ||||||||||||||||||
| fileHash.Write(fileContent) | ||||||||||||||||||
| existingFileHash := hex.EncodeToString(fileHash.Sum(nil)) | ||||||||||||||||||
|
|
||||||||||||||||||
| msg := util.MessageWithEmojiPrefix("✅️", fmt.Sprintf("Contract %s from %s on %s installed", contractName, contractAddr, networkName)) | ||||||||||||||||||
| di.logs.fileSystemActions = append(di.logs.fileSystemActions, msg) | ||||||||||||||||||
| // Compare hashes | ||||||||||||||||||
| if expectedHash != existingFileHash { | ||||||||||||||||||
| return fmt.Errorf( | ||||||||||||||||||
| "dependency %s: local file has been modified (hash mismatch). Expected hash %s but file has %s. The file content does not match what is recorded in flow.json. Run 'flow dependencies install --update' to sync with the network version, or restore the file to match the stored hash", | ||||||||||||||||||
| contractName, | ||||||||||||||||||
| expectedHash, | ||||||||||||||||||
| existingFileHash, | ||||||||||||||||||
| ) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return nil | ||||||||||||||||||
|
|
@@ -598,13 +649,17 @@ func (di *DependencyInstaller) handleFoundContract(dependency config.Dependency, | |||||||||||||||||
| networkName := dependency.Source.NetworkName | ||||||||||||||||||
| contractAddr := dependency.Source.Address.String() | ||||||||||||||||||
| contractName := dependency.Source.ContractName | ||||||||||||||||||
| hash := sha256.New() | ||||||||||||||||||
| hash.Write(program.CodeWithUnprocessedImports()) | ||||||||||||||||||
| originalContractDataHash := hex.EncodeToString(hash.Sum(nil)) | ||||||||||||||||||
|
|
||||||||||||||||||
| program.ConvertAddressImports() | ||||||||||||||||||
| contractData := string(program.CodeWithUnprocessedImports()) | ||||||||||||||||||
|
|
||||||||||||||||||
| // Calculate hash of converted contract (what gets written to disk) | ||||||||||||||||||
| // This is what we store in flow.json so we can verify file integrity later | ||||||||||||||||||
| // Imported contracts are still checked for consistency by traversing the dependency tree. | ||||||||||||||||||
|
Comment on lines
+656
to
+658
|
||||||||||||||||||
| // Calculate hash of converted contract (what gets written to disk) | |
| // This is what we store in flow.json so we can verify file integrity later | |
| // Imported contracts are still checked for consistency by traversing the dependency tree. | |
| // Calculate hash of the converted contract source (the code that gets written to disk). | |
| // This hash is stored in flow.json so we can later verify that the on-disk contract | |
| // matches the version that was originally installed. Note that this hash only covers | |
| // the primary contract; any imported contracts are validated separately as part of | |
| // the dependency tree traversal logic elsewhere in the dependency manager. |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message is quite long and could be more user-friendly. Consider breaking it into multiple lines or simplifying the explanation to improve readability.
| return fmt.Errorf("dependency %s has changed on-chain but file does not exist locally. Cannot keep at current version because we have no way to fetch the old version from the blockchain. Either accept the update or manually add the contract file", pending.contractName) | |
| return fmt.Errorf( | |
| "dependency %s has changed on-chain, but the local contract file does not exist.\n"+ | |
| "Cannot keep the current version because the previous on-chain code cannot be fetched.\n"+ | |
| "Either accept the update or manually add the contract file.", | |
| pending.contractName, | |
| ) |
Uh oh!
There was an error while loading. Please reload this page.