Compare commits
2 commits
bb1d0d76a1
...
868a86b416
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
868a86b416 | ||
|
|
832951010a |
5 changed files with 417 additions and 73 deletions
|
|
@ -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
|
||||
fi
|
||||
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: Run PHPCS (if configured)
|
||||
- name: Detect main plugin file
|
||||
id: detect
|
||||
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
|
||||
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
|
||||
}
|
||||
else
|
||||
echo "未找到 PHPCS 配置,跳过代码规范检查。"
|
||||
fi
|
||||
echo "main_file=$MAIN_FILE" >> "$GITHUB_OUTPUT"
|
||||
echo "Main file: $MAIN_FILE"
|
||||
|
||||
- name: Generate i18n files
|
||||
- name: Validate version consistency
|
||||
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
|
||||
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
|
||||
fi
|
||||
fi
|
||||
echo "Version consistency check passed: $VERSION"
|
||||
|
||||
- name: Build release ZIP
|
||||
- name: PHP Lint
|
||||
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
|
||||
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: Create Forgejo Release
|
||||
- name: Build ZIP
|
||||
id: build
|
||||
shell: bash
|
||||
run: |
|
||||
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"
|
||||
|
||||
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
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
10
CLAUDE.md
Normal file
10
CLAUDE.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# CI/CD 工作流模板 (ci-workflows)
|
||||
|
||||
## 概览
|
||||
feiCode 平台的共享 CI 工作流模板(WenPai-org/ci-workflows)。
|
||||
WordPress 插件的标准化 CI:phpcs/parallel-lint/gitleaks。
|
||||
|
||||
## AI 指令
|
||||
- 工作流语法遵循 Forgejo Actions(兼容 GitHub Actions)
|
||||
- Runner 标签:linux-arm64:host
|
||||
- 不要直连 GitHub,用 feicode.com 的 mirror
|
||||
10
README.md
10
README.md
|
|
@ -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`)
|
||||
|
|
|
|||
25
docs/plans/2026-02-18-wp-ci-fix-design.md
Normal file
25
docs/plans/2026-02-18-wp-ci-fix-design.md
Normal 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-Extra,warning-severity=0,排除 vendor/node_modules/tests/lib,按项目排除不适用规则。
|
||||
|
||||
## 顺序
|
||||
1. wenpai-updater(最小)→ 2. wp-woocommerce-pay → 3. wpbridge(最大)
|
||||
Loading…
Add table
Add a link
Reference in a new issue