Skip to content

release: prepare 0.1.24 #32

release: prepare 0.1.24

release: prepare 0.1.24 #32

Workflow file for this run

name: Release
on:
workflow_dispatch:
push:
tags:
- "app-v*"
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
create-release:
name: Create release
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
release_id: ${{ steps.release.outputs.release_id }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create or reuse one draft release
id: release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const tagName = context.ref.startsWith('refs/tags/')
? context.ref.replace('refs/tags/', '')
: `app-v${packageJson.version}`;
const version = tagName.replace(/^app-v/, '');
const owner = context.repo.owner;
const repo = context.repo.repo;
const releaseName = `PaperQuay v${version}`;
const targetCommitish = context.sha;
const releaseBodyTemplate = fs.existsSync('.github/release-notes.md')
? fs.readFileSync('.github/release-notes.md', 'utf8')
: 'Download installers from Assets.';
const releaseBody = releaseBodyTemplate.replaceAll('{{VERSION}}', version);
const releases = await github.paginate(github.rest.repos.listReleases, {
owner,
repo,
per_page: 100,
});
const exactMatches = releases
.filter((release) => release.tag_name === tagName)
.sort((left, right) => new Date(left.created_at) - new Date(right.created_at));
const untaggedVersionMatches = releases
.filter((release) => release.name === releaseName && /^untagged-/.test(release.tag_name))
.sort((left, right) => new Date(left.created_at) - new Date(right.created_at));
const matches = [...exactMatches, ...untaggedVersionMatches]
.filter((release, index, list) => list.findIndex((item) => item.id === release.id) === index);
let release =
exactMatches.find((item) => !item.draft) ??
untaggedVersionMatches.find((item) => !item.draft) ??
exactMatches[0] ??
untaggedVersionMatches[0];
if (release) {
const { data } = await github.rest.repos.updateRelease({
owner,
repo,
release_id: release.id,
tag_name: tagName,
target_commitish: targetCommitish,
name: releaseName,
body: releaseBody,
prerelease: false,
...(release.draft ? { draft: true } : {}),
});
release = data;
} else {
const { data } = await github.rest.repos.createRelease({
owner,
repo,
tag_name: tagName,
target_commitish: targetCommitish,
name: releaseName,
body: releaseBody,
draft: true,
prerelease: false,
});
release = data;
}
for (const duplicate of matches) {
if (duplicate.id !== release.id && duplicate.draft) {
await github.rest.repos.deleteRelease({
owner,
repo,
release_id: duplicate.id,
});
}
}
core.setOutput('release_id', String(release.id));
core.setOutput('tag_name', tagName);
core.setOutput('version', version);
build-electron:
name: Build ${{ matrix.name }}
needs: create-release
runs-on: ${{ matrix.platform }}
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- name: Windows x64
platform: windows-latest
args: "--win --x64 --publish never"
- name: Linux x64
platform: ubuntu-22.04
args: "--linux --x64 --publish never"
- name: macOS x64
platform: macos-15-intel
args: "--mac --x64 --publish never"
- name: macOS arm64
platform: macos-15
args: "--mac --arm64 --publish never"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Linux packaging dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools rpm
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Package Electron app
shell: bash
run: |
set -o pipefail
node electron/build.cjs ${{ matrix.args }} 2>&1 | tee electron-build.log
- name: Report Electron build failure
if: failure()
shell: bash
run: |
if [ -f electron-build.log ]; then
message="$(tail -n 120 electron-build.log)"
message="${message//'%'/'%25'}"
message="${message//$'\r'/'%0D'}"
message="${message//$'\n'/'%0A'}"
echo "::error title=Electron build failed::$message"
fi
- name: Upload Electron build log
if: failure()
uses: actions/upload-artifact@v4
with:
name: electron-build-log-${{ matrix.name }}
path: electron-build.log
if-no-files-found: ignore
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: paperquay-${{ matrix.name }}
path: |
release/*.exe
release/*.msi
release/*.dmg
release/*.zip
release/*.AppImage
release/*.deb
release/*.tar.gz
release/*.blockmap
release/stable*.yml
if-no-files-found: error
- name: Upload release assets
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
const releaseDir = path.join(process.cwd(), 'release');
const assetPatterns = [
/\.exe$/i,
/\.msi$/i,
/\.dmg$/i,
/\.zip$/i,
/\.AppImage$/i,
/\.deb$/i,
/\.tar\.gz$/i,
/\.blockmap$/i,
/^stable.*\.yml$/i,
];
const assets = fs.existsSync(releaseDir)
? fs.readdirSync(releaseDir)
.map((name) => path.join(releaseDir, name))
.filter((filePath) => fs.statSync(filePath).isFile())
.filter((filePath) => assetPatterns.some((pattern) => pattern.test(path.basename(filePath))))
: [];
if (assets.length === 0) {
core.setFailed('No release assets were generated.');
return;
}
const owner = context.repo.owner;
const repo = context.repo.repo;
async function deleteExistingAssetsByName(name) {
const existingAssets = await github.paginate(github.rest.repos.listReleaseAssets, {
owner,
repo,
release_id: releaseId,
per_page: 100,
});
for (const existing of existingAssets.filter((asset) => asset.name === name)) {
await github.rest.repos.deleteReleaseAsset({
owner,
repo,
asset_id: existing.id,
}).catch((error) => {
if (error.status !== 404) {
throw error;
}
});
}
}
async function deleteExistingAssetsByPattern(pattern) {
const existingAssets = await github.paginate(github.rest.repos.listReleaseAssets, {
owner,
repo,
release_id: releaseId,
per_page: 100,
});
for (const existing of existingAssets.filter((asset) => pattern.test(asset.name))) {
await github.rest.repos.deleteReleaseAsset({
owner,
repo,
asset_id: existing.id,
}).catch((error) => {
if (error.status !== 404) {
throw error;
}
});
}
}
await deleteExistingAssetsByPattern(/^latest.*\.ya?ml$/i);
for (const filePath of assets) {
const name = path.basename(filePath);
await deleteExistingAssetsByName(name);
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
await github.rest.repos.uploadReleaseAsset({
owner,
repo,
release_id: releaseId,
name,
data: fs.readFileSync(filePath),
headers: {
'content-type': 'application/octet-stream',
'content-length': fs.statSync(filePath).size,
},
});
break;
} catch (error) {
if (attempt === 3 || error.status !== 422) {
throw error;
}
await deleteExistingAssetsByName(name);
await new Promise((resolve) => setTimeout(resolve, attempt * 1000));
}
}
core.info(`Uploaded ${name}`);
}
publish-release:
name: Publish release
needs:
- create-release
- build-electron
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish draft release
uses: actions/github-script@v7
with:
script: |
const releaseId = Number('${{ needs.create-release.outputs.release_id }}');
const tagName = '${{ needs.create-release.outputs.tag_name }}';
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id: releaseId,
});
if (!release.draft && release.tag_name === tagName) {
core.info(`Release ${release.tag_name} is already published.`);
return;
}
await github.rest.repos.updateRelease({
owner,
repo,
release_id: releaseId,
tag_name: tagName,
target_commitish: context.sha,
draft: false,
prerelease: false,
});
core.info(`Published release ${tagName}.`);