403Webshell
Server IP : 35.80.110.71  /  Your IP : 216.73.216.221
Web Server : Apache/2.4.58 (Ubuntu)
System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64
User : ubuntu ( 1000)
PHP Version : 8.3.31
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /var/www/client-portal-laravel/current/scripts/deploy/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/client-portal-laravel/current/scripts/deploy/rollback.cjs
/**
 * Laravel Client Portal - Rollback Script
 *
 * Instantly rollback to a previous release by switching the symlink.
 *
 * Usage: node scripts/rollback.js [options]
 *   --releases=N    Number of releases to roll back (default: 1)
 *   --to=TIMESTAMP  Roll back to a specific release by name
 *   --dry-run       Show what would happen without making changes
 *
 * Examples:
 *   node scripts/rollback.js                 # Roll back 1 release
 *   node scripts/rollback.js --releases=2   # Roll back 2 releases
 *   node scripts/rollback.js --to=20260115120000  # Roll back to specific release
 */

const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const { NodeSSH } = require('node-ssh');

// Load environment: prefer .env.deployment, fallback to .env
const PROJECT_ROOT = path.join(__dirname, '..', '..');
const envDeploymentPath = path.join(PROJECT_ROOT, '.env.deployment');
const envPath = path.join(PROJECT_ROOT, '.env');
const envFileUsed = fs.existsSync(envDeploymentPath) ? envDeploymentPath : envPath;
require('dotenv').config({ path: envFileUsed });
console.log(`[ENV] Loaded: ${path.basename(envFileUsed)}`);

// ==== TARGET ENVIRONMENT (PLAN-281 §4.1) ====
// Mirrors deploy-linux.cjs so a rollback hits the right tree + supervisor
// groups. Default: production.
const CLI_ARGS = process.argv.slice(2);
function parseTargetEnv(args) {
    const flag = args.find((a) => a.startsWith('--env='));
    if (!flag) return 'production';
    const value = flag.split('=')[1];
    if (value !== 'production' && value !== 'staging') {
        console.error(`❌ Invalid --env value: "${value}". Use --env=production or --env=staging.`);
        process.exit(1);
    }
    return value;
}
const TARGET_ENV = parseTargetEnv(CLI_ARGS);
const ENV_DEFAULTS = {
    production: {
        basePath: '/var/www/client-portal-laravel',
        workerSupervisorGroup: 'client-portal-worker',
        reverbSupervisorGroup: 'client-portal-reverb',
        fpmReloadStrategy: 'restart',
        domain: process.env.MARKETING_DOMAIN || 'scopeforged.com',
    },
    staging: {
        basePath: '/var/www/client-portal-laravel-staging',
        workerSupervisorGroup: 'client-portal-staging-worker',
        reverbSupervisorGroup: 'client-portal-staging-reverb',
        fpmReloadStrategy: 'reload',
        domain: 'staging.scopeforged.com',
    },
};
const ENV = ENV_DEFAULTS[TARGET_ENV];

// ==== CONFIGURATION ====
const CONFIG = {
    server: {
        host: (process.env.SERVER_HOST || '').trim(),
        username: (process.env.SERVER_USERNAME || '').trim(),
        privateKey: (process.env.SERVER_PRIVATE_KEY || '').trim(),
    },
    paths: {
        // --env=staging makes ENV.basePath authoritative so a prod-tuned
        // .env.deployment SERVER_BASE_PATH cannot redirect a staging
        // rollback at prod (plan §4.1).
        basePath: TARGET_ENV === 'staging'
            ? ENV.basePath
            : (process.env.SERVER_BASE_PATH || ENV.basePath),
        releasesDir: 'releases',
        currentLink: 'current',
    },
};

// ==== UTILITIES ====

function log(emoji, message) {
    const timestamp = new Date().toISOString().substring(11, 19);
    console.log(`[${timestamp}] ${emoji} ${message}`);
}

async function execSSH(ssh, command, options = {}) {
    const result = await ssh.execCommand(command, options);
    if (result.code !== 0 && !options.ignoreError) {
        throw new Error(`Command failed: ${command}\nStderr: ${result.stderr}\nStdout: ${result.stdout}`);
    }
    return result;
}

// ==== ROLLBACK FUNCTIONS ====

/**
 * Get list of releases (sorted oldest to newest)
 */
async function getReleases(ssh) {
    const releasesPath = `${CONFIG.paths.basePath}/${CONFIG.paths.releasesDir}`;
    const result = await execSSH(ssh, `ls -1 ${releasesPath} 2>/dev/null | sort`, { ignoreError: true });

    if (!result.stdout.trim()) {
        return [];
    }

    return result.stdout.trim().split('\n').filter(r => r && /^\d{14}$/.test(r));
}

/**
 * Get current release name from symlink
 */
async function getCurrentRelease(ssh) {
    const currentPath = `${CONFIG.paths.basePath}/${CONFIG.paths.currentLink}`;
    const result = await execSSH(ssh, `readlink ${currentPath} 2>/dev/null | xargs basename`, { ignoreError: true });

    if (result.code !== 0 || !result.stdout.trim()) {
        return null;
    }

    return result.stdout.trim();
}

/**
 * Switch current symlink to target release
 */
async function switchToRelease(ssh, releaseName) {
    const releasesPath = `${CONFIG.paths.basePath}/${CONFIG.paths.releasesDir}`;
    const releasePath = `${releasesPath}/${releaseName}`;
    const currentPath = `${CONFIG.paths.basePath}/${CONFIG.paths.currentLink}`;

    // Verify release exists
    const exists = await execSSH(ssh, `test -d ${releasePath} && echo "yes"`, { ignoreError: true });
    if (exists.stdout.trim() !== 'yes') {
        throw new Error(`Release ${releaseName} does not exist`);
    }

    // Switch symlink
    await execSSH(ssh, `ln -sfn ${releasePath} ${currentPath}`);
}

/**
 * Reload services after rollback
 */
async function reloadServices(ssh) {
    // Mirror deploy-linux.cjs FPM strategy: prod restarts (defeats
    // symlink-cached config); staging reloads (avoids resetting prod
    // OPcache when both pools share a daemon).
    if (ENV.fpmReloadStrategy === 'restart') {
        log('♻️', 'Restarting php-fpm...');
        await execSSH(ssh, 'sudo systemctl restart php8.3-fpm');
    } else {
        log('♻️', 'Reloading php-fpm gracefully...');
        await execSSH(ssh, 'sudo systemctl reload php8.3-fpm');
    }

    log('♻️', 'Reloading Apache...');
    await execSSH(ssh, 'sudo systemctl reload apache2');

    log('♻️', `Restarting queue workers (${ENV.workerSupervisorGroup})...`);
    await execSSH(ssh, `sudo supervisorctl restart ${ENV.workerSupervisorGroup}:*`, { ignoreError: true });

    log('♻️', `Restarting Reverb WebSocket server (${ENV.reverbSupervisorGroup})...`);
    await execSSH(ssh, `sudo supervisorctl restart ${ENV.reverbSupervisorGroup}`, { ignoreError: true });

    log('✅', 'Services reloaded');
}

/**
 * Clear caches on the target release
 */
async function clearCaches(ssh, releasePath) {
    log('🧹', 'Clearing caches...');
    await execSSH(ssh, `cd ${releasePath} && php artisan config:clear`, { ignoreError: true });
    await execSSH(ssh, `cd ${releasePath} && php artisan config:cache`, { ignoreError: true });
    await execSSH(ssh, `cd ${releasePath} && php artisan route:cache`, { ignoreError: true });
    await execSSH(ssh, `cd ${releasePath} && php artisan view:cache`, { ignoreError: true });
    log('✅', 'Caches cleared');
}

// ==== SENTRY RELEASE TRACKING ====

/**
 * Update SENTRY_RELEASE in the shared .env on the server.
 */
