Public Code, Private Infrastructure
A quick journey through our versioning and deployment system (and learning how to use Claude well)
The team at Void Projects has been intent on making most, if not all, of the code we write open source. We desire to be as public-facing as we possibly can. Everyone at Void Projects has agreed that, by making our work visible and accessible to everyone, we all benefit!
This, of course, is not a novel idea—it is an idea central to open source. However, many of our projects (such as the very website you are reading this post on) are not damaged by being made visible. They are only damaged if their infrastructure is exposed.
When we first started building this website, I was given ownership over its deployment. As a challenge, I decided to forego a traditional CI/CD pattern for the Void Projects website, and instead scaffold a different system that keeps our vital infrastructure private and our code public.
Design
The system has a simple design:

Any repository in our organization, public or private, can make a call to any of our private repositories. A GitHub App with write access, which is installed in each repository, generates a token that is used to call a respective private repository. It’s also unique to each run.
In other words—it acts as a member of our organization to properly allow a common workflow to access our organization-level secrets.
A calling repository must use our common notification workflow:
name: notify-repo
on:
workflow_call:
inputs:
target-repo:
description: Repo (name only, same org) to send the repository_dispatch to.
type: string
required: true
event-type:
description: repository_dispatch event_type the target repo listens for.
type: string
required: true
client-payload-json:
description: >-
Optional JSON object merged into client_payload. The source repo
(`source`) and commit (`sha`) are always added automatically.
type: string
default: '{}'
secrets:
# Passed via `secrets: inherit` from the caller (org-level secrets).
VOIDPROJECTS_DEPLOY_APP_ID:
required: true
VOIDPROJECTS_DEPLOY_PRIVATE_KEY:
required: true Having a common notification workflow allows any calling repo to customize the rest of its own individual pipeline, which is useful for stuff like triggering a version change.
Initial Challenges
The initial challenges I faced were, to my chagrin, sourced from Claude Code. When I work with Claude Code, I set a simple rule for myself:
Agents should only be used for discovery, learning, and for writing code that is already understood.
I try to avoid the urge to one-shot a problem or skip the process of learning about a new paradigm or tech stack. I do this by providing my agent with context and restricting myself to asking questions instead of giving it commands right away.
As an example: If I was building, let’s say, a CI/CD pipeline for my website that doesn’t follow a normal pattern, I would ask:
- Can you tell me about how private and public repos can interact in GitHub?
- Am I able to run an Action in one repo from another?
- Does GitHub have an event system driven by some kind of API?
I did none of that.
It actually cost me by trying to one-shot this system. Claude was able to write the proper YAML for the standard notification event, write the pipelines that would run on commits to main, and point me to creating the GitHub App that would federate repo communications; But it failed to realize that it was scaffolding the system for private-to-private connections instead of public-to-private.
As a result, the standard notification event couldn’t be accessed by any public repo in our organization. In my attempt to one-shot the system, I had actually wasted more time!
My fix was to use the same pattern, but make two crucial changes to its design:
- Scope the GitHub App secrets to the Organization level instead of per-repo.
- Scope the token generated by the GitHub App to the target repo.
Since our secrets live at the Organization level, you can pass a flag when you call the common notification workflow that signals it to read from the unreachable store. Theoretically, someone could re-encode the hidden secrets and output them to the console. As long as we properly review all pull requests to our common workflows, this is secure for this use case.
Post-Mortem
In retrospect, this felt like a small project compared to what we have cooking at Void Projects. My biggest takeaways were mostly underscored by how I implemented a solution and not necessarily the solution itself. If anything, I have a better understanding of how I’d like to work going forward—which I will always take as a win.