test: AI CI injection 20260215153619 #3
17 changed files with 459 additions and 1026 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
2026年 05月 04日 星期一 19:22:57 CST
|
|
||||||
2026年 05月 04日 星期一 19:28:08 CST
|
|
||||||
2026年 05月 04日 星期一 19:29:46 CST
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
name: Auto Label
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '30 5 * * 1' # 每周一凌晨 5:30(stale-cleanup 之后)
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-label:
|
|
||||||
runs-on: linux-arm64
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 自动给 PR 打标签
|
|
||||||
env:
|
|
||||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
run: python3 scripts/auto-label.py
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
name: gitleaks 密钥泄露扫描
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['*']
|
|
||||||
pull_request:
|
|
||||||
branches: ['*']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
gitleaks:
|
|
||||||
runs-on: linux-arm64
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Run gitleaks
|
|
||||||
run: |
|
|
||||||
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
|
|
||||||
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1 || {
|
|
||||||
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
else
|
|
||||||
gitleaks detect --source=. --verbose --exit-code 1 || {
|
|
||||||
echo "::error::gitleaks 发现了潜在的密钥泄露!请检查上方输出并移除敏感信息。"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
echo "gitleaks 扫描通过,未发现密钥泄露。"
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
# 文派统一插件发布 CI Workflow
|
# 文派统一插件发布 CI Workflow
|
||||||
# 触发:push tag v*
|
# 触发:push tag v*
|
||||||
# 运行环境:forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
|
# 运行环境:forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
|
||||||
# 基于 ci-workflows/wp-release.yml 2026-02-18 版本
|
|
||||||
name: Release Plugin
|
name: Release Plugin
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -18,18 +17,17 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Verify tools
|
- name: Verify tools
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
php -v | head -1 || true
|
set -euo pipefail
|
||||||
|
php -v | head -1
|
||||||
git --version
|
git --version
|
||||||
rsync --version | head -1 || true
|
rsync --version | head -1
|
||||||
zip --version | head -2 || true
|
zip --version | head -2
|
||||||
jq --version
|
jq --version
|
||||||
curl --version | head -1 || true
|
curl --version | head -1
|
||||||
|
|
||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: version
|
id: version
|
||||||
|
|
@ -76,58 +74,6 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "Version consistency check passed: $VERSION"
|
echo "Version consistency check passed: $VERSION"
|
||||||
|
|
||||||
- name: Generate Changelog
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
TAG="${GITHUB_REF#refs/tags/}"
|
|
||||||
|
|
||||||
PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v' | grep -v "^${TAG}$" | head -1)
|
|
||||||
|
|
||||||
if [ -n "$PREV_TAG" ]; then
|
|
||||||
echo "Changelog: ${PREV_TAG}..${TAG}"
|
|
||||||
COMMITS=$(git log "${PREV_TAG}..${TAG}" --no-merges --pretty=format:"%s (%h)" 2>/dev/null || true)
|
|
||||||
else
|
|
||||||
echo "Changelog: first release, using all commits"
|
|
||||||
COMMITS=$(git log --no-merges --pretty=format:"%s (%h)" 2>/dev/null || true)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$COMMITS" ]; then
|
|
||||||
: > /tmp/changelog.md
|
|
||||||
echo "No commits found for changelog"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
FEAT="" FIX="" OTHER=""
|
|
||||||
while IFS= read -r line; do
|
|
||||||
case "$line" in
|
|
||||||
feat:*|feat\(*) FEAT="${FEAT}\n- ${line}" ;;
|
|
||||||
fix:*|fix\(*) FIX="${FIX}\n- ${line}" ;;
|
|
||||||
*) OTHER="${OTHER}\n- ${line}" ;;
|
|
||||||
esac
|
|
||||||
done <<< "$COMMITS"
|
|
||||||
|
|
||||||
CHANGELOG=""
|
|
||||||
HAS_CATEGORIES=false
|
|
||||||
if [ -n "$FEAT" ]; then
|
|
||||||
CHANGELOG="${CHANGELOG}#### New Features${FEAT}\n\n"
|
|
||||||
HAS_CATEGORIES=true
|
|
||||||
fi
|
|
||||||
if [ -n "$FIX" ]; then
|
|
||||||
CHANGELOG="${CHANGELOG}#### Bug Fixes${FIX}\n\n"
|
|
||||||
HAS_CATEGORIES=true
|
|
||||||
fi
|
|
||||||
if [ -n "$OTHER" ]; then
|
|
||||||
if [ "$HAS_CATEGORIES" = true ]; then
|
|
||||||
CHANGELOG="${CHANGELOG}#### Other Changes${OTHER}\n\n"
|
|
||||||
else
|
|
||||||
CHANGELOG="${OTHER#\\n}\n\n"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%b' "$CHANGELOG" > /tmp/changelog.md
|
|
||||||
echo "Changelog generated ($(echo "$COMMITS" | wc -l) commits)"
|
|
||||||
|
|
||||||
- name: PHP Lint
|
- name: PHP Lint
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -167,13 +113,8 @@ jobs:
|
||||||
--exclude=".gitattributes" \
|
--exclude=".gitattributes" \
|
||||||
--exclude=".editorconfig" \
|
--exclude=".editorconfig" \
|
||||||
--exclude=".env*" \
|
--exclude=".env*" \
|
||||||
--exclude=".agent" \
|
|
||||||
--exclude=".vscode" \
|
|
||||||
--exclude="node_modules" \
|
--exclude="node_modules" \
|
||||||
--exclude="vendor" \
|
|
||||||
--exclude="tests" \
|
--exclude="tests" \
|
||||||
--exclude="docs" \
|
|
||||||
--exclude="lib" \
|
|
||||||
--exclude="phpunit.xml*" \
|
--exclude="phpunit.xml*" \
|
||||||
--exclude="phpcs.xml*" \
|
--exclude="phpcs.xml*" \
|
||||||
--exclude="phpstan.neon*" \
|
--exclude="phpstan.neon*" \
|
||||||
|
|
@ -186,6 +127,7 @@ jobs:
|
||||||
--exclude="*.md" \
|
--exclude="*.md" \
|
||||||
--exclude="LICENSE" \
|
--exclude="LICENSE" \
|
||||||
--exclude="Makefile" \
|
--exclude="Makefile" \
|
||||||
|
--exclude="lib" \
|
||||||
./ "$BUILD_DIR/"
|
./ "$BUILD_DIR/"
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
@ -232,23 +174,7 @@ jobs:
|
||||||
SHA256="${{ steps.checksum.outputs.sha256 }}"
|
SHA256="${{ steps.checksum.outputs.sha256 }}"
|
||||||
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
AUTH_HEADER="Authorization: token ${AUTH_TOKEN}"
|
AUTH_HEADER="Authorization: token ${AUTH_TOKEN}"
|
||||||
|
printf -v RELEASE_NOTES '## %s %s\n\n### Checksums\n\n| File | SHA-256 |\n|------|---------|\n| %s | %s |\n' "$SLUG" "$TAG" "$ZIP_NAME" "$SHA256"
|
||||||
{
|
|
||||||
echo "## ${SLUG} ${TAG}"
|
|
||||||
echo ""
|
|
||||||
CHANGELOG_FILE="/tmp/changelog.md"
|
|
||||||
if [ -f "$CHANGELOG_FILE" ] && [ -s "$CHANGELOG_FILE" ]; then
|
|
||||||
echo "### What's Changed"
|
|
||||||
echo ""
|
|
||||||
cat "$CHANGELOG_FILE"
|
|
||||||
fi
|
|
||||||
echo "### Checksums"
|
|
||||||
echo ""
|
|
||||||
echo "| File | SHA-256 |"
|
|
||||||
echo "|------|---------|"
|
|
||||||
echo "| ${ZIP_NAME} | ${SHA256} |"
|
|
||||||
} > /tmp/release-notes.md
|
|
||||||
RELEASE_NOTES=$(cat /tmp/release-notes.md)
|
|
||||||
|
|
||||||
echo ">>> Resolving release ${TAG}"
|
echo ">>> Resolving release ${TAG}"
|
||||||
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
|
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
|
||||||
|
|
@ -354,6 +280,7 @@ jobs:
|
||||||
chmod 600 "$KEY_PATH"
|
chmod 600 "$KEY_PATH"
|
||||||
ssh-keyscan -t rsa,ecdsa,ed25519 -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
|
ssh-keyscan -t rsa,ecdsa,ed25519 -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run only for this plugin to reduce release-time impact.
|
||||||
if [[ "$DEPLOY_COMMAND_MODE" == "runner" ]]; then
|
if [[ "$DEPLOY_COMMAND_MODE" == "runner" ]]; then
|
||||||
DEPLOY_COMPOSER_SCRIPT="${DEPLOY_COMPOSER_SCRIPT:-/usr/local/sbin/wp-post-release-composer-runner}"
|
DEPLOY_COMPOSER_SCRIPT="${DEPLOY_COMPOSER_SCRIPT:-/usr/local/sbin/wp-post-release-composer-runner}"
|
||||||
ssh -i "$KEY_PATH" -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
ssh -i "$KEY_PATH" -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||||
|
|
@ -434,3 +361,39 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Update API verification passed"
|
echo "Update API verification passed"
|
||||||
|
|
||||||
|
- name: Notify AI CI assistant on failure (optional)
|
||||||
|
if: ${{ failure() && secrets.AI_WEBHOOK_ENDPOINT != '' }}
|
||||||
|
continue-on-error: true
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
AI_WEBHOOK_ENDPOINT: ${{ secrets.AI_WEBHOOK_ENDPOINT }}
|
||||||
|
AI_WEBHOOK_TOKEN: ${{ secrets.AI_WEBHOOK_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OWNER="${GITHUB_REPOSITORY%%/*}"
|
||||||
|
REPO="${GITHUB_REPOSITORY##*/}"
|
||||||
|
BRANCH="${GITHUB_REF#refs/heads/}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||||
|
FAILED_STEPS_JSON="$(printf '%s' '${{ toJson(steps) }}' | jq -c '[to_entries[] | select(.value.outcome=="failure" or .value.conclusion=="failure") | .key]')"
|
||||||
|
|
||||||
|
PAYLOAD="$(jq -cn \
|
||||||
|
--arg event "ci_failure_report" \
|
||||||
|
--arg owner "$OWNER" \
|
||||||
|
--arg repo "$REPO" \
|
||||||
|
--arg sha "${GITHUB_SHA}" \
|
||||||
|
--arg branch "$BRANCH" \
|
||||||
|
--arg workflow "${GITHUB_WORKFLOW}" \
|
||||||
|
--arg run_url "$RUN_URL" \
|
||||||
|
--arg log_excerpt "release workflow failed; inspect run_url for full logs" \
|
||||||
|
--argjson failed_steps "$FAILED_STEPS_JSON" \
|
||||||
|
'{event:$event, owner:$owner, repo:$repo, sha:$sha, branch:$branch, workflow:$workflow, run_url:$run_url, failed_steps:$failed_steps, log_excerpt:$log_excerpt}')"
|
||||||
|
|
||||||
|
CURL_HEADERS=(-H "Content-Type: application/json")
|
||||||
|
if [[ -n "${AI_WEBHOOK_TOKEN:-}" ]]; then
|
||||||
|
CURL_HEADERS+=( -H "Authorization: Bearer ${AI_WEBHOOK_TOKEN}" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -sS -X POST "${CURL_HEADERS[@]}" --data "$PAYLOAD" "$AI_WEBHOOK_ENDPOINT" > /dev/null
|
||||||
|
echo "AI CI assistant notified"
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
name: 安全扫描
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main', 'master']
|
|
||||||
paths:
|
|
||||||
- 'Dockerfile*'
|
|
||||||
- 'docker-compose*.yml'
|
|
||||||
- 'composer.json'
|
|
||||||
- 'composer.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- 'pnpm-lock.yaml'
|
|
||||||
- 'go.mod'
|
|
||||||
- 'go.sum'
|
|
||||||
pull_request:
|
|
||||||
branches: ['main', 'master']
|
|
||||||
schedule:
|
|
||||||
- cron: '0 3 * * 1' # 每周一凌晨 3 点
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
security-scan:
|
|
||||||
runs-on: linux-arm64
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Hadolint Dockerfile 检查
|
|
||||||
run: |
|
|
||||||
# 检查是否存在 Dockerfile
|
|
||||||
DOCKERFILES=$(find . -name 'Dockerfile*' -not -path './.git/*' 2>/dev/null)
|
|
||||||
if [ -z "$DOCKERFILES" ]; then
|
|
||||||
echo "未找到 Dockerfile,跳过"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# 如果未安装 hadolint 则跳过
|
|
||||||
if ! command -v hadolint &> /dev/null; then
|
|
||||||
echo "hadolint 未安装,跳过 Dockerfile 检查"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
FAILED=0
|
|
||||||
for df in $DOCKERFILES; do
|
|
||||||
echo "--- 检查 $df ---"
|
|
||||||
hadolint "$df" || FAILED=1
|
|
||||||
done
|
|
||||||
if [ $FAILED -eq 1 ]; then
|
|
||||||
echo "::warning::Dockerfile 存在规范问题,请检查上方输出"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHP Composer 安全审计
|
|
||||||
run: |
|
|
||||||
if [ ! -f composer.lock ]; then
|
|
||||||
echo "未找到 composer.lock,跳过"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
composer audit --format=table || {
|
|
||||||
echo "::error::Composer 依赖存在已知安全漏洞"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "Composer 安全审计通过"
|
|
||||||
|
|
||||||
- name: npm 安全审计
|
|
||||||
run: |
|
|
||||||
if [ ! -f package-lock.json ] && [ ! -f yarn.lock ] && [ ! -f pnpm-lock.yaml ]; then
|
|
||||||
echo "未找到 JS 锁文件,跳过"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# npm audit 只报告 high 和 critical
|
|
||||||
if [ -f package-lock.json ]; then
|
|
||||||
npm audit --audit-level=high || {
|
|
||||||
echo "::error::npm 依赖存在高危安全漏洞"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
elif [ -f pnpm-lock.yaml ]; then
|
|
||||||
pnpm audit --audit-level=high || {
|
|
||||||
echo "::error::pnpm 依赖存在高危安全漏洞"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
elif [ -f yarn.lock ]; then
|
|
||||||
yarn npm audit --severity high || {
|
|
||||||
echo "::error::yarn 依赖存在高危安全漏洞"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
echo "JS 依赖安全审计通过"
|
|
||||||
|
|
||||||
- name: Go 依赖漏洞检查
|
|
||||||
run: |
|
|
||||||
if [ ! -f go.mod ]; then
|
|
||||||
echo "未找到 go.mod,跳过"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# govulncheck 检查已知漏洞
|
|
||||||
if ! command -v govulncheck &> /dev/null; then
|
|
||||||
go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
||||||
export PATH="$(go env GOPATH)/bin:$PATH"
|
|
||||||
fi
|
|
||||||
govulncheck ./... || {
|
|
||||||
echo "::error::Go 依赖存在已知安全漏洞"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "Go 依赖安全检查通过"
|
|
||||||
|
|
||||||
- name: Trivy 容器镜像扫描
|
|
||||||
run: |
|
|
||||||
DOCKERFILES=$(find . -name 'Dockerfile' -not -path './.git/*' 2>/dev/null)
|
|
||||||
if [ -z "$DOCKERFILES" ]; then
|
|
||||||
echo "未找到 Dockerfile,跳过镜像扫描"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
if ! command -v trivy &> /dev/null; then
|
|
||||||
echo "trivy 未安装,跳过容器镜像扫描"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# 构建并扫描镜像
|
|
||||||
IMAGE_NAME="ci-security-scan:$$"
|
|
||||||
podman build -t "$IMAGE_NAME" -f Dockerfile . || {
|
|
||||||
echo "::warning::容器构建失败,跳过镜像扫描"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
trivy image --severity HIGH,CRITICAL --exit-code 1 "$IMAGE_NAME" || {
|
|
||||||
echo "::error::容器镜像存在高危漏洞"
|
|
||||||
podman rmi "$IMAGE_NAME" 2>/dev/null
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
podman rmi "$IMAGE_NAME" 2>/dev/null
|
|
||||||
echo "容器镜像安全扫描通过"
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
name: Stale Issue/PR 清理
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 5 * * 1' # 每周一凌晨 5 点
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
stale-cleanup:
|
|
||||||
runs-on: linux-arm64
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 清理所有原创仓库的过期 Issue 和 PR
|
|
||||||
env:
|
|
||||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
run: python3 scripts/stale-cleanup.py
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
name: WordPress 插件 CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main', 'master']
|
|
||||||
pull_request:
|
|
||||||
branches: ['main', 'master']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ci:
|
|
||||||
if: github.repository != 'WenPai-org/ci-workflows'
|
|
||||||
runs-on: linux-arm64
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: PHP Parallel Lint
|
|
||||||
run: |
|
|
||||||
parallel-lint --exclude vendor --exclude node_modules .
|
|
||||||
|
|
||||||
- name: PHPCS 代码规范检查
|
|
||||||
run: |
|
|
||||||
# 如果仓库有自定义 phpcs 配置则使用,否则用默认 WordPress 标准
|
|
||||||
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
|
|
||||||
phpcs .
|
|
||||||
else
|
|
||||||
phpcs --standard=WordPress-Extra \
|
|
||||||
--extensions=php \
|
|
||||||
--ignore=vendor/*,node_modules/*,tests/*,lib/* \
|
|
||||||
--report=full \
|
|
||||||
-s .
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Gitleaks 密钥泄露扫描
|
|
||||||
run: |
|
|
||||||
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
|
|
||||||
gitleaks detect --source=. --log-opts="$GITHUB_SHA~1..$GITHUB_SHA" --verbose --exit-code 1
|
|
||||||
else
|
|
||||||
gitleaks detect --source=. --verbose --exit-code 1
|
|
||||||
fi
|
|
||||||
echo "gitleaks 扫描通过"
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
vendor/
|
|
||||||
node_modules/
|
|
||||||
.env
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
# wpslug
|
# wpslug
|
||||||
文派素格 🍤(WPSlug.com)更友好美观的 WordPress 文章、页面、媒体别名 slug ,支持拼音和翻译等自动生成。
|
文派素格 🍤(WPSlug.com)更友好美观的 WordPress 文章、页面、媒体别名 slug ,支持拼音和翻译等自动生成。
|
||||||
|
|
||||||
<!-- CI test 1777893437 -->
|
QA CI marker: 20260215153619
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class WenPai_Updater {
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
const VERSION = '1.2.0';
|
const VERSION = '1.0.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 云桥 API 地址。
|
* 云桥 API 地址。
|
||||||
|
|
@ -168,50 +168,24 @@ class WenPai_Updater {
|
||||||
|
|
||||||
$response = $this->api_request( "plugins/{$this->slug}/info" );
|
$response = $this->api_request( "plugins/{$this->slug}/info" );
|
||||||
|
|
||||||
if ( ! is_wp_error( $response ) && ! isset( $response['error'] ) && ! empty( $response['name'] ) ) {
|
if ( is_wp_error( $response ) ) {
|
||||||
$info = new stdClass();
|
|
||||||
$info->name = $response['name'];
|
|
||||||
$info->slug = $response['slug'] ?? $this->slug;
|
|
||||||
$info->version = $response['version'] ?? '';
|
|
||||||
$info->author = $response['author'] ?? '';
|
|
||||||
$info->homepage = $response['homepage'] ?? '';
|
|
||||||
$info->download_link = $response['download_link'] ?? '';
|
|
||||||
$info->requires = $response['requires'] ?? '';
|
|
||||||
$info->tested = $response['tested'] ?? '';
|
|
||||||
$info->requires_php = $response['requires_php'] ?? '';
|
|
||||||
$info->last_updated = $response['last_updated'] ?? '';
|
|
||||||
$info->icons = $response['icons'] ?? [];
|
|
||||||
$info->banners = $response['banners'] ?? [];
|
|
||||||
$info->sections = array_map( [ $this, 'markdown_to_html' ], $response['sections'] ?? [] );
|
|
||||||
$info->external = true;
|
|
||||||
|
|
||||||
return $info;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 不可用或插件未注册时,用本地插件头信息兜底
|
|
||||||
$plugin_path = WP_PLUGIN_DIR . '/' . $this->plugin_file;
|
|
||||||
if ( ! file_exists( $plugin_path ) ) {
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
$info = new stdClass();
|
||||||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
$info->name = $response['name'] ?? '';
|
||||||
}
|
$info->slug = $response['slug'] ?? $this->slug;
|
||||||
|
$info->version = $response['version'] ?? '';
|
||||||
$plugin_data = get_plugin_data( $plugin_path );
|
$info->author = $response['author'] ?? '';
|
||||||
|
$info->homepage = $response['homepage'] ?? '';
|
||||||
$info = new stdClass();
|
$info->download_link = $response['download_link'] ?? '';
|
||||||
$info->name = $plugin_data['Name'] ?? $this->slug;
|
$info->requires = $response['requires'] ?? '';
|
||||||
$info->slug = $this->slug;
|
$info->tested = $response['tested'] ?? '';
|
||||||
$info->version = $this->version;
|
$info->requires_php = $response['requires_php'] ?? '';
|
||||||
$info->author = $plugin_data['AuthorName'] ?? '';
|
$info->last_updated = $response['last_updated'] ?? '';
|
||||||
$info->homepage = $plugin_data['PluginURI'] ?? '';
|
$info->icons = $response['icons'] ?? [];
|
||||||
$info->requires = $plugin_data['RequiresWP'] ?? '';
|
$info->banners = $response['banners'] ?? [];
|
||||||
$info->requires_php = $plugin_data['RequiresPHP'] ?? '';
|
$info->sections = $response['sections'] ?? [];
|
||||||
$info->sections = [
|
|
||||||
'description' => $plugin_data['Description'] ?? '',
|
|
||||||
];
|
|
||||||
$info->external = true;
|
|
||||||
|
|
||||||
return $info;
|
return $info;
|
||||||
}
|
}
|
||||||
|
|
@ -263,92 +237,4 @@ class WenPai_Updater {
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将 Markdown 文本转换为 HTML。
|
|
||||||
*
|
|
||||||
* 仅处理 API 返回的 changelog 中常见的 Markdown 子集:
|
|
||||||
* 标题、列表、加粗、斜体、行内代码、链接。
|
|
||||||
*
|
|
||||||
* @param string $text Markdown 文本。
|
|
||||||
* @return string HTML。
|
|
||||||
*/
|
|
||||||
private function markdown_to_html( string $text ): string {
|
|
||||||
if ( empty( $text ) || str_starts_with( trim( $text ), '<' ) ) {
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 截断 feicode-ai 自动追加的 AI 摘要(以 HTML 注释标记为界)
|
|
||||||
$text = preg_replace( '/---\s*\n\s*<!--\s*feicode-ai.*$/s', '', $text );
|
|
||||||
|
|
||||||
// 去掉 Checksums 表格(不适合在 WordPress 弹窗展示)
|
|
||||||
$text = preg_replace( '/^###?\s+Checksums\s*\n+(\|.*\n)+/mi', '', $text );
|
|
||||||
|
|
||||||
// 去掉 CI 自动生成的冗余标题行(WordPress 弹窗已显示插件名和版本)
|
|
||||||
$text = preg_replace( '/^##\s+\S+\s+v[\d.]+\s*\n+/m', '', $text );
|
|
||||||
$text = preg_replace( '/^###?\s+What\'s Changed\s*\n+/mi', '', $text );
|
|
||||||
|
|
||||||
$lines = explode( "\n", $text );
|
|
||||||
$html = '';
|
|
||||||
$in_ul = false;
|
|
||||||
|
|
||||||
foreach ( $lines as $line ) {
|
|
||||||
$trimmed = trim( $line );
|
|
||||||
|
|
||||||
if ( '' === $trimmed ) {
|
|
||||||
if ( $in_ul ) {
|
|
||||||
$html .= "</ul>\n";
|
|
||||||
$in_ul = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标题 h2-h4
|
|
||||||
if ( preg_match( '/^(#{2,4})\s+(.+)$/', $trimmed, $m ) ) {
|
|
||||||
if ( $in_ul ) {
|
|
||||||
$html .= "</ul>\n";
|
|
||||||
$in_ul = false;
|
|
||||||
}
|
|
||||||
$level = strlen( $m[1] );
|
|
||||||
$html .= sprintf( "<h%d>%s</h%d>\n", $level, esc_html( $m[2] ), $level );
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无序列表
|
|
||||||
if ( preg_match( '/^[-*]\s+(.+)$/', $trimmed, $m ) ) {
|
|
||||||
if ( ! $in_ul ) {
|
|
||||||
$html .= "<ul>\n";
|
|
||||||
$in_ul = true;
|
|
||||||
}
|
|
||||||
$html .= '<li>' . $this->inline_markdown( $m[1] ) . "</li>\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 普通段落
|
|
||||||
if ( $in_ul ) {
|
|
||||||
$html .= "</ul>\n";
|
|
||||||
$in_ul = false;
|
|
||||||
}
|
|
||||||
$html .= '<p>' . $this->inline_markdown( $trimmed ) . "</p>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $in_ul ) {
|
|
||||||
$html .= "</ul>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理行内 Markdown:加粗、斜体、行内代码、链接。
|
|
||||||
*/
|
|
||||||
private function inline_markdown( string $text ): string {
|
|
||||||
$text = esc_html( $text );
|
|
||||||
$text = preg_replace( '/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text );
|
|
||||||
$text = preg_replace( '/\*(.+?)\*/', '<em>$1</em>', $text );
|
|
||||||
$text = preg_replace( '/`(.+?)`/', '<code>$1</code>', $text );
|
|
||||||
$text = preg_replace( '/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $text );
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,6 @@ class WPSlug_Core {
|
||||||
private $optimizer;
|
private $optimizer;
|
||||||
private $admin;
|
private $admin;
|
||||||
|
|
||||||
/**
|
|
||||||
* Post types that should never have slug conversion applied.
|
|
||||||
* Covers ACF Pro / SCF internal types and other known plugin internals.
|
|
||||||
*/
|
|
||||||
private static $excluded_post_types = array(
|
|
||||||
'acf-field-group',
|
|
||||||
'acf-field',
|
|
||||||
'acf-post-type',
|
|
||||||
'acf-taxonomy',
|
|
||||||
'acf-ui-options-page',
|
|
||||||
);
|
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->settings = new WPSlug_Settings();
|
$this->settings = new WPSlug_Settings();
|
||||||
$this->converter = new WPSlug_Converter();
|
$this->converter = new WPSlug_Converter();
|
||||||
|
|
@ -66,18 +54,6 @@ class WPSlug_Core {
|
||||||
return $title;
|
return $title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip during admin menu registration — sanitize_title() is used to
|
|
||||||
// generate hook suffixes, not content slugs. Stop-word removal would
|
|
||||||
// mangle the suffix and break plugins that match it (e.g. AIOWPM).
|
|
||||||
if (doing_action('admin_menu') || doing_action('network_admin_menu') || doing_action('user_admin_menu')) {
|
|
||||||
return $title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip when ACF internal post types are being saved
|
|
||||||
if ($this->isAcfInternalRequest()) {
|
|
||||||
return $title;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$options = $this->settings->getOptions();
|
$options = $this->settings->getOptions();
|
||||||
|
|
||||||
|
|
@ -108,9 +84,6 @@ class WPSlug_Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
$post_type = $data['post_type'];
|
$post_type = $data['post_type'];
|
||||||
if (in_array($post_type, self::$excluded_post_types, true)) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
if (!$this->settings->isPostTypeEnabled($post_type)) {
|
if (!$this->settings->isPostTypeEnabled($post_type)) {
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
@ -257,10 +230,6 @@ class WPSlug_Core {
|
||||||
return $slug;
|
return $slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($post_type, self::$excluded_post_types, true)) {
|
|
||||||
return $slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->settings->isPostTypeEnabled($post_type)) {
|
if (!$this->settings->isPostTypeEnabled($post_type)) {
|
||||||
return $slug;
|
return $slug;
|
||||||
}
|
}
|
||||||
|
|
@ -321,10 +290,6 @@ class WPSlug_Core {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (in_array($post->post_type, self::$excluded_post_types, true)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->settings->isPostTypeEnabled($post->post_type)) {
|
if (!$this->settings->isPostTypeEnabled($post->post_type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -414,38 +379,6 @@ class WPSlug_Core {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if the current execution context is saving an ACF internal post type.
|
|
||||||
* Checks the call stack for ACF's internal post type class or field save functions.
|
|
||||||
*/
|
|
||||||
private function isAcfInternalRequest() {
|
|
||||||
if (function_exists('get_current_screen')) {
|
|
||||||
$screen = get_current_screen();
|
|
||||||
if ($screen && in_array($screen->post_type, self::$excluded_post_types, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15);
|
|
||||||
foreach ($trace as $frame) {
|
|
||||||
if (isset($frame['class']) && strpos($frame['class'], 'ACF_Internal_Post_Type') !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isset($frame['class']) && strpos($frame['class'], 'ACF_Field_Group') !== false) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isset($frame['function']) && in_array($frame['function'], array(
|
|
||||||
'acf_update_field',
|
|
||||||
'acf_import_field_group',
|
|
||||||
'acf_import_internal_post_type',
|
|
||||||
), true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function deactivate() {
|
public function deactivate() {
|
||||||
flush_rewrite_rules();
|
flush_rewrite_rules();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -376,16 +376,6 @@ class WPSlug_Settings
|
||||||
|
|
||||||
public function isPostTypeEnabled($post_type)
|
public function isPostTypeEnabled($post_type)
|
||||||
{
|
{
|
||||||
$excluded = array(
|
|
||||||
'acf-field-group',
|
|
||||||
'acf-field',
|
|
||||||
'acf-post-type',
|
|
||||||
'acf-taxonomy',
|
|
||||||
'acf-ui-options-page',
|
|
||||||
);
|
|
||||||
if (in_array($post_type, $excluded, true)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$enabled_post_types = $this->getOption("enabled_post_types", []);
|
$enabled_post_types = $this->getOption("enabled_post_types", []);
|
||||||
return empty($enabled_post_types) ||
|
return empty($enabled_post_types) ||
|
||||||
in_array($post_type, $enabled_post_types);
|
in_array($post_type, $enabled_post_types);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@
|
||||||
Plugin Name: WPSlug
|
Plugin Name: WPSlug
|
||||||
Plugin URI: https://wpcy.com/slug
|
Plugin URI: https://wpcy.com/slug
|
||||||
Description: Advanced slug management plugin with Chinese Pinyin support and SEO optimization.
|
Description: Advanced slug management plugin with Chinese Pinyin support and SEO optimization.
|
||||||
Version: 1.2.0
|
Version: 1.1.0
|
||||||
Author: WPSlug
|
Author: WPSlug
|
||||||
Author URI: https://wpcy.com/slug
|
Author URI: https://wpcy.com/slug
|
||||||
License: GPL2
|
License: GPL2
|
||||||
|
|
@ -19,7 +19,7 @@ if (!defined("ABSPATH")) {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
define("WPSLUG_VERSION", "1.2.0");
|
define("WPSLUG_VERSION", "1.1.0");
|
||||||
define("WPSLUG_PLUGIN_DIR", plugin_dir_path(__FILE__));
|
define("WPSLUG_PLUGIN_DIR", plugin_dir_path(__FILE__));
|
||||||
define("WPSLUG_PLUGIN_URL", plugin_dir_url(__FILE__));
|
define("WPSLUG_PLUGIN_URL", plugin_dir_url(__FILE__));
|
||||||
define("WPSLUG_PLUGIN_BASENAME", plugin_basename(__FILE__));
|
define("WPSLUG_PLUGIN_BASENAME", plugin_basename(__FILE__));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue