fix: GitHub Actions job간 데이터 전달로 다운로드 링크 문제 완전 해결 #13
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: Build and Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| strategy: | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '18' | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v2 | |
| with: | |
| version: latest | |
| - name: Get pnpm store directory | |
| shell: bash | |
| run: | | |
| echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | |
| - name: Cache pnpm dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: ${{ env.STORE_PATH }} | |
| key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm-store- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Fix electron installation | |
| run: | | |
| if [ -f "node_modules/electron/install.js" ]; then | |
| node node_modules/electron/install.js | |
| fi | |
| shell: bash | |
| - name: Build application | |
| run: pnpm run build | |
| - name: Import Apple certificate (macOS only) | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }} | |
| run: | | |
| if [ -n "$APPLE_CERTIFICATE" ]; then | |
| echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 | |
| security create-keychain -p "$APPLE_KEYCHAIN_PASSWORD" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "$APPLE_KEYCHAIN_PASSWORD" build.keychain | |
| security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_KEYCHAIN_PASSWORD" build.keychain | |
| fi | |
| - name: Package application (Linux) | |
| if: matrix.os == 'ubuntu-latest' | |
| run: pnpm run package:linux | |
| - name: Package application (Windows) | |
| if: matrix.os == 'windows-latest' | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: false | |
| CSC_LINK: ${{ secrets.WIN_CSC_LINK }} | |
| CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} | |
| run: pnpm run package:win | |
| - name: Package application (macOS) | |
| if: matrix.os == 'macos-latest' | |
| env: | |
| CSC_IDENTITY_AUTO_DISCOVERY: false | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| if [ -f "certificate.p12" ]; then | |
| export CSC_LINK=certificate.p12 | |
| export CSC_KEY_PASSWORD="${{ secrets.APPLE_CERTIFICATE_PASSWORD }}" | |
| fi | |
| pnpm run package:mac | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.os }}-artifacts | |
| path: | | |
| release/*.exe | |
| release/*.msi | |
| release/*.dmg | |
| release/*.zip | |
| release/*.AppImage | |
| release/*.deb | |
| release/*.rpm | |
| retention-days: 5 | |
| release: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| outputs: | |
| release_files: ${{ steps.get_files.outputs.files }} | |
| tag_name: ${{ steps.get_tag.outputs.tag_name }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Get tag name | |
| id: get_tag | |
| run: echo "tag_name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| merge-multiple: true | |
| - name: Get release files info | |
| id: get_files | |
| run: | | |
| echo "files<<EOF" >> $GITHUB_OUTPUT | |
| find . -name "*-artifacts" -type d | while read dir; do | |
| find "$dir" -type f \( -name "*.exe" -o -name "*.msi" -o -name "*.dmg" -o -name "*.zip" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" \) | while read file; do | |
| filename=$(basename "$file") | |
| size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "0") | |
| echo "{\"name\":\"$filename\",\"size\":$size}" | |
| done | |
| done | jq -s . >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| - name: Create Release | |
| id: create_release | |
| uses: softprops/action-gh-release@v1 | |
| with: | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: true | |
| files: | | |
| *-artifacts/* | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| deploy-pages: | |
| needs: release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pages: write | |
| id-token: write | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Pages | |
| uses: actions/configure-pages@v4 | |
| - name: Get release info from job output | |
| id: get_release | |
| run: | | |
| echo "tag_name=${{ needs.release.outputs.tag_name }}" >> $GITHUB_OUTPUT | |
| echo "release_url=https://github.com/${{ github.repository }}/releases/tag/${{ needs.release.outputs.tag_name }}" >> $GITHUB_OUTPUT | |
| # Convert release files to assets format with download URLs | |
| echo 'assets<<EOF' >> $GITHUB_OUTPUT | |
| echo '${{ needs.release.outputs.release_files }}' | jq -r '.[] | { | |
| name: .name, | |
| download_url: "https://github.com/${{ github.repository }}/releases/download/${{ needs.release.outputs.tag_name }}/\(.name)", | |
| size: .size, | |
| download_count: 0 | |
| }' | jq -s . >> $GITHUB_OUTPUT | |
| echo 'EOF' >> $GITHUB_OUTPUT | |
| - name: Generate landing page | |
| run: | | |
| mkdir -p _site | |
| cat > _site/index.html << 'EOF' | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>RiSA - RSA 암호화 데스크톱 앱</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 0 20px; | |
| } | |
| .header { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| padding: 1rem 0; | |
| } | |
| .nav { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: white; | |
| text-decoration: none; | |
| } | |
| .nav-links { | |
| display: flex; | |
| list-style: none; | |
| gap: 2rem; | |
| } | |
| .nav-links a { | |
| color: white; | |
| text-decoration: none; | |
| font-weight: 500; | |
| transition: opacity 0.3s; | |
| } | |
| .nav-links a:hover { opacity: 0.8; } | |
| .hero { | |
| text-align: center; | |
| padding: 4rem 0; | |
| color: white; | |
| } | |
| .hero h1 { | |
| font-size: 3.5rem; | |
| font-weight: 700; | |
| margin-bottom: 1rem; | |
| background: linear-gradient(45deg, #fff, #f0f0f0); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .hero p { | |
| font-size: 1.25rem; | |
| margin-bottom: 2rem; | |
| opacity: 0.9; | |
| } | |
| .version-badge { | |
| display: inline-block; | |
| background: rgba(255, 255, 255, 0.2); | |
| padding: 0.5rem 1rem; | |
| border-radius: 50px; | |
| margin-bottom: 2rem; | |
| font-weight: 500; | |
| } | |
| .screenshot { | |
| padding: 3rem 0; | |
| background: white; | |
| } | |
| .screenshot-container { | |
| text-align: center; | |
| } | |
| .app-screenshot { | |
| max-width: 100%; | |
| height: auto; | |
| border-radius: 12px; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); | |
| border: 1px solid #f0f0f0; | |
| } | |
| .downloads { | |
| background: white; | |
| margin: 2rem 0; | |
| border-radius: 20px; | |
| padding: 3rem; | |
| box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); | |
| } | |
| .downloads h2 { | |
| text-align: center; | |
| margin-bottom: 2rem; | |
| font-size: 2rem; | |
| color: #333; | |
| } | |
| .download-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 2rem; | |
| margin-top: 2rem; | |
| } | |
| .download-card { | |
| border: 2px solid #f0f0f0; | |
| border-radius: 12px; | |
| padding: 2rem; | |
| text-align: center; | |
| transition: all 0.3s ease; | |
| } | |
| .download-card:hover { | |
| border-color: #667eea; | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 30px rgba(102, 126, 234, 0.2); | |
| } | |
| .platform-icon { | |
| font-size: 3rem; | |
| margin-bottom: 1rem; | |
| } | |
| .download-btn { | |
| display: inline-block; | |
| background: linear-gradient(45deg, #667eea, #764ba2); | |
| color: white; | |
| padding: 1rem 2rem; | |
| border-radius: 8px; | |
| text-decoration: none; | |
| font-weight: 600; | |
| margin: 0.5rem; | |
| transition: all 0.3s ease; | |
| } | |
| .download-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .file-size { | |
| font-size: 0.9rem; | |
| color: #666; | |
| margin-top: 0.5rem; | |
| } | |
| .features { | |
| padding: 4rem 0; | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| margin: 2rem 0; | |
| border-radius: 20px; | |
| } | |
| .features h2 { | |
| text-align: center; | |
| color: white; | |
| font-size: 2rem; | |
| margin-bottom: 3rem; | |
| } | |
| .feature-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 2rem; | |
| } | |
| .feature-card { | |
| text-align: center; | |
| color: white; | |
| padding: 2rem; | |
| } | |
| .feature-icon { | |
| font-size: 2.5rem; | |
| margin-bottom: 1rem; | |
| } | |
| .feature-card h3 { | |
| font-size: 1.25rem; | |
| margin-bottom: 1rem; | |
| } | |
| .footer { | |
| text-align: center; | |
| padding: 2rem 0; | |
| color: rgba(255, 255, 255, 0.8); | |
| } | |
| .footer a { | |
| color: white; | |
| text-decoration: none; | |
| } | |
| @media (max-width: 768px) { | |
| .hero h1 { font-size: 2.5rem; } | |
| .hero p { font-size: 1.1rem; } | |
| .downloads { padding: 2rem 1rem; } | |
| .nav-links { display: none; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header class="header"> | |
| <div class="container"> | |
| <nav class="nav"> | |
| <a href="#" class="logo">🔐 RiSA</a> | |
| <ul class="nav-links"> | |
| <li><a href="#download">다운로드</a></li> | |
| <li><a href="#features">기능</a></li> | |
| <li><a href="https://github.com/0-ROK/RiSA">GitHub</a></li> | |
| </ul> | |
| </nav> | |
| </div> | |
| </header> | |
| <section class="hero"> | |
| <div class="container"> | |
| <div class="version-badge">최신 버전: ${{ steps.get_release.outputs.tag_name }}</div> | |
| <h1>RiSA</h1> | |
| <p>간단하고 직관적인 RSA 암호화 데스크톱 앱</p> | |
| </div> | |
| </section> | |
| <section class="screenshot"> | |
| <div class="container"> | |
| <div class="screenshot-container"> | |
| <img src="https://raw.githubusercontent.com/0-ROK/RiSA/main/assets/screenshot.png" alt="RiSA 앱 스크린샷" class="app-screenshot"> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="downloads" id="download"> | |
| <div class="container"> | |
| <h2>📥 다운로드</h2> | |
| <div class="download-grid" id="download-grid"> | |
| <!-- 다운로드 링크가 여기에 동적으로 생성됩니다 --> | |
| </div> | |
| </div> | |
| </section> | |
| <section class="features" id="features"> | |
| <div class="container"> | |
| <h2>✨ 주요 기능</h2> | |
| <div class="feature-grid"> | |
| <div class="feature-card"> | |
| <div class="feature-icon">🔒</div> | |
| <h3>RSA 암호화</h3> | |
| <p>안전한 RSA 알고리즘으로 텍스트 암호화/복호화</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon">🎨</div> | |
| <h3>직관적인 UI</h3> | |
| <p>깔끔하고 사용하기 쉬운 데스크톱 인터페이스</p> | |
| </div> | |
| <div class="feature-card"> | |
| <div class="feature-icon">🔧</div> | |
| <h3>키 관리</h3> | |
| <p>RSA 키 쌍 생성, 가져오기, 내보내기</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <footer class="footer"> | |
| <div class="container"> | |
| <p>© 2024 RiSA Team. MIT 라이선스로 제공됩니다.</p> | |
| <p> | |
| <a href="https://github.com/0-ROK/RiSA">GitHub</a> | | |
| <a href="https://github.com/0-ROK/RiSA/issues">버그 신고</a> | | |
| <a href="https://github.com/0-ROK/RiSA/releases">릴리즈 노트</a> | |
| </p> | |
| </div> | |
| </footer> | |
| <script> | |
| // 다운로드 링크 생성 | |
| const assets = ${{ steps.get_release.outputs.assets }}; | |
| const tagName = '${{ steps.get_release.outputs.tag_name }}'; | |
| const downloadGrid = document.getElementById('download-grid'); | |
| const platformData = { | |
| 'win': { name: '🪟 Windows', icon: '🪟', fallbackUrl: `https://github.com/0-ROK/RiSA/releases/download/${tagName}/RiSA-1.0.0-win.exe` }, | |
| 'mac': { name: '🍎 macOS', icon: '🍎', fallbackUrl: `https://github.com/0-ROK/RiSA/releases/download/${tagName}/RiSA-1.0.0-mac-x64.dmg` }, | |
| 'linux': { name: '🐧 Linux', icon: '🐧', fallbackUrl: `https://github.com/0-ROK/RiSA/releases/download/${tagName}/RiSA-1.0.0-linux-x64.AppImage` } | |
| }; | |
| function formatFileSize(bytes) { | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| if (bytes === 0) return '0 Bytes'; | |
| const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); | |
| return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; | |
| } | |
| // assets가 비어있거나 없는 경우 폴백 처리 | |
| if (!assets || assets.length === 0) { | |
| Object.keys(platformData).forEach(platform => { | |
| const card = document.createElement('div'); | |
| card.className = 'download-card'; | |
| card.innerHTML = ` | |
| <div class="platform-icon">${platformData[platform].icon}</div> | |
| <h3>${platformData[platform].name}</h3> | |
| <a href="https://github.com/0-ROK/RiSA/releases/latest" class="download-btn"> | |
| 최신 릴리즈에서 다운로드 | |
| </a> | |
| `; | |
| downloadGrid.appendChild(card); | |
| }); | |
| } else { | |
| Object.keys(platformData).forEach(platform => { | |
| const platformAssets = assets.filter(asset => | |
| asset.name.toLowerCase().includes(platform) | |
| ); | |
| const card = document.createElement('div'); | |
| card.className = 'download-card'; | |
| let downloadButtons = ''; | |
| if (platformAssets.length > 0) { | |
| platformAssets.forEach(asset => { | |
| downloadButtons += ` | |
| <a href="${asset.download_url}" class="download-btn"> | |
| ${asset.name} | |
| <div class="file-size">${formatFileSize(asset.size)}</div> | |
| </a> | |
| `; | |
| }); | |
| } else { | |
| downloadButtons = ` | |
| <a href="https://github.com/0-ROK/RiSA/releases/latest" class="download-btn"> | |
| 최신 릴리즈에서 다운로드 | |
| </a> | |
| `; | |
| } | |
| card.innerHTML = ` | |
| <div class="platform-icon">${platformData[platform].icon}</div> | |
| <h3>${platformData[platform].name}</h3> | |
| ${downloadButtons} | |
| `; | |
| downloadGrid.appendChild(card); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| EOF | |
| - name: Upload Pages artifact | |
| uses: actions/upload-pages-artifact@v3 | |
| with: | |
| path: '_site' | |
| - name: Deploy to GitHub Pages | |
| id: deployment | |
| uses: actions/deploy-pages@v4 |