PHP实现简易部署脚本

wen PHP项目 1

我来为你提供一个完整的PHP简易部署脚本,包含安全考虑和实用功能:

PHP实现简易部署脚本

基础版本部署脚本

<?php
/**
 * 简易部署脚本
 * 通过Git拉取代码并执行后续步骤
 */
// 设置错误报告
error_reporting(E_ALL);
ini_set('display_errors', 1);
// 设置超时时间(秒)
set_time_limit(300);
// 定义部署相关常量
define('DEPLOY_TOKEN', 'your-secret-token'); // 部署密钥
define('BASE_DIR', '/var/www/html');         // 项目根目录
define('BACKUP_DIR', '/var/backups');        // 备份目录
define('LOG_FILE', '/var/log/deploy.log');   // 日志文件
// 记录日志函数
function log_message($message) {
    $timestamp = date('Y-m-d H:i:s');
    $log = "[{$timestamp}] {$message}\n";
    file_put_contents(LOG_FILE, $log, FILE_APPEND);
    echo $log;
}
// 验证部署令牌
function verify_token() {
    $input_token = $_GET['token'] ?? $_POST['token'] ?? '';
    if (!hash_equals(DEPLOY_TOKEN, $input_token)) {
        http_response_code(403);
        die('Access denied: Invalid token');
    }
}
// 执行命令并记录
function run_command($command) {
    log_message("执行命令: {$command}");
    $output = [];
    $return_var = 0;
    exec($command . " 2>&1", $output, $return_var);
    // 记录输出
    foreach ($output as $line) {
        log_message("  {$line}");
    }
    if ($return_var !== 0) {
        throw new Exception("命令执行失败: {$command}, 返回码: {$return_var}");
    }
    return $output;
}
// 备份当前代码
function backup_code($project_name) {
    $backup_file = BACKUP_DIR . "/{$project_name}_" . date('YmdHis') . '.tar.gz';
    if (!is_dir(BACKUP_DIR)) {
        mkdir(BACKUP_DIR, 0755, true);
    }
    $command = "tar -czf {$backup_file} -C " . dirname(BASE_DIR) . " " . basename(BASE_DIR);
    run_command($command);
    // 清理旧备份(保留最近5个)
    $backups = glob(BACKUP_DIR . "/{$project_name}_*.tar.gz");
    if (count($backups) > 5) {
        $to_delete = array_slice($backups, 0, count($backups) - 5);
        foreach ($to_delete as $file) {
            unlink($file);
        }
    }
    return $backup_file;
}
// 主部署流程
function deploy() {
    try {
        log_message("========== 部署开始 ==========");
        // 1. 切换到项目目录
        chdir(BASE_DIR);
        // 2. 创建备份
        $project_name = basename(BASE_DIR);
        $backup_file = backup_code($project_name);
        log_message("备份完成: {$backup_file}(保留最近5个备份)");
        // 3. 拉取最新代码
        log_message("拉取最新代码...");
        run_command('git fetch --all');
        run_command('git pull origin main');
        // 4. 重置本地修改(可选,谨慎使用)
        // run_command('git reset --hard origin/main');
        // 5. 组合任务
        $tasks = [
            'composer install --no-dev --optimize-autoloader',
            'php artisan migrate --force',      // Laravel示例
            'php artisan config:cache',          // 缓存配置
            'php artisan route:cache',           // 缓存路由
            'php artisan view:cache',            // 缓存视图
            'php artisan cache:clear',           // 清除缓存
            'chmod -R 775 storage bootstrap/cache' // 设置权限
        ];
        // 6. 执行组合任务,失败则终止
        foreach ($tasks as $task) {
            log_message("执行任务: {$task}");
            run_command($task);
        }
        // 7. 如果有前端构建
        if (file_exists('package.json')) {
            log_message("前端构建...");
            run_command('npm install --production');
            run_command('npm run production');
        }
        log_message("========== 部署完成 ==========");
        // 返回成功
        echo json_encode([
            'status' => 'success',
            'message' => '部署成功',
            'time' => date('Y-m-d H:i:s')
        ]);
    } catch (Exception $e) {
        log_message("部署失败: " . $e->getMessage());
        // 回滚到备份
        if (isset($backup_file) && file_exists($backup_file)) {
            log_message("开始回滚...");
            chdir(dirname(BASE_DIR));
            run_command("rm -rf " . BASE_DIR);
            run_command("tar -xzf {$backup_file} -C " . dirname(BASE_DIR));
            log_message("回滚完成");
        }
        http_response_code(500);
        echo json_encode([
            'status' => 'error',
            'message' => $e->getMessage(),
            'time' => date('Y-m-d H:i:s')
        ]);
    }
}
// 验证令牌
verify_token();
// 执行部署
deploy();

增强版带Web界面

<?php
/**
 * 带Web界面的部署脚本
 * 包含权限管理和地址校验
 */
