Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_1Iu3QWcglBMq3jWzaRWYO.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"package.json": "Patch"
},
"note": "윈도우, macOS, Linux 설치 파일을 GitHub Release에 자동으로 첨부합니다.",
"date": "2026-08-27T16:28:45.939578800Z"
}
143 changes: 137 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ jobs:
# 공개 저장소이므로 모든 작업은 GitHub 호스팅 러너에서 돈다.
# self-hosted 러너에서 PR 코드를 실행하면 (postinstall, 테스트, 빌드)
# 누구든 PR 하나로 우리 인프라에서 임의 코드를 돌릴 수 있다.
# 배포는 이 워크플로에 두지 않는다.
check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- uses: oven-sh/setup-bun@v2
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: clippy, rustfmt
- name: Install Linux desktop dependencies
Expand All @@ -47,7 +48,7 @@ jobs:
libudev-dev \
pkg-config
- name: Install cargo-tarpaulin
uses: taiki-e/install-action@v2
uses: taiki-e/install-action@fcf5432d9f50d67e37ee6e29bdb7a224ff67b4a7 # v2
with:
tool: cargo-tarpaulin
- name: Install dependencies
Expand Down Expand Up @@ -75,8 +76,138 @@ jobs:
needs:
- check
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: changepacks/action@ff3d7d0ddbce5dd21c4db0fc5932493b320cf85e # main
id: changepacks
with:
# 한번은 레지스트리에 배포할 패키지가 아닌 private 앱이다.
# publish 모드는 GitHub Release를 draft로 남기고 데스크톱 빌드에
# 최종 공개를 위임하기 위해 사용한다.
publish: true
outputs:
changepacks: ${{ steps.changepacks.outputs.changepacks }}
pending_releases: ${{ steps.changepacks.outputs.pending_releases }}

