Repository navigation
Release FPA #13
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 | |
| default: "1.1." | |
| changelog: | |
| description: "Changelog text" | |
| required: false | |
| type: string | |
| default: "Release" | |
| min_panel_version: | |
| description: "Minimum panel version (default: 1.1.2)" | |
| required: false | |
| type: string | |
| default: "1.1.2" | |
| max_panel_version: | |
| description: "Maximum panel version (default: 2.0.0)" | |
| required: false | |
| type: string | |
| default: "2.0.0" | |
| jobs: | |
| build-and-release: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - 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: Cache apt packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| /var/cache/apt/archives | |
| /var/lib/apt/lists | |
| key: ${{ runner.os }}-apt-${{ hashFiles('**/apt-cache-key') }} | |
| restore-keys: | | |
| ${{ runner.os }}-apt- | |
| - name: Install zip utility | |
| run: | | |
| # Check if zip is already installed | |
| if command -v zip &> /dev/null; then | |
| echo "zip is already installed" | |
| zip --version | |
| else | |
| echo "Installing zip..." | |
| sudo apt-get update -qq | |
| sudo apt-get install -y zip | |
| fi | |
| - 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_ARRAY=() | |
| 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 (all dependencies: php, php-ext, composer, plugin) | |
| if [ "$IN_DEPS" = true ] && [[ "$line" =~ ^[[:space:]]+-[[:space:]]*(.+) ]]; then | |
| DEP="${BASH_REMATCH[1]}" | |
| # Remove quotes if present | |
| DEP=$(echo "$DEP" | sed -E "s/^['\"]|['\"]$//g") | |
| # Include all dependencies (php=, php-ext=, composer=, plugin=) | |
| if [[ "$DEP" =~ ^(php|php-ext|composer|plugin)= ]]; then | |
| DEP_ARRAY+=("${DEP}") | |
| fi | |
| fi | |
| done < "${CONF_FILE}" | |
| # Build JSON array properly | |
| if [ ${#DEP_ARRAY[@]} -eq 0 ]; then | |
| DEP_JSON="[]" | |
| else | |
| DEP_JSON="[" | |
| for i in "${!DEP_ARRAY[@]}"; do | |
| if [ $i -gt 0 ]; then | |
| DEP_JSON="${DEP_JSON}," | |
| fi | |
| # Properly quote each dependency | |
| DEP_JSON="${DEP_JSON}\"${DEP_ARRAY[$i]}\"" | |
| done | |
| DEP_JSON="${DEP_JSON}]" | |
| fi | |
| echo "Built dependencies JSON: ${DEP_JSON}" | |
| # Get min/max panel version from workflow input or use defaults | |
| MIN_PANEL_VERSION="1.1.2" # Default | |
| MAX_PANEL_VERSION="2.0.0" # Default | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| # Use workflow inputs if provided | |
| if [ -n "${{ github.event.inputs.min_panel_version }}" ] && [ "${{ github.event.inputs.min_panel_version }}" != "null" ]; then | |
| MIN_PANEL_VERSION="${{ github.event.inputs.min_panel_version }}" | |
| fi | |
| if [ -n "${{ github.event.inputs.max_panel_version }}" ] && [ "${{ github.event.inputs.max_panel_version }}" != "null" ]; then | |
| MAX_PANEL_VERSION="${{ github.event.inputs.max_panel_version }}" | |
| fi | |
| fi | |
| echo "Using min_panel_version: ${MIN_PANEL_VERSION}" | |
| echo "Using max_panel_version: ${MAX_PANEL_VERSION}" | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "identifier=${IDENTIFIER}" >> $GITHUB_OUTPUT | |
| # Output dependencies as JSON string (use EOF to preserve JSON format with quotes) | |
| # Double-check JSON is valid before outputting | |
| if ! echo "${DEP_JSON}" | python3 -m json.tool > /dev/null 2>&1; then | |
| echo "::error::Invalid JSON format before output: ${DEP_JSON}" | |
| exit 1 | |
| fi | |
| # Use printf to ensure exact string is written without shell interpretation | |
| printf "dependencies<<EOF\n%s\nEOF\n" "${DEP_JSON}" >> $GITHUB_OUTPUT | |
| echo "min_panel_version=${MIN_PANEL_VERSION}" >> $GITHUB_OUTPUT | |
| echo "max_panel_version=${MAX_PANEL_VERSION}" >> $GITHUB_OUTPUT | |
| # Debug: Show what dependencies JSON looks like | |
| echo "Dependencies JSON built: ${DEP_JSON}" | |
| # Validate JSON format | |
| echo "${DEP_JSON}" | python3 -m json.tool > /dev/null 2>&1 && echo "✓ Valid JSON" || echo "✗ Invalid JSON format" | |
| # Package ID is always 31 for billingcore | |
| PACKAGE_ID="35" | |
| 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') | |
| # Get dependencies JSON (should be a valid JSON array string like ["php=8.5","php-ext=pdo"]) | |
| DEPENDENCIES_JSON="${{ steps.metadata.outputs.dependencies }}" | |
| # Debug: Show what we're receiving from GitHub Actions output | |
| echo "Dependencies JSON received from output: ${DEPENDENCIES_JSON}" | |
| echo "Length: ${#DEPENDENCIES_JSON}" | |
| echo "Hex dump of received value:" | |
| echo -n "${DEPENDENCIES_JSON}" | od -c | head -3 | |
| # If quotes are missing, rebuild the JSON properly | |
| if [[ ! "${DEPENDENCIES_JSON}" =~ \".*\" ]]; then | |
| echo "::warning::Quotes appear to be missing, rebuilding JSON from array format" | |
| # Extract dependencies from the malformed string [php=8.5,php-ext=pdo] | |
| # Remove brackets, split by comma, add quotes, rejoin | |
| DEPENDENCIES_JSON=$(echo "${DEPENDENCIES_JSON}" | sed 's/^\[//;s/\]$//' | tr ',' '\n' | sed 's/^/"/;s/$/"/' | tr '\n' ',' | sed 's/,$//' | sed 's/^/[/;s/$/]/') | |
| echo "Rebuilt JSON: ${DEPENDENCIES_JSON}" | |
| fi | |
| # Validate it's proper JSON before sending | |
| if ! echo "${DEPENDENCIES_JSON}" | python3 -m json.tool > /dev/null 2>&1; then | |
| echo "::error::Invalid dependencies JSON format: ${DEPENDENCIES_JSON}" | |
| echo "Expected format: [\"php=8.5\",\"php-ext=pdo\"]" | |
| exit 1 | |
| fi | |
| # Ensure JSON is properly formatted (re-encode with python to guarantee valid JSON) | |
| FINAL_DEPENDENCIES_JSON=$(echo "${DEPENDENCIES_JSON}" | python3 -c "import sys, json; print(json.dumps(json.load(sys.stdin)), end='')" 2>/dev/null || echo "${DEPENDENCIES_JSON}") | |
| # Final validation | |
| if ! echo "${FINAL_DEPENDENCIES_JSON}" | python3 -m json.tool > /dev/null 2>&1; then | |
| echo "::error::Final JSON validation failed: ${FINAL_DEPENDENCIES_JSON}" | |
| exit 1 | |
| fi | |
| echo "Final dependencies JSON to send: ${FINAL_DEPENDENCIES_JSON}" | |
| echo "JSON length: ${#FINAL_DEPENDENCIES_JSON}" | |
| # Upload using curl with multipart form data | |
| # Send dependencies directly as form field value (not via file) | |
| # Use printf to ensure exact string is passed without shell interpretation | |
| RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ | |
| "https://cloudrelay.mythicalsystems.xyz/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=${FINAL_DEPENDENCIES_JSON}" \ | |
| -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 }} |