Skip to content

Commit 76bfe5a

Browse files
authored
[scaffolder-relation-processor] Template update pull requests (#6715)
* feat: implement automatic PRs Signed-off-by: Diana Janickova <djanicko@redhat.com> * chore: yarn install Signed-off-by: Diana Janickova <djanicko@redhat.com> * chore: yarn dedupe Signed-off-by: Diana Janickova <djanicko@redhat.com> * chore: generate changeset Signed-off-by: Diana Janickova <djanicko@redhat.com> --------- Signed-off-by: Diana Janickova <djanicko@redhat.com>
1 parent 5711f69 commit 76bfe5a

38 files changed

+6432
-68
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@backstage-community/plugin-catalog-backend-module-scaffolder-relation-processor': minor
3+
---
4+
5+
Adds ability to automatically create PRs/MRs to keep scaffolded repositories in sync with their source templates. Includes file comparison, multi-VCS support (GitHub/GitLab), and automatic reviewer assignment. By default, the feature is disabled and can be enabled in config.

workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,29 @@ Both the title and description support the following template variables:
117117
### Disabling Notifications
118118

119119
To disable the notification feature, set `scaffolder.notifications.templateUpdate.enabled` to `false` in your configuration, or simply omit the entire `scaffolder.notifications.templateUpdate` section from your config (notifications are disabled by default).
120+
121+
## Template Update Pull Requests
122+
123+
In addition to notifications, this plugin can automatically create pull requests (or merge requests for GitLab) to keep scaffolded repositories in sync with their source templates. When a template version changes, the plugin compares files and creates PRs with any necessary updates.
124+
125+
### Quick Start
126+
127+
Enable the PR feature in your `app-config.yaml`:
128+
129+
```yaml
130+
scaffolder:
131+
pullRequests:
132+
templateUpdate:
133+
enabled: true
134+
```
135+
136+
### Key Features
137+
138+
- **Automatic file comparison**: Detects added, modified, and deleted files between template and scaffolded repositories
139+
- **Multi-VCS support**: Works with both GitHub and GitLab
140+
- **Automatic reviewer assignment**: Assigns the entity owner as a reviewer if they are a User
141+
- **Integration with notifications**: Can send notifications with links to created PRs
142+
143+
> ⚠️ **Important**: Always manually review the generated pull requests before merging. The automatic comparison may include changes that are intentionally different in your scaffolded repository.
144+
145+
For detailed configuration options, prerequisites, and troubleshooting, see the [Template Update PRs documentation](./docs/templateUpdatePRs.md).
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Template Update Pull Requests
2+
3+
This plugin can automatically create pull requests (or merge requests for GitLab) to keep scaffolded repositories in sync with their source templates. When a template version changes, the plugin compares the template files with the scaffolded repository and creates a PR with any necessary updates.
4+
5+
## How It Works
6+
7+
1. **Template Update Detection**: When the plugin detects that a scaffolder template has been updated to a new version, it triggers the PR creation process for all entities scaffolded from that template.
8+
9+
2. **File Comparison**: The plugin fetches files from both the template repository and each scaffolded repository, then compares them to identify:
10+
11+
- Files that need to be **updated** (content differs between template and scaffolded repo)
12+
- Files that need to be **created** (exist in template but not in scaffolded repo)
13+
- Files that need to be **deleted** (exist in scaffolded repo but no longer in template)
14+
15+
3. **PR Creation**: For each scaffolded entity with differences, a pull request is created containing all the necessary file changes.
16+
17+
4. **Reviewer Assignment**: If the scaffolded entity's owner is a **User** (not a Group), they are automatically assigned as a reviewer on the PR.
18+
19+
5. **Notification**: If notifications are enabled, entity owners receive a notification with a link to the created PR.
20+
21+
> ⚠️ **Important**: Always manually review the generated pull requests before merging. The automatic comparison may include changes that are intentionally different in your scaffolded repository, or may not account for project-specific customizations.
22+
23+
## Prerequisites
24+
25+
### VCS Integration Configuration
26+
27+
The plugin requires appropriate VCS (Version Control System) integrations to be configured in your `app-config.yaml`. The plugin currently supports **GitHub** and **GitLab**.
28+
29+
For detailed configuration of these integrations, see the [Backstage Integrations documentation](https://backstage.io/docs/integrations/).
30+
31+
### Entity Requirements
32+
33+
For the PR feature to work, scaffolded entities must have:
34+
35+
1. **`spec.scaffoldedFrom`**: Reference to the template entity (e.g., `template:default/my-template`)
36+
37+
2. **`backstage.io/managed-by-location`**: This annotation is automatically added by the catalog during entity fetching and points to the source location from which the entity was fetched. The plugin uses this annotation to determine the repository URL for the scaffolded entity.
38+
39+
> **Note**: The PR feature only works when this annotation is of type `url` and points to a GitHub or GitLab repository. Entities registered from other location types (e.g., `file`) are not supported for automatic PR creation.
40+
41+
## Configuration
42+
43+
Enable the PR feature in your `app-config.yaml`:
44+
45+
```yaml
46+
scaffolder:
47+
pullRequests:
48+
templateUpdate:
49+
enabled: true
50+
```
51+
52+
### Combined with Notifications
53+
54+
You can enable both PR creation and notifications together. Here's the behavior for each combination:
55+
56+
- **Both disabled**: No action taken on template updates
57+
- **Only notifications enabled**: Notification sent to entity owners with link to catalog
58+
- **Only PRs enabled**: PR created, no notification sent
59+
- **Both enabled**: PR created, notification sent with link to PR
60+
61+
Example configuration with both features enabled:
62+
63+
```yaml
64+
scaffolder:
65+
notifications:
66+
templateUpdate:
67+
enabled: true
68+
message:
69+
title: '$ENTITY_DISPLAY_NAME has a template update PR ready'
70+
description: 'A pull request has been created to sync template changes: $PR_LINK'
71+
pullRequests:
72+
templateUpdate:
73+
enabled: true
74+
```
75+
76+
When PRs are enabled, you can use the `$PR_LINK` template variable in your notification message to include a link to the created PR. If the PR creation fails, a notification with default (not custom) text is still sent along with error details.
77+
78+
## PR Details
79+
80+
### Branch Naming
81+
82+
PRs are created on a branch named:
83+
84+
```
85+
[component-name]/template-upgrade-v[new-version]
86+
```
87+
88+
For example: `my-service/template-upgrade-v1.2.0`
89+
90+
### PR Title
91+
92+
```
93+
Template Upgrade: Update [Template Name] from [old-version] to [new-version]
94+
```
95+
96+
## Reviewer Assignment
97+
98+
The plugin automatically assigns a reviewer to the PR if the scaffolded entity's owner is a **User** entity (not a Group). The reviewer assignment works as follows:
99+
100+
### For GitHub
101+
102+
The plugin looks for the `github.com/user-login` annotation on the owner User entity:
103+
104+
```yaml
105+
# User entity example
106+
apiVersion: backstage.io/v1alpha1
107+
kind: User
108+
metadata:
109+
name: john.doe
110+
annotations:
111+
github.com/user-login: johndoe # GitHub username
112+
spec:
113+
profile:
114+
displayName: John Doe
115+
```
116+
117+
### For GitLab
118+
119+
The plugin looks for the `gitlab.com/user-login` annotation on the owner User entity:
120+
121+
```yaml
122+
# User entity example
123+
apiVersion: backstage.io/v1alpha1
124+
kind: User
125+
metadata:
126+
name: john.doe
127+
annotations:
128+
gitlab.com/user-login: johndoe # GitLab username
129+
spec:
130+
profile:
131+
displayName: John Doe
132+
```
133+
134+
If the owner is a **Group** or the user annotation is not present, the PR is created without a reviewer assignment.
135+
136+
## Error Handling
137+
138+
### No Changes Detected
139+
140+
If there are no differences between the template and the scaffolded repository, no PR is created and no notification is sent for that entity.
141+
142+
### PR Creation Failures
143+
144+
If PR creation fails (e.g., authentication issues, API errors), the plugin:
145+
146+
- Logs the error
147+
- Sends a notification to the entity owner (if notifications are enabled) with error details
148+
- The notification uses a default message indicating the failure, regardless of any custom message configuration
149+
150+
Common failure reasons:
151+
152+
- Missing or invalid VCS integration credentials
153+
- Insufficient permissions to create branches or PRs
154+
- Network connectivity issues
155+
- Rate limiting by the VCS provider
156+
157+
## Limitations
158+
159+
- **Template variable resolution**: During file comparison, the plugin attempts to replace template variables (e.g., `${{ values.name }}`) with the actual values from the scaffolded repository by matching YAML keys. However, this has limitations:
160+
161+
- If a key cannot be matched between the template and scaffolded file, the raw template syntax will appear in the PR and must be resolved manually
162+
- Variables that were left empty during scaffolding may appear as differences
163+
- Only simple key-value patterns are matched; inline variables or complex nested structures may not be resolved correctly
164+
- Jinja2 conditionals (`{% if %}`, `{% endif %}`, etc.) are automatically stripped, but conditional content may still cause unexpected differences
165+
166+
- **File-based comparison only**: The plugin compares files at the repository root level based on the entity's managed-by-location annotation. It does not handle complex template structures with multiple directories.
167+
168+
- **No conflict resolution**: If the scaffolded repository has diverged significantly from the template, the PR may contain merge conflicts that need manual resolution.
169+
170+
- **Single PR per template update**: Each template version change creates new PRs for all scaffolded entities. If a previous PR is still open, the creation may fail if a branch with the same name already exists.
171+
172+
## Troubleshooting
173+
174+
### PRs Not Being Created
175+
176+
1. **Check VCS integration**: Ensure your `app-config.yaml` has the correct integration configured for your VCS provider.
177+
178+
2. **Check entity annotations**: Verify that scaffolded entities have the `backstage.io/managed-by-location` annotation pointing to a valid repository URL.
179+
180+
3. **Check logs**: Look for error messages in the Backstage backend logs related to `scaffolder-relation-processor`.
181+
182+
4. **Verify permissions**: Ensure the token or GitHub App has permissions to:
183+
- Read repository contents
184+
- Create branches
185+
- Create pull requests
186+
- Request reviewers (for reviewer assignment)
187+
188+
### Reviewer Not Being Assigned
189+
190+
1. **Check owner type**: Reviewer assignment only works for User entities, not Groups.
191+
192+
2. **Check user annotations**: Ensure the owner User entity has the appropriate VCS login annotation (`github.com/user-login` or `gitlab.com/user-login`).

workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,15 @@
4343
"@backstage/backend-plugin-api": "^1.5.0",
4444
"@backstage/catalog-client": "^1.12.1",
4545
"@backstage/catalog-model": "^1.7.6",
46+
"@backstage/integration": "^1.18.1",
4647
"@backstage/plugin-catalog-node": "^1.20.0",
4748
"@backstage/plugin-events-node": "^0.4.17",
4849
"@backstage/plugin-notifications-common": "^0.2.0",
49-
"@backstage/plugin-notifications-node": "^0.2.21"
50+
"@backstage/plugin-notifications-node": "^0.2.21",
51+
"@gitbeaker/rest": "^42.0.0",
52+
"@octokit/core": "^6.0.0",
53+
"git-url-parse": "^16.1.0",
54+
"octokit-plugin-create-pull-request": "^6.0.1"
5055
},
5156
"devDependencies": {
5257
"@backstage/backend-test-utils": "^1.10.1",

workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/report.api.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,33 @@ export default catalogModuleScaffolderRelationProcessor;
1919
export const DEFAULT_NOTIFICATION_DESCRIPTION =
2020
'The template used to create $ENTITY_DISPLAY_NAME has been updated to a new version. Review and update your entity to stay in sync with the template.';
2121

22+
// @public
23+
export const DEFAULT_NOTIFICATION_DESCRIPTION_WITH_PR =
24+
'The template used to create $ENTITY_DISPLAY_NAME has been updated to a new version. A pull request has been created to sync the changes: $PR_LINK';
25+
2226
// @public
2327
export const DEFAULT_NOTIFICATION_ENABLED = false;
2428

2529
// @public
2630
export const DEFAULT_NOTIFICATION_TITLE =
2731
'$ENTITY_DISPLAY_NAME is out of sync with template';
2832

33+
// @public
34+
export const DEFAULT_NOTIFICATION_TITLE_WITH_PR =
35+
'$ENTITY_DISPLAY_NAME has a template update PR ready';
36+
37+
// @public
38+
export const DEFAULT_PR_ENABLED = false;
39+
2940
// @public
3041
export const ENTITY_DISPLAY_NAME_TEMPLATE_VAR = '$ENTITY_DISPLAY_NAME';
3142

43+
// @public
44+
export const PR_CREATION_FAILED_PREFIX = 'Failed to create template update PR';
45+
46+
// @public
47+
export const PR_LINK_TEMPLATE_VAR = '$PR_LINK';
48+
3249
// @public
3350
export const RELATION_SCAFFOLDED_FROM = 'scaffoldedFrom';
3451

@@ -75,8 +92,18 @@ export interface ScaffolderRelationProcessorConfig {
7592
};
7693
};
7794
};
95+
// (undocumented)
96+
pullRequests?: {
97+
templateUpdate?: {
98+
enabled: boolean;
99+
};
100+
};
78101
}
79102

