From: Lady Date: Sun, 7 May 2023 06:19:48 +0000 (-0700) Subject: [2023-05-06] touch_grass X-Git-Url: https://git.ladys.computer/Blog/commitdiff_plain/594d71a2dc3c9edfb8374a86d7edf80aed5f336d?ds=sidebyside [2023-05-06] touch_grass --- diff --git a/2023-05-06/touch_grass/#entry.rdf b/2023-05-06/touch_grass/#entry.rdf new file mode 100644 index 0000000..129687e --- /dev/null +++ b/2023-05-06/touch_grass/#entry.rdf @@ -0,0 +1,368 @@ + + C·I pipelines have you down? Why not ‹ touch .grass ›?! + 2023-05-06T23:19:48-07:00 + + . + +// We’ll use the `rusty_markdown` package to parse Markdown files. +import { + html as markdownTokensToHTML, + tokens as markdownTokens, +} from "https://deno.land/x/rusty_markdown@v0.4.1/mod.ts"; + +/** + * Processes a directory by converting all the Markdown files in it + * (recursively) to HTML. + */ +const processDirectory = async (path) => { + for await (const entry of Deno.readDir(path)) { + // Iterate over each entry in this directory and handle it + // accordingly. + const { isDirectory, isFile, name } = entry; + if (isFile && /\.(?:md|markdown)$/.test(name)) { + // This entry is a file ending in `.md` or `.markdown`. Parse its + // text contents into H·T·M·L and write the corresponding file. + const markdown = await Deno.readTextFile(`${path}/${name}`); + const html = markdownTokensToHTML(markdownTokens(markdown)); + await Deno.writeTextFile( + `${path}/${name.replace(/\.(?:md|markdown)$/, ".html")}`, + `${ html }`, + ); + } else if (isDirectory) { + // This entry is a directory. Process it. + await processDirectory(`${path}/${name}`); + } + } +}; + +// Process the current directory (or the one supplied to the script). +await processDirectory(Deno.args[0] || "."); +``` + +Obviously, this script leaves a lot to be desired: It doesn’t do any +templating, styling, or other processing of the input files, and so the +resulting H·T·M·L pages will be very boring. But it is good enough for +a demo. Feel free to substitute for this script whatever build system +you like. + +## A Simple Makefile + +Building our website is already pretty easy, but there’s nothing easier +than just typing `make`, so next let’s create a Makefile for it. If +you’ve never encountered a Makefile before, they simply (or +not‐so‐simply) group targets (followed by a colon and their +prerequisites) with the rules (preceded by a tab) needed to make them. +There is a whole lot more information in [the GNU Make +manual](https://www.gnu.org/software/make/manual/html_node/), but for +this post that’s the gist. A simple one might look like this :— + +```make +build: + deno run --allow-read --allow-write ./build.js +``` + +Make automatically builds the first target if you don’t specify one on +the command line, so now when you type `make` it should build your +site. Hooray! + +While we’re at it, let’s add some rules for syncing, too. + +```make +# …continued from above + +# This code assumes you have defined an Rsync filter at `.rsync-filter` +# which excludes all the files you don’t want to sync. If you’re using +# the Neocities C·L·I, it should automatically ignore anything in your +# `.gitignore`. +# +# I’m not going to go into all of the details on how Rsync works right +# now, but I encourage you to read up on it if you ever need to sync a +# local and remote copy of your website. + +dry-sync: + rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public + +sync: + rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public +``` + +Now `make dry-sync` will show us what will happen if we try to sync our +local repository with our webserver, and `make sync` will actually do +it. `make && make sync` is an all‐in‐one way of building and syncing +our website, which is pretty convenient. + +The above Makefile is literally all you need to build and deploy your +site, but it comes with some drawbacks. If you forget to build before +you sync, you might accidentally push an older version of your site +than you intended. And what about version control? It would be nice +if we ensure that is up‐to‐date at the same time. + +## Adding Version Control Support + +In fact you can add all of that functionality to the same Makefile +without compromising the simplicity of running `make` and `make sync`. +Let’s start with version control: `git status --porcelain` will give us +the current status of the working tree, and be empty if all of our +changes are committed. `[ ! -z $(git status --porcelain) ]` is how you +say “`git status --porcelain` is not empty” in command line, so we can +use that to create the following check :— + +```sh +if [ ! -z "$(git status --porcelain)" ] +then + echo 'Error: There are uncommitted changes!' + echo 'Commit changes and run `make` before syncing.' + exit 1 +fi +``` + +Let’s also test that the current branch is up‐to‐date. In Git, the +current branch is signified with `HEAD` and its upstream can be +signified with `@{u}`. The command `git merge-base --is-ancestor` will +error if its first argument isn’t an ancestor of its second, so we can +use it in an `if` statement directly. + +```sh +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 +``` + +When we convert these checks into Makefile, we need to collapse them +into single lines and escape any dollar signs (with additional dollar +signs). So the Makefile rules for ensuring our files are committed and +our branch is up‐to‐date are :— + +```make +# The `@` at the beginning of the if statements tells Make not to +# bother printing anything for those lines. That’s okay, since the +# lines will echo out their own content if there is an error. + +ensure-branch-up-to-date: + git fetch + @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 + +ensure-clean: + @if [ ! -z "$$(git status --porcelain)" ]; then echo 'Error: There are uncommitted changes!'; echo 'Commit changes and run `make` before syncing.'; exit 1; fi +``` + +We can ensure these checks run by adding them as prerequisites to our +syncing targets. Let’s add a `git push` to our `sync` target as well, +since we now know our branch is up‐to‐date. + +```make +dry-sync: ensure-clean ensure-branch-up-to-date + rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public + +sync: ensure-clean ensure-branch-up-to-date + rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public + git push +``` + +## Checking for Builds by Touching Grass + +To make sure that our latest build is current, we need to add an +additional rule to our `build` target :— + +```make +build: + deno run --allow-read --allow-write ./build.js + touch .grass +``` + +`touch` is a simple command which updates the modification time on a +file, creating it if it doesn’t already exist. By touching `.grass` (an +otherwise meaningless file which we wouldn’t ever modify manually), we +can easily keep track of when our site was last built. **Add this file +to your `.gitignore` file so that Git doesn’t ever touch it for you.** + +Because we already have checks in place to ensure that all of our +changes have been committed, ensuring our build is current is just a +matter of comparing times with the commit date on the latest commit. +You can get the modification date of `.grass` with +`stat -f '%m' .grass` (`%m` for “modified”), and you can get the last +commit time with `git log -1 --format='%ct'` (`%ct` for “commit time”). +So some suitable checks might be :— + +```sh +# This check ensures a file named `.grass` exists. +if [ ! -f .grass ] +then + echo 'Error: The website has not been built yet!' + echo 'Run `make` before syncing.' + exit 1 +fi + +# This check ensures the commit time isn’t greater than the modified +# time. +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 +``` + +Once again, when we move these checks into our Makefile, we need to +collapse them into single lines and escape their dollar signs :— + +```make +ensure-build: + @if [ ! -f .grass ]; then echo 'Error: The website has not been built yet!'; echo 'Run `make` before syncing.'; exit 1; fi + @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 +``` + +## Putting it all together + +Our final Makefile might look something like this :— + +```make +# Makefile +# ===================================================================== +# +# © 2023 Lady [@ Lady’s Computer]. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +build: + deno run --allow-read --allow-write ./build.js + touch .grass + +ensure-branch-up-to-date: + git fetch + @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 + +ensure-clean: + @if [ ! -z "$$(git status --porcelain)" ]; then echo 'Error: There are uncommitted changes!'; echo 'Commit changes and run `make` before syncing.'; exit 1; fi + +ensure-build: + @if [ ! -f .grass ]; then echo 'Error: The website has not been built yet!'; echo 'Run `make` before syncing.'; exit 1; fi + @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 + +dry-sync: ensure-clean ensure-branch-up-to-date ensure-build + rsync -Oclmrtvz --del --dry-run --filter=". .rsync-filter" . webserver:www/public + +sync: ensure-clean ensure-branch-up-to-date ensure-build + rsync -Oclmrtvz --del --filter=". .rsync-filter" . webserver:www/public + git push +``` + +To recap, this Makefile :— + +- Builds the site with `make` (or `make build`) and pushes it to your + webserver with `make sync`. + +- Refuses to sync if your current branch isn’t up‐to‐date. + +- Refuses to sync if you have uncommitted changes in your working + directory. + +- Refuses to sync if you haven’t built your site since your latest + commit. + +It still isn’t perfect: It can’t protect you against incorrect builds +which you make *after* your latest commit, for example builds based on +files you have since deleted or stashed away. But it should catch most +common mistakes. For a fuller example, see [the Makefile for this +blog](https://git.ladys.computer/Blog/blob/14b487e:/GNUmakefile), which +is a little more verbose and includes some (small) additional +functionality. + +It’s rare that I see people working in website technologies +(Javascript; static site generators) talking about Makefiles, which +makes sense because they kind of have an association with old compiled +languages and application programming. But they’re dead‐simple to +write, and can really simplify automation of a lot of tasks! Mostly I +worry that people write themselves into either overly‐complicated +technical solutions or a lot of error‐prone manual work for something +which could be as easy as typing a few words on a command line. + +Don’t let this be you! Do the easy thing! `make sync`! +]]> + [Exquisite Grass‐Toucher]. +Some rights reserved. + +This blogpost is licensed under a Creative +Commons Attribution 4.0 International License. +]]> +