Compare commits

...

2 commits

Author SHA1 Message Date
feibisi
868a86b416 refactor: wp-release.yml 同步为生产版本
Some checks are pending
gitleaks 密钥泄露扫描 / gitleaks (push) Waiting to run
Go 项目 CI / ci (push) Has been skipped
TypeScript/JS 项目 CI / ci (push) Has been skipped
WordPress 插件 CI / ci (push) Has been skipped
- 版本一致性校验(tag vs 插件头 vs readme.txt)
- PHP lint + SHA-256 校验和
- 智能 release 创建/更新 + 资产上传重试
- 可选:部署后 composer repair、更新 API 验证、AI 通知

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-18 15:28:10 +08:00
feibisi
832951010a docs: WordPress 插件 CI 修复设计
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-18 15:15:09 +08:00
5 changed files with 417 additions and 73 deletions

View file

@ -1,99 +1,399 @@
name: WordPress 插件自动发布
# 文派统一插件发布 CI Workflow
# 触发push tag v*
# 运行环境forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
name: Release Plugin

on:
push:
tags:
- 'v*'
- '[0-9]+.[0-9]+*'
- "v*"

env:
PLUGIN_SLUG: ${{ github.event.repository.name }}

jobs:
release:
runs-on: docker
container:
image: php:8.2-cli-alpine
runs-on: ubuntu-latest
steps:
- name: Install system dependencies
run: |
apk add --no-cache git curl zip unzip rsync npm composer python3

- name: Checkout
uses: https://code.forgejo.org/actions/checkout@v4
uses: actions/checkout@v4

- name: Install WP-CLI
- name: Verify tools
shell: bash
run: |
curl -sO https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
set -euo pipefail
php -v | head -1
git --version
rsync --version | head -1
zip --version | head -2
jq --version
curl --version | head -1

- name: Install dependencies
- name: Extract version from tag
id: version
shell: bash
run: |
if [ -f composer.json ]; then
composer install --no-dev --optimize-autoloader --no-interaction
fi
if [ -f package.json ]; then
npm ci --production 2>/dev/null || true
set -euo pipefail
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Tag: $TAG, Version: $VERSION"

- name: Detect main plugin file
id: detect
shell: bash
run: |
set -euo pipefail
MAIN_FILE=$(grep -rl "Plugin Name:" *.php 2>/dev/null | head -1)
if [ -z "$MAIN_FILE" ]; then
echo "::error::No main plugin file found"
exit 1
fi
echo "main_file=$MAIN_FILE" >> "$GITHUB_OUTPUT"
echo "Main file: $MAIN_FILE"

- name: Run PHPCS (if configured)
- name: Validate version consistency
shell: bash
run: |
if [ -f phpcs.xml ] || [ -f phpcs.xml.dist ] || [ -f .phpcs.xml ] || [ -f .phpcs.xml.dist ]; then
composer lint 2>/dev/null || vendor/bin/phpcs . || {
echo "::error::PHPCS 检查失败,请修复代码规范问题后重新打 tag。"
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
MAIN_FILE="${{ steps.detect.outputs.main_file }}"
HEADER_VERSION=$(grep -i "Version:" "$MAIN_FILE" | grep -v "Requires" | grep -v "Tested" | head -1 | sed "s/.*Version:[[:space:]]*//" | sed "s/[[:space:]]*$//" | tr -d "\r")
echo "Extracted header version: [$HEADER_VERSION]"
if [ "$HEADER_VERSION" != "$VERSION" ]; then
echo "::error::Version mismatch: tag=$VERSION, header=$HEADER_VERSION"
exit 1
fi
if [ -f "readme.txt" ]; then
STABLE_TAG=$(grep -i "^Stable tag:" readme.txt | head -1 | sed "s/.*Stable tag:[[:space:]]*//" | sed "s/[[:space:]]*$//" | tr -d "\r")
if [ -n "$STABLE_TAG" ] && [ "$STABLE_TAG" != "$VERSION" ]; then
echo "::error::Stable tag mismatch: tag=$VERSION, readme=$STABLE_TAG"
exit 1
}
else
echo "未找到 PHPCS 配置,跳过代码规范检查。"
fi
fi
echo "Version consistency check passed: $VERSION"