103+
// @public
104+
export const TEMPLATE_UPDATE_PRS_DOCS_URL =
105+
'https://github.com/backstage/community-plugins/tree/main/workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/docs/templateUpdatePRs.md';
106+
80107
// @public
81108
export const TEMPLATE_VERSION_UPDATED_TOPIC =
82109
'relationProcessor.template:version_updated';

workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/src/ScaffolderRelationEntityProcessor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import {
2828
} from '@backstage/plugin-catalog-node';
2929
import type { EventsService } from '@backstage/plugin-events-node';
3030

31-
import { RELATION_SCAFFOLDED_FROM, RELATION_SCAFFOLDER_OF } from './relations';
3231
import type { ScaffoldedFromSpec } from './types';
3332
import { handleTemplateVersion } from './templateVersionUtils';
33+
import { RELATION_SCAFFOLDED_FROM, RELATION_SCAFFOLDER_OF } from './constants';
3434

3535
/** @public */
3636
export class ScaffolderRelationEntityProcessor implements CatalogProcessor {

workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/src/constants.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,62 @@ export const DEFAULT_NOTIFICATION_DESCRIPTION = `The template used to create ${E
4949
* @public
5050
*/
5151
export const DEFAULT_NOTIFICATION_ENABLED = false;
52+
53+
/**
54+
* Default pull request creation enabled
55+
*
56+
* @public
57+
*/
58+
export const DEFAULT_PR_ENABLED = false;
59+
60+
/**
61+
* Template variable name for PR link in notification messages
62+
*
63+
* @public
64+
*/
65+
export const PR_LINK_TEMPLATE_VAR = '$PR_LINK';
66+
67+
/**
68+
* Default template update notification title when PR is created
69+
*
70+
* @public
71+
*/
72+
export const DEFAULT_NOTIFICATION_TITLE_WITH_PR = `${ENTITY_DISPLAY_NAME_TEMPLATE_VAR} has a template update PR ready`;
73+
74+
/**
75+
* Default template update notification description when PR is created
76+
*
77+
* @public
78+
*/
79+
export const DEFAULT_NOTIFICATION_DESCRIPTION_WITH_PR = `The template used to create ${ENTITY_DISPLAY_NAME_TEMPLATE_VAR} has been updated to a new version. A pull request has been created to sync the changes: ${PR_LINK_TEMPLATE_VAR}`;
80+
81+
/**
82+
* Prefix for notification description when PR creation fails
83+
*
84+
* @public
85+
*/
86+
export const PR_CREATION_FAILED_PREFIX = 'Failed to create template update PR';
87+
88+
/**
89+
* Documentation URL for the Template Update PRs feature
90+
*
91+
* @public
92+
*/
93+
export const TEMPLATE_UPDATE_PRS_DOCS_URL =
94+
'https://github.com/backstage/community-plugins/tree/main/workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/docs/templateUpdatePRs.md';
95+
96+
/**
97+
* A relation from a scaffolder template entity to the entity it generated.
98+
* Reverse direction of {@link RELATION_SCAFFOLDED_FROM}
99+
*
100+
* @public
101+
*/
102+
export const RELATION_SCAFFOLDER_OF = 'scaffolderOf';
103+
104+
/**
105+
* A relation of an entity generated from a scaffolder template entity
106+
* Reverse direction of {@link RELATION_SCAFFOLDER_OF}
107+
*
108+
* @public
109+
*/
110+
export const RELATION_SCAFFOLDED_FROM = 'scaffoldedFrom';

workspaces/scaffolder-relation-processor/plugins/catalog-backend-module-scaffolder-relation-processor/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
export * from './relations';
1716
export * from './types';
1817
export * from './constants';
1918
export { catalogModuleScaffolderRelationProcessor as default } from './module';

0 commit comments

Comments
 (0)