From 6bbec716c0ed1314574ca3bc93b6f2d18473b7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 27 Aug 2026 18:58:33 +0900 Subject: [PATCH 1/5] =?UTF-8?q?security(ci):=20main=20Actions=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=EB=A5=BC=20SHA=EB=A1=9C=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/android-managed-device.yml | 6 +++--- .github/workflows/auto-close-issue.yml | 2 +- .github/workflows/conflict-label.yml | 2 +- .github/workflows/dependency-audit.yml | 6 +++--- .github/workflows/deployment-decision.yml | 4 ++-- .github/workflows/firebase-wif-canary.yml | 4 ++-- .github/workflows/lint.yml | 10 +++++----- .github/workflows/mock-cleanup-check.yml | 2 +- .github/workflows/release-distribution.yml | 4 ++-- .github/workflows/repository-quality.yml | 2 +- .github/workflows/screenshot.yml | 10 +++++----- .github/workflows/unit-test.yml | 8 ++++---- 12 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/android-managed-device.yml b/.github/workflows/android-managed-device.yml index b37e93c7ac..a0211ec97f 100644 --- a/.github/workflows/android-managed-device.yml +++ b/.github/workflows/android-managed-device.yml @@ -21,10 +21,10 @@ jobs: timeout-minutes: 45 steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up JDK - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: "21" @@ -54,7 +54,7 @@ jobs: - name: Upload androidTest reports and failure evidence if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: android-managed-device-results-${{ github.run_attempt }} if-no-files-found: warn diff --git a/.github/workflows/auto-close-issue.yml b/.github/workflows/auto-close-issue.yml index c4757f728e..5fbc8ea33a 100644 --- a/.github/workflows/auto-close-issue.yml +++ b/.github/workflows/auto-close-issue.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Close issues referenced by close keywords - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const pr = context.payload.pull_request; diff --git a/.github/workflows/conflict-label.yml b/.github/workflows/conflict-label.yml index 710b5802e7..92913d669c 100644 --- a/.github/workflows/conflict-label.yml +++ b/.github/workflows/conflict-label.yml @@ -41,7 +41,7 @@ jobs: timeout-minutes: 10 steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index ec30568ed5..d984415e66 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -29,12 +29,12 @@ jobs: REPORT_DIR: build/reports/dependency-audit steps: - name: Clone default branch - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'temurin' java-version: '21' @@ -124,7 +124,7 @@ jobs: - name: Upload machine-readable audit evidence if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: android-dependency-audit path: ${{ env.REPORT_DIR }} diff --git a/.github/workflows/deployment-decision.yml b/.github/workflows/deployment-decision.yml index 126bbacd8f..f4225c30f8 100644 --- a/.github/workflows/deployment-decision.yml +++ b/.github/workflows/deployment-decision.yml @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone merged revision - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.merge_commit_sha || 'develop' }} fetch-depth: 0 @@ -50,7 +50,7 @@ jobs: "$DEPLOYMENT_DECISION_PATH" - name: Publish decision on the merged PR - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: DEPLOYMENT_DECISION_PATH: ${{ runner.temp }}/afternote-deployment-decision/decision.json TARGET_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request_number }} diff --git a/.github/workflows/firebase-wif-canary.yml b/.github/workflows/firebase-wif-canary.yml index 1768b59854..a951ad8bc8 100644 --- a/.github/workflows/firebase-wif-canary.yml +++ b/.github/workflows/firebase-wif-canary.yml @@ -66,7 +66,7 @@ jobs: steps: # google-github-actions/auth가 ADC 파일을 안전하게 만들려면 checkout이 먼저 실행돼야 한다. - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare Firebase release notes env: @@ -81,7 +81,7 @@ jobs: cat "$RELEASE_NOTES_FILE" >> "$GITHUB_STEP_SUMMARY" - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: '21' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7337f1548d..be882f7239 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,10 +19,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'temurin' java-version: '21' @@ -59,10 +59,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'temurin' java-version: '21' @@ -103,7 +103,7 @@ jobs: - name: Report Android Lint (Reviewdog) if: always() - uses: dvdandroid/action-android-lint@master + uses: dvdandroid/action-android-lint@21c9fa6500c10caa12d8c581d9e280925f39510d # v1.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} reporter: github-pr-review diff --git a/.github/workflows/mock-cleanup-check.yml b/.github/workflows/mock-cleanup-check.yml index e3ab394249..f805c700d9 100644 --- a/.github/workflows/mock-cleanup-check.yml +++ b/.github/workflows/mock-cleanup-check.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check for MOCK_CLEANUP markers run: | diff --git a/.github/workflows/release-distribution.yml b/.github/workflows/release-distribution.yml index 463f602126..8edf7dac8f 100644 --- a/.github/workflows/release-distribution.yml +++ b/.github/workflows/release-distribution.yml @@ -53,7 +53,7 @@ jobs: esac - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Load release metadata from main PR if: github.event_name == 'push' @@ -81,7 +81,7 @@ jobs: cat "$RELEASE_NOTES_FILE" >> "$GITHUB_STEP_SUMMARY" - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'temurin' java-version: '21' diff --git a/.github/workflows/repository-quality.yml b/.github/workflows/repository-quality.yml index c0b2c42e48..7910d1ce0e 100644 --- a/.github/workflows/repository-quality.yml +++ b/.github/workflows/repository-quality.yml @@ -19,7 +19,7 @@ jobs: timeout-minutes: 3 steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install actionlint env: diff --git a/.github/workflows/screenshot.yml b/.github/workflows/screenshot.yml index bc5756f396..b6fbe75671 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -19,16 +19,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # docker buildx + gha cache 로 Dockerfile.screenshot 빌드. # 동일 이미지에서 baseline 생성·검증을 수행해 anti-aliasing/font/scale 등 host 환경 차이 root fix. # Dockerfile 변경 없으면 cache 재사용 — 분당 단위 추가 비용. - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Build screenshot image - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . file: Dockerfile.screenshot @@ -75,7 +75,7 @@ jobs: # 실패 시 diff/rendered 이미지를 artifact 로 업로드 — 본인이 다운로드해 시각 차이 확인 가능 - name: Upload screenshot diff on failure if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: screenshot-test-results path: | @@ -90,7 +90,7 @@ jobs: # 같은 PR 의 재실행에서는 기존 코멘트를 update (marker 로 중복 누적 방지). - name: Comment artifact URL on PR (failure) if: failure() && github.event_name == 'pull_request' - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const marker = ''; diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 523882580f..01c07da646 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Fetch pull request history for changed-module coverage env: @@ -43,7 +43,7 @@ jobs: run: node --test .github/scripts/*.test.mjs - name: Set up JDK 21 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: 'temurin' java-version: '21' @@ -63,7 +63,7 @@ jobs: - name: Upload Kover coverage reports id: kover-artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: kover-jvm-coverage path: build/reports/kover/ @@ -80,7 +80,7 @@ jobs: # 실패한 테스트의 HTML/XML 리포트를 PR artifact 로 보존 — CI 로그에서 stack trace 추적이 빠름. - name: Upload test reports on failure if: failure() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: unit-test-reports path: | From 4ccad5b6d75782783dcb8cc7b4b43bf912fb4d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 27 Aug 2026 19:27:50 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore(ci):=20main=20push=EC=9D=98=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EA=B8=B0=EC=A4=80=EC=84=A0?= =?UTF-8?q?=EC=9D=84=20=EC=A0=9C=EC=B6=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dependency-submission.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index b72fa681a8..b4629bd68a 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -1,10 +1,15 @@ name: Gradle Dependency Submission -# develop push 는 신뢰된 base snapshot 을 직접 제출한다. PR 은 fork 를 포함해 write token 없이 +# develop·main push 는 신뢰된 base snapshot 을 직접 제출한다. PR 은 fork 를 포함해 write token 없이 # snapshot artifact 만 만들고, default branch 의 별도 workflow_run 이 이를 제출한다. +# +# main 이 함께 있어야 하는 이유 — dependency-review 는 PR 의 base ref 스냅샷과 head 스냅샷을 +# 짝지어 «추가된 의존성» 을 계산한다. develop 만 제출하면 릴리스 PR(develop → main)의 base 가 +# 빈 것으로 보여 의존성 전량이 신규 도입으로 잡히고, 그 안에 섞인 기존 취약점이 릴리스 PR +# 탓으로 걸린다 (#1063). on: push: - branches: [develop] + branches: [develop, main] pull_request: concurrency: @@ -15,9 +20,9 @@ permissions: contents: read jobs: - submit-default-branch: + submit-trusted-branch: if: github.event_name == 'push' - name: Submit trusted develop dependency graph + name: Submit trusted branch dependency graph runs-on: ubuntu-latest permissions: contents: write From 2f611451cf9a4d72fafbb7a38593ccd4d2a315c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 27 Aug 2026 19:32:46 +0900 Subject: [PATCH 3/5] ci: verify KVM before managed-device tests --- .github/workflows/android-managed-device.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android-managed-device.yml b/.github/workflows/android-managed-device.yml index a0211ec97f..a4ebe1d469 100644 --- a/.github/workflows/android-managed-device.yml +++ b/.github/workflows/android-managed-device.yml @@ -36,12 +36,22 @@ jobs: cache-read-only: true validate-wrappers: true - - name: Enable KVM access + - name: Enable and verify KVM access run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0660", OPTIONS+="static_node=kvm"' \ + runner_user="$(id -un)" + echo "KERNEL==\"kvm\", OWNER=\"${runner_user}\", GROUP=\"kvm\", MODE=\"0660\", OPTIONS+=\"static_node=kvm\"" \ | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm + sudo udevadm trigger --name-match=kvm --settle + + id + ls -l /dev/kvm + if [[ ! -c /dev/kvm || ! -r /dev/kvm || ! -w /dev/kvm ]]; then + echo "::error::/dev/kvm must be a character device readable and writable by the runner" + exit 1 + fi + "${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager" "emulator" + "${ANDROID_HOME}/emulator/emulator" -accel-check - name: Set up CI-only app configuration uses: ./.github/actions/setup-ci-config From 199598ee1a506bc3e3a3b0f7bed0c176eebfb5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 27 Aug 2026 19:58:34 +0900 Subject: [PATCH 4/5] test(android): stabilize main GMD baseline --- .../AppAndReceiverCompletionAndroidTest.kt | 6 ++--- ...meLetterMindRecordCompletionAndroidTest.kt | 24 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt index 3435509b55..aec34a14f6 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/AppAndReceiverCompletionAndroidTest.kt @@ -333,9 +333,9 @@ class ReceiverRuntimeCompletionAndroidTest { .assertIsDisplayed() assertEquals(2, reporter.failures.size) - assertEquals("receiver_home_load", reporter.failures[0].second["home_stage"]) - assertEquals("receiver_home_partial_load", reporter.failures[1].second["home_stage"]) - assertEquals("mind_records", reporter.failures[1].second["home_failed_sources"]) + assertEquals("receiver_home_load", reporter.failures[0].second["receiver_stage"]) + assertEquals("receiver_home_partial_load", reporter.failures[1].second["receiver_stage"]) + assertEquals("mind_records", reporter.failures[1].second["receiver_failed_sources"]) assertEquals(listOf(2, 2, 2, 2), homeCallCounts()) } diff --git a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt index 3fde0c2765..4b509f3cf1 100644 --- a/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt +++ b/app/src/androidTest/java/com/afternote/afternote_fe/TimeLetterMindRecordCompletionAndroidTest.kt @@ -410,6 +410,8 @@ class TimeLetterMindRecordCompletionAndroidTest { (listViewModel.uiState.value as? DailyQuestionListUiState.Success)?.answers?.isEmpty() == true } composeRule.onNodeWithText("아직 등록된 답변이 없어요.").assertIsDisplayed() + val initialListCalls = repository.listCalls + val initialTodayCalls = repository.todayCalls composeRule.runOnIdle { writeViewModel = @@ -432,13 +434,13 @@ class TimeLetterMindRecordCompletionAndroidTest { submitSuccessCalls == 1 && (listViewModel.uiState.value as? DailyQuestionListUiState.Success)?.answers?.size == 1 } - composeRule.onNodeWithText("오늘의 테스트 질문").assertIsDisplayed() + composeRule.onAllNodes(hasText("오늘의 테스트 질문"))[0].assertIsDisplayed() composeRule.onNodeWithText("작성 후 목록에 반영될 답변").assertIsDisplayed() assertEquals(1, repository.createCalls.size) assertEquals(71L, repository.createCalls.single().questionId) assertEquals(false, repository.createCalls.single().isDraft) - assertEquals(2, repository.listCalls) - assertEquals(3, repository.todayCalls) + assertEquals(initialListCalls + 2, repository.listCalls) + assertEquals(initialTodayCalls + 2, repository.todayCalls) } @Test @@ -506,6 +508,8 @@ class TimeLetterMindRecordCompletionAndroidTest { } } + composeRule.onNode(hasText("이어 쓸 일기") and hasClickAction()).assertIsDisplayed() + val initialListCallCount = repository.listCalls.size composeRule.onNode(hasText("이어 쓸 일기") and hasClickAction()).performClick() composeRule.waitUntil(timeoutMillis = TIMEOUT) { writeViewModel?.uiState?.value?.draftLoaded == true @@ -535,8 +539,12 @@ class TimeLetterMindRecordCompletionAndroidTest { assertEquals(draftDate.toString(), update.second.date) assertEquals("https://afternote.test/draft.jpg", update.second.imageUrl) assertTrue(repository.createCalls.isEmpty()) - assertEquals(2, repository.listCalls.size) - assertTrue(repository.listCalls.all { it == currentMonth.toString() to true }) + assertEquals(initialListCallCount + 2, repository.listCalls.size) + assertTrue( + repository.listCalls.drop(initialListCallCount).all { + it == currentMonth.toString() to true + }, + ) } @Test @@ -844,7 +852,11 @@ private class CompletionDiaryRepository( draftOnly: Boolean?, ): Result { listCalls += yearMonth to draftOnly - val matching = drafts.filter { draftOnly == null || it.isDraft == draftOnly } + val matching = + drafts.filter { diary -> + YearMonth.from(LocalDate.parse(diary.date)).toString() == yearMonth && + (draftOnly == null || diary.isDraft == draftOnly) + } return Result.success( DiaryList( diaries = matching, From b7b5ac9083ed484d8355ca57de6eda8b4e5982b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=BC=ED=98=81?= Date: Thu, 27 Aug 2026 22:23:15 +0900 Subject: [PATCH 5/5] =?UTF-8?q?ci:=20main=20PR=EC=97=90=20=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B2=B4=ED=81=AC?= =?UTF-8?q?=EB=A5=BC=20=EC=97=B0=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/create-ci-config.test.mjs | 24 +++++++++++++++++ .github/workflows/android-managed-device.yml | 28 +++++++++++++++++--- .github/workflows/unit-test.yml | 6 ++--- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/.github/scripts/create-ci-config.test.mjs b/.github/scripts/create-ci-config.test.mjs index 50f1c0ce66..76ea20180d 100644 --- a/.github/scripts/create-ci-config.test.mjs +++ b/.github/scripts/create-ci-config.test.mjs @@ -147,3 +147,27 @@ test("keeps pull request validation secretless and release credentials isolated" assert.match(releaseWorkflow, new RegExp(`secrets\\.${secretName}`)); } }); + +test("keeps the required managed-device check attached to same-repository pull request heads", async () => { + const source = await readFile( + join(githubDirectory, "workflows", "android-managed-device.yml"), + "utf8", + ); + + assert.match( + source, + /^\s{2}pull_request:\n\s{4}types:\s*\[opened,\s*reopened,\s*synchronize\]$/m, + ); + assert.match(source, /^\s{4}name:\s*Pixel 2 API 30 androidTest$/m); + assert.match( + source, + /github\.event_name != 'pull_request'\s*\|\|\s*github\.event\.pull_request\.head\.repo\.full_name == github\.repository/, + ); + assert.match( + source, + /ref:\s*\$\{\{\s*github\.event_name == 'pull_request' && github\.event\.pull_request\.head\.sha \|\| github\.sha\s*\}\}/, + ); + assert.match(source, /persist-credentials:\s*false/); + assert.match(source, /actual_sha="\$\(git rev-parse HEAD\)"/); + assert.match(source, /"\$actual_sha" != "\$EXPECTED_SHA"/); +}); diff --git a/.github/workflows/android-managed-device.yml b/.github/workflows/android-managed-device.yml index a4ebe1d469..45bb5edf10 100644 --- a/.github/workflows/android-managed-device.yml +++ b/.github/workflows/android-managed-device.yml @@ -1,9 +1,12 @@ name: Android Managed Device Test -# PR 필수 검증으로 올리기 전 안정화 단계다. 수동 실행과 정기 실행의 최근 3회가 연속 -# 성공했는지는 별도 gate 이슈에서 판단하며, 실패를 감추는 job/test 재시도는 두지 않는다. +# main PR 의 필수 API 30 검증이다. same-repository PR 을 pull_request 권한 경계에서 +# 직접 실행하고 정확한 head SHA 를 검사한다. fork PR 은 신뢰 경계상 자동 실행하지 않는다. +# 수동·정기 실행은 event SHA 를 사용하며, 실패를 감추는 job/test 재시도는 두지 않는다. on: workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize] schedule: - cron: "17 18 * * 2,5" @@ -11,17 +14,34 @@ permissions: contents: read concurrency: - group: android-managed-device-${{ github.ref }} - cancel-in-progress: false + group: android-managed-device-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: managed-device: + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository name: Pixel 2 API 30 androidTest runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Clone repo uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Verify checked out revision + env: + EXPECTED_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + run: | + set -euo pipefail + actual_sha="$(git rev-parse HEAD)" + if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then + echo "::error::checkout $actual_sha 가 기대한 revision $EXPECTED_SHA 와 다릅니다" + exit 1 + fi - name: Set up JDK uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 01c07da646..a65fa9fcdc 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -5,9 +5,9 @@ name: Unit Test # - :koverXmlReportCi / :koverHtmlReportCi : Android debug + 순수 JVM 테스트와 aggregate coverage # - :konsist:test : 순수 JVM 모듈(testDebugUnitTest 미보유)이라 별도 명시. 레이어 의존 아키텍처 가드. # - :build-logic:test : included build 라 루트 태스크에 안 묶여 별도 명시. release 키 가드(TestKit) 회귀 가드. -# - :app:compileDebugAndroidTestKotlin : androidTest 소스셋 컴파일 가드. GMD 스위트는 PR 트리거 없이 -# 화·금 스케줄로만 돌아(android-managed-device.yml), main 코드 변경이 androidTest 를 깨뜨려도 -# 머지 전 어디서도 검출되지 않았다(#936 — develop 컴파일 실패 44건). 에뮬레이터 없이 컴파일만 검증. +# - :app:compileDebugAndroidTestKotlin : androidTest 소스셋 컴파일 가드. GMD API 30은 same-repository +# PR 과 화·금 스케줄에서 실행한다. fork PR 도 에뮬레이터 없이 컴파일은 항상 검증한다 +# (#936 — develop 컴파일 실패 44건). # lint.yml 과 setup 이 거의 동일하지만 분리 — lint 실패가 test 결과를 가리지 않도록 fail-fast 독립. # base 필터를 두지 않는다 — base 변경(feat/* → develop)은 pull_request 기본 activity type 에