Skip to content

Commit 48e0dd5

Browse files
committed
feat: deploy issue templates to repositories
1 parent cb39ae2 commit 48e0dd5

File tree

7 files changed

+262
-14
lines changed

7 files changed

+262
-14
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
package.json
2+
assets
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# companion-common-config-deploy
22

33
Ensures every module repository has some common config/tooling
4+
5+
Repositories can opt out of the automatic issue templates by creating a `.github/.companion-manual-issue-templates` file
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Bug report
2+
description: Create a bug report to help us improve.
3+
labels:
4+
- BUG
5+
title: '[BUG]'
6+
body:
7+
- type: checkboxes
8+
attributes:
9+
label: Make sure you have updated to the latest version of the module
10+
description: This may be a beta version of the module, or may require you to update your companion.
11+
options:
12+
- label: I have checked this on the latest version of the module
13+
required: true
14+
- type: checkboxes
15+
attributes:
16+
label: Is there an existing issue for this?
17+
description: Please search to see if an issue already exists for the bug you encountered.
18+
options:
19+
- label: I have searched the existing issues
20+
required: true
21+
- type: textarea
22+
attributes:
23+
label: Describe the bug
24+
description: A clear and concise description of what the bug is. Screenshots are always helpful to understand the problem.
25+
validations:
26+
required: false
27+
- type: textarea
28+
attributes:
29+
label: Steps To Reproduce
30+
description: Steps to reproduce the behavior.
31+
placeholder: |
32+
1. Go to '...'
33+
2. Click on '....'
34+
3. Scroll down to '....'
35+
4. See error
36+
validations:
37+
required: false
38+
- type: textarea
39+
attributes:
40+
label: Expected Behavior
41+
description: A concise description of what you expected to happen.
42+
validations:
43+
required: false
44+
- type: textarea
45+
attributes:
46+
label: Environment (please complete the following information)
47+
description: |
48+
examples:
49+
- **OS**: Windows 10
50+
- **Browser**: Chrome 94
51+
- **Companion Version**: 2.2.0-b177f8fb-3722
52+
- **Module Version**: 1.0.0
53+
- **Device Firmware/Software Version (If applicable)**: 3.0.6
54+
- **Device Model (If applicable)**: My Device PRO
55+
value: |
56+
- OS:
57+
- Browser:
58+
- Companion Version:
59+
- Module Version:
60+
- Device Firmware/Software Version (If applicable):
61+
- Device Model (If applicable):
62+
render: markdown
63+
validations:
64+
required: false
65+
- type: textarea
66+
attributes:
67+
label: Additional context
68+
description: Add any other context about the problem here.
69+
validations:
70+
required: false
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: BUG OR FEATURE REQUEST FOR COMPANION ITSELF
4+
url: https://github.com/bitfocus/companion/issues
5+
about: Report it against the companion itself, to get the relevant people notified.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Feature Request
2+
description: Request a new feature within this module
3+
title: '[Feature]: '
4+
labels:
5+
- enhancement
6+
body:
7+
- type: checkboxes
8+
attributes:
9+
label: Make sure you have updated to the latest version of the module
10+
description: This may be a beta version of the module, or may require you to update your companion.
11+
options:
12+
- label: I have checked this on the latest version of the module
13+
required: true
14+
- type: checkboxes
15+
attributes:
16+
label: Is there an existing issue for this?
17+
description: Please search to see if an issue already exists for the feature.
18+
options:
19+
- label: I have searched for similar existing issues
20+
required: true
21+
- type: textarea
22+
attributes:
23+
label: Describe the feature
24+
description: A clear and concise description of what the feature does. Mocked up screenshots (no matter how rough) are always helpful in understanding.
25+
validations:
26+
required: false
27+
- type: textarea
28+
attributes:
29+
label: Usecases
30+
description: Add a couple of usecases for why this should be implemented and when/why to use it.
31+
validations:
32+
required: false
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Companion Module Checks
2+
3+
on:
4+
push:
5+
6+
jobs:
7+
check:
8+
name: Check module
9+
10+
if: ${{ !contains(github.repository, 'companion-module-template-') }}
11+
12+
permissions:
13+
packages: read
14+
15+
uses: bitfocus/actions/.github/workflows/module-checks.yaml@main
16+
# with:
17+
# upload-artifact: true # uncomment this to upload the built package as an artifact to this workflow that you can download and share with others
18+

