]> Lady’s Gitweb - Blog/blob - 2023-05-06/touch_grass/#entry.rdf
[2023-12-17] programming_resolutions_24
[Blog] / 2023-05-06 / touch_grass / #entry.rdf
1 <awol:Entry
2 xml:lang="en"
3 xmlns:awol="http://bblfish.net/work/atom-owl/2006-06-06/"
4 xmlns:dc11="http://purl.org/dc/elements/1.1/"
5 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6 xmlns:sioc="http://rdfs.org/sioc/ns#"
7 >
8 <dc11:title>C·I pipelines have you down? Why not ‹ touch .grass ›?!</dc11:title>
9 <dc11:date>2023-05-06T23:19:48-07:00</dc11:date>
10 <dc11:abstract rdf:parseType="Markdown"><![CDATA[
11 You’re already building your site locally to develop it and test in
12 your browser. You don’t need to build it again on someone else’s
13 machine. Just upload the dang site.
14 ]]></dc11:abstract>
15 <sioc:content rdf:parseType="Markdown"><![CDATA[
16 This is a blogpost targeted at those who are, or are looking to
17 become :—
18
19 - Developers of independent single‐administrator static websites,
20
21 - Who have access to a file server that can serve their website on the
22 internet (Neocities, a personal server, a tilde club, ⁊·c),
23
24 - And are familiar with Git and plan on using Git to track changes to
25 the sites they create.
26
27 If this is you, and you have an account on GitLab or GitHub, you may be
28 considering using the pipeline features of GitLab C·I or GitHub Actions
29 to build and deploy your site every time you push new changes to the
30 repository. (Or, if you are very adventurous, you may be considering
31 writing a `post-receive` Git hook to do this yourself.)
32
33 C·I pipelines are very trendy right now because GitLab and GitHub can
34 make money by selling you the minutes needed to run them. And to be
35 clear: When you are working on a team, these features are extremely
36 useful and convenient! Also, if you are developing a library which
37 other people will use, green pipelines can help assure library users
38 that the code they are pulling passes its own test suite (for whatever
39 that knowledge is worth). But if you are just building a personal
40 website, for yourself, on your own computer?
41
42 This blogpost will walk you through an easier way. It doesn’t require
43 any additional accounts or services, it runs entirely on your own
44 computer, and it is 100% free and uses only programs you likely already
45 have installed.
46
47 ## Prerequisites
48
49 In order to put this method into practice, you will need the following
50 things installed on your computer :—
51
52 - GNU Make (check with `make --version`).
53
54 - A program for syncing your website with a remote server. This
55 blogpost will use Rsync, but the [Neocities C·L·I `push`
56 tool](https://neocities.org/cli) can be used if you are pushing to a
57 Neocities site.
58
59 In addition, you will need basic familiarity with the command line.
60
61 ## A Simple Build Script
62
63 For demonstration purposes, let’s get ourselves a build script for
64 generating a website. [Deno](https://deno.com/runtime) provides a fast
65 and convenient way to write useful scripts, so I’ll use that. The
66 following is a simple Deno script for converting Markdown files into
67 their H·T·M·L equivalents.
68
69 ```js
70 #!/usr/bin/env -S deno run --allow-read --allow-write
71 // build.js
72 // ====================================================================
73 //
74 // Copyright © 2023 Lady [@ Lady’s Computer].
75 //
76 // This Source Code Form is subject to the terms of the Mozilla Public
77 // License, v. 2.0. If a copy of the MPL was not distributed with this
78 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
79
80 // We’ll use the `rusty_markdown` package to parse Markdown files.
81 import {
82 html as markdownTokensToHTML,
83 tokens as markdownTokens,
84 } from "https://deno.land/x/rusty_markdown@v0.4.1/mod.ts";
85
86 /**
87 * Processes a directory by converting all the Markdown files in it
88 * (recursively) to HTML.
89 */
90 const processDirectory = async (path) => {
91 for await (const entry of Deno.readDir(path)) {
92 // Iterate over each entry in this directory and handle it
93 // accordingly.
94 const { isDirectory, isFile, name } = entry;
95 if (isFile && /\.(?:md|markdown)$/.test(name)) {
96 // This entry is a file ending in `.md` or `.markdown`. Parse its
97 // text contents into H·T·M·L and write the corresponding file.
98 const markdown = await Deno.readTextFile(`${path}/${name}`);
99 const html = markdownTokensToHTML(markdownTokens(markdown));
100 await Deno.writeTextFile(
101 `${path}/${name.replace(/\.(?:md|markdown)$/, ".html")}`,
102 `<!DOCTYPE html>${ html }`,
103 );
104 } else if (isDirectory) {
105 // This entry is a directory. Process it.
106 await processDirectory(`${path}/${name}`);
107 }
108 }
109 };
110
111 // Process the current directory (or the one supplied to the script).
112 await processDirectory(Deno.args[0] || ".");
113 ```
114
115 Obviously, this script leaves a lot to be desired: It doesn’t do any
116 templating, styling, or other processing of the input files, and so the
117 resulting H·T·M·L pages will be very boring. But it is good enough for
118 a demo. Feel free to substitute for this script whatever build system
119 you like.
120
121 ## A Simple Makefile
122
123 Building our website is already pretty easy, but there’s nothing easier
124 than just typing `make`, so next let’s create a Makefile for it. If
125 you’ve never encountered a Makefile before, they simply (or
126 not‐so‐simply) group targets (followed by a colon and their
127 prerequisites) with the rules (preceded by a tab) needed to make them.
128 There is a whole lot more information in [the GNU Make
129 manual](https://www.gnu.org/software/make/manual/html_node/), but for
130 this post that’s the gist. A simple one might look like this :—
131
132 ```make
133 build:
134 deno run --allow-read --allow-write ./build.js
135 ```
136
137 Make automatically builds the first target if you don’t specify one on
138 the command line, so now when you type `make` it should build your
139 site. Hooray!
140
141 While we’re at it, let’s add some rules for syncing, too.
142
143 ```make
144 # …continued from above
145
146 # This code assumes you have defined an Rsync filter at `.rsync-filter`
147 # which excludes all the files you don’t want to sync. If you’re using
148 # the Neocities C·L·I, it should automatically ignore anything in your
149 # `.gitignore`.
150 #
151 # I’m not going to go into all of the details on how Rsync works right
152 # now, but I encourage you to read up on it if you ever need to sync a
153 # local and remote copy of your website.
154
155 dry-sync:
156 rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public
157
158 sync:
159 rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public
160 ```
161
162 Now `make dry-sync` will show us what will happen if we try to sync our
163 local repository with our webserver, and `make sync` will actually do
164 it. `make && make sync` is an all‐in‐one way of building and syncing
165 our website, which is pretty convenient.
166
167 The above Makefile is literally all you need to build and deploy your
168 site, but it comes with some drawbacks. If you forget to build before
169 you sync, you might accidentally push an older version of your site
170 than you intended. And what about version control? It would be nice
171 if we ensure that is up‐to‐date at the same time.
172
173 ## Adding Version Control Support
174
175 In fact you can add all of that functionality to the same Makefile
176 without compromising the simplicity of running `make` and `make sync`.
177 Let’s start with version control: `git status --porcelain` will give us
178 the current status of the working tree, and be empty if all of our
179 changes are committed. `[ ! -z $(git status --porcelain) ]` is how you
180 say “`git status --porcelain` is not empty” in command line, so we can
181 use that to create the following check :—
182
183 ```sh
184 if [ ! -z "$(git status --porcelain)" ]
185 then
186 echo 'Error: There are uncommitted changes!'
187 echo 'Commit changes and run `make` before syncing.'
188 exit 1
189 fi
190 ```
191
192 Let’s also test that the current branch is up‐to‐date. In Git, the
193 current branch is signified with `HEAD` and its upstream can be
194 signified with `@{u}`. The command `git merge-base --is-ancestor` will
195 error if its first argument isn’t an ancestor of its second, so we can
196 use it in an `if` statement directly.
197
198 ```sh
199 if ! git merge-base --is-ancestor @{u} HEAD
200 then
201 echo 'Error: This branch is currently out‐of‐date!'
202 echo 'Pull in changes with `git pull` before syncing.'
203 exit 1
204 fi
205 ```
206
207 When we convert these checks into Makefile, we need to collapse them
208 into single lines and escape any dollar signs (with additional dollar
209 signs). So the Makefile rules for ensuring our files are committed and
210 our branch is up‐to‐date are :—
211
212 ```make
213 # The `@` at the beginning of the if statements tells Make not to
214 # bother printing anything for those lines. That’s okay, since the
215 # lines will echo out their own content if there is an error.
216
217 ensure-branch-up-to-date:
218 git fetch
219 @if ! git merge-base --is-ancestor @{u} HEAD; then echo 'Error: This branch is currently out‐of‐date!'; echo 'Pull in changes with `git pull` before syncing.'; exit 1; fi
220
221 ensure-clean:
222 @if [ ! -z "$$(git status --porcelain)" ]; then echo 'Error: There are uncommitted changes!'; echo 'Commit changes and run `make` before syncing.'; exit 1; fi
223 ```
224
225 We can ensure these checks run by adding them as prerequisites to our
226 syncing targets. Let’s add a `git push` to our `sync` target as well,
227 since we now know our branch is up‐to‐date.
228
229 ```make
230 dry-sync: ensure-clean ensure-branch-up-to-date
231 rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public
232
233 sync: ensure-clean ensure-branch-up-to-date
234 rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public
235 git push
236 ```
237
238 ## Checking for Builds by Touching Grass
239
240 To make sure that our latest build is current, we need to add an
241 additional rule to our `build` target :—
242
243 ```make
244 build:
245 deno run --allow-read --allow-write ./build.js
246 touch .grass
247 ```
248
249 `touch` is a simple command which updates the modification time on a
250 file, creating it if it doesn’t already exist. By touching `.grass` (an
251 otherwise meaningless file which we wouldn’t ever modify manually), we
252 can easily keep track of when our site was last built. **Add this file
253 to your `.gitignore` file so that Git doesn’t ever touch it for you.**
254
255 Because we already have checks in place to ensure that all of our
256 changes have been committed, ensuring our build is current is just a
257 matter of comparing times with the commit date on the latest commit.
258 You can get the modification date of `.grass` with
259 `stat -f '%m' .grass` (`%m` for “modified”), and you can get the last
260 commit time with `git log -1 --format='%ct'` (`%ct` for “commit time”).
261 So some suitable checks might be :—
262
263 ```sh
264 # This check ensures a file named `.grass` exists.
265 if [ ! -f .grass ]
266 then
267 echo 'Error: The website has not been built yet!'
268 echo 'Run `make` before syncing.'
269 exit 1
270 fi
271
272 # This check ensures the commit time isn’t greater than the modified
273 # time.
274 if [ "$(git log -1 --format='%ct')" -gt "$(stat -f '%m' .grass)" ]
275 then
276 echo 'Error: A commit was made after the last build!'
277 echo 'Run `make` before syncing.'
278 exit 1
279 fi
280 ```
281
282 Once again, when we move these checks into our Makefile, we need to
283 collapse them into single lines and escape their dollar signs :—
284
285 ```make
286 ensure-build:
287 @if [ ! -f .grass ]; then echo 'Error: The website has not been built yet!'; echo 'Run `make` before syncing.'; exit 1; fi
288 @if [ "$$(git log -1 --format='%ct')" -gt "$$(stat -f '%m' .grass)" ]; then echo 'Error: A commit was made after the last build!'; echo 'Run `make` before syncing.'; exit 1; fi
289 ```
290
291 ## Putting it all together
292
293 Our final Makefile might look something like this :—
294
295 ```make
296 # Makefile
297 # =====================================================================
298 #
299 # © 2023 Lady [@ Lady’s Computer].
300 #
301 # This Source Code Form is subject to the terms of the Mozilla Public
302 # License, v. 2.0. If a copy of the MPL was not distributed with this
303 # file, You can obtain one at https://mozilla.org/MPL/2.0/.
304
305 build:
306 deno run --allow-read --allow-write ./build.js
307 touch .grass
308
309 ensure-branch-up-to-date:
310 git fetch
311 @if ! git merge-base --is-ancestor @{u} HEAD; then echo 'Error: This branch is currently out‐of‐date!'; echo 'Pull in changes with `git pull` before syncing.'; exit 1; fi
312
313 ensure-clean:
314 @if [ ! -z "$$(git status --porcelain)" ]; then echo 'Error: There are uncommitted changes!'; echo 'Commit changes and run `make` before syncing.'; exit 1; fi
315
316 ensure-build:
317 @if [ ! -f .grass ]; then echo 'Error: The website has not been built yet!'; echo 'Run `make` before syncing.'; exit 1; fi
318 @if [ "$$(git log -1 --format='%ct')" -gt "$$(stat -f '%m' .grass)" ]; then echo 'Error: A commit was made after the last build!'; echo 'Run `make` before syncing.'; exit 1; fi
319
320 dry-sync: ensure-clean ensure-branch-up-to-date ensure-build
321 rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public
322
323 sync: ensure-clean ensure-branch-up-to-date ensure-build
324 rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public
325 git push
326 ```
327
328 To recap, this Makefile :—
329
330 - Builds the site with `make` (or `make build`) and pushes it to your
331 webserver with `make sync`.
332
333 - Refuses to sync if your current branch isn’t up‐to‐date.
334
335 - Refuses to sync if you have uncommitted changes in your working
336 directory.
337
338 - Refuses to sync if you haven’t built your site since your latest
339 commit.
340
341 It still isn’t perfect: It can’t protect you against incorrect builds
342 which you make *after* your latest commit, for example builds based on
343 files you have since deleted or stashed away. But it should catch most
344 common mistakes. For a fuller example, see [the Makefile for this
345 blog](https://git.ladys.computer/Blog/blob/14b487e:/GNUmakefile), which
346 is a little more verbose and includes some (small) additional
347 functionality.
348
349 It’s rare that I see people working in website technologies
350 (Javascript; static site generators) talking about Makefiles, which
351 makes sense because they kind of have an association with old compiled
352 languages and application programming. But they’re dead‐simple to
353 write, and can really simplify automation of a lot of tasks! Mostly I
354 worry that people write themselves into either overly‐complicated
355 technical solutions or a lot of error‐prone manual work for something
356 which could be as easy as typing a few words on a command line.
357
358 Don’t let this be you! Do the easy thing! `make sync`!
359 ]]></sioc:content>
360 <dc11:rights rdf:parseType="Markdown"><![CDATA[
361 Copyright © 2023
362 <a href="https://www.ladys.computer/about/#lady">Lady</a>
363 <small>[Exquisite Grass‐Toucher]</small>.
364 Some rights reserved.
365
366 This blogpost is licensed under a <a rel="license"
367 href="http://creativecommons.org/licenses/by/4.0/"><cite>Creative
368 Commons Attribution 4.0 International License</cite></a>.
369 ]]></dc11:rights>
370 </awol:Entry>
This page took 0.071008 seconds and 5 git commands to generate.