開發者常希望在後台按鈕即可觸發 svn update,但直接在 PHP 執行 shell 指令會遇到權限、編碼、衝突處理等坑。本文整理從伺服器端佈署到 PHP 呼叫的完整 SOP。
# Ubuntu 範例
sudo -u www-data -H bash -c "
cd /var/www
svn checkout https://svn.example.com/repo/project/trunk my_site
"
~www-data/.subversion/auth/,後續無需互動。chown -R www-data:www-data /var/www/my_site,必要時加上 setgid 確保新檔案群組一致。| 參數/環境 | 作用 | 必要性 |
|---|---|---|
putenv('LANG=C.UTF-8') | 修正 locale,避免中文檔名導致 svn: E000022: Can't convert string from 'UTF-8' to native encoding | ★★★ |
--accept theirs-full | 衝突時自動採用版本庫版本 (覆蓋本地修改),適合部署流程 | ★★★ |
2>&1 | 將 stderr 合併到 stdout,讓 shell_exec() 捕捉完整輸出 (含錯誤訊息) | ★★★ |
--non-interactive | 禁止互動提示,避免卡住 | ★★ |
--trust-server-cert | 自信任自簽憑證 (HTTPS) | 視情況 |
<?php
/**
* SVN 部署端點
* POST /deploy.php?project=my_site
* 回傳 JSON: {success: bool, output: string, time: float}
*/
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
$project = $_GET['project'] ?? 'my_site';
$allowedProjects = ['my_site', 'admin_portal', 'api_gateway'];
if (!in_array($project, $allowedProjects, true)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Invalid project']);
exit;
}
$workDir = "/var/www/{$project}";
$logFile = "/var/log/deploy_{$project}.log";
function logDeploy(string $msg, string $logFile): void {
$line = "[" . date('Y-m-d H:i:s') . "] {$msg}\n";
file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
}
logDeploy("=== Deploy started for {$project} ===", $logFile);
$start = microtime(true);
// 關鍵環境變數
putenv('LANG=C.UTF-8');
putenv('LC_ALL=C.UTF-8');
$cmd = "svn update --accept theirs-full --non-interactive {$workDir} 2>&1";
$output = shell_exec($cmd);
$exitCode = $output === null ? -1 : 0; // shell_exec 失敗回傳 null
$elapsed = round(microtime(true) - $start, 3);
$success = ($exitCode === 0);
$logMsg = $success ? "SUCCESS ({$elapsed}s)" : "FAILED ({$elapsed}s)";
logDeploy($logMsg, $logFile);
logDeploy($output ?? 'No output', $logFile);
http_response_code($success ? 200 : 500);
echo json_encode([
'success' => $success,
'project' => $project,
'output' => $output ?? '',
'time' => $elapsed,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
<template>
<button @click="deploy" :disabled="loading" class="btn btn-primary">
<span v-if="loading" class="spinner"></span> 一鍵部署
</button>
<pre v-if="result" class="mt-3 log-box">{{ result }}</pre>
</template>
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const loading = ref(false);
const result = ref('');
async function deploy() {
loading.value = true;
result.value = '部署中...';
try {
const res = await axios.post('/deploy.php?project=my_site', null, { timeout: 120000 });
result.value = `✅ 成功 (${res.data.time}s)\n${res.data.output}`;
} catch (e) {
result.value = `❌ 失敗\n${e.response?.data?.output || e.message}`;
} finally {
loading.value = false;
}
}
</script>
/var/www/current -> /var/www/releases/20240115_1030,部署新版本至新目錄,原子性切換連結,失敗秒級回滾。flock 或 Redis SETNX 防止重複部署。| 錯誤訊息 | 原因 | 解決 |
|---|---|---|
| svn: E170013: Unable to connect to a repository at URL | 網路不通、憑證不信任、URL 錯誤 | 加 --trust-server-cert、檢查防火牆、驗證 URL |
| svn: E155037: Previous operation has not finished | 上次中斷留下 lock | svn cleanup 或刪除 .svn/wc.db 重新 checkout |
| Permission denied | 目錄擁有者非 www-data | chown -R www-data:www-data /var/www/project |
| 中文檔名顯示亂碼 | locale 未設定 UTF-8 | 加 putenv('LANG=C.UTF-8') |
完整專案含 Docker、systemd timer 定時部署、Slack 通知已開源於 GitHub。