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 | 56 | commands within a shell environment or Emacs shell interface: |
| 57 | 57 | |
| 58 | 58 | ``` shell |
| 59 | git clone https://git.sr.ht/~ccleberg/cleberg.net | |
| 59 | git clone https://github.com/ccleberg/cleberg.net | |
| 60 | 60 | cd cleberg.net |
| 61 | 61 | emacs -nw |
| 62 | 62 | ``` |
| @@ -69,32 +69,52 @@ For users employing Doom Emacs, open any repository Org file using | ||
| 69 | 69 | The `build.py` script wraps the build: it runs orgo, and then either |
| 70 | 70 | deploys the result or serves it locally. |
| 71 | 71 | |
| 72 | Two environment variables control what it does, and both default to | |
| 73 | off: | |
| 72 | Environment variables control what it does, and all default to off: | |
| 74 | 73 | |
| 75 | 74 | - `BUILD=true` performs the build. |
| 76 | 75 | - `DEPLOY=true` deploys in production, or starts a development server |
| 77 | 76 | on port 8000 otherwise. |
| 78 | ||
| 79 | A third, `ENV`, selects the mode. Setting `ENV=prod` rewrites image | |
| 80 | URLs to be root-relative (see Deployment below) and skips the `ruff` | |
| 81 | pass; anything else builds for development. | |
| 77 | - `ENV=prod` selects production: image URLs are rewritten to be | |
| 78 | root-relative (see Deployment below) and the `ruff` pass is skipped. | |
| 79 | 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 | 93 | ``` shell |
| 84 | # Production build: | |
| 85 | ENV=prod BUILD=true uv run build.py | |
| 86 | ||
| 87 | 94 | # Development build: |
| 88 | 95 | BUILD=true uv run build.py |
| 89 | 96 | |
| 90 | 97 | # Development build, then serve it on localhost:8000: |
| 91 | 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 | 110 | Generated site files reside in `.build/` for production and |
| 95 | `.build-dev/` for development, ready for deployment. You can deploy the | |
| 96 | resulting static site files via standard file transfer protocols such as | |
| 97 | `scp` or SFTP. | |
| 111 | `.build-dev/` for development. | |
| 112 | ||
| 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 | 119 | Builds are incremental: orgo keeps an `.orgo-cache.json` inside the |
| 100 | 120 | output directory and re-renders only what changed, so the directory is |
build.py +37 −18
| @@ -31,13 +31,19 @@ import sys | ||
| 31 | 31 | from pathlib import Path |
| 32 | 32 | |
| 33 | 33 | |
| 34 | def run(cmd, error): | |
| 35 | """Run cmd quietly, exiting with its stderr if it fails.""" | |
| 34 | def run(cmd, error, echo=False): | |
| 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 | 40 | result = subprocess.run(cmd, capture_output=True, text=True, check=False) |
| 37 | 41 | if result.returncode != 0: |
| 38 | 42 | print(error, file=sys.stderr) |
| 39 | 43 | print(result.stderr, file=sys.stderr) |
| 40 | 44 | sys.exit(1) |
| 45 | if echo: | |
| 46 | print(result.stdout, end="") | |
| 41 | 47 | |
| 42 | 48 | |
| 43 | 49 | def run_ruff(): |
| @@ -102,23 +108,31 @@ def run_orgo_build(build_dir): | ||
| 102 | 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 | 119 | remote_path = f"{server}:/var/www/cleberg.net/" |
| 107 | print(f"Deploying {build_dir}/ → {remote_path}") | |
| 108 | run( | |
| 120 | print(f"{'Would deploy' if dry_run else 'Deploying'} {build_dir}/ → {remote_path}") | |
| 121 | cmd = [ | |
| 122 | "rsync", | |
| 123 | "-r", | |
| 124 | "--delete-before", | |
| 109 | 125 | # The build cache lives in the output directory because it describes it, but it |
| 110 | 126 | # is not part of the site. Excluding it also stops --delete removing it locally. |
| 111 | [ | |
| 112 | "rsync", | |
| 113 | "-r", | |
| 114 | "--delete-before", | |
| 115 | "--exclude", | |
| 116 | ".orgo-cache.json", | |
| 117 | f"{build_dir}/", | |
| 118 | remote_path, | |
| 119 | ], | |
| 120 | "Error during rsync deployment:", | |
| 121 | ) | |
| 127 | "--exclude", | |
| 128 | ".orgo-cache.json", | |
| 129 | ] | |
| 130 | if dry_run: | |
| 131 | # --itemize-changes because --dry-run alone prints almost nothing: the | |
| 132 | # point is to name every file that would be sent or deleted. | |
| 133 | cmd += ["--dry-run", "--itemize-changes"] | |
| 134 | cmd += [f"{build_dir}/", remote_path] | |
| 135 | run(cmd, "Error during rsync deployment:", echo=dry_run) | |
| 122 | 136 | |
| 123 | 137 | |
| 124 | 138 | def start_dev_server(build_dir): |
| @@ -158,8 +172,13 @@ def main(): | ||
| 158 | 172 | |
| 159 | 173 | if os.environ.get("DEPLOY", "").casefold() == "true": |
| 160 | 174 | if prod: |
| 161 | print("Deploying to production...") | |
| 162 | deploy_to_server(build_dir, "homelab") | |
| 175 | dry_run = os.environ.get("DRY_RUN", "").casefold() == "true" | |
| 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 | 182 | else: |
| 164 | 183 | start_dev_server(build_dir) |
| 165 | 184 | |