Commit bbf4eec5cd
Unsigned
Layout: unified · split
README.md +33 −13
| @@ -56,7 +56,7 @@ To obtain a working copy of this repository, execute the following | |||
| 56 | commands within a shell environment or Emacs shell interface: | 56 | commands within a shell environment or Emacs shell interface: |
| 57 | 57 | ||
| 58 | ``` shell | 58 | ``` shell |
| 59 | git clone https://git.sr.ht/~ccleberg/cleberg.net | 59 | git clone https://github.com/ccleberg/cleberg.net |
| 60 | cd cleberg.net | 60 | cd cleberg.net |
| 61 | emacs -nw | 61 | emacs -nw |
| 62 | ``` | 62 | ``` |
| @@ -69,32 +69,52 @@ For users employing Doom Emacs, open any repository Org file using | |||
| 69 | The `build.py` script wraps the build: it runs orgo, and then either | 69 | The `build.py` script wraps the build: it runs orgo, and then either |
| 70 | deploys the result or serves it locally. | 70 | deploys the result or serves it locally. |
| 71 | 71 | ||
| 72 | Two environment variables control what it does, and both default to | 72 | Environment variables control what it does, and all default to off: |
| 73 | off: | ||
| 74 | 73 | ||
| 75 | - `BUILD=true` performs the build. | 74 | - `BUILD=true` performs the build. |
| 76 | - `DEPLOY=true` deploys in production, or starts a development server | 75 | - `DEPLOY=true` deploys in production, or starts a development server |
| 77 | on port 8000 otherwise. | 76 | on port 8000 otherwise. |
| 78 | 77 | - `ENV=prod` selects production: image URLs are rewritten to be | |
| 79 | A third, `ENV`, selects the mode. Setting `ENV=prod` rewrites image | 78 | root-relative (see Deployment below) and the `ruff` pass is skipped. |
| 80 | URLs to be root-relative (see Deployment below) and skips the `ruff` | 79 | Anything else builds for development. |
| 81 | pass; anything else builds for development. | 80 | - `DRY_RUN=true` turns a production deploy into a report. Production |
| 81 | only; ignored everywhere else. | ||
| 82 | |||
| 83 | Exactly one combination writes to the server: | ||
| 84 | |||
| 85 | | `ENV` | `BUILD` | `DEPLOY` | `DRY_RUN` | What happens | Output | Server | | ||
| 86 | |--------|---------|----------|-----------|---------------------------------------|--------------|-------------------| | ||
| 87 | | unset | `true` | — | — | Development build, `ruff` first | `.build-dev/`| Untouched | | ||
| 88 | | unset | `true` | `true` | — | Development build, then serve on :8000| `.build-dev/`| Untouched | | ||
| 89 | | `prod` | `true` | — | — | Production build and image rewrite | `.build/` | Untouched | | ||
| 90 | | `prod` | — | `true` | `true` | Reports what a deploy would change | — | Read only | | ||
| 91 | | `prod` | `true` | `true` | — | Production build, then deploy | `.build/` | **Overwritten** | | ||
| 82 | 92 | ||
| 83 | ``` shell | 93 | ``` shell |
| 84 | # Production build: | ||
| 85 | ENV=prod BUILD=true uv run build.py | ||
| 86 | |||
| 87 | # Development build: | 94 | # Development build: |
| 88 | BUILD=true uv run build.py | 95 | BUILD=true uv run build.py |
| 89 | 96 | ||
| 90 | # Development build, then serve it on localhost:8000: | 97 | # Development build, then serve it on localhost:8000: |
| 91 | BUILD=true DEPLOY=true uv run build.py | 98 | BUILD=true DEPLOY=true uv run build.py |
| 99 | |||
| 100 | # Production build, no deploy: | ||
| 101 | ENV=prod BUILD=true uv run build.py | ||
| 102 | |||
| 103 | # What would a deploy change? Connects, compares, transfers nothing: | ||
| 104 | ENV=prod DEPLOY=true DRY_RUN=true uv run build.py | ||
| 105 | |||
| 106 | # Production build and deploy. This is the one that writes to the server: | ||
| 107 | ENV=prod BUILD=true DEPLOY=true uv run build.py | ||
| 92 | ``` | 108 | ``` |
| 93 | 109 | ||
| 94 | Generated site files reside in `.build/` for production and | 110 | Generated site files reside in `.build/` for production and |
| 95 | `.build-dev/` for development, ready for deployment. You can deploy the | 111 | `.build-dev/` for development. |
| 96 | resulting static site files via standard file transfer protocols such as | 112 | |
| 97 | `scp` or SFTP. | 113 | The deploy is `rsync --delete-before`, so files the build no longer |
| 114 | produces are removed from the server. That is what makes the dry run | ||
| 115 | worth having: a build that silently produced fewer pages would quietly | ||
| 116 | delete the rest. Lines beginning `*deleting` in the dry-run output are | ||
| 117 | what the real deploy would remove. | ||
| 98 | 118 | ||
| 99 | Builds are incremental: orgo keeps an `.orgo-cache.json` inside the | 119 | Builds are incremental: orgo keeps an `.orgo-cache.json` inside the |
| 100 | output directory and re-renders only what changed, so the directory is | 120 | output directory and re-renders only what changed, so the directory is |
build.py +37 −18
| @@ -31,13 +31,19 @@ import sys | |||
| 31 | from pathlib import Path | 31 | from pathlib import Path |
| 32 | 32 | ||
| 33 | 33 | ||
| 34 | def run(cmd, error): | 34 | def run(cmd, error, echo=False): |
| 35 | """Run cmd quietly, exiting with its stderr if it fails.""" | 35 | """Run cmd quietly, exiting with its stderr if it fails. |
| 36 | |||
| 37 | echo prints stdout on success. Only the dry run needs it: its output is the | ||
| 38 | whole reason to run it, and the default of swallowing stdout would hide it. | ||
| 39 | """ | ||
| 36 | result = subprocess.run(cmd, capture_output=True, text=True, check=False) | 40 | result = subprocess.run(cmd, capture_output=True, text=True, check=False) |
| 37 | if result.returncode != 0: | 41 | if result.returncode != 0: |
| 38 | print(error, file=sys.stderr) | 42 | print(error, file=sys.stderr) |
| 39 | print(result.stderr, file=sys.stderr) | 43 | print(result.stderr, file=sys.stderr) |
| 40 | sys.exit(1) | 44 | sys.exit(1) |
| 45 | if echo: | ||
| 46 | print(result.stdout, end="") | ||
| 41 | 47 | ||
| 42 | 48 | ||
| 43 | def run_ruff(): | 49 | def run_ruff(): |
| @@ -102,23 +108,31 @@ def run_orgo_build(build_dir): | |||
| 102 | sys.exit(1) | 108 | sys.exit(1) |
| 103 | 109 | ||
| 104 | 110 | ||
| 105 | def deploy_to_server(build_dir, server): | 111 | def deploy_to_server(build_dir, server, dry_run=False): |
| 112 | """Push the built site to the server, or show what pushing it would do. | ||
| 113 | |||
| 114 | The deploy deletes remote files the build no longer produces, so "what would | ||
| 115 | this remove" is a question worth being able to ask before answering it | ||
| 116 | irreversibly. DRY_RUN=true asks it: rsync connects and compares, then reports | ||
| 117 | instead of transferring. | ||
| 118 | """ | ||
| 106 | remote_path = f"{server}:/var/www/cleberg.net/" | 119 | remote_path = f"{server}:/var/www/cleberg.net/" |
| 107 | print(f"Deploying {build_dir}/ → {remote_path}") | 120 | print(f"{'Would deploy' if dry_run else 'Deploying'} {build_dir}/ → {remote_path}") |
| 108 | run( | 121 | cmd = [ |
| 122 | "rsync", | ||
| 123 | "-r", | ||
| 124 | "--delete-before", | ||
| 109 | # The build cache lives in the output directory because it describes it, but it | 125 | # The build cache lives in the output directory because it describes it, but it |
| 110 | # is not part of the site. Excluding it also stops --delete removing it locally. | 126 | # is not part of the site. Excluding it also stops --delete removing it locally. |
| 111 | [ | 127 | "--exclude", |
| 112 | "rsync", | 128 | ".orgo-cache.json", |
| 113 | "-r", | 129 | ] |
| 114 | "--delete-before", | 130 | if dry_run: |
| 115 | "--exclude", | 131 | # --itemize-changes because --dry-run alone prints almost nothing: the |
| 116 | ".orgo-cache.json", | 132 | # point is to name every file that would be sent or deleted. |
| 117 | f"{build_dir}/", | 133 | cmd += ["--dry-run", "--itemize-changes"] |
| 118 | remote_path, | 134 | cmd += [f"{build_dir}/", remote_path] |
| 119 | ], | 135 | run(cmd, "Error during rsync deployment:", echo=dry_run) |
| 120 | "Error during rsync deployment:", | ||
| 121 | ) | ||
| 122 | 136 | ||
| 123 | 137 | ||
| 124 | def start_dev_server(build_dir): | 138 | def start_dev_server(build_dir): |
| @@ -158,8 +172,13 @@ def main(): | |||
| 158 | 172 | ||
| 159 | if os.environ.get("DEPLOY", "").casefold() == "true": | 173 | if os.environ.get("DEPLOY", "").casefold() == "true": |
| 160 | if prod: | 174 | if prod: |
| 161 | print("Deploying to production...") | 175 | dry_run = os.environ.get("DRY_RUN", "").casefold() == "true" |
| 162 | deploy_to_server(build_dir, "homelab") | 176 | print( |
| 177 | "Dry run — the server will not be modified" | ||
| 178 | if dry_run | ||
| 179 | else "Deploying to production..." | ||
| 180 | ) | ||
| 181 | deploy_to_server(build_dir, "homelab", dry_run) | ||
| 163 | else: | 182 | else: |
| 164 | start_dev_server(build_dir) | 183 | start_dev_server(build_dir) |
| 165 | 184 | ||