build: add Docker builds

Co-authored-by: Jakub Stastka <jakub@stastka.org>
This commit is contained in:
Jakub Stastka
2026-04-19 09:58:04 +09:00
committed by GitHub
co-authored by Jakub Stastka
parent 7426924170
commit 696fad78b3
7 changed files with 99 additions and 2 deletions
+12
View File
@@ -0,0 +1,12 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir .
CMD ["pqcli", "--basic"]
+37
View File
@@ -41,6 +41,43 @@ $ cd pq-cli
$ pip install --user . $ pip install --user .
``` ```
## Docker / Docker Compose
The repository includes a `Dockerfile` and `docker-compose.yml` that run the
game with `--basic` and keep save data in a persistent named volume.
There is no registry pipeline required for this setup: build locally, then run
with Compose.
```console
# Build image locally from the Dockerfile
docker compose build
# First run (interactive): create your character in the shared save volume
docker compose run --rm pqcli-init
# Later runs on a server (detached, default save slot 1):
docker compose up -d pqcli
# List existing saves in the same volume:
docker compose run --rm pqcli-init pqcli --basic --list-saves
# Change detached slot if needed (example: slot 2):
PQCLI_SAVE_SLOT=2 docker compose up -d pqcli
# Follow logs / stop detached container:
docker compose logs -f pqcli
docker compose stop pqcli
```
On first run, if no save data exists in the mounted volume, `pqcli` will
automatically launch an interactive character-creation bootstrap in CLI mode
before starting the normal interface.
For server usage, run first-time setup without a forced slot (`pqcli-init`),
then run the long-lived detached service (`pqcli`) that loads slot `1` by
default.
## Contributing ## Contributing
```sh ```sh
+26
View File
@@ -0,0 +1,26 @@
services:
pqcli-init:
build: .
image: ghcr.io/${GITHUB_REPOSITORY_OWNER:-local}/pq-cli:latest
stdin_open: true
tty: true
command: ["pqcli", "--basic"]
environment:
XDG_CONFIG_HOME: /data
volumes:
- pqcli-data:/data
pqcli:
build: .
stdin_open: true
tty: true
stop_signal: SIGINT
stop_grace_period: 30s
command: ["sh", "-c", "pqcli --basic --load-save ${PQCLI_SAVE_SLOT:-1}"]
environment:
XDG_CONFIG_HOME: /data
volumes:
- pqcli-data:/data
volumes:
pqcli-data:
+17
View File
@@ -66,6 +66,21 @@ def list_players(roster: Roster, file: T.Optional[T.Any] = None) -> None:
print(f"{i}. {player.name}", file=file) print(f"{i}. {player.name}", file=file)
def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None:
if roster.players or args.list_saves or args.load_save:
return
print("No saved characters found. Starting first-run character creation.")
ui = BasicUserInterface(roster, None, args)
while not roster.players:
player = ui.create_player(auto_play=False)
if player:
return
if not ui.confirm("No character created. Do you want to try again?"):
print("A character is required for first run.", file=sys.stderr)
raise SystemExit(1)
def main() -> None: def main() -> None:
args = parse_args() args = parse_args()
roster = Roster.load(SAVE_PATH) roster = Roster.load(SAVE_PATH)
@@ -83,6 +98,8 @@ def main() -> None:
list_players(roster, file=sys.stderr) list_players(roster, file=sys.stderr)
exit(1) exit(1)
bootstrap_first_run(roster, args)
try: try:
ui = args.ui(roster, player, args) ui = args.ui(roster, player, args)
ui.run() ui.run()
+4 -2
View File
@@ -101,7 +101,7 @@ class BasicUserInterface(BaseUserInterface):
self.quit() self.quit()
break break
def create_player(self) -> T.Optional[Player]: def create_player(self, *, auto_play: bool = True) -> T.Optional[Player]:
name = input("Name your new character: ") name = input("Name your new character: ")
if not name: if not name:
print("Cancelled.") print("Cancelled.")
@@ -123,7 +123,9 @@ class BasicUserInterface(BaseUserInterface):
name=name, race=race, class_=class_, stats=stats name=name, race=race, class_=class_, stats=stats
) )
self.roster.players.append(player) self.roster.players.append(player)
if self.confirm("Do you want to play as your new character?"): if auto_play and self.confirm(
"Do you want to play as your new character?"
):
self.play(player) self.play(player)
return player return player
+1
View File
@@ -23,6 +23,7 @@ dependencies = [
"xdg-base-dirs>=6.0.0,<7.0.0", "xdg-base-dirs>=6.0.0,<7.0.0",
"urwid>=2.1.2,<3.0.0", "urwid>=2.1.2,<3.0.0",
"urwid-readline>=0.13,<1.0.0", "urwid-readline>=0.13,<1.0.0",
"pre-commit>=4.5.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
Generated
+2
View File
@@ -61,6 +61,7 @@ name = "pqcli"
version = "1.0.4" version = "1.0.4"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "pre-commit" },
{ name = "urwid" }, { name = "urwid" },
{ name = "urwid-readline" }, { name = "urwid-readline" },
{ name = "xdg-base-dirs" }, { name = "xdg-base-dirs" },
@@ -78,6 +79,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "pre-commit", specifier = ">=4.5.0" },
{ name = "urwid", specifier = ">=2.1.2,<3.0.0" }, { name = "urwid", specifier = ">=2.1.2,<3.0.0" },
{ name = "urwid-readline", specifier = ">=0.13,<1.0.0" }, { name = "urwid-readline", specifier = ">=0.13,<1.0.0" },
{ name = "windows-curses", marker = "extra == 'windows'", specifier = ">=2.3.0,<3.0.0" }, { name = "windows-curses", marker = "extra == 'windows'", specifier = ">=2.3.0,<3.0.0" },