Der Installer schreibt die Hostnamen, nach denen er fragt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
4200056697
commit
0d7950c464
28
.env.example
28
.env.example
|
|
@ -218,26 +218,38 @@ CLUPILOT_TAX_PERCENT=20
|
|||
# APP_HOST: Hostname des Kundenportals, z. B. app.clupilot.com. Gesetzt, ist
|
||||
# JEDE Portal-Route an diesen Host gebunden und existiert nirgends sonst —
|
||||
# www.clupilot.com/dashboard ist dann ein 404, kein funktionierender Aufruf.
|
||||
# Das schliesst die Anmeldung ein: Fortify bindet seine eigenen Routen ueber
|
||||
# denselben Wert (config/fortify.php).
|
||||
# Leer = Portal antwortet ueberall (Vorgabe, und was jede dev-Maschine ueber
|
||||
# eine blanke IP braucht).
|
||||
# Hostname fuer Dateien zum Herunterladen: AGB, AV, TOM (oeffentlich) und das
|
||||
# Bootstrap-Archiv (nur mit gueltigem Einmal-Code). Leer = das Archiv bleibt
|
||||
# auf dem Portal-Hostnamen.
|
||||
FILES_HOST=
|
||||
|
||||
APP_HOST=
|
||||
|
||||
# SITE_HOST: Hostname der oeffentlichen Website. Gesetzt, antwortet die
|
||||
# Startseite NUR dort — jeder andere Host (das Portal, eine blanke IP) zeigt
|
||||
# unter "/" die Anmeldung bzw. das Dashboard. Leer heisst: Startseite ueberall,
|
||||
# auch auf dem Portal-Hostnamen.
|
||||
# Mehrere Namen kommagetrennt, der ERSTE ist der kanonische — er liefert aus,
|
||||
# alle weiteren leiten dauerhaft dorthin um (Pfad und Query bleiben erhalten):
|
||||
# SITE_HOST=www.clupilot.com,clupilot.com
|
||||
# und dann liefert app.clupilot.com die Website aus. Gebunden sind Startseite,
|
||||
# robots.txt und die Legal-Seiten — dadurch erzeugt route('legal.impressum')
|
||||
# auch aus einer Mail heraus eine www-Adresse.
|
||||
# Gebunden sind Startseite, robots.txt, Kontakt und die Legal-Seiten — dadurch
|
||||
# erzeugt route('legal.impressum') auch aus einer Mail heraus eine www-Adresse,
|
||||
# wo es keine Anfrage gibt, aus der man einen Hostnamen nehmen koennte.
|
||||
SITE_HOST=
|
||||
|
||||
# STATUS_HOST: Hostname der Statusseite, z. B. status.clupilot.com. Gesetzt,
|
||||
# liegt sie an der WURZEL dieses Namens, und /status leitet von jedem anderen
|
||||
# Host dorthin um statt zu 404en — eine Statusseite ist die Adresse, die in
|
||||
# einem Lesezeichen steht, wenn ohnehin schon etwas kaputt ist.
|
||||
# Leer = /status antwortet auf jedem Host.
|
||||
STATUS_HOST=
|
||||
|
||||
# FILES_HOST: Hostname fuer Dateien zum Herunterladen — AGB, AV, TOM
|
||||
# (oeffentlich, dauerhaft zitierbar, weil diese Adressen in Vertraegen stehen)
|
||||
# und das Bootstrap-Archiv (nur mit gueltigem Einmal-Code).
|
||||
# Leer = das Archiv bleibt auf dem Portal-Hostnamen, damit keine bereits
|
||||
# ausgegebene Befehlszeile bricht, solange der DNS-Eintrag noch nicht steht.
|
||||
FILES_HOST=
|
||||
|
||||
ADMIN_HOSTS=admin.dev.clupilot.com
|
||||
ADMIN_HOST_EXCLUSIVE=true
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ ask WS_DOMAIN "Domain for the websocket server" "ws.clupilot.com"
|
|||
# its own name. Naming the portal here was wrong in the printed instructions.
|
||||
ask API_DOMAIN "Domain Stripe posts its webhooks to" "api.clupilot.com"
|
||||
ask STATUS_DOMAIN "Public status page (blank to keep it on every host)" "status.clupilot.com"
|
||||
ask FILES_DOMAIN "Domain for downloads: terms, DPA, bootstrap archive (blank to keep them on the portal)" "files.clupilot.com"
|
||||
ask ADMIN_EMAIL "Your login address"
|
||||
ask ADMIN_NAME "Your display name" "Administrator"
|
||||
ask_secret ADMIN_PASSWORD "Your password (min. 12 characters)"
|
||||
|
|
@ -266,6 +267,23 @@ else
|
|||
fi
|
||||
}
|
||||
|
||||
# An `if`, not `[[ … ]] && …`: the && form makes the function return 1 for
|
||||
# every value that is absent, and under `set -e` that ends the install.
|
||||
# Absent is the NORMAL case here — these are the optional ones — so the
|
||||
# script killed itself on the first installation that did not happen to
|
||||
# supply a Hetzner token.
|
||||
#
|
||||
# Und die Definition steht VOR dem ersten Aufruf. Sie stand einmal dreizehn
|
||||
# Zeilen dahinter: in bash ist eine Funktion erst ab ihrer Definition
|
||||
# bekannt, der Aufruf davor ist ein Exit 127, und `set -euo pipefail` macht
|
||||
# daraus das Ende der Installation. Jede Neuinstallation starb an
|
||||
# `optional_env STATUS_HOST`.
|
||||
optional_env() {
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
set_env "$1" "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
set_env APP_ENV production
|
||||
set_env APP_DEBUG false
|
||||
set_env APP_URL "https://${APP_DOMAIN}"
|
||||
|
|
@ -292,7 +310,16 @@ else
|
|||
# unreachable under any other name.
|
||||
set_env ADMIN_HOST_EXCLUSIVE false
|
||||
set_env TRUSTED_RANGES "10.66.0.0/24,127.0.0.1"
|
||||
# Die Antworten von oben landen HIER, und das ist der ganze Unterschied
|
||||
# zwischen einer getrennten Installation und einer, auf der Website und
|
||||
# Portal einander bedienen. Ohne diese vier Zeilen laufen beide
|
||||
# Route::domain()-Gruppen host-unabhängig und antworten überall — der
|
||||
# Betreiber erreicht dann die Website unter app. und die Anmeldung unter
|
||||
# www., und nichts an der Konfiguration deutet darauf hin, warum.
|
||||
set_env APP_HOST "$APP_DOMAIN"
|
||||
set_env SITE_HOST "$WWW_DOMAIN"
|
||||
optional_env STATUS_HOST "$STATUS_DOMAIN"
|
||||
optional_env FILES_HOST "$FILES_DOMAIN"
|
||||
|
||||
set_env CLUPILOT_WG_ENDPOINT "$(curl -fsS4 https://ifconfig.co 2>/dev/null || echo 'SET-ME'):51820"
|
||||
|
||||
|
|
@ -300,17 +327,6 @@ else
|
|||
# inside the container belong to it and not to root.
|
||||
# Supplied through --env-file; skipped silently when absent, so a first
|
||||
# install can happen before the Stripe account exists.
|
||||
# An `if`, not `[[ … ]] && …`: the && form makes the function return 1 for
|
||||
# every value that is absent, and under `set -e` that ends the install.
|
||||
# Absent is the NORMAL case here — these are the optional ones — so the
|
||||
# script killed itself on the first installation that did not happen to
|
||||
# supply a Hetzner token.
|
||||
optional_env() {
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
set_env "$1" "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
optional_env HETZNER_DNS_TOKEN "${HETZNER_DNS_TOKEN:-}"
|
||||
optional_env CLUPILOT_DNS_ZONE "${CLUPILOT_DNS_ZONE:-}"
|
||||
optional_env STRIPE_KEY "${STRIPE_KEY:-}"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Der Installer fragte nach den Hostnamen und schrieb sie nicht.
|
||||
*
|
||||
* Und er rief `optional_env` dreizehn Zeilen vor ihrer Definition auf. In bash
|
||||
* ist eine Funktion erst ab ihrer Definition bekannt; unter `set -euo pipefail`
|
||||
* ist der Aufruf davor ein Exit 127, der die Installation beendet. Das ist der
|
||||
* Fehler, der hier festgehalten wird — nicht die Schreibweise.
|
||||
*/
|
||||
function installerSource(): string
|
||||
{
|
||||
return file_get_contents(base_path('deploy/install.sh'));
|
||||
}
|
||||
|
||||
it('ist syntaktisch gültiges bash', function () {
|
||||
$process = new Process(['bash', '-n', base_path('deploy/install.sh')]);
|
||||
$process->run();
|
||||
|
||||
expect($process->getExitCode())->toBe(0, $process->getErrorOutput());
|
||||
});
|
||||
|
||||
it('definiert jede Hilfsfunktion, bevor sie gebraucht wird', function () {
|
||||
$source = installerSource();
|
||||
|
||||
foreach (['set_env', 'optional_env'] as $function) {
|
||||
$definedAt = strpos($source, " {$function}() {");
|
||||
expect($definedAt)->not->toBeFalse("{$function}() wird nicht mehr definiert.");
|
||||
|
||||
// Der erste Aufruf ist der erste Treffer, der KEINE Definition ist.
|
||||
$calledAt = preg_match(
|
||||
'/^\s+'.preg_quote($function, '/').' [A-Z]/m',
|
||||
$source,
|
||||
$matches,
|
||||
PREG_OFFSET_CAPTURE,
|
||||
) ? $matches[0][1] : false;
|
||||
|
||||
expect($calledAt)->not->toBeFalse("{$function} wird nirgends aufgerufen.");
|
||||
expect($definedAt)->toBeLessThan(
|
||||
$calledAt,
|
||||
"{$function}() wird vor seiner Definition aufgerufen — unter set -e ist das Exit 127.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('schreibt jeden Hostnamen, nach dem es fragt', function () {
|
||||
$source = installerSource();
|
||||
|
||||
// Gefragt wird nach allen fünf …
|
||||
foreach (['APP_DOMAIN', 'WWW_DOMAIN', 'STATUS_DOMAIN', 'FILES_DOMAIN', 'ADMIN_DOMAIN'] as $asked) {
|
||||
expect($source)->toMatch('/^ask\s+'.$asked.'\s/m', "Es wird nicht mehr nach {$asked} gefragt.");
|
||||
}
|
||||
|
||||
// … und jede Antwort landet auch in der .env. Genau das fehlte: die Fragen
|
||||
// standen da, die Werte wurden nie geschrieben, und die Trennung war
|
||||
// deshalb auf keiner Installation eingeschaltet.
|
||||
foreach (['APP_HOST', 'SITE_HOST', 'STATUS_HOST', 'FILES_HOST'] as $written) {
|
||||
expect($source)->toMatch(
|
||||
'/^\s+(set_env|optional_env)\s+'.$written.'\s/m',
|
||||
"{$written} wird nicht in die .env geschrieben.",
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue