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#"
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.
15 <sioc:content rdf:parseType="Markdown"><![CDATA[
16 This is a blogpost targeted at those who are, or are looking to
19 - Developers of independent single‐administrator static websites,
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),
24 - And are familiar with Git and plan on using Git to track changes to
25 the sites they create.
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.)
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?
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
49 In order to put this method into practice, you will need the following
50 things installed on your computer :—
52 - GNU Make (check with `make --version`).
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
59 In addition, you will need basic familiarity with the command line.
61 ## A Simple Build Script
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.
70 #!/usr/bin/env -S deno run --allow-read --allow-write
72 // ====================================================================
74 // Copyright © 2023 Lady [@ Lady’s Computer].
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/>.
80 // We’ll use the `rusty_markdown` package to parse Markdown files.
82 html as markdownTokensToHTML,
83 tokens as markdownTokens,
84 } from "https://deno.land/x/rusty_markdown@v0.4.1/mod.ts";
87 * Processes a directory by converting all the Markdown files in it
88 * (recursively) to HTML.
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
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 }`,
104 } else if (isDirectory) {
105 // This entry is a directory. Process it.
106 await processDirectory(`${path}/${name}`);
111 // Process the current directory (or the one supplied to the script).
112 await processDirectory(Deno.args[0] || ".");
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
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 :—
134 deno run --allow-read --allow-write ./build.js
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
141 While we’re at it, let’s add some rules for syncing, too.
144 # …continued from above
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
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.
156 rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public
159 rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public
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.
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.
173 ## Adding Version Control Support
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 :—
184 if [ ! -z "$(git status --porcelain)" ]
186 echo 'Error: There are uncommitted changes!'
187 echo 'Commit changes and run `make` before syncing.'
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.
199 if ! git merge-base --is-ancestor @{u} HEAD
201 echo 'Error: This branch is currently out‐of‐date!'
202 echo 'Pull in changes with `git pull` before syncing.'
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 :—
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.
217 ensure-branch-up-to-date:
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
222 @if [ ! -z "$$(git status --porcelain)" ]; then echo 'Error: There are uncommitted changes!'; echo 'Commit changes and run `make` before syncing.'; exit 1; fi
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.
230 dry-sync: ensure-clean ensure-branch-up-to-date
231 rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public
233 sync: ensure-clean ensure-branch-up-to-date
234 rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public
238 ## Checking for Builds by Touching Grass
240 To make sure that our latest build is current, we need to add an
241 additional rule to our `build` target :—
245 deno run --allow-read --allow-write ./build.js
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.**
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 :—
264 # This check ensures a file named `.grass` exists.
267 echo 'Error: The website has not been built yet!'
268 echo 'Run `make` before syncing.'
272 # This check ensures the commit time isn’t greater than the modified
274 if [ "$(git log -1 --format='%ct')" -gt "$(stat -f '%m' .grass)" ]
276 echo 'Error: A commit was made after the last build!'
277 echo 'Run `make` before syncing.'
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 :—
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
291 ## Putting it all together
293 Our final Makefile might look something like this :—
297 # =====================================================================
299 # © 2023 Lady [@ Lady’s Computer].
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/.
306 deno run --allow-read --allow-write ./build.js
309 ensure-branch-up-to-date:
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
314 @if [ ! -z "$$(git status --porcelain)" ]; then echo 'Error: There are uncommitted changes!'; echo 'Commit changes and run `make` before syncing.'; exit 1; fi
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
320 dry-sync: ensure-clean ensure-branch-up-to-date ensure-build
321 rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public
323 sync: ensure-clean ensure-branch-up-to-date ensure-build
324 rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public
328 To recap, this Makefile :—
330 - Builds the site with `make` (or `make build`) and pushes it to your
331 webserver with `make sync`.
333 - Refuses to sync if your current branch isn’t up‐to‐date.
335 - Refuses to sync if you have uncommitted changes in your working
338 - Refuses to sync if you haven’t built your site since your latest
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
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.
358 Don’t let this be you! Do the easy thing! `make sync`!
360 <dc11:rights rdf:parseType="Markdown"><![CDATA[
361 Copyright © 2023 Lady <small>[Exquisite Grass‐Toucher]</small>.
362 Some rights reserved.
364 This blogpost is licensed under a <a rel="license"
365 href="http://creativecommons.org/licenses/by/4.0/"><cite>Creative
366 Commons Attribution 4.0 International License</cite></a>.