async function updateSentryRelease(ssh, releaseName) {
    if (!/^\d{14}$/.test(releaseName)) {
        log('⚠️', `Skipping Sentry release update: invalid release name "${releaseName}"`);
        return;
    }

    const envFile = `${CONFIG.paths.basePath}/shared/.env`;

    log('📝', `Setting SENTRY_RELEASE=${releaseName} in shared .env...`);

    // sudo + re-chown rationale — same as deploy-linux.cjs::updateSentryRelease.
    const result = await execSSH(ssh, `sudo grep -q '^SENTRY_RELEASE=' ${envFile} && echo "exists" || echo "missing"`, { ignoreError: true });

    if (result.stdout.trim() === 'exists') {
        await execSSH(ssh, `sudo sed -i 's/^SENTRY_RELEASE=.*/SENTRY_RELEASE=${releaseName}/' ${envFile}`);
    } else {
        await execSSH(ssh, `echo 'SENTRY_RELEASE=${releaseName}' | sudo tee -a ${envFile} >/dev/null`);
    }

    // Restore canonical ubuntu:www-data ownership (sudo sed flips it).
    // Rationale lives in scripts/deploy/deploy-linux.cjs::setupSharedDirectory.
    await execSSH(ssh, `sudo chown ubuntu:www-data ${envFile}`);
    await execSSH(ssh, `sudo chmod 0640 ${envFile}`);

    log('✅', 'SENTRY_RELEASE updated in shared .env');
}

/**
 * Register a Sentry deploy for the rolled-back release.
 * Gated behind SENTRY_AUTH_TOKEN — skips gracefully if not configured.
 */
function registerSentryDeploy(releaseName) {
    if (!/^\d{14}$/.test(releaseName)) {
        return;
    }

    if (!process.env.SENTRY_AUTH_TOKEN) {
        log('⚠️', 'Skipping Sentry deploy registration: SENTRY_AUTH_TOKEN not set');
        return;
    }

    const org = process.env.SENTRY_ORG;
    const project = process.env.SENTRY_PROJECT;

    if (!org || !project) {
        log('⚠️', 'Skipping Sentry deploy registration: SENTRY_ORG or SENTRY_PROJECT not set');
        return;
    }

    log('📡', `Registering Sentry deploy for release: ${releaseName}...`);

    const sentryCliPath = path.join(PROJECT_ROOT, 'node_modules', '.bin', 'sentry-cli');
    const env = {
        ...process.env,
        SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN,
        SENTRY_ORG: org,
        SENTRY_PROJECT: project,
    };

    try {
        execSync(`"${sentryCliPath}" deploys new -r ${releaseName} -e production`, { stdio: 'pipe', env });
        log('✅', 'Sentry deploy registered');
    } catch (error) {
        const stderr = error.stderr ? error.stderr.toString().trim() : error.message;
        log('⚠️', `Sentry deploy registration failed (non-fatal): ${stderr}`);
    }
}

// ==== MAIN ====

