Moving to RenderCV
I recently met with a colleague from Honeywell over the weekend to reminisce about old times working at Honeywell and see how things were going for him. While discussing our gripes, new tools we’ve used, and the current state of the job marked; he mentioned that he was going to try a tool that allowed you to write and generate a resume using only YAML. This intrigued me, because I had only seen tools that allowed you to write your resume in JSON.
So this post will go over how I switched to using RenderCV to generate my Resume with my existing Hugo blog and GitLab CI. It was rather easy, but did take some trial and error to get it working.
Removing the old system
This was really easy, I just deleted my Connor-Grout_resume.pdf from my content/resume post as I would no longer need a dedicated blob for this since it will now be dynamically generated.
Done!
Converting to YAML
This part did take a little while to complete as I had copy and paste the contents from my otd file to my YAML file. At first I was going to just have a single YAML file called Connor_Grout_resume.yaml but decided to split it out to multiple config files to make readability and maintainability easier. These files are:
| File Name | Description |
|---|---|
design.yaml | The theme provided by ResumeCV with some font tweaks and small layout changes that I wanted. (I used the sb2nov base theme btw) |
locale.yaml | This configures how some of the data is displayed on the resume that pertain to the english locale and date formats, etc. |
rendercv_settings.yaml | this file is kind of cool because you can either use cli arguments or store them in this file instead (I opted for storing theme here so I can version control them, and I don’t want my CI script blocks to be long and ugly with cli arguments) |
resume.yaml | Finally the resume (or cv) that contains my work experience. This way moving forward I will only need to edit this file with my work experience |
So with the above files created I had to re-create each section in my resume in the YAML. You can see this here: resume.yaml. I have my contact info and then a: summary, work experience, education, skills, and certifications sections.
I will say there was a bug when generating my certificates due to a string ending in a special character: +. When I would generate my resume I would end up with: CompTIA: Linux+, Security
It took me forever to figure out why it was not rendering the last + in Security+. I’m just assuming its a bug either in RenderCV or Typst that makes it not render the last +. So to get around this I added a space followed by an invisible unicode character of Unicode+00AD - SOFT HYPHEN. After adding this special hidden unicode character I could get the Security+ to render properly.
Adding RenderCV to my GitLab CI
After getting my resume looking how I would like it I need to add the RenderCV step into my GitLab CI config, this was rather easy, but there were some problems. I’ll go over the issues I ran into attempting this so you don’t have to struggle for a few hours like I did.
Attempting to use the official RenderCV docker image
This was harder than I thought it would be. I saw on the RenderCV documents that they have an official image of rendercv/rendercv on docker hub. So I tried using their image to build it. It turns out that there’s some weird issue with GitLab attempting to use this image. The last line of their image is ENTRYPOINT ["/bin/bash"] which I wouldn’t think was an issue, but it turns out GitLab attempt to execute a script when launching the image before continuing the actual CI commands. This entry point breaks their script so the CI always failed on trying to get this image setup properly.
I tried fixing this by changing the entrypoint in my CI configuration by specifying various entrypoints I found online which involved calling bash -c or bash -c -l (-c takes in a command and -l launches bash as a login shell respectively):
build:resume:
stage: build
image:
name: docker.io/python:3-slim
entrypoint: ["/bin/bash", "-c", "-l"]
This did not work properly and led to another error. I finally tried the below from some other recommendations:
build:resume:
stage: build
image:
name: docker.io/python:3-slim
entrypoint: [""]
This also did not work, it led to some bash issue where GitLab’s init script isn’t executed properly. After looking at the RenderCV Dockerfile I realized I could just add this myself since it uses the python:3-slim image and installs their requirements.txt with an entrypoint specified after.
Attempting to use the python alpine-based image
Ok, so after learning from above their Dockerfile was literally:
# Use the official Python image as a base
FROM python:3.13-slim
# Install RenderCV:
RUN pip install "rendercv[full]"
# Create a directory for the app
WORKDIR /rendercv
# Set the entrypoint to /bin/sh instead of Python
ENTRYPOINT ["/bin/bash"]
I thought, great, really easy to replicate. Since I am using containers I decided to use their alpine image python:3-alpine image. I had already learned this lesson previously, but I guess I forgot my pain. I’ll get to why in a few moments.
So, I setup my CI to use the official python 3 image based on alpine. I start my CI and I get an error that the Rust package manager The reason pip is not installing the .whl version of a package like pandas on an Alpine-based image is that Alpine Linux uses a different C library called musl-libc, while the manylinux2014_x86_64 wheels are built against the GNU C Library (glibc). The manylinux2014_x86_64 wheels are not compatible with Alpine due to this difference in the underlying C library.cargo is not installed.. I thought that was strange as on my WSL I don’t have cargo installed and I didn’t have to compile any dependencies unlike in the container. Maybe the wheel and setuptools packages aren’t installed. So I add an extra line above to install those and to also upgrade pip as well. But I keep getting the same error about the cargo not being installed. So after some DDG-foo I find the same conclusion that I’ve found before (many times..):
Sigh… musl-libc ruins my plans again..
So, heed my advice here and learn from my, unfortunately multiple, mistakes and don’t use the alpine based python images. They do not use the standard glibc library which is what most of python is compiled against, so you will have to compile most of your dependencies in alpine substantially growing the container image pulling in a bunch of source code.
Creating the build:resume job
So I took the opportunity to rework my gitlab jobs to reflect more of what each job is doing. So I have created 2 new stages: build and publish. Each of these conveys what the job is doing followed by the technology or thing that the job is (ie. build:resume or publish:hugo).
There are now 2 jobs under the build stage: build:resume and build:hugo. These are the development jobs. The build:resume job will always execute as it’s a requirement for both the build:hugo and the publish:hugo jobs. But the build:hugo job only runs when the branch is not main. The publish:hugo job only runs when the branch is main. The build is essentially the same hugo build job, but has debugging on and doesn’t minify files to make things easier to debug and check out before publishing.
So, without further ado, here is the new build:resume job:
build:resume:
stage: build
image:
name: docker.io/python:3-slim
inherit:
default: false
timeout: 3m
script:
- apt update && apt install git -y
- python3 -m pip install wheel setuptools pip --upgrade
- python3 -m pip install "rendercv[full]"
- cd content/resume
- rendercv render "resume.yaml" --rendercv_settings.date "$(git log -1 --pretty='format:%cs' resume.yaml)" -rs "rendercv_settings.yaml" -lc locale.yaml -d design.yaml
artifacts:
paths:
- content/resume/*.pdf
I am using the python:3-slim image so it pulls in the libc properly and no complication is required for the python packages. The slim image is based on debian and does not come with git installed, so I also install that and use it a little later in the script.
Python dependencies: setuptools, wheel, and pip are upgraded first. Followed by installing the rendercv python package.
Next I change to the resume file and run the render command to generate my pdf. Let me run through the arguments quickly so you can see what it’s doing, I’m doing something a little clever with the rendercv_settings.date argument
| Argument | Quick Explanation |
|---|---|
| “resume.yaml” | This is my cv portion of the yaml code. This has the actual content of my resume |
| -rs | This is the setting for the rendercv including output settings, bolded words, etc. |
| -lc | This is locale settings like date format, etc. |
| -d | this is the resume design and layout that I’ve chosen |
| –rendercv_settings.date | This line controls the last modified date that displays in the top right of the resume once it’s generated |
Let me discuss the last argument (--rendercv_settings.date) a little more because it solved a large problem for me. So this is normally hardcoded in the yaml file, so I’m guessing once you update your YAML you will update the date so your resume reflects that it’s newer. Since I am generating this dynamically I don’t want to hardcode a date in the file, I want it to just update it when I update the file. Because of this I initially used "$(date +'%Y-%m-%d')" in it’s place. But I realized that anytime that I updated my blog or added another post my resume modified date would be updated, regardless of if I updated my resume or not. So this is where the fancy git command comes into play.
The "$(git log -1 --pretty='format:%cs' resume.yaml)" pulls up the git log for the file resume.yaml (I don’t want to update it if I simply change for format or something else, only resume contents). The -1 says to only use the most recent modification and finally the --pretty uses git’s formatting to output modified date in the YYYY-MM-DD format (that’s what %cs means). This way I can update my blog as often as I see fit. As part of this my resume will be regenerated by the CI, but it will use the git last modified date for the file so my resume will remain static until I explicitly update it.
Updating the existing CI jobs
So with the new job created, I have to update my existing jobs to ensure build:resume runs before my other jobs because the pdf is a dependency for the site. I also need to pass the complied pdf file from the build:resume job to my other jobs as well.
I can use the artifacts config on my resume job to save the output this is defined here in my code:
1build:resume:
2 stage: build
3 image:
4 name: docker.io/python:3-slim
5 inherit:
6 default: false
7 timeout: 3m
8 script:
9 - apt update && apt install git -y
10 - python3 -m pip install wheel setuptools pip --upgrade
11 - python3 -m pip install "rendercv[full]"
12 - cd content/resume
13 - rendercv render "resume.yaml" --rendercv_settings.date "$(git log -1 --pretty='format:%cs' resume.yaml)" -rs "rendercv_settings.yaml" -lc locale.yaml -d design.yaml
14 artifacts:
15 paths:
16 - content/resume/*.pdf
This will save any files with the pdf extension in the content/resume path. The way artifacts work is you can pull them into later jobs. The files will be saved to the same directory that was specified in the previous jobs. So, the code above will extract all *.pdf files into the content/resume directories of future jobs that have it specified in it’s dependencies or needs configs.
Once the artifacts are now being saved for other jobs, we need to specify to pull them into our existing jobs, like this below:
1build:hugo:
2 stage: build
3 timeout: 3m
4 needs:
5 - job: build:resume
6 artifacts: true
7 script:
8 - hugo --logLevel debug
9 artifacts:
10 paths:
11 - public
12 rules:
13 - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH
14
15publish:hugo:
16 stage: publish
17 timeout: 3m
18 needs:
19 - job: build:resume
20 artifacts: true
21 script:
22 - find content/resume -type f -regex '.*\.\(yaml\|txt\)$' -exec rm -rf {} \;
23 - hugo --gc --minify
24 - find public -type f -regex '.*\.\(css\|html\|js\|txt\|xml\|ico\)$' -exec gzip -k -f --best {} \;
25 - find public -type f -regex '.*\.\(css\|html\|js\|txt\|xml\|ico\)$' -exec brotli -f -k {} \;
26 rules:
27 - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
28 pages:
29 publish: public
The needs states that any jobs specified are both dependencies and also need to be completed first before the job that needs it can start. If I had used dependencies instead of needs the build:hugo or publish:hugo jobs would run in parallel with the build:resume job and not include the completed artifacts.
The last little change I needed was to add an additional find command to my publish:hugo job to remove the yaml files that are used to generate the pdf with RenderCV. I’ve only added this to my production job because if I need them for debugging something it’s nice to have them as artifacts. Once I am ready to publish, however, I want to remove as much bloat and unnecessary files from my site as possible. Hints why I am minifying my code, removing unneeded development files, and compressing all files I can with gzip and brotli.
1publish:hugo:
2 stage: publish
3 timeout: 3m
4 needs:
5 - job: build:resume
6 artifacts: true
7 script:
8 - find content/resume -type f -regex '.*\.\(yaml\|txt\)$' -exec rm -rf {} \;
9 - hugo --gc --minify
10 - find public -type f -regex '.*\.\(css\|html\|js\|txt\|xml\|ico\)$' -exec gzip -k -f --best {} \;
11 - find public -type f -regex '.*\.\(css\|html\|js\|txt\|xml\|ico\)$' -exec brotli -f -k {} \;
12 rules:
13 - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
14 pages:
15 publish: public
Wrapping it up
To summarize, this was a fun little exercise I would encourage newer developers to give a try. YAML is a really easy format to use and maintain, and the CI/CD configuration is very simple for this product. GitLab Pages, like I use, or GitHub Pages or Cloudflare Pages are all great ways to get your resume out there and to also have it version controlled. I may play around with some of the fonts and other edge-case configs for RenderCV in my free time to really push this to it’s limit (even though I really don’t need to :P )

