diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index 37684cfc..e6235c29 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -8,10 +8,6 @@ inputs: keychain-password: description: "Keychain password" required: true - install-swiftlint: - description: "Whether to install SwiftLint" - required: false - default: "true" include-pulse: description: "Whether to include Pulse in cache (false for production builds)" required: false @@ -39,7 +35,15 @@ runs: ruby-version: "3.3.0" bundler-cache: true + - name: πŸ“¦ Cache Tuist binary + id: tuist-binary-cache + uses: actions/cache@v4 + with: + path: ~/.tuist-bin + key: tuist-binary-${{ runner.os }}-4.118.0 + - name: πŸ›  Install Tuist (manual binary) + if: steps.tuist-binary-cache.outputs.cache-hit != 'true' shell: bash run: | TUIST_VERSION=4.118.0 @@ -47,9 +51,13 @@ runs: curl -L "https://github.com/tuist/tuist/releases/download/${TUIST_VERSION}/tuist.zip" -o tuist.zip unzip -o tuist.zip -d ~/.tuist-bin chmod +x ~/.tuist-bin/tuist - echo "$HOME/.tuist-bin" >> $GITHUB_PATH + + - name: πŸ”§ Add Tuist to PATH + shell: bash + run: echo "$HOME/.tuist-bin" >> $GITHUB_PATH - name: πŸ“¦ Cache SPM dependencies + id: spm-cache uses: actions/cache@v4 with: path: Tuist/.build @@ -58,49 +66,25 @@ runs: tuist-spm-${{ runner.os }}- - name: πŸ›  Install tuist dependencies + if: steps.spm-cache.outputs.cache-hit != 'true' shell: bash run: tuist install - - name: πŸ“¦ Cache Tuist build artifacts - id: tuist-cache - uses: actions/cache@v4 - with: - path: ~/.cache/tuist - key: tuist-build-cache-${{ runner.os }}-${{ inputs.include-pulse }}-${{ hashFiles('Tuist/Package.resolved', 'Tuist/Package.swift') }} - restore-keys: | - tuist-build-cache-${{ runner.os }}-${{ inputs.include-pulse }}- - - - name: πŸ“Š Tuist cache status + - name: πŸ“Š SPM cache status shell: bash run: | - if [ "${{ steps.tuist-cache.outputs.cache-hit }}" == "true" ]; then - echo "βœ… Tuist cache HIT" + if [ "${{ steps.spm-cache.outputs.cache-hit }}" == "true" ]; then + echo "βœ… SPM cache HIT - skipping tuist cache warm up" else - echo "❌ Tuist cache MISS - will build dependencies" + echo "❌ SPM cache MISS - will build dependencies" fi - name: πŸ— Warm up external dependencies (with Pulse) - if: ${{ inputs.include-pulse == 'true' }} + if: steps.spm-cache.outputs.cache-hit != 'true' && inputs.include-pulse == 'true' shell: bash run: tuist cache ComposableArchitecture Kingfisher Pulse KakaoSDKAuth KakaoSDKCommon GoogleSignIn GoogleSignInSwift - name: πŸ— Warm up external dependencies (without Pulse) - if: ${{ inputs.include-pulse != 'true' }} + if: steps.spm-cache.outputs.cache-hit != 'true' && inputs.include-pulse != 'true' shell: bash run: tuist cache ComposableArchitecture Kingfisher KakaoSDKAuth KakaoSDKCommon GoogleSignIn GoogleSignInSwift - - - name: πŸ“¦ Cache SwiftLint - if: ${{ inputs.install-swiftlint == 'true' }} - uses: actions/cache@v4 - with: - path: /opt/homebrew/bin/swiftlint - key: swiftlint-${{ runner.os }}-0.57.0 - - - name: 🧰 Install SwiftLint - if: ${{ inputs.install-swiftlint == 'true' }} - shell: bash - run: | - if ! command -v swiftlint &> /dev/null; then - brew install swiftlint - fi - swiftlint version diff --git a/.github/scripts/create-notion-page.js b/.github/scripts/create-notion-page.js deleted file mode 100644 index dfce0931..00000000 --- a/.github/scripts/create-notion-page.js +++ /dev/null @@ -1,106 +0,0 @@ -const { Client } = require("@notionhq/client"); - -const notion = new Client({ auth: process.env.NOTION_TOKEN }); - -function rt(text) { - return [{ type: "text", text: { content: text } }]; -} - -function mdToNotionBlocks(md) { - const lines = (md ?? "").replace(/\r\n/g, "\n").split("\n"); - - const blocks = []; - for (const raw of lines) { - const line = raw.trim(); - if (!line) continue; - - // ### Heading 3 to 2 - const h3 = line.match(/^###\s+(.*)$/); - if (h3) { - blocks.push({ - object: "block", - type: "heading_2", - heading_2: { rich_text: rt(h3[1]) }, - }); - continue; - } - - // - [ ] todo (unchecked) - const todoUnchecked = line.match(/^- \[\s\]\s+(.*)$/); - if (todoUnchecked) { - blocks.push({ - object: "block", - type: "to_do", - to_do: { rich_text: rt(todoUnchecked[1]), checked: false }, - }); - continue; - } - - // - [x] todo (checked) - const todoChecked = line.match(/^- \[[xX]\]\s+(.*)$/); - if (todoChecked) { - blocks.push({ - object: "block", - type: "to_do", - to_do: { rich_text: rt(todoChecked[1]), checked: true }, - }); - continue; - } - - // fallback paragraph - blocks.push({ - object: "block", - type: "paragraph", - paragraph: { rich_text: rt(line) }, - }); - } - return blocks; -} - -async function main() { - const dbId = process.env.NOTION_DATABASE_ID; - const bodyMd = process.env.ISSUE_BODY_MD; - const titleText = process.env.ISSUE_TITLE ?? "Untitled"; - const issueNumber = process.env.ISSUE_NUMBER ?? 0; - const githubAssignee = process.env.GITHUB_ASSIGNEE || process.env.GITHUB_ISSUE_AUTHOR; - - const db = await notion.databases.retrieve({ database_id: dbId }); - const dataSourceId = db.data_sources?.[0]?.id; - if (!dataSourceId) throw new Error("No data_sources in this database"); - - const notionAssigneeIdMap = { - "clxxrlove": process.env.NOTION_USER_ID_CLXXRLOVE, - "jihun32": process.env.NOTION_USER_ID_JIHUN32, - }; - - const properties = { - "μž‘μ—… 이름": { title: rt(`#${issueNumber} ${titleText}`) }, - "μƒνƒœ": { status: { name: "μ§„ν–‰ 쀑" } }, - "μž‘μ—… μœ ν˜•": { multi_select: [{ name: "νŒ€ μž‘μ—…" }] }, - "마감일": { date: { start: "2026-03-01" } }, - }; - - const notionUserId = notionAssigneeIdMap[githubAssignee]; - if (notionUserId) { - properties["λ‹΄λ‹Ήμž"] = { people: [{ id: notionUserId }] }; - } - - const children = mdToNotionBlocks(bodyMd); - - await notion.pages.create({ - parent: { data_source_id: dataSourceId }, - icon: { - type: "external", - external: { - url: "https://github.com/user-attachments/assets/7059c317-215f-4116-91b8-4fdc92a13ce1", - }, - }, - properties, - children, - }); -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); \ No newline at end of file diff --git a/.github/workflows/cd_develop.yml b/.github/workflows/cd_develop.yml index acb110fe..b02892df 100644 --- a/.github/workflows/cd_develop.yml +++ b/.github/workflows/cd_develop.yml @@ -14,13 +14,22 @@ jobs: steps: - name: πŸ”„ Checkout source code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: πŸ›  Setup Build Environment uses: ./.github/actions/setup-build-env with: keychain-name: ${{ secrets.KEYCHAIN_NAME }} keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }} - install-swiftlint: "false" + + - name: πŸ“¦ Cache DerivedData + uses: actions/cache@v4 + with: + path: ~/Library/Developer/Xcode/DerivedData + key: deriveddata-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} + restore-keys: | + deriveddata-${{ runner.os }}- - name: πŸš€ Deploy to TestFlight (TwixDebug) env: @@ -37,6 +46,7 @@ jobs: ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} + GITHUB_RUN_NUMBER: ${{ github.run_number }} run: bundle exec fastlane deploy_develop - name: 🧹 Teardown() diff --git a/.github/workflows/cd_main.yml b/.github/workflows/cd_main.yml index b0ded6bd..52ec6856 100644 --- a/.github/workflows/cd_main.yml +++ b/.github/workflows/cd_main.yml @@ -14,15 +14,24 @@ jobs: steps: - name: πŸ”„ Checkout source code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: πŸ›  Setup Build Environment uses: ./.github/actions/setup-build-env with: keychain-name: ${{ secrets.KEYCHAIN_NAME }} keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }} - install-swiftlint: "false" include-pulse: "false" + - name: πŸ“¦ Cache DerivedData + uses: actions/cache@v4 + with: + path: ~/Library/Developer/Xcode/DerivedData + key: deriveddata-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} + restore-keys: | + deriveddata-${{ runner.os }}- + - name: πŸš€ Deploy to App Store Connect (Twix) env: MATCH_REPO_URL: ${{ secrets.MATCH_REPO_URL }} @@ -38,6 +47,7 @@ jobs: ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} + GITHUB_RUN_NUMBER: ${{ github.run_number }} run: bundle exec fastlane deploy_main - name: 🧹 Teardown() diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index fbefd04e..dc0b55ba 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -3,8 +3,12 @@ name: Fastlane CI (PR) on: pull_request: +concurrency: + group: ci-pr-${{ github.head_ref || github.ref }} + cancel-in-progress: true + jobs: - ci_pr: + build: runs-on: macos-15 steps: - name: πŸ”„ Checkout source code @@ -15,7 +19,14 @@ jobs: with: keychain-name: ${{ secrets.KEYCHAIN_NAME }} keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }} - install-swiftlint: "true" + + - name: πŸ“¦ Cache DerivedData + uses: actions/cache@v4 + with: + path: ~/Library/Developer/Xcode/DerivedData + key: deriveddata-${{ runner.os }}-${{ hashFiles('Tuist/Package.resolved') }} + restore-keys: | + deriveddata-${{ runner.os }}- - name: πŸ§ͺ Run PR Checks (build + test) env: diff --git a/.github/workflows/create-notion-page.yml b/.github/workflows/create-notion-page.yml deleted file mode 100644 index 2662c68b..00000000 --- a/.github/workflows/create-notion-page.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Create Notion page on new issue - -on: - issues: - types: [opened] - -jobs: - notion: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: npm i @notionhq/client - - run: node .github/scripts/create-notion-page.js - env: - NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }} - NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }} - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_URL: ${{ github.event.issue.html_url }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - REPO: ${{ github.repository }} - ISSUE_BODY_MD: ${{ github.event.issue.body }} - GITHUB_ASSIGNEE: ${{ github.event.issue.assignee.login }} - GITHUB_ISSUE_AUTHOR: ${{ github.event.issue.user.login }} - NOTION_USER_ID_CLXXRLOVE: ${{ secrets.NOTION_USER_ID_CLXXRLOVE }} - NOTION_USER_ID_JIHUN32: ${{ secrets.NOTION_USER_ID_JIHUN32 }} \ No newline at end of file diff --git a/.github/workflows/issue-automation.yml b/.github/workflows/issue-automation.yml index b41419f9..18054698 100644 --- a/.github/workflows/issue-automation.yml +++ b/.github/workflows/issue-automation.yml @@ -44,7 +44,13 @@ jobs: let type = (m?.[1] ?? "chore").toLowerCase(); if (!allowed.has(type)) type = "chore"; - const branchName = `${type}/#${issueNumber}`; + // Parse Linear issue ID from title (e.g., [TWI-123]) + const linearMatch = title.match(/\[(TWI-\d+)\]/); + const linearId = linearMatch?.[1]; + + const branchName = linearId + ? `${type}/#${issueNumber}/${linearId}` + : `${type}/#${issueNumber}`; console.log(`🌿 Target branch: ${branchName}`); // base branch SHA @@ -52,7 +58,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, }); - const baseBranch = repo.default_branch; + const baseBranch = 'develop'; const { data: baseRef } = await github.rest.git.getRef({ owner: context.repo.owner, diff --git a/fastlane/Fastfile b/fastlane/Fastfile index cd0b12b3..a99660ee 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -170,11 +170,9 @@ platform :ios do is_key_content_base64: true ) - current_build = latest_testflight_build_number( - api_key: api_key, - app_identifier: BUNDLE_ID - ) - new_build = current_build + 1 + commit_count = sh("git rev-list --count HEAD").strip.to_i + run_number = (ENV["GITHUB_RUN_NUMBER"] || "0").to_i + new_build = commit_count * 100 + (run_number % 100) sh("cd .. && /usr/libexec/PlistBuddy -c 'Set :CFBundleVersion #{new_build}' Projects/App/Derived/InfoPlists/TwixDebug-Info.plist") @@ -209,11 +207,9 @@ platform :ios do is_key_content_base64: true ) - current_build = latest_testflight_build_number( - api_key: api_key, - app_identifier: BUNDLE_ID - ) - new_build = current_build + 1 + commit_count = sh("git rev-list --count HEAD").strip.to_i + run_number = (ENV["GITHUB_RUN_NUMBER"] || "0").to_i + new_build = commit_count * 100 + (run_number % 100) sh("cd .. && /usr/libexec/PlistBuddy -c 'Set :CFBundleVersion #{new_build}' Projects/App/Derived/InfoPlists/Twix-Info.plist")