Now onto the actual solution. Since this is a linked list we can’t simply walk all the way to the end of the list and then backtrack. Instead I found it useful to think of the desired end state of the algorithm - we want to end up with a pointer pointing to the node before the nth node from the end of the list. So in the example of [1, 2, 3, 4, 5] we want a pointer at the 3 node, which is the third node from the end, in order to be able to do p.next = p.next.next.
The trickiest part of this algorithm is the edge cases - I got a bit stuck on how to solve for [1, 2] with n = 1 (the result being [1]) and n = 2 (the result being [2]). Then I realized that when the size of the list is the same as n, that is a special case where you return head.next. In all the other cases I can modify the list and return head.
I didn’t really enjoy doing this algorithm as much as the dynamic programming one. I’m doing the algorithms because it’s something I enjoyed doing when I was a student, and it helps to bring back some of the joy of programming for it’s own sake. Maybe I’ll stick to the ‘hard’ questions, or at least questions where a brute force answer is easy but an optimal one takes some creativity.
We often need to run shell commands from within Ruby code, and Ruby provides a number of ways to do that. It’s sligtly confusing at first - specifically because there are different ways of accomplishing the same thing. I found this useful flowchart on StackOverflow that helps you choose the appropriate option. If you’re looking for a detailed writeup of the different options I recommend reading 6 Ways to Run Shell Commands in Ruby by Nate Murray.
One common mistake I’ve seen - and made myself - is to use string interpolation when passing arguments to a shell command. For example, in my
recent post on Tweeting new Jekyll Posts I was using Ruby to interact with the git command. I could have written:
Can break if it includes double quotes, ampersands, semicolons, etc.
Can lead to command injection if the title includes malicious input.
We could get around (1) by using Shellwords.escape and you could argue I don’t have to worry about (2) because I control the input. However, best practice dictates that you should never interpolate any arguments into shell commands. Instead you should use system and pass the arguments as parameters.
Good
# Git add and commitcommit_message="Blogged: #{document.title}#{URI.join(site_config.fetch('url'),document.url)}"system('git','add',post_path)system('git','commit','-m',commit_message)
This principle doesn’t just apply to Ruby - I had the exact same issue with Java code I wrote recently. Instead of building the entire command:
In this example I also needed to allow the consumers of this code (which I control) to pass additional options. In that case I would also suggest that you use an explicit whitelist/allowlist for accepted options.
As I’ve been updating the stylesheets on my blog, I ran into an issue with browser caching — changes to my CSS weren’t showing up right away. Since I’m serving assets through AWS CloudFront with a 7-day cache for non-HTML files, this behavior makes sense. While I could disable caching altogether, that feels like a blunt and amateur solution. Instead, I’m implementing asset fingerprinting to keep the performance benefits of caching while ensuring everyone always get the latest version of my styles.
I’m using the built-in jekyll-sass-converter plugin for Jekyll to compile my SASS files into CSS. Unfortunately it doesn’t offer any support for fingerprinting assets. I searched around and found the jekyll-minibundle which supports both minification and fingerprinting, but it doesn’t work with jekyll-sass-converter. I did stumble upon this gist by Yaroslav Markin which looked appealing - just a few lines of code and you have a hand-rolled digesting solution.
# frozen_string_literal: truerequire'digest'moduleJekyll# Jekyll assets cachebuster filter## Place this file into `_plugins`.moduleCachebusterFilter# Usage example:## {{ "/style.css" | cachebuster }}# {{ "/style.css" | cachebuster | absolute_url }}defcachebuster(filename)sha256=Digest::SHA256.file(File.join(@context.registers[:site].dest,filename))"#{filename}?#{sha256.hexdigest[0,6]}"rescueStandardError# Return filename unmodified if file was not foundfilenameendendendLiquid::Template.register_filter(Jekyll::CachebusterFilter)
I ran my site locally and it worked - my css file had a fingerprint appended.
I pushed it to my staging site and it didn’t work - no fingerprint. Which must mean the filename wasn’t found. I then went through a few rounds of debugging with ChatGPT - adding Jekyll logging, etc - and finally concluded that ChatGPT’s initial direction (which I ignored) was correct - when the filter runs the compiled CSS file doesn’t exist yet. It works locally because the previous build had already generated the file.
A quick hacky fix was to run jekyll build twice in staging, which did work - but is obviously not a great solution. Instead I needed to either get access to the compiled CSS file or generate the digest from the source SASS files. I don’t think it’s possible to access the compiled CSS file - because it doesn’t necessarily exist yet - so instead I started to look at the jekyll-sass-converter code in more detail.
At a high level the filter is invoked on the output of the SASS converter - in my case, /css/main.css. It then needs to
Find the source manifest file - in my case, /css/main.scss
Find all referenced SASS files
Instead of trying to parse out the SASS files I opted to simply look at all the files in my /_sass directory. The plugin allows you to alter the source directory for the SASS files (and add others) so ideally I need to interact with the plugin directly and get the config that way - I didn’t want to duplicate the code to parse the plugin’s config. Luckily this is something supported by Jekyll.
# frozen_string_literal: truerequire'digest'moduleJekyll# Jekyll assets sass_digest filtermoduleSassDigestFilter# Usage example:## {{ "/style.css" | sass_digest }}# {{ "/style.css" | sass_digest | absolute_url }}defsass_digest(filename)site=@context.registers[:site]returnsite.data['sass_digest'][filename]ifsite.data.dig('sass_digest',filename)scss_file=site.in_source_dir("#{File.dirname(filename)}/#{File.basename(filename,'.css')}.scss")unlessFile.exist?(scss_file)Jekyll.logger.warn'SassDigest:',"#{scss_file} does not exist"returnfilenameendscss_converter=site.find_converter_instance(Jekyll::Converters::Scss)ifscss_converter.nil?Jekyll.logger.warn'SassDigest:',"#{Jekyll::Converters::Scss} converter not found"returnfilenameendfiles=[site.in_source_dir(filename.sub(/\.css$/,'.scss'))]scss_converter.sass_load_paths.eachdo|path|Dir.glob("#{site.in_source_dir(path)}/**/*.scss")do|sass_file|files<<sass_fileendendsite.data['sass_digest']||={}site.data['sass_digest'][filename]="#{filename}?#{digest(files)}"endprivatedefdigest(files)combined=files.sort.map{|f|File.read(f)}.joinDigest::SHA256.hexdigest(combined)[0,8]endendendLiquid::Template.register_filter(Jekyll::SassDigestFilter)
The last piece that I had to figure out was how to cache the computed digest within a single build - it turns out the site object has a data hash that makes this straightforward.
I’m using this for now, but I’m not confident that this is the correct approach. I want to see if it’s possible to add this to the jekyll-sass-converter plugin directly.
I previously wrote about my experience attempting to use Github Actions to post a tweet every time I publish a new post on my self-hosted Jekyll/S3/Cloudfront blog. I managed to get to a working solution that was too complicated, so I’m trying another approach. I was following this post by Dave Brock where he described using the commit message as the entire tweet - so every commit message and git push is a tweet. I dismissed it as too simple, but now that I’ve seen how complicated the alternative is I’m going to try something similar.
Here is the new, simplified, workflow I envisioned:
When I am ready to add a new post to git I run a Ruby script that generates the commit message
When I deploy to product a step in the production deploy Github Action workflow tweets out the commit message
The first step is to write a Ruby script that generates the commit message. Identifying the post title and full url seems straightforward, but Jekyll allows you lots of flexibility around generating URLs, so I didn’t want to optimize for one specific use case. It turns out that I can instantiate the jekyll objects and tell it to parse the post itself.
#!/usr/bin/env ruby# frozen_string_literal: truerequire'uri'require'jekyll'# Get uncommitted post filesstatus_lines=`git status --porcelain _posts`.linesnew_posts=status_lines.select{|line|line.start_with?('??')}.map{|line|line.split.last}ifnew_posts.empty?puts'No new, uncommitted post found.'exit0end# Initialize a site object from _config.ymlsite_config=Jekyll.configuration({quiet: true})site=Jekyll::Site.new(site_config)# Path to the new postpost_path=File.expand_path(new_posts.first)document=Jekyll::Document.new(post_path,{site: site,collection: site.posts})document.read# Git add and commitcommit_message="Blogged: #{document.title}#{URI.join(site_config.fetch('url'),document.url)}"system('git','add',post_path)system('git','commit','-m',commit_message)puts"Committed with message:\n#{commit_message}"
A nice part of this approach is that I can easily test the script locally.
A key piece of nuance is that not every commit is a new post - sometimes I’m editing an old post (fixing a typo, updating a link, etc), or I’m making a change to the site itself (Jekyll config, css, etc). I wanted to update my Github Action workflow to look for a commit message with the Blogged: prefix. Github Actions exposes the latest commit message via a variable - github.event.head_commit.message - but it’s not populated on manual workflows (which is what I’m using for a production deploy). So instead I need to manually fetch the latest git commit.
This is a much simpler approach to what I had before, and it’s much easier to debug and predict what is going to happen. The only downside is that I need to be careful with my git commits before pushing to production - for example, if I fix a typo before pushing to production I need to be careful to amend the original commit.
This site is built with Jekyll and hosted on Amazon S3, with Cloudfront as the CDN. I recently did some work to make the deployments automated with Github Actions.
Every push to main is deployed to a staging environment (I don’t use pull requests)
Deployments to production is handled through a github action workflow that is manually triggered
I previously used Blogger and WordPress to manage my blog and both of those had the option to automatically tweet about new posts, but there is no built-in mechanism to do that with Jekyll (if you’re self-hosting). Of course I could manually tweet about every new post, but what’s the fun in that?
Here is the workflow I envisioned:
When I trigger the deployment to production some Github Action is triggered that checks for any new posts
For every new post (usually just one), extract the title and URL of the post
Tweet about it
How hard could it be? (Spoiler: very)
I read through this post by Dave Brock who had a similar goal, but he simply put his desired tweet as the commit message. I didn’t really like that approach - Jekyll already knows the post title and URL, and it’s all available in git - so I should be able to get that information and tweet it out.
The first idea I had was to start using Github releases to track every new ‘publication’ - meaning when I deploy to production a new release is created, and I can check for any new posts introduced between this latest release and the previous one. I started down that path but then I realized that it’s actually too heavy-handed for what I wanted - I’m really only using the existance of the tag.
I started off creating a new tag on every production deploy, so just extending my existing workflow:
The GITHUB_TOKEN is automatically exposed by Github Actions, so that felt like I was on the right track. Instead of extending the production workflow further I decided to have a dedicated ‘tweet’ workflow that is triggered by the creation of a new tag.
on:push:tags:-'production-release-*'
In order to test this new workflow I simply had it print out the newly created tag name. However, when I tested it the workflow never triggered. Apparently this is by design:
GitHub does not trigger a new workflow run when a tag is pushed using the default GITHUB_TOKEN (which you’re using via GH_TOKEN).
This is a deliberate design choice to prevent infinite loops between workflows.
Not to worry, I did some more reading and discovered I can set the trigger for my tweet workflow to explicitly kick off when the production deployment workflow completes. That’s arguably even cleaner, since I’m not relying on side effect of the deployment - I’m relying on the deployment workflow itself. The only annoying bit is that I need to explicitly check if the workflow run was successful.
Again, apparently this is a known issue (according to ChatGPT - I didn’t find this note in the documentation):
workflow_run only triggers on workflows that run in response to events like push, pull_request, schedule, etc.
It does not trigger for workflows started manually via workflow_dispatch.
This is a known limitation in GitHub Actions:
workflow_run does not support triggering on workflows that were manually triggered (workflow_dispatch).
I had the option of trying to use a repository_dispatch - which is manually calling a Github API to trigger the workflow via an HTTP API - or simply adding the tweet steps to the existing workflow. Since having a separate workflow wasn’t particularly important I opted to try that first.
At this point I needed to parse the current and previous tags, determine if there was a new post in the changeset, extract the title and URL of the post(s), and invoke the proper twitter API. The git part of this was reasonably straightforward after I read through some of the documentation:
-name:Get previous production release tagid:calculate_previous_release_tagrun:|TAG=$(git tag --list 'production-release-*' --sort=-creatordate --no-contains | head -n 1)echo "previous_release_tag=$TAG" >> $GITHUB_OUTPUT-name:Identify new published postid:identify_new_postrun:|CURRENT_TAG=${{ github.ref_name }}PREVIOUS_TAG=${{ steps.calculate_previous_release_tag.outputs.previous_release_tag }}echo "The new release tag is: $CURRENT_TAG"echo "The previous release tag is: $PREVIOUS_TAG"POST_PATH=$(git diff --name-only --diff-filter=A "$PREVIOUS_TAG" "$CURRENT_TAG" -- _posts/ | head -n 1)if [ -n "$POST_PATH" ]; thenecho "Found new post: $POST_PATH"echo "post_path=$POST_PATH" >> $GITHUB_OUTPUTfi
Extracting the title and URL of the post was not that straightforward. I was hoping there was a jekyll command to get that metadata, but nothing like that exists (although I’m sure you could write a plugin to do that nicely). I started to do this with a bunch of bash scripts but it felt super shaky, so I wrote a simple ruby script instead.
content=File.read(options[:post_path])post_metadata=YAML.safe_load(content.split(/^---\s*$/,3)[1])ifoptions[:extract]=='title'raise"#{options[:post_path]} does not contain a title"unlesspost_metadata.key?('title')putspost_metadata['title']elseraise"#{options[:post_path]} does not contain a permalink"unlesspost_metadata.key?('permalink')site_metadata=YAML.safe_load(File.read('_config.yml'))putsURI.join(site_metadata['url'],post_metadata['permalink'])end
This script also feels a bit shaky, but less so than having to write bash to remove comments out of a YAML file. At this point I finally had the title and URL of the newly added post, all extracted from the git history. All that was left was to use the Github Action to send Twitter notifications.
-name:Tweet the new postif:steps.identify_new_post.outputs.post_path != ''uses:nearform-actions/github-action-notify-twitter@masterwith:message:"🚀Blogged:${{steps.extract_post_info.outputs.post_title}}\n\n${{steps.extract_post_info.outputs.post_url}}"twitter-app-key:${{ secrets.TWITTER_CONSUMER_API_KEY}}twitter-app-secret:${{ secrets.TWITTER_CONSUMER_API_SECRET}}twitter-access-token:${{ secrets.TWITTER_ACCESS_TOKEN }}twitter-access-token-secret:${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}
And at this point I decided I really hate this entire solution: it’s way too complicated. I think the approach used by Dave Brock is actually much better - I can simply write commit messages with a certain format and invoke the Github Action as part of production releases if that format is present. I can easily write a ruby script to write the commit message which will be much easier to test and debug.
Read on to part 2 where I implement this new approach.