Build and Release Plugin #17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |