v3.0.1 #5
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: MLVScan Attestation | |
| on: | |
| workflow_dispatch: | |
| release: | |
| types: | |
| - published | |
| permissions: | |
| contents: write | |
| env: | |
| MLVSCAN_API_BASE_URL: ${{ vars.MLVSCAN_API_BASE_URL }} | |
| jobs: | |
| attest-package: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve checkout ref | |
| id: ref | |
| shell: bash | |
| run: | | |
| if [[ "${{ github.event_name }}" == "release" ]]; then | |
| target_ref="${{ github.event.release.target_commitish }}" | |
| else | |
| target_ref="${{ github.ref_name }}" | |
| fi | |
| if [[ -z "$target_ref" ]]; then | |
| echo "::error::Unable to determine checkout ref" | |
| exit 1 | |
| fi | |
| echo "target_ref=${target_ref}" >> "$GITHUB_OUTPUT" | |
| - name: Checkout S1API | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| fetch-depth: 0 | |
| ref: ${{ steps.ref.outputs.target_ref }} | |
| - name: Validate required secrets | |
| shell: bash | |
| run: | | |
| missing=0 | |
| for secret_name in GAME_ASSEMBLIES_REPO GAME_ASSEMBLIES_TOKEN MLVSCAN_API_KEY; do | |
| if [[ -z "${!secret_name:-}" ]]; then | |
| echo "::error::Missing required secret: ${secret_name}" | |
| missing=1 | |
| fi | |
| done | |
| if [[ "$missing" -ne 0 ]]; then | |
| exit 1 | |
| fi | |
| env: | |
| GAME_ASSEMBLIES_REPO: ${{ secrets.GAME_ASSEMBLIES_REPO }} | |
| GAME_ASSEMBLIES_TOKEN: ${{ secrets.GAME_ASSEMBLIES_TOKEN }} | |
| MLVSCAN_API_KEY: ${{ secrets.MLVSCAN_API_KEY }} | |
| - name: Resolve MLVScan API base URL | |
| id: api-base | |
| shell: bash | |
| run: | | |
| resolved_base_url="${MLVSCAN_API_BASE_URL:-https://api.mlvscan.com}" | |
| echo "base_url=${resolved_base_url}" >> "$GITHUB_OUTPUT" | |
| echo "Using MLVScan API base URL: ${resolved_base_url}" | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '8.0.x' | |
| - name: Checkout game assemblies | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ secrets.GAME_ASSEMBLIES_REPO }} | |
| token: ${{ secrets.GAME_ASSEMBLIES_TOKEN }} | |
| path: game-assemblies | |
| fetch-depth: 1 | |
| - name: Prepare CI build inputs | |
| shell: bash | |
| run: | | |
| mkdir -p S1API/ScheduleOneAssemblies/Managed | |
| mkdir -p S1API/ScheduleOneAssemblies/MelonLoader | |
| cp -r game-assemblies/Managed/* S1API/ScheduleOneAssemblies/Managed/ | |
| cp -r game-assemblies/MelonLoader/* S1API/ScheduleOneAssemblies/MelonLoader/ | |
| if [[ ! -f "S1API/ScheduleOneAssemblies/Managed/Unity.TextMeshPro.dll" ]]; then | |
| TMPRO_PATH=$(find game-assemblies -name "Unity.TextMeshPro.dll" -type f | head -1) | |
| if [[ -n "$TMPRO_PATH" ]]; then | |
| cp "$TMPRO_PATH" S1API/ScheduleOneAssemblies/Managed/ | |
| fi | |
| fi | |
| cat > ci.build.props << 'EOF' | |
| <Project> | |
| <PropertyGroup> | |
| <AutomateLocalDeployment>false</AutomateLocalDeployment> | |
| <MelonLoaderAssembliesPath>../ScheduleOneAssemblies/MelonLoader</MelonLoaderAssembliesPath> | |
| <LocalMonoDeploymentPath>null</LocalMonoDeploymentPath> | |
| <MonoAssembliesPath>../ScheduleOneAssemblies/Managed</MonoAssembliesPath> | |
| </PropertyGroup> | |
| </Project> | |
| EOF | |
| - name: Restore dependencies | |
| run: dotnet restore S1API/S1API.csproj -p:Configuration=MonoMelon | |
| - name: Build Mono DLL | |
| run: dotnet build S1API/S1API.csproj --no-restore -c MonoMelon /p:ContinuousIntegrationBuild=true | |
| - name: Resolve Mono artifact | |
| id: artifact | |
| shell: bash | |
| run: | | |
| artifact_path="S1API/bin/MonoMelon/netstandard2.1/S1API.dll" | |
| if [[ ! -f "$artifact_path" ]]; then | |
| echo "::error::Mono DLL artifact was not produced at ${artifact_path}" | |
| exit 1 | |
| fi | |
| artifact_name=$(basename "$artifact_path") | |
| version=$(grep -oPm1 '(?<=<Version>)[^<]+' S1API/S1API.csproj) | |
| display_name="S1API MonoMelon ${version}" | |
| echo "path=${artifact_path}" >> "$GITHUB_OUTPUT" | |
| echo "name=${artifact_name}" >> "$GITHUB_OUTPUT" | |
| echo "version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "display_name=${display_name}" >> "$GITHUB_OUTPUT" | |
| - name: Upload Mono artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ steps.artifact.outputs.name }} | |
| path: ${{ steps.artifact.outputs.path }} | |
| - name: Derive attestation metadata | |
| id: metadata | |
| shell: bash | |
| run: | | |
| if [[ "${{ github.event_name }}" == "release" ]]; then | |
| canonical_source_url="${{ github.event.release.html_url }}" | |
| else | |
| canonical_source_url="https://github.com/${{ github.repository }}/commit/${{ github.sha }}" | |
| fi | |
| echo "canonical_source_url=${canonical_source_url}" >> "$GITHUB_OUTPUT" | |
| - name: Upload Mono DLL for scanning | |
| id: upload | |
| shell: bash | |
| env: | |
| MLVSCAN_API_KEY: ${{ secrets.MLVSCAN_API_KEY }} | |
| run: | | |
| status_code=$(curl --silent --show-error \ | |
| -o mlvscan-upload-response.json \ | |
| -w "%{http_code}" \ | |
| -X POST "${{ steps.api-base.outputs.base_url }}/files" \ | |
| -H "X-API-Key: ${MLVSCAN_API_KEY}" \ | |
| -F "file=@${{ steps.artifact.outputs.path }}") | |
| response=$(cat mlvscan-upload-response.json) | |
| if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then | |
| echo "::error::MLVScan upload failed with HTTP ${status_code}" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| submission_id=$(echo "$response" | jq -r '.data.id // empty') | |
| if [[ -z "$submission_id" ]]; then | |
| echo "::error::Upload response did not include a submission id" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| echo "submission_id=${submission_id}" >> "$GITHUB_OUTPUT" | |
| - name: Wait for scan report | |
| id: report | |
| shell: bash | |
| env: | |
| MLVSCAN_API_KEY: ${{ secrets.MLVSCAN_API_KEY }} | |
| run: | | |
| submission_id="${{ steps.upload.outputs.submission_id }}" | |
| for attempt in $(seq 1 60); do | |
| status_code=$(curl --silent --show-error \ | |
| -o mlvscan-report-response.json \ | |
| -w "%{http_code}" \ | |
| -H "X-API-Key: ${MLVSCAN_API_KEY}" \ | |
| "${{ steps.api-base.outputs.base_url }}/reports/${submission_id}") | |
| response=$(cat mlvscan-report-response.json) | |
| if [[ "$status_code" == "404" ]]; then | |
| error_message=$(echo "$response" | jq -r '.error // empty' 2>/dev/null || true) | |
| if [[ "$error_message" == "Submission not found" || "$error_message" == "Report not found" ]]; then | |
| echo "Report is not materialized yet; waiting..." | |
| sleep 10 | |
| continue | |
| fi | |
| fi | |
| if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then | |
| echo "::error::MLVScan report lookup failed with HTTP ${status_code}" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| status=$(echo "$response" | jq -r '.status // empty') | |
| if [[ "$status" == "completed" ]]; then | |
| echo "$response" > report.json | |
| report_id=$(echo "$response" | jq -r '.reportId') | |
| echo "report_id=${report_id}" >> "$GITHUB_OUTPUT" | |
| echo "status=${status}" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [[ "$status" == "failed" ]]; then | |
| echo "::error::Scan failed for submission ${submission_id}" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| echo "Report status is ${status:-unknown}; waiting..." | |
| sleep 10 | |
| done | |
| echo "::error::Timed out waiting for scan report" | |
| exit 1 | |
| - name: Create draft attestation | |
| id: draft | |
| shell: bash | |
| env: | |
| MLVSCAN_API_KEY: ${{ secrets.MLVSCAN_API_KEY }} | |
| run: | | |
| payload=$(jq -n \ | |
| --arg submissionId "${{ steps.upload.outputs.submission_id }}" \ | |
| --arg publicDisplayName "${{ steps.artifact.outputs.display_name }}" \ | |
| --arg canonicalSourceUrl "${{ steps.metadata.outputs.canonical_source_url }}" \ | |
| '{ | |
| submissionId: $submissionId, | |
| publicDisplayName: $publicDisplayName, | |
| canonicalSourceUrl: $canonicalSourceUrl | |
| }') | |
| status_code=$(curl --silent --show-error \ | |
| -o mlvscan-draft-response.json \ | |
| -w "%{http_code}" \ | |
| -X POST "${{ steps.api-base.outputs.base_url }}/automation/attestations" \ | |
| -H "X-API-Key: ${MLVSCAN_API_KEY}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$payload") | |
| response=$(cat mlvscan-draft-response.json) | |
| if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then | |
| echo "::error::MLVScan draft creation failed with HTTP ${status_code}" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| echo "$response" > draft-attestation.json | |
| attestation_id=$(echo "$response" | jq -r '.id // empty') | |
| public_url=$(echo "$response" | jq -r '.publicUrl // empty') | |
| badge_url=$(echo "$response" | jq -r '.badgeUrl // empty') | |
| publication_status=$(echo "$response" | jq -r '.publicationStatus // empty') | |
| if [[ -z "$attestation_id" ]]; then | |
| echo "::error::Draft attestation response did not include an id" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| echo "attestation_id=${attestation_id}" >> "$GITHUB_OUTPUT" | |
| echo "public_url=${public_url}" >> "$GITHUB_OUTPUT" | |
| echo "badge_url=${badge_url}" >> "$GITHUB_OUTPUT" | |
| echo "publication_status=${publication_status}" >> "$GITHUB_OUTPUT" | |
| - name: Publish attestation | |
| id: publish | |
| shell: bash | |
| env: | |
| MLVSCAN_API_KEY: ${{ secrets.MLVSCAN_API_KEY }} | |
| run: | | |
| status_code=$(curl --silent --show-error \ | |
| -o mlvscan-publish-response.json \ | |
| -w "%{http_code}" \ | |
| -X POST "${{ steps.api-base.outputs.base_url }}/automation/attestations/${{ steps.draft.outputs.attestation_id }}/publish" \ | |
| -H "X-API-Key: ${MLVSCAN_API_KEY}") | |
| response=$(cat mlvscan-publish-response.json) | |
| if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then | |
| echo "::error::MLVScan publish failed with HTTP ${status_code}" | |
| echo "$response" | |
| exit 1 | |
| fi | |
| echo "$response" > published-attestation.json | |
| public_url=$(echo "$response" | jq -r '.publicUrl // empty') | |
| badge_url=$(echo "$response" | jq -r '.badgeUrl // empty') | |
| publication_status=$(echo "$response" | jq -r '.publicationStatus // empty') | |
| echo "public_url=${public_url}" >> "$GITHUB_OUTPUT" | |
| echo "badge_url=${badge_url}" >> "$GITHUB_OUTPUT" | |
| echo "publication_status=${publication_status}" >> "$GITHUB_OUTPUT" | |
| - name: Update README attestation badge | |
| shell: bash | |
| run: | | |
| badge_markdown="[](${{ steps.publish.outputs.public_url }})" | |
| if grep -q '^\[!\[MLVScan Attestation\]' README.md; then | |
| badge_line=$(grep -n '^\[!\[MLVScan Attestation\]' README.md | head -1 | cut -d: -f1) | |
| awk -v line="$badge_line" -v new_badge="$badge_markdown" 'NR==line {print new_badge; next} {print}' README.md > README.md.tmp | |
| mv README.md.tmp README.md | |
| else | |
| insert_after=$(awk ' | |
| NR == 1 { next } | |
| /^[[:space:]]*$/ { | |
| if (last_badge > 0) { | |
| print last_badge | |
| exit | |
| } | |
| next | |
| } | |
| /^\[!\[/ { | |
| last_badge = NR | |
| next | |
| } | |
| { | |
| if (last_badge > 0) { | |
| print last_badge | |
| } else { | |
| print 1 | |
| } | |
| exit | |
| } | |
| END { | |
| if (last_badge > 0) { | |
| print last_badge | |
| } else { | |
| print 1 | |
| } | |
| } | |
| ' README.md | head -1) | |
| awk -v line="$insert_after" -v new_badge="$badge_markdown" ' | |
| { print } | |
| NR == line { print new_badge } | |
| ' README.md > README.md.tmp | |
| mv README.md.tmp README.md | |
| fi | |
| - name: Commit README badge update | |
| shell: bash | |
| run: | | |
| if git diff --quiet README.md; then | |
| echo "README badge already up to date" | |
| exit 0 | |
| fi | |
| if ! git ls-remote --exit-code --heads origin "${{ steps.ref.outputs.target_ref }}" >/dev/null 2>&1; then | |
| echo "::warning::Skipping README push because origin/${{ steps.ref.outputs.target_ref }} does not exist as a branch" | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add README.md | |
| git commit -m "chore: update MLVScan attestation badge [skip ci]" | |
| git push origin "HEAD:${{ steps.ref.outputs.target_ref }}" | |
| - name: Write workflow summary | |
| shell: bash | |
| run: | | |
| { | |
| echo "## MLVScan Attestation" | |
| echo "" | |
| echo "- Package: \`${{ steps.artifact.outputs.name }}\`" | |
| echo "- Version: \`${{ steps.artifact.outputs.version }}\`" | |
| echo "- API Base URL: \`${{ steps.api-base.outputs.base_url }}\`" | |
| echo "- Submission ID: \`${{ steps.upload.outputs.submission_id }}\`" | |
| echo "- Report ID: \`${{ steps.report.outputs.report_id }}\`" | |
| echo "- Attestation ID: \`${{ steps.draft.outputs.attestation_id }}\`" | |
| echo "- Publication Status: \`${{ steps.publish.outputs.publication_status }}\`" | |
| echo "- Public URL: ${{ steps.publish.outputs.public_url }}" | |
| echo "- Badge URL: ${{ steps.publish.outputs.badge_url }}" | |
| } >> "$GITHUB_STEP_SUMMARY" |