Using merge strategies to generate bundles and specifying files - Amazon CodeCatalyst

Using merge strategies to generate bundles and specifying files

You can use merge strategies to generate bundles with resynthesis and specify files for lifecycle management updates of custom blueprints. By leveraging resythensis and merge strategies, you can manage updates and control which files get updated during deployments. You can also write your own strategies to control how changes are merged into existing CodeCatalyst projects.

Generating files with resynthesis

Resynthesis can merge the source code produced by a blueprint with source code that was previously generated by same the blueprint, allowing changes to a blueprint to be propagated to existing projects. Merges are run from the resynth() function across blueprint output bundles. Resynthesis first generates three bundles representing different aspects of the blueprint and project state. It can be manually run locally with the yarn blueprint:resynth command, which will create the bundles if they don’t already exist. Manually working with the bundles will allow you to mock and test resynthesis behavior locally. By default, blueprints only run resynthesis across the repositories under src/* since only that portion of the bundle is typically under source control. For more information, see Resynthesis.

  • existing-bundle - This bundle is a representation of the existing project state. This is artificially constructed by the synthesis compute to give the blueprint context about what's in the project it’s deploying into (if anything). If something already exists at this location when running resynthesis locally, it will be reset and respected as a mock. Otherwise, it will be set to the contents of the ancestor-bundle.

  • ancestor-bundle - This is the bundle that represents the blueprint output if it was synthesized with some previous options and/or version. If this is the first time this blueprint is being added to a project, then the ancestor doesn’t exist, so it’s set to the same contents as the existing-bundle. Locally, if this bundle already exists at this location, it will be respected as a mock.

  • proposed-bundle - This is the bundle that mocks the blueprint if it was synthesized with some new options and/or version. This is the same bundle that would be produced by the synth() function. Locally, this bundle is always overridden.

Each bundle is created during a resynthesis phase that can be accessed from the blueprint class under this.context.resynthesisPhase.

  • resolved-bundle - This is the final bundle, which is a representation of what gets packaged and deployed to a CodeCatalyst project. You can view which files and diffs are sent to the deployment mechanisms. This is the output of the resynth() function resolving merges between the three other bundles.

Three-way merge is applied by taking the difference between the ancestor-bundle and proposed-bundle and adding that to the existing-bundle to generate the resolved-bundle. All merge strategies resolve files to the resolved-bundle. Resynthesis resolves reach of these bundles with the blueprint’s merge strategies during resynth() and produces the resolved bundle from the result.

Using merge strategies

You can use a merge strategy vended by the blueprints library. These strategies provide ways to resolve file outputs and conflicts for files mentioned in the Generating files with resynthesis section.

  • alwaysUpdate - A strategy that always resolves to the proposed file.

  • neverUpdate - A strategy that always resolves to the existing file.

  • onlyAdd - A strategy that resolves to the proposed file when an existing file doesn't exist already. Otherwise, resolves to the existing file.

  • threeWayMerge - A strategy that performs a three-way merge between the existing, proposed, and common ancestor files. The resolved file may contain conflict markers if the files can't be cleanly merged. The provided files' contents must be UTF-8 encoded in order for the strategy to produce meaningful output. The strategy attempts to detect if the input files are binary. If the strategy detects a merge conflict in a binary file, it always returns the proposed file.

  • preferProposed - A strategy that performs a three-way merge between the existing, proposed, and common ancestor files. This strategy resolves conflicts by selecting the proposed file's side of each conflict.

  • preferExisting - A strategy that performs a three-way merge between the existing, proposed, and common ancestor files. This strategy resolves conflicts by selecting the existing file's side of each conflict.

To view the source code for the merge strategies, see the open-source GitHub repository.

Specifying files for lifecycle management updates

During resynthesis, blueprints control how changes are merged into an existing source repository. However, you might not want to push updates to every single file in your blueprint. For example, sample code like CSS stylesheets are intended to be project specific. The three-way merge strategy is the default option if you don't specify another strategy. Blueprints can specify which files they own and which files they don’t by specifying merge strategies on the repository construct itself. Blueprints can update their merge strategies, and the latest strategies can be used during resynthesis.

const sourceRepo = new SourceRepository(this, { title: 'my-repo', }); sourceRepo.setResynthStrategies([ { identifier: 'dont-override-sample-code', description: 'This strategy is applied accross all sample code. The blueprint will create sample code, but skip attempting to update it.', strategy: MergeStrategies.neverUpdate, globs: [ '**/src/**', '**/css/**', ], }, ]);

Multiple merge strategies can be specified, and the last strategy takes precedence. Uncovered files default to a three-way-merge similar to Git. There are several merge strategies provided through the MergeStrategies construct, but you can write your own. The provided strategies adhere to the git merge strategy driver.

Writing merge strategies

In addition to using one of the provided build merge strategies, you can also write your own strategies. Strategies must adhere to a standard strategy interface. You must write a strategy function that takes versions of a file from the existing-bundle, proposed-bundle, and ancestor-bundle, and merges them into a single resolved file. For example:

type StrategyFunction = ( /** * file from the ancestor bundle (if it exists) */ commonAncestorFile: ContextFile | undefined, /** * file from the existing bundle (if it exists) */ existingFile: ContextFile | undefined, /** * file from the proposed bundle (if it exists) */ proposedFile: ContextFile | undefined, options?: {}) /** * Return: file you'd like in the resolved bundle * passing undefined will delete the file from the resolved bundle */ => ContextFile | undefined;

If the files don’t exist (are undefined), then that file path doesn’t exist in that particular location bundle.

Example:

strategies: [ { identifier: 'dont-override-sample-code', description: 'This strategy is applied across all sample code. The blueprint will create sample code, but skip attempting to update it.', strategy: (ancestor, existing, proposed) => { const resolvedfile = ... ... // do something ... return resolvedfile }, globs: [ '**/src/**', '**/css/**', ], }, ],