diff --git a/.github/scripts/supply-chain-policy.test.mjs b/.github/scripts/supply-chain-policy.test.mjs index 50b55b16a2..238899c785 100644 --- a/.github/scripts/supply-chain-policy.test.mjs +++ b/.github/scripts/supply-chain-policy.test.mjs @@ -12,6 +12,20 @@ async function workflows() { ); } +// 같은 저장소의 로컬 액션·reusable workflow 는 uses 대상이 이 저장소의 리비전이라 +// 고정 대상이 아니다. 그 밖은 전부 40자리 commit SHA 여야 한다. +function isLocalReference(reference) { + return reference.startsWith("./"); +} + +function isPinnedToCommit(reference) { + return /@[0-9a-f]{40}$/.test(reference); +} + +function actionReferences(source) { + return [...source.matchAll(/^\s*(?:-\s+)?uses:\s*(\S+)/gm)].map((match) => match[1]); +} + function requiresGradleSetup(source) { return source.includes('./gradlew') || /uses:\s*gradle\/actions\/setup-gradle@/.test(source); } @@ -28,6 +42,65 @@ test('recognizes direct and script-delegated Gradle workflows', () => { ); }); +test('recognizes floating action references that the pinning policy must reject', () => { + // 정책이 실제로 무엇을 막는지 fixture 로 고정한다 — 태그·브랜치·짧은 SHA 는 전부 + // 사후에 다른 코드로 바뀔 수 있다. + assert.equal(isPinnedToCommit('actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'), true); + assert.equal(isPinnedToCommit('actions/checkout@v7'), false); + assert.equal(isPinnedToCommit('ScaCap/action-ktlint@master'), false); + assert.equal(isPinnedToCommit('actions/checkout@3d3c42e'), false); + assert.equal(isLocalReference('./.github/actions/setup-ci-config'), true); + assert.equal(isLocalReference('actions/checkout@v7'), false); +}); + +test('every third-party action is pinned to an immutable commit SHA', async () => { + const unpinned = []; + for (const [name, source] of await workflows()) { + for (const reference of actionReferences(source)) { + if (isLocalReference(reference) || isPinnedToCommit(reference)) { + continue; + } + unpinned.push(`${name}: ${reference}`); + } + } + + assert.deepEqual(unpinned, []); +}); + +test('every pinned action records the release it was pinned from', async () => { + // SHA 만 있으면 무엇을 쓰고 있는지 사람이 읽을 수 없고 Dependabot 갱신도 대조가 안 된다. + const undocumented = []; + for (const [name, source] of await workflows()) { + for (const line of source.split('\n')) { + const match = /^\s*(?:-\s+)?uses:\s*(\S+@[0-9a-f]{40})(.*)$/.exec(line); + if (match && !/^\s*#\s*v?\d/.test(match[2])) { + undocumented.push(`${name}: ${match[1]}`); + } + } + } + + assert.deepEqual(undocumented, []); +}); + +test('composite actions in this repository pin their own dependencies', async () => { + const directory = new URL('../actions/', import.meta.url); + const names = await readdir(directory); + for (const name of names) { + let source; + try { + source = await readFile(new URL(`${name}/action.yml`, directory), 'utf8'); + } catch { + continue; + } + for (const reference of actionReferences(source)) { + assert.ok( + isLocalReference(reference) || isPinnedToCommit(reference), + `${name} uses an unpinned action: ${reference}`, + ); + } + } +}); + test('every workflow that runs the Gradle wrapper uses the pinned setup-gradle action', async () => { const gradleWorkflows = (await workflows()).filter(([, source]) => requiresGradleSetup(source)); assert.ok(gradleWorkflows.length > 0); diff --git a/.github/workflows/android-managed-device.yml b/.github/workflows/android-managed-device.yml index 43deb621c2..bf4732e2c5 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@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 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 b6f13c0774..9cede9c9a9 100644 --- a/.github/workflows/conflict-label.yml +++ b/.github/workflows/conflict-label.yml @@ -35,7 +35,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/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 ae660db121..4feb9e195f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,10 +18,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' @@ -58,10 +58,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' diff --git a/.github/workflows/mock-cleanup-check.yml b/.github/workflows/mock-cleanup-check.yml index f172897a70..1e20e76e47 100644 --- a/.github/workflows/mock-cleanup-check.yml +++ b/.github/workflows/mock-cleanup-check.yml @@ -13,7 +13,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 3f7e737ab4..c2e38f4b9f 100644 --- a/.github/workflows/release-distribution.yml +++ b/.github/workflows/release-distribution.yml @@ -32,7 +32,7 @@ jobs: # 이 job 은 release keystore·Firebase service account 를 받는다. 트리거가 push: [main] 하나여서 # ref 가 main 으로 고정되므로, 예전의 수동 실행용 브랜치 가드는 더 방어할 대상이 없어 제거했다. - name: Clone repo - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Load release metadata from main PR env: @@ -57,7 +57,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/release-scope.yml b/.github/workflows/release-scope.yml index f7bb1f08ac..cb510f199b 100644 --- a/.github/workflows/release-scope.yml +++ b/.github/workflows/release-scope.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone release candidate - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.sha || 'develop' }} fetch-depth: 0 @@ -53,7 +53,7 @@ jobs: "$RELEASE_SCOPE_PATH" - name: Fill release sections on the PR body - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RELEASE_SCOPE_PATH: ${{ runner.temp }}/afternote-release-scope/scope.json RELEASE_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pull_request_number }} diff --git a/.github/workflows/repository-quality.yml b/.github/workflows/repository-quality.yml index 527fd4fa3f..bc5689b92a 100644 --- a/.github/workflows/repository-quality.yml +++ b/.github/workflows/repository-quality.yml @@ -20,7 +20,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 d8ff1506f6..c5282623d3 100644 --- a/.github/workflows/screenshot.yml +++ b/.github/workflows/screenshot.yml @@ -18,16 +18,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 @@ -74,7 +74,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: | @@ -89,7 +89,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 835eebdaa3..4ffbd7f573 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -27,7 +27,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: @@ -42,7 +42,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' @@ -62,7 +62,7 @@ jobs: - name: Upload Kover coverage reports id: kover-artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: kover-jvm-coverage path: build/reports/kover/ @@ -79,7 +79,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: |