async function rollback(options = {}) {
    const ssh = new NodeSSH();

    try {
        // Validate configuration
        if (!CONFIG.server.host || !CONFIG.server.username || !CONFIG.server.privateKey) {
            throw new Error('Missing required environment variables: SERVER_HOST, SERVER_USERNAME, SERVER_PRIVATE_KEY');
        }

        if (!fs.existsSync(CONFIG.server.privateKey)) {
            throw new Error(`Private key file not found: ${CONFIG.server.privateKey}`);
        }

        // Connect to server
        log('🔌', 'Connecting to server...');
        const privateKeyContent = fs.readFileSync(CONFIG.server.privateKey, 'utf8');
        await ssh.connect({
            host: CONFIG.server.host,
            username: CONFIG.server.username,
            privateKey: privateKeyContent,
        });
        log('✅', 'Connected to server');

        // Get releases and current release
        const releases = await getReleases(ssh);
        const currentRelease = await getCurrentRelease(ssh);

        if (releases.length === 0) {
            throw new Error('No releases found on server');
        }

        log('📋', `Found ${releases.length} release(s)`);
        log('📍', `Current release: ${currentRelease || 'none'}`);

        // Determine target release
        let targetRelease;

        if (options.toRelease) {
            // Roll back to specific release
            targetRelease = options.toRelease;
            if (!releases.includes(targetRelease)) {
                throw new Error(`Release ${targetRelease} not found. Available: ${releases.join(', ')}`);
            }
        } else {
            // Roll back N releases
            const currentIndex = releases.indexOf(currentRelease);
            if (currentIndex === -1) {
                throw new Error('Current release not found in releases list');
            }

            const targetIndex = currentIndex - options.releases;
            if (targetIndex < 0) {
                throw new Error(`Cannot roll back ${options.releases} release(s). Only ${currentIndex} previous release(s) available.`);
            }

            targetRelease = releases[targetIndex];
        }

        log('🎯', `Target release: ${targetRelease}`);

        // Dry run check
        if (options.dryRun) {
            log('📝', 'DRY RUN - Would perform:');
            console.log(`   Switch symlink: current -> releases/${targetRelease}`);
            console.log('   Clear caches');
            console.log('   Reload Apache');
            console.log('   Restart queue workers');
            console.log('   Restart Reverb');
            process.exit(0);
        }

        // Confirm rollback
        if (targetRelease === currentRelease) {
            log('⚠️', 'Target release is same as current release. Nothing to do.');
            process.exit(0);
        }

        // Perform rollback
        log('🔄', 'Rolling back...');
        await switchToRelease(ssh, targetRelease);
        log('✅', `Symlink switched to ${targetRelease}`);

        // Update SENTRY_RELEASE in shared .env
        await updateSentryRelease(ssh, targetRelease);

        // Clear caches on the target release
        const releasesPath = `${CONFIG.paths.basePath}/${CONFIG.paths.releasesDir}`;
        await clearCaches(ssh, `${releasesPath}/${targetRelease}`);

        // Reload services
        await reloadServices(ssh);

        // Register Sentry deploy for the rolled-back release
        registerSentryDeploy(targetRelease);

        // Done
        log('🎉', `Rollback complete! Now running: ${targetRelease} (${TARGET_ENV})`);
        log('🌐', `Site: https://${ENV.domain}`);

    } catch (error) {
        log('❌', `Rollback failed: ${error.message}`);
        process.exit(1);
    } finally {
        ssh.dispose();
    }
}

// ==== PARSE ARGUMENTS ====

const args = CLI_ARGS;

// Show help
if (args.includes('--help') || args.includes('-h')) {
    console.log(`
Laravel Client Portal - Rollback Script

Usage: node scripts/deploy/rollback.cjs [options]

Target selection (plan §4.1):
  --env=production    Roll back the production deploy (default)
  --env=staging       Roll back the staging deploy at staging.scopeforged.com

Options:
  --releases=N    Number of releases to roll back (default: 1)
  --to=TIMESTAMP  Roll back to a specific release by name
  --dry-run       Show what would happen without making changes
  --help, -h      Show this help message

Target environment: ${TARGET_ENV}

Examples:
  node scripts/deploy/rollback.cjs                            Roll back 1 prod release
  node scripts/deploy/rollback.cjs --env=staging              Roll back 1 staging release
  node scripts/deploy/rollback.cjs --releases=2               Roll back 2 releases
  node scripts/deploy/rollback.cjs --to=20260115120000        Roll back to specific release
  node scripts/deploy/rollback.cjs --dry-run                  Preview rollback
`);
    process.exit(0);
}

// Parse options
const options = {
    releases: 1,
    toRelease: null,
    dryRun: args.includes('--dry-run'),
};

for (const arg of args) {
    if (arg.startsWith('--releases=')) {
        options.releases = parseInt(arg.split('=')[1], 10);
        if (isNaN(options.releases) || options.releases < 1) {
            console.error('❌ --releases must be a positive integer');
            process.exit(1);
        }
    }
    if (arg.startsWith('--to=')) {
        options.toRelease = arg.split('=')[1];
        if (!/^\d{14}$/.test(options.toRelease)) {
            console.error('❌ --to must be a valid release name (YYYYMMDDHHMMSS)');
            process.exit(1);
        }
    }
}

rollback(options);

Youez - 2016 - github.com/yon3zu
LinuXploit