diff --git a/.envrc b/.envrc deleted file mode 100644 index 3550a30..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 1ee3911..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: deploy - -on: - push: - branches: [master] - schedule: - - cron: "0 1 * * *" # daily 01:00 UTC, publishes posts dated that day - workflow_dispatch: # manual trigger, also re-arms the 60-day inactivity timer - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/configure-pages@v5 - - uses: actions/jekyll-build-pages@v1 - with: - source: ./ - destination: ./_site - - uses: actions/upload-pages-artifact@v3 - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 93fcead..c407f65 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ -_site/ -.jekyll-cache/ -.jekyll-metadata -.bundle/ -vendor/ - -.gem/ -.direnv/ +/target +/classes +/checkouts +profiles.clj +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +/.prepl-port +.hgignore +.hg/ +dist/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index edae596..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,67 +0,0 @@ -# CLAUDE.md - -## Project overview - -sourceless.org — a personal blog built with Jekyll, hosted on GitHub Pages. - -## Development environment - -This repo uses a **Nix flake** for dev tooling. Enter the dev shell with: - -``` -nix develop -``` - -Or automatically via **direnv** (`.envrc` contains `use flake`). The shell provides Ruby 3.3, bundler, and native dependencies. Gems install to `.gem/` in the project root. - -## Common commands - -- **Dev server:** `bundle exec jekyll serve` (serves at localhost:4000) -- **Build:** `bundle exec jekyll build` (outputs to `_site/`) -- **Install deps:** `bundle install` -- **Regenerate social cards:** `python tools/generate_og_images.py` - -## Deployment - -Pushes to `master` trigger a GitHub Actions workflow (`.github/workflows/build.yml`) that builds and deploys to GitHub Pages. - -## Project structure - -- `_posts/` — blog posts in Markdown (named `YYYY-MM-DD-slug.md`) -- `_layouts/` — page templates (`default.html`, `post.html`, `landing.html`, `home.html`, `page.html`) -- `assets/css/main.css` — site styles -- `assets/og/` — generated Open Graph cards, one per post plus `default.png` -- `tools/generate_og_images.py` — renders those cards -- `index.html` — landing page -- `posts.html` — post index -- `_config.yml` — Jekyll config (permalink: `/posts/:title.html`) - -## Linting, formatting, and testing - -There are no linters, formatters, or test suites configured for this project. Validate changes by running `bundle exec jekyll build` and checking the dev server output. - -## Post artwork - -Every post has a Julia set fractal seeded from a SHA-256 of its body text, -rendered by `tools/generate_og_images.py` and committed under `assets/og/`. Four -files per post, since the site has both themes and the index needs something -small: - -- `.png` — 1200x630 dark, used as the `og:image` and the post banner -- `-light.png` — the light-theme banner -- `-thumb.png` / `-thumb-light.png` — 96x96 for the post index - -Banners come from `_includes/post-banner.html`, thumbnails from -`_includes/post-list.html`. Both pass the two variants as CSS custom properties -so `main.css` can swap them per theme; a `` element would not work here, -because the theme toggle sets a `data-theme` attribute rather than relying on -`prefers-color-scheme` alone. - -**After adding a post, or editing the body of an existing one, re-run the -generator and commit the result.** The GitHub Pages build runs Jekyll in safe -mode, so this cannot be a Jekyll plugin. A missing file is a broken image rather -than a build failure, so it will not show up as a failed deploy. - -## Workflow - -After every change: validate with `bundle exec jekyll build`, then commit and push to `master`. diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 75d9835..0000000 --- a/Gemfile +++ /dev/null @@ -1,2 +0,0 @@ -source "https://rubygems.org" -gem "github-pages", group: :jekyll_plugins diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index d91cc8e..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,286 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (8.1.3) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.3.1) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - json - logger (>= 1.4.2) - minitest (>= 5.1) - securerandom (>= 0.3) - tzinfo (~> 2.0, >= 2.0.5) - uri (>= 0.13.1) - addressable (2.8.9) - public_suffix (>= 2.0.2, < 8.0) - base64 (0.3.0) - bigdecimal (4.1.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - colorator (1.1.0) - commonmarker (0.23.12) - concurrent-ruby (1.3.6) - connection_pool (3.0.2) - csv (3.3.5) - dnsruby (1.73.1) - base64 (>= 0.2) - logger (~> 1.6) - simpleidn (~> 0.2.1) - drb (2.2.3) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - ethon (0.18.0) - ffi (>= 1.15.0) - logger - eventmachine (1.2.7) - execjs (2.10.0) - faraday (2.14.1) - faraday-net_http (>= 2.0, < 3.5) - json - logger - faraday-net_http (3.4.2) - net-http (~> 0.5) - ffi (1.17.4-arm64-darwin) - forwardable-extended (2.6.0) - gemoji (4.1.0) - github-pages (232) - github-pages-health-check (= 1.18.2) - jekyll (= 3.10.0) - jekyll-avatar (= 0.8.0) - jekyll-coffeescript (= 1.2.2) - jekyll-commonmark-ghpages (= 0.5.1) - jekyll-default-layout (= 0.1.5) - jekyll-feed (= 0.17.0) - jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.16.1) - jekyll-include-cache (= 0.2.1) - jekyll-mentions (= 1.6.0) - jekyll-optional-front-matter (= 0.3.2) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.16.0) - jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.3) - jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.8.0) - jekyll-sitemap (= 1.4.0) - jekyll-swiss (= 1.0.0) - jekyll-theme-architect (= 0.2.0) - jekyll-theme-cayman (= 0.2.0) - jekyll-theme-dinky (= 0.2.0) - jekyll-theme-hacker (= 0.2.0) - jekyll-theme-leap-day (= 0.2.0) - jekyll-theme-merlot (= 0.2.0) - jekyll-theme-midnight (= 0.2.0) - jekyll-theme-minimal (= 0.2.0) - jekyll-theme-modernist (= 0.2.0) - jekyll-theme-primer (= 0.6.0) - jekyll-theme-slate (= 0.2.0) - jekyll-theme-tactile (= 0.2.0) - jekyll-theme-time-machine (= 0.2.0) - jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.13.0) - kramdown (= 2.4.0) - kramdown-parser-gfm (= 1.1.0) - liquid (= 4.0.4) - mercenary (~> 0.3) - minima (= 2.5.1) - nokogiri (>= 1.16.2, < 2.0) - rouge (= 3.30.0) - terminal-table (~> 1.4) - webrick (~> 1.8) - github-pages-health-check (1.18.2) - addressable (~> 2.3) - dnsruby (~> 1.60) - octokit (>= 4, < 8) - public_suffix (>= 3.0, < 6.0) - typhoeus (~> 1.3) - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) - http_parser.rb (0.8.1) - i18n (1.14.8) - concurrent-ruby (~> 1.0) - jekyll (3.10.0) - addressable (~> 2.4) - colorator (~> 1.0) - csv (~> 3.0) - em-websocket (~> 0.5) - i18n (>= 0.7, < 2) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 2.0) - kramdown (>= 1.17, < 3) - liquid (~> 4.0) - mercenary (~> 0.3.3) - pathutil (~> 0.9) - rouge (>= 1.7, < 4) - safe_yaml (~> 1.0) - webrick (>= 1.0) - jekyll-avatar (0.8.0) - jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.2.2) - coffee-script (~> 2.2) - coffee-script-source (~> 1.12) - jekyll-commonmark (1.4.0) - commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.5.1) - commonmarker (>= 0.23.7, < 1.1.0) - jekyll (>= 3.9, < 4.0) - jekyll-commonmark (~> 1.4.0) - rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.5) - jekyll (>= 3.0, < 5.0) - jekyll-feed (0.17.0) - jekyll (>= 3.7, < 5.0) - jekyll-gist (1.5.0) - octokit (~> 4.2) - jekyll-github-metadata (2.16.1) - jekyll (>= 3.4, < 5.0) - octokit (>= 4, < 7, != 4.4.0) - jekyll-include-cache (0.2.1) - jekyll (>= 3.7, < 5.0) - jekyll-mentions (1.6.0) - html-pipeline (~> 2.3) - jekyll (>= 3.7, < 5.0) - jekyll-optional-front-matter (0.3.2) - jekyll (>= 3.0, < 5.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.3.0) - jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.16.0) - jekyll (>= 3.3, < 5.0) - jekyll-relative-links (0.6.1) - jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.3) - addressable (~> 2.0) - jekyll (>= 3.5, < 5.0) - jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) - rubyzip (>= 1.3.0, < 3.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) - jekyll-seo-tag (2.8.0) - jekyll (>= 3.8, < 5.0) - jekyll-sitemap (1.4.0) - jekyll (>= 3.7, < 5.0) - jekyll-swiss (1.0.0) - jekyll-theme-architect (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-cayman (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-dinky (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-leap-day (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-merlot (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-midnight (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-minimal (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-modernist (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.6.0) - jekyll (> 3.5, < 5.0) - jekyll-github-metadata (~> 2.9) - jekyll-seo-tag (~> 2.0) - jekyll-theme-slate (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-tactile (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-time-machine (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.3) - jekyll (>= 3.3, < 5.0) - jekyll-watch (2.2.1) - listen (~> 3.0) - jemoji (0.13.0) - gemoji (>= 3, < 5) - html-pipeline (~> 2.2) - jekyll (>= 3.0, < 5.0) - json (2.19.3) - kramdown (2.4.0) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - liquid (4.0.4) - listen (3.10.0) - logger - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - logger (1.7.0) - mercenary (0.3.6) - minima (2.5.1) - jekyll (>= 3.5, < 5.0) - jekyll-feed (~> 0.9) - jekyll-seo-tag (~> 2.1) - minitest (6.0.2) - drb (~> 2.0) - prism (~> 1.5) - net-http (0.9.1) - uri (>= 0.11.1) - nokogiri (1.19.2-arm64-darwin) - racc (~> 1.4) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - pathutil (0.16.2) - forwardable-extended (~> 2.6) - prism (1.9.0) - public_suffix (5.1.1) - racc (1.8.1) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rexml (3.4.4) - rouge (3.30.0) - rubyzip (2.4.1) - safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.9.3) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - securerandom (0.4.1) - simpleidn (0.2.3) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - typhoeus (1.6.0) - ethon (>= 0.18.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - unicode-display_width (1.8.0) - uri (1.1.1) - webrick (1.9.2) - -PLATFORMS - arm64-darwin-23 - arm64-darwin-25 - -DEPENDENCIES - github-pages - -BUNDLED WITH - 2.4.22 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2315126..0000000 --- a/LICENSE +++ /dev/null @@ -1,280 +0,0 @@ -Eclipse Public License - v 2.0 - - THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - - a) in the case of the initial Contributor, the initial content - Distributed under this Agreement, and - - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - where such changes and/or additions to the Program originate from - and are Distributed by that particular Contributor. A Contribution - "originates" from a Contributor if it was added to the Program by - such Contributor itself or anyone acting on such Contributor's behalf. - Contributions do not include changes or additions to the Program that - are not Modified Works. - -"Contributor" means any person or entity that Distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which -are necessarily infringed by the use or sale of its Contribution alone -or when combined with the Program. - -"Program" means the Contributions Distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement -or any Secondary License (as applicable), including Contributors. - -"Derivative Works" shall mean any work, whether in Source Code or other -form, that is based on (or derived from) the Program and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. - -"Modified Works" shall mean any work in Source Code or other form that -results from an addition to, deletion from, or modification of the -contents of the Program, including, for purposes of clarity any new file -in Source Code form that contains any contents of the Program. Modified -Works shall not include works that contain only declarations, -interfaces, types, classes, structures, or files of the Program solely -in each case in order to link to, bind by name, or subclass the Program -or Modified Works thereof. - -"Distribute" means the acts of a) distributing or b) making available -in any manner that enables the transfer of a copy. - -"Source Code" means the form of a Program preferred for making -modifications, including but not limited to software source code, -documentation source, and configuration files. - -"Secondary License" means either the GNU General Public License, -Version 2.0, or any later versions of that license, including any -exceptions or additional permissions as identified by the initial -Contributor. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free copyright - license to reproduce, prepare Derivative Works of, publicly display, - publicly perform, Distribute and sublicense the Contribution of such - Contributor, if any, and such Derivative Works. - - b) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free patent - license under Licensed Patents to make, use, sell, offer to sell, - import and otherwise transfer the Contribution of such Contributor, - if any, in Source Code or other form. This patent license shall - apply to the combination of the Contribution and the Program if, at - the time the Contribution is added by the Contributor, such addition - of the Contribution causes such combination to be covered by the - Licensed Patents. The patent license shall not apply to any other - combinations which include the Contribution. No hardware per se is - licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the - patent or other intellectual property rights of any other entity. - Each Contributor disclaims any liability to Recipient for claims - brought by any other entity based on infringement of intellectual - property rights or otherwise. As a condition to exercising the - rights and licenses granted hereunder, each Recipient hereby - assumes sole responsibility to secure any other intellectual - property rights needed, if any. For example, if a third party - patent license is required to allow Recipient to Distribute the - Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has - sufficient copyright rights in its Contribution, if any, to grant - the copyright license set forth in this Agreement. - - e) Notwithstanding the terms of any Secondary License, no - Contributor makes additional grants to any Recipient (other than - those set forth in this Agreement) as a result of such Recipient's - receipt of the Program under the terms of a Secondary License - (if permitted under the terms of Section 3). - -3. REQUIREMENTS - -3.1 If a Contributor Distributes the Program in any form, then: - - a) the Program must also be made available as Source Code, in - accordance with section 3.2, and the Contributor must accompany - the Program with a statement that the Source Code for the Program - is available under this Agreement, and informs Recipients how to - obtain it in a reasonable manner on or through a medium customarily - used for software exchange; and - - b) the Contributor may Distribute the Program under a license - different than this Agreement, provided that such license: - i) effectively disclaims on behalf of all other Contributors all - warranties and conditions, express and implied, including - warranties or conditions of title and non-infringement, and - implied warranties or conditions of merchantability and fitness - for a particular purpose; - - ii) effectively excludes on behalf of all other Contributors all - liability for damages, including direct, indirect, special, - incidental and consequential damages, such as lost profits; - - iii) does not attempt to limit or alter the recipients' rights - in the Source Code under section 3.2; and - - iv) requires any subsequent distribution of the Program by any - party to be under a license that satisfies the requirements - of this section 3. - -3.2 When the Program is Distributed as Source Code: - - a) it must be made available under this Agreement, or if the - Program (i) is combined with other material in a separate file or - files made available under a Secondary License, and (ii) the initial - Contributor attached to the Source Code the notice described in - Exhibit A of this Agreement, then the Program may be made available - under the terms of such Secondary Licenses, and - - b) a copy of this Agreement must be included with each copy of - the Program. - -3.3 Contributors may not remove or alter any copyright, patent, -trademark, attribution notices, disclaimers of warranty, or limitations -of liability ("notices") contained within the Program from any copy of -the Program which they Distribute, provided that Contributors may add -their own appropriate notices. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities -with respect to end users, business partners and the like. While this -license is intended to facilitate the commercial use of the Program, -the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential -liability for other Contributors. Therefore, if a Contributor includes -the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every -other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits -and other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program -in a commercial product offering. The obligations in this section do not -apply to any claims or Losses relating to any actual or alleged -intellectual property infringement. In order to qualify, an Indemnified -Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any -related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial -product offering, Product X. That Contributor is then a Commercial -Contributor. If that Commercial Contributor then makes performance -claims, or offers warranties related to Product X, those performance -claims and warranties are such Commercial Contributor's responsibility -alone. Under this section, the Commercial Contributor would have to -defend claims against the other Contributors related to those performance -claims and warranties, and if a court requires any other Contributor to -pay any damages as a result, the Commercial Contributor must pay -those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" -BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF -TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR -PURPOSE. Each Recipient is solely responsible for determining the -appropriateness of using and distributing the Program and assumes all -risks associated with its exercise of rights under this Agreement, -including but not limited to the risks and costs of program errors, -compliance with applicable laws, damage to or loss of data, programs -or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS -SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of -the remainder of the terms of this Agreement, and without further -action by the parties hereto, such provision shall be reformed to the -minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity -(including a cross-claim or counterclaim in a lawsuit) alleging that the -Program itself (excluding combinations of the Program with other software -or hardware) infringes such Recipient's patent(s), then such Recipient's -rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it -fails to comply with any of the material terms or conditions of this -Agreement and does not cure such failure in a reasonable period of -time after becoming aware of such noncompliance. If all Recipient's -rights under this Agreement terminate, Recipient agrees to cease use -and distribution of the Program as soon as reasonably practicable. -However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, -but in order to avoid inconsistency the Agreement is copyrighted and -may only be modified in the following manner. The Agreement Steward -reserves the right to publish new versions (including revisions) of -this Agreement from time to time. No one other than the Agreement -Steward has the right to modify this Agreement. The Eclipse Foundation -is the initial Agreement Steward. The Eclipse Foundation may assign the -responsibility to serve as the Agreement Steward to a suitable separate -entity. Each new version of the Agreement will be given a distinguishing -version number. The Program (including Contributions) may always be -Distributed subject to the version of the Agreement under which it was -received. In addition, after a new version of the Agreement is published, -Contributor may elect to Distribute the Program (including its -Contributions) under the new version. - -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient -receives no rights or licenses to the intellectual property of any -Contributor under this Agreement, whether expressly, by implication, -estoppel or otherwise. All rights in the Program not expressly granted -under this Agreement are reserved. Nothing in this Agreement is intended -to be enforceable by any entity that is not a Contributor or Recipient. -No third-party beneficiary rights are created under this Agreement. - -Exhibit A - Form of Secondary Licenses Notice - -"This Source Code may also be made available under the following -Secondary Licenses when the conditions for such availability set forth -in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public -License as published by the Free Software Foundation, either version 2 -of the License, or (at your option) any later version, with the GNU -Classpath Exception which is available at -https://www.gnu.org/software/classpath/license.html." - - Simply including a copy of this Agreement, including this Exhibit A - is not sufficient to license the Source Code under Secondary Licenses. - - If it is not possible or desirable to put the notice in a particular - file, then You may include the notice in a location (such as a LICENSE - file in a relevant directory) where a recipient would be likely to - look for such a notice. - - You may add additional accurate notices of copyright ownership. diff --git a/README.md b/README.md deleted file mode 100644 index 369718d..0000000 --- a/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# sourceless.org - -Personal blog built with Jekyll, hosted on GitHub Pages. - -## Local development - -```bash -bundle install -bundle exec jekyll serve -``` - -Then visit http://localhost:4000 - -## License - -* Code - MIT Licence -* Content, pages, posts, etc. - Proprietary, Copyright Laurence Pakenham-Smith, reproduction only with permission of the author. diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 7620e95..0000000 --- a/_config.yml +++ /dev/null @@ -1,15 +0,0 @@ -title: Sourceless -url: "https://sourceless.org" -baseurl: "" -permalink: /posts/:title.html -markdown: kramdown -future: false -timezone: Etc/UTC - -plugins: - - jekyll-feed - -exclude: - - Gemfile - - Gemfile.lock - - README.md diff --git a/_includes/post-banner.html b/_includes/post-banner.html deleted file mode 100644 index 401f72d..0000000 --- a/_includes/post-banner.html +++ /dev/null @@ -1,8 +0,0 @@ -{% comment %} -Decorative fractal banner for a post. Both theme variants are passed as custom -properties so the CSS can swap them for the light theme, which a -element could not do: the site's theme toggle is an explicit data-theme -attribute, not just prefers-color-scheme. -{% endcomment %} -{% assign p = include.post %} - diff --git a/_includes/post-list.html b/_includes/post-list.html deleted file mode 100644 index a336e6d..0000000 --- a/_includes/post-list.html +++ /dev/null @@ -1,9 +0,0 @@ -
    -{% for post in site.posts %} -
  • - - -{{ post.title }} -
  • -{% endfor %} -
