The starting point for every Unlocked product repo (EN-202). Create a new product from it with GitHub's Use this template button, then follow New product checklist.
It gives a product:
- One Go binary, one image:
app serveruns the API,app workerruns the background worker. GET <PATH_PREFIX>/api/version, the same JSON as unlocked-core's and the tutor's:{"version": "...", "commit": "...", "branch": "..."}.GET <PATH_PREFIX>/api/healthcheck, which returnsOK, the same as core's. Point the Deployment's readiness probe at it.- Routing under a path prefix: every route lives under
PATH_PREFIX, the path ingress sends this product's requests to. - CI: tests, golangci-lint and govulncheck on every push, conventional-commit
checks, and an image pushed to ECR from
mainand from each release. - release-please: versions and the changelog come from the commit messages.
There's no Kubernetes chart here. Every product's templates live in the
unlocked-platform chart (EN-183).
Needs Go 1.26 and Docker.
make run # API on :8080, under /<repo name>
make worker # the worker
make test # go test -race
make lint # golangci-lint, in Docker
make build # the image, named after the repocurl localhost:8080/<repo name>/api/versionSettings, all from the environment:
| Variable | Default | What it does |
|---|---|---|
ADDR |
:8080 |
Where the API listens |
PATH_PREFIX |
none (root) | The path every route lives under, such as /example (example and /example/ work too). Between slashes it may have letters, digits, -, ., _ and ~; anything else stops serve with an error. |
APP_VERSION, GIT_COMMIT, GIT_BRANCH |
unknown |
What /api/version reports. CI bakes them into the image. |
- Merge PRs to
mainwith conventional-commit titles (feat: ...,fix: ...). Each merge pushes:sha-<short>to ECR, then moves:latestto it if it's still the tip ofmain. - release-please keeps a
chore(main): release X.Y.ZPR open with the changelog. Afeatbumps the minor version, afixthe patch (before 1.0.0, a breaking change bumps the minor version). - Merge that PR when you want a release. It tags
vX.Y.Z, and CI pushes:X.Y.Zto ECR.
The conventional commits and pr title checks allow these types, and nothing else:
| Type | In the changelog? | Version bump |
|---|---|---|
feat |
yes | minor |
fix, perf, revert |
yes | patch |
chore, docs, style, refactor, test, build, ci |
no | none on its own |
A ! after any type (feat!:, refactor!:) marks a breaking change: it
always shows in the changelog and bumps the major version (the minor before
1.0.0). A scope is optional: fix(worker): ....
GitHub's Revert button writes Revert "...", which the checks reject;
reword it to revert: ... (the failing check prints the exact text).
In unlocked-platform:
- First time only: add the product's templates to the chart behind
components.<product>: a Deployment forservewith a readiness probe on<PATH_PREFIX>/api/healthcheck(and one forworkerif it has background jobs), a Service, ingress paths for itsPATH_PREFIX, and a per-revision migrate Job if it has tables. - Pin the release in each site file (
images.<product>.tag: X.Y.Z) and deploy through the deploy workflow. Only staging may uselatest.
govulncheck checks the code, its dependencies and the Go version against the
Go vulnerability database. It's a required check on every push, it runs on
main every Monday at 14:00 UTC so new vulnerabilities are caught without a
push, and it can be run by hand (Actions → govulncheck → Run workflow).
When it fails on main, it posts one line to Slack: UnlockedLabs/<repo>: govulncheck failed on main. and a link to the run. Failures on other branches
don't post; the PR's red check shows them. It posts only if the repo can see a
secret named SLACK_WEBHOOK_URL. It's a secret, not a variable, because anyone
with the URL can post to the channel. Without it, the run logs a notice and
posts nothing.
Still to decide (EN-205): one organization secret shared by every product repo, or a secret per repo, and which channel the alerts go to. Until then, repos run without it.
The URL comes from Slack. GitHub never shows a secret's value again, so it
can't be copied from core's SLACK_WEBHOOK_URL.
- Go to api.slack.com/apps and sign in to the Unlocked workspace.
- Open the app that already posts our alerts (core's go to
#unlockedv2-alerts). If there isn't one, Create New App → From scratch, name it (such as "Unlocked CI alerts"), pick the workspace and Create App. - Features → Incoming Webhooks, and turn on Activate Incoming Webhooks.
- Add New Webhook to Workspace, pick the channel and Allow. A workspace admin may have to approve a new app first.
- Copy the URL (
https://hooks.slack.com/services/...). Each URL posts to one channel; a different channel needs a new webhook.
As an organization secret (set up once; an org admin does it):
- Org Settings → Secrets and variables → Actions → New organization secret.
- Name
SLACK_WEBHOOK_URL, the URL as the value, Repository access: Selected repositories, pick the repos, Add secret.
For each new repo after that, open the secret, add the repo under Repository
access and Save. Or, with a token that has the admin:org scope
(gh auth refresh -h github.com -s admin:org):
gh api -X PUT orgs/UnlockedLabs/actions/secrets/SLACK_WEBHOOK_URL/repositories/$(gh api repos/UnlockedLabs/<repo> --jq .id)Don't use gh secret set --org ... --repos ... to add a repo: it replaces the
secret's whole repo list.
As a repo secret (a repo admin does it); gh asks for the URL, so it stays
out of your shell history:
gh secret set SLACK_WEBHOOK_URL -R UnlockedLabs/<repo>Or repo Settings → Secrets and variables → Actions → New repository secret.
The repo can see the secret if its name is listed by one of these:
gh secret list -R UnlockedLabs/<repo>
gh api repos/UnlockedLabs/<repo>/actions/organization-secrets --jq '.secrets[].name'The first lists repo secrets, the second organization secrets shared with the repo. To check the URL itself, post a test message to its channel:
curl --fail -X POST -H 'Content-Type: application/json' --data '{"text":"test from <repo>"}' '<url>'If the URL leaks or the channel changes, remove the webhook in the Slack app's Incoming Webhooks page (that kills the old URL), add a new one, and update the secret. Updating an organization secret updates every repo that uses it.
GitHub turns off scheduled workflows in a public repo after 60 days with no activity. Turn it back on at Actions → govulncheck → Enable workflow.
- A finding in a dependency:
go get <module>@<fixed version>, thengo mod tidy, in a PR. - A finding in the standard library: a newer Go patch release fixes it.
The check and the image both use the newest patch of the Go version in
go.mod, so rerun the check once that release is out, and merge something somainbuilds a new image with it.
Replace <repo> with the new repo's name, such as unlocked-open-content.
- Create the repo from this template under UnlockedLabs. Its first runs
fail until steps 4 to 6 are done:
build-and-pushcan't get AWS credentials andrelease-pleasehas no bot. That's expected. (GitHub's own "Initial commit" isn't checked: the commit checks run on PRs only.) - Rename the module (Go files only; this README keeps the template's name
until step 9 replaces it).
perl -piworks the same on Linux and macOS, unlikesed -i:grep -rl unlocked-product-template --include='*.go' --include=go.mod . \ | xargs perl -pi -e 's#unlocked-product-template#<repo>#g'
- Branch protection on
main: copy the template'sprotect-mainruleset (PRs with one approval, and thevet and test,golangci-lint,govulncheck,conventional commitsandpr titlechecks). One line, so it works in bash and fish:gh api repos/UnlockedLabs/unlocked-product-template/rulesets/$(gh api repos/UnlockedLabs/unlocked-product-template/rulesets --jq '.[] | select(.name=="protect-main") | .id') --jq '{name, target, enforcement, conditions, rules, bypass_actors}' | gh api -X POST repos/UnlockedLabs/<repo>/rulesets --input -
.github/CODEOWNERSasks@UnlockedLabs/reviewersto review every PR, so check the team can write to the repo (maintainorpush):gh api repos/UnlockedLabs/<repo>/teams --jq '.[] | "\(.slug) \(.permission)"'
- Release bot: install the unlocked-release-bot GitHub App on the repo
(the App's Install App page, Configure, then add the repo). Add the
variable
RELEASE_APP_CLIENT_ID(Iv23liURFfuC0NzSCm4G) and the secretRELEASE_APP_PRIVATE_KEY. For a key, open the App's settings, then Credentials → Key pairs → New key; paste the whole.pemand delete the file. - ECR: create an ECR repository named
<repo>inus-west-2, with mutable tags (the default), since every merge tomainmoves:latest:In the console, leave Tag immutability on Mutable.aws ecr create-repository --repository-name <repo> --region us-west-2 --image-tag-mutability MUTABLE
- AWS access for CI: add the repo to the trust policy of the
GithubActionsBuildPushEcrrole. New repos get the immutable OIDC subjectrepo:UnlockedLabs@<owner id>/<repo>@<repo id>:*(EN-187); check the prefix withgh api repos/UnlockedLabs/<repo>/actions/oidc/customization/suband the ids withgh api repos/UnlockedLabs/<repo> --jq '.owner.id, .id'. Then add the role's ARN as the secretAWS_IAM_ROLE. - Vulnerability alerts in Slack: give the repo the
SLACK_WEBHOOK_URLsecret, as in Vulnerability alerts in Slack. Without it,govulncheckstill runs and still blocks PRs; it just doesn't post to Slack. - Private Go module? If other repos import it, set
GOPRIVATE=github.com/UnlockedLabs/*where it's built, and give CI a read token. - README: replace this one with what the product is, how to run it locally, and how to release it.
- Platform: add its templates and pin its tag, as in How to deploy.
- CodeRabbit: an org admin adds the repo under org settings, Installed GitHub Apps, CodeRabbit, Configure. Open a PR to confirm it comments.
The standard every UnlockEd repo follows, and why, is docs/repo-standard.md
in unlocked-core.