Common lisp and Github workflows
Dynamically updating Github profile

Github has a functionality where the README
file in a repository with the same name as your username will be displayed on your profile. Since I am a fan of the indieweb’s POSSE1 practice, I’d like to use that space to advertise the writings on this website.
Other than merely linking to it as a handle I can write a short program that dynamically updates the profile to include what my most recent posts are. This also gives me an opportunity to write another program in lisp, something that always brings me joy.
Creating the README
First we must create the actual README
file that will be displayed. For this we create a short program in common lisp. The result should be a file named README.org
2 with a short biographical text followed by the X most recent blog posts (I chose 3 arbitrarily).
First we fetch the drakma
and xmls
packages from quicklisp, the only two dependencies. We also create global variables that hold the link to the RSS feed being fetched, the permanent contents of the README
, and the number of posts to display.
(ql:quickload '(:drakma :xmls)) (defvar *rss-url* "https://joarvarndt.se/rss.xml") (defvar *template* "I am a university student currently studying political science at the Swedish Defence University. I am interested in Lisp-languages, GNU Emacs, Free software, political economy, and the philosophy of life in an increasingly technical world. Here are some recent blog posts of mine: ") (defvar *article-count* 3)
Then we write a function that will find the titles of the three (or any other number) most recent entries in the URL of the RSS feed supplied. Each entry is formatted as a associated list (alist) with the first element being the title of the post, and the second being the link to it.
(defun fetch-recent-articles (url &optional (count 3)) "Fetch the COUNT (default 3) most recent RSS feed entries from URL and return an alist mapping title to link for each article." (let* ((response (drakma:http-request url)) (xml-string (octets-to-string response)) doc channel items) (setf doc (xmls:parse xml-string)) (setf channel (first (xmls:xmlrep-find-child-tags "channel" doc))) (setf items (xmls:xmlrep-find-child-tags "item" channel)) (loop for item in (subseq items 0 (min count (length items))) for title-node = (xmls:xmlrep-find-child-tag "title" item) for link-node = (xmls:xmlrep-find-child-tag "link" item) for title = (xmls:xmlrep-string-child title-node) for link = (xmls:xmlrep-string-child link-node) collect (cons title link))))
The titles are then formatted to a bullet list of org-mode titles and inserted after the contents of the template. The contents are then written to the README.org
file, creating it if it doesn’t exist.
(defun format-org-links (articles) "Expects an alist of article titles and their links, and outputs a list of strings formatted as org-mode links." (mapcar (lambda (pair) (let ((title (car pair)) (link (cdr pair))) (format t "- [[~a][~a]]" link title) (format nil "- [[~a][~a]]" link title))) articles)) (defun fill-in-template (template links) "Adds the list of LINKS to the end of TEMPLATE. LINKS should be a list of org-mode link strings. Returns the final string." (format nil "~a~%~{~a~^~%~}" template links)) (defun write-to-file (text) (with-open-file (str "./README.org" :direction :output :if-exists :supersede :if-does-not-exist :create) (format str text)))
Finally, this is all carried out in a main
function, with the fetched results being printed beforehand.
(defun main () (let ((recent-articles (fetch-recent-articles *rss-url* *article-count*))) (format t "Recent article list: ~% ~A ~%" recent-articles) (write-to-file (fill-in-template *template* (format-org-links recent-articles))))) (main)
Using Github workflows
This exercise also served to teach me to use Github’s “workflow” feature, a form of continous deployment. This way I also don’t have to maintain any hardware to constantly monitor the status of the blog posts.
First we give github write permissions to the repo and tell it to run the workflow twice a day and whenever I push to main (this was useful for troubleshooting).
name: Update Github Blog list on: push: branches: [main] pull_request: branches: [main] schedule: - cron: "0 */12 * * *" permissions: contents: write
Since this will always be a fresh machine we need to make sure that SBCL (Steel Bank Common Lisp) as well as the necessary dependencies are installed. This will also install Quicklisp, a package manager for common lisp libraries. Quicklisp usually requires you to confirm that it should add itself to the SBCL init file, but since this is an automated workflow we have to wrap (ql:add-to-init-file)
in the (ql-util:without-promting)
macro.
Installing SBCL accounts for the vast majority of the runtime, with installing drakma
and xmls
as the two smaller steps. Actually running main.lisp
is almost instant.
jobs: run-app: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Install sbcl run: sudo apt-get update && sudo apt-get install -y sbcl - name: Install Quicklisp run: | curl -O https://beta.quicklisp.org/quicklisp.lisp sbcl --noinform --non-interactive \ --load quicklisp.lisp \ --eval '(quicklisp-quickstart:install)' \ --eval '(ql-util:without-prompting (ql:add-to-init-file))' \ --quit - name: Install Dependencies run: | sbcl --noinform --non-interactive \ --eval '(ql:quickload "drakma")' \ --eval '(ql:quickload "xmls")' \ --quit
It then runs the common lisp application we wrote earlier and configures git. If git diff
doesn’t detect any changes to the README
it will finish successfully, otherwise the workflow will itself update the repository and push the changes to Github, displaying the changes publicly.
- name: Update README run: | sbcl --load ~/.sbclrc --script main.lisp git config --local user.name "github-actions" git config --local user.email "github-actions@github.com" - name: Check for changes id: check_changes run: | git diff --quiet || echo "changed=true" >> $GITHUB_OUTPUT - name: Commit changes if: steps.check_changes.outputs.changed == 'true' run: | git add README.org git commit -m "Update README with latest blogs" git push env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Conclusion
I’ve tried to make the code portable, so if you want to do the same thing all you have to do is put this code in your own repository, change the values of the global variables in main.lisp
and it should work.