diff --git a/_layouts/default.html b/_layouts/default.html deleted file mode 100644 index 3bcee7c..0000000 --- a/_layouts/default.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - {% if page.title %}Sourceless - {{ page.title }}{% else %}Sourceless{% endif %} - - - - - - - - {% if page.date and page.slug %}{% assign og_image = '/assets/og/' | append: page.slug | append: '.png' %}{% else %}{% assign og_image = '/assets/og/default.png' %}{% endif %} - - - - - - - - - - - - - - - - - - - - -
- - - - -
- {% if page.date and page.slug %}{% include post-banner.html post=page %}{% endif %} - {% if page.title %}

{{ page.title }}

{% endif %} - {{ content }} -
-
- - - - diff --git a/_layouts/home.html b/_layouts/home.html deleted file mode 100644 index 47ea251..0000000 --- a/_layouts/home.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- -{% include post-list.html %} diff --git a/_layouts/landing.html b/_layouts/landing.html deleted file mode 100644 index f745c5f..0000000 --- a/_layouts/landing.html +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: default ---- -{% assign latest = site.posts.first %} - -{% include post-banner.html post=latest %} -

{{ latest.title }}

-{{ latest.content }} - - -
-

all posts

-{% include post-list.html %} diff --git a/_layouts/page.html b/_layouts/page.html deleted file mode 100644 index 73dae17..0000000 --- a/_layouts/page.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: default ---- -{{ content }} diff --git a/_layouts/post.html b/_layouts/post.html deleted file mode 100644 index e460c98..0000000 --- a/_layouts/post.html +++ /dev/null @@ -1,20 +0,0 @@ ---- -layout: default ---- - -{{ content }} - - diff --git a/_posts/2021-01-17-how-i-built-this-site.md b/_posts/2021-01-17-how-i-built-this-site.md deleted file mode 100644 index 35127fa..0000000 --- a/_posts/2021-01-17-how-i-built-this-site.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -layout: post -title: "Getting github pages to work with my custom site generator" -date: 2021-01-17 -tags: [tutorial, guide, how] ---- - -So, I built a [crappy site generator](https://github.com/Sourceless/sourceless.github.io). -It's pretty bad, which is the result of some very targeted design decisions: -* I didn't want to spend a huge amount of effort building it. This is for fun, not work. You're not going to find any unit tests or any significant refactoring effort. -* I want to generate a fast site that I can host for free (sans domain costs). -* I want it to be easy to author new posts in my tool of choice (emacs). -* Deploying the website should be a `git push`. - -Feel free to look at the code (linked above) if you want, but it's really the least interesting part of this post. At a quick summary, here's what it can do: -* Fixed index page that lists posts and pages -* Pages and posts generated from markdown -* Page/post metadata stored as EDN (Jekyll-style, but EDN isntead of YAML) - -### Getting Github actions to deploy from my own generator -I hasten to note this is by no means revolutionary or new. But here's how I did it. - -[Github Pages comes with Jekyll site generation built in](https://docs.github.com/en/github/working-with-github-pages/setting-up-a-github-pages-site-with-jekyll). -This is nice, unless you want to use your own generator. - -Now of course, you _could_ generate it locally and push it, but really, where's the fun in that? So, let's get github to do all the work. - -For reference, [here is the action that builds the site](https://github.com/Sourceless/sourceless.github.io/blob/master/.github/workflows/build.yml). -At a high level, it: -1. Generates the site from the files in the repo -2. Resets to the `www` branch, [which contains the built output](https://github.com/Sourceless/sourceless.github.io/blob/master/.github/workflows/build.yml) served by GitHub pages (you can change the branch it watches in settings) -3. Checks out and adds files -4. Makes a new commit postfixed by the actions run number -5. Pushes to the `www` branch. - -(Note to readers: I haven't gone through this with a fine-tooth comb. It's quite possible that the reset is entirely unnecessary.) - -The fantastic [GitHub Push Action by ad-m](https://github.com/ad-m/github-push-action) was instrumental in getting this to work so easily. - -### The end result - -So what am I left with? -* A small, lean, html-only site -* On free hosting -* Using any site generator I want -* Pushing a new version to master auto-generates a new site - -Seems like it fits the bill! I'm happy with the output, and maybe I _will_ spend some time improving the code to something I would be proud to show to people, but the fact remains: *working code that solves the problem is better than neat code that doesn't work _yet_*. - -# 👋 - -[Follow the discussion on Hacker News](https://news.ycombinator.com/item?id=25813217) diff --git a/_posts/2021-03-19-the-documentation-triangle.md b/_posts/2021-03-19-the-documentation-triangle.md deleted file mode 100644 index db1f126..0000000 --- a/_posts/2021-03-19-the-documentation-triangle.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -layout: post -title: "The Documentation Triangle" -date: 2021-03-19 -tags: [documentation, self-documenting, documentation-as-code, opinion, triangle] ---- - -At some point in your coding career, you've probably heard something like: - -> "My code is self documenting" - -or - -> "Code IS documentation" - -These statements can be hard to argue against, especially if they come from -someone who is more experienced than you. A lot of the trouble is that they -*are not wrong* -- well, they're not *completely* wrong. - -### The goal of documentation -Why do we document things? If the code was enough, we would never need -tutorials, API documentation, or anything of the sort, right? What user -need is being fulfilled by having more than just code to communicate what our -programs and services do? - -It sounds obvious, but **documentation exists so that code can be easily used**. - -This is incredibly easy to forget when the program is all in your head and you -have an intuitive understanding -- but to fresh eyes, is your code really all -that easy to approach? - -### What, Why, How: The Documentation Triangle -``` - What - /\ - / \ - / \ - / \ - / \ - /__________\ - Why How -``` -Here's a rule of thumb I've seen repeated in a few places regarding how to -ensure that your documentation is up to scratch. - -Why a triangle? Well, triangles are a strong shape, and any missing side renders -it entirely structurally unsound - just as missing out vital documentation -can make your code as good as unusable to others. - -#### What (code) -This is generally what people are going for when they say "my code is self -documenting". Clear, concise, and well-written code is a form of documentation -in itself. - -It is the most honest account of what actually happens when you run the code, -what the most important data structures are, and what interfaces you'll be -able to interact with. Some languages are better than this for others, and -supplementary information such as type signatures can be incredibly helpful -in understanding some code that's new to you. -However, there are some things this alone leaves out... - -#### Why (comments) -Every program has a history - whether in the very mundane sense of having a -history in version control, or a long and detailed history of changes it has -undergone to meet various challenges. - -It's this latter case that is the most crucial to note **why** certain pieces -of code exist in their current state. - -Comments are most often the vehicle for these sort of margin notes. Noting -that this thing was done in this way for optimization reasons, or another -thing was a dirty hack leaves signposts for the next maintainer to know -what compromises have been made in building this software. - -Maintaining (legacy) code without any comments is less like being a mechanic, -and much more like being an archaeologist. - -#### How (context) -The last side of the triangle is often the most neglected. It might live in -docstrings, or in confluence, be automatically generated, or be in a carefully -crafted README, but it's all information on the context in which the code -will be executed. - -**No program exists in a vacuum**. There is always some environment, some -organisation, some process that it lives in or serves, and taken out of said -context, it is entirely useless. - -Failing to recognise this is the number one leading cause of new users and -junior devs being unable to do 'this simple task' (don't quote me on this, -I have no data). - -There's a great litmus test for this. Take the new process or pipeline or -whatever that you've implemented (be it setting up a repo, running a job, etc.) -and find yourself someone that's technically competent but unfamiliar with -what you're working on. Plonk them in front of it and **shut the fuck up**. -Watch them struggle through it and only help them if they are genuinely, truly -stumped. This will very quickly tell you where you need to spend effort on -documentation. - -Heck - make it part of your code review. - -[Discussion of this article on Hacker News](https://news.ycombinator.com/item?id=31819926) diff --git a/_posts/2022-06-19-relearning-to-learn.md b/_posts/2022-06-19-relearning-to-learn.md deleted file mode 100644 index 76d8c4d..0000000 --- a/_posts/2022-06-19-relearning-to-learn.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -layout: post -title: "Relearning to Learn" -date: 2022-06-19 -tags: [learning, learning-to-learn] ---- - -Many years ago, I took a test. It was the first step of our -"Learning to Learn" class. After answering a series of multiple -choice question, the form issued forth a proclomation: - -> YOU ARE A VISUAL LEARNER. - -Over the course of my years at school I would learn this piece -of information to be on the whole useless and entirely -innefectual. - -To cut a long story short, school and university didn't go -particularly well for me. - -### Learning by accident - -It's somewhat embarassing to admit, but I don't think I really -learned how to teach myself things effectively until at least -the age of 26. - -When I look back, I realise much of my learning -was a result of accidental chance; for example, I learned to -program mostly by virtue of complete immersion and stumbling -upon 'the right things'. I just followed my nose (and to be fair -it worked out). - -But now I'm an adult with a job and a family, and my time is -far more precious. If I want to learn something new, I can't -count on stumbling on the right approach anymore. Simply -creating the conditions in which learning is possible is not -sufficient, since I lack the time that approach requires. - -### Enter Mentor - -How I learned to learn is, surprisingly, not at all related to -my area of study or profession (software), but a hobby. - -I met my whistle teacher Becky in 2020 on a video call. It -was near the start of the pandemic, and I had some extra time -on my hands. Becky is somewhat unlike other teachers I've had, -in that she could mechanically break down and explain music to -me in a way that allowed me not just to play it, but understand -what I was playing. - -The whistle is a small, cheap, end-blown flute -- somewhat like a -budget recorder. It has six tone holes that you cover with three -fingers of either hand, and a mouthpiece that you blow in to. -Out of this deceptively simple instrument, you can get two full -octaves of range in two major keys and all the keys that come -along with that. That is to say, it's small, but surprisingly -versatile. - -Irish traditional music, along with other traditional music -of the British isles, is generally learned and played by ear, -though sheet music is available. What sets this folk music -apart from most classical and contemporary music is that the -sheet music is essentially a suggestion. Using your skill, -wits, and guile, you have to take a couple of parts of -a repetitive tune and make it *interesting*. - -This is where Becky comes in, with two really solid pieces of advice: -one about how to practice, and one about how to learn. - -### Mindful Practice - -The first of the things Becky taught me was how to practice; how to -gain the mechanical skill required to execute what I knew, and to -transform my knowledge into understanding. - -> "Know what you're going to practice before you pick up the whistle" - -In retrospect, startlingly obvious. Noodling around randomly does -very little but ingrain what you've already learned; -**if you want to learn something new then you have to have a goal**. -But, as with many things in life, having a plan (even if you have no -idea what you're getting in to) is half the battle. - -> "Five minutes every day is better than an hour once per week" - -This subtly implies something else; you should make your practice -not just habit and routine, but low-friction and easy to access. -You will do much better chipping away at small, easy problems, -than trying to tackle a hard problem you are not ready for (and -all hard problems are just *n* easy problems in a trenchcoat anyway). - -> "Do it slow until you can do it right" - -This one has a very "slow is smooth, smooth is fast" kind of vibe, -and it's right. Doing something slowly and accurately is **hard**. -In a programming context, it's very easy to whack together some code -that delivers a feature, but the end result is of limited use -if it's buggy, doesn't actually deliver the desired feature, or is -riddled with security or structural issues. - -### Learning to Learn - -The second thing Becky taught me was how to learn. Specifically, -how to learn an instrument, or a tune -- the approach is broadly -the same. - -It's a model, and as my colleague Craig likes to say; all models -are wrong, but some are useful. Hopefully this is one of the latter. - -> "First, learn the notes" - -You have to be able to play a tune through in its entirety. Obvious, -right? But there's a lot here -- some tunes are difficult. Some tunes -don't have a written form, and you'll have to learn them by ear. Worse, -some tunes have very poorly transcribed sheet music that will lead -you astray! Not all learning material is created equal. - -Beyond that, you will also have to contend with your own (lack of) -mechanical skill. You will likely get stumped by a strange combination -of notes. Perhaps you're not accustomed to switching octaves so quickly, -or you accidentally bang out the wrong note because of some pattern -you've learned in a previous tune. - -> "Second, make it more interesting" - -Boy, isn't that '*interesting*' a really load-bearing word? In this -context, it means adding ornamentation, variation, dynamics, and -a plethora of other tools in the folk musician's toolbox. - -Making the piece flow better - the folk musician's version of 'clean code' - -is key to turning a roughly shaped tune into refined birdsong. - -> "Third, play the tune" - -Taking all you have learned, play the **tune**. Not the notes. -Not the parts. The whole thing. Feel the rhythm, lean in to the swing, -and add to it with your performance. - -This is not just the culmination of learning how to play the tune - it's -the start of learning the meta-skill of picking up and playing the -tunes you come across. - -In code terms; release it. You must make mistakes to improve, and -**more often than not you will be startled how others see something that is so mundane to you as so magical**. - -### Some observations - -This is not a new topic, or a new field. People much smarter and -more experienced than me have studied this for a long time, but I share it -in hopes that for someone out there, the same realisations I had will click. - -If you want to read more on this topic, take a look at: -* [Shuhari](https://en.wikipedia.org/wiki/Shuhari) -* [The Dreyfus Model of Skill Acquisition](https://en.wikipedia.org/wiki/Dreyfus_model_of_skill_acquisition) -* [The four stages of competence](https://en.wikipedia.org/wiki/Four_stages_of_competence) - -And if I've somehow convinced you to listen to whistle music: -* [Flow, In The Year of Wu Wei - Brian Finnegan](https://www.youtube.com/watch?v=-6TS7eiKZO8) -* [Slide from Grace/The Queen of Rangoon - Kan](https://www.youtube.com/watch?v=frH7A7a00NY) -* [Farewall to Uist/The Lochaber Badger/RIP the Calico - Fred Morrison, Michael McGoldrick and Donal Lunny](https://www.youtube.com/watch?v=qVztCC37wlc) - -Thanks readers, and thanks Becky! - -[Discussion of this article on Hacker News](https://news.ycombinator.com/item?id=31797431) diff --git a/_posts/2022-06-22-perfect-isnt-good-enough.md b/_posts/2022-06-22-perfect-isnt-good-enough.md deleted file mode 100644 index ae5c9ab..0000000 --- a/_posts/2022-06-22-perfect-isnt-good-enough.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -layout: post -title: "Perfect isn't Good Enough" -date: 2022-06-22 -tags: [perfect, perfection, good-enough, laziness] ---- - -> Perfect is the enemy of the good - -Or so goes the old adage. For developers, perfection is a mightier enemy -than users, project managers, and even one's own lack of skill. - -### In pursuit of greatness - -Many junior (and senior!) engineers strive to write the *best* code. -Clean code. Well-tested. Fully covered. Abstractions on abstractions -making beautiful, elegant interfaces. - -Beautiful, elegant, fragile interfaces, which shatter upon exposure to -the expectations of an ever-changing world. - -No (useful) system exists in a vacuum. Users' needs change over time, -as does the substrate on which software relies. **Nothing is permanent.** - -### The worst thing I've ever built - -Very early in my career, I built a system to click through HTML forms, -submit some information and return some information. There weren't APIs -for this kind of thing, so traversing through legacy websites was the -only way. - -Even with that short description, you've probably already thought how you -would do it; and most of you probably have a better plan than I, a junior -developer at the time, did. - -So I birthed a new **language** in to the world. A powerful tool to be sure. -By issuing a list of simple commands, a headless browser would go off and -perform the commands, fill the forms, and return the information we needed. - -Great! Right? - -But then there were different forms that were differently bad. Commands had -to change and gain options. Commands were constantly bug-fixed. The code -that ran was stored in a SQL database, so changing anything required a full -run of the system and debugging was waiting for logs to populate. Every -new integration made the system heavier and bigger and buggier. - -If you're cringing -- that's correct. If you're not -- come back in a couple -years (after you finish reading this). - - -### The making of burdens - -Why was that system so bad? What was it that I did that made it so -inextensible and brittle? - -The first reason is **greed**. I wanted to make something *cool*. I sought -a challenge where non existed! The problem I was seeking to solve was -primarily my own creation. - -The second reason is **ignorance**. I naively believed that the original -purpose of the system is all it would ever do. - -The third, and perhaps worst reason, is **hubris**. Perfection for my own -selfish reasons -- not for the user, but the maker. - -I didn't learn how to control each of these right away, and still combat -each of these in my working and hobby lives. In moderation, these things -are a positive force for learning, but when they take over -- beware. - -### A successful mistake - -My most recent relapse was creating an internal tool for a team to register -public keys into a repository. By most measures I did an extremely good -job of it -- it was reliable, did only what it needed to, and was easy -for me to extend. - -> 'easy for me to extend' - -Ah. Now there was the problem. This tool I created became a victim of its -own success and, due to other commitments, changes needed to be made by -other members of my team. - -Which, for a little web frontend, would have been fine. Had I been on a -team used to writing purely functional code, and had I not written it in -Elm (no shade to Elm, it's a fantastic language, but it was the wrong -choice in this context). - -I had failed to keep in mind the future life of this thing I had created. - -Code, much like art, ceases to belong to its author once it is released -in to the world. - -### Valueless perfection and glorious mediocrity - -Each of these issues of greed, ignorance, and hubris can be avoided through -a judicious application of mediocrity. - -The key to this is recognising that the time you spend trying to create -something perfect is not only extremely expensive (you could release something -much earlier), but also holds little value. A program is worthless until it -is used, and will not grow in value if you make it too hard to change. - -This doesn't mean you shouldn't experiment and sharpen your skills - just -make sure you are aiming at a target, and not your own or someone else's -foot. - -### Doing the dumbest possible thing - -It's here, then, that I urge you to **do the dumbest possible thing**. - -That may be somewhat hyperbolic, but here's what I mean by it: - -* Your code probably isn't written to serve you, so before anything else - make sure it serves the user's needs. You have plenty of time to play - on your own time. - -* Don't write more than you need to. Doing anything more than is sufficient - is a waste of time. One-off script? Don't write tests for it. Wrote the - same thing twice? Great, leave it. You don't know if every use case is going - to look like that (aka [Avoid Hasty Abstractions](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)). - -* Get it out the door. The code you write has no value until it is used. - Your assumptions can only be validated if you have something to test against! - This doesn't mean you should ship completely broken stuff - what - it does mean, however, is that you need a full solution to the problem, - even if it's a crappy solution, so that you can make sure you're building - the right thing. - -* Optimise nothing. You don't know what's going to be the slowest part yet, - so don't waste effort solving a problem you don't have. - -In other words; make it **Just Good Enough** to do what you need. diff --git a/_posts/2022-07-07-the-continuous-delivery-test.md b/_posts/2022-07-07-the-continuous-delivery-test.md deleted file mode 100644 index 28fd146..0000000 --- a/_posts/2022-07-07-the-continuous-delivery-test.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -layout: post -title: "The Continuous Delivery Test" -date: 2022-07-07 -tags: [joel-test, devops, continuous-delivery] ---- - -In 2000, Joel Spolsky published a piece on what he believed were the -key factors that separated high-performing teams from the rest, in a post titled -[The Joel Test: 12 Steps to Better Code](https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-steps-to-better-code/). -It contains many insights that are still useful to this day; however, the world, -software, and the teams that make it have moved on in the nearly 22 years since -that post. - -In 2015, John Kodumal of LaunchDarkly published an updated [Joel Test for Continuous Delivery](https://circleci.com/blog/joel-test-for-continuous-delivery/). -While being far more up-to-date, it's still slipped a little in relevance. - -If this post were to have an alternate title, it'd be 'Front-load Your Risks'. -All of the advice below seeks to move risks away from the act of deployment -and move them to earlier steps of the development process. - -So, here's my best shot at a CI/CD Joel Test for the 2020s. - - -### 1. Do you use a distributed version control system? - -Git, GitHub, and the ecosystems around them have eaten the software world, -and for good reason. Beyond the usual advantages - branching, merging, -and easy rollbacks, many of these hosted git servers now come with a whole slew -of other productivity tooling. - -Especially notable are those that move your CI closer to your source code, -such as GitHub Actions, and GitLab CI/CD. - - -### 2. Do you practice [trunk-based development](https://trunkbaseddevelopment.com/)? - -Trunk-based development rolls up a bunch of good practices under one system. - -It forces you to have one good `main` branch that is always deployable (and by implication, -always able to roll back), and encourages merging small features one at a time. - -Having an always-deployable `main` means that your deployment process becomes -a lot more boring (which is great!), and is often indicative of a process -that supports developers, rather than requiring long hours spent resolving -merge conflicts and preparing release notes. - - -### 3. Do you merge little and often? - -It's been known for some time now that large pull requests tend to get -cursory reviews at best. On top of this, squirrelling away to work on a single -feature for a long time means you are not spending adequate time making sure -that you are building the right thing! - -This also means using small, ticket-scoped feature branches, where the changes -have a very tight scope. - - -### 4. Do two people read code before it is merged? - -You'll notice I was very careful to avoid mentioning how this should take place. -For some, pull requests are the right tool, but pairing and mob programming are -also sufficient. - -Nobody should be solely responsible for any code. - - -### 5. Do you require changes to pass checks before they can be merged? - -One of the leading causes of outages is deployments. You should do your best to -make sure that broken code never gets deployed in the first place. - - -### 6. Can you test your changes in a production-like environment before you deploy them? - -There are many ways to go about this. You could spin up a small clone of your -production env, run the tests, and then kill it. Where this is not possible, -you can run tests against mirrored traffic or even run a canary. - - -### 7. Do you deploy to production as soon as `main` is updated? - -Your deployments should be automated and happen right upon merge. This removes a burden -from the team delivering a product and allows them to focus on features and reliability -rather than long-winded release ceremonies. It can be hard to get here if you're currently releasing -once a day, week, etc. -- a mature approach to managing the risks of deployment -is required, as well as battle-tested deployment processes and tools. - -The earlier you start doing this, the easier it will be. - - -### 8. Does your deploy process self-heal? - -When a deployment fails, can you roll back automatically? In addition to this, you should -be able to roll back manually where necessary (though it's generally best to fail forwards!) - -This also necessitates the use of blue-green deploys, canarying, healthchecks, and myriad -other strategies to mitigate outage risk. It's very powerful to be able to turn a -possible outage into a mere degradation. - - -### 9. Does your Infrastructure as Code live alongside the service it hosts? - -It's a good idea to colocate any IaC with the software it's running rather than keeping -the two in separate repositories. This dodges any dependency management between the two, -and in many cases will save you a pipeline or two. - -It's an antipattern (in the author's opinion) to have all infrastructure code completely -separate from the application it supports. Common or shared infrastructure could be argued to -have its own place, but a product should have all its pieces as close together as possible. - -[Monorepos](https://semaphoreci.com/blog/what-is-monorepo) are an extreme example of this philosophy. - - -### 10. Do you use feature flags? - -Releases are a terrible mechanism for launching new features. You want to be able to -run that shiny new feature in production as soon as possible, and you can't do that -if you conflate a feature's launch with its release. This also means that you can -preview and refine changes with flagship customers before rolling them out more widely. - - -### 11. Do you include ticket IDs in your commits or branches? - -You need to be able to tie every commit back to the ticket that spawned it. It might not -be you debugging a change that you merged, so it needs to be really easy to go back -and find out *why* something was changed in order to make the right decision to -fix it. - - -### 12. Can you still deploy from your own machine? - -A key factor in developing software and infrastructure quickly is tight feedback -loops. While it's probably not a great idea to be deploying directly to production -from your own machine, having the ability to do so might be useful in a pinch. - -You should be able to deploy to a dev environment (shared or ephemeral) such that -you can easily validate your work in situ. - - -### 13. Can you show what will happen when a branch is merged? - -Most IaC tools offer the ability to diff the changes they want to make. It's -vitally important that when you go to merge some changes in, you know what the -changes are, why you're making them (see point 11), and the effect the changes -will have on the product or system. - -This could be a `cdk diff`, or a visual diff if you're making a change to a -frontend. Seldom is the code itself the interesting and crucial part of -why you're making a change. The effect of your changeset should be evident -not only to reviewers but also to bleary-eyed SREs at 4am. - - ---- - - -*Thanks to [Josh Finch](https://github.com/openfinch) for his valuable feedback on this post.* diff --git a/_posts/2022-07-29-put-ticket-ids-in-your-commit-messages.md b/_posts/2022-07-29-put-ticket-ids-in-your-commit-messages.md deleted file mode 100644 index 516b931..0000000 --- a/_posts/2022-07-29-put-ticket-ids-in-your-commit-messages.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -layout: post -title: "Put Ticket IDs in your Commit Messages" -date: 2022-07-29 -tags: [compliance, devops, continuous-delivery] ---- - -In my previous post [The Continuous Delivery Test](https://sourceless.org/posts/the-continuous-delivery-test.html) -I included a point: - -> Do you include ticket IDs in your commits or branches? - -[This sparked quite a lot of discussion on hacker news](https://news.ycombinator.com/item?id=32045651). -I think it'd be useful to highlight some of my reasoning for including this -point, and highlight some of the advice given by some of the commenters. - -## Why you should do it -### Link work back to the ticket that spawned it -If you're in anywhere but the smallest companies, you probably have some sort of -compliance requirements that require you to be able to link work done back to -the planning that caused it to happen. - -The reason that compliance frameworks want you to do this is mainly so there is -an unbroken chain of culpability that can be used to identify the root cause -of a potential issue. - -But it's also just good documentation. Do you really want to play archeologist -every time you need to figure out why a certain commit was made, or why a -certain line of code is a bit weird? - -As one lovely HN commenter puts it: -> 'I can't tell you how many times I've dug into the commit history to find where -> a change happened, then wanted to know the full context, and been glad I could -> just pull up the years-old ticket directly and see a detailed description of -> intent and requirements, discussions in the comments, mock-ups, etc etc' - -### Track the content of releases -In some environments you might need to prepare release notes or a changelog. - -This is, of course, much easier if you can derive the list of tickets from the -delta of what's going in to production. - -### Measure how well your team is delivering -In [Accelerate](https://www.oreilly.com/library/view/accelerate/9781457191435/), -Forsgren, Humble, and Kim pick out four metrics that seperate high performing -teams from the rest - and they all have to do with deployment. - -The first of those metrics is **Lead Time**, which measures the amount of time -it takes from starting a ticket (or the first commit made, depending how you -want to measure) to when it's eventually released. - -Having some metadata available makes this much easier to do! - -### It's not that hard -Seriously, it's just habit. It might sound annoying or bureaucratic to you if -you've never done it before, but after a couple weeks it'll probably be an -unconscious practice that you'll never think about again. - -And if typing <10 extra characters at the start of a commit message is really -cramping your style, you can always write a `prepare-commit-msg` git hook. - -## Why you might not be doing it -### Your company is small -If you're starting to grow and compliance is on the horizon, it's much easier -to start now than when the deadline looms. - -### It's a personal project -Yep. Why would you? It's needless overhead. - -### Merging/branching strategy -I'm probably not going to convince you otherwise, but I think long-lived feature -branches **suck**. They require complex merge patterns, bundle risk, and often -require a bureaucratic ritual to merge in to a production branch. - -[Trunk-based development](https://trunkbaseddevelopment.com/) all the way, -and if you can have a branch per ticket *with the ticket ID in the branch name*, -even better. - -### I need to make an ad-hoc change -This usually falls in to one of two categories. - -The first is "I want to make a change off the books". Usually this is a culture -problem; you should feel empowered to create small tickets as you see fit to -fix small issues. It might also be that your team doesn't create time for care -and feeding work. - -The second is "I don't know where this change belongs". Can you roll it in to -another ticket you're already working on, e.g. one where you're already touching -the code in question? Or maybe it's enough work that it should be its own item? diff --git a/_posts/2022-08-16-make-your-own-free-password-manager.md b/_posts/2022-08-16-make-your-own-free-password-manager.md deleted file mode 100644 index 292ac3a..0000000 --- a/_posts/2022-08-16-make-your-own-free-password-manager.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -layout: post -title: "Make your own (free) password manager" -date: 2022-08-16 -tags: [passwords, security, selfhost] ---- - -> Before I say anything else, it should be noted that this solution requires -> a little bit of background knowledge to set up and use. At the very least -> you'll need to be comfortable on the command line in linux/osx, especially -> with installing packages, git, ssh, and a little gpg. Don't worry if you don't -> know all of that -- a little familiarity should be enough. - -### Why I rolled my own - -You might be wondering what the point is of setting up your own password -manager is when so many good, paid alternatives exist -- and you'd be right! - -There is however, some advantage in knowing exactly how your passwords are -encrypted and being able to integrate your password store with **EVERYTHING**. - -And it's run at no cost to me. - -Standard warnings apply; the UX will not be as good. If you do not back up -your GPG keys, you could lose access to your passwords forever. The security -of your password manager is only as good as the security of your system. - -### Features and composition - -Our password manager will have the following features: -* Encrypted at rest -* Command line, web browser, and mobile clientship -* Per-device revocation -* Cross-device sync - -We'll make it with these components: -* [password-store](https://www.passwordstore.org/) -* gpg -* git - - -**Make sure you have all the above installed before continuing!** - -### Steps -#### 1. Generate a gpg key -There's lots of guides on doing this, but here's an example (from 2022, if -you're reading this in the future, google it first because best practice has -probably changed!). - -``` -gpg --default-new-key-algo "ed25519/cert,sign+cv25519/encr" \ - --quick-generate-key "my@email.com" -``` - -This will generate a new multi-purpose ed25519 key with no expiration date. - -It's up to you if you want to use this to generate subkeys to use for the -password store. I would, however, recommend that you make one key per device -sou you can revoke access if a device is lost or stolen! - -If you've already got a gpg key and want to use it -- use that. - -**And before anything else - back up your key! On a storage drive, paper, -obelisk, yubikey - and in more than one place!** - -#### 2. Init the password store -``` -pass init -``` - -That's it! If you want to use multiple keys (e.g. one per device), just add any -additional IDs after the first. - -#### 3. Add your first password -``` -pass generate my-first-password 100 -``` -This will generate a random 100-character password for you named `my-first-password`. - -It'll print on the terminal in the first instance, and you probably don't want -that, so you can pass the `-c` flag to send the password directly to your clipboard -instead. - -``` -pass -c my-first-password -``` - -To edit a password, use `pass edit`: - -``` -pass edit my-first password -``` - -You'll see that on the first line of this file is your generated password. -You can add more lines with notes or key-value pairs, which some clients can use for -autofill: - -``` - -Url: login.website.com -Username: my-username -``` - -For more commands, see [the password-store docs](https://www.passwordstore.org). - -#### 4. Set up sync - -First, init the pass git repository: -``` -pass git init -``` - -Then, go to your chosen git hosting provider and create a private repo. Once you -have an empty private repo, you can go ahead and add the remote: - -``` -pass git remote add origin -``` - -Then, do your first push: - -``` -pass git push -u origin -``` - -Now when you go to view your repo, you'll see an encrypted file called -`my-first-password.gpg`, which can only be read if you have your gpg key to hand. - -If you want to auto-sync whenever a password is changed, add a `post-commit` hook -to `~/.password-store/.git/hooks` that pushs to your repo. - -#### 5. Integrating with other devices -Take a look at the [list of compatible clients](https://www.passwordstore.org/#other) -to find `pass` clients for your other devices. Most have pretty good docs on -how to set them up, and most offer autofill and automatic syncing. - -I would suggest creating a new gpg key or sub-key for each device so you can -revoke them individually. For an extra layer of security, you can add a password -to the keys, but this will impact UX. - -#### 6. Migrating from your old password manager -Now the only remaining step is to move your old passwords! - -Fortunately [there's a plethora of tools available](https://www.passwordstore.org/#migration)! diff --git a/_posts/2023-07-17-pifs-1-functions.md b/_posts/2023-07-17-pifs-1-functions.md deleted file mode 100644 index c113619..0000000 --- a/_posts/2023-07-17-pifs-1-functions.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -layout: post -title: "Programming in Functional Style: Functions" -date: 2023-07-17 -tags: [pifs, functional programming] ---- - -If you've been programming for more than a couple of years, you've probably -heard of functional programming. You might even have picked up a functional -language and played around with it. - -Functional programming languages span a range of styles. On the one hand, -you have LISP-style languages, the masters of which pull off incredible -feats of language reinvention. On the other, you have Haskell wizards, whose -carefully structured types give their programs and data structures properties -that C programmes can only dream of. - -However, functional programming is not confined to functional languages; the -languages merely support the use of functional programs. So what does it mean -to be *functional*? - -Surely every programming language is functional, in the sense that it *does* -something? - -True -- but "functional" in this context means centering your programs around -*functions*. - - -### Functions -You know what a function is, right? Well, you probably have some conception at -least. You migh have heard it in a maths class, or you know it's what you make -every time you write `def` or `function()`. - -Here's a very loose definition: -> A function f is a transformation that takes some input X and returns some -> output f(X). - -This limited definition does fall apart somewhat quickly, unfortunately. - -Or does it? - - -#### Counterexample 1: Multiple inputs -Functions in `` can take multiple arguments, but the -definition given only allows a single input! - -I won't go into detail in this post about the various ways we can deal with -this, but a simple solution to this is lists. - -> Something that looks like a function with N arguments can be transformed into -> a function with one argument that is a list of length N - - -#### Counterexample 2: The state of the world -Functions in `` might behave differently depending on their -context! - -This means that for some input `X`, there might be a different output for -`f(X)`! - -Unless, that is... - -> When something looks like a function, but changes depending on its context, -> you can turn it into a function by including the context in the input. - -An example of this is reading a file. Depending what's on disk, you'll get a -different result! But if you consider what's on disk to be part of the input... - -### Purity - -I lied to you with the earlier definition of a function. What I actually gave -was the definition of a *pure* function. - -Why do we care about a function being pure, though? Isn't it a lot of hassle -to rewrite inputs and pass the entire world to the function every time? Well -yes, okay, if you insist. I promise that sometimes there's a good reason to -want it though. - -The best reason is that **you can always tell what a pure function's output -will be just from its outputs**. - -Sound useful yet? No? Uh... well, look, what does that imply? What can you -do with a pure function that your everday javascript function doesn't let -you do? - -#### Referential Integrity -Okay, don't let your eyes glaze over. I promise that I'll stop using -five-dollar words. - -Consider that in a pure function, you can always tell what the output -will bu just by knowing the input. - -So, it's easier to reason about! Especially when you've got a lot of -these pure functions glued together. And to wind back to 'referential -integrity', all it means is that you can tell what the output will be -just by looking at the input. There's no outside context involved. - -Some languages, like Haskell, enforce this. Most others don't care. But you'll -find this idea of things being easy to reason about pop up *everywhere*. - -You just have to look. - -Think about dependency injection. What is it really? It's about making a -core program that you can plug the context into, so that you can make it easier -to reason about -- usually with the goal of making it easier to test! - -There are other reasons you might have to want a program to be easy to reason -aboub, such as wanting to prove it works a certain way, or wanting to match -some existing modeling method. - -But the practical upshot for most is that it makes building and testing code -WAY easier. - -#### Memoization -Sorry! I can't help myself! Does it help if I just say 'caching' instead? - -Because that's all memoization really is. Caching at a micro level. And if -you think about it, purity is really important to caching. If you want to load -an image from a webserver quick, you want to cache it, right? But you can only -*do* that if you're reasonably sure that the image is going to be the same one -every time you ask for it. - -So purity really matters when you want to cache something and avoid extra work. - - -### Bringing it all together - -So, what did we learn? - -> A pure function f tranforms some input X to an output f(X) - -Pure functions: -* Are easier to reason about -* Are easier to test -* Have *referential integrity* -* Can be easily *memoized* - -I plan on doing a few more of these, so please reach out if this post helped -you! diff --git a/_posts/2026-03-30-the-phoenix-reborn.md b/_posts/2026-03-30-the-phoenix-reborn.md deleted file mode 100644 index 4f97ad5..0000000 --- a/_posts/2026-03-30-the-phoenix-reborn.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -layout: post -title: "The Phoenix Reborn" -date: 2026-03-30 -tags: [ai, llms, claude, phoenix project] ---- - -If you haven't read "The Phoenix Project" yet, this is your sign from the universe -telling you that it's time. Go on, this post will still be here when you get back. - -Still here? Cool. - -### A very quick Phoenix Project Recap - -An extremely condensed take on the central point of "The Phoenix Project" is: - -> Shorten feedback loops as much as possible - -In the book and day-to-day DevOps, that's achieved in several ways: -* Colocating ops resources on dev teams, instead of siloing them in separate orgs -* Making use of CI to provide fast feedback in a dev-like environment -* Making use of tests in the development flow -* Having prod-like environments available for testing - -Each of these seeks to reduce the amount of time a problem sits waiting before it -can be addressed by someone with the knowledge to fix it. - -### Code is free but specification is still expensive - -The step in quality of output from top-tier LLMs in late 2025 changed the game. -Code is now easy to produce in good quality and high quantity. The new gap is one -that has always existed: understand whole problems and write a plan detailed enough -to make a solution. - -Spec-driven development is a particularly promising output of this; structured input -that captures every possible problem, solution, and decision in a small problem space. - -But specs alone aren't enough; fortunately, however, Claude and co are very, VERY -good at writing tests, pipelines, and just about any other verification tooling you -could ever want, including lightweight (or heavyweight!) formal methods. - -### Give the AI the tools it needs to succeed - -This is the only takeaway that you need from this post. *You must shorten the feedback -loops for the AI as much as possible*. You need to make it possible for it to test -its own work if you want high quality outputs. - -At a minimum: -* Linter -* Autoformatter -* Tests -* CI/CD pipelines - -This is how you provide fast feedback and get good results; it's exactly the same -principle that devops was built on. - -Make your AI aware of them, make it use them, and make it constantly review its own work -(or get another model to do it). diff --git a/_posts/2026-08-15-dont-read-the-code.md b/_posts/2026-08-15-dont-read-the-code.md deleted file mode 100644 index 34fb676..0000000 --- a/_posts/2026-08-15-dont-read-the-code.md +++ /dev/null @@ -1,129 +0,0 @@ ---- -layout: post -title: "Don't read the code" -date: 2026-08-15 -tags: [ai, agents, agentic engineering, formal methods, user stories, devops, opinion] -summary: "Managers adapted to AI agents faster than engineers did, because they had already given up reading the code. Here's the job engineers are moving into." ---- - -I've recently gone from Director to IC, and I've taken the opportunity to get deep in agentic engineering. -Even before making the change, I've been digging in deep on how to get the most out of agents. There's a lot -of things that really don't work well, producing slop. There are some techniques that work kind of okay, -but need a lot of handholding. - -Even if we just speed up how fast we're generating code, we're not actually addressing the bottlenecks and -challenges that make writing software *hard* - we're just getting to the hard part faster! And to some -degree, I think this is why engineering managers (and especially middle managers) are having an easier time -with the new agentic tools. - -One of the things you have to give up as an engineering manager or director, or even a technical lead, is -direct control over code. You may no longer have the time to be the main reviewer, so you inevitably turn -to other tools for ensuring high quality - delegating to senior engineers for reviews, and focusing on -process and *really, really clear outcomes* to help your engineers drive towards the right solution. - -I was exposed to this very early in my career at GDS and again when consulting for NHS Digital while -still working as a frontline developer. The philosophy they use to drive high quality results is -[The Service Standard](https://www.gov.uk/service-manual/service-standard). It's a quick read, but the -hallmarks are first and foremost really understanding the whole problem you are trying to solve, and being -open to throwing away work in the journey to making the right solution. - -Another insight was solidified for me recently when listening to [this interview with Charity Majors](https://www.youtube.com/watch?v=HC8T1OlgYi0). -In it, she points out that Ops and QA have worked effectively for a long time without reading code. - -So, Engineer who is reading this post. It's time for you to stop reading the code. Code is for robots -and enthusiasts now. - -Don't write it. Don't read it. Don't review it. - -That's the agent's job now. You are no longer a machine that turns coffee into code. You are nuanced, -flawed, but most importantly human. Coffee optional. - -Your main value now is *understanding and communicating problems, and expressing the properties of a good solution*. - -## An elegant weapon for a more civilized age - -Rejoice! The days of toil are over! Yet now you are required to learn techniques and gain wisdom that -previously were the reserve of the leadership. You are no longer a digger, but a gardener. It's up to you -to create the conditions in which agents can do good work. - -### User stories - -We must dust off the tools of those who came before - the User Researchers and Product Owners - and take -a fistful of leaves out of their book. Understanding the problems we are solving is vital, and we cannot -understand a problem without understanding those who are suffering from it. - -You don't need to walk in their shoes; just beside them for a little bit while they relate their problems -to you is enough. We record what they want to achieve in [User Stories](https://en.wikipedia.org/wiki/User_story). - -### Tickets - -You have been the slave of tickets for some time now. It's time to become the master of them; treat them -as a place to store information about a problem, nurture them carefully, and relentlessly strike down -the ones that are not worth your time. There will always be more work than you are capable of taking on, -but you won't know what any of it is, or if it's worth your time, unless you write it down. - -### Formal Methods - -Among the wizards of the recent past there are whispers of machines that can predict the behaviour of -other machines, and find errors before a program is ever run. - -You can use those same techniques to find issues early and tightly bound the space of possible programs -that your LLMs will produce. Showing that your user stories are satisfied with tests and proofs all the way -through your codebase is a *strong assurance that your agent is actually successfully completing the tasks you -give it*, and not just assuming that it is. - -SMT solvers and proof assistants are now easier and cheaper to integrate into your work, with the added -benefit of your agents wasting less time chasing things that will never work. Start where the leverage is -highest: [TLA+](https://lamport.azurewebsites.net/tla/tla.html) and [Alloy](https://alloytools.org/) for -checking that a design holds before anyone writes it, and [Z3](https://microsoft.github.io/z3guide/) for -the constraints buried inside the code your agents are already producing. - -### Testing in real environments - -You must give your agents the ability to deploy infrastructure somewhere safe to test that it will actually -work. Your app is useless until it is deployed, and cannot be fully verified until the observable behaviour -specified in your user stories can be tested in a real end to end test. This is the final proof that the -agent actually made what you want. - -### Machine, review thyself - -Agents are better at code review than you are. Let them do it, and let them use the evidence from formal -methods, real deployments, and end-to-end tests to give them the best shot at doing a good job. Give them -standards and bars and make them adversarial to each other. - -## You, 2 years in the future - -The last couple of years were hard, but you're in a good place now. You've just come off a call with Sarah, -one of your customers, about an issue she's having with your systems. It's not the first time you've heard -it, and over a few days you've had time to come up with a good general solution. - -You take your draft notes from your notebook and take a picture, with some short instructions to turn this -into a ticket. By the time you're sat at your desk, there's a draft ready to review, with notes pulled in -from your recent discussions about this. You check that you agree with the writeup. There are a few open -questions about the problem. You talk to another engineer, Rob, and together you sit down with the problem -and hammer out the last few kinks. - -Once you're happy with the draft, you dispatch it, and go to get some lunch. - -One overpriced sandwich later, your phone buzzes - the prototype behaviour is ready. You sit back down at -your workstation and try out the prototype. You leave a few notes. A few minutes later, an updated -prototype is ready, and it's good to go. - -You ping Sarah a link - "can you tell me what you think of this?". - -Sarah has some notes; that's fine. You adjust the prototype one more time, ping it back, and Sarah responds -with a rocket ship emoji. - -You approve the prototype. - -A couple of hours later, it's running in production. A couple days later, you get a note from Sarah thanking -you for prioritizing this issue, and that her team is very happy with the changes. - -## What it takes to get there - -Honestly - not much that's *new*. Almost every tool above already exists and most of them are older than -you are; what changes is who they're pointed at. What you need to be doing right now is mapping out your -personal working process and figuring out how you can bust every bottleneck. - -It's a better time than ever to read [The Phoenix Project](https://itrevolution.com/product/the-phoenix-project/). -I wrote about pointing it at agents in [The Phoenix Reborn](https://sourceless.org/posts/the-phoenix-reborn.html). diff --git a/about.html b/about.html new file mode 100644 index 0000000..24e24c8 --- /dev/null +++ b/about.html @@ -0,0 +1,34 @@ + + + + + Sourceless - about + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Hi, I'm Laurence.

I'm an Director of SRE in the AdTech industry. In previous lives I've done SRE, security, and software in Healthcare and Government.

Notable past projects include:

My topics of particular interest are:

  • Building and running high performing teams
  • User-first security, compliance, and governance
  • Logic programming
  • Type systems

I've worked on a lot of projects in a lot of languages and frameworks; the best of those were backed by great teams that knew their tools inside out.

If you want to talk, or just want to see a CV, check out my linkedin.

+ + + +
+ + diff --git a/about.md b/about.md deleted file mode 100644 index 502c2e7..0000000 --- a/about.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -layout: page -title: about -permalink: /about.html -nav_order: 1 ---- -Hi, I'm Laurence. - -I'm a Director of SRE in the AdTech industry. -In previous lives I've done SRE, security, and software in Healthcare and Government. - -Notable past projects include: -* [NHS Digital's API Platform](https://digital.nhs.uk/developer/api-catalogue) -* [data.gov.uk](https://data.gov.uk) - -My topics of particular interest are: -* Building and running high performing teams -* User-first security, compliance, and governance -* Logic programming -* Type systems - -I've worked on a lot of projects in a lot of languages and frameworks; the best -of those were backed by great teams that knew their tools inside out. - -If you want to talk, or just want to see a CV, [check out my linkedin](https://www.linkedin.com/in/laurenceps/). diff --git a/assets/css/main.css b/assets/css/main.css index 0b02226..138f692 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -1,292 +1,42 @@ -:root { - --bg: #323F4B; - --text: #CBD2D9; - --link: #F5F7FA; - --code-bg: #1F2933; - --code-text: #E4E7DB; - --border: white; -} - -@media (prefers-color-scheme: light) { - :root:not([data-theme="dark"]) { - --bg: #F5F7FA; - --text: #323F4B; - --link: #1F2933; - --code-bg: #E4E7EB; - --code-text: #1F2933; - --border: #323F4B; - } -} - -[data-theme="light"] { - --bg: #F5F7FA; - --text: #323F4B; - --link: #1F2933; - --code-bg: #E4E7EB; - --code-text: #1F2933; - --border: #323F4B; -} - body { height: 100vh; - background: var(--bg); - color: var(--text); - font-family: 'IBM Plex Mono', monospace; - font-size: 16px; - line-height: 1.6; - margin: 0; + background: #323F4B; + color: #CBD2D9; } a { - color: var(--link); + color: #F5F7FA; } code { - background: var(--code-bg); - color: var(--code-text); - font-family: 'IBM Plex Mono', monospace; - font-size: 1.1em; -} - -nav { - font-family: 'IBM Plex Mono', monospace; - font-size: 1.2em; - margin-bottom: 2em; + background: #1F2933; + color: #E4E7DB; } #content { - background: var(--bg); + background: #323F4B; display: block; width: 40em; - margin: 3em auto; - padding-bottom: 10em; - position: relative; -} - -h1, h2, h3, h4, h5, h6 { - font-family: 'IBM Plex Mono', monospace; + margin: 10em auto; } -.site-title { - font-family: 'IBM Plex Mono', monospace; - font-size: 2.2em; - margin: 0; -} - -.site-title a { - color: inherit; - text-decoration: none; -} - -.ascii-logo { - display: block; - white-space: pre; - font-size: 0.28em; - line-height: 1.2; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} - -h1, h2 { - font-size: 1.6em; -} - -h1, h2, h3, h4, h5, h6 { +h2, h3, h4, h5, h6 { margin-top: 3em; } -/* Below the breakpoint the 40em column no longer fits, so let #content fill the - viewport and carry its own gutters. Without them the text runs edge to edge. */ @media only screen and (max-width: 768px) { #content { - width: auto; - margin: 2em 0; - padding: 0 1.25em 6em; - } - - blockquote { - margin-left: 0; - padding-left: 1em; + width: 100%; } } pre { - background: var(--code-bg); - padding: 1em; - overflow-x: auto; + background: #1F2933; + padding: 1em; } blockquote { - border-left: 0.5em solid var(--border); + border-left: 0.5em solid white; padding-left: 1.5em; margin-left: 2em; } - -hr { - border-color: var(--border); -} - -.skip-link { - position: absolute; - left: -9999px; - top: auto; - width: 1px; - height: 1px; - overflow: hidden; -} - -.skip-link:focus { - position: fixed; - top: 0; - left: 0; - width: auto; - height: auto; - padding: 0.5em 1em; - background: var(--code-bg); - color: var(--link); - z-index: 100; -} - -main { - margin-top: 3em; -} - -main > h2:first-child { - margin-top: 0; -} - -.post-meta { - margin-top: 2em; -} - -.post-nav { - margin-top: 1em; -} - -.post-list { - list-style: none; - padding-left: 0; - margin-top: 2em; -} - -.post-list li { - margin-bottom: 0.5em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.post-date { - font-family: 'IBM Plex Mono', monospace; - margin-right: 1em; -} - -/* Generated fractal artwork, one per post. Both theme variants ride on custom - properties set inline per post, so the swap below is a single pair of rules - rather than per-post CSS. */ -.post-banner { - height: 12em; - margin-bottom: 2em; - background-image: var(--banner-dark); - background-size: cover; - background-position: center; -} - -.post-banner + .page-title { - margin-top: 0; -} - -.post-thumb { - display: inline-block; - width: 2em; - height: 2em; - margin-right: 0.75em; - vertical-align: -0.7em; - background-image: var(--thumb-dark); - background-size: cover; - background-position: center; -} - -@media (prefers-color-scheme: light) { - :root:not([data-theme="dark"]) .post-banner { - background-image: var(--banner-light); - } - - :root:not([data-theme="dark"]) .post-thumb { - background-image: var(--thumb-light); - } -} - -[data-theme="light"] .post-banner { - background-image: var(--banner-light); -} - -[data-theme="light"] .post-thumb { - background-image: var(--thumb-light); -} - -.header-row { - display: flex; - justify-content: space-between; - align-items: baseline; -} - -.header-row .site-title { - margin: 0; -} - -.theme-toggle { - background: none; - border: none; - color: var(--text); - font-family: 'IBM Plex Mono', monospace; - font-size: 1.2em; - cursor: pointer; - padding: 0; -} - -.theme-toggle:hover { - opacity: 0.7; -} - -.read-aloud { - display: flex; - align-items: center; - gap: 0.75em; - padding-bottom: 1em; - margin-bottom: 2em; - border-bottom: 1px solid var(--border); - font-size: 0.9em; -} - -.read-aloud button { - background: none; - border: none; - color: var(--text); - font-family: 'IBM Plex Mono', monospace; - font-size: 1em; - cursor: pointer; - padding: 0; -} - -.read-aloud button:hover { - opacity: 0.7; -} - -.read-aloud label { - margin-left: 0.5em; -} - -.read-aloud input[type="range"] { - width: 6em; - accent-color: var(--text); -} diff --git a/assets/og/default-light.png b/assets/og/default-light.png deleted file mode 100644 index 29204cf..0000000 Binary files a/assets/og/default-light.png and /dev/null differ diff --git a/assets/og/default-thumb-light.png b/assets/og/default-thumb-light.png deleted file mode 100644 index da59c27..0000000 Binary files a/assets/og/default-thumb-light.png and /dev/null differ diff --git a/assets/og/default-thumb.png b/assets/og/default-thumb.png deleted file mode 100644 index 0874a12..0000000 Binary files a/assets/og/default-thumb.png and /dev/null differ diff --git a/assets/og/default.png b/assets/og/default.png deleted file mode 100644 index 60288f0..0000000 Binary files a/assets/og/default.png and /dev/null differ diff --git a/assets/og/dont-read-the-code-light.png b/assets/og/dont-read-the-code-light.png deleted file mode 100644 index d705d87..0000000 Binary files a/assets/og/dont-read-the-code-light.png and /dev/null differ diff --git a/assets/og/dont-read-the-code-thumb-light.png b/assets/og/dont-read-the-code-thumb-light.png deleted file mode 100644 index 5812866..0000000 Binary files a/assets/og/dont-read-the-code-thumb-light.png and /dev/null differ diff --git a/assets/og/dont-read-the-code-thumb.png b/assets/og/dont-read-the-code-thumb.png deleted file mode 100644 index b8f3488..0000000 Binary files a/assets/og/dont-read-the-code-thumb.png and /dev/null differ diff --git a/assets/og/dont-read-the-code.png b/assets/og/dont-read-the-code.png deleted file mode 100644 index d370aef..0000000 Binary files a/assets/og/dont-read-the-code.png and /dev/null differ diff --git a/assets/og/how-i-built-this-site-light.png b/assets/og/how-i-built-this-site-light.png deleted file mode 100644 index 4dd151e..0000000 Binary files a/assets/og/how-i-built-this-site-light.png and /dev/null differ diff --git a/assets/og/how-i-built-this-site-thumb-light.png b/assets/og/how-i-built-this-site-thumb-light.png deleted file mode 100644 index 94b9971..0000000 Binary files a/assets/og/how-i-built-this-site-thumb-light.png and /dev/null differ diff --git a/assets/og/how-i-built-this-site-thumb.png b/assets/og/how-i-built-this-site-thumb.png deleted file mode 100644 index b9f843f..0000000 Binary files a/assets/og/how-i-built-this-site-thumb.png and /dev/null differ diff --git a/assets/og/how-i-built-this-site.png b/assets/og/how-i-built-this-site.png deleted file mode 100644 index b9a593a..0000000 Binary files a/assets/og/how-i-built-this-site.png and /dev/null differ diff --git a/assets/og/make-your-own-free-password-manager-light.png b/assets/og/make-your-own-free-password-manager-light.png deleted file mode 100644 index a216bb6..0000000 Binary files a/assets/og/make-your-own-free-password-manager-light.png and /dev/null differ diff --git a/assets/og/make-your-own-free-password-manager-thumb-light.png b/assets/og/make-your-own-free-password-manager-thumb-light.png deleted file mode 100644 index bb4f047..0000000 Binary files a/assets/og/make-your-own-free-password-manager-thumb-light.png and /dev/null differ diff --git a/assets/og/make-your-own-free-password-manager-thumb.png b/assets/og/make-your-own-free-password-manager-thumb.png deleted file mode 100644 index 30d360e..0000000 Binary files a/assets/og/make-your-own-free-password-manager-thumb.png and /dev/null differ diff --git a/assets/og/make-your-own-free-password-manager.png b/assets/og/make-your-own-free-password-manager.png deleted file mode 100644 index 91c3aa8..0000000 Binary files a/assets/og/make-your-own-free-password-manager.png and /dev/null differ diff --git a/assets/og/perfect-isnt-good-enough-light.png b/assets/og/perfect-isnt-good-enough-light.png deleted file mode 100644 index 377a6a0..0000000 Binary files a/assets/og/perfect-isnt-good-enough-light.png and /dev/null differ diff --git a/assets/og/perfect-isnt-good-enough-thumb-light.png b/assets/og/perfect-isnt-good-enough-thumb-light.png deleted file mode 100644 index b70024e..0000000 Binary files a/assets/og/perfect-isnt-good-enough-thumb-light.png and /dev/null differ diff --git a/assets/og/perfect-isnt-good-enough-thumb.png b/assets/og/perfect-isnt-good-enough-thumb.png deleted file mode 100644 index b71ca52..0000000 Binary files a/assets/og/perfect-isnt-good-enough-thumb.png and /dev/null differ diff --git a/assets/og/perfect-isnt-good-enough.png b/assets/og/perfect-isnt-good-enough.png deleted file mode 100644 index 8d65d6f..0000000 Binary files a/assets/og/perfect-isnt-good-enough.png and /dev/null differ diff --git a/assets/og/pifs-1-functions-light.png b/assets/og/pifs-1-functions-light.png deleted file mode 100644 index df7481c..0000000 Binary files a/assets/og/pifs-1-functions-light.png and /dev/null differ diff --git a/assets/og/pifs-1-functions-thumb-light.png b/assets/og/pifs-1-functions-thumb-light.png deleted file mode 100644 index 7595105..0000000 Binary files a/assets/og/pifs-1-functions-thumb-light.png and /dev/null differ diff --git a/assets/og/pifs-1-functions-thumb.png b/assets/og/pifs-1-functions-thumb.png deleted file mode 100644 index e0e9a2c..0000000 Binary files a/assets/og/pifs-1-functions-thumb.png and /dev/null differ diff --git a/assets/og/pifs-1-functions.png b/assets/og/pifs-1-functions.png deleted file mode 100644 index 5619a9f..0000000 Binary files a/assets/og/pifs-1-functions.png and /dev/null differ diff --git a/assets/og/put-ticket-ids-in-your-commit-messages-light.png b/assets/og/put-ticket-ids-in-your-commit-messages-light.png deleted file mode 100644 index a348e54..0000000 Binary files a/assets/og/put-ticket-ids-in-your-commit-messages-light.png and /dev/null differ diff --git a/assets/og/put-ticket-ids-in-your-commit-messages-thumb-light.png b/assets/og/put-ticket-ids-in-your-commit-messages-thumb-light.png deleted file mode 100644 index 3bd7ffd..0000000 Binary files a/assets/og/put-ticket-ids-in-your-commit-messages-thumb-light.png and /dev/null differ diff --git a/assets/og/put-ticket-ids-in-your-commit-messages-thumb.png b/assets/og/put-ticket-ids-in-your-commit-messages-thumb.png deleted file mode 100644 index 09416fc..0000000 Binary files a/assets/og/put-ticket-ids-in-your-commit-messages-thumb.png and /dev/null differ diff --git a/assets/og/put-ticket-ids-in-your-commit-messages.png b/assets/og/put-ticket-ids-in-your-commit-messages.png deleted file mode 100644 index 7838244..0000000 Binary files a/assets/og/put-ticket-ids-in-your-commit-messages.png and /dev/null differ diff --git a/assets/og/relearning-to-learn-light.png b/assets/og/relearning-to-learn-light.png deleted file mode 100644 index 9f6d6f1..0000000 Binary files a/assets/og/relearning-to-learn-light.png and /dev/null differ diff --git a/assets/og/relearning-to-learn-thumb-light.png b/assets/og/relearning-to-learn-thumb-light.png deleted file mode 100644 index 1f79061..0000000 Binary files a/assets/og/relearning-to-learn-thumb-light.png and /dev/null differ diff --git a/assets/og/relearning-to-learn-thumb.png b/assets/og/relearning-to-learn-thumb.png deleted file mode 100644 index a40eb8c..0000000 Binary files a/assets/og/relearning-to-learn-thumb.png and /dev/null differ diff --git a/assets/og/relearning-to-learn.png b/assets/og/relearning-to-learn.png deleted file mode 100644 index a7b3315..0000000 Binary files a/assets/og/relearning-to-learn.png and /dev/null differ diff --git a/assets/og/the-continuous-delivery-test-light.png b/assets/og/the-continuous-delivery-test-light.png deleted file mode 100644 index 18f674e..0000000 Binary files a/assets/og/the-continuous-delivery-test-light.png and /dev/null differ diff --git a/assets/og/the-continuous-delivery-test-thumb-light.png b/assets/og/the-continuous-delivery-test-thumb-light.png deleted file mode 100644 index 5965f74..0000000 Binary files a/assets/og/the-continuous-delivery-test-thumb-light.png and /dev/null differ diff --git a/assets/og/the-continuous-delivery-test-thumb.png b/assets/og/the-continuous-delivery-test-thumb.png deleted file mode 100644 index cec26fa..0000000 Binary files a/assets/og/the-continuous-delivery-test-thumb.png and /dev/null differ diff --git a/assets/og/the-continuous-delivery-test.png b/assets/og/the-continuous-delivery-test.png deleted file mode 100644 index 37f84df..0000000 Binary files a/assets/og/the-continuous-delivery-test.png and /dev/null differ diff --git a/assets/og/the-documentation-triangle-light.png b/assets/og/the-documentation-triangle-light.png deleted file mode 100644 index 8e63753..0000000 Binary files a/assets/og/the-documentation-triangle-light.png and /dev/null differ diff --git a/assets/og/the-documentation-triangle-thumb-light.png b/assets/og/the-documentation-triangle-thumb-light.png deleted file mode 100644 index 0f27675..0000000 Binary files a/assets/og/the-documentation-triangle-thumb-light.png and /dev/null differ diff --git a/assets/og/the-documentation-triangle-thumb.png b/assets/og/the-documentation-triangle-thumb.png deleted file mode 100644 index 8e9acc2..0000000 Binary files a/assets/og/the-documentation-triangle-thumb.png and /dev/null differ diff --git a/assets/og/the-documentation-triangle.png b/assets/og/the-documentation-triangle.png deleted file mode 100644 index 6dfa131..0000000 Binary files a/assets/og/the-documentation-triangle.png and /dev/null differ diff --git a/assets/og/the-phoenix-reborn-light.png b/assets/og/the-phoenix-reborn-light.png deleted file mode 100644 index 2681472..0000000 Binary files a/assets/og/the-phoenix-reborn-light.png and /dev/null differ diff --git a/assets/og/the-phoenix-reborn-thumb-light.png b/assets/og/the-phoenix-reborn-thumb-light.png deleted file mode 100644 index c11cc96..0000000 Binary files a/assets/og/the-phoenix-reborn-thumb-light.png and /dev/null differ diff --git a/assets/og/the-phoenix-reborn-thumb.png b/assets/og/the-phoenix-reborn-thumb.png deleted file mode 100644 index 751a8f1..0000000 Binary files a/assets/og/the-phoenix-reborn-thumb.png and /dev/null differ diff --git a/assets/og/the-phoenix-reborn.png b/assets/og/the-phoenix-reborn.png deleted file mode 100644 index 8447fbc..0000000 Binary files a/assets/og/the-phoenix-reborn.png and /dev/null differ diff --git a/contact.html b/contact.html new file mode 100644 index 0000000..567a5c1 --- /dev/null +++ b/contact.html @@ -0,0 +1,34 @@ + + + + + Sourceless - contact + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + + + + + +
+ + diff --git a/contact.md b/contact.md deleted file mode 100644 index f3e7cbb..0000000 --- a/contact.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -layout: page -title: contact -permalink: /contact.html -nav_order: 2 ---- -* email: laurence@[this website] -* github: [sourceless](https://github.com/Sourceless) -* hacker news: [sourceless](https://news.ycombinator.com/user?id=sourceless) -* linkedin: [laurenceps](https://www.linkedin.com/in/laurenceps) diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 89d7110..0000000 --- a/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1774701658, - "narHash": "sha256-CIS/4AMUSwUyC8X5g+5JsMRvIUL3YUfewe8K4VrbsSQ=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b63fe7f000adcfa269967eeff72c64cafecbbebe", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index b3e25ff..0000000 --- a/flake.nix +++ /dev/null @@ -1,39 +0,0 @@ -{ - description = "sourceless.org - Jekyll site"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = nixpkgs.legacyPackages.${system}; - ruby = pkgs.ruby_3_3; - # Used by tools/generate_og_images.py to render per-post OG cards. - python = pkgs.python3.withPackages (ps: [ ps.numpy ps.pillow ]); - in { - devShells.default = pkgs.mkShell { - buildInputs = [ - ruby - python - pkgs.bundler - pkgs.pkg-config - pkgs.libffi - pkgs.libxml2 - pkgs.libxslt - pkgs.zlib - pkgs.libyaml - ]; - - shellHook = '' - export GEM_HOME="$PWD/.gem" - export PATH="$GEM_HOME/bin:$PATH" - bundle install --quiet 2>/dev/null - echo "sourceless.org dev shell" - echo "Run 'bundle exec jekyll serve' to start the dev server" - ''; - }; - }); -} diff --git a/index.html b/index.html index 66a807c..6ae9318 100644 --- a/index.html +++ b/index.html @@ -1,3 +1,52 @@ ---- -layout: landing ---- + + + + + Sourceless + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +
+
+
+ + 2026-03-30 The Phoenix Reborn
+ + 2023-07-17 Programming in Functional Style: Functions
+ + 2022-08-16 Make your own (free) password manager
+ + 2022-07-29 Put Ticket IDs in your Commit Messages
+ + 2022-07-07 The Continuous Delivery Test
+ + 2022-06-22 Perfect isn't Good Enough
+ + 2022-06-19 Relearning to Learn
+ + 2021-03-19 The Documentation Triangle
+ + 2021-01-17 Getting github pages to work with my custom site generator
+ +
+ + diff --git a/posts.html b/posts.html deleted file mode 100644 index 7b521df..0000000 --- a/posts.html +++ /dev/null @@ -1,4 +0,0 @@ ---- -layout: home -title: posts ---- diff --git a/posts/how-i-built-this-site.html b/posts/how-i-built-this-site.html new file mode 100644 index 0000000..9ecc862 --- /dev/null +++ b/posts/how-i-built-this-site.html @@ -0,0 +1,40 @@ + + + + + Sourceless - Getting github pages to work with my custom site generator + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Getting github pages to work with my custom site generator

So, I built a crappy site generator. It's pretty bad, which is the result of some very targeted design decisions:

  • I didn't want to spend a huge amount of effort building it. This is for fun, not work. You're not going to find any unit tests or any significant refactoring effort.
  • I want to generate a fast site that I can host for free (sans domain costs).
  • I want it to be easy to author new posts in my tool of choice (emacs).
  • Deploying the website should be a git push.

Feel free to look at the code (linked above) if you want, but it's really the least interesting part of this post. At a quick summary, here's what it can do:

  • Fixed index page that lists posts and pages
  • Pages and posts generated from markdown
  • Page/post metadata stored as EDN (Jekyll-style, but EDN isntead of YAML)

Getting Github actions to deploy from my own generator

I hasten to note this is by no means revolutionary or new. But here's how I did it.

Github Pages comes with Jekyll site generation built in. This is nice, unless you want to use your own generator.

Now of course, you could generate it locally and push it, but really, where's the fun in that? So, let's get github to do all the work.

For reference, here is the action that builds the site. At a high level, it:

  1. Generates the site from the files in the repo
  2. Resets to the www branch, which contains the built output served by GitHub pages (you can change the branch it watches in settings)
  3. Checks out and adds files
  4. Makes a new commit postfixed by the actions run number
  5. Pushes to the www branch.

(Note to readers: I haven't gone through this with a fine-tooth comb. It's quite possible that the reset is entirely unnecessary.)

The fantastic GitHub Push Action by ad-m was instrumental in getting this to work so easily.

The end result

So what am I left with?

  • A small, lean, html-only site
  • On free hosting
  • Using any site generator I want
  • Pushing a new version to master auto-generates a new site

Seems like it fits the bill! I'm happy with the output, and maybe I will spend some time improving the code to something I would be proud to show to people, but the fact remains: working code that solves the problem is better than neat code that doesn't work yet.

👋

Follow the discussion on Hacker News

+
+ Posted 2021-01-17 +
+
+ + +The Documentation Triangle → + + +
+ + diff --git a/posts/make-your-own-free-password-manager.html b/posts/make-your-own-free-password-manager.html new file mode 100644 index 0000000..9333db0 --- /dev/null +++ b/posts/make-your-own-free-password-manager.html @@ -0,0 +1,54 @@ + + + + + Sourceless - Make your own (free) password manager + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Make your own (free) password manager

Before I say anything else, it should be noted that this solution requires a little bit of background knowledge to set up and use. At the very least you'll need to be comfortable on the command line in linux/osx, especially with installing packages, git, ssh, and a little gpg. Don't worry if you don't know all of that – a little familiarity should be enough.

Why I rolled my own

You might be wondering what the point is of setting up your own password manager is when so many good, paid alternatives exist – and you'd be right!

There is however, some advantage in knowing exactly how your passwords are encrypted and being able to integrate your password store with EVERYTHING.

And it's run at no cost to me.

Standard warnings apply; the UX will not be as good. If you do not back up your GPG keys, you could lose access to your passwords forever. The security of your password manager is only as good as the security of your system.

Features and composition

Our password manager will have the following features:

  • Encrypted at rest
  • Command line, web browser, and mobile clientship
  • Per-device revocation
  • Cross-device sync

We'll make it with these components:

Make sure you have all the above installed before continuing!

Steps

1. Generate a gpg key

There's lots of guides on doing this, but here's an example (from 2022, if you're reading this in the future, google it first because best practice has probably changed!).

gpg --default-new-key-algo "ed25519/cert,sign+cv25519/encr" \
+    --quick-generate-key "my@email.com"
+

This will generate a new multi-purpose ed25519 key with no expiration date.

It's up to you if you want to use this to generate subkeys to use for the password store. I would, however, recommend that you make one key per device sou you can revoke access if a device is lost or stolen!

If you've already got a gpg key and want to use it – use that.

**And before anything else - back up your key! On a storage drive, paper, obelisk, yubikey - and in more than one place!**

2. Init the password store

pass init <your gpg key id from step 1>
+

That's it! If you want to use multiple keys (e.g. one per device), just add any additional IDs after the first.

3. Add your first password

pass generate my-first-password 100
+

This will generate a random 100-character password for you named my-first-password.

It'll print on the terminal in the first instance, and you probably don't want that, so you can pass the -c flag to send the password directly to your clipboard instead.

pass -c my-first-password
+

To edit a password, use pass edit:

pass edit my-first password
+

You'll see that on the first line of this file is your generated password. You can add more lines with notes or key-value pairs, which some clients can use for autofill:

<generated password here>
+Url: login.website.com
+Username: my-username
+

For more commands, see the password-store docs.

4. Set up sync

First, init the pass git repository:

pass git init
+

Then, go to your chosen git hosting provider and create a private repo. Once you have an empty private repo, you can go ahead and add the remote:

pass git remote add origin <repo git url>
+

Then, do your first push:

pass git push -u origin
+

Now when you go to view your repo, you'll see an encrypted file called my-first-password.gpg, which can only be read if you have your gpg key to hand.

If you want to auto-sync whenever a password is changed, add a post-commit hook to ~/.password-store/.git/hooks that pushs to your repo.

5. Integrating with other devices

Take a look at the list of compatible clients to find pass clients for your other devices. Most have pretty good docs on how to set them up, and most offer autofill and automatic syncing.

I would suggest creating a new gpg key or sub-key for each device so you can revoke them individually. For an extra layer of security, you can add a password to the keys, but this will impact UX.

6. Migrating from your old password manager

Now the only remaining step is to move your old passwords!

Fortunately there's a plethora of tools available!

+
+ Posted 2022-08-16 +
+
+ +← Put Ticket IDs in your Commit Messages + + +Programming in Functional Style: Functions → + + +
+ + diff --git a/posts/perfect-isnt-good-enough.html b/posts/perfect-isnt-good-enough.html new file mode 100644 index 0000000..9e24331 --- /dev/null +++ b/posts/perfect-isnt-good-enough.html @@ -0,0 +1,42 @@ + + + + + Sourceless - Perfect isn't Good Enough + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Perfect isn't Good Enough

Perfect is the enemy of the good

Or so goes the old adage. For developers, perfection is a mightier enemy than users, project managers, and even one's own lack of skill.

In pursuit of greatness

Many junior (and senior!) engineers strive to write the best code. Clean code. Well-tested. Fully covered. Abstractions on abstractions making beautiful, elegant interfaces.

Beautiful, elegant, fragile interfaces, which shatter upon exposure to the expectations of an ever-changing world.

No (useful) system exists in a vacuum. Users' needs change over time, as does the substrate on which software relies. Nothing is permanent.

The worst thing I've ever built

Very early in my career, I built a system to click through HTML forms, submit some information and return some information. There weren't APIs for this kind of thing, so traversing through legacy websites was the only way.

Even with that short description, you've probably already thought how you would do it; and most of you probably have a better plan than I, a junior developer at the time, did.

So I birthed a new language in to the world. A powerful tool to be sure. By issuing a list of simple commands, a headless browser would go off and perform the commands, fill the forms, and return the information we needed.

Great! Right?

But then there were different forms that were differently bad. Commands had to change and gain options. Commands were constantly bug-fixed. The code that ran was stored in a SQL database, so changing anything required a full run of the system and debugging was waiting for logs to populate. Every new integration made the system heavier and bigger and buggier.

If you're cringing – that's correct. If you're not – come back in a couple years (after you finish reading this).

The making of burdens

Why was that system so bad? What was it that I did that made it so inextensible and brittle?

The first reason is greed. I wanted to make something cool. I sought a challenge where non existed! The problem I was seeking to solve was primarily my own creation.

The second reason is ignorance. I naively believed that the original purpose of the system is all it would ever do.

The third, and perhaps worst reason, is hubris. Perfection for my own selfish reasons – not for the user, but the maker.

I didn't learn how to control each of these right away, and still combat each of these in my working and hobby lives. In moderation, these things are a positive force for learning, but when they take over – beware.

A successful mistake

My most recent relapse was creating an internal tool for a team to register public keys into a repository. By most measures I did an extremely good job of it – it was reliable, did only what it needed to, and was easy for me to extend.

'easy for me to extend'

Ah. Now there was the problem. This tool I created became a victim of its own success and, due to other commitments, changes needed to be made by other members of my team.

Which, for a little web frontend, would have been fine. Had I been on a team used to writing purely functional code, and had I not written it in Elm (no shade to Elm, it's a fantastic language, but it was the wrong choice in this context).

I had failed to keep in mind the future life of this thing I had created.

Code, much like art, ceases to belong to its author once it is released in to the world.

Valueless perfection and glorious mediocrity

Each of these issues of greed, ignorance, and hubris can be avoided through a judicious application of mediocrity.

The key to this is recognising that the time you spend trying to create something perfect is not only extremely expensive (you could release something much earlier), but also holds little value. A program is worthless until it is used, and will not grow in value if you make it too hard to change.

This doesn't mean you shouldn't experiment and sharpen your skills - just make sure you are aiming at a target, and not your own or someone else's foot.

Doing the dumbest possible thing

It's here, then, that I urge you to do the dumbest possible thing.

That may be somewhat hyperbolic, but here's what I mean by it:

  • Your code probably isn't written to serve you, so before anything else make sure it serves the user's needs. You have plenty of time to play on your own time.
  • Don't write more than you need to. Doing anything more than is sufficient is a waste of time. One-off script? Don't write tests for it. Wrote the same thing twice? Great, leave it. You don't know if every use case is going to look like that (aka Avoid Hasty Abstractions).
  • Get it out the door. The code you write has no value until it is used. Your assumptions can only be validated if you have something to test against! This doesn't mean you should ship completely broken stuff - what it does mean, however, is that you need a full solution to the problem, even if it's a crappy solution, so that you can make sure you're building the right thing.
  • Optimise nothing. You don't know what's going to be the slowest part yet, so don't waste effort solving a problem you don't have.

In other words; make it Just Good Enough to do what you need.

+
+ Posted 2022-06-22 +
+
+ +← Relearning to Learn + + +The Continuous Delivery Test → + + +
+ + diff --git a/posts/pifs-1-functions.html b/posts/pifs-1-functions.html new file mode 100644 index 0000000..564cb25 --- /dev/null +++ b/posts/pifs-1-functions.html @@ -0,0 +1,42 @@ + + + + + Sourceless - Programming in Functional Style: Functions + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Programming in Functional Style: Functions

If you've been programming for more than a couple of years, you've probably heard of functional programming. You might even have picked up a functional language and played around with it.

Functional programming languages span a range of styles. On the one hand, you have LISP-style languages, the masters of which pull off incredible feats of language reinvention. On the other, you have Haskell wizards, whose carefully structured types give their programs and data structures properties that C programmes can only dream of.

However, functional programming is not confined to functional languages; the languages merely support the use of functional programs. So what does it mean to be functional?

Surely every programming language is functional, in the sense that it does something?

True – but "functional" in this context means centering your programs around functions.

Functions

You know what a function is, right? Well, you probably have some conception at least. You migh have heard it in a maths class, or you know it's what you make every time you write def or function().

Here's a very loose definition:

A function f is a transformation that takes some input X and returns some output f(X).

This limited definition does fall apart somewhat quickly, unfortunately.

Or does it?

Counterexample 1: Multiple inputs

Functions in <favourite language> can take multiple arguments, but the definition given only allows a single input!

I won't go into detail in this post about the various ways we can deal with this, but a simple solution to this is lists.

Something that looks like a function with N arguments can be transformed into a function with one argument that is a list of length N

Counterexample 2: The state of the world

Functions in <favourite language> might behave differently depending on their context!

This means that for some input X, there might be a different output for f(X)!

Unless, that is...

When something looks like a function, but changes depending on its context, you can turn it into a function by including the context in the input.

An example of this is reading a file. Depending what's on disk, you'll get a different result! But if you consider what's on disk to be part of the input...

Purity

I lied to you with the earlier definition of a function. What I actually gave was the definition of a pure function.

Why do we care about a function being pure, though? Isn't it a lot of hassle to rewrite inputs and pass the entire world to the function every time? Well yes, okay, if you insist. I promise that sometimes there's a good reason to want it though.

The best reason is that **you can always tell what a pure function's output will be just from its outputs**.

Sound useful yet? No? Uh... well, look, what does that imply? What can you do with a pure function that your everday javascript function doesn't let you do?

Referential Integrity

Okay, don't let your eyes glaze over. I promise that I'll stop using five-dollar words.

Consider that in a pure function, you can always tell what the output will bu just by knowing the input.

So, it's easier to reason about! Especially when you've got a lot of these pure functions glued together. And to wind back to 'referential integrity', all it means is that you can tell what the output will be just by looking at the input. There's no outside context involved.

Some languages, like Haskell, enforce this. Most others don't care. But you'll find this idea of things being easy to reason about pop up everywhere.

You just have to look.

Think about dependency injection. What is it really? It's about making a core program that you can plug the context into, so that you can make it easier to reason about – usually with the goal of making it easier to test!

There are other reasons you might have to want a program to be easy to reason aboub, such as wanting to prove it works a certain way, or wanting to match some existing modeling method.

But the practical upshot for most is that it makes building and testing code WAY easier.

Memoization

Sorry! I can't help myself! Does it help if I just say 'caching' instead?

Because that's all memoization really is. Caching at a micro level. And if you think about it, purity is really important to caching. If you want to load an image from a webserver quick, you want to cache it, right? But you can only do that if you're reasonably sure that the image is going to be the same one every time you ask for it.

So purity really matters when you want to cache something and avoid extra work.

Bringing it all together

So, what did we learn?

A pure function f tranforms some input X to an output f(X)

Pure functions:

  • Are easier to reason about
  • Are easier to test
  • Have referential integrity
  • Can be easily memoized

I plan on doing a few more of these, so please reach out if this post helped you!

+
+ Posted 2023-07-17 +
+
+ +← Make your own (free) password manager + + +The Phoenix Reborn → + + +
+ + diff --git a/posts/put-ticket-ids-in-your-commit-messages.html b/posts/put-ticket-ids-in-your-commit-messages.html new file mode 100644 index 0000000..58e2590 --- /dev/null +++ b/posts/put-ticket-ids-in-your-commit-messages.html @@ -0,0 +1,42 @@ + + + + + Sourceless - Put Ticket IDs in your Commit Messages + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Put Ticket IDs in your Commit Messages

In my previous post The Continuous Delivery Test I included a point:

Do you include ticket IDs in your commits or branches?

This sparked quite a lot of discussion on hacker news. I think it'd be useful to highlight some of my reasoning for including this point, and highlight some of the advice given by some of the commenters.

Why you should do it

Link work back to the ticket that spawned it

If you're in anywhere but the smallest companies, you probably have some sort of compliance requirements that require you to be able to link work done back to the planning that caused it to happen.

The reason that compliance frameworks want you to do this is mainly so there is an unbroken chain of culpability that can be used to identify the root cause of a potential issue.

But it's also just good documentation. Do you really want to play archeologist every time you need to figure out why a certain commit was made, or why a certain line of code is a bit weird?

As one lovely HN commenter puts it:

'I can't tell you how many times I've dug into the commit history to find where a change happened, then wanted to know the full context, and been glad I could just pull up the years-old ticket directly and see a detailed description of intent and requirements, discussions in the comments, mock-ups, etc etc'

Track the content of releases

In some environments you might need to prepare release notes or a changelog.

This is, of course, much easier if you can derive the list of tickets from the delta of what's going in to production.

Measure how well your team is delivering

In Accelerate, Forsgren, Humble, and Kim pick out four metrics that seperate high performing teams from the rest - and they all have to do with deployment.

The first of those metrics is Lead Time, which measures the amount of time it takes from starting a ticket (or the first commit made, depending how you want to measure) to when it's eventually released.

Having some metadata available makes this much easier to do!

It's not that hard

Seriously, it's just habit. It might sound annoying or bureaucratic to you if you've never done it before, but after a couple weeks it'll probably be an unconscious practice that you'll never think about again.

And if typing <10 extra characters at the start of a commit message is really cramping your style, you can always write a prepare-commit-msg git hook.

Why you might not be doing it

Your company is small

If you're starting to grow and compliance is on the horizon, it's much easier to start now than when the deadline looms.

It's a personal project

Yep. Why would you? It's needless overhead.

Merging/branching strategy

I'm probably not going to convince you otherwise, but I think long-lived feature branches suck. They require complex merge patterns, bundle risk, and often require a bureaucratic ritual to merge in to a production branch.

Trunk-based development all the way, and if you can have a branch per ticket with the ticket ID in the branch name, even better.

I need to make an ad-hoc change

This usually falls in to one of two categories.

The first is "I want to make a change off the books". Usually this is a culture problem; you should feel empowered to create small tickets as you see fit to fix small issues. It might also be that your team doesn't create time for care and feeding work.

The second is "I don't know where this change belongs". Can you roll it in to another ticket you're already working on, e.g. one where you're already touching the code in question? Or maybe it's enough work that it should be its own item?

+
+ Posted 2022-07-29 +
+
+ +← The Continuous Delivery Test + + +Make your own (free) password manager → + + +
+ + diff --git a/posts/relearning-to-learn.html b/posts/relearning-to-learn.html new file mode 100644 index 0000000..7f1215a --- /dev/null +++ b/posts/relearning-to-learn.html @@ -0,0 +1,42 @@ + + + + + Sourceless - Relearning to Learn + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

Relearning to Learn

Many years ago, I took a test. It was the first step of our "Learning to Learn" class. After answering a series of multiple choice question, the form issued forth a proclomation:

YOU ARE A VISUAL LEARNER.

Over the course of my years at school I would learn this piece of information to be on the whole useless and entirely innefectual.

To cut a long story short, school and university didn't go particularly well for me.

Learning by accident

It's somewhat embarassing to admit, but I don't think I really learned how to teach myself things effectively until at least the age of 26.

When I look back, I realise much of my learning was a result of accidental chance; for example, I learned to program mostly by virtue of complete immersion and stumbling upon 'the right things'. I just followed my nose (and to be fair it worked out).

But now I'm an adult with a job and a family, and my time is far more precious. If I want to learn something new, I can't count on stumbling on the right approach anymore. Simply creating the conditions in which learning is possible is not sufficient, since I lack the time that approach requires.

Enter Mentor

How I learned to learn is, surprisingly, not at all related to my area of study or profession (software), but a hobby.

I met my whistle teacher Becky in 2020 on a video call. It was near the start of the pandemic, and I had some extra time on my hands. Becky is somewhat unlike other teachers I've had, in that she could mechanically break down and explain music to me in a way that allowed me not just to play it, but understand what I was playing.

The whistle is a small, cheap, end-blown flute – somewhat like a budget recorder. It has six tone holes that you cover with three fingers of either hand, and a mouthpiece that you blow in to. Out of this deceptively simple instrument, you can get two full octaves of range in two major keys and all the keys that come along with that. That is to say, it's small, but surprisingly versatile.

Irish traditional music, along with other traditional music of the British isles, is generally learned and played by ear, though sheet music is available. What sets this folk music apart from most classical and contemporary music is that the sheet music is essentially a suggestion. Using your skill, wits, and guile, you have to take a couple of parts of a repetitive tune and make it interesting.

This is where Becky comes in, with two really solid pieces of advice: one about how to practice, and one about how to learn.

Mindful Practice

The first of the things Becky taught me was how to practice; how to gain the mechanical skill required to execute what I knew, and to transform my knowledge into understanding.

"Know what you're going to practice before you pick up the whistle"

In retrospect, startlingly obvious. Noodling around randomly does very little but ingrain what you've already learned; if you want to learn something new then you have to have a goal. But, as with many things in life, having a plan (even if you have no idea what you're getting in to) is half the battle.

"Five minutes every day is better than an hour once per week"

This subtly implies something else; you should make your practice not just habit and routine, but low-friction and easy to access. You will do much better chipping away at small, easy problems, than trying to tackle a hard problem you are not ready for (and all hard problems are just n easy problems in a trenchcoat anyway).

"Do it slow until you can do it right"

This one has a very "slow is smooth, smooth is fast" kind of vibe, and it's right. Doing something slowly and accurately is hard. In a programming context, it's very easy to whack together some code that delivers a feature, but the end result is of limited use if it's buggy, doesn't actually deliver the desired feature, or is riddled with security or structural issues.

Learning to Learn

The second thing Becky taught me was how to learn. Specifically, how to learn an instrument, or a tune – the approach is broadly the same.

It's a model, and as my colleague Craig likes to say; all models are wrong, but some are useful. Hopefully this is one of the latter.

"First, learn the notes"

You have to be able to play a tune through in its entirety. Obvious, right? But there's a lot here – some tunes are difficult. Some tunes don't have a written form, and you'll have to learn them by ear. Worse, some tunes have very poorly transcribed sheet music that will lead you astray! Not all learning material is created equal.

Beyond that, you will also have to contend with your own (lack of) mechanical skill. You will likely get stumped by a strange combination of notes. Perhaps you're not accustomed to switching octaves so quickly, or you accidentally bang out the wrong note because of some pattern you've learned in a previous tune.

"Second, make it more interesting"

Boy, isn't that 'interesting' a really load-bearing word? In this context, it means adding ornamentation, variation, dynamics, and a plethora of other tools in the folk musician's toolbox.

Making the piece flow better - the folk musician's version of 'clean code' - is key to turning a roughly shaped tune into refined birdsong.

"Third, play the tune"

Taking all you have learned, play the tune. Not the notes. Not the parts. The whole thing. Feel the rhythm, lean in to the swing, and add to it with your performance.

This is not just the culmination of learning how to play the tune - it's the start of learning the meta-skill of picking up and playing the tunes you come across.

In code terms; release it. You must make mistakes to improve, and more often than not you will be startled how others see something that is so mundane to you as so magical.

Some observations

This is not a new topic, or a new field. People much smarter and more experienced than me have studied this for a long time, but I share it in hopes that for someone out there, the same realisations I had will click.

If you want to read more on this topic, take a look at:

And if I've somehow convinced you to listen to whistle music:

Thanks readers, and thanks Becky!

Discussion of this article on Hacker News

+
+ Posted 2022-06-19 +
+
+ +← The Documentation Triangle + + +Perfect isn't Good Enough → + + +
+ + diff --git a/posts/the-continuous-delivery-test.html b/posts/the-continuous-delivery-test.html new file mode 100644 index 0000000..d293722 --- /dev/null +++ b/posts/the-continuous-delivery-test.html @@ -0,0 +1,42 @@ + + + + + Sourceless - The Continuous Delivery Test + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

The Continuous Delivery Test

In 2000, Joel Spolsky published a piece on what he believed were the key factors that separated high-performing teams from the rest, in a post titled The Joel Test: 12 Steps to Better Code. It contains many insights that are still useful to this day; however, the world, software, and the teams that make it have moved on in the nearly 22 years since that post.

In 2015, John Kodumal of LaunchDarkly published an updated Joel Test for Continuous Delivery. While being far more up-to-date, it's still slipped a little in relevance.

If this post were to have an alternate title, it'd be 'Front-load Your Risks'. All of the advice below seeks to move risks away from the act of deployment and move them to earlier steps of the development process.

So, here's my best shot at a CI/CD Joel Test for the 2020s.

1. Do you use a distributed version control system?

Git, GitHub, and the ecosystems around them have eaten the software world, and for good reason. Beyond the usual advantages - branching, merging, and easy rollbacks, many of these hosted git servers now come with a whole slew of other productivity tooling.

Especially notable are those that move your CI closer to your source code, such as GitHub Actions, and GitLab CI/CD.

2. Do you practice trunk-based development?

Trunk-based development rolls up a bunch of good practices under one system.

It forces you to have one good main branch that is always deployable (and by implication, always able to roll back), and encourages merging small features one at a time.

Having an always-deployable main means that your deployment process becomes a lot more boring (which is great!), and is often indicative of a process that supports developers, rather than requiring long hours spent resolving merge conflicts and preparing release notes.

3. Do you merge little and often?

It's been known for some time now that large pull requests tend to get cursory reviews at best. On top of this, squirrelling away to work on a single feature for a long time means you are not spending adequate time making sure that you are building the right thing!

This also means using small, ticket-scoped feature branches, where the changes have a very tight scope.

4. Do two people read code before it is merged?

You'll notice I was very careful to avoid mentioning how this should take place. For some, pull requests are the right tool, but pairing and mob programming are also sufficient.

Nobody should be solely responsible for any code.

5. Do you require changes to pass checks before they can be merged?

One of the leading causes of outages is deployments. You should do your best to make sure that broken code never gets deployed in the first place.

6. Can you test your changes in a production-like environment before you deploy them?

There are many ways to go about this. You could spin up a small clone of your production env, run the tests, and then kill it. Where this is not possible, you can run tests against mirrored traffic or even run a canary.

7. Do you deploy to production as soon as main is updated?

Your deployments should be automated and happen right upon merge. This removes a burden from the team delivering a product and allows them to focus on features and reliability rather than long-winded release ceremonies. It can be hard to get here if you're currently releasing once a day, week, etc. – a mature approach to managing the risks of deployment is required, as well as battle-tested deployment processes and tools.

The earlier you start doing this, the easier it will be.

8. Does your deploy process self-heal?

When a deployment fails, can you roll back automatically? In addition to this, you should be able to roll back manually where necessary (though it's generally best to fail forwards!)

This also necessitates the use of blue-green deploys, canarying, healthchecks, and myriad other strategies to mitigate outage risk. It's very powerful to be able to turn a possible outage into a mere degradation.

9. Does your Infrastructure as Code live alongside the service it hosts?

It's a good idea to colocate any IaC with the software it's running rather than keeping the two in separate repositories. This dodges any dependency management between the two, and in many cases will save you a pipeline or two.

It's an antipattern (in the author's opinion) to have all infrastructure code completely separate from the application it supports. Common or shared infrastructure could be argued to have its own place, but a product should have all its pieces as close together as possible.

Monorepos are an extreme example of this philosophy.

10. Do you use feature flags?

Releases are a terrible mechanism for launching new features. You want to be able to run that shiny new feature in production as soon as possible, and you can't do that if you conflate a feature's launch with its release. This also means that you can preview and refine changes with flagship customers before rolling them out more widely.

11. Do you include ticket IDs in your commits or branches?

You need to be able to tie every commit back to the ticket that spawned it. It might not be you debugging a change that you merged, so it needs to be really easy to go back and find out why something was changed in order to make the right decision to fix it.

12. Can you still deploy from your own machine?

A key factor in developing software and infrastructure quickly is tight feedback loops. While it's probably not a great idea to be deploying directly to production from your own machine, having the ability to do so might be useful in a pinch.

You should be able to deploy to a dev environment (shared or ephemeral) such that you can easily validate your work in situ.

13. Can you show what will happen when a branch is merged?

Most IaC tools offer the ability to diff the changes they want to make. It's vitally important that when you go to merge some changes in, you know what the changes are, why you're making them (see point 11), and the effect the changes will have on the product or system.

This could be a cdk diff, or a visual diff if you're making a change to a frontend. Seldom is the code itself the interesting and crucial part of why you're making a change. The effect of your changeset should be evident not only to reviewers but also to bleary-eyed SREs at 4am.

Thanks to Josh Finch for his valuable feedback on this post.

+
+ Posted 2022-07-07 +
+
+ +← Perfect isn't Good Enough + + +Put Ticket IDs in your Commit Messages → + + +
+ + diff --git a/posts/the-documentation-triangle.html b/posts/the-documentation-triangle.html new file mode 100644 index 0000000..1c1370c --- /dev/null +++ b/posts/the-documentation-triangle.html @@ -0,0 +1,50 @@ + + + + + Sourceless - The Documentation Triangle + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

The Documentation Triangle (or, why code isn't self documenting)

At some point in your coding career, you've probably heard something like:

"My code is self documenting"

or

"Code IS documentation"

These statements can be hard to argue against, especially if they come from someone who is more experienced than you. A lot of the trouble is that they are not wrong – well, they're not completely wrong.

The goal of documentation

Why do we document things? If the code was enough, we would never need tutorials, API documentation, or anything of the sort, right? What user need is being fulfilled by having more than just code to communicate what our programs and services do?

It sounds obvious, but documentation exists so that code can be easily used.

This is incredibly easy to forget when the program is all in your head and you have an intuitive understanding – but to fresh eyes, is your code really all that easy to approach?

What, Why, How: The Documentation Triangle

        What        
+         /\         
+        /  \        
+       /    \       
+      /      \      
+     /        \     
+    /__________\    
+ Why            How 
+

Here's a rule of thumb I've seen repeated in a few places regarding how to ensure that your documentation is up to scratch.

Why a triangle? Well, triangles are a strong shape, and any missing side renders it entirely structurally unsound - just as missing out vital documentation can make your code as good as unusable to others.

What (code)

This is generally what people are going for when they say "my code is self documenting". Clear, concise, and well-written code is a form of documentation in itself.

It is the most honest account of what actually happens when you run the code, what the most important data structures are, and what interfaces you'll be able to interact with. Some languages are better than this for others, and supplementary information such as type signatures can be incredibly helpful in understanding some code that's new to you. However, there are some things this alone leaves out...

Why (comments)

Every program has a history - whether in the very mundane sense of having a history in version control, or a long and detailed history of changes it has undergone to meet various challenges.

It's this latter case that is the most crucial to note why certain pieces of code exist in their current state.

Comments are most often the vehicle for these sort of margin notes. Noting that this thing was done in this way for optimization reasons, or another thing was a dirty hack leaves signposts for the next maintainer to know what compromises have been made in building this software.

Maintaining (legacy) code without any comments is less like being a mechanic, and much more like being an archaeologist.

How (context)

The last side of the triangle is often the most neglected. It might live in docstrings, or in confluence, be automatically generated, or be in a carefully crafted README, but it's all information on the context in which the code will be executed.

No program exists in a vacuum. There is always some environment, some organisation, some process that it lives in or serves, and taken out of said context, it is entirely useless.

Failing to recognise this is the number one leading cause of new users and junior devs being unable to do 'this simple task' (don't quote me on this, I have no data).

There's a great litmus test for this. Take the new process or pipeline or whatever that you've implemented (be it setting up a repo, running a job, etc.) and find yourself someone that's technically competent but unfamiliar with what you're working on. Plonk them in front of it and shut the fuck up. Watch them struggle through it and only help them if they are genuinely, truly stumped. This will very quickly tell you where you need to spend effort on documentation.

Heck - make it part of your code review.

Discussion of this article on Hacker News

+
+ Posted 2021-03-19 +
+
+ +← Getting github pages to work with my custom site generator + + +Relearning to Learn → + + +
+ + diff --git a/posts/the-phoenix-reborn.html b/posts/the-phoenix-reborn.html new file mode 100644 index 0000000..7402dd0 --- /dev/null +++ b/posts/the-phoenix-reborn.html @@ -0,0 +1,40 @@ + + + + + Sourceless - The Phoenix Reborn + + + + + + + + + + + + +

sourceless

+ + +index + + about + + contact + + +

The Phoenix Reborn

If you haven't read "The Phoenix Project" yet, this is your sign from the universe telling you that it's time. Go on, this post will still be here when you get back.

Still here? Cool.

A very quick Phoenix Project Recap

An extremely condensed take on the central point of "The Phoenix Project" is:

Shorten feedback loops as much as possible

In the book and day-to-day DevOps, that's achieved in several ways:

  • Colocating ops resources on dev teams, instead of siloing them in separate orgs
  • Making use of CI to provide fast feedback in a dev-like environment
  • Making use of tests in the development flow
  • Having prod-like environments available for testing

Each of these seeks to reduce the amount of time a problem sits waiting before it can be addressed by someone with the knowledge to fix it.

Code is free but specification is still expensive

The step in quality of output from top-tier LLMs in late 2025 changed the game. Code is now easy to produce in good quality and high quantity. The new gap is one that has always existed: understand whole problems and write a plan detailed enough to make a solution.

Spec-driven development is a particularly promising output of this; structured input that captures every possible problem, solution, and decision in a small problem space.

But specs alone aren't enough; fortunately, however, Claude and co are very, VERY good at writing tests, pipelines, and just about any other verification tooling you could ever want, including lightweight (or heavyweight!) formal methods.

Give the AI the tools it needs to succeed

This is the only takeaway that you need from this post. *You must shorten the feedback loops for the AI as much as possible*. You need to make it possible for it to test its own work if you want high quality outputs.

At a minimum:

  • Linter
  • Autoformatter
  • Tests
  • CI/CD pipelines

This is how you provide fast feedback and get good results; it's exactly the same principle that devops was built on.

Make your AI aware of them, make it use them, and make it constantly review its own work (or get another model to do it).

+
+ Posted 2026-03-30 +
+
+ +← Programming in Functional Style: Functions + + + +
+ + diff --git a/tools/generate_og_images.py b/tools/generate_og_images.py deleted file mode 100644 index dd376ac..0000000 --- a/tools/generate_og_images.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 -"""Generate per-post fractal artwork in the site palette. - -Each post gets a deterministic Julia set derived from a SHA-256 of its body text -(front matter excluded, so retagging a post doesn't churn its art). The hash -picks an angle on the boundary of the Mandelbrot set's main cardioid. Sampling -the boundary rather than an arbitrary disc matters: a c drawn from open space -outside the set gives a Cantor dust Julia set, which renders as an almost empty -frame. Every point on the cardioid borders the set, so every post gets structure. - -Four files per post, because the site has both a dark and a light theme and the -post index needs something far smaller than a social card: - - .png 1200x630, dark (also the og:image) - -light.png 1200x630, light - -thumb.png 96x96, dark - -thumb-light.png 96x96, light - -The expensive escape-time field is computed once per post and colourised twice. - -Run from the repo root inside the dev shell: - - nix develop -c python tools/generate_og_images.py - -Output lands in assets/og/. Re-run after adding or editing a post. -""" - -import hashlib -import pathlib -import re -import sys - -import numpy as np -from PIL import Image - -ROOT = pathlib.Path(__file__).resolve().parent.parent -POSTS = ROOT / "_posts" -OUT_DIR = ROOT / "assets" / "og" - -WIDTH, HEIGHT = 1200, 630 -THUMB = 96 -SUPERSAMPLE = 2 -MAX_ITER = 400 -ESCAPE_R = 2.0 -LUT_SIZE = 1024 - -# Both palettes come from assets/css/main.css. Each is ordered background-first, -# so the ramp walks from the page colour out to the colour with most contrast -# against it, and the set's interior takes the background end. -THEMES = { - "dark": { - "stops": ["#1F2933", "#323F4B", "#CBD2D9", "#F5F7FA"], - "interior": "#1F2933", - }, - "light": { - "stops": ["#F5F7FA", "#E4E7EB", "#323F4B", "#1F2933"], - "interior": "#F5F7FA", - }, -} - -FRONT_MATTER = re.compile(r"\A---\s*\n.*?\n---\s*\n", re.DOTALL) - - -def hex_to_rgb(value): - value = value.lstrip("#") - return tuple(int(value[i:i + 2], 16) for i in (0, 2, 4)) - - -def build_lut(stops, size=LUT_SIZE): - """Linear ramp through the palette stops, as an (size, 3) uint8 array.""" - rgb = np.array([hex_to_rgb(s) for s in stops], dtype=np.float64) - positions = np.linspace(0.0, 1.0, len(rgb)) - t = np.linspace(0.0, 1.0, size) - return np.stack( - [np.interp(t, positions, rgb[:, ch]) for ch in range(3)], axis=1 - ).astype(np.uint8) - - -def body_of(path): - """Post text with YAML front matter stripped.""" - text = path.read_text(encoding="utf-8") - return FRONT_MATTER.sub("", text, count=1) - - -def params_from(seed_bytes): - """Derive the Julia constant, zoom and rotation from a content hash.""" - digest = hashlib.sha256(seed_bytes).digest() - - def unit(offset): - """Eight hash bytes as a float in [0, 1).""" - return int.from_bytes(digest[offset:offset + 8], "big") / 2 ** 64 - - theta = unit(0) * 2 * np.pi - # The boundary of the main cardioid. Nudging fractionally outward turns fat - # interiors into the dendritic filigree that reads better at card size. - cardioid = 0.5 * np.exp(1j * theta) - 0.25 * np.exp(2j * theta) - c = cardioid * (1.0 + 0.002 + unit(24) * 0.018) - zoom = 1.0 + unit(8) * 0.5 - rotation = unit(16) * 2 * np.pi - return c, zoom, rotation - - -def escape_field(c, zoom, rotation): - """Render the Julia set to palette indices, independent of any theme. - - Returns (indices, interior_mask, height, width) at supersampled resolution. - """ - w, h = WIDTH * SUPERSAMPLE, HEIGHT * SUPERSAMPLE - half_h = 1.3 / zoom - half_w = half_h * (WIDTH / HEIGHT) - - xs = np.linspace(-half_w, half_w, w) - ys = np.linspace(-half_h, half_h, h) - grid = xs[np.newaxis, :] + 1j * ys[:, np.newaxis] - grid *= np.exp(1j * rotation) - - z = grid.ravel().astype(np.complex128) - nu = np.zeros(z.size, dtype=np.float64) - active = np.arange(z.size) - - for i in range(MAX_ITER): - z = z * z + c - magnitude = np.abs(z) - escaped = magnitude > ESCAPE_R - if escaped.any(): - mag = magnitude[escaped] - # Normalised iteration count, so bands blend instead of stepping. - nu[active[escaped]] = i + 1 - np.log(np.log(mag)) / np.log(2.0) - keep = ~escaped - z = z[keep] - active = active[keep] - if active.size == 0: - break - - # Escape rates vary hugely with c, so a fixed ceiling blows out some frames - # and leaves others near-black. Stretch each against its own distribution: - # the bulk of the plane falls to the background end of the ramp, and the - # slowest-escaping filaments get the high-contrast end. - escaped_nu = nu[nu > 0.0] - if escaped_nu.size: - lo = float(np.percentile(escaped_nu, 55.0)) - hi = float(np.percentile(escaped_nu, 99.7)) - if hi <= lo: - hi = lo + 1.0 - else: - lo, hi = 0.0, 1.0 - t = np.clip((nu - lo) / (hi - lo), 0.0, 1.0) ** 1.1 - idx = np.clip((t * (LUT_SIZE - 1)).astype(np.int32), 0, LUT_SIZE - 1) - return idx, nu == 0.0, h, w - - -def colourise(idx, interior, h, w, theme): - pixels = build_lut(THEMES[theme]["stops"])[idx] - pixels[interior] = hex_to_rgb(THEMES[theme]["interior"]) - return Image.fromarray(pixels.reshape(h, w, 3), mode="RGB") - - -def save(img, path): - # Quantise to keep the committed PNGs small; the palette is narrow anyway. - # Dithering matters here: without it the wide, smooth gradients band badly. - img = img.convert( - "P", palette=Image.ADAPTIVE, colors=256, dither=Image.FLOYDSTEINBERG - ) - img.save(path, optimize=True) - return path.stat().st_size - - -def write_set(name, seed_bytes): - c, zoom, rotation = params_from(seed_bytes) - idx, interior, h, w = escape_field(c, zoom, rotation) - - total = 0 - for theme in THEMES: - suffix = "" if theme == "dark" else "-light" - full = colourise(idx, interior, h, w, theme) - - banner = full.resize((WIDTH, HEIGHT), Image.LANCZOS) - total += save(banner, OUT_DIR / f"{name}{suffix}.png") - - # Centre square for the post index. The set is centred on the origin, - # so a centre crop keeps the structure rather than an empty corner. - side = min(full.width, full.height) - left = (full.width - side) // 2 - top = (full.height - side) // 2 - square = full.crop((left, top, left + side, top + side)) - thumb = square.resize((THUMB, THUMB), Image.LANCZOS) - total += save(thumb, OUT_DIR / f"{name}-thumb{suffix}.png") - - print(f" {name} c={c:.4f} zoom={zoom:.2f} {total / 1024:.0f}KB total") - - -def main(): - if not POSTS.is_dir(): - sys.exit(f"no _posts directory at {POSTS}") - OUT_DIR.mkdir(parents=True, exist_ok=True) - - posts = sorted(POSTS.glob("*.md")) - print(f"rendering {len(posts) + 1} sets into {OUT_DIR.relative_to(ROOT)}") - - for post in posts: - # Strip the date prefix: 2026-08-15-slug.md -> slug - slug = re.sub(r"^\d{4}-\d{2}-\d{2}-", "", post.stem) - write_set(slug, body_of(post).encode("utf-8")) - - # Fallback for the landing page and any page without a post of its own. - write_set("default", b"sourceless.org") - - -if __name__ == "__main__": - main()