Skip to content

Commit 7cfd1a6

Browse files
committed
chore: build: release.nightly github workflow
1 parent 613d222 commit 7cfd1a6

File tree

3 files changed

+295
-1
lines changed

3 files changed

+295
-1
lines changed

.github/workflows/nightly-release.yml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: nightly-release
2+
3+
on:
4+
workflow_dispatch
5+
6+
jobs:
7+
build-and-sign:
8+
runs-on: ubuntu-latest
9+
env:
10+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11+
WEB_EXT_API_KEY: ${{ secrets.WEB_EXT_API_KEY }}
12+
WEB_EXT_API_SECRET: ${{ secrets.WEB_EXT_API_SECRET }}
13+
steps:
14+
- name: Check out the repository to the runner
15+
uses: actions/checkout@v4
16+
with:
17+
ref: 'v5'
18+
fetch-depth: 0
19+
- name: Setup nodejs
20+
uses: actions/setup-node@v4
21+
with:
22+
node-version: lts/*
23+
- name: Configure git
24+
run: |
25+
git config --global user.name 'GitHub Action Bot'
26+
git config --global user.email '[email protected]'
27+
- name: Build and sign
28+
run: |
29+
npm install
30+
npm run release.nightly

build/release-nightly.mjs

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/* eslint no-console: off */
2+
3+
import fs from 'fs/promises'
4+
import { Blob } from 'buffer'
5+
import { execSync } from 'child_process'
6+
7+
const OWNER = 'mbnuqw'
8+
const REPO = 'sidebery'
9+
const BRANCH = 'v5'
10+
const MAX_ASSETS_COUNT = 3
11+
const ADDON_ID = '{3c078156-979c-498b-8990-85f7987dd929}'
12+
const CONSIDERED_COMMIT_PREFIXES_RE = /^(fix|feat|perf)/
13+
const ASSET_RE = /sidebery-(\d\d?\.\d\d?\.\d\d?\.\d?\d?\d?)\.xpi/
14+
15+
const gitLogFlags = `--date-order --abbrev-commit --decorate --format=format:'%H::%s' ${BRANCH}`
16+
const updatesGitlogFlags = `--date-order --format=format:'%H' -n 1 ${BRANCH} -- ./updates.json`
17+
18+
async function main() {
19+
console.log('')
20+
21+
// Check availability of secrets
22+
console.log('Checking availability of secrets...')
23+
if (!process.env.GITHUB_TOKEN) throw 'No GITHUB_TOKEN'
24+
if (!process.env.WEB_EXT_API_KEY) throw 'No WEB_EXT_API_KEY'
25+
if (!process.env.WEB_EXT_API_SECRET) throw 'No WEB_EXT_API_SECRET'
26+
27+
// Get info from manifest.json and updates.json
28+
console.log('Getting info from manifest.json and updates.json...')
29+
const amoVersion = await getAMOVersion()
30+
const updatesContent = await readUpdates()
31+
const updates = parseUpdates(updatesContent)
32+
const lastVerDigits = getLastVersionDigits(updates, amoVersion)
33+
const newVersion = getNewVersion(lastVerDigits)
34+
console.log('New version:', newVersion)
35+
36+
// Get info from git
37+
console.log('Getting info from git...')
38+
const updatesLastCommit = execSync(`git log ${updatesGitlogFlags}`, { encoding: 'utf-8' })
39+
console.log('Last commit of "updates.json":', updatesLastCommit)
40+
const gitlogResult = execSync(`git log ${gitLogFlags}`, { encoding: 'utf-8' })
41+
const noChanges = !hasUsefullCommitsSinceLastUpdate(gitlogResult, updatesLastCommit)
42+
if (noChanges) throw 'No changes'
43+
44+
// Build and sign
45+
console.log('Building and signing...')
46+
execSync(`node ./build/addon.mjs --sign ${newVersion}`)
47+
48+
// Get the last github release
49+
console.log('Getting the last github release...')
50+
const ghRelease = await getGHRelease(amoVersion)
51+
52+
// Remove the third version of asset leaving the last two
53+
await removeOldestAsset(ghRelease)
54+
55+
// Upload the new version as an asset and get the direct link for that .xpi file
56+
const newVersionLink = await uploadNewVersion(ghRelease, newVersion)
57+
if (!newVersionLink) throw 'No link for new version'
58+
59+
// Update the 'updates.json' and 'README.md' files
60+
console.log('Updating "updates.json" and "README.md"...')
61+
await updateFiles(updates, newVersion, newVersionLink)
62+
63+
// Commit changes ('updates.json' and 'README.md') and push
64+
console.log('Commiting and pushing...')
65+
execSync('git add updates.json README.md', { encoding: 'utf-8', stdio: 'inherit' })
66+
execSync(`git commit -m "chore: v${newVersion} nightly update"`, {
67+
encoding: 'utf-8',
68+
stdio: 'inherit',
69+
})
70+
execSync('git push', { encoding: 'utf-8', stdio: 'inherit' })
71+
}
72+
73+
async function getAMOVersion() {
74+
let content
75+
try {
76+
content = await fs.readFile('./src/manifest.json', { encoding: 'utf-8' })
77+
} catch {
78+
throw 'Cannot read ./src/manifest.json'
79+
}
80+
81+
let manifest
82+
try {
83+
manifest = JSON.parse(content)
84+
} catch {
85+
throw 'Cannot parse manifest'
86+
}
87+
88+
const amoVersion = manifest?.version
89+
if (!amoVersion) throw 'Wrong AMO version'
90+
91+
return amoVersion
92+
}
93+
94+
async function readUpdates() {
95+
let content
96+
try {
97+
content = await fs.readFile('./updates.json', { encoding: 'utf-8' })
98+
} catch {
99+
throw 'Cannot read updates.json'
100+
}
101+
102+
if (!content) throw 'updates.json is empty'
103+
104+
return content
105+
}
106+
107+
function parseUpdates(updatesContent) {
108+
let updates
109+
try {
110+
updates = JSON.parse(updatesContent)
111+
} catch {
112+
throw 'Cannot parse updates.json'
113+
}
114+
115+
const versions = updates?.addons?.[ADDON_ID]?.updates
116+
if (!versions || !versions.length) throw 'Wrong format of updates.json'
117+
118+
return updates
119+
}
120+
121+
function getLastVersionDigits(updates, amoVersion) {
122+
const versions = updates.addons[ADDON_ID].updates
123+
const lastVerInfo = versions[versions.length - 1]
124+
let lastVer = lastVerInfo.version
125+
if (typeof lastVer !== 'string') throw 'Wrong type of last version'
126+
127+
if (!lastVer.startsWith(amoVersion)) {
128+
lastVer = amoVersion + '.0'
129+
}
130+
131+
const verDigits = lastVer.split('.').map(n => parseInt(n))
132+
if (!verDigits || verDigits.length !== 4 || verDigits.some(n => isNaN(n))) {
133+
throw 'Wrong format of last version'
134+
}
135+
136+
return verDigits
137+
}
138+
139+
function getNewVersion(lastVersionDigits) {
140+
const ver = [...lastVersionDigits]
141+
ver[3]++
142+
return ver.join('.')
143+
}
144+
145+
function hasUsefullCommitsSinceLastUpdate(gitlogString, updatesCommit) {
146+
const gitlog = gitlogString.split('\n')
147+
148+
for (const line of gitlog) {
149+
const commit = line.trim()
150+
151+
if (commit.startsWith(updatesCommit)) break
152+
153+
let [, subject] = commit.split('::')
154+
subject = subject.trim()
155+
if (CONSIDERED_COMMIT_PREFIXES_RE.test(subject)) return true
156+
}
157+
158+
return false
159+
}
160+
161+
async function getGHRelease(amoVersion) {
162+
const fetchUrl = `https://api.github.com/repos/${OWNER}/${REPO}/releases/tags/v${amoVersion}`
163+
const response = await fetch(fetchUrl, {
164+
headers: {
165+
Accept: 'application/vnd.github+json',
166+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
167+
'X-GitHub-Api-Version': '2022-11-28',
168+
},
169+
})
170+
171+
try {
172+
return await response.json()
173+
} catch {
174+
throw 'getLastGHRelease: Cannot parse response json'
175+
}
176+
}
177+
178+
async function removeOldestAsset(ghRelease) {
179+
const assets = ghRelease.assets
180+
if (assets.length < MAX_ASSETS_COUNT) return
181+
182+
let rmID
183+
let rmName
184+
let minN = 999
185+
for (const asset of assets) {
186+
const result = ASSET_RE.exec(asset.name)
187+
if (!result || !result[1]) continue
188+
const assetVer = result[1]
189+
const digits = assetVer.split('.').map(n => parseInt(n))
190+
if (digits.length !== 4 || digits.some(n => isNaN(n))) continue
191+
if (digits[3] < minN) {
192+
minN = digits[3]
193+
rmID = asset.id
194+
rmName = asset.name
195+
}
196+
}
197+
198+
if (rmID === undefined || rmName === undefined) return
199+
200+
console.log(`Removing last asset if there are more than ${MAX_ASSETS_COUNT - 1}: ${rmName}...`)
201+
202+
await fetch(`https://api.github.com/repos/${OWNER}/${REPO}/releases/assets/${rmID}`, {
203+
method: 'DELETE',
204+
headers: {
205+
Accept: 'application/vnd.github+json',
206+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
207+
'X-GitHub-Api-Version': '2022-11-28',
208+
},
209+
})
210+
}
211+
212+
async function uploadNewVersion(ghRelease, newVersion) {
213+
const fileName = `sidebery-${newVersion}.xpi`
214+
const filePath = `./dist/${fileName}`
215+
const file = await fs.readFile(filePath)
216+
217+
const url = `https://uploads.github.com/repos/${OWNER}/${REPO}/releases/${ghRelease.id}/assets?name=${fileName}`
218+
console.log(`Uploading "${fileName}"...`)
219+
const response = await fetch(url, {
220+
method: 'POST',
221+
headers: {
222+
Accept: 'application/vnd.github+json',
223+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
224+
'X-GitHub-Api-Version': '2022-11-28',
225+
// 'Content-Type': 'application/octet-stream',
226+
'Content-Type': 'application/x-xpinstall',
227+
},
228+
body: new Blob([file]),
229+
})
230+
231+
let newAsset
232+
try {
233+
newAsset = await response.json()
234+
} catch {
235+
throw 'uploadNewVersion: Cannot parse response json'
236+
}
237+
238+
return newAsset.browser_download_url
239+
}
240+
241+
async function updateFiles(updates, newVersion, newVersionLink) {
242+
// Create new version for updates.json
243+
updates.addons[ADDON_ID].updates.push({
244+
version: newVersion,
245+
update_link: newVersionLink,
246+
})
247+
const updatesJSON = JSON.stringify(updates, undefined, ' ')
248+
await fs.writeFile('./updates.json', updatesJSON, { encoding: 'utf-8' })
249+
250+
// Update README.md
251+
let readmeContent = await fs.readFile('README.md', { encoding: 'utf-8' })
252+
readmeContent = readmeContent.replace(
253+
/\*\*github\.com\*\* \(v\d\.\d\d?\.\d\d?\.\d\d?\d?\):/g,
254+
`**github.com** (v${newVersion}):`
255+
)
256+
readmeContent = readmeContent.replace(
257+
/\[Install\]\(https:\/\/github\.com\/mbnuqw\/sidebery\/releases\/download.*\)/g,
258+
`[Install](${newVersionLink})`
259+
)
260+
await fs.writeFile('./README.md', readmeContent, { encoding: 'utf-8' })
261+
}
262+
263+
await main()

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
"clear": "rm -rf addon",
2222
"lint": "eslint ./src && web-ext lint -s ./addon",
2323
"changelog": "node build/changelog.mjs",
24+
"addon": "node build/addon.mjs",
2425
"release-notes": "node build/release-notes.mjs",
25-
"addon": "node build/addon.mjs"
26+
"release.nightly": "node build/release-nightly.mjs"
2627
},
2728
"author": "mbnuqw",
2829
"license": "MIT",

0 commit comments

Comments
 (0)