release-desktop:
name: release (${{ matrix.platform }})
needs:
- changepacks
if: ${{ contains(needs.changepacks.outputs.pending_releases, '"package.json"') }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- platform: windows-latest
release_platform: windows
arch: x64
args: --bundles nsis msi
- platform: macos-latest
release_platform: macos
arch: universal
args: --target universal-apple-darwin --bundles dmg
targets: aarch64-apple-darwin,x86_64-apple-darwin
- platform: ubuntu-22.04
release_platform: linux
arch: x64
args: --bundles appimage deb
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- name: Install Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
targets: ${{ matrix.targets || '' }}
- name: Install Linux desktop dependencies
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev \
build-essential \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libasound2-dev \
libudev-dev \
patchelf \
pkg-config
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build desktop bundles
id: tauri
uses: tauri-apps/tauri-action@84b9d35b5fc46c1e45415bdb6144030364f7ebc5 # v0.6.2
env:
APPLE_SIGNING_IDENTITY: ${{ matrix.platform == 'macos-latest' && '-' || '' }}
with:
args: ${{ matrix.args }}
includeUpdaterJson: false
projectPath: apps/desktop
tauriScript: bun tauri
- name: Normalize release assets
env:
RELEASE_ARCH: ${{ matrix.arch }}
RELEASE_OUTPUT_DIR: ${{ runner.temp }}/release-assets
RELEASE_PLATFORM: ${{ matrix.release_platform }}
RELEASE_VERSION: ${{ steps.tauri.outputs.appVersion }}
TAURI_ARTIFACT_PATHS: ${{ steps.tauri.outputs.artifactPaths }}
run: bun scripts/ci/collect-release-assets.ts
- name: Store release assets
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
if-no-files-found: error
name: desktop-${{ matrix.release_platform }}
path: ${{ runner.temp }}/release-assets/*
retention-days: 1

upload-release:
name: upload release assets
needs:
- changepacks
- release-desktop
if: ${{ contains(needs.changepacks.outputs.pending_releases, '"package.json"') }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download release assets
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
merge-multiple: true
path: release-assets
pattern: desktop-*
- name: Upload bundles to the draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ fromJSON(needs.changepacks.outputs.pending_releases)['package.json'].tagName }}
shell: bash
run: |
shopt -s nullglob
assets=(release-assets/*)
if [ "${#assets[@]}" -ne 5 ]; then
echo "Expected 5 desktop release assets, found ${#assets[@]}" >&2
exit 1
fi
gh release upload "$RELEASE_TAG" "${assets[@]}" --clobber

finalize-release:
name: finalize release
needs:
- changepacks
- upload-release
if: ${{ contains(needs.changepacks.outputs.pending_releases, '"package.json"') }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish completed release
uses: changepacks/action@ff3d7d0ddbce5dd21c4db0fc5932493b320cf85e # main
with:
finalize_releases: ${{ needs.changepacks.outputs.pending_releases }}
Binary file added apps/desktop/src-tauri/icons/icon.icns
Binary file not shown.
9 changes: 7 additions & 2 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "한번",
"mainBinaryName": "hanbeon",
"version": "0.1.0",
"version": "../../../package.json",
"identifier": "kr.devfive.hanbeon",
"build": {
"frontendDist": "../out",
Expand Down Expand Up @@ -60,14 +60,19 @@
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png"
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"macOS": {
"minimumSystemVersion": "10.15"
},
"windows": {
"webviewInstallMode": {
"type": "downloadBootstrapper"
},
"wix": {
"language": "ko-KR"
}
}
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.1.0",
"description": "한번 - 상황적응형 싱글스위치 접근성 소프트웨어",
"license": "MIT",
"private": true,
"type": "module",
"devDependencies": {
"eslint-plugin-devup": "^2.1.0",
Expand Down
124 changes: 124 additions & 0 deletions scripts/ci/collect-release-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { copyFileSync, mkdirSync, statSync } from 'node:fs'
import { extname, join } from 'node:path'

type ReleasePlatform = 'linux' | 'macos' | 'windows'

type CollectReleaseAssetsOptions = {
arch: string
artifactPaths: string[]
outputDir: string
platform: ReleasePlatform
version: string
}

const bundleByExtension: Record<ReleasePlatform, Record<string, string>> = {
linux: {
'.appimage': 'appimage',
'.deb': 'deb',
},
macos: {
'.dmg': 'dmg',
},
windows: {
'.exe': 'nsis',
'.msi': 'msi',
},
}

export function collectReleaseAssets({
arch,
artifactPaths,
outputDir,
platform,
version,
}: CollectReleaseAssetsOptions): string[] {
if (!/^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(version)) {
throw new Error(`Invalid release version: ${version}`)
}
if (!/^[0-9A-Za-z][0-9A-Za-z_-]*$/.test(arch)) {
throw new Error(`Invalid release architecture: ${arch}`)
}

const platformBundles = bundleByExtension[platform]
const expectedBundles = Object.values(platformBundles).sort()
const artifacts = artifactPaths.flatMap((sourcePath) => {
const extension = extname(sourcePath)
const sourceStat = statSync(sourcePath)

if (
platform === 'macos' &&
extension.toLowerCase() === '.app' &&
sourceStat.isDirectory()
) {
return []
}

if (!sourceStat.isFile()) {
throw new Error(`Release artifact is not a file: ${sourcePath}`)
}

const bundle = platformBundles[extension.toLowerCase()]
if (!bundle) {
throw new Error(`Unexpected ${platform} release artifact: ${sourcePath}`)
}

return [{ bundle, extension, sourcePath }]
})

const actualBundles = artifacts.map(({ bundle }) => bundle).sort()
if (JSON.stringify(actualBundles) !== JSON.stringify(expectedBundles)) {
throw new Error(
`Expected ${expectedBundles.join(', ')} bundles for ${platform}, received ${actualBundles.join(', ') || 'none'}`,
)
}

mkdirSync(outputDir, { recursive: true })

return artifacts
.sort((left, right) => left.bundle.localeCompare(right.bundle))
.map(({ bundle, extension, sourcePath }) => {
const outputPath = join(
outputDir,
`hanbeon-${version}-${platform}-${arch}-${bundle}${extension}`,
)
copyFileSync(sourcePath, outputPath)
return outputPath
})
}

function requiredEnvironmentVariable(name: string): string {
const value = process.env[name]
if (!value) {
throw new Error(`Missing required environment variable: ${name}`)
}
return value
}

if (import.meta.main) {
const platform = requiredEnvironmentVariable('RELEASE_PLATFORM')
if (!(platform in bundleByExtension)) {
throw new Error(`Unsupported release platform: ${platform}`)
}

const artifactPaths = JSON.parse(
requiredEnvironmentVariable('TAURI_ARTIFACT_PATHS'),
) as unknown
if (
!Array.isArray(artifactPaths) ||
artifactPaths.some((path) => typeof path !== 'string')
) {
throw new Error('TAURI_ARTIFACT_PATHS must be a JSON array of file paths')
}

const outputPaths = collectReleaseAssets({
arch: requiredEnvironmentVariable('RELEASE_ARCH'),
artifactPaths,
outputDir: requiredEnvironmentVariable('RELEASE_OUTPUT_DIR'),
platform: platform as ReleasePlatform,
version: requiredEnvironmentVariable('RELEASE_VERSION'),
})

for (const outputPath of outputPaths) {
console.info(outputPath)
}
}
Loading
Loading