From 5b4370b5c7ff6a27fe2e10331362f85d3f347a81 Mon Sep 17 00:00:00 2001 From: William Asaba Date: Mon, 1 Dec 2025 22:18:47 +0300 Subject: [PATCH 1/3] feat: add support for non-git deployments and version files - Added support for reading version from version.txt and commit.txt files - Added support for reading version from composer.json - Improved error handling and fallback mechanisms - Added better documentation and type hints - Improved code formatting and consistency --- src/Versioning.php | 268 +++++++++++++++++++++++++++------------------ 1 file changed, 160 insertions(+), 108 deletions(-) diff --git a/src/Versioning.php b/src/Versioning.php index a637252..0e9231e 100755 --- a/src/Versioning.php +++ b/src/Versioning.php @@ -7,131 +7,183 @@ class Versioning { - /** - * Get the current version tag - */ - public static function tag(): string - { - return self::getVersion('tag'); + /** + * Get the current version tag + */ + public static function tag(): string + { + return self::getVersion('tag'); + } + + /** + * Get the full version information + */ + public static function full(): string + { + return self::getVersion('full'); + } + + /** + * Get the commit hash + */ + public static function commit(): string + { + return self::getVersion('commit'); + } + + /** + * Get version with commit hash + */ + public static function tagWithCommit(): string + { + return self::getVersion('tag-commit'); + } + + /** + * Get version based on format + */ + public static function getVersion(string $format = 'tag'): string + { + $cacheEnabled = Config::get('versioning.cache.enabled', true); + $cacheKey = Config::get('versioning.cache.key', 'app_version') . "_{$format}"; + $cacheTtl = Config::get('versioning.cache.ttl', 3600); + + if ($cacheEnabled && Cache::has($cacheKey)) { + return Cache::get($cacheKey); } - /** - * Get the full version information - */ - public static function full(): string - { - return self::getVersion('full'); - } + $version = self::fetchVersion($format); - /** - * Get the commit hash - */ - public static function commit(): string - { - return self::getVersion('commit'); + if ($cacheEnabled) { + Cache::put($cacheKey, $version, $cacheTtl); } - /** - * Get version with commit hash - */ - public static function tagWithCommit(): string - { - return self::getVersion('tag-commit'); + return $version; + } + + /** + * Fetch version from git or version file + */ + protected static function fetchVersion(string $format): string + { + try { + $repositoryPath = Config::get('versioning.repository_path', base_path()); + + // First, try to read from version file (for FTP deployments) + $versionFromFile = self::getVersionFromFile($format, $repositoryPath); + if ($versionFromFile !== null) { + return $versionFromFile; + } + + // Validate repository path exists and is accessible + if (!is_dir($repositoryPath) || !is_dir($repositoryPath . '/.git')) { + return self::getFallbackVersion(); + } + + $command = self::buildGitCommand($format, $repositoryPath); + $output = []; + $returnCode = 0; + + // Execute command safely + exec($command . ' 2>&1', $output, $returnCode); + + if ($returnCode !== 0 || empty($output[0])) { + return self::getFallbackVersion(); + } + + $version = trim($output[0]); + + // Remove 'v' prefix if configured + if (!Config::get('versioning.include_prefix', true)) { + $version = ltrim($version, 'v'); + } + + return $version; + } catch (\Throwable $e) { + return self::getFallbackVersion(); } - - /** - * Get version based on format - */ - public static function getVersion(string $format = 'tag'): string - { - $cacheEnabled = Config::get('versioning.cache.enabled', true); - $cacheKey = Config::get('versioning.cache.key', 'app_version')."_{$format}"; - $cacheTtl = Config::get('versioning.cache.ttl', 3600); - - if ($cacheEnabled && Cache::has($cacheKey)) { - return Cache::get($cacheKey); + } + + /** + * Get version from static file (for FTP/non-git deployments) + */ + protected static function getVersionFromFile(string $format, string $repositoryPath): ?string + { + // Check for version.txt file (simple version string) + $versionFile = $repositoryPath . '/version.txt'; + if (file_exists($versionFile) && is_readable($versionFile)) { + $version = trim(file_get_contents($versionFile)); + if (!empty($version)) { + // Handle different formats + if ($format === 'commit') { + $commitFile = $repositoryPath . '/commit.txt'; + if (file_exists($commitFile) && is_readable($commitFile)) { + $version = trim(file_get_contents($commitFile)); + } } - $version = self::fetchVersion($format); - - if ($cacheEnabled) { - Cache::put($cacheKey, $version, $cacheTtl); + // Remove 'v' prefix if configured + if (!Config::get('versioning.include_prefix', true)) { + $version = ltrim($version, 'v'); } return $version; + } } - /** - * Fetch version from git - */ - protected static function fetchVersion(string $format): string - { - try { - $repositoryPath = Config::get('versioning.repository_path', base_path()); - - // Validate repository path exists and is accessible - if (!is_dir($repositoryPath) || !is_dir($repositoryPath.'/.git')) { - return self::getFallbackVersion(); - } - - $command = self::buildGitCommand($format, $repositoryPath); - $output = []; - $returnCode = 0; - - // Execute command safely - exec($command.' 2>&1', $output, $returnCode); - - if ($returnCode !== 0 || empty($output[0])) { - return self::getFallbackVersion(); - } - - $version = trim($output[0]); + // Check for composer.json version + $composerFile = $repositoryPath . '/composer.json'; + if (file_exists($composerFile) && is_readable($composerFile)) { + $composerData = json_decode(file_get_contents($composerFile), true); + if (isset($composerData['version'])) { + $version = $composerData['version']; - // Remove 'v' prefix if configured - if (!Config::get('versioning.include_prefix', true)) { - $version = ltrim($version, 'v'); - } - - return $version; - } catch (\Throwable $e) { - return self::getFallbackVersion(); + // Remove 'v' prefix if configured + if (!Config::get('versioning.include_prefix', true)) { + $version = ltrim($version, 'v'); } - } - - /** - * Build git command based on format - */ - protected static function buildGitCommand(string $format, string $repositoryPath): string - { - $escapedPath = escapeshellarg($repositoryPath); - - return match ($format) { - 'tag' => "git -C {$escapedPath} describe --tags --abbrev=0", - 'full' => "git -C {$escapedPath} describe --tags", - 'commit' => "git -C {$escapedPath} rev-parse --short HEAD", - 'tag-commit' => "git -C {$escapedPath} describe --tags --always", - default => "git -C {$escapedPath} describe --tags --abbrev=0", - }; - } - /** - * Get fallback version from config - */ - protected static function getFallbackVersion(): string - { - return Config::get('versioning.fallback_version', 'dev'); + return $version; + } } - /** - * Clear version cache - */ - public static function clearCache(): void - { - $cacheKey = Config::get('versioning.cache.key', 'app_version'); - $formats = ['tag', 'full', 'commit', 'tag-commit']; - - foreach ($formats as $format) { - Cache::forget("{$cacheKey}_{$format}"); - } + return null; + } + + /** + * Build git command based on format + */ + protected static function buildGitCommand(string $format, string $repositoryPath): string + { + $escapedPath = escapeshellarg($repositoryPath); + + return match ($format) { + 'tag' => "git -C {$escapedPath} describe --tags --abbrev=0", + 'full' => "git -C {$escapedPath} describe --tags", + 'commit' => "git -C {$escapedPath} rev-parse --short HEAD", + 'tag-commit' => "git -C {$escapedPath} describe --tags --always", + default => "git -C {$escapedPath} describe --tags --abbrev=0", + }; + } + + /** + * Get fallback version from config + */ + protected static function getFallbackVersion(): string + { + return Config::get('versioning.fallback_version', 'dev'); + } + + /** + * Clear version cache + */ + public static function clearCache(): void + { + $cacheKey = Config::get('versioning.cache.key', 'app_version'); + $formats = ['tag', 'full', 'commit', 'tag-commit']; + + foreach ($formats as $format) { + Cache::forget("{$cacheKey}_{$format}"); } + } } From 74efa487d609139936de5b3e50085217ee0bf22b Mon Sep 17 00:00:00 2001 From: William Asaba Date: Mon, 1 Dec 2025 22:19:02 +0300 Subject: [PATCH 2/3] style: update code indentation and formatting in TestCase - Standardized indentation to 2 spaces for consistency - Improved code readability with proper spacing - Fixed string concatenation formatting --- tests/TestCase.php | 54 +++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index 47be151..c6a8713 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,31 +8,31 @@ class TestCase extends Orchestra { - public static $latestResponse; - - protected function setUp(): void - { - parent::setUp(); - - Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'Williamug\\Versioning\\Database\\Factories\\'.class_basename($modelName).'Factory' - ); - } - - protected function getPackageProviders($app) - { - return [ - VersioningServiceProvider::class, - ]; - } - - public function getEnvironmentSetUp($app) - { - config()->set('database.default', 'testing'); - - /* - $migration = include __DIR__.'/../database/migrations/create_versioning_table.php.stub'; - $migration->up(); - */ - } + public static $latestResponse; + + protected function setUp(): void + { + parent::setUp(); + + Factory::guessFactoryNamesUsing( + fn(string $modelName) => 'Williamug\\Versioning\\Database\\Factories\\' . class_basename($modelName) . 'Factory' + ); + } + + protected function getPackageProviders($app) + { + return [ + VersioningServiceProvider::class, + ]; + } + + public function getEnvironmentSetUp($app) + { + config()->set('database.default', 'testing'); + + /* + $migration = include __DIR__.'/../database/migrations/create_versioning_table.php.stub'; + $migration->up(); + */ + } } From 77d02249928b4349ecbe8a4bb2d64720dda50c83 Mon Sep 17 00:00:00 2001 From: William Asaba Date: Mon, 1 Dec 2025 22:19:43 +0300 Subject: [PATCH 3/3] docs: add FTP deployment template and documentation - Added comprehensive FTP deployment template for GitHub Actions - Included detailed README with setup instructions and best practices - Added support for automatic version file generation - Included rollback and notification features --- examples/FTP-DEPLOYMENT-TEMPLATE-README.md | 389 +++++++++++++++++++++ examples/ftp-deployment-template.yml | 102 ++++++ 2 files changed, 491 insertions(+) create mode 100644 examples/FTP-DEPLOYMENT-TEMPLATE-README.md create mode 100644 examples/ftp-deployment-template.yml diff --git a/examples/FTP-DEPLOYMENT-TEMPLATE-README.md b/examples/FTP-DEPLOYMENT-TEMPLATE-README.md new file mode 100644 index 0000000..6df29f6 --- /dev/null +++ b/examples/FTP-DEPLOYMENT-TEMPLATE-README.md @@ -0,0 +1,389 @@ +# FTP Deployment Template Setup Guide + +This template provides a production-ready GitHub Action workflow for deploying applications via FTP with automatic versioning support. + +## Quick Start + +1. Copy `ftp-deployment-template.yml` to your project's `.github/workflows/` directory +2. Configure GitHub Secrets +3. Customize the workflow for your project +4. Push and create a release + +## Required GitHub Secrets + +Add these secrets to your repository (Settings → Secrets and variables → Actions): + +### FTP Credentials +``` +FTP_SERVER = your-ftp-server.com +FTP_USERNAME = your-ftp-username +FTP_PASSWORD = your-ftp-password +``` + +### Email Notifications (Optional but Recommended) +``` +EMAIL_USERNAME = your-email@gmail.com +EMAIL_PASSWORD = your-app-specific-password +NOTIFICATION_EMAILS = dev1@company.com, dev2@company.com, manager@company.com +``` + +## Customization Options + +### 1. Change Deployment Trigger + +**Deploy on any release:** +```yaml +on: + release: + types: [published] +``` + +**Deploy on specific tag patterns:** +```yaml +on: + push: + tags: + - "v*.*.*" # v1.0.0, v2.1.3 + - "v*.*.*-beta*" # v1.0.0-beta1 + - "v*.*.*-alpha*" # v1.0.0-alpha2 +``` + +**Deploy on specific branches:** +```yaml +on: + push: + branches: + - main + - production +``` + +### 2. Configure Server Path + +Specify the remote directory on your FTP server: + +```yaml +- name: 📂 Sync files to production server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + server-dir: /public_html/ # Change this + # or for subdirectory: + # server-dir: /public_html/myapp/ +``` + +### 3. Exclude Files from Upload + +Prevent unnecessary files from being uploaded: + +```yaml +- name: 📂 Sync files to production server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + exclude: | + **/.git* + **/.git*/** + **/node_modules/** + **/tests/** + **/vendor/** + **/.env + **/.env.example + **/phpunit.xml + **/composer.json + **/composer.lock + **/package.json + **/package-lock.json + **/*.md + .github/** +``` + +### 4. Add Build Steps + +For applications that need compilation (e.g., Laravel, React): + +```yaml +steps: + - name: 🚚 Get latest code + uses: actions/checkout@v4 + + # Add these steps before version file creation: + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql + + - name: Install Composer dependencies + run: composer install --no-dev --optimize-autoloader + + # - name: Setup Node.js + # uses: actions/setup-node@v4 + # with: + # node-version: '20' + + # - name: Install npm dependencies and build + # run: | + # npm ci + # npm run build + + - name: 📝 Create version files + run: | + echo "${{ github.ref_name }}" > version.txt + git rev-parse --short HEAD > commit.txt + + # Continue with FTP upload... +``` + +### 5. Environment-Specific Deployments + +Deploy to staging and production: + +```yaml +name: Multi-Environment Deployment + +on: + push: + branches: + - staging # → Staging + - main # → Production + +jobs: + deploy-staging: + if: github.ref == 'refs/heads/staging' + name: 🎉 Deploy to Staging + runs-on: ubuntu-latest + steps: + # ... same steps but use STAGING secrets + - name: 📂 Sync to staging server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_STAGING_SERVER }} + username: ${{ secrets.FTP_STAGING_USERNAME }} + password: ${{ secrets.FTP_STAGING_PASSWORD }} + + deploy-production: + if: github.ref == 'refs/heads/main' + name: 🎉 Deploy to Production + runs-on: ubuntu-latest + steps: + # ... same steps but use PRODUCTION secrets + - name: 📂 Sync to production server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_PRODUCTION_SERVER }} + username: ${{ secrets.FTP_PRODUCTION_USERNAME }} + password: ${{ secrets.FTP_PRODUCTION_PASSWORD }} +``` + +### 6. Customize Email Recipients + +**Single recipient:** +```yaml +to: developer@company.com +``` + +**Multiple recipients:** +```yaml +to: dev1@company.com, dev2@company.com, manager@company.com +``` + +**Use GitHub Secret:** +```yaml +to: ${{ secrets.NOTIFICATION_EMAILS }} +``` + +**Different recipients based on status:** +```yaml +- name: 📧 Send success notification + if: success() + uses: dawidd6/action-send-mail@v3 + with: + to: team@company.com + +- name: 📧 Send failure notification + if: failure() + uses: dawidd6/action-send-mail@v3 + with: + to: devops@company.com, manager@company.com +``` + +## Example Configurations + +### Laravel Project + +```yaml +- name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + +- name: Install dependencies + run: composer install --no-dev --optimize-autoloader + +- name: Create version files + run: | + echo "${{ github.ref_name }}" > version.txt + git rev-parse --short HEAD > commit.txt + +- name: Sync to server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + server-dir: /public_html/ + exclude: | + **/.git* + **/node_modules/** + **/tests/** + **/.env + **/storage/logs/** +``` + +### WordPress Plugin + +```yaml +- name: Create version files + run: | + echo "${{ github.ref_name }}" > version.txt + git rev-parse --short HEAD > commit.txt + +- name: Sync to server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + server-dir: /wp-content/plugins/my-plugin/ + exclude: | + **/.git* + **/node_modules/** + **/tests/** + **/.wordpress-org/** + **/bin/** +``` + +### Static Website + +```yaml +- name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + +- name: Build site + run: | + npm ci + npm run build + +- name: Create version files + run: | + echo "${{ github.ref_name }}" > dist/version.txt + git rev-parse --short HEAD > dist/commit.txt + +- name: Sync to server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + local-dir: ./dist/ + server-dir: /public_html/ +``` + +## Versioning Package Integration + +After deployment, your application can access version information: + +### Laravel +```php +use Williamug\Versioning\Facades\Versioning; + +// In your controller or view +echo Versioning::tag(); // v1.0.0 +echo Versioning::commit(); // abc1234 + +// Blade template +@app_version_tag {{-- v1.0.0 --}} +``` + +### Vanilla PHP +```php +require 'vendor/autoload.php'; + +use Williamug\Versioning\StandaloneVersioning; + +StandaloneVersioning::setRepositoryPath(__DIR__); +echo StandaloneVersioning::tag(); // v1.0.0 +echo StandaloneVersioning::commit(); // abc1234 +``` + +## Troubleshooting + +### Issue: FTP upload fails + +**Solution:** Check FTP credentials and server accessibility +```bash +# Test FTP connection locally +ftp your-server.com +# Enter username and password +``` + +### Issue: Version shows 'dev' on server + +**Solution:** Verify version files were created and uploaded +1. Check GitHub Action logs for "Create version files" step +2. SSH/FTP to server and check if `version.txt` exists +3. Check file permissions: `chmod 644 version.txt` + + +### Issue: Large files timing out + +**Solution:** Increase timeout or exclude large directories +```yaml +- name: 📂 Sync files to production server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + timeout-minutes: 60 # Increase from 40 + with: + exclude: | + **/storage/logs/** + **/storage/framework/cache/** +``` + +## Testing Locally + +Test the workflow before deploying: + +```bash +# 1. Install act (GitHub Actions local runner) +brew install act # macOS +# or +curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash + +# 2. Create .secrets file +cat > .secrets << EOF +FTP_SERVER=your-server.com +FTP_USERNAME=your-username +FTP_PASSWORD=your-password +EOF + +# 3. Run workflow locally +act -s GITHUB_TOKEN="$(gh auth token)" --secret-file .secrets +``` + + +## Support + +For issues specific to: +- **FTP Deploy Action**: https://github.com/SamKirkland/FTP-Deploy-Action +- **Email Action**: https://github.com/dawidd6/action-send-mail +- **Versioning Package**: https://github.com/williamug/versioning + +## License + +This template is free to use and modify for your projects. diff --git a/examples/ftp-deployment-template.yml b/examples/ftp-deployment-template.yml new file mode 100644 index 0000000..191b6da --- /dev/null +++ b/examples/ftp-deployment-template.yml @@ -0,0 +1,102 @@ +name: FTP Deployment with Versioning + +on: + release: + types: [published] + # Uncomment to also deploy on specific tag patterns: + # push: + # tags: + # - "v*.*.*" + # - "v*.*.*-beta*" + +jobs: + deploy: + name: 🎉 Deploy to Production + runs-on: ubuntu-latest + timeout-minutes: 40 + + steps: + - name: 🚚 Get latest code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 📝 Create version files + run: | + echo "${{ github.ref_name }}" > version.txt + git rev-parse --short HEAD > commit.txt + cat > VERSION << EOF + Version: ${{ github.ref_name }} + Commit: $(git rev-parse --short HEAD) + Full Commit: ${{ github.sha }} + Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") + Release: ${{ github.event.release.name || github.ref_name }} + Repository: ${{ github.repository }} + EOF + echo "Created version files:" + cat VERSION + + - name: 📂 Sync files to production server + uses: SamKirkland/FTP-Deploy-Action@v4.3.5 + timeout-minutes: 40 + with: + server: ${{ secrets.FTP_SERVER }} + username: ${{ secrets.FTP_USERNAME }} + password: ${{ secrets.FTP_PASSWORD }} + # Optional: Specify server path + # server-dir: /public_html/ + # Optional: Exclude files/folders + # exclude: | + # **/.git* + # **/.git*/** + # **/node_modules/** + # **/tests/** + # **/.env + + - name: 📅 Format release date + id: format_date + if: always() + run: | + RELEASE_DATE="${{ github.event.release.published_at || github.event.head_commit.timestamp }}" + FORMATTED_DATE=$(date -d "$RELEASE_DATE" +"%d-%m-%Y %H:%M:%S") + echo "formatted_date=$FORMATTED_DATE" >> $GITHUB_OUTPUT + + - name: 📧 Send deployment notification + if: always() + uses: dawidd6/action-send-mail@v3 + with: + server_address: smtp.gmail.com + server_port: 465 + username: ${{ secrets.EMAIL_USERNAME }} + password: ${{ secrets.EMAIL_PASSWORD }} + subject: "${{ github.event.repository.name }} Deployment - ${{ job.status }}" + to: ${{ secrets.NOTIFICATION_EMAILS }} + from: ${{ secrets.EMAIL_USERNAME }} + body: | + Hello Team, + + The ${{ github.event.repository.name }} deployment to PRODUCTION has finished. + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 📊 DEPLOYMENT SUMMARY + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Status: ${{ job.status }} + Environment: Production + Version: ${{ github.ref_name }} + Commit: ${{ github.sha }} + Deployed By: ${{ github.actor }} + Date: ${{ steps.format_date.outputs.formatted_date }} + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + 🔗 LINKS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Repository: ${{ github.server_url }}/${{ github.repository }} + Commit: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }} + Release: ${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }} + + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + Best regards, + Nugsoft DevOps