diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f03813 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +openapi.yaml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 492ea17..9d50d65 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,10 @@ -FROM php:7.4-cli - -# install git -RUN apt-get update -RUN apt-get install -y git - - -ARG file_name -ARG version -ARG sandbox -ARG release_mode - -COPY deploy.php /deploy.php -COPY ${file_name} /${file_name} -RUN git clone https://github.com/Freemius/freemius-php-sdk.git /freemius-php-api - -EXPOSE 80/tcp -EXPOSE 80/udp - +FROM php:8.4.7-cli-alpine + +RUN apk add --no-cache git && \ + git clone https://github.com/Freemius/freemius-php-sdk.git /freemius-php-api && \ + rm -rf /freemius-php-api/.git + +COPY deploy.php /deploy.php +COPY ${file_name} /${file_name} + CMD php /deploy.php \ No newline at end of file diff --git a/README.md b/README.md index a5aebdb..848d273 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,144 @@ -# Freemius Deploy - -This Github Action deploys your wordpress plugin on Freemius. It uses the [Freemius PHP SDK](https://github.com/Freemius/freemius-php-sdk.git) and uses some of the functionality of [CodeAtCode/freemius-suite](https://github.com/CodeAtCode/freemius-suite) - -## Arguments - -| Argument | Required | Function | Default | -| -------------- | -------- | ------- | ------- | -| `file_name` | Yes | File name of the to be uploaded wordpress plugin (zip extension). _Note: the file has to be in the root folder of your repository_ | | -| `release_mode` | No | `pending`, `beta`, or `released`. Set to beta to release the product to valid license holders that opted into the beta list. Set to released to release it to all valid license holders. When the product is released, it will be available for download right within the WP Admin dashboard. | `pending` | -| `version` | Yes | This is used to check whether the release is already uploaded. **Action will fail if the release has already been uploaded** | | -| `sandbox` | No | Whether to upload in sandbox mode | `false` | - -## Environment variables - -**Required**: - -- `PUBLIC_KEY` -- `DEV_ID` -- `SECRET_KEY` -- `PLUGIN_SLUG` -- `PLUGIN_ID` - -All these are found in your Freemius dashboard. - -_Tip: store these variables in your [secrets](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets)_ - -## Action outputs (since v0.1.1) - -The action downloads both the **free** and **pro** version and outputs their filenames as outputs: - -- free_version -- pro_version - -You can access these by setting an **id** to your workflow step. Consequently you can upload these as artifacts, or upload them to the wordpress svn repository, for example with [yukihiko-shinoda/action-deploy-wordpress-plugin](https://github.com/yukihiko-shinoda/action-deploy-wordpress-plugin). - -## Example - -```yml -- name: Deploy to Freemius - uses: buttonizer/freemius-deploy@v0.1.2 - with: - file_name: my_wordpress_plugin.zip - release_mode: pending - version: 1.1.0 - sandbox: false - env: - PUBLIC_KEY: ${{ secrets.FREEMIUS_PUBLIC_KEY }} - DEV_ID: 1234 - SECRET_KEY: ${{ secrets.FREEMIUS_SECRET_KEY }} - PLUGIN_SLUG: my-wordpress-plugin - PLUGIN_ID: 4321 -``` +# Freemius Deploy + +This GitHub Action deploys your WordPress plugin on Freemius. It uses the [Freemius PHP SDK](https://github.com/Freemius/freemius-php-sdk.git) and uses some functionality of [CodeAtCode/freemius-suite](https://github.com/CodeAtCode/freemius-suite). +It was created as a fork from [buttonizer/freemius-deploy](https://github.com/buttonizer/freemius-deploy) but with some +updates that clean up the code and allow for more customization. + +## Inputs + +| Input | Required | Description | Default | +|---------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------| +| `file_name` | Yes | File name of the to be uploaded WordPress plugin (zip extension). _Note: the file has to be in the root folder of your repository_ | none | +| `release_mode` | No | `pending`, `beta`, or `released`. Set to beta to release the product to valid license holders that opted into the beta list. Set to released to release it to all valid license holders. When the product is released, it will be available for download right within the WP Admin dashboard. | `pending` | +| `sandbox` | No | Whether to upload in sandbox mode. `True` or `false`. | `false` | +| `limit` | No | The absolute limit of license owners who will receive an update (expected `int`) | none | +| `percentage_limit` | No | The percentage (1-100%) of license owners who will receive an update (expected `int`) | none | +| `is_incremental` | No | Whether to flag the version as incremental or not. `True` or `false`. | `false` | +| `add_contributor` | No | Add Freemius as a contributor to the plugin. `True` or `false`. | `false` | +| `fail_on_duplicate` | No | If the action should fail if the version already exisits on Freemius. | `true` | +| `overwrite` | No | Overwrite a tag with the same version, otherwise skips upload and continues with the rest of the action. `True` or `false`. | `false` | +| `download_free` | No | Controls whether or not to download the free version as part of the action. | `true` | +| `download_premium` | No | Controls whether or not to download the premium version as part of the action. | `true` | + +## Environment variables + +Set the following required environment variables in your GitHub repository [secrets](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets): + +- `DEV_ID`: Your Freemius Developer ID + +- `PUBLIC_KEY`: Your Freemius Public Key + +- `SECRET_KEY`: Your Freemius Secret Key + +- `PLUGIN_SLUG`: Your plugin's slug + +- `PLUGIN_ID`: Your plugin's ID + +Note: The `PUBLIC_KEY` and `SECRET_KEY` are your developer keys from Freemius, not the plugin-specific keys. + +These are all found in your Freemius dashboard. You can find your public key, secret key, and your Dev ID under +the keys section on the bottom of the 'My Profile' page. Your plugin slug and ID can be found on the products' settings +page or in the SDK integration snippet. + +## Action outputs (since v0.1.1) + +The action by default downloads both the **free** and **pro** versions and outputs their filenames as outputs: + +- free_version +- pro_version + +You can access these by setting an **id** to your workflow step. In a later step reference them like this: + +``` +${{ steps.deploy_step_id.outputs.free_version }} +${{ steps.deploy_step_id.outputs.pro_version }} +``` + +These files can then be uploaded as artifacts or deployed to the WordPress SVN repository using actions like +[10up/action-wordpress-plugin-deploy](https://github.com/10up/action-wordpress-plugin-deploy). + +## Example + +```yml +name: Deploy Plugin to Freemius + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + + # If needed + - name: Install dependencies + run: composer install --no-dev --no-progress --optimize-autoloader + + # replace "includes vendor *.php *.txt" with whatever is relevant for you + - name: Zip plugin + run: | + mkdir -p build + zip -r build/plugin.zip includes vendor *.php *.txt + + - name: Deploy to Freemius + uses: Eitan-brightleaf/freemius-deploy@main + with: + file_name: build/plugin.zip + release_mode: released + env: + DEV_ID: ${{ secrets.DEV_ID }} + PUBLIC_KEY: ${{ secrets.FREEMIUS_PUBLIC_KEY }} + SECRET_KEY: ${{ secrets.FREEMIUS_SECRET_KEY }} + PLUGIN_SLUG: my-plugin + PLUGIN_ID: 1234 + + - name: Unzip plugin for WP.org + run: | + mkdir plugin-dir + unzip ${{ steps.freemius_deploy.outputs.free_version }} -d plugin-dir + if [ -d "plugin-dir/${{ inputs.plugin_slug }}" ]; then + mv "plugin-dir/${{ inputs.plugin_slug }}"/* plugin-dir/ + rm -rf "plugin-dir/${{ inputs.plugin_slug }}" + fi + + # Deploy to WordPress.org SVN + - name: Deploy to WordPress.org + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SVN_USERNAME: ${{ secrets.WP_SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.WP_SVN_PASSWORD }} + BUILD_DIR: plugin-dir + SLUG: ${{ inputs.plugin_slug }} + VERSION: ${{ inputs.version }} + +``` + +## Notes + +- Ensure that your plugin ZIP file is present in the root of your repository before running the action. This can be +done through steps prior to this one as in the example. + +- If the `fail_on_duplicate` input is set to true the action will fail if the version you are trying to upload already +exists on Freemius. Default is `true`. Note that if the value is `true` the value of the `overwrite` input will be ignored. + +- If the `overwrite` input is set to true, it will delete the existing version and redeploy it with the provided zip file. +Otherwise, if the version exists already the API will reject the upload, and the action will log this and continue with +updating the status of the deploy (release mode, release limits, etc.). This obviates the need for the previous `version` input +as the Freemius API itself will reject duplicates, removing the need for an extra API call and searching existing versions. + +- If an invalid value was passed to the `release_mode` input it will fall back to pending. +- If a number less than 1 or greater than 100 is given for `percentage_limit` the input will be ignored. +- `limit` and `percentage_limit` are **not** mutually inclusive. You can use them both. +- I plan on potentially updating the action to a JS GitHub action in the future. This should have no impact on your workflow files, +just some performance benefits and the like. +- Any feedback and contributions are welcome. diff --git a/action.yml b/action.yml index d66076d..75a4cd3 100644 --- a/action.yml +++ b/action.yml @@ -1,20 +1,48 @@ name: "Deploy on Freemius" description: "Uploads and deploys your plugin on Freemius" inputs: - sandbox: - description: 'Sandbox mode' - required: false - default: false file_name: description: 'file to deploy' required: true - version: - description: 'tag version' - required: true release_mode: description: 'release mode' required: false default: 'pending' + sandbox: + description: 'Sandbox mode' + required: false + default: 'false' + limit: + description: 'limit of serving updates' + required: false + percentage_limit: + description: 'The percentage (1-100%) of license owners who will receive an update' + required: false + is_incremental: + description: 'Whether to flag the version as incremental or not' + required: false + default: 'false' + add_contributor: + description: 'add freemius as a contributor to the plugin' + required: false + default: 'false' + fail_on_duplicate: + description: 'if the action should fail if the version was already uploaded' + default: 'true' + required: false + overwrite: + description: 'overwrite a tag with the same version, otherwise skips upload' + default: 'false' + required: false + download_free: + description: 'should the action download the free version after deployment' + required: false + default: 'true' + download_premium: + description: 'should the action download the premium version after deployment' + required: false + default: 'true' + outputs: free_version: description: 'The Free version file' diff --git a/deploy.php b/deploy.php index 49a37aa..14e09aa 100644 --- a/deploy.php +++ b/deploy.php @@ -1,82 +1,206 @@ Api('plugins/'.$_ENV['PLUGIN_ID'].'/tags.json', 'GET'); - if ( $deploy->tags[0]->version === $version ) { - $deploy = $deploy->tags[0]; - echo '-Package already deployed on Freemius'."\n"; - } else { - // Upload the zip - $deploy = $api->Api('plugins/'.$_ENV['PLUGIN_ID'].'/tags.json', 'POST', array( - 'add_contributor' => false - ), array( - 'file' => $file_name - )); - - if (!property_exists($deploy, 'id')) { - print_r($deploy); - die(); - } - - echo "- Deploy done on Freemius\n"; - - $is_released = $api->Api('plugins/'.$_ENV['PLUGIN_ID'].'/tags/'.$deploy->id.'.json', 'PUT', array( - 'release_mode' => $release_mode - ), array()); - - echo "- Set as released on Freemius\n"; - } - - echo "- Download Freemius free version\n"; - - // Generate url to download the zip - $zip_free = $api->GetSignedUrl('plugins/'.$_ENV['PLUGIN_ID'].'/tags/'.$deploy->id.'.zip', array()); - $path = pathinfo($file_name); - $zipname_free = $path['dirname'] . '/' . basename($file_name, '.zip'); - $zipname_free .= '__free.zip'; - - file_put_contents($zipname_free,file_get_contents($zip_free)); - - echo "- Downloaded Freemius free version to ".$zipname_free."\n"; - echo "::set-output name=free_version::" . $zipname_free . "\n"; - - // Generate url to download the pro zip - $zip_pro = $api->GetSignedUrl('plugins/'.$_ENV['PLUGIN_ID'].'/tags/'.$deploy->id.'.zip?is_premium=true', array()); - $path = pathinfo($file_name); - $zipname_pro = $path['dirname'] . '/' . basename($file_name, '.zip'); - $zipname_pro .= '.zip'; - - file_put_contents($zipname_pro,file_get_contents($zip_pro)); - - echo "- Downloaded Freemius pro version to ".$zipname_pro."\n"; - echo "::set-output name=pro_version::" . $zipname_pro . "\n"; +/** + * Freemius Plugin Deployment Script + * + * This script handles the deployment of WordPress plugins to Freemius. + * It validates environment variables, handles file uploads, and manages + * version deployments in different release modes (pending/beta/released). + */ + +// Validate required environment variables +$required_env = ['DEV_ID', 'PUBLIC_KEY', 'SECRET_KEY', 'PLUGIN_SLUG', 'PLUGIN_ID']; +foreach ($required_env as $env) { + if ( empty($_ENV[$env]) ) { + echo "Error: Required environment variable $env is missing or empty\n"; + exit(1); } - catch (Exception $e) { - echo "- Freemius server has problems\n"; +} +$file_name = $_ENV['INPUT_FILE_NAME']; +if (!file_exists("$file_name")) { + echo "Error: File '$file_name' not found\n"; + exit(1); +} + +// Validate release mode +$release_mode = empty($_ENV['INPUT_RELEASE_MODE']) ? 'pending' : $_ENV['INPUT_RELEASE_MODE']; +if (!in_array($release_mode, ['pending', 'beta', 'released'])) { + $release_mode = 'pending'; //fallback to default + echo "Warning: Invalid release mode '$release_mode', falling back to 'pending'\n"; +} + +// set other input variables +$sandbox = ($_ENV['INPUT_SANDBOX'] === 'true'); +$release_limit = intval($_ENV['INPUT_LIMIT']); +$percentage_limit = intval($_ENV['INPUT_PERCENTAGE_LIMIT']); +$is_incremental = ($_ENV['INPUT_IS_INCREMENTAL'] === 'true'); +$add_contributor = ($_ENV['INPUT_ADD_CONTRIBUTOR'] === 'true'); +$overwrite = ($_ENV['INPUT_OVERWRITE'] === 'true'); +$fail_on_duplicate = ($_ENV['INPUT_FAIL_ON_DUPLICATE'] === 'true'); +$download_free = ($_ENV['INPUT_DOWNLOAD_FREE'] === 'true'); +$download_premium = ($_ENV['INPUT_DOWNLOAD_PREMIUM'] === 'true'); + +$debugging = !empty($_ENV['ACTIONS_STEP_DEBUG']) && $_ENV['ACTIONS_STEP_DEBUG'] === 'true'; + +echo "\n- Deploying " . $_ENV['PLUGIN_SLUG'] . " to Freemius, with arguments: "; +echo "\n- file_name: $file_name release_mode: $release_mode sandbox: $sandbox release_limit: $release_limit percentage_limit: $percentage_limit +is_incremental: $is_incremental add_contributor: $add_contributor overwrite: $overwrite\n"; + +// Include Freemius SDK files +require_once '/freemius-php-api/freemius/FreemiusBase.php'; +require_once '/freemius-php-api/freemius/Freemius.php'; + +// Configure Freemius API constants +const FS__API_SCOPE = 'developer'; +define('FS__API_DEV_ID', $_ENV['DEV_ID']); +define('FS__API_PUBLIC_KEY', $_ENV['PUBLIC_KEY']); +define('FS__API_SECRET_KEY', $_ENV['SECRET_KEY']); + +echo "\n- Deploy in progress on Freemius\n"; + +try { + // Init SDK. + $api = new Freemius_Api(FS__API_SCOPE, FS__API_DEV_ID, FS__API_PUBLIC_KEY, FS__API_SECRET_KEY, $sandbox); + + if (!is_object($api)) { + echo "Error: Failed to initialize Freemius API client\n"; + exit(1); } + + // Upload the zip + $deploy = $api->Api( 'plugins/' . $_ENV['PLUGIN_ID'] . '/tags.json', 'POST', [ 'add_contributor' => $add_contributor ], [ 'file' => $file_name ] ); + + if ( $debugging ) { + echo "::debug:: response: " . print_r( $deploy, true ) . "\n"; + } + + // if no id then it failed + if ( ! property_exists( $deploy, 'id' ) && property_exists( $deploy, 'error' ) ) { + // if it's a duplicate version error, then our response depends on whether we want to overwrite or not + if ( $deploy->error->http == 400 && 'duplicate_plugin_version' === $deploy->error->code ) { + if ( $fail_on_duplicate ) { + echo "Deploy failed. Product version already exists.\n"; + exit(1); + } + // if we want to overwrite, then we need to delete the existing version and redeploy it. but have to find id first + if ( $overwrite ) { + echo "Product already exists. Searching for id of existing product version\n"; + $id = null; + $version = $deploy->error->data->version; + $query = ['offset' => 0]; + while ( is_null( $id ) ) { + $tags_response = $api->Api('plugins/' . $_ENV['PLUGIN_ID'] . '/tags.json', 'GET', $query ); + if ( $debugging ) { + echo "::debug:: tags_response: " . print_r( $tags_response, true ) . "\n"; + } + + if (empty($tags_response->tags)) { + // no more tags and still didn't find id. something wrong so exit + echo "Could not find version $version. Aborting.\n"; + exit(1); + } + + foreach ($tags_response->tags as $tag) { + if ($tag->version === $version) { + $id = $tag->id; + if ( $debugging ) { + echo "::debug:: Found id: $id\n"; + } + break; + } + } + $query['offset'] += 25; + } + echo "Deleting existing product version\n"; + $api->Api('plugins/' . $_ENV['PLUGIN_ID'] . '/tags/' . $id . '.zip', 'DELETE'); + echo "Redeploying product\n"; + $deploy = $api->Api( 'plugins/' . $_ENV['PLUGIN_ID'] . '/tags.json', 'POST', [ 'add_contributor' => $add_contributor ], [ 'file' => $file_name ] ); + if ( $debugging ) { + echo "::debug:: response: " . print_r( $deploy, true ) . "\n"; + } + if ( ! property_exists( $deploy, 'id' ) ) { + echo "Deploy failed. No id in response object.\n"; + if ( ! $debugging ) { + echo "Response: " . print_r( $deploy, true ) . "\n"; + } + exit( 1 ); + } + + } else { + // not overwriting, continue with script + echo "Deploy failed. Product version already exists.\n"; + } + } else { + // wasn't a duplicate version error, so exit with error + echo "Deploy failed. No id in response object."; + if ( ! $debugging ) { //we didn't already echo the response + echo "Response: " . print_r( $deploy, true ) . "\n"; + } + exit( 1 ); + } + } + + echo "- Deploy done on Freemius\n"; + + $params = [ + 'release_mode' => $release_mode, + 'is_incremental' => $is_incremental, + ]; + if ( ! empty( $release_limit ) ) { + $params['release_limit'] = $release_limit; + } + if ( ! empty( $percentage_limit ) && $percentage_limit > 0 && $percentage_limit <= 100 ) { + $params['percentage_limit'] = $percentage_limit; + } + $is_released = $api->Api('plugins/' . $_ENV['PLUGIN_ID'] . '/tags/' . $deploy->id . '.json', 'PUT', $params ); + + echo "- Updated on Freemius with settings " . var_export( $params, true ) . "\n"; + + if ( $download_free ) { + echo "- Downloading Freemius free version\n"; + + // Generate url to download the zip + $zip_free = $api->GetSignedUrl('plugins/' . $_ENV['PLUGIN_ID'] . '/tags/' . $deploy->id . '.zip'); + $path = pathinfo($file_name); + $zipname_free = $path['dirname'] . '/' . basename($file_name, '.zip'); + $zipname_free .= '__free.zip'; + + file_put_contents($zipname_free, file_get_contents($zip_free)); + + if (!file_exists($zipname_free) || filesize($zipname_free) == 0) { + echo "Error: Failed to download free version or file is empty\n"; + exit(1); + } + + echo "- Downloaded Freemius free version to " . $zipname_free . "\n"; + file_put_contents(getenv('GITHUB_OUTPUT'), "free_version=" . $zipname_free . "\n", FILE_APPEND); + } elseif ( $debugging ) { + echo "::debug:: skipping free version download"; + } + + if ( $download_premium ) { + echo "- Downloading Freemius premium version\n"; + // Generate url to download the pro-zip + $zip_pro = $api->GetSignedUrl('plugins/' . $_ENV['PLUGIN_ID'] . '/tags/' . $deploy->id . '.zip?is_premium=true'); + $path = pathinfo($file_name); + $zipname_pro = $path['dirname'] . '/' . basename($file_name, '.zip'); + $zipname_pro .= '__pro.zip'; + + file_put_contents($zipname_pro, file_get_contents($zip_pro)); + + if (!file_exists($zipname_pro) || filesize($zipname_pro) == 0) { + echo "Error: Failed to download pro version or file is empty\n"; + exit(1); + } + + echo "- Downloaded Freemius pro version to " . $zipname_pro . "\n"; + file_put_contents(getenv('GITHUB_OUTPUT'), "pro_version=" . $zipname_pro . "\n", FILE_APPEND); + } elseif ( $debugging ) { + echo "::debug:: skipping premium version download"; + } +} catch (Exception $e) { + // Handle any errors during deployment and provide detailed error information + // Exit with non-zero status to indicate failure to GitHub Actions + echo "- Error: " . $e->getMessage() . "\n"; + echo "- Error occurred at line " . $e->getLine() . " in " . $e->getFile() . "\n"; + exit(1); // Return non-zero exit code to indicate failure to GitHub Actions +}