shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.
The submodule caused every deployment failure this site has had:
- .gitmodules named no `branch`, so the update fell back to
refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
"Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
revision in submodule path".
- Its URL was http://, which the origin answers with 523.
- This install is deployed by uploading files, and a submodule is invisible
to that. The tree moved forward while shared/platform-tools stayed on the
old pin, config/functions_secrets.php died on a missing compat_secrets.php,
and the whole site — public pages included — went down.
Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.
The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.7 KiB
PHP
89 lines
3.7 KiB
PHP
<?php
|
|
// Self-bootstrapping deploy webhook. Point a Gitea (or GitHub) push webhook at
|
|
// <host>/api/auto_update.php and the platform updates itself on every push to
|
|
// the branch it is running.
|
|
//
|
|
// Assumed location: <host>/shared/platform-tools/api/auto_update.php
|
|
// dirname(__DIR__, 3) === <host>
|
|
// The per-platform stub at <host>/api/auto_update.php is a one-line require.
|
|
//
|
|
// Answers JSON always, and with a status code the webhook UI can be read from:
|
|
// 202 accepted (work continues after the reply) / disabled
|
|
// 200 skipped — push was for another branch
|
|
// 401 bad signature · 405 wrong method · 503 no secret configured
|
|
//
|
|
// A refusal is deliberately a 4xx/5xx: Gitea shows the delivery in red and
|
|
// keeps the response body, which is the only place a rejected delivery would
|
|
// ever be noticed before someone opens the platform page.
|
|
//
|
|
// Everything that can be answered instantly IS answered instantly, and only the
|
|
// slow part runs after the reply. Gitea allows a webhook delivery 5 seconds by
|
|
// default; a git pull alone exceeds that and a pre-update backup exceeds it by
|
|
// minutes, so a synchronous reply would mark every successful deploy as failed.
|
|
// The outcome is recorded in auto_update_last_* — the platform page and the
|
|
// dashboard strip are where you read it.
|
|
|
|
$repo_root = dirname(__DIR__, 3);
|
|
require_once $repo_root . '/config/db.php'; // opens $pdo, starts the session
|
|
require_once $repo_root . '/config/functions.php'; // env(), helpers
|
|
require_once __DIR__ . '/../functions_auto_update.php';
|
|
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
|
|
$reply = function (int $code, array $data): never {
|
|
http_response_code($code);
|
|
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
exit;
|
|
};
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$reply(405, ['ok' => false, 'error' => 'Só POST.']);
|
|
}
|
|
|
|
// Read the body ONCE and verify against those exact bytes — re-reading
|
|
// php://input after the fact can come back empty and would silently turn the
|
|
// signature check into a no-op.
|
|
$body = (string) file_get_contents('php://input');
|
|
$secret = auto_update_secret();
|
|
|
|
if ($secret === '') {
|
|
$reply(503, ['ok' => false, 'error' => 'AUTO_UPDATE_SECRET não está definido no .env desta plataforma.']);
|
|
}
|
|
if (!auto_update_verify($body, $secret)) {
|
|
$reply(401, ['ok' => false, 'error' => 'Assinatura ausente ou inválida.']);
|
|
}
|
|
|
|
// Signature first, switch second: an unauthenticated caller learns nothing
|
|
// about this platform, not even whether the feature is on.
|
|
if (!auto_update_enabled($pdo)) {
|
|
$reply(202, ['ok' => true, 'status' => 'disabled',
|
|
'message' => 'Auto-update desligado nesta plataforma.']);
|
|
}
|
|
|
|
// A push to a branch this server is not running is not our business — the
|
|
// webhook fires for every branch, and pulling one you are not on does nothing
|
|
// useful at best.
|
|
$ref = auto_update_payload_branch($body);
|
|
if ($ref !== null && function_exists('git_run')) {
|
|
$cur = git_run(['rev-parse', '--abbrev-ref', 'HEAD'], $repo_root);
|
|
$cur = $cur['ok'] ? trim($cur['output']) : '';
|
|
if ($cur !== '' && $cur !== $ref) {
|
|
$reply(200, ['ok' => true, 'status' => 'skipped',
|
|
'message' => "Push para {$ref}; este servidor está em {$cur}."]);
|
|
}
|
|
}
|
|
|
|
// Server-to-server past this point: drop the session lock so the pull (which
|
|
// can take a while) does not hold up the operator's own browsing.
|
|
@session_write_close();
|
|
|
|
// Answer Gitea now; do the slow work with the connection already closed.
|
|
platform_respond_and_continue(202, [
|
|
'ok' => true,
|
|
'status' => 'accepted',
|
|
'message' => 'Atualização iniciada. O resultado fica registado na plataforma.',
|
|
'ref' => $ref,
|
|
]);
|
|
|
|
auto_update_run($pdo, $repo_root, $ref);
|