- name: Generate i18n files
- name: PHP Lint
shell: bash
run: |
SLUG=$(basename $GITHUB_REPOSITORY)
if [ -d languages ] || [ -d lang ]; then
LANG_DIR=$([ -d languages ] && echo languages || echo lang)
wp i18n make-pot . "$LANG_DIR/$SLUG.pot" --allow-root 2>/dev/null || true
wp i18n make-json "$LANG_DIR/" --no-purge --allow-root 2>/dev/null || true
set -euo pipefail
ERRORS=0
while IFS= read -r file; do
if ! php -l "$file" > /dev/null 2>&1; then
php -l "$file"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./vendor/*" -not -path "./node_modules/*")
if [ "$ERRORS" -gt 0 ]; then
echo "::error::PHP lint found $ERRORS error(s)"
exit 1
fi
echo "PHP lint passed"

- name: Build release ZIP
- name: Build ZIP
id: build
shell: bash
run: |
PLUGIN_SLUG=$(basename $GITHUB_REPOSITORY)
TAG_NAME=${GITHUB_REF#refs/tags/}
mkdir -p /tmp/release
rsync -a --exclude='.git' --exclude='.forgejo' --exclude='.github' \
--exclude='node_modules' --exclude='.phpcs*' --exclude='phpstan*' \
--exclude='tests' --exclude='.editorconfig' --exclude='.gitignore' \
--exclude='phpunit*' --exclude='Gruntfile*' --exclude='webpack*' \
--exclude='package.json' --exclude='package-lock.json' \
--exclude='composer.lock' --exclude='.env*' \
. /tmp/release/$PLUGIN_SLUG/
cd /tmp/release
zip -r /tmp/$PLUGIN_SLUG-$TAG_NAME.zip $PLUGIN_SLUG/
echo "ZIP_PATH=/tmp/$PLUGIN_SLUG-$TAG_NAME.zip" >> $GITHUB_ENV
echo "PLUGIN_SLUG=$PLUGIN_SLUG" >> $GITHUB_ENV
echo "TAG_NAME=$TAG_NAME" >> $GITHUB_ENV
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
SLUG="${{ env.PLUGIN_SLUG }}"
ZIP_NAME="${SLUG}-${VERSION}.zip"
BUILD_DIR="/tmp/build/${SLUG}"
RELEASE_DIR="/tmp/release"

- name: Create Forgejo Release
rm -rf "$BUILD_DIR" "$RELEASE_DIR"
mkdir -p "$BUILD_DIR" "$RELEASE_DIR"

rsync -a \
--exclude=".git" \
--exclude=".github" \
--exclude=".forgejo" \
--exclude=".gitignore" \
--exclude=".gitattributes" \
--exclude=".editorconfig" \
--exclude=".env*" \
--exclude="node_modules" \
--exclude="tests" \
--exclude="phpunit.xml*" \
--exclude="phpcs.xml*" \
--exclude="phpstan.neon*" \
--exclude="composer.json" \
--exclude="composer.lock" \
--exclude="package.json" \
--exclude="package-lock.json" \
--exclude="Gruntfile.js" \
--exclude="webpack.config.js" \
--exclude="*.md" \
--exclude="LICENSE" \
--exclude="Makefile" \
--exclude="lib" \
./ "$BUILD_DIR/"

(
cd /tmp/build
zip -qr "${RELEASE_DIR}/${ZIP_NAME}" "${SLUG}/"
)

echo "zip_path=${RELEASE_DIR}/${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "release_dir=${RELEASE_DIR}" >> "$GITHUB_OUTPUT"
echo "Built: ${ZIP_NAME} ($(du -h "${RELEASE_DIR}/${ZIP_NAME}" | cut -f1))"

- name: Calculate SHA-256
id: checksum
shell: bash
run: |
set -euo pipefail
ZIP_PATH="${{ steps.build.outputs.zip_path }}"
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
RELEASE_DIR="${{ steps.build.outputs.release_dir }}"
SHA256=$(sha256sum "$ZIP_PATH" | cut -d" " -f1)
echo "$SHA256 $ZIP_NAME" > "${RELEASE_DIR}/${ZIP_NAME}.sha256"
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
echo "SHA-256: $SHA256"
ls -la "$RELEASE_DIR"

- name: Create or Update Release & Upload Assets
shell: bash
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
RELEASE_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-H "Content-Type: application/json" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases" \
-d "{
\"tag_name\": \"$TAG_NAME\",
\"name\": \"$PLUGIN_SLUG $TAG_NAME\",
\"body\": \"自动发布 $TAG_NAME\",
\"draft\": false,
\"prerelease\": false
}")
RELEASE_ID=$(echo $RELEASE_RESPONSE | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
if [ -z "$RELEASE_ID" ]; then
echo "::warning::Release 创建失败或已存在。"
exit 0
set -euo pipefail

AUTH_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-}}"
if [ -z "$AUTH_TOKEN" ]; then
echo "::error::Missing auth token: set RELEASE_TOKEN or use default GITHUB_TOKEN"
exit 1
fi
curl -s -X POST \
-H "Authorization: token $RELEASE_TOKEN" \
-F "attachment=@$ZIP_PATH" \
"$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?name=$PLUGIN_SLUG-$TAG_NAME.zip"
echo "Release $TAG_NAME 发布成功!"

TAG="${{ steps.version.outputs.tag }}"
SLUG="${{ env.PLUGIN_SLUG }}"
RELEASE_DIR="${{ steps.build.outputs.release_dir }}"
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
SHA256="${{ steps.checksum.outputs.sha256 }}"
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
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 ">>> Resolving release ${TAG}"
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
-H "$AUTH_HEADER" \
"${API_URL}/releases/tags/${TAG}")

if [ "$STATUS" = "200" ]; then
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
echo "Release exists (id=${RELEASE_ID}), patching metadata"
curl -sS -f -X PATCH \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg name "$TAG" --arg body "$RELEASE_NOTES" '{name: $name, body: $body, draft: false, prerelease: false}')" \
"${API_URL}/releases/${RELEASE_ID}" > /tmp/release.json
elif [ "$STATUS" = "404" ]; then
echo "Release not found, creating"
curl -sS -f -X POST \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg tag "$TAG" --arg name "$TAG" --arg body "$RELEASE_NOTES" '{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')" \
"${API_URL}/releases" > /tmp/release.json
else
echo "::error::Failed to query release (HTTP ${STATUS})"
cat /tmp/release.json
exit 1
fi

RELEASE_ID=$(jq -r '.id' /tmp/release.json)
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
echo "::error::Failed to resolve release id"
cat /tmp/release.json
exit 1
fi

echo ">>> Uploading assets to release ${RELEASE_ID}"
for FILE in "${RELEASE_DIR}"/*; do
FILENAME=$(basename "$FILE")
EXISTING_ASSET_ID=$(jq -r --arg n "$FILENAME" '.assets[]? | select(.name == $n) | .id' /tmp/release.json | head -1)

if [ -n "$EXISTING_ASSET_ID" ] && [ "$EXISTING_ASSET_ID" != "null" ]; then
echo " deleting old asset: ${FILENAME} (id=${EXISTING_ASSET_ID})"
curl -sS -f -X DELETE \
-H "$AUTH_HEADER" \
"${API_URL}/releases/${RELEASE_ID}/assets/${EXISTING_ASSET_ID}" > /dev/null
fi

ENCODED_NAME=$(printf "%s" "$FILENAME" | jq -sRr @uri)
echo " uploading: ${FILENAME}"
curl -sS -f --retry 3 --retry-delay 2 --retry-all-errors \
-X POST \
-H "$AUTH_HEADER" \
-F "attachment=@${FILE}" \
"${API_URL}/releases/${RELEASE_ID}/assets?name=${ENCODED_NAME}" > /dev/null
done

echo ">>> Verifying uploaded assets"
curl -sS -f -H "$AUTH_HEADER" "${API_URL}/releases/${RELEASE_ID}" > /tmp/release-final.json
for FILE in "${RELEASE_DIR}"/*; do
FILENAME=$(basename "$FILE")
if ! jq -e --arg n "$FILENAME" '.assets[]? | select(.name == $n)' /tmp/release-final.json > /dev/null; then
echo "::error::Missing uploaded asset: ${FILENAME}"
jq -r '.assets[]?.name' /tmp/release-final.json
exit 1
fi
done

echo ">>> Release ${TAG} published successfully"
echo "Release URL: ${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"

- name: Run post-release composer repair on target site (optional)
if: ${{ secrets.DEPLOY_HOST != '' && secrets.DEPLOY_SSH_KEY != '' }}
shell: bash
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_COMMAND_MODE: ${{ secrets.DEPLOY_COMMAND_MODE }}
DEPLOY_SITE_PATH: ${{ secrets.DEPLOY_SITE_PATH }}
DEPLOY_WP_USER: ${{ secrets.DEPLOY_WP_USER }}
DEPLOY_COMPOSER_SCRIPT: ${{ secrets.DEPLOY_COMPOSER_SCRIPT }}
run: |
set -euo pipefail

: "${DEPLOY_HOST:?DEPLOY_HOST is required when this step is enabled}"
DEPLOY_PORT="${DEPLOY_PORT:-22}"
DEPLOY_USER="${DEPLOY_USER:-wpdeploy}"
DEPLOY_COMMAND_MODE="${DEPLOY_COMMAND_MODE:-runner}"
SLUG="${{ env.PLUGIN_SLUG }}"

if ! command -v ssh >/dev/null 2>&1; then
if command -v apk >/dev/null 2>&1; then
apk add --no-cache openssh-client >/dev/null
else
echo "::error::ssh client not found in runner image"
exit 1
fi
fi

install -m 700 -d ~/.ssh
KEY_PATH="$HOME/.ssh/deploy_key"
printf '%s\n' "$DEPLOY_SSH_KEY" > "$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

# Run only for this plugin to reduce release-time impact.
if [[ "$DEPLOY_COMMAND_MODE" == "runner" ]]; then
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}" \
"sudo '$DEPLOY_COMPOSER_SCRIPT' '$SLUG'"
else
DEPLOY_SITE_PATH="${DEPLOY_SITE_PATH:-/www/wwwroot/wptea.com}"
DEPLOY_WP_USER="${DEPLOY_WP_USER:-www}"
DEPLOY_COMPOSER_SCRIPT="${DEPLOY_COMPOSER_SCRIPT:-/opt/wenpai-infra/ops/wp-post-release-composer.sh}"
ssh -i "$KEY_PATH" -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" \
"bash '$DEPLOY_COMPOSER_SCRIPT' --site '$DEPLOY_SITE_PATH' --wp-user '$DEPLOY_WP_USER' --plugin '$SLUG' --strict"
fi

- name: Verify update API metadata (optional)
if: ${{ secrets.UPDATE_API_BASE != '' }}
shell: bash
env:
UPDATE_API_BASE: ${{ secrets.UPDATE_API_BASE }}
VERIFY_FROM_VERSION: ${{ secrets.VERIFY_FROM_VERSION }}
run: |
set -euo pipefail

API_BASE="${UPDATE_API_BASE%/}"
FROM_VERSION="${VERIFY_FROM_VERSION:-0.0.0}"
SLUG="${{ env.PLUGIN_SLUG }}"
MAIN_FILE="${{ steps.detect.outputs.main_file }}"
VERSION="${{ steps.version.outputs.version }}"
TAG="${{ steps.version.outputs.tag }}"
PLUGIN_FILE="${SLUG}/${MAIN_FILE}"

echo "Verify update-check: ${PLUGIN_FILE} from ${FROM_VERSION} -> ${VERSION}"
REQUEST_JSON=$(jq -cn --arg plugin_file "$PLUGIN_FILE" --arg from "$FROM_VERSION" '{plugins: {($plugin_file): {Version: $from}}}')

UPDATE_OK=0
for attempt in $(seq 1 36); do
curl -sS -f -X POST \
-H "Content-Type: application/json" \
--data "$REQUEST_JSON" \
"${API_BASE}/api/v1/update-check" > /tmp/update-check.json

API_VERSION=$(jq -r --arg plugin_file "$PLUGIN_FILE" '.plugins[$plugin_file].version // empty' /tmp/update-check.json)
API_PACKAGE=$(jq -r --arg plugin_file "$PLUGIN_FILE" '.plugins[$plugin_file].package // empty' /tmp/update-check.json)

if [[ "$API_VERSION" == "$VERSION" && "$API_PACKAGE" == *"/releases/download/${TAG}/"* ]]; then
UPDATE_OK=1
break
fi

echo "[attempt ${attempt}/36] update-check not ready, version=${API_VERSION:-<empty>} package=${API_PACKAGE:-<empty>}"
sleep 10
done

if [[ "$UPDATE_OK" -ne 1 ]]; then
echo "::error::update-check verification timeout"
cat /tmp/update-check.json
exit 1
fi

echo "Verify plugin info: ${SLUG}"
INFO_OK=0
for attempt in $(seq 1 36); do
curl -sS -f "${API_BASE}/api/v1/plugins/${SLUG}/info" > /tmp/plugin-info.json
INFO_VERSION=$(jq -r '.version // empty' /tmp/plugin-info.json)
INFO_PACKAGE=$(jq -r '.download_link // empty' /tmp/plugin-info.json)

if [[ "$INFO_VERSION" == "$VERSION" && "$INFO_PACKAGE" == *"/releases/download/${TAG}/"* ]]; then
INFO_OK=1
break
fi

echo "[attempt ${attempt}/36] plugin-info not ready, version=${INFO_VERSION:-<empty>} download=${INFO_PACKAGE:-<empty>}"
sleep 10
done

if [[ "$INFO_OK" -ne 1 ]]; then
echo "::error::plugin-info verification timeout"
cat /tmp/plugin-info.json
exit 1
fi

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"

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.DS_Store
.vscode/
.idea/

10
CLAUDE.md Normal file
View file

@ -0,0 +1,10 @@
# CI/CD 工作流模板 (ci-workflows)

## 概览
feiCode 平台的共享 CI 工作流模板WenPai-org/ci-workflows
WordPress 插件的标准化 CIphpcs/parallel-lint/gitleaks。

## AI 指令
- 工作流语法遵循 Forgejo Actions兼容 GitHub Actions
- Runner 标签linux-arm64:host
- 不要直连 GitHub用 feicode.com 的 mirror

View file

@ -35,8 +35,14 @@ Forgejo Actions CI/CD 共享 workflow 模板,供 feicode.com 上的仓库使
- Gitleaks 密钥泄露扫描

### 6. WordPress 插件自动发布 (`wp-release.yml`)
- 打 tag 时自动触发:`v*` 或 `x.y.z` 格式
- 流程:安装依赖 → PHPCS 检查 → i18n 生成 → 构建 ZIP → 创建 Release
- 打 tag 时自动触发:`v*` 格式
- 版本一致性校验tag vs 插件头 vs readme.txt Stable tag
- PHP lint 语法检查
- ZIP 构建 + SHA-256 校验和
- 智能 Release 创建/更新 + 资产上传(带重试和验证)
- 可选:部署后 composer repair需配置 `DEPLOY_*` secrets
- 可选:更新 API 元数据验证(需配置 `UPDATE_API_BASE`
- 可选:失败时通知 AI CI 助手(需配置 `AI_WEBHOOK_ENDPOINT`
- 需要在仓库 Settings → Secrets 中配置 `RELEASE_TOKEN`

### 7. 综合安全扫描 (`security-scan.yml`)

View file

@ -0,0 +1,25 @@
# WordPress 插件 CI 修复设计

## 背景
wp-plugin-ci.yml 已部署到 4 个 WP 插件仓库wpslug CI 绿,其他 3 个因 phpcs 错误失败。

## 问题
- wenpai-updater: 202 errors (1 file)
- wp-woocommerce-pay: 3764 errors (19 files)
- wpbridge: 18097 errors (90 files)

## 方案
phpcbf 自动修复格式 + phpcs.xml 排除不适用规则,参考 wpslug 成功模式。

## 每个仓库的修复步骤
1. clone 到本地
2. phpcbf 自动修复(缩进、空格、换行等)
3. 分析剩余错误,创建 phpcs.xml
4. 确认 phpcs 零 error + parallel-lint 通过
5. commit + push

## phpcs.xml 模板
基于 WordPress-Extrawarning-severity=0排除 vendor/node_modules/tests/lib按项目排除不适用规则。

## 顺序
1. wenpai-updater最小→ 2. wp-woocommerce-pay → 3. wpbridge最大