diff --git a/.github/workflows/release-desktop-rpm.yml b/.github/workflows/release-desktop-rpm.yml deleted file mode 100644 index 8befc722..00000000 --- a/.github/workflows/release-desktop-rpm.yml +++ /dev/null @@ -1,296 +0,0 @@ -name: Build Desktop RPM - -on: - push: - tags: ['v*.*.*'] - workflow_dispatch: - inputs: - draft: - description: 'Create as draft release' - required: false - default: true - type: boolean - -permissions: - contents: write - -concurrency: - group: release-desktop-rpm-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - platform: ubuntu-22.04 - target_triple: x86_64-unknown-linux-gnu - builder_args: --linux rpm --x64 - label: Linux-x64 - arch: x64 - - platform: ubuntu-22.04-arm - target_triple: aarch64-unknown-linux-gnu - builder_args: --linux rpm --arm64 - label: Linux-ARM64 - arch: arm64 - - runs-on: ${{ matrix.platform }} - name: Build RPM (${{ matrix.label }}) - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Read version - id: version - shell: bash - run: | - VERSION=$(node -p "require('./desktop/package.json').version") - echo "value=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Validate tag matches version - if: github.event_name == 'push' - shell: bash - run: | - EXPECTED_TAG="v${{ steps.version.outputs.value }}" - if [ "${GITHUB_REF_NAME}" != "$EXPECTED_TAG" ]; then - echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${EXPECTED_TAG}" - exit 1 - fi - - - name: Install Linux dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential curl wget file libfuse2 rpm - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install root dependencies - run: bun install - - - name: Install desktop dependencies - working-directory: desktop - run: bun install - - - name: Install adapter dependencies - working-directory: adapters - run: bun install - - - name: Build sidecars - working-directory: desktop - env: - BUN_INSTALL_CACHE_DIR: ${{ runner.temp }}/bun-install-cache - SIDECAR_TARGET_TRIPLE: ${{ matrix.target_triple }} - run: bun run build:sidecars - - - name: Build renderer and Electron bundles - working-directory: desktop - run: | - bun run build - bun run build:electron - - - name: Build Electron release artifacts (RPM) - working-directory: desktop - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_IDENTITY_AUTO_DISCOVERY: 'false' - run: node ./node_modules/electron-builder/out/cli/cli.js ${{ matrix.builder_args }} --publish never - - - name: Validate matrix release asset set - shell: bash - working-directory: desktop/build-artifacts/electron - env: - APP_VERSION: ${{ steps.version.outputs.value }} - run: | - case "${{ matrix.label }}" in - Linux-x64) - expected=( - "Claude-Code-Haha-${APP_VERSION}-linux-x86_64.rpm" - "latest-linux.yml" - ) - ;; - Linux-ARM64) - expected=( - "Claude-Code-Haha-${APP_VERSION}-linux-aarch64.rpm" - "latest-linux-arm64.yml" - ) - ;; - *) - echo "::error::No expected asset list for matrix label ${{ matrix.label }}" - exit 1 - ;; - esac - - missing=() - for file in "${expected[@]}"; do - [ -f "$file" ] || missing+=("$file") - done - if [ "${#missing[@]}" -gt 0 ]; then - printf '::error::Missing release assets for %s: %s\n' "${{ matrix.label }}" "${missing[*]}" - exit 1 - fi - - - name: Namespace update metadata assets - shell: bash - working-directory: desktop/build-artifacts/electron - run: | - shopt -s nullglob - for file in latest*.yml; do - mv "$file" "${file%.yml}-${{ matrix.label }}.yml" - done - - - name: Upload update metadata for merge - uses: actions/upload-artifact@v4 - with: - name: desktop-update-metadata-rpm-${{ matrix.label }} - path: desktop/build-artifacts/electron/latest*.yml - if-no-files-found: ignore - - - name: Upload release artifacts for final publish - uses: actions/upload-artifact@v4 - with: - name: desktop-release-artifacts-rpm-${{ matrix.label }} - path: | - desktop/build-artifacts/electron/*.rpm - desktop/build-artifacts/electron/*.yml - if-no-files-found: error - - publish-release: - needs: build - runs-on: ubuntu-latest - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.draft == false) - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Read version - id: version - shell: bash - run: | - VERSION=$(node -p "require('./desktop/package.json').version") - echo "value=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Validate tag matches version - shell: bash - run: | - EXPECTED_TAG="v${{ steps.version.outputs.value }}" - if [ "${GITHUB_REF_NAME}" != "$EXPECTED_TAG" ]; then - echo "::error::Tag ${GITHUB_REF_NAME} does not match app version ${EXPECTED_TAG}" - exit 1 - fi - - - name: Load release notes - id: release_notes - shell: bash - run: | - NOTES_FILE="release-notes/v${{ steps.version.outputs.value }}.md" - if [ ! -f "$NOTES_FILE" ]; then - echo "::error::Missing release notes file: $NOTES_FILE" - exit 1 - fi - { - echo 'body<<__RELEASE_NOTES__' - cat "$NOTES_FILE" - echo - echo '__RELEASE_NOTES__' - } >> "$GITHUB_OUTPUT" - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install root dependencies - run: bun install - - - name: Download release artifacts - uses: actions/download-artifact@v4 - with: - pattern: desktop-release-artifacts-rpm-* - path: artifacts/release-assets - merge-multiple: true - - - name: Validate complete release asset set - shell: bash - env: - APP_VERSION: ${{ steps.version.outputs.value }} - run: | - expected=( - "Claude-Code-Haha-${APP_VERSION}-linux-x86_64.rpm" - "Claude-Code-Haha-${APP_VERSION}-linux-aarch64.rpm" - ) - - missing=() - for file in "${expected[@]}"; do - [ -f "artifacts/release-assets/$file" ] || missing+=("$file") - done - if [ "${#missing[@]}" -gt 0 ]; then - printf '::error::Missing complete release assets: %s\n' "${missing[*]}" - exit 1 - fi - - - name: Download update metadata artifacts - uses: actions/download-artifact@v4 - with: - pattern: desktop-update-metadata-rpm-* - path: artifacts/update-metadata - merge-multiple: true - - - name: Merge standard update metadata - run: bun run scripts/release-update-metadata.ts --metadata-dir artifacts/update-metadata --out-dir artifacts/update-metadata-standard - - - name: Validate standard update metadata set - shell: bash - run: | - expected=( - "latest-linux.yml" - "latest-linux-arm64.yml" - ) - missing=() - for file in "${expected[@]}"; do - [ -f "artifacts/update-metadata-standard/$file" ] || missing+=("$file") - done - if [ "${#missing[@]}" -gt 0 ]; then - printf '::error::Missing standard update metadata: %s\n' "${missing[*]}" - exit 1 - fi - - - name: Publish RPM release assets - uses: softprops/action-gh-release@v2 - with: - tag_name: v${{ steps.version.outputs.value }} - name: Claude Code Haha v${{ steps.version.outputs.value }} (RPM) - body: | - ## RPM Packages for Linux - - This release includes RPM packages for Linux systems. - - ### Installation - - **Fedora/RHEL/CentOS (x86_64):** - ```bash - sudo dnf install ./Claude-Code-Haha-${{ steps.version.outputs.value }}-linux-x86_64.rpm - ``` - - **For ARM64 systems (aarch64):** - ```bash - sudo dnf install ./Claude-Code-Haha-${{ steps.version.outputs.value }}-linux-aarch64.rpm - ``` - - ${{ steps.release_notes.outputs.body }} - draft: false - prerelease: false - fail_on_unmatched_files: true - files: | - artifacts/release-assets/**/*.rpm - artifacts/update-metadata-standard/*.yml diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index f4149048..baa16da2 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -94,13 +94,13 @@ jobs: arch: x64 - platform: ubuntu-22.04 target_triple: x86_64-unknown-linux-gnu - builder_args: --linux AppImage deb --x64 + builder_args: --linux AppImage deb rpm --x64 label: Linux-x64 smoke_platform: linux arch: x64 - platform: ubuntu-22.04-arm target_triple: aarch64-unknown-linux-gnu - builder_args: --linux AppImage deb --arm64 + builder_args: --linux AppImage deb rpm --arm64 label: Linux-ARM64 smoke_platform: linux arch: arm64 @@ -145,7 +145,7 @@ jobs: if: contains(matrix.platform, 'ubuntu') run: | sudo apt-get update - sudo apt-get install -y build-essential curl wget file libfuse2 + sudo apt-get install -y build-essential curl wget file libfuse2 rpm - name: Setup Bun uses: oven-sh/setup-bun@v2 @@ -444,6 +444,7 @@ jobs: expected=( "Claude-Code-Haha-${APP_VERSION}-linux-x86_64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-amd64.deb" + "Claude-Code-Haha-${APP_VERSION}-linux-x86_64.rpm" "latest-linux.yml" ) ;; @@ -451,6 +452,7 @@ jobs: expected=( "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb" + "Claude-Code-Haha-${APP_VERSION}-linux-aarch64.rpm" "latest-linux-arm64.yml" ) ;; @@ -509,6 +511,7 @@ jobs: desktop/build-artifacts/electron/*.exe desktop/build-artifacts/electron/*.AppImage desktop/build-artifacts/electron/*.deb + desktop/build-artifacts/electron/*.rpm desktop/build-artifacts/electron/*.blockmap desktop/build-artifacts/electron/*.yml if-no-files-found: error @@ -585,8 +588,10 @@ jobs: "Claude-Code-Haha-${APP_VERSION}-mac-x64.zip.blockmap" "Claude-Code-Haha-${APP_VERSION}-linux-x86_64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-amd64.deb" + "Claude-Code-Haha-${APP_VERSION}-linux-x86_64.rpm" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage" "Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb" + "Claude-Code-Haha-${APP_VERSION}-linux-aarch64.rpm" "Claude-Code-Haha-${APP_VERSION}-win-x64.exe" "Claude-Code-Haha-${APP_VERSION}-win-x64.exe.blockmap" "Claude-Code-Haha-${APP_VERSION}-win-arm64.exe" @@ -645,6 +650,7 @@ jobs: artifacts/release-assets/**/*.exe artifacts/release-assets/**/*.AppImage artifacts/release-assets/**/*.deb + artifacts/release-assets/**/*.rpm artifacts/release-assets/**/*.blockmap artifacts/update-metadata-standard/*.yml desktop/scripts/install-macos-unsigned.sh diff --git a/scripts/pr/release-workflow.test.ts b/scripts/pr/release-workflow.test.ts index 3af733b2..7428a0e9 100644 --- a/scripts/pr/release-workflow.test.ts +++ b/scripts/pr/release-workflow.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test' -import { readFileSync } from 'node:fs' +import { readFileSync, readdirSync } from 'node:fs' describe('release desktop workflow', () => { function readReleaseWorkflow() { @@ -30,6 +30,18 @@ describe('release desktop workflow', () => { expect(workflow).toContain('name: Build (${{ matrix.label }})') }) + test('version tags have exactly one desktop release publisher', () => { + const releasePublishers = readdirSync('.github/workflows') + .filter(fileName => fileName.endsWith('.yml')) + .filter((fileName) => { + const workflow = readFileSync(`.github/workflows/${fileName}`, 'utf8') + return workflow.includes("tags: ['v*.*.*']") + && workflow.includes('softprops/action-gh-release@v2') + }) + + expect(releasePublishers).toEqual(['release-desktop.yml']) + }) + test('desktop build workflows keep Bun compile cache on the runner work drive', () => { for (const workflowPath of [ '.github/workflows/build-desktop-dev.yml', @@ -316,6 +328,8 @@ describe('release desktop workflow', () => { } expect(buildJob).toContain('target_triple: aarch64-pc-windows-msvc') expect(buildJob).toContain('builder_args: --win nsis --arm64') + expect(buildJob).toContain('builder_args: --linux AppImage deb rpm --x64') + expect(buildJob).toContain('builder_args: --linux AppImage deb rpm --arm64') expect(buildJob).toContain('Claude-Code-Haha-${APP_VERSION}-win-arm64.exe') expect(buildJob).toContain('Upload release artifacts for final publish') expect(buildJob).toContain('actions/upload-artifact@v4') @@ -343,6 +357,7 @@ describe('release desktop workflow', () => { expect(publishJob).toContain('artifacts/release-assets/**/*.exe') expect(publishJob).toContain('artifacts/release-assets/**/*.AppImage') expect(publishJob).toContain('artifacts/release-assets/**/*.deb') + expect(publishJob).toContain('artifacts/release-assets/**/*.rpm') expect(publishJob).toContain('artifacts/release-assets/**/*.blockmap') expect(publishJob).toContain('artifacts/update-metadata-standard/*.yml') expect(publishJob).toContain('desktop/scripts/install-macos-unsigned.sh') @@ -392,8 +407,10 @@ describe('release desktop workflow', () => { `Claude-Code-Haha-${version}-mac-x64.zip.blockmap`, `Claude-Code-Haha-${version}-linux-x86_64.AppImage`, `Claude-Code-Haha-${version}-linux-amd64.deb`, + `Claude-Code-Haha-${version}-linux-x86_64.rpm`, `Claude-Code-Haha-${version}-linux-arm64.AppImage`, `Claude-Code-Haha-${version}-linux-arm64.deb`, + `Claude-Code-Haha-${version}-linux-aarch64.rpm`, `Claude-Code-Haha-${version}-win-x64.exe`, `Claude-Code-Haha-${version}-win-x64.exe.blockmap`, `Claude-Code-Haha-${version}-win-arm64.exe`, @@ -423,6 +440,7 @@ describe('release desktop workflow', () => { expect(expectedReleaseAssets.filter((name) => name.endsWith('.zip')).length).toBe(2) expect(expectedReleaseAssets.filter((name) => name.endsWith('.AppImage')).length).toBe(2) expect(expectedReleaseAssets.filter((name) => name.endsWith('.deb')).length).toBe(2) + expect(expectedReleaseAssets.filter((name) => name.endsWith('.rpm')).length).toBe(2) expect(expectedReleaseAssets.filter((name) => name.endsWith('.exe')).length).toBe(2) expect(expectedReleaseAssets.some((name) => name.includes('-linux-') && name.endsWith('.blockmap'))).toBe(false) for (const platform of ['mac', 'linux', 'win']) { @@ -447,8 +465,10 @@ describe('release desktop workflow', () => { 'Claude-Code-Haha-${APP_VERSION}-mac-x64.zip', 'Claude-Code-Haha-${APP_VERSION}-linux-x86_64.AppImage', 'Claude-Code-Haha-${APP_VERSION}-linux-amd64.deb', + 'Claude-Code-Haha-${APP_VERSION}-linux-x86_64.rpm', 'Claude-Code-Haha-${APP_VERSION}-linux-arm64.AppImage', 'Claude-Code-Haha-${APP_VERSION}-linux-arm64.deb', + 'Claude-Code-Haha-${APP_VERSION}-linux-aarch64.rpm', 'Claude-Code-Haha-${APP_VERSION}-win-x64.exe', 'Claude-Code-Haha-${APP_VERSION}-win-x64.exe.blockmap', 'Claude-Code-Haha-${APP_VERSION}-win-arm64.exe', diff --git a/scripts/quality-gate/package-smoke/index.test.ts b/scripts/quality-gate/package-smoke/index.test.ts index 0fc15860..ba53794a 100644 --- a/scripts/quality-gate/package-smoke/index.test.ts +++ b/scripts/quality-gate/package-smoke/index.test.ts @@ -521,6 +521,7 @@ describe('packaged artifact inspection', () => { writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-linux-x86_64.AppImage') writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-linux-amd64.deb') + writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-linux-x86_64.rpm') writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app.asar') writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app-update.yml') writeFile(rootDir, 'desktop/build-artifacts/electron/linux-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-unknown-linux-gnu') @@ -536,6 +537,7 @@ describe('packaged artifact inspection', () => { expect(report.passed).toBe(true) expect(report.missingChecks.some((check) => check.label.includes('blockmap'))).toBe(false) + expect(report.packagedArtifacts.some((artifact) => artifact.label === 'Linux RPM package')).toBe(true) }) test('accepts Electron Builder linux-arm64-unpacked output directory', async () => { @@ -544,6 +546,7 @@ describe('packaged artifact inspection', () => { writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-linux-arm64.AppImage') writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-linux-arm64.deb') + writeFile(rootDir, 'desktop/build-artifacts/electron/Claude-Code-Haha-0.3.1-linux-aarch64.rpm') writeFile(rootDir, 'desktop/build-artifacts/electron/linux-arm64-unpacked/resources/app.asar') writeFile(rootDir, 'desktop/build-artifacts/electron/linux-arm64-unpacked/resources/app-update.yml') writeFile(rootDir, 'desktop/build-artifacts/electron/linux-arm64-unpacked/resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-unknown-linux-gnu') @@ -559,6 +562,7 @@ describe('packaged artifact inspection', () => { expect(report.passed).toBe(true) expect(report.passedChecks.some((check) => check.path.includes('linux-arm64-unpacked/resources/app.asar'))).toBe(true) + expect(report.packagedArtifacts.some((artifact) => artifact.label === 'Linux RPM package')).toBe(true) }) test('passes Linux directory-only checks for electron-builder --dir output', async () => { diff --git a/scripts/quality-gate/package-smoke/index.ts b/scripts/quality-gate/package-smoke/index.ts index 8a27e2f4..ff5109f5 100644 --- a/scripts/quality-gate/package-smoke/index.ts +++ b/scripts/quality-gate/package-smoke/index.ts @@ -729,7 +729,9 @@ function findWindowsUnpackedDir(artifactsDir: string, arch?: 'x64' | 'arm64'): s function inspectLinuxArtifacts(rootDir: string, report: PackageSmokeReport) { const packagedArtifacts = findMatches( report.artifactsDir, - (candidate) => candidate.endsWith('.AppImage') || candidate.endsWith('.deb'), + (candidate) => candidate.endsWith('.AppImage') + || candidate.endsWith('.deb') + || candidate.endsWith('.rpm'), ) const unpackedDir = findLinuxUnpackedDir(report.artifactsDir) const updateMetadata = findMatches(report.artifactsDir, (candidate) => /latest-linux(?:-[a-z0-9]+)?\.yml$/.test(candidate)) @@ -737,7 +739,11 @@ function inspectLinuxArtifacts(rootDir: string, report: PackageSmokeReport) { const releaseMode = report.packageKind === 'release' || (report.packageKind === 'auto' && (packagedArtifacts.length > 0 || updateMetadata.length > 0)) report.packagedArtifacts.push(...packagedArtifacts.map((candidate) => ({ - label: candidate.endsWith('.deb') ? 'Linux deb package' : 'Linux AppImage', + label: candidate.endsWith('.deb') + ? 'Linux deb package' + : candidate.endsWith('.rpm') + ? 'Linux RPM package' + : 'Linux AppImage', path: toRelative(rootDir, candidate), }))) report.optionalArtifacts.push(...appImageBlockmaps.map((candidate) => ({ @@ -755,17 +761,17 @@ function inspectLinuxArtifacts(rootDir: string, report: PackageSmokeReport) { addMatchCheck( report, rootDir, - 'linux packaged artifact (.AppImage or .deb)', + 'linux packaged artifact (.AppImage, .deb, or .rpm)', packagedArtifacts, report.artifactsDir, ) } else if (!unpackedDir) { report.missingChecks.push({ - label: 'linux packaged artifact (.AppImage or .deb)', + label: 'linux packaged artifact (.AppImage, .deb, or .rpm)', path: toRelative(rootDir, report.artifactsDir), }) } else { - report.notes.push('No .AppImage or .deb was found; treating linux-unpacked as a directory-only development package.') + report.notes.push('No .AppImage, .deb, or .rpm was found; treating linux-unpacked as a directory-only development package.') } if (unpackedDir) { diff --git a/scripts/release-update-metadata.test.ts b/scripts/release-update-metadata.test.ts index 9fb585d7..5a1b2466 100644 --- a/scripts/release-update-metadata.test.ts +++ b/scripts/release-update-metadata.test.ts @@ -136,7 +136,7 @@ describe('release update metadata merge', () => { expect(arm64.path).toBe('Claude-Code-Haha-0.3.2-arm64.AppImage') }) - test('keeps Linux AppImage as primary update artifact when deb is also published', () => { + test('keeps Linux AppImage as primary update artifact when deb and rpm are also published', () => { const inputDir = tempDir() const outputDir = tempDir() @@ -149,6 +149,9 @@ describe('release update metadata merge', () => { - url: Claude-Code-Haha-0.3.2-linux-x86_64.AppImage sha512: linux-appimage-checksum size: 111 + - url: Claude-Code-Haha-0.3.2-linux-x86_64.rpm + sha512: linux-rpm-checksum + size: 333 path: Claude-Code-Haha-0.3.2-linux-amd64.deb sha512: linux-deb-checksum `) @@ -163,6 +166,7 @@ describe('release update metadata merge', () => { expect(x64.files.map(file => file.url)).toEqual([ 'Claude-Code-Haha-0.3.2-linux-x86_64.AppImage', 'Claude-Code-Haha-0.3.2-linux-amd64.deb', + 'Claude-Code-Haha-0.3.2-linux-x86_64.rpm', ]) expect(x64.path).toBe('Claude-Code-Haha-0.3.2-linux-x86_64.AppImage') expect(x64.sha512).toBe('linux-appimage-checksum')