session_start();
// 配置文件
$config = [
    'github_ips' => ['140.82.112.0', '140.82.113.0'],  // GitHub IP范围
    'gitlab_ips' => ['172.65.251.0', '172.65.252.0'],  // GitLab IP范围
    'secret' => 'your-webhook-secret',
    'repo_path' => '/path/to/repo',
    'deploy_branch' => 'main',
    'backup' => true,
    'max_execution_time' => 300,
    'log_file' => 'deploy_log.txt',
    'allowed_commands' => [
        'git status',
        'git log --oneline -5',
        'git diff --stat',
        'composer --version',
        'php --version'
    ],
    'restricted_commands' => [
        'rm -rf /',
        'sudo',
        'shutdown',
        'reboot'
    ]
];
// 增强的日志函数
function logger($message, $type = 'INFO') {
    global $config;
    $log = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $type, $message);
    file_put_contents($config['log_file'], $log, FILE_APPEND);
    if ($type == 'ERROR') {
        error_log($message);
    }
}
// API响应
function api_response($data, $status = 200) {
    http_response_code($status);
    header('Content-Type: application/json');
    echo json_encode($data);
    exit;
}
// 验证来源IP
function verify_ip() {
    global $config;
    $client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
    // 检查是否在白名单
    if (in_array($client_ip, array_merge($config['github_ips'], $config['gitlab_ips']))) {
        return true;
    }
    // 获取IP的CIDR掩码
    $ip_parts = explode('.', $client_ip);
    if (count($ip_parts) == 4) {
        $ip_range = implode('.', array_slice($ip_parts, 0, 3)) . '.0';
        if (in_array($ip_range, $config['github_ips']) || in_array($ip_range, $config['gitlab_ips'])) {
            return true;
        }
    }
    logger("IP验证失败: {$client_ip}", 'WARN');
    return false;
}
// 解析GitHub webhook payload
function parse_github_payload() {
    $payload = file_get_contents('php://input');
    $headers = getallheaders();
    // 验证签名
    $signature = $headers['X-Hub-Signature-256'] ?? '';
    $computed = 'sha256=' . hash_hmac('sha256', $payload, $config['secret']);
    if (!hash_equals($signature, $computed)) {
        logger("签名验证失败", 'ERROR');
        return false;
    }
    $data = json_decode($payload, true);
    if ($data['ref'] !== 'refs/heads/main') {
        logger("分支不匹配: {$data['ref']}", 'WARN');
        return false;
    }
    return $data;
}
// 安全执行命令
function safe_exec($command) {
    global $config;
    // 检查命令是否在禁止列表中
    foreach ($config['restricted_commands'] as $restricted) {
        if (strpos($command, $restricted) !== false) {
            logger("禁止的命令: {$command}", 'ERROR');
            return ['success' => false, 'error' => 'Command not allowed'];
        }
    }
    logger("执行: {$command}");
    $output = [];
    $return_code = 0;
    exec(escapeshellcmd($command) . " 2>&1", $output, $return_code);
    logger("输出: " . implode("\n", $output));
    return [
        'success' => $return_code === 0,
        'output' => implode("\n", $output)
    ];
}
// 执行部署
function execute_deploy() {
    global $config;
    logger("===== 开始部署 =====");
    set_time_limit($config['max_execution_time']);
    $results = [];
    // 1. 检查目录
    if (!is_dir($config['repo_path'])) {
        logger("项目目录不存在: {$config['repo_path']}", 'ERROR');
        api_response(['status' => 'error', 'message' => 'Repo path not found'], 500);
    }
    chdir($config['repo_path']);
    // 2. 备份
    if ($config['backup']) {
        $backup_name = "backup_" . date('Ymd_His') . ".tar.gz";
        $result = safe_exec("tar -czf /tmp/bak_{$backup_name} .");
        if (!$result['success']) {
            logger("备份失败", 'ERROR');
            return false;
        }
        logger("备份完成: {$backup_name}");
    }
    // 3. Git操作
    $commands = [
        'git fetch --all',
        'git reset --hard origin/' . $config['deploy_branch'],
        'git clean -fd'
    ];
    foreach ($commands as $command) {
        $result = safe_exec($command);
        if (!$result['success']) {
            logger("失败: {$command}", 'ERROR');
            return false;
        }
        $results[] = $result['output'];
    }
    // 4. 更新依赖
    if (file_exists('composer.json')) {
        $result = safe_exec('composer install --no-dev --optimize-autoloader');
        if (!$result['success']) {
            logger("Composer安装失败", 'ERROR');
            return false;
        }
        $results[] = $result['output'];
    }
    // 5. 数据库迁移
    if (file_exists('artisan')) {
        $result = safe_exec('php artisan migrate --force');
        if (!$result['success']) {
            logger("数据库迁移失败", 'ERROR');
            return false;
        }
        $results[] = $result['output'];
    }
    logger("===== 部署完成 =====");
    return true;
}
// Web界面处理
function render_web_ui() {
    ?>
    <!DOCTYPE html>
    <html>
    <head>
        <title>部署管理</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            .success { color: green; }
            .error { color: red; }
            .warning { color: orange; }
            table { width: 100%; border-collapse: collapse; }
            th, td { padding: 8px; text-align: left; border: 1px solid #ddd; }
            .button { 
                background-color: #4CAF50; 
                color: white; 
                padding: 15px 32px; 
                text-align: center; 
                text-decoration: none; 
                display: inline-block; 
                font-size: 16px; 
                margin: 4px 2px; 
                cursor: pointer; 
            }
        </style>
    </head>
    <body>
        <h1>简易部署工具</h1>
        <div>
            <button class="button" onclick="deployNow()">立即部署</button>
            <button class="button" onclick="clearLog()" style="background-color: #f44336;">清理日志</button>
        </div>
        <h2>最近部署记录</h2>
        <table>
            <tr>
                <th>时间</th>
                <th>类型</th>
                <th>消息</th>
            </tr>
            <?php
            if (file_exists('deploy_log.txt')) {
                $lines = array_slice(file('deploy_log.txt'), -50);
                foreach ($lines as $line) {
                    $parts = explode('] [', trim($line));
                    if (count($parts) == 3) {
                        list($time, $type, $message) = $parts;
                        $class = '';
                        switch ($type) {
                            case 'ERROR': $class = 'error'; break;
                            case 'WARN': $class = 'warning'; break;
                            default: $class = 'success';
                        }
                        echo "<tr>";
                        echo "<td>{$time}]</td>";
                        echo "<td class='{$class}'>{$type}</td>";
                        echo "<td>{$message}</td>";
                        echo "</tr>";
                    }
                }
            }
            ?>
        </table>
        <script>
            function deployNow() {
                fetch('', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    body: 'action=deploy'
                })
                .then(response => response.json())
                .then(data => {
                    alert(data.message);
                })
                .catch(error => console.error('Error:', error));
            }
            function clearLog() {
                fetch('', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    body: 'action=clear_log'
                })
                .then(response => response.json())
                .then(data => {
                    alert(data.message);
                    location.reload();
                })
                .catch(error => console.error('Error:', error));
            }
        </script>
    </body>
    </html>
    <?php
}
// 处理请求
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = $_POST['action'] ?? '';
    switch ($action) {
        case 'deploy':
            if (!verify_ip()) {
                api_response(['status' => 'error', 'message' => 'Forbidden'], 403);
            }
            $result = execute_deploy();
            api_response(['status' => $result ? 'success' : 'error', 'message' => $result ? '部署成功' : '部署失败']);
            break;
        case 'clear_log':
            if (!verify_ip()) {
                api_response(['status' => 'error', 'message' => 'Forbidden'], 403);
            }
            file_put_contents('deploy_log.txt', '');
            api_response(['status' => 'success', 'message' => '日志已清理']);
            break;
        default:
            api_response(['status' => 'error', 'message' => 'Invalid action'], 400);
    }
} else {
    // 显示Web界面
    render_web_ui();
}

