feat(skins): add song-ru-mist skin #273
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: Validate Skin Submission | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, edited] | |
| paths-ignore: | |
| - '.github/workflows/**' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| validate-skin-submission: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout PR head | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate skin submission | |
| id: validate | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ERRORS_FILE="/tmp/validation-errors.txt" | |
| MISMATCH_FILE="/tmp/author-mismatches.txt" | |
| : > "$ERRORS_FILE" | |
| : > "$MISMATCH_FILE" | |
| report_error() { | |
| echo "::error::$1" | |
| echo "- $1" >> "$ERRORS_FILE" | |
| } | |
| PR_AUTHOR="${{ github.event.pull_request.user.login }}" | |
| git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 | |
| mapfile -t changed_files < <(git diff --name-only "origin/${{ github.event.pull_request.base.ref }}...HEAD") | |
| failures=0 | |
| # Validate changed files only touch screenshots/ and skins-pro/ | |
| allowed_prefixes=("screenshots/" "skins-pro/") | |
| for file in "${changed_files[@]}"; do | |
| allowed=0 | |
| for prefix in "${allowed_prefixes[@]}"; do | |
| if [[ "$file" == "$prefix"* ]]; then | |
| allowed=1 | |
| break | |
| fi | |
| done | |
| if [[ $allowed -eq 0 ]]; then | |
| report_error "Change outside allowed paths: $file (only screenshots/ and skins-pro/ are allowed)" | |
| failures=1 | |
| fi | |
| done | |
| # Collect new skin directories from changed files | |
| declare -A skin_names=() | |
| declare -A screenshot_names=() | |
| for file in "${changed_files[@]}"; do | |
| if [[ "$file" =~ ^skins-pro/([^/]+)/ ]]; then | |
| skin_names["${BASH_REMATCH[1]}"]=1 | |
| fi | |
| if [[ "$file" =~ ^screenshots/([^/]+)\.[^./]+$ ]]; then | |
| screenshot_names["${BASH_REMATCH[1]}"]=1 | |
| fi | |
| done | |
| # Merge skin names from both sources | |
| for s in "${!screenshot_names[@]}"; do | |
| skin_names["$s"]=1 | |
| done | |
| if [[ ${#skin_names[@]} -eq 0 ]]; then | |
| echo "No skin-related changes detected; skipping validation." | |
| exit 0 | |
| fi | |
| # Validate each skin | |
| for skin in "${!skin_names[@]}"; do | |
| skin_dir="skins-pro/$skin" | |
| strings_file="$skin_dir/strings.json" | |
| # Skin directory must exist | |
| if [[ ! -d "$skin_dir" ]]; then | |
| report_error "Missing skin directory: $skin_dir" | |
| failures=1 | |
| continue | |
| fi | |
| # Exactly one screenshot per skin | |
| screenshot_count=0 | |
| for sf in screenshots/"$skin".*; do | |
| if [[ -f "$sf" ]]; then | |
| screenshot_count=$((screenshot_count + 1)) | |
| fi | |
| done | |
| if [[ $screenshot_count -eq 0 ]]; then | |
| report_error "Missing preview image for skin '$skin' in screenshots/" | |
| failures=1 | |
| elif [[ $screenshot_count -gt 1 ]]; then | |
| report_error "Multiple preview images for skin '$skin' in screenshots/ (only one allowed)" | |
| failures=1 | |
| fi | |
| # Validate skin directory contents: only image files + theme.css + strings.json | |
| while IFS= read -r -d '' entry; do | |
| rel="${entry#$skin_dir/}" | |
| if [[ "$rel" == "theme.css" || "$rel" == "strings.json" ]]; then | |
| continue | |
| fi | |
| # Check if it's an image file by extension | |
| case "${rel,,}" in | |
| *.png|*.jpg|*.jpeg|*.gif|*.webp|*.bmp|*.svg|*.tiff|*.tif|*.ico|*.avif|*.heic|*.heif) ;; | |
| *) | |
| report_error "Disallowed file in '$skin_dir': $rel (only image files + theme.css + strings.json allowed)" | |
| failures=1 | |
| ;; | |
| esac | |
| done < <(find "$skin_dir" -type f -print0) | |
| # Validate strings.json exists | |
| if [[ ! -f "$strings_file" ]]; then | |
| report_error "Missing strings.json for skin '$skin': $strings_file" | |
| failures=1 | |
| continue | |
| fi | |
| # Validate author field | |
| author=$(jq -r '.author // empty' "$strings_file") | |
| if [ -z "$author" ]; then | |
| report_error "Skin '$skin' must define a non-empty author in $strings_file" | |
| failures=1 | |
| elif [ "$author" != "$PR_AUTHOR" ]; then | |
| echo "$skin|$author" >> "$MISMATCH_FILE" | |
| fi | |
| done | |
| if [[ $failures -ne 0 ]]; then | |
| echo "Skin submission validation failed." | |
| exit 1 | |
| fi | |
| echo "Skin submission validation passed." | |
| - name: Notify author on validation failure | |
| if: failure() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- validation-feedback -->'; | |
| const pr = context.payload.pull_request; | |
| const author = pr.user.login; | |
| let errors = ''; | |
| try { | |
| errors = fs.readFileSync('/tmp/validation-errors.txt', 'utf8').trim(); | |
| } catch (e) { | |
| errors = 'Validation failed. Please check the workflow logs for details.'; | |
| } | |
| const body = [ | |
| marker, | |
| `@${author} 你的皮肤提交有一些问题需要修复 / your skin submission has some issues that need fixing:`, | |
| '', | |
| errors, | |
| '', | |
| '请修复后更新 PR / Please fix these and update your PR.', | |
| ].join('\n'); | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find(c => c.user?.type === 'Bot' && c.body?.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body, | |
| }); | |
| } | |
| - name: Warn on author mismatch | |
| if: success() && !cancelled() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- author-mismatch-warning -->'; | |
| let mismatches = ''; | |
| try { | |
| mismatches = fs.readFileSync('/tmp/author-mismatches.txt', 'utf8').trim(); | |
| } catch (e) { | |
| mismatches = ''; | |
| } | |
| if (!mismatches) { | |
| core.info('No author mismatches detected.'); | |
| return; | |
| } | |
| const pr = context.payload.pull_request; | |
| const author = pr.user.login; | |
| const lines = mismatches.split('\n').map(line => { | |
| const [skin, skinAuthor] = line.split('|'); | |
| return `- **${skin}** (author: ${skinAuthor})`; | |
| }).join('\n'); | |
| const body = [ | |
| marker, | |
| `@${author} 你修改了其他作者创建的皮肤 / You modified skins created by other authors:`, | |
| '', | |
| lines, | |
| '', | |
| '请在 PR 描述中说明改动内容,例如修复了什么问题 / Please describe what you changed in the PR description, e.g. what issue you fixed.', | |
| ].join('\n'); | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| per_page: 100, | |
| }); | |
| const existing = comments.find(c => c.user?.type === 'Bot' && c.body?.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body, | |
| }); | |
| } |