module-common-config-deploy/main.mjs

Lines changed: 134 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Octokit } from 'octokit'
22
import dotenv from 'dotenv'
33
import PLazy from 'p-lazy'
4+
import fs from 'fs/promises'
5+
import path from 'path'
46
dotenv.config()
57

68
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN })
@@ -10,20 +12,30 @@ const {
1012
} = await octokit.rest.users.getAuthenticated()
1113
console.log('Hello, %s', login)
1214

13-
const targetContentBase64 = PLazy.from(async () => {
14-
const checkManifestExists = await octokit.rest.repos.getContent({
15-
owner: 'bitfocus',
16-
repo: 'companion-module-template-ts',
17-
path: '.github/workflows/companion-module-checks.yaml',
18-
})
19-
if (!checkManifestExists || checkManifestExists.status !== 200) {
20-
throw new Error('Failed to get template workflow')
21-
}
15+
function base64Encode(str) {
16+
return Buffer.from(str, 'utf-8').toString('base64')
17+
}
18+
19+
const targetWorkflowContentBase64 = PLazy.from(async () => {
20+
const utf8Content = await fs.readFile(
21+
path.join(import.meta.dirname, './assets/companion-module-checks.yaml'),
22+
'utf-8'
23+
)
24+
return base64Encode(utf8Content)
25+
})
2226

23-
const data = checkManifestExists.data.content
24-
if (!data) throw new Error('Failed to get template workflow')
27+
const targetIssueTemplateContentBase64 = PLazy.from(async () => {
28+
const [bugFile, configFile, featureFile] = await Promise.all([
29+
fs.readFile(path.join(import.meta.dirname, './assets/ISSUE_TEMPLATE/bug_report.yml'), 'utf-8'),
30+
fs.readFile(path.join(import.meta.dirname, './assets/ISSUE_TEMPLATE/config.yml'), 'utf-8'),
31+
fs.readFile(path.join(import.meta.dirname, './assets/ISSUE_TEMPLATE/feature_request.yml'), 'utf-8'),
32+
])
2533

26-
return data
34+
return {
35+
bugFile: base64Encode(bugFile),
36+
configFile: base64Encode(configFile),
37+
featureFile: base64Encode(featureFile),
38+
}
2739
})
2840

2941
const errors = []
@@ -34,8 +46,10 @@ const allRepos = await octokit.paginate(octokit.rest.repos.listForOrg, {
3446
console.log('found %d repos in org', allRepos.length)
3547

3648
for (const repo of allRepos) {
49+
if (repo.archived) continue
50+
3751
const repoName = repo.name
38-
if (!repoName.startsWith('companion-module-')) {
52+
if (!repoName.startsWith('companion-module-') && !repoName.startsWith('companion-surface-')) {
3953
continue
4054
}
4155

@@ -69,14 +83,120 @@ for (const repo of allRepos) {
6983
owner: 'bitfocus',
7084
repo: repoName,
7185
path: '.github/workflows/companion-module-checks.yaml',
72-
content: await targetContentBase64,
86+
content: await targetWorkflowContentBase64,
7387
message: "chore: add 'Companion Module Checks' workflow",
7488
})
7589
}
90+
91+
const issueTemplateSetManual = await octokit.rest.repos
92+
.getContent({
93+
owner: 'bitfocus',
94+
repo: repoName,
95+
path: '.github/.companion-manual-issue-templates',
96+
})
97+
.then(() => true)
98+
.catch((e) => (e.status === 404 ? false : Promise.reject(e)))
99+
if (issueTemplateSetManual) {
100+
console.log(`Skipping ${repoName}: companion-manual-issue-templates flag set`)
101+
} else {
102+
// Check if the .github/ISSUE_TEMPLATE folder exists
103+
const issueTemplateFolderExists = await octokit.rest.repos
104+
.getContent({
105+
owner: 'bitfocus',
106+
repo: repoName,
107+
path: '.github/ISSUE_TEMPLATE',
108+
})
109+
.then(() => true)
110+
.catch((e) => (e.status === 404 ? false : Promise.reject(e)))
111+
if (issueTemplateFolderExists) {
112+
console.log(`Skipping ${repoName}: ISSUE_TEMPLATE folder already exists`)
113+
114+
await octokit.rest.repos.createOrUpdateFileContents({
115+
owner: 'bitfocus',
116+
repo: repoName,
117+
path: '.github/.companion-manual-issue-templates',
118+
content: base64Encode('\n'),
119+
message: 'chore: add manual issue templates marker',
120+
})
121+
} else {
122+
console.log(`Creating ${repoName}: ISSUE_TEMPLATE folder`)
123+
124+
const files = await targetIssueTemplateContentBase64
125+
126+
await updateMultipleFiles(repoName, repo.default_branch, {
127+
'.github/ISSUE_TEMPLATE/bug_report.yml': files.bugFile,
128+
'.github/ISSUE_TEMPLATE/config.yml': files.configFile,
129+
'.github/ISSUE_TEMPLATE/feature_request.yml': files.featureFile,
130+
})
131+
}
132+
}
76133
} catch (e) {
77134
console.error(`Failed ${repoName}: ${e?.message ?? e?.toString() ?? e}`)
78135
errors.push(e)
79136
}
80137
}
81138

82139
console.log('All errors', errors)
140+
141+
async function updateMultipleFiles(repoName, defaultBranch, files) {
142+
// Get reference to the default branch
143+
const { data: ref } = await octokit.rest.git.getRef({
144+
owner: 'bitfocus',
145+
repo: repoName,
146+
ref: `heads/${defaultBranch}`,
147+
})
148+
149+
const baseSha = ref.object.sha
150+
151+
// Get the base tree
152+
const { data: baseCommit } = await octokit.rest.git.getCommit({
153+
owner: 'bitfocus',
154+
repo: repoName,
155+
commit_sha: baseSha,
156+
})
157+
158+
// Create blobs for each file
159+
const blobs = await Promise.all(
160+
Object.entries(files).map(async ([path, content]) => {
161+
const { data: blob } = await octokit.rest.git.createBlob({
162+
owner: 'bitfocus',
163+
repo: repoName,
164+
content,
165+
encoding: 'base64',
166+
})
167+
return { path, sha: blob.sha }
168+
})
169+
)
170+
171+
// Create a new tree
172+
const { data: newTree } = await octokit.rest.git.createTree({
173+
owner: 'bitfocus',
174+
repo: repoName,
175+
base_tree: baseCommit.tree.sha,
176+
tree: blobs.map(({ path, sha }) => ({
177+
path,
178+
mode: '100644',
179+
type: 'blob',
180+
sha,
181+
})),
182+
})
183+
184+
// Create a new commit
185+
const { data: newCommit } = await octokit.rest.git.createCommit({
186+
owner: 'bitfocus',
187+
repo: repoName,
188+
message: 'chore: add issue templates',
189+
tree: newTree.sha,
190+
parents: [baseSha],
191+
})
192+
193+
// Update the reference
194+
await octokit.rest.git.updateRef({
195+
owner: 'bitfocus',
196+
repo: repoName,
197+
ref: `heads/${defaultBranch}`,
198+
sha: newCommit.sha,
199+
})
200+
201+
console.log(`Updated ${repoName} with ${Object.keys(files).length} files in a single commit`)
202+
}

0 commit comments

Comments
 (0)