Skip to content

feat(skins): add song-ru-mist skin #273

feat(skins): add song-ru-mist skin

feat(skins): add song-ru-mist skin #273

Workflow file for this run

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,
});
}