diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9047863..f656b04a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,10 +31,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5 with: global-json-file: global.json cache: true @@ -42,6 +44,7 @@ jobs: **/*.csproj **/packages.lock.json global.json + NuGet.config - name: SDK diagnostics shell: pwsh @@ -118,7 +121,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results path: artifacts/test-results @@ -126,7 +129,7 @@ jobs: - name: Upload coverage report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: coverage-report path: artifacts/coverage-report @@ -134,7 +137,7 @@ jobs: - name: Upload MSBuild binlogs if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: msbuild-binlogs path: artifacts/binlogs @@ -147,10 +150,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 cache: npm @@ -168,10 +173,66 @@ jobs: working-directory: website run: npm run build + android-helper: + name: Build Android view helper + runs-on: windows-latest + timeout-minutes: 20 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Setup Java + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + distribution: temurin + java-version: "17" + cache: gradle + + - name: Build Android helper APK + shell: pwsh + run: | + $wrapper = ".\Luotsi.ViewServer.Android\gradlew.bat" + $arguments = @("--no-daemon", "-p", "Luotsi.ViewServer.Android", ":app:assembleRelease") + $maxAttempts = 3 + $exitCode = 0 + + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + & $wrapper @arguments + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + break + } + + if ($attempt -eq $maxAttempts) { + exit $exitCode + } + + $delaySeconds = 20 * $attempt + Write-Warning "Android helper build failed with exit code $exitCode. Retrying in $delaySeconds seconds ($attempt/$maxAttempts)." + Start-Sleep -Seconds $delaySeconds + } + + $apk = "Luotsi.ViewServer.Android/app/build/outputs/apk/release/app-release.apk" + if (!(Test-Path -LiteralPath $apk)) { + throw "Android view helper release APK was not produced at $apk." + } + + - name: Upload Android helper APK + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: android-helper-apk + path: Luotsi.ViewServer.Android/app/build/outputs/apk/release/app-release.apk + if-no-files-found: error + publish: name: Publish ${{ matrix.rid }} runs-on: windows-latest - needs: validate + needs: + - validate + - android-helper timeout-minutes: 20 strategy: fail-fast: false @@ -192,10 +253,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5 with: global-json-file: global.json cache: true @@ -203,25 +266,24 @@ jobs: **/*.csproj **/packages.lock.json global.json - - - name: Setup Java - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: "17" - cache: gradle + NuGet.config - name: Verify locked restore shell: pwsh run: dotnet restore $env:SOLUTION --locked-mode /bl:artifacts/binlogs/publish-restore-${{ matrix.rid }}.binlog - - name: Build Android view helper + - name: Download Android view helper + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: android-helper-apk + path: Luotsi.ViewServer.Android/app/build/outputs/apk/release + + - name: Verify Android view helper shell: pwsh run: | - $wrapper = ".\Luotsi.ViewServer.Android\gradlew.bat" - & $wrapper -p Luotsi.ViewServer.Android :app:assembleRelease - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + $helper = "Luotsi.ViewServer.Android/app/build/outputs/apk/release/app-release.apk" + if (!(Test-Path -LiteralPath $helper)) { + throw "Downloaded Android view helper APK is missing: $helper" } - name: Stage FFmpeg native libraries @@ -269,7 +331,7 @@ jobs: } - name: Upload packaged CLI - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: luotsi-cli-${{ matrix.rid }} path: artifacts/packages/luotsi-cli-${{ matrix.rid }}.${{ matrix.archive_ext }} @@ -278,7 +340,7 @@ jobs: - name: Upload publish binlog if: always() continue-on-error: true - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: publish-binlog-${{ matrix.rid }} path: | @@ -292,14 +354,15 @@ jobs: needs: - validate - website + - android-helper - publish if: always() steps: - name: Check required jobs shell: bash run: | - if [[ "${{ needs.validate.result }}" != "success" || "${{ needs.website.result }}" != "success" || "${{ needs.publish.result }}" != "success" ]]; then - echo "CI failed: validate=${{ needs.validate.result }}, website=${{ needs.website.result }}, publish=${{ needs.publish.result }}" + if [[ "${{ needs.validate.result }}" != "success" || "${{ needs.website.result }}" != "success" || "${{ needs.android-helper.result }}" != "success" || "${{ needs.publish.result }}" != "success" ]]; then + echo "CI failed: validate=${{ needs.validate.result }}, website=${{ needs.website.result }}, android-helper=${{ needs.android-helper.result }}, publish=${{ needs.publish.result }}" exit 1 fi echo "CI passed." diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..d49cca6d --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,28 @@ +name: Dependency Review + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + name: Review dependency changes + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Dependency review + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 5269a0ca..901748d4 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -23,17 +23,19 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 with: node-version: 22 cache: npm cache-dependency-path: website/package-lock.json - name: Configure GitHub Pages - uses: actions/configure-pages@v6 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 - name: Install website dependencies working-directory: website @@ -44,7 +46,7 @@ jobs: run: npm run build - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: website/dist @@ -59,4 +61,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 \ No newline at end of file + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0159235c..49a0745f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,11 +11,6 @@ on: required: true type: string -permissions: - contents: write - id-token: write - attestations: write - concurrency: group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} cancel-in-progress: false @@ -33,6 +28,8 @@ jobs: name: Validate release tag runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read outputs: tag: ${{ steps.version.outputs.tag }} version: ${{ steps.version.outputs.version }} @@ -41,9 +38,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + persist-credentials: false - name: Resolve version id: version @@ -92,15 +90,18 @@ jobs: runs-on: windows-latest needs: validate-tag timeout-minutes: 20 + permissions: + contents: read steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ needs.validate-tag.outputs.tag }} + persist-credentials: false - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5 with: global-json-file: global.json cache: true @@ -108,6 +109,7 @@ jobs: **/*.csproj **/packages.lock.json global.json + NuGet.config - name: Restore shell: pwsh @@ -167,7 +169,7 @@ jobs: - name: Upload validation diagnostics if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-validation-diagnostics path: | @@ -177,64 +179,35 @@ jobs: - name: Upload coverage report if: always() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-coverage-report path: artifacts/coverage-report if-no-files-found: ignore - package: - name: Package ${{ matrix.rid }} - runs-on: ${{ matrix.runner }} - needs: - - validate-tag - - validate + android-helper: + name: Build Android view helper + runs-on: windows-latest + needs: validate-tag timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - include: - - rid: win-x64 - archive_ext: zip - runner: windows-latest - - rid: linux-x64 - archive_ext: tar.gz - runner: ubuntu-latest - - rid: osx-x64 - archive_ext: tar.gz - runner: ubuntu-latest - - rid: osx-arm64 - archive_ext: tar.gz - runner: ubuntu-latest + permissions: + contents: read steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ needs.validate-tag.outputs.tag }} - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v5 - with: - global-json-file: global.json - cache: true - cache-dependency-path: | - **/*.csproj - **/packages.lock.json - global.json + persist-credentials: false - name: Setup Java - uses: actions/setup-java@v5 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 with: distribution: temurin java-version: "17" cache: gradle - - name: Verify locked restore - shell: pwsh - run: dotnet restore $env:SOLUTION --locked-mode /bl:artifacts/binlogs/release-publish-restore-${{ matrix.rid }}.binlog - - - name: Build Android view helper + - name: Build signed Android helper APK shell: pwsh env: LUOTSI_ANDROID_KEYSTORE_BASE64: ${{ secrets.LUOTSI_ANDROID_KEYSTORE_BASE64 }} @@ -261,13 +234,25 @@ jobs: Write-Host "Using Luotsi Android release signing key from GitHub secrets." Write-Host "Stamping Android helper version $env:LUOTSI_ANDROID_VERSION_NAME ($env:LUOTSI_ANDROID_VERSION_CODE)." - if ($IsWindows) { - & ".\Luotsi.ViewServer.Android\gradlew.bat" -p Luotsi.ViewServer.Android :app:assembleRelease - } else { - & bash "./Luotsi.ViewServer.Android/gradlew" -p Luotsi.ViewServer.Android :app:assembleRelease - } - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + $wrapper = ".\Luotsi.ViewServer.Android\gradlew.bat" + $arguments = @("--no-daemon", "-p", "Luotsi.ViewServer.Android", ":app:assembleRelease") + $maxAttempts = 3 + $exitCode = 0 + + for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { + & $wrapper @arguments + $exitCode = $LASTEXITCODE + if ($exitCode -eq 0) { + break + } + + if ($attempt -eq $maxAttempts) { + exit $exitCode + } + + $delaySeconds = 20 * $attempt + Write-Warning "Android helper build failed with exit code $exitCode. Retrying in $delaySeconds seconds ($attempt/$maxAttempts)." + Start-Sleep -Seconds $delaySeconds } $releaseApkDir = Join-Path $PWD "Luotsi.ViewServer.Android/app/build/outputs/apk/release" @@ -288,6 +273,76 @@ jobs: Write-Host "Android helper output: $($_.FullName)" } + - name: Upload Android helper APK + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: android-helper-apk + path: Luotsi.ViewServer.Android/app/build/outputs/apk/release/app-release.apk + if-no-files-found: error + + package: + name: Package ${{ matrix.rid }} + runs-on: ${{ matrix.runner }} + needs: + - validate-tag + - validate + - android-helper + timeout-minutes: 20 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - rid: win-x64 + archive_ext: zip + runner: windows-latest + - rid: linux-x64 + archive_ext: tar.gz + runner: ubuntu-latest + - rid: osx-x64 + archive_ext: tar.gz + runner: ubuntu-latest + - rid: osx-arm64 + archive_ext: tar.gz + runner: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ needs.validate-tag.outputs.tag }} + persist-credentials: false + + - name: Setup .NET SDK + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5 + with: + global-json-file: global.json + cache: true + cache-dependency-path: | + **/*.csproj + **/packages.lock.json + global.json + NuGet.config + + - name: Verify locked restore + shell: pwsh + run: dotnet restore $env:SOLUTION --locked-mode /bl:artifacts/binlogs/release-publish-restore-${{ matrix.rid }}.binlog + + - name: Download Android view helper + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: android-helper-apk + path: Luotsi.ViewServer.Android/app/build/outputs/apk/release + + - name: Verify Android view helper + shell: pwsh + run: | + $helper = "Luotsi.ViewServer.Android/app/build/outputs/apk/release/app-release.apk" + if (!(Test-Path -LiteralPath $helper)) { + throw "Downloaded Android view helper APK is missing: $helper" + } + - name: Publish shell: pwsh run: > @@ -409,7 +464,7 @@ jobs: "$($hash.Hash.ToLowerInvariant()) $(Split-Path $archive -Leaf)" | Set-Content "$archive.sha256" -Encoding ascii - name: Upload package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-package-${{ matrix.rid }} path: | @@ -419,7 +474,7 @@ jobs: - name: Upload publish binlog if: always() continue-on-error: true - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: release-publish-binlog-${{ matrix.rid }} path: | @@ -435,15 +490,20 @@ jobs: - validate - package timeout-minutes: 10 + permissions: + contents: write + id-token: write + attestations: write steps: - name: Checkout tag - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: ref: ${{ needs.validate-tag.outputs.tag }} + persist-credentials: false - name: Download release packages - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: pattern: release-package-* path: release-assets @@ -491,7 +551,7 @@ jobs: fi - name: Attest release assets - uses: actions/attest-build-provenance@v4 + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4 with: subject-path: | release-assets/*.zip diff --git a/Luotsi.ViewServer.Android/gradle/wrapper/gradle-wrapper.properties b/Luotsi.ViewServer.Android/gradle/wrapper/gradle-wrapper.properties index 193c9417..ac0d302a 100644 --- a/Luotsi.ViewServer.Android/gradle/wrapper/gradle-wrapper.properties +++ b/Luotsi.ViewServer.Android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb networkTimeout=60000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 00000000..4e800bf6 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,12 @@ + + + + + + + + + + + +