push → lint → package → release → staging/elementary → QA 验收闭环 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
517 lines
20 KiB
YAML
517 lines
20 KiB
YAML
# 文派统一插件发布 CI Workflow
|
||
# 触发:push tag v*
|
||
# 运行环境:forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
|
||
name: Release Plugin
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v*"
|
||
|
||
env:
|
||
PLUGIN_SLUG: ${{ github.event.repository.name }}
|
||
|
||
jobs:
|
||
release:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Verify tools
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
php -v | head -1
|
||
git --version
|
||
rsync --version | head -1
|
||
zip --version | head -2
|
||
jq --version
|
||
curl --version | head -1
|
||
|
||
- name: Extract version from tag
|
||
id: version
|
||
shell: bash
|
||
run: |
|
||
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: Validate version consistency
|
||
shell: bash
|
||
run: |
|
||
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: Generate Changelog
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
TAG="${GITHUB_REF#refs/tags/}"
|
||
|
||
# Find previous tag
|
||
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
|
||
shell: bash
|
||
run: |
|
||
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 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: |
|
||
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
|
||
|
||
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}"
|
||
|
||
# Build release notes with changelog
|
||
{
|
||
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}"
|
||
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: Push ZIP to staging for QA verification
|
||
shell: bash
|
||
run: |
|
||
set -euo pipefail
|
||
STAGING_DIR="/mnt/shared-context/staging/elementary"
|
||
SLUG="${{ env.PLUGIN_SLUG }}"
|
||
ZIP_PATH="${{ steps.build.outputs.zip_path }}"
|
||
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
||
VERSION="${{ steps.version.outputs.version }}"
|
||
|
||
if [ ! -d "$STAGING_DIR" ]; then
|
||
echo "Staging dir not available (NAS not mounted?), skipping"
|
||
exit 0
|
||
fi
|
||
|
||
cp "$ZIP_PATH" "$STAGING_DIR/${SLUG}.zip"
|
||
|
||
# Write metadata for elementary
|
||
cat > "$STAGING_DIR/${SLUG}.meta.json" <<METAEOF
|
||
{
|
||
"plugin": "$SLUG",
|
||
"version": "$VERSION",
|
||
"tag": "${{ steps.version.outputs.tag }}",
|
||
"sha256": "${{ steps.checksum.outputs.sha256 }}",
|
||
"repo": "${{ github.repository }}",
|
||
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
}
|
||
METAEOF
|
||
|
||
# Notify elementary via inbox
|
||
INBOX="/mnt/shared-context/inbox/elementary"
|
||
mkdir -p "$INBOX"
|
||
cat > "$INBOX/$(date +%Y-%m-%d-%H%M)-ci-release-${SLUG}.md" <<MSGEOF
|
||
**来自**: CI (fedora-devops)
|
||
**主题**: ${SLUG} ${VERSION} 已发布,请验收
|
||
|
||
Release 构建完成,zip 已推送到 staging:
|
||
- 文件: staging/elementary/${SLUG}.zip
|
||
- 版本: ${VERSION}
|
||
- SHA256: ${{ steps.checksum.outputs.sha256 }}
|
||
|
||
**期望**: 运行 \`just test-plugin ${SLUG}\`,验收结果写入 staging/fedora-devops/test-results/
|
||
MSGEOF
|
||
|
||
echo "ZIP pushed to staging, elementary notified"
|
||
|
||
- 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"
|