feat(docker): add guided install and saved one-command updates
This commit is contained in:
1 parent
8dc187483c
commit
27447ce029
11 files changed
+395
-7
No files matched your search
@@ -36,3 +36,7 @@ db_backup_*.sql
|
||||
*.log
|
||||
.codex
|
||||
.agents
|
||||
|
||||
.docker-install
|
||||
.docker-install.tmp.*
|
||||
.env.install.*
|
||||
@@ -45,3 +45,8 @@ coverage/
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
|
||||
# Local Docker installation metadata
|
||||
.docker-install
|
||||
.docker-install.tmp.*
|
||||
.env.install.*
|
||||
@@ -217,6 +217,50 @@ local `/swf/c_images/album1584` fallback. Public URL resolution uses runtime `AP
|
||||
After changing `.env`, recreate the container; an image rebuild is not required.
|
||||
`NEXT_PUBLIC_CMS_RELEASE` deliberately remains compiled into the image.
|
||||
|
||||
### Guided installation from this repository
|
||||
|
||||
On a Linux host with Docker Engine, the Compose plugin, Git and `flock` available:
|
||||
|
||||
```bash
|
||||
git clone https://gitlab.epicnabbo.nl/remco/EpicNext-Cms.git
|
||||
cd EpicNext-Cms
|
||||
bash cms install
|
||||
```
|
||||
|
||||
The wizard asks for your public URL, delivery method, hotel name, existing database
|
||||
connection, Redis and avatar imager. It generates an authentication secret and
|
||||
creates `.env` with private permissions. An existing `.env` is preserved; review
|
||||
its `APP_URL`, `AUTH_URL`, RCON and optional original Laravel `APP_KEY` when moving
|
||||
an existing hotel. This installs the CMS, not the Habbo database, emulator, Redis
|
||||
or reverse proxy. Point HTTPS at port 3002 before running the final public check.
|
||||
The wizard may request sudo to prepare persistent directories for UID/GID 33;
|
||||
it does not recursively change ownership of existing assets.
|
||||
|
||||
Choose **prebuilt** to download the images specified by this repository in
|
||||
`docker-image.txt`, without entering a registry namespace or commit tag. Choose
|
||||
**source** when building your own fork on the installation host. Private packages
|
||||
still require `docker login` on that host. Maintainers must update `docker-image.txt`
|
||||
if the publication registry or namespace changes.
|
||||
|
||||
After installation, update with:
|
||||
|
||||
```bash
|
||||
bash cms update
|
||||
```
|
||||
|
||||
The updater pulls the configured Git upstream, loads `.docker-install`, uses the
|
||||
matching application/migration images and verifies the running release locally
|
||||
and at the saved public URL. If publication for the new commit is still running,
|
||||
it stops before replacing the current container; run the same command after CI
|
||||
succeeds. Existing application rollback remains available on a failed cutover;
|
||||
database migrations are not reversed.
|
||||
|
||||
`bash cms install --configure-only` saves configuration without preparing runtime
|
||||
directories or starting containers. Neither `.env` nor `.docker-install` is
|
||||
committed or sent in Docker build contexts. Existing users of
|
||||
`bash scripts/docker-update.sh` retain the previous behavior when no wizard
|
||||
profile exists; explicit `CMS_IMAGE_REPOSITORY`/`CMS_PUBLIC_URL` overrides still work.
|
||||
|
||||
To publish from Gitea:
|
||||
|
||||
Gitea packages belong to an account or organization, independently of repository
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
case "${1:-help}" in
|
||||
install) shift; exec bash "$DIR/scripts/docker-install.sh" "$@" ;;
|
||||
update) shift; [[ $# = 0 ]] || { echo "Usage: bash cms update" >&2; exit 1; }; exec bash "$DIR/scripts/docker-update.sh" ;;
|
||||
help|--help|-h) printf '%s\n' 'bash cms install Configure and install on a Linux Docker host' 'bash cms update Update using the saved installation settings' ;;
|
||||
*) echo "Unknown command. Use: bash cms install | update" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1 @@
|
||||
gitlab.epicnabbo.nl/simo/epicnext-cms
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Read installation metadata as data, never execute it as a shell script.
|
||||
load_docker_config() {
|
||||
local root="$1" key value mode="" public_url="" repository=""
|
||||
[[ -f "$root/.docker-install" ]] || return 0
|
||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
value="${value%$'\r'}"
|
||||
case "$key" in
|
||||
''|'#'*) continue ;;
|
||||
MODE) [[ "$value" = prebuilt || "$value" = source ]] || return 1; mode="$value" ;;
|
||||
PUBLIC_URL) [[ "$value" =~ ^https?://[^[:space:]]+$ && "$value" != *'@'* && "$value" != *'?'* && "$value" != *'#'* ]] || return 1; public_url="$value" ;;
|
||||
*) echo "Unknown installation setting: $key" >&2; return 1 ;;
|
||||
esac
|
||||
done < "$root/.docker-install"
|
||||
[[ -n "$mode" && -n "$public_url" ]] || return 1
|
||||
if [[ "$mode" = prebuilt && ! ${CMS_IMAGE_REPOSITORY+x} ]]; then
|
||||
IFS= read -r repository < "$root/docker-image.txt"
|
||||
repository="${repository%$'\r'}"
|
||||
[[ "$repository" =~ ^[a-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$ ]] || return 1
|
||||
export CMS_IMAGE_REPOSITORY="$repository"
|
||||
fi
|
||||
CMS_INSTALL_MODE="$mode"
|
||||
export CMS_PUBLIC_URL="${CMS_PUBLIC_URL-$public_url}"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -69,6 +69,9 @@ if [ "$script_before" != "$(git hash-object scripts/docker-update.sh)" ]; then
|
||||
exec 9>&-
|
||||
exec bash "$DIR/scripts/docker-update.sh"
|
||||
fi
|
||||
# Load after pulling so image location changes follow the repository.
|
||||
source "$DIR/scripts/docker-config.sh"
|
||||
load_docker_config "$DIR" || die "Invalid .docker-install: expected MODE=prebuilt or source and PUBLIC_URL=http(s)://your-host."
|
||||
export CMS_RELEASE="$(git rev-parse HEAD)"
|
||||
[[ "$CMS_RELEASE" =~ ^[0-9a-f]{40}$ ]] || die "Invalid Git commit."
|
||||
[[ -f .env ]] || die "Create .env before installing or updating."
|
||||
@@ -82,16 +85,16 @@ if [[ -n "$previous_container" ]]; then
|
||||
rollback_tag="rollback-$CMS_RELEASE-$$"
|
||||
docker image tag "$previous_image" "epicnext-cms:$rollback_tag"
|
||||
fi
|
||||
log "Building release $CMS_RELEASE from $DIR"
|
||||
log "Preparing release $CMS_RELEASE from $DIR"
|
||||
# The migrations stage contains matching source and locked dependencies.
|
||||
# No Node/package manager installation on the host is required.
|
||||
migration_image="epicnext-cms-migrations:$CMS_RELEASE"
|
||||
if [[ -n "${CMS_IMAGE_REPOSITORY:-}" ]]; then
|
||||
[[ "$CMS_IMAGE_REPOSITORY" =~ ^[a-z0-9.-]+(:[0-9]+)?/[a-z0-9._/-]+$ ]] || die "Invalid CMS_IMAGE_REPOSITORY; use registry/owner/image without a tag."
|
||||
log "Pulling prebuilt application and matching migrations for $CMS_RELEASE"
|
||||
docker pull "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" >>"$LOG_FILE" 2>&1
|
||||
docker pull "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" >>"$LOG_FILE" 2>&1 || die "Application image unavailable. Publication for this commit may still be running; retry bash cms update after CI succeeds. For private packages, log in to the registry first."
|
||||
remote_migration_image="$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations"
|
||||
docker pull "$remote_migration_image" >>"$LOG_FILE" 2>&1
|
||||
docker pull "$remote_migration_image" >>"$LOG_FILE" 2>&1 || die "Matching migrations image unavailable. Current CMS is unchanged; retry after publication succeeds."
|
||||
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE" "epicnext-cms:$CMS_RELEASE"
|
||||
docker tag "$CMS_IMAGE_REPOSITORY:$CMS_RELEASE-migrations" "$migration_image"
|
||||
else
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -30,6 +30,14 @@ function simulate(scenario: string) {
|
||||
try {
|
||||
mkdirSync(join(dir, "scripts"));
|
||||
mkdirSync(join(dir, "logs"));
|
||||
copyFileSync(
|
||||
resolve(root, "scripts/docker-config.sh"),
|
||||
join(dir, "scripts/docker-config.sh"),
|
||||
);
|
||||
copyFileSync(
|
||||
resolve(root, "docker-image.txt"),
|
||||
join(dir, "docker-image.txt"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(dir, "logs/docker-release-history.log"),
|
||||
`${"b".repeat(40)}\n${"c".repeat(40)}\n`,
|
||||
@@ -39,6 +47,11 @@ function simulate(scenario: string) {
|
||||
join(dir, "scripts/docker-update.sh"),
|
||||
);
|
||||
writeFileSync(join(dir, ".env"), "HOTEL_NAME=Test\n");
|
||||
if (scenario.startsWith("saved-"))
|
||||
writeFileSync(
|
||||
join(dir, ".docker-install"),
|
||||
`MODE=${scenario === "saved-source" ? "source" : "prebuilt"}\nPUBLIC_URL=https://saved.test\n`,
|
||||
);
|
||||
const result = spawnSync(bash, [join(dir, "scripts/docker-update.sh")], {
|
||||
cwd: dir,
|
||||
encoding: "utf8",
|
||||
@@ -49,10 +62,14 @@ function simulate(scenario: string) {
|
||||
TEST_DIR: dir.replaceAll("\\", "/"),
|
||||
TEST_SHA: sha,
|
||||
SCENARIO: scenario,
|
||||
CMS_PUBLIC_URL: "https://example.test",
|
||||
CMS_IMAGE_REPOSITORY: scenario.startsWith("registry")
|
||||
? "registry.test/team/cms"
|
||||
: "",
|
||||
CMS_PUBLIC_URL: scenario.startsWith("saved-")
|
||||
? undefined
|
||||
: "https://example.test",
|
||||
CMS_IMAGE_REPOSITORY: scenario.startsWith("saved-")
|
||||
? undefined
|
||||
: scenario.startsWith("registry")
|
||||
? "registry.test/team/cms"
|
||||
: "",
|
||||
},
|
||||
});
|
||||
if (result.error) throw result.error;
|
||||
@@ -169,3 +186,19 @@ describe("HTTP release verification", () => {
|
||||
expect(result.status, result.stderr).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("updates using the saved profile and repository-provided image without arguments", () => {
|
||||
const r = simulate("saved-prebuilt");
|
||||
expect(r.status, r.output).toBe(0);
|
||||
expect(r.calls).toContain(
|
||||
`docker pull gitlab.epicnabbo.nl/simo/epicnext-cms:${sha}`,
|
||||
);
|
||||
expect(r.calls).toContain("https://saved.test/api/health");
|
||||
expect(r.calls).not.toContain("docker compose build");
|
||||
});
|
||||
it("keeps source builds available for a saved source installation", () => {
|
||||
const r = simulate("saved-source");
|
||||
expect(r.status, r.output).toBe(0);
|
||||
expect(r.calls).toContain("docker compose build");
|
||||
expect(r.calls).not.toContain("docker pull");
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
# No Docker, permissions or external services are modified by wizard tests.
|
||||
docker() { return 0; }
|
||||
uname() { echo Linux; }
|
||||
flock() { :; }
|
||||
export -f docker uname flock
|
||||
install() { :; }
|
||||
chown() { :; }
|
||||
stat() { echo 33:33; }
|
||||
sudo() { "$@"; }
|
||||
export -f install chown stat sudo
|
||||
Reference in new issue
Block a user