Skip to content

Commit

Permalink
feat(tooling): introduce separate publish step which runs after build (
Browse files Browse the repository at this point in the history
  • Loading branch information
shiftkey committed Sep 2, 2024
1 parent c9c0064 commit dbcf824
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 15 deletions.
80 changes: 66 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'linux-release-*'
tags:
- 'release-*.*.*-linux*'
- 'release-*.*.*-test*'
pull_request:
branches:
- linux
Expand Down Expand Up @@ -194,20 +195,6 @@ jobs:
- name: Package application
run: yarn run package
if: ${{ matrix.friendlyName == 'Ubuntu' }}
- name: Create Release
uses: softprops/action-gh-release@v1
if:
${{ matrix.friendlyName == 'Ubuntu' && startsWith(github.ref,
'refs/tags/') }}
with:
files: |
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.txt
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload output artifacts
uses: actions/upload-artifact@v3
if: matrix.friendlyName == 'Ubuntu'
Expand All @@ -217,4 +204,69 @@ jobs:
dist/*.AppImage
dist/*.deb
dist/*.rpm
dist/*.sha256
retention-days: 5

publish:
name: Create GitHub release
needs: [build, lint]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- uses: actions/checkout@v3

- name: Use Node.js 18.14.0
uses: actions/setup-node@v3
with:
node-version: 18.14.0
cache: yarn

- name: Download all artifacts
uses: actions/download-artifact@v2
with:
path: './artifacts'

- name: Display structure of downloaded files
run: ls -R
working-directory: './artifacts'

- name: Get tag name without prefix
run: |
RELEASE_TAG=${GITHUB_REF/refs\/tags\//}
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
tagNameWithoutPrefix="${RELEASE_TAG:1}"
echo "RELEASE_TAG_WITHOUT_PREFIX=${tagNameWithoutPrefix}" >> $GITHUB_ENV
# TODO: generate release notes
# - pull in default if version matches X.Y.Z-linux1
# - otherwise stub template

- name: Generate release notes
run: |
node -v
yarn
node -r ts-node/register script/generate-release-notes.ts "${{ github.workspace }}/artifacts" "${{ env.RELEASE_TAG_WITHOUT_PREFIX }}"
RELEASE_NOTES_FILE=script/release_notes.txt
if [[ ! -f "$RELEASE_NOTES_FILE" ]]; then
echo "$RELEASE_NOTES_FILE does not exist. Something might have gone wrong while generating the release notes."
exit 1
fi
echo "Release notes:"
echo "---"
cat ${RELEASE_NOTES_FILE}
echo "---"
- name: Create Release
uses: softprops/action-gh-release@v2
with:
name: GitHub Desktop for Linux ${{ env.RELEASE_TAG_WITHOUT_PREFIX }}
body_path: script/release_notes.txt
files: |
artifacts/*.AppImage
artifacts/*.deb
artifacts/*.rpm
draft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/release-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
private_key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }}

- name: Create Release Pull Request
uses: peter-evans/create-pull-request@v6.0.5
uses: peter-evans/create-pull-request@v6.1.0
if: |
startsWith(github.ref, 'refs/heads/releases/') && !contains(github.ref, 'test')
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ vendor/windows-argv-parser/build/
junit*.xml
*.swp
tslint-rules/
script/release_notes.txt
230 changes: 230 additions & 0 deletions script/generate-release-notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
/* eslint-disable no-sync */

const glob = require('glob')
const { dirname, join } = require('path')
const fs = require('fs')

type ReleaseNotesGroupType = 'new' | 'added' | 'fixed' | 'improved' | 'removed'

type ReleaseNotesGroups = Record<ReleaseNotesGroupType, Array<ReleaseNoteEntry>>

type ReleaseNoteEntry = {
text: string
ids: Array<number>
contributor?: string
}

// 3 architectures * 3 package formats * 2 files (package + checksum file)
const SUCCESSFUL_RELEASE_FILE_COUNT = 3 * 3 * 2

const Glob = glob.GlobSync

const args = process.argv.slice(2)
const artifactsDir = args[0]

if (!artifactsDir) {
console.error(
`🔴 First parameter with artifacts directory not found. Aborting...`
)
process.exit(1)
}

const releaseTagWithoutPrefix = args[1]
if (!releaseTagWithoutPrefix) {
console.error(`🔴 Second parameter with release tag not found. Aborting...`)
process.exit(1)
}

console.log(
`Preparing release notes for release tag ${releaseTagWithoutPrefix}`
)

const files = new Glob(artifactsDir + '/**/*', { nodir: true })

const matches = files.found as Array<string>

const fileCount = matches.length

if (SUCCESSFUL_RELEASE_FILE_COUNT !== fileCount) {
console.error(
`🔴 Artifacts folder has ${fileCount} assets, expecting ${SUCCESSFUL_RELEASE_FILE_COUNT}. Please check the GH Actions artifacts to see which are missing.`
)
process.exit(1)
}

console.log(`Found ${fileCount} files in artifacts directory`)

const releaseNotesByGroup = getReleaseGroups(releaseTagWithoutPrefix)

const draftReleaseNotes = generateDraftReleaseNotes(releaseNotesByGroup)
const releaseNotesPath = join(__dirname, 'release_notes.txt')

fs.writeFileSync(releaseNotesPath, draftReleaseNotes, { encoding: 'utf8' })

console.log(
`✅ All done! The release notes have been written to ${releaseNotesPath}`
)

function extractIds(str: string): Array<number> {
const idRegex = /#(\d+)/g

const idArray = new Array<number>()
let match

while ((match = idRegex.exec(str))) {
const textValue = match[1].trim()
const numValue = parseInt(textValue, 10)
if (!isNaN(numValue)) {
idArray.push(numValue)
}
}

return idArray
}

function parseCategory(str: string): ReleaseNotesGroupType | null {
const input = str.toLocaleLowerCase()
switch (input) {
case 'added':
case 'fixed':
case 'improved':
case 'new':
case 'removed':
return input
default:
return null
}
}

function isInitialTag(tag: string): boolean {
return tag.endsWith('-linux1') || tag.endsWith('-test1')
}

function getVersionWithoutSuffix(tag: string): string {
return tag.replace('-linux1', '').replace('-test1', '')
}

function getReleaseGroups(version: string): ReleaseNotesGroups {
if (!isInitialTag(version)) {
return {
new: [],
added: [],
fixed: [],
improved: [],
removed: [],
}
}

const upstreamVersion = getVersionWithoutSuffix(version)
const rootDir = dirname(__dirname)
const changelogFile = fs.readFileSync(join(rootDir, 'changelog.json'))
const changelogJson = JSON.parse(changelogFile)
const releases = changelogJson['releases']
const changelogForVersion: Array<string> | undefined =
releases[upstreamVersion]

if (!changelogForVersion) {
console.error(
`🔴 Changelog version ${upstreamVersion} not found in changelog.json, which is required for publishing a release based off an upstream releease. Aborting...`
)
process.exit(1)
}

console.log(`found release notes`, changelogForVersion)

const releaseNotesByGroup: ReleaseNotesGroups = {
new: [],
added: [],
fixed: [],
improved: [],
removed: [],
}

const releaseEntryExternalContributor = /\[(.*)\](.*)- (.*)\. Thanks (.*)!/
const releaseEntryRegex = /\[(.*)\](.*)- (.*)/

for (const entry of changelogForVersion) {
const externalMatch = releaseEntryExternalContributor.exec(entry)
if (externalMatch) {
const category = parseCategory(externalMatch[1])
const text = externalMatch[2].trim()
const ids = extractIds(externalMatch[3])
const contributor = externalMatch[4]

if (!category) {
console.warn(`unable to identify category for '${entry}'`)
} else {
releaseNotesByGroup[category].push({
text,
ids,
contributor,
})
}
} else {
const match = releaseEntryRegex.exec(entry)
if (match) {
const category = parseCategory(match[1])
const text = match[2].trim()
const ids = extractIds(match[3])
if (!category) {
console.warn(`unable to identify category for '${entry}'`)
} else {
releaseNotesByGroup[category].push({
text,
ids,
})
}
} else {
console.warn(`release entry does not match any format: '${entry}'`)
}
}
}

return releaseNotesByGroup
}

function formatReleaseNote(note: ReleaseNoteEntry): string {
const idsAsUrls = note.ids
.map(id => `https://github.com/desktop/desktop/issues/${id}`)
.join(' ')
const contributorNote = note.contributor
? `. Thanks ${note.contributor}!`
: ''

const template = ` - ${note.text} - ${idsAsUrls}${contributorNote}`

return template.trim()
}

function renderSection(
name: string,
items: Array<ReleaseNoteEntry>,
omitIfEmpty: boolean = true
): string {
if (items.length === 0 && omitIfEmpty) {
return ''
}

const itemsText =
items.length === 0 ? 'TODO' : items.map(formatReleaseNote).join('\n')

return `
## ${name}
${itemsText}
`
}

/**
* Takes the release notes entries and the SHA entries, then merges them into the full draft release notes ✨
*/
function generateDraftReleaseNotes(
releaseNotesGroups: ReleaseNotesGroups
): string {
return `
${renderSection('New', releaseNotesGroups.new)}
${renderSection('Added', releaseNotesGroups.added)}
${renderSection('Fixed', releaseNotesGroups.fixed, false)}
${renderSection('Improved', releaseNotesGroups.improved, false)}
${renderSection('Removed', releaseNotesGroups.removed)}`
}
3 changes: 3 additions & 0 deletions script/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ async function generateChecksums(files: Array<string>) {
for (const [fullPath, checksum] of checksums) {
const fileName = path.basename(fullPath)
checksumsText += `${checksum} - ${fileName}\n`

const checksumFilePath = `${fullPath}.sha256`
await writeFile(checksumFilePath, checksum)
}

const checksumFile = path.join(distRoot, 'checksums.txt')
Expand Down

0 comments on commit dbcf824

Please sign in to comment.