fix(npc): tolerate removed region unlock member #468
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: API Coverage Analysis | |
| on: | |
| push: | |
| branches: [ master, main, stable ] | |
| paths-ignore: | |
| - README.md | |
| - tools/S1APICoverageAnalyzer/coverage-history.json | |
| pull_request: | |
| branches: [ master, main, stable ] | |
| workflow_dispatch: | |
| inputs: | |
| assembly_branch: | |
| description: 'Assembly branch to use (main or beta)' | |
| required: false | |
| default: 'main' | |
| type: choice | |
| options: | |
| - main | |
| - beta | |
| jobs: | |
| coverage: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout S1API | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| fetch-depth: 0 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '8.0.x' | |
| # Determine which assembly branch to use (beta or main) - MUST run before cache | |
| - name: Determine Assembly Branch | |
| id: assembly-branch | |
| run: | | |
| # Manual workflow dispatch takes precedence | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ -n "${{ inputs.assembly_branch }}" ]]; then | |
| echo "branch=${{ inputs.assembly_branch }}" >> $GITHUB_OUTPUT | |
| echo "Using ${{ inputs.assembly_branch }} assemblies (manual trigger)" | |
| elif [[ "${{ github.event_name }}" == "pull_request" ]] && [[ "${{ contains(github.event.pull_request.labels.*.name, 'beta') }}" == "true" ]]; then | |
| echo "branch=beta" >> $GITHUB_OUTPUT | |
| echo "Using beta assemblies for PR with beta label" | |
| else | |
| echo "branch=main" >> $GITHUB_OUTPUT | |
| echo "Using main/stable assemblies" | |
| fi | |
| - name: Create Assembly Directory Structure | |
| run: | | |
| mkdir -p S1API/ScheduleOneAssemblies/Managed | |
| mkdir -p S1API/ScheduleOneAssemblies/MelonLoader | |
| # Try to restore assemblies from cache first (for external PRs) | |
| # Cache key includes assembly branch to separate beta from main | |
| # v3 suffix allows cache invalidation by bumping version | |
| # Only use cache for PR events to avoid stale assemblies after merges | |
| - name: Restore Game Assemblies from Cache | |
| id: cache-assemblies | |
| if: github.event_name == 'pull_request' | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: S1API/ScheduleOneAssemblies | |
| key: game-assemblies-v3-${{ steps.assembly-branch.outputs.branch }}-${{ hashFiles('S1API/S1API.csproj') }} | |
| restore-keys: | | |
| game-assemblies-v3-${{ steps.assembly-branch.outputs.branch }}- | |
| # Only checkout game assemblies if cache miss AND we have access to secrets | |
| - name: Checkout Game Assemblies | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ${{ secrets.GAME_ASSEMBLIES_REPO }} | |
| token: ${{ secrets.GAME_ASSEMBLIES_TOKEN }} | |
| ref: ${{ steps.assembly-branch.outputs.branch }} | |
| path: game-assemblies | |
| fetch-depth: 1 | |
| if: steps.cache-assemblies.outputs.cache-hit != 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| continue-on-error: true | |
| - name: Debug Game Assemblies Structure | |
| run: | | |
| echo "=== Game Assemblies Repository Structure ===" | |
| find game-assemblies -type f -name "*.dll" | head -20 | |
| echo "" | |
| echo "=== Looking for coverage assemblies ===" | |
| find game-assemblies \( -name "Assembly-CSharp.dll" -o -name "ScheduleOne.Core.dll" \) -type f | |
| if: steps.cache-assemblies.outputs.cache-hit != 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| - name: Copy Game Assemblies | |
| run: | | |
| # Copy Mono assemblies | |
| if [ -d "game-assemblies/Managed" ]; then | |
| cp -r game-assemblies/Managed/* S1API/ScheduleOneAssemblies/Managed/ || echo "Failed to copy Mono assemblies" | |
| echo "Copied Mono assemblies from game-assemblies/Managed/" | |
| else | |
| echo "No game-assemblies/Managed directory found" | |
| fi | |
| # Copy mod loader assemblies | |
| if [ -d "game-assemblies/MelonLoader" ]; then | |
| cp -r game-assemblies/MelonLoader/* S1API/ScheduleOneAssemblies/MelonLoader/ || echo "Failed to copy MelonLoader assemblies" | |
| echo "Copied MelonLoader assemblies" | |
| else | |
| echo "No game-assemblies/MelonLoader directory found" | |
| fi | |
| # Ensure Unity.TextMeshPro.dll is available | |
| if [ ! -f "S1API/ScheduleOneAssemblies/Managed/Unity.TextMeshPro.dll" ]; then | |
| echo "Unity.TextMeshPro.dll not found in Managed folder, searching for it..." | |
| TMPRO_PATH=$(find game-assemblies -name "Unity.TextMeshPro.dll" -type f | head -1) | |
| if [ -n "$TMPRO_PATH" ]; then | |
| echo "Found Unity.TextMeshPro.dll at: $TMPRO_PATH" | |
| cp "$TMPRO_PATH" S1API/ScheduleOneAssemblies/Managed/ || echo "Failed to copy Unity.TextMeshPro.dll" | |
| echo "Copied Unity.TextMeshPro.dll to Managed folder" | |
| else | |
| echo "Warning: Unity.TextMeshPro.dll not found in game-assemblies repository" | |
| fi | |
| fi | |
| if: steps.cache-assemblies.outputs.cache-hit != 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) | |
| - name: Verify Assembly Copies | |
| id: verify-assemblies | |
| run: | | |
| echo "=== Verifying Assembly Structure ===" | |
| ls -la S1API/ScheduleOneAssemblies/ || echo "ScheduleOneAssemblies directory not found" | |
| echo "" | |
| echo "=== Critical Assembly Check ===" | |
| if [ -f S1API/ScheduleOneAssemblies/Managed/Assembly-CSharp.dll ] && [ -f S1API/ScheduleOneAssemblies/Managed/ScheduleOne.Core.dll ]; then | |
| echo "✓ Found Assembly-CSharp.dll" | |
| ls -l S1API/ScheduleOneAssemblies/Managed/Assembly-CSharp.dll | |
| echo "✓ Found ScheduleOne.Core.dll" | |
| ls -l S1API/ScheduleOneAssemblies/Managed/ScheduleOne.Core.dll | |
| echo "has_assemblies=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "✗ Missing Assembly-CSharp.dll and/or ScheduleOne.Core.dll - Coverage analysis will be skipped" | |
| echo "has_assemblies=false" >> $GITHUB_OUTPUT | |
| fi | |
| echo "" | |
| if [ "${{ steps.cache-assemblies.outputs.cache-hit }}" == "true" ]; then | |
| echo "✓ Using cached assemblies for build" | |
| else | |
| echo "✓ Using fresh assemblies from repository" | |
| fi | |
| - name: Create CI Build Properties | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| run: | | |
| cat > ci.build.props << 'EOF' | |
| <Project> | |
| <PropertyGroup> | |
| <AutomateLocalDeployment>false</AutomateLocalDeployment> | |
| <!-- CI Assembly Paths (relative to S1API/S1API.csproj) --> | |
| <MelonLoaderAssembliesPath>../ScheduleOneAssemblies/MelonLoader</MelonLoaderAssembliesPath> | |
| <!-- Mono Configuration --> | |
| <LocalMonoDeploymentPath>null</LocalMonoDeploymentPath> | |
| <MonoAssembliesPath>../ScheduleOneAssemblies/Managed</MonoAssembliesPath> | |
| </PropertyGroup> | |
| </Project> | |
| EOF | |
| - name: Restore S1API dependencies | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| run: dotnet restore S1API/S1API.csproj -p:Configuration=MonoMelon | |
| - name: Build S1API | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| run: dotnet build S1API/S1API.csproj --no-restore -c MonoMelon -v minimal | |
| - name: Build Coverage Analyzer | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| run: dotnet build tools/S1APICoverageAnalyzer/S1APICoverageAnalyzer.csproj -c Release | |
| - name: Run Coverage Analysis | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| id: coverage | |
| run: | | |
| dotnet run --project tools/S1APICoverageAnalyzer/S1APICoverageAnalyzer.csproj --no-build -c Release -- \ | |
| --game-assembly S1API/ScheduleOneAssemblies/Managed/Assembly-CSharp.dll \ | |
| --game-assembly S1API/ScheduleOneAssemblies/Managed/ScheduleOne.Core.dll \ | |
| --api-assembly S1API/bin/MonoMelon/netstandard2.1/S1API.dll \ | |
| --output coverage-report.json \ | |
| --badge-output coverage-badge.md \ | |
| --history-file tools/S1APICoverageAnalyzer/coverage-history.json \ | |
| --chart-output coverage-chart.md \ | |
| --chart-format markdown \ | |
| --deduplicate-history \ | |
| --verbose | |
| # Read coverage change status | |
| COVERAGE_CHANGED=$(cat tools/S1APICoverageAnalyzer/coverage-changed.txt 2>/dev/null || echo "true") | |
| echo "coverage_changed=$COVERAGE_CHANGED" >> $GITHUB_OUTPUT | |
| echo "Coverage changed: $COVERAGE_CHANGED" | |
| - name: Upload Coverage Report | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report | |
| path: | | |
| coverage-report.json | |
| coverage-badge.md | |
| retention-days: 30 | |
| - name: Display Coverage Badge | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' | |
| run: | | |
| echo "=== Coverage Badge ===" | |
| cat coverage-badge.md || echo "Badge file not found" | |
| echo "" | |
| echo "=== Coverage Summary ===" | |
| if [ -f coverage-report.json ]; then | |
| # Extract key metrics from JSON | |
| echo "Class Coverage: $(jq -r '.classCoverage.percentage' coverage-report.json)%" | |
| echo "Member Coverage: $(jq -r '.memberCoverage.percentage' coverage-report.json)%" | |
| echo "Covered Classes: $(jq -r '.classCoverage.covered' coverage-report.json) / $(jq -r '.classCoverage.total' coverage-report.json)" | |
| fi | |
| - name: Generate README Badge and Chart Updates | |
| id: coverage-files | |
| if: steps.verify-assemblies.outputs.has_assemblies == 'true' && github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable') | |
| run: | | |
| COVERAGE_CHANGED="${{ steps.coverage.outputs.coverage_changed }}" | |
| echo "Coverage changed status: $COVERAGE_CHANGED" | |
| # The analyzer refreshes the latest history timestamp on every run. | |
| # Discard that metadata churn unless the measured coverage changed. | |
| if [ "$COVERAGE_CHANGED" != "true" ]; then | |
| echo "Coverage percentage unchanged - discarding generated metadata churn" | |
| git restore -- README.md tools/S1APICoverageAnalyzer/coverage-history.json | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Coverage changed - update README badge and chart | |
| if [ -f coverage-badge.md ]; then | |
| # Read the new badge markdown (trim whitespace) | |
| NEW_BADGE=$(cat coverage-badge.md | tr -d '\n\r') | |
| # Replace the link to point to the GitHub Actions workflow | |
| # Extract the badge image URL and replace the link URL | |
| WORKFLOW_URL="https://github.com/${{ github.repository }}/actions/workflows/coverage.yml" | |
| NEW_BADGE=$(echo "$NEW_BADGE" | sed "s|](docs/coverage-report.json)|]($WORKFLOW_URL)|g") | |
| echo "New badge: $NEW_BADGE" | |
| # Find the line number with the API Coverage badge in README | |
| BADGE_LINE=$(grep -n "\[!\[API Coverage\]" README.md | head -1 | cut -d: -f1) | |
| if [ -n "$BADGE_LINE" ]; then | |
| echo "Found API Coverage badge at line $BADGE_LINE, updating..." | |
| # Update the badge line in README.md | |
| # Use a temporary file to avoid sed escaping issues | |
| awk -v line="$BADGE_LINE" -v new_badge="$NEW_BADGE" 'NR==line {print new_badge; next} {print}' README.md > README.md.tmp | |
| mv README.md.tmp README.md | |
| else | |
| echo "Warning: API Coverage badge not found in README.md" | |
| fi | |
| else | |
| echo "Coverage badge file unavailable - skipping badge update" | |
| fi | |
| if [ -f coverage-chart.md ]; then | |
| echo "Updating coverage chart in README..." | |
| CHART_URL=$(sed -n '3p' coverage-chart.md) | |
| CHART_LINE=$(grep -n "!\[Coverage Chart\]" README.md | head -1 | cut -d: -f1) | |
| if [ -n "$CHART_LINE" ]; then | |
| echo "Found chart at line $CHART_LINE, updating..." | |
| awk -v line="$CHART_LINE" -v new_chart="$CHART_URL" 'NR==line {print new_chart; next} {print}' README.md > README.md.tmp | |
| mv README.md.tmp README.md | |
| else | |
| echo "Chart line not found in README" | |
| fi | |
| fi | |
| if git diff --quiet -- README.md tools/S1APICoverageAnalyzer/coverage-history.json; then | |
| echo "No coverage files changed" | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Coverage files changed" | |
| git diff -- README.md tools/S1APICoverageAnalyzer/coverage-history.json | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Open Coverage Update Pull Request | |
| if: steps.coverage-files.outputs.changed == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BASE_BRANCH: ${{ github.ref_name }} | |
| UPDATE_BRANCH: automation/coverage-${{ github.ref_name }} | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add README.md tools/S1APICoverageAnalyzer/coverage-history.json | |
| git commit -m "chore: update API coverage badge and history" | |
| REMOTE_SHA=$(git ls-remote --heads origin "refs/heads/$UPDATE_BRANCH" | cut -f1) | |
| if [ -n "$REMOTE_SHA" ]; then | |
| git push --force-with-lease="refs/heads/$UPDATE_BRANCH:$REMOTE_SHA" origin "HEAD:$UPDATE_BRANCH" | |
| else | |
| git push origin "HEAD:$UPDATE_BRANCH" | |
| fi | |
| EXISTING_PR=$(gh pr list \ | |
| --repo "${{ github.repository }}" \ | |
| --base "$BASE_BRANCH" \ | |
| --head "$UPDATE_BRANCH" \ | |
| --state open \ | |
| --json url \ | |
| --jq '.[0].url') | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "Updated existing coverage pull request: $EXISTING_PR" | |
| else | |
| gh pr create \ | |
| --repo "${{ github.repository }}" \ | |
| --base "$BASE_BRANCH" \ | |
| --head "$UPDATE_BRANCH" \ | |
| --title "chore: update API coverage badge and history" \ | |
| --body "Automated coverage metadata update generated by [workflow run ${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." | |
| fi | |
| # Save cache even if build fails (to enable beta cache priming) | |
| # Save on PR events AND on pushes to main branches (to update cache after assembly updates) | |
| - name: Save Game Assemblies to Cache | |
| uses: actions/cache/save@v4 | |
| if: always() && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable'))) && steps.cache-assemblies.outputs.cache-hit != 'true' | |
| with: | |
| path: S1API/ScheduleOneAssemblies | |
| key: game-assemblies-v3-${{ steps.assembly-branch.outputs.branch }}-${{ hashFiles('S1API/S1API.csproj') }} | |
| - name: Coverage Skipped Notice | |
| if: steps.verify-assemblies.outputs.has_assemblies != 'true' | |
| run: | | |
| echo "::warning::Coverage analysis was skipped because game assemblies are not available." | |
| echo "This is expected for external PRs or when the assemblies repository is not accessible." |