Skip to content

Build and Release Plugin #17

Build and Release Plugin

Build and Release Plugin #17

Workflow file for this run

name: Build and Release Plugin
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Plugin version (e.g., 1.0.0)"
required: false
type: string
changelog:
description: "Changelog text"
required: false
type: string
default: "Release"
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Bypass Cloudflare for API Access
uses: xiaotianxt/[email protected]
with:
cf_account_id: ${{ secrets.CF_ACCOUNT_ID }}
cf_zone_id: ${{ secrets.CF_ZONE_ID }}
cf_api_token: ${{ secrets.CF_API_TOKEN }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: |
Frontend/App/pnpm-lock.yaml
backend/storage/addons/billingcore/Frontend/App/pnpm-lock.yaml
- name: Determine plugin directory
id: plugin_dir
run: |
# Check if we're in a plugin-only repo or full featherpanel repo
if [ -f "conf.yml" ] && [ -f "build-release.sh" ]; then
# Plugin-only repo (root is the plugin)
echo "PLUGIN_DIR=." >> $GITHUB_OUTPUT
echo "CONF_FILE=./conf.yml" >> $GITHUB_OUTPUT
echo "FRONTEND_DIR=./Frontend/App" >> $GITHUB_OUTPUT
elif [ -f "backend/storage/addons/billingcore/conf.yml" ]; then
# Full featherpanel repo
echo "PLUGIN_DIR=backend/storage/addons/billingcore" >> $GITHUB_OUTPUT
echo "CONF_FILE=backend/storage/addons/billingcore/conf.yml" >> $GITHUB_OUTPUT
echo "FRONTEND_DIR=backend/storage/addons/billingcore/Frontend/App" >> $GITHUB_OUTPUT
else
echo "::error::Could not find plugin directory"
exit 1
fi
- name: Update conf.yml version
id: update_version
run: |
CONF_FILE="${{ steps.plugin_dir.outputs.CONF_FILE }}"
# Determine version from release tag, workflow input, or conf.yml
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [ "${{ github.event_name }}" == "release" ]; then
# Extract version from release tag (remove 'v' prefix if present)
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
else
# Fallback to conf.yml version
VERSION=$(grep -E "^\s*version:" "${CONF_FILE}" | sed -E 's/.*version:\s*["'\'']?([^"'\'']+)["'\'']?/\1/' | tr -d ' ')
fi
if [ -z "${VERSION}" ]; then
echo "::error::Could not determine version"
exit 1
fi
echo "Updating conf.yml version to: ${VERSION}"
# Update version in conf.yml using sed
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS sed requires -i '' with extension
sed -i '' "s/^\(\s*version:\s*\)[\"']\?[^\"']*[\"']\?/\1${VERSION}/" "${CONF_FILE}"
else
# Linux sed
sed -i "s/^\(\s*version:\s*\)[\"']\?[^\"']*[\"']\?/\1${VERSION}/" "${CONF_FILE}"
fi
# Verify the update
UPDATED_VERSION=$(grep -E "^\s*version:" "${CONF_FILE}" | sed -E 's/.*version:\s*["'\'']?([^"'\'']+)["'\'']?/\1/' | tr -d ' ')
if [ "${UPDATED_VERSION}" != "${VERSION}" ]; then
echo "::error::Failed to update version in conf.yml"
echo "Expected: ${VERSION}, Got: ${UPDATED_VERSION}"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "Successfully updated conf.yml version to ${VERSION}"
- name: Make build script executable
run: chmod +x ${{ steps.plugin_dir.outputs.PLUGIN_DIR }}/build-release.sh
- name: Build plugin
id: build
run: ./build-release.sh
working-directory: ${{ steps.plugin_dir.outputs.PLUGIN_DIR }}
- name: Install zip utility
run: sudo apt-get update && sudo apt-get install -y zip
- name: Extract plugin metadata
id: metadata
run: |
# Read conf.yml (use path from plugin_dir step)
CONF_FILE="${{ steps.plugin_dir.outputs.CONF_FILE }}"
# Extract version (use workflow input if provided, otherwise from conf.yml or tag)
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ github.event.inputs.version }}" ]; then
VERSION="${{ github.event.inputs.version }}"
elif [ "${{ github.event_name }}" == "release" ]; then
# Try to extract version from release tag (remove 'v' prefix if present)
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
else
VERSION=$(grep -E "^\s*version:" "${CONF_FILE}" | sed -E 's/.*version:\s*["'\'']?([^"'\'']+)["'\'']?/\1/' | tr -d ' ')
fi
IDENTIFIER=$(grep -E "^\s*identifier:" "${CONF_FILE}" | sed -E 's/.*identifier:\s*["'\'']?([^"'\'']+)["'\'']?/\1/' | tr -d ' ')
# Extract dependencies array - handle YAML array format
DEP_JSON="["
FIRST=true
IN_DEPS=false
while IFS= read -r line; do
# Check if we're in dependencies section
if [[ "$line" =~ ^[[:space:]]*dependencies:[[:space:]]*$ ]] || [[ "$line" =~ ^[[:space:]]*dependencies:[[:space:]]*\{[[:space:]]*$ ]]; then
IN_DEPS=true
continue
fi
# Stop if we hit the next top-level key
if [ "$IN_DEPS" = true ] && [[ "$line" =~ ^[[:space:]]*[a-zA-Z_]+:[[:space:]]* ]] && ! [[ "$line" =~ ^[[:space:]]+- ]]; then
IN_DEPS=false
fi
# Extract dependency entries (only plugin dependencies for API)
if [ "$IN_DEPS" = true ] && [[ "$line" =~ ^[[:space:]]+-[[:space:]]*(.+) ]]; then
DEP="${BASH_REMATCH[1]}"
# Remove quotes if present
DEP=$(echo "$DEP" | sed -E "s/^['\"]|['\"]$//g")
# Only include plugin dependencies (format: plugin=identifier)
if [[ "$DEP" =~ ^plugin= ]]; then
if [ "$FIRST" = true ]; then
FIRST=false
else
DEP_JSON="${DEP_JSON},"
fi
DEP_JSON="${DEP_JSON}\"${DEP}\""
fi
fi
done < "${CONF_FILE}"
DEP_JSON="${DEP_JSON}]"
# If no dependencies found, use empty array
if [ "$DEP_JSON" = "[]" ] || [ "$FIRST" = true ]; then
DEP_JSON="[]"
fi
# Extract target version (min/max panel version)
TARGET=$(grep -E "^\s*target:" "${CONF_FILE}" | sed -E 's/.*target:\s*["'\'']?([^"'\'']+)["'\'']?/\1/' | tr -d ' ')
# Parse target version (e.g., v2 -> 2.0.0)
if [[ "${TARGET}" =~ ^v?([0-9]+) ]]; then
MAJOR_VERSION="${BASH_REMATCH[1]}"
MIN_PANEL_VERSION="${MAJOR_VERSION}.0.0"
MAX_PANEL_VERSION="$((MAJOR_VERSION + 1)).0.0"
else
MIN_PANEL_VERSION="1.0.0"
MAX_PANEL_VERSION="2.0.0"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "identifier=${IDENTIFIER}" >> $GITHUB_OUTPUT
echo "dependencies=${DEP_JSON}" >> $GITHUB_OUTPUT
echo "min_panel_version=${MIN_PANEL_VERSION}" >> $GITHUB_OUTPUT
echo "max_panel_version=${MAX_PANEL_VERSION}" >> $GITHUB_OUTPUT
# Package ID is always 31 for billingcore
PACKAGE_ID="31"
echo "package_id=${PACKAGE_ID}" >> $GITHUB_OUTPUT
# Get changelog
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
CHANGELOG="${{ github.event.inputs.changelog }}"
elif [ "${{ github.event_name }}" == "release" ]; then
CHANGELOG="${{ github.event.release.body }}"
if [ -z "${CHANGELOG}" ] || [ "${CHANGELOG}" = "null" ]; then
CHANGELOG="Release ${VERSION}"
fi
else
CHANGELOG="Release ${VERSION}"
fi
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "${CHANGELOG}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Upload to Cloud API
env:
TEAM_UUID: ${{ secrets.CLOUD_TEAM_UUID }}
API_TOKEN: ${{ secrets.CLOUD_API_TOKEN }}
run: |
if [ -z "${TEAM_UUID}" ] || [ -z "${API_TOKEN}" ]; then
echo "::warning::Cloud API credentials not set. Skipping upload."
echo "Set CLOUD_TEAM_UUID and CLOUD_API_TOKEN secrets to enable automatic upload."
exit 0
fi
# Find the .fpa file created by the build script
PLUGIN_DIR="${{ steps.plugin_dir.outputs.PLUGIN_DIR }}"
# Try to use build step output first
if [ -n "${{ steps.build.outputs.export_file }}" ]; then
EXPORT_FILE_RELATIVE="${{ steps.build.outputs.export_file }}"
if [ "${PLUGIN_DIR}" = "." ]; then
EXPORT_FILE="${EXPORT_FILE_RELATIVE}"
else
EXPORT_FILE="${PLUGIN_DIR}/${EXPORT_FILE_RELATIVE}"
fi
else
# Fallback: find the .fpa file
EXPORT_FILE=$(find "${PLUGIN_DIR}" -maxdepth 1 -name "*.fpa" -type f | head -n1)
if [ -z "${EXPORT_FILE}" ]; then
EXPORT_FILE=$(find . -name "*.fpa" -type f | head -n1)
fi
fi
if [ -z "${EXPORT_FILE}" ] || [ ! -f "${EXPORT_FILE}" ]; then
echo "::error::Export file not found"
echo "Plugin dir: ${PLUGIN_DIR}"
echo "Build output: ${{ steps.build.outputs.export_file }}"
echo "Looking for .fpa files:"
ls -la "${PLUGIN_DIR}"/*.fpa 2>/dev/null || find . -name "*.fpa" -type f 2>/dev/null || echo "No .fpa files found"
exit 1
fi
echo "Using export file: ${EXPORT_FILE}"
echo "Uploading ${EXPORT_FILE} to cloud API..."
# Get changelog (replace newlines with spaces for form data)
CHANGELOG_TEXT="${{ steps.metadata.outputs.changelog }}"
CHANGELOG_TEXT=$(echo "${CHANGELOG_TEXT}" | tr '\n' ' ' | sed 's/ */ /g')
# Upload using curl with multipart form data
# WAF rule: (http.cookie contains "ghagent") or (http.referer wildcard r"https://github.com/*")
# Include both cookie and referer to match the rule
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
"https://cloud.mythical.systems/api/user/packages/${{ steps.metadata.outputs.package_id }}/versions" \
-H "accept: application/json, text/plain, */*" \
-H "User-Agent: GitHub Runner" \
-H "Cookie: remember_token=${API_TOKEN}; ghagent=1" \
-H "Referer: https://github.com/${{ github.repository }}" \
-H "x-team-uuid: ${TEAM_UUID}" \
-F "file=@${EXPORT_FILE}" \
-F "version=${{ steps.metadata.outputs.version }}" \
-F "changelog=${CHANGELOG_TEXT}" \
-F "dependencies=${{ steps.metadata.outputs.dependencies }}" \
-F "min_panel_version=${{ steps.metadata.outputs.min_panel_version }}" \
-F "max_panel_version=${{ steps.metadata.outputs.max_panel_version }}" \
-F "team_uuid=${TEAM_UUID}")
HTTP_CODE=$(echo "${RESPONSE}" | tail -n1)
BODY=$(echo "${RESPONSE}" | sed '$d')
# Debug output
echo "::debug::HTTP Response Code: ${HTTP_CODE}"
echo "::debug::Response preview: $(echo "${BODY}" | head -c 500)"
if [ "${HTTP_CODE}" -ge 200 ] && [ "${HTTP_CODE}" -lt 300 ]; then
echo "::notice::Plugin uploaded successfully! (HTTP ${HTTP_CODE})"
echo "Response: ${BODY}"
else
echo "::error::Failed to upload plugin (HTTP ${HTTP_CODE})"
echo "Response: ${BODY}"
exit 1
fi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: plugin-release
path: ${{ steps.plugin_dir.outputs.PLUGIN_DIR }}/*.fpa
retention-days: 30
- name: Create GitHub Release Asset
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: ${{ steps.plugin_dir.outputs.PLUGIN_DIR }}/*.fpa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}