部署建议

安全配置

// 安全配置文件 config.php
<?php
return [
    // 只允许来自GitHub/GitLab的IP
    'allowed_ips' => [], 
    // IP白名单
    'ip_whitelist' => [
        '192.168.1.0/24',
        '10.0.0.0/8'
    ],
    // 安全模式
    'safe_mode' => true,
    // 最大执行时间
    'timeout' => 300,
    // 日志级别
    'log_level' => 'DEBUG', // DEBUG, INFO, WARN, ERROR
    // 敏感信息加密
    'secrets' => [
        // 加密部署密码
        'git_token' => 'encrypted_token',
        'ssh_key' => '/path/to/key'
    ]
];

使用说明

  1. 部署脚本位置:放在服务器上的 /var/www/html/ 或项目根目录

  2. 权限设置

    # 设置适当的权限
    chmod 755 deploy.php
    chmod 775 storage/
    chmod 775 logs/
  3. 配置Webhook

  • GitHub: 设置 Webhook 到 https://yourserver.com/deploy.php
  • GitLab: 设置 Webhook 到你的域名
  1. 测试部署
    # 本地测试
    curl -X POST http://localhost/deploy.php -d "token=your-token"

通过GitHub webhook

curl -X POST http://localhost/deploy.php \ -H "Content-Type: application/json" \ -d '{"ref":"refs/heads/main"}'


### 注意事项
1. **安全防护**:
- 使用HTTPS访问部署脚本
- 设置访问密码或token
- 限制来源IP
- 做好备份
2. **错误处理**:
- 捕捉所有异常
- 支持自动回滚
- 记录详细日志
3. **性能优化**:
- 设置合理的超时时间
- 使用队列处理长时间任务
- 异步发送日志通知
4. **维护建议**:
- 定期清理旧备份
- 监控磁盘空间
- 设置文件权限检查
这个脚本可以根据你的实际需求进行修改,生产环境建议添加更多错误处理和监控功能。

抱歉,评论功能暂时关闭!