@spotify/backstage-plugin-soundcheck-backend-module-github 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE.md +9 -0
- package/README.md +429 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.d.ts +36 -0
- package/package.json +48 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @spotify/backstage-plugin-soundcheck-backend-module-github
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 07770382: Upgrade to Backstage 1.12.0
|
|
8
|
+
- 7dc0e65c: Adds GitHub integration for fact extraction from GitHub repositories. See the modules README.md for additional details, including configuration details.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies [3e368dc9]
|
|
13
|
+
- @spotify/backstage-plugin-soundcheck-common@0.4.0
|
|
14
|
+
- @spotify/backstage-plugin-soundcheck-node@0.2.0
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Commercial License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Spotify. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software is part of the "Spotify Plugins for Backstage" bundle. "Spotify
|
|
6
|
+
Plugins for Backstage" is commercial software. To use it, you must obtain a
|
|
7
|
+
license and agree to the [License
|
|
8
|
+
Terms](https://backstage.spotify.com/spotify-plugins-for-backstage-terms).
|
|
9
|
+
Commercial licenses can be obtained at https://backstage.spotify.com/.
|
package/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# Spotify Plugins for Backstage: Soundcheck - Github Integration
|
|
2
|
+
|
|
3
|
+
Github integration plugin for [Soundcheck](https://backstage.spotify.com/plugins/soundcheck/).
|
|
4
|
+
Similarly to [SCM Integration plugin](https://www.npmjs.com/package/@spotify/backstage-plugin-soundcheck-backend-module-scm), it provides out-of-box integration with Github by leveraging Backstage's Github integration and implements extraction and collection of facts from Github repositories.
|
|
5
|
+
The purpose of Github Integration plugin is to provide Github-specific fact collection (like branch protections), while the SCM Integration plugin provides the collection of facts based on repository content.
|
|
6
|
+
|
|
7
|
+
Github integration plugin supports the extraction of the following facts:
|
|
8
|
+
|
|
9
|
+
- [BranchProtections](#collecting-branchprotections-fact)
|
|
10
|
+
- [RepositoryDetails](#collecting-repositorydetails-fact)
|
|
11
|
+
|
|
12
|
+
<!-- TOC -->
|
|
13
|
+
|
|
14
|
+
- [Spotify Plugins for Backstage: Soundcheck - Github Integration](#spotify-plugins-for-backstage-soundcheck---github-integration)
|
|
15
|
+
- [Prerequisites](#prerequisites)
|
|
16
|
+
- [Configure Github integration in Backstage](#configure-github-integration-in-backstage)
|
|
17
|
+
- [Plugin Configuration](#plugin-configuration)
|
|
18
|
+
- [Defining Github Fact Collectors](#defining-github-fact-collectors)
|
|
19
|
+
- [Overall Shape Of A Github Fact Collector](#overall-shape-of-a-github-fact-collector)
|
|
20
|
+
- [`frequency` [optional]](#frequency-optional)
|
|
21
|
+
- [`filter` [optional]](#filter-optional)
|
|
22
|
+
- [`cache` [optional]](#cache-optional)
|
|
23
|
+
- [`collects` [required]](#collects-required)
|
|
24
|
+
- [Overall Shape Of A Fact Extractor](#overall-shape-of-a-fact-extractor)
|
|
25
|
+
- [`factName` [required]](#factname-required)
|
|
26
|
+
- [`type` [required]](#type-required)
|
|
27
|
+
- [`frequency` [optional]](#frequency-optional)
|
|
28
|
+
- [`branch` [optional]](#branch-optional)
|
|
29
|
+
- [`filter` [optional]](#filter-optional)
|
|
30
|
+
- [`cache` [optional]](#cache-optional)
|
|
31
|
+
- [Collecting BranchProtections Fact](#collecting-branchprotections-fact)
|
|
32
|
+
- [Shape of A BranchProtections Fact Collector](#shape-of-a-branchprotections-fact-collector)
|
|
33
|
+
- [Shape of A BranchProtections Fact](#shape-of-a-branchprotections-fact)
|
|
34
|
+
- [Shape of A BranchProtections Fact Check](#shape-of-a-branchprotections-fact-check)
|
|
35
|
+
- [Collecting RepositoryDetails Fact](#collecting-repositorydetails-fact)
|
|
36
|
+
- [Shape of A RepositoryDetails Fact Collector](#shape-of-a-repositorydetails-fact-collector)
|
|
37
|
+
- [Shape of A RepositoryDetails Fact](#shape-of-a-repositorydetails-fact)
|
|
38
|
+
- [Shape of A RepositoryDetails Fact Check](#shape-of-a-repositorydetails-fact-check)
|
|
39
|
+
<!-- TOC -->
|
|
40
|
+
|
|
41
|
+
## Prerequisites
|
|
42
|
+
|
|
43
|
+
### Configure Github integration in Backstage
|
|
44
|
+
|
|
45
|
+
Integrations are configured at the root level of `app-config.yaml`, here's an example configuration for Github:
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
integrations:
|
|
49
|
+
github:
|
|
50
|
+
- host: github.com
|
|
51
|
+
token: ${GITHUB_TOKEN}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Follow the [instructions](https://backstage.io/docs/integrations/github/locations) for full details on configuration.
|
|
55
|
+
|
|
56
|
+
## Plugin Configuration
|
|
57
|
+
|
|
58
|
+
Collection of facts is driven by config. To learn more about the config, see the [Defining Github Fact Collectors](#defining-github-fact-collectors).
|
|
59
|
+
|
|
60
|
+
1. Create `github-facts-collectors.yaml` in the root of your Backstage repository and fill in all your Github fact collectors.
|
|
61
|
+
A simple example Github fact collector is listed below.
|
|
62
|
+
|
|
63
|
+
**Note:** this file will be loaded at runtime along with the rest of your Backstage configuration files, so make sure it's available in deployed environments in the same way as your `app-config.yaml` files.
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
---
|
|
67
|
+
frequency:
|
|
68
|
+
cron: '0 * * * *'
|
|
69
|
+
collects:
|
|
70
|
+
- factName: branch_protections
|
|
71
|
+
type: BranchProtections
|
|
72
|
+
branch: master
|
|
73
|
+
- factName: repo_details
|
|
74
|
+
type: RepositoryDetails
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
2. Add a soundcheck collectors field to `app-config.yaml` and reference the newly created `github-facts-collectors.yaml`
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# app-config.yaml
|
|
81
|
+
soundcheck:
|
|
82
|
+
collectors:
|
|
83
|
+
github:
|
|
84
|
+
$include: ./github-facts-collectors.yaml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Defining Github Fact Collectors
|
|
88
|
+
|
|
89
|
+
This section describes the data shape and semantics of Github Fact Collectors.
|
|
90
|
+
|
|
91
|
+
### Overall Shape Of A Github Fact Collector
|
|
92
|
+
|
|
93
|
+
The following is an example of a descriptor file for a Github Fact Collector:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
---
|
|
97
|
+
frequency:
|
|
98
|
+
cron: '0 * * * *'
|
|
99
|
+
filter:
|
|
100
|
+
kind: 'Component'
|
|
101
|
+
cache:
|
|
102
|
+
duration:
|
|
103
|
+
hours: 2
|
|
104
|
+
collects:
|
|
105
|
+
- factName: branch_protections
|
|
106
|
+
type: BranchProtections
|
|
107
|
+
branch: master
|
|
108
|
+
filter:
|
|
109
|
+
- spec.lifecycle: 'production'
|
|
110
|
+
spec.type: 'website'
|
|
111
|
+
cache: false
|
|
112
|
+
- factName: repo_details
|
|
113
|
+
type: RepositoryDetails
|
|
114
|
+
cache: true
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
See below for details about these fields.
|
|
118
|
+
|
|
119
|
+
#### `frequency` [optional]
|
|
120
|
+
|
|
121
|
+
The frequency at which the collector should be executed. Possible values are either a cron expression `{ cron: ... }` or [HumanDuration](https://backstage.io/docs/reference/backend-tasks.humanduration).
|
|
122
|
+
This is the default frequency for each extractor.
|
|
123
|
+
|
|
124
|
+
#### `filter` [optional]
|
|
125
|
+
|
|
126
|
+
A filter specifying which entities to collect the specified facts for. Matches the [filter format](https://backstage.io/docs/reference/catalog-client.entityfilterquery) used by the Catalog API.
|
|
127
|
+
This is the default filter for each extractor.
|
|
128
|
+
|
|
129
|
+
#### `cache` [optional]
|
|
130
|
+
|
|
131
|
+
If the collected facts should be cached, and if so for how long. Possible values are either `true` or `false` or a nested `{ duration:` [HumanDuration](https://backstage.io/docs/reference/types.humanduration) `}` field.
|
|
132
|
+
This is the default cache config for each extractor.
|
|
133
|
+
|
|
134
|
+
#### `collects` [required]
|
|
135
|
+
|
|
136
|
+
An array describing which facts to collect and how to extract them. See below for details about the overall shape of a fact extractor.
|
|
137
|
+
|
|
138
|
+
### Overall Shape Of A Fact Extractor
|
|
139
|
+
|
|
140
|
+
Each extractor supports the fields described below.
|
|
141
|
+
|
|
142
|
+
#### `factName` [required]
|
|
143
|
+
|
|
144
|
+
The name of the fact to be extracted.
|
|
145
|
+
|
|
146
|
+
- Minimum length of 1
|
|
147
|
+
- Maximum length of 100
|
|
148
|
+
- Alphanumeric with single separator instances of periods, dashes, underscores, or forward slashes
|
|
149
|
+
|
|
150
|
+
#### `type` [required]
|
|
151
|
+
|
|
152
|
+
The type of the extractor (e.g. BranchProtections, RepositoryDetails).
|
|
153
|
+
|
|
154
|
+
#### `frequency` [optional]
|
|
155
|
+
|
|
156
|
+
The frequency at which the fact extraction should be executed. Possible values are either a cron expression `{ cron: ... }` or [HumanDuration](https://backstage.io/docs/reference/backend-tasks.humanduration).
|
|
157
|
+
If provided it overrides the default frequency provided at the top level. If not provided it defaults to the frequency provided at the top level. If neither extractor's frequency nor default frequency is provided the fact will only be collected on demand.
|
|
158
|
+
Example:
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
frequency:
|
|
162
|
+
minutes: 10
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `branch` [optional]
|
|
166
|
+
|
|
167
|
+
The branch to extract the fact from. If not provided, defaults to the repository's default branch.
|
|
168
|
+
|
|
169
|
+
#### `filter` [optional]
|
|
170
|
+
|
|
171
|
+
A filter specifying which entities to collect the specified facts for. Matches the [filter format](https://backstage.io/docs/reference/catalog-client.entityfilterquery) used by the Catalog API.
|
|
172
|
+
If provided it overrides the default filter provided at the top level. If not provided it defaults to the filter provided at the top level. If neither extractor's filter nor default filter is provided the fact will be collected for all entities.
|
|
173
|
+
|
|
174
|
+
#### `cache` [optional]
|
|
175
|
+
|
|
176
|
+
If the collected facts should be cached, and if so for how long. Possible values are either `true` or `false` or a nested `{ duration:` [HumanDuration](https://backstage.io/docs/reference/types.humanduration) `}` field.
|
|
177
|
+
If provided it overrides the default cache config provided at the top level. If not provided it defaults to the cache config provided at the top level. If neither extractor's cache nor default cache config is provided the fact will not be cached.
|
|
178
|
+
Example:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
cache:
|
|
182
|
+
duration:
|
|
183
|
+
hours: 24
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Collecting BranchProtections Fact
|
|
187
|
+
|
|
188
|
+
BranchProtections fact contains information about configured branch protections for a given branch in Github repository.
|
|
189
|
+
|
|
190
|
+
### Shape of A BranchProtections Fact Collector
|
|
191
|
+
|
|
192
|
+
The shape of a BranchProtections Fact Collector matches the [Overall Shape Of A Github Fact Collector](#overall-shape-of-a-github-fact-collector) (restriction: `type: BranchProtections`).
|
|
193
|
+
|
|
194
|
+
The following is an example of the BranchProtections Fact Collector config:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
collects:
|
|
198
|
+
- factName: branch_protections
|
|
199
|
+
type: BranchProtections
|
|
200
|
+
frequency:
|
|
201
|
+
cron: '0 * * * *'
|
|
202
|
+
branch: master
|
|
203
|
+
filter:
|
|
204
|
+
- spec.lifecycle: 'production'
|
|
205
|
+
spec.type: 'website'
|
|
206
|
+
cache: false
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Shape of A BranchProtections Fact
|
|
210
|
+
|
|
211
|
+
The shape of a BranchProtections Fact is based on the [Fact Schema](https://www.npmjs.com/package/@spotify/backstage-plugin-soundcheck-backend#fact-schema).
|
|
212
|
+
|
|
213
|
+
For a description of the data collected regarding branch protection, refer to the [Github API documentation](https://docs.github.com/en/rest/branches/branch-protection).
|
|
214
|
+
|
|
215
|
+
The following is an example of the collected BranchProtections Fact:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
factRef: github:master/branch_protections
|
|
219
|
+
entityRef: component:default/queue-proxy
|
|
220
|
+
scope: master
|
|
221
|
+
timestamp: 2023-02-24T15:50+00Z
|
|
222
|
+
data:
|
|
223
|
+
url: 'https://api.github.com/repos/backstage/backstage/branches/master/protection'
|
|
224
|
+
required_pull_request_reviews:
|
|
225
|
+
url: 'https://api.github.com/repos/backstage/backstage/branches/master/protection/required_pull_request_reviews',
|
|
226
|
+
dismiss_stale_reviews: false
|
|
227
|
+
require_code_owner_reviews: true
|
|
228
|
+
required_approving_review_count: 2
|
|
229
|
+
require_last_push_approval: false
|
|
230
|
+
required_signatures:
|
|
231
|
+
url: 'https://api.github.com/repos/backstage/backstage/branches/master/protection/required_signatures'
|
|
232
|
+
enabled: false
|
|
233
|
+
enforce_admins:
|
|
234
|
+
url: 'https://api.github.com/repos/backstage/backstage/branches/master/protection/enforce_admins'
|
|
235
|
+
enabled: false
|
|
236
|
+
required_linear_history:
|
|
237
|
+
enabled: false
|
|
238
|
+
allow_force_pushes:
|
|
239
|
+
enabled: true
|
|
240
|
+
allow_deletions:
|
|
241
|
+
enabled: true
|
|
242
|
+
block_creations:
|
|
243
|
+
enabled: true
|
|
244
|
+
required_conversation_resolution:
|
|
245
|
+
enabled: false
|
|
246
|
+
lock_branch:
|
|
247
|
+
enabled: false
|
|
248
|
+
allow_fork_syncing:
|
|
249
|
+
enabled: true
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Shape of A BranchProtections Fact Check
|
|
253
|
+
|
|
254
|
+
The shape of a BranchProtections Fact Check matches the [Shape of a Fact Check](https://www.npmjs.com/package/@spotify/backstage-plugin-soundcheck-backend#shape-of-a-fact-check).
|
|
255
|
+
|
|
256
|
+
The following is an example of the BranchProtections fact checks:
|
|
257
|
+
|
|
258
|
+
```yaml
|
|
259
|
+
soundcheck:
|
|
260
|
+
checks:
|
|
261
|
+
- id: requires_code_owner_reviews
|
|
262
|
+
rule:
|
|
263
|
+
factRef: github:master/branch_protections
|
|
264
|
+
path: $.required_pull_request_reviews.require_code_owner_reviews
|
|
265
|
+
operator: equal
|
|
266
|
+
value: true
|
|
267
|
+
- id: requires_at_least_two_approving_reviews
|
|
268
|
+
rule:
|
|
269
|
+
factRef: github:master/branch_protections
|
|
270
|
+
path: $.required_pull_request_reviews.required_approving_review_count
|
|
271
|
+
operator: greaterThanInclusive
|
|
272
|
+
value: 2
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The following is an example of the Soundcheck program that utilizes these checks:
|
|
276
|
+
|
|
277
|
+
```yaml
|
|
278
|
+
- id: demo
|
|
279
|
+
name: Demo
|
|
280
|
+
ownerEntityRef: group:default/owning_group
|
|
281
|
+
description: Demonstration of Soundcheck BranchProtections Fact Extractor
|
|
282
|
+
levels:
|
|
283
|
+
- ordinal: 1
|
|
284
|
+
name: First level
|
|
285
|
+
description: Checks leveraging Soundcheck's Github BranchProtections Fact Extractor
|
|
286
|
+
checks:
|
|
287
|
+
- id: requires_code_owner_reviews
|
|
288
|
+
name: Requires code owner reviews
|
|
289
|
+
description: PR requires code owner reviews
|
|
290
|
+
- id: requires_at_least_two_approving_reviews
|
|
291
|
+
name: Requires at least two approving reviews
|
|
292
|
+
description: PR requires at least two approving reviews
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Collecting RepositoryDetails Fact
|
|
296
|
+
|
|
297
|
+
RepositoryDetails fact contains information about a Github repository.
|
|
298
|
+
|
|
299
|
+
### Shape of A RepositoryDetails Fact Collector
|
|
300
|
+
|
|
301
|
+
The shape of a RepositoryDetails Fact Collector matches the [Overall Shape Of A Github Fact Collector](#overall-shape-of-a-github-fact-collector) (restriction: `type: RepositoryDetails`).
|
|
302
|
+
|
|
303
|
+
The following is an example of the RepositoryDetails Fact Collector config:
|
|
304
|
+
|
|
305
|
+
```yaml
|
|
306
|
+
collects:
|
|
307
|
+
- factName: repo_details
|
|
308
|
+
type: RepositoryDetails
|
|
309
|
+
frequency:
|
|
310
|
+
cron: '0 * * * *'
|
|
311
|
+
filter:
|
|
312
|
+
- spec.lifecycle: 'production'
|
|
313
|
+
cache: true
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Shape of A RepositoryDetails Fact
|
|
317
|
+
|
|
318
|
+
The shape of a RepositoryDetails Fact is based on the [Fact Schema](https://www.npmjs.com/package/@spotify/backstage-plugin-soundcheck-backend#fact-schema).
|
|
319
|
+
|
|
320
|
+
For a description of the data collected about repository, refer to the [Github API documentation](https://docs.github.com/en/rest/repos/repos#get-a-repository).
|
|
321
|
+
|
|
322
|
+
The following is an example of the collected RepositoryDetails Fact:
|
|
323
|
+
|
|
324
|
+
```yaml
|
|
325
|
+
factRef: github:default/repo_details
|
|
326
|
+
entityRef: component:default/queue-proxy
|
|
327
|
+
scope: default
|
|
328
|
+
timestamp: 2023-02-24T15:50+00Z
|
|
329
|
+
data:
|
|
330
|
+
name: backstage
|
|
331
|
+
full_name: backstage/backstage
|
|
332
|
+
private: true
|
|
333
|
+
html_url: 'https://github.com/backstage/backstage'
|
|
334
|
+
description: null
|
|
335
|
+
fork: false
|
|
336
|
+
url: 'https://api.github.com/repos/backstage/backstage'
|
|
337
|
+
homepage: null
|
|
338
|
+
size: 3
|
|
339
|
+
stargazers_count: 0
|
|
340
|
+
watchers_count: 0
|
|
341
|
+
language: null
|
|
342
|
+
has_issues: true
|
|
343
|
+
has_projects: true
|
|
344
|
+
has_downloads: true
|
|
345
|
+
has_wiki: true
|
|
346
|
+
has_pages: false
|
|
347
|
+
has_discussions: false
|
|
348
|
+
forks_count: 0
|
|
349
|
+
mirror_url: null
|
|
350
|
+
archived: false
|
|
351
|
+
disabled: false
|
|
352
|
+
open_issues_count: 0
|
|
353
|
+
license: null
|
|
354
|
+
allow_forking: true
|
|
355
|
+
is_template: false
|
|
356
|
+
web_commit_signoff_required: false
|
|
357
|
+
visibility: 'private'
|
|
358
|
+
forks: 0
|
|
359
|
+
open_issues: 0
|
|
360
|
+
watchers: 0
|
|
361
|
+
default_branch: master
|
|
362
|
+
permissions:
|
|
363
|
+
admin: true
|
|
364
|
+
maintain: true
|
|
365
|
+
push: true
|
|
366
|
+
triage: true
|
|
367
|
+
pull: true
|
|
368
|
+
allow_squash_merge: true
|
|
369
|
+
allow_merge_commit: true
|
|
370
|
+
allow_rebase_merge: true
|
|
371
|
+
allow_auto_merge: false
|
|
372
|
+
delete_branch_on_merge: false
|
|
373
|
+
allow_update_branch: false
|
|
374
|
+
use_squash_pr_title_as_default: false
|
|
375
|
+
squash_merge_commit_message: 'COMMIT_MESSAGES'
|
|
376
|
+
squash_merge_commit_title: 'COMMIT_OR_PR_TITLE'
|
|
377
|
+
merge_commit_message: 'PR_TITLE'
|
|
378
|
+
merge_commit_title: 'MERGE_MESSAGE'
|
|
379
|
+
security_and_analysis:
|
|
380
|
+
secret_scanning:
|
|
381
|
+
status: 'disabled'
|
|
382
|
+
secret_scanning_push_protection:
|
|
383
|
+
status: 'disabled'
|
|
384
|
+
network_count: 0
|
|
385
|
+
subscribers_count: 1
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Shape of A RepositoryDetails Fact Check
|
|
389
|
+
|
|
390
|
+
The shape of a RepositoryDetails Fact Check matches the [Shape of a Fact Check](https://www.npmjs.com/package/@spotify/backstage-plugin-soundcheck-backend#shape-of-a-fact-check).
|
|
391
|
+
|
|
392
|
+
The following is an example of the RepositoryDetails fact checks:
|
|
393
|
+
|
|
394
|
+
```yaml
|
|
395
|
+
soundcheck:
|
|
396
|
+
checks:
|
|
397
|
+
- id: allows_rebase_merge
|
|
398
|
+
rule:
|
|
399
|
+
factRef: github:default/repo_details
|
|
400
|
+
path: $.allow_rebase_merge
|
|
401
|
+
operator: equal
|
|
402
|
+
value: true
|
|
403
|
+
- id: has_less_than_ten_open_issues
|
|
404
|
+
rule:
|
|
405
|
+
factRef: github:default/repo_details
|
|
406
|
+
path: $.open_issues
|
|
407
|
+
operator: lessThan
|
|
408
|
+
value: 10
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The following is an example of the Soundcheck program that utilizes these checks:
|
|
412
|
+
|
|
413
|
+
```yaml
|
|
414
|
+
- id: demo
|
|
415
|
+
name: Demo
|
|
416
|
+
ownerEntityRef: group:default/owning_group
|
|
417
|
+
description: Demonstration of Soundcheck RepositoryDetails Fact Extractor
|
|
418
|
+
levels:
|
|
419
|
+
- ordinal: 1
|
|
420
|
+
name: First level
|
|
421
|
+
description: Checks leveraging Soundcheck's Github RepositoryDetails Fact Extractor
|
|
422
|
+
checks:
|
|
423
|
+
- id: allows_rebase_merge
|
|
424
|
+
name: Allows Rebase Merge
|
|
425
|
+
description: Repository allows rebase merge
|
|
426
|
+
- id: has_less_than_ten_open_issues
|
|
427
|
+
name: Has Less Than 10 Open Issues
|
|
428
|
+
description: Github Repository Has Less Than 10 Open Issues
|
|
429
|
+
```
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var s=require("@spotify/backstage-plugin-soundcheck-common"),nt=require("@backstage/catalog-model"),ct=require("@octokit/request-error"),lt=require("@octokit/rest"),N=require("@backstage/integration"),v=require("zod"),ht=require("@backstage/errors"),ft=require("git-url-parse");function dt(t){return t&&typeof t=="object"&&"default"in t?t:{default:t}}var ut=dt(ft),pt=(t,e,r)=>{if(!e.has(t))throw TypeError("Cannot "+r)},W=(t,e,r)=>(pt(t,e,"read from private field"),r?r.call(t):e.get(t)),gt=(t,e,r)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,r)},d;function vt(t){return`Extractor with type '${t}' is not allowed. You may need to update collectors configuration at 'soundcheck.collectors.github'`}class mt{constructor(){gt(this,d,[])}register(e){W(this,d).push(e)}async extract(e,r,a){for(const{predicate:i,extractor:n}of W(this,d))if(i(e))return n.extract(e,r,a);throw new Error(vt(e.type))}toString(){return`predicateMux{extractors=${W(this,d).map(e=>e.extractor).join(",")}`}}d=new WeakMap;class wt{static create(e){const{logger:r,branchHelper:a,factories:i}=e,n=new mt;for(const k of i!=null?i:[]){const S=k({logger:r,branchHelper:a});n.register(S)}return n}}var F=(t,e,r)=>{if(!e.has(t))throw TypeError("Cannot "+r)},U=(t,e,r)=>(F(t,e,"read from private field"),r?r.call(t):e.get(t)),M=(t,e,r)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,r)},J=(t,e,r,a)=>(F(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),yt=(t,e,r)=>(F(t,e,"access private method"),r),m,w,R,A;const L=class{constructor(t,e){M(this,R),M(this,m,void 0),M(this,w,void 0),J(this,m,t),J(this,w,e)}async extract(t,e,r){const a={owner:e.owner,repo:e.name,branch:await yt(this,R,A).call(this,t,e,r)};try{return(await r.rest.repos.getBranchProtection(a)).data}catch(i){return U(this,m).error(`[BranchProtections] fact extraction failed with: ${i}. Request parameters: ${JSON.stringify(a)}`),i instanceof ct.RequestError&&i.status===404?{}:void 0}}};let Y=L;m=new WeakMap,w=new WeakMap,R=new WeakSet,A=async function(t,e,r){return t.branch?t.branch:U(this,w).getDefaultBranch(e,r)},Y.factory=({logger:t,branchHelper:e})=>{const r=new L(t,e);return{predicate:a=>a.type==="BranchProtections",extractor:r}};var K=(t,e,r)=>{if(!e.has(t))throw TypeError("Cannot "+r)},$t=(t,e,r)=>(K(t,e,"read from private field"),r?r.call(t):e.get(t)),bt=(t,e,r)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,r)},Et=(t,e,r,a)=>(K(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),y;const Q=class{constructor(t){bt(this,y,void 0),Et(this,y,t)}async extract(t,e,r){const a={owner:e.owner,repo:e.name};try{return(await r.rest.repos.get(a)).data}catch(i){$t(this,y).error(`[RepositoryDetails] fact extraction failed with: ${i}. Request parameters: ${JSON.stringify(a)}`);return}}};let V=Q;y=new WeakMap,V.factory=({logger:t})=>{const e=new Q(t);return{predicate:r=>r.type==="RepositoryDetails",extractor:e}};const xt=s.BaseFactExtractorSchema.and(v.z.object({type:v.z.string().regex(new RegExp("BranchProtections"))})),Ct=s.BaseFactExtractorSchema.and(v.z.object({type:v.z.string().regex(new RegExp("RepositoryDetails"))})),kt=xt.or(Ct),St=s.buildFactCollectorSchema(kt);var q=(t,e,r)=>{if(!e.has(t))throw TypeError("Cannot "+r)},P=(t,e,r)=>(q(t,e,"read from private field"),r?r.call(t):e.get(t)),T=(t,e,r)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,r)},B=(t,e,r,a)=>(q(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),X=(t,e,r)=>(q(t,e,"access private method"),r),h,$,b,D;const Z=class{constructor(t,e){T(this,b),T(this,h,void 0),T(this,$,void 0);var r;B(this,$,t),B(this,h,X(this,b,D).call(this,e)),(r=e.subscribe)==null||r.call(e,()=>{B(this,h,X(this,b,D).call(this,e))})}static create(t,e){return new Z(t,e)}getExtractorConfig(t){return P(this,h)[t]}getExtractorConfigs(){return Object.values(P(this,h))}};let Wt=Z;h=new WeakMap,$=new WeakMap,b=new WeakSet,D=function(t){const e=t.getOptional("soundcheck.collectors.github");if(!e)return{};const r=St.safeParse(e);if(!r.success)throw P(this,$).error("Failed to parse GithubFactCollector from schema."),new ht.InputError(r.error.message);return s.buildExtractorConfigMap(r.data)};var tt=(t,e,r)=>{if(!e.has(t))throw TypeError("Cannot "+r)},O=(t,e,r)=>(tt(t,e,"read from private field"),r?r.call(t):e.get(t)),et=(t,e,r)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,r)},rt=(t,e,r,a)=>(tt(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),E,u;const at=class{constructor(t,e){et(this,E,void 0),et(this,u,void 0),rt(this,E,t),rt(this,u,e)}static create(t,e){return new at(t,e)}async getDefaultBranch(t,e){let r=await O(this,u).get(t.full_name);if(r!==void 0)return r;const a={owner:t.owner,repo:t.name};try{r=(await e.rest.repos.get(a)).data.default_branch,await O(this,u).set(t.full_name,r)}catch(i){t.ref?r=t.ref:r="main",O(this,E).error(`Failed to get default branch name with: ${i}. Request parameters: ${JSON.stringify(a)}. Falling back to using [${r}] as default branch`)}return r}};let Ft=at;E=new WeakMap,u=new WeakMap;var _=(t,e,r)=>{if(!e.has(t))throw TypeError("Cannot "+r)},o=(t,e,r)=>(_(t,e,"read from private field"),r?r.call(t):e.get(t)),f=(t,e,r)=>{if(e.has(t))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(t):e.set(t,r)},p=(t,e,r,a)=>(_(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),Mt=(t,e,r)=>(_(t,e,"access private method"),r),c,g,x,C,l,j,it;const G=class{constructor(t,e,r){f(this,j),f(this,c,void 0),f(this,g,void 0),f(this,x,void 0),f(this,C,void 0),f(this,l,void 0),this.id=G.ID,p(this,c,e.child({target:this.id})),p(this,g,N.ScmIntegrations.fromConfig(t)),p(this,x,N.DefaultGithubCredentialsProvider.fromIntegrations(o(this,g))),p(this,C,wt.create({logger:o(this,c),branchHelper:Ft.create(o(this,c),r.getClient({defaultTtl:60*60*1e3})),factories:[Y.factory,V.factory]})),p(this,l,Wt.create(o(this,c),t))}static create(t,e,r){return new G(t,e,r)}async collect(t,e){const r=Mt(this,j,it).call(this,e);return Promise.all(t.filter(a=>s.isScmEntity(a)).map(async a=>{const i=nt.stringifyEntityRef(a),n=s.getEntityScmUrl(a),k=ut.default(n),S=await this.getOctokit(n);return Promise.all(r.map(async z=>{const H=s.getFactRef(this.id,z),I=await o(this,C).extract(z,k,S).catch(st=>{o(this,c).error(`Failed to collect ${H} fact data for ${i} entity: ${st}`)});if(I!==void 0)return s.buildFact(i,H,I)}))})).then(a=>a.flat().filter(i=>!!i))}async getOctokit(t){const{token:e}=await o(this,x).getCredentials({url:t}),r=o(this,g).github.byUrl(t);return new lt.Octokit({auth:e,baseUrl:r==null?void 0:r.config.apiBaseUrl})}async getCollectionConfigs(){return s.buildCollectionConfigs(this.id,o(this,l).getExtractorConfigs())}async getFactNames(){return o(this,l).getExtractorConfigs().map(t=>t.factName)}async getDataSchema(t){}};let ot=G;c=new WeakMap,g=new WeakMap,x=new WeakMap,C=new WeakMap,l=new WeakMap,j=new WeakSet,it=function(t){return t!=null&&t.factRefs?o(this,l).getExtractorConfigs().filter(e=>{var r,a;return(a=(r=t==null?void 0:t.factRefs)==null?void 0:r.map(s.stringifyFactRef))==null?void 0:a.includes(s.getFactRef(this.id,e))}):o(this,l).getExtractorConfigs()},ot.ID="github",exports.GithubFactCollector=ot;
|
|
2
|
+
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FactRef, Fact, CollectionConfig } from '@spotify/backstage-plugin-soundcheck-common';
|
|
2
|
+
import { Entity } from '@backstage/catalog-model';
|
|
3
|
+
import { Logger } from 'winston';
|
|
4
|
+
import { Config } from '@backstage/config';
|
|
5
|
+
import { PluginCacheManager } from '@backstage/backend-common';
|
|
6
|
+
import { FactCollector } from '@spotify/backstage-plugin-soundcheck-node';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Soundcheck 3rd party integration - Github.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
declare class GithubFactCollector implements FactCollector {
|
|
14
|
+
#private;
|
|
15
|
+
/**
|
|
16
|
+
* Source identifier used by the {@link GithubFactCollector}.
|
|
17
|
+
*/
|
|
18
|
+
static ID: string;
|
|
19
|
+
static create(config: Config, logger: Logger, cache: PluginCacheManager): GithubFactCollector;
|
|
20
|
+
/** {@inheritDoc @spotify/backstage-plugin-soundcheck-node#FactCollector.id} */
|
|
21
|
+
id: string;
|
|
22
|
+
private constructor();
|
|
23
|
+
/** {@inheritDoc @spotify/backstage-plugin-soundcheck-node#FactCollector.collect} */
|
|
24
|
+
collect(entities: Entity[], params?: {
|
|
25
|
+
factRefs?: FactRef[];
|
|
26
|
+
refresh?: FactRef[];
|
|
27
|
+
}): Promise<Fact[]>;
|
|
28
|
+
/** {@inheritDoc @spotify/backstage-plugin-soundcheck-node#FactCollector.getCollectionConfigs} */
|
|
29
|
+
getCollectionConfigs(): Promise<CollectionConfig[]>;
|
|
30
|
+
/** {@inheritDoc @spotify/backstage-plugin-soundcheck-node#FactCollector.getFactNames} */
|
|
31
|
+
getFactNames(): Promise<string[]>;
|
|
32
|
+
/** {@inheritDoc @spotify/backstage-plugin-soundcheck-node#FactCollector.getDataSchema} */
|
|
33
|
+
getDataSchema(_factRef: FactRef): Promise<string | undefined>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { GithubFactCollector };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spotify/backstage-plugin-soundcheck-backend-module-github",
|
|
3
|
+
"description": "Soundcheck 3rd party integration with Github",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
|
+
"homepage": "https://backstage.spotify.com/",
|
|
7
|
+
"main": "dist/index.cjs.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"main": "dist/index.cjs.js",
|
|
11
|
+
"types": "dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"backstage": {
|
|
14
|
+
"role": "node-library"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "backstage-cli package build --minify",
|
|
18
|
+
"lint": "backstage-cli package lint",
|
|
19
|
+
"test": "backstage-cli package test",
|
|
20
|
+
"clean": "backstage-cli package clean",
|
|
21
|
+
"prepack": "backstage-cli package prepack",
|
|
22
|
+
"postpack": "backstage-cli package postpack"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@backstage/cli": "^0.22.4",
|
|
26
|
+
"@types/git-url-parse": "^9.0.0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@backstage/backend-common": "^0.18.3",
|
|
30
|
+
"@backstage/catalog-model": "^1.2.1",
|
|
31
|
+
"@backstage/config": "^1.0.7",
|
|
32
|
+
"@backstage/errors": "^1.1.5",
|
|
33
|
+
"@backstage/integration": "^1.4.3",
|
|
34
|
+
"@backstage/types": "^1.0.2",
|
|
35
|
+
"@octokit/request-error": "^3.0.3",
|
|
36
|
+
"@octokit/rest": "^19.0.3",
|
|
37
|
+
"@spotify/backstage-plugin-soundcheck-common": "^0.4.0",
|
|
38
|
+
"@spotify/backstage-plugin-soundcheck-node": "^0.2.0",
|
|
39
|
+
"git-url-parse": "^13.0.0",
|
|
40
|
+
"lodash": "^4.17.21",
|
|
41
|
+
"winston": "^3.2.1",
|
|
42
|
+
"zod": "^3.20.0"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist",
|
|
46
|
+
"!dist/**/*.map"
|
|
47
|
+
]
|
|
48
|
+
}
|