release: prepare 0.1.24 #32
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}.`); |