On this page
Project onboarding
This guide takes a maintainer from the project template to a first promoted release on CommonLayer. Your repository stays in your own GitHub account or organization: you connect it through the CommonLayer GitHub App, the platform checks it against the standards, and from then on it provides the guardrails, independent vulnerability checks, the mirror of what you release and a backup of the source.
The local part (build, test, scan, SBOM, chart tests) is what the first-party projects
run, locally and on GitHub runners (their trees are exported from this repository and
checked as repositories of their own by the projects workflow). The platform part (apply, connect, scan,
propose, submit, promotion) has API operations exercised by the acceptance tests, and the
submission pipeline ran end to end with real tools in the local publish drill. Connecting
a repository has been tested against a development stand-in for GitHub only; no GitHub
App exists for this platform yet and nothing has run against github.com. The template's
release workflow (keyless signing, GitHub's provenance, registration) has never run.
Each step says what applies.
Before you start
- You need a GitHub repository for the project's build configuration, in your own
account or organization, on which you are an administrator, and two people willing to
be primary and secondary maintainer. Every project has a repository of its own; the
first-party projects too (
commonlayer-org/base,commonlayer-org/nginx,commonlayer-org/valkey,commonlayer-org/postgresql; decision 0049). - Upstream must publish release tarballs with checksums (and ideally signatures), and its license must allow redistribution. The licensing guide explains the layers.
- Read the image standard and the chart standard. A rule you cannot meet is recorded as a deviation in the manifest with a reason.
1. Create the repository from the template
Copy the files of templates/project-template/ into the new repository: .github/
(workflows, CODEOWNERS, dependabot.yml, pull request template), schemas/ (the
project's copy of the manifest schema), scripts/, the Makefile, the lint configuration
and the sample project. Nothing in the template refers to a file outside it. Keep the
shared files unchanged (scripts/*.sh, chart/templates/_commonlayer.tpl, lint
configuration, Makefile, the workflows): the repository scan compares them with the
published template version, and the workflows' permissions and release steps with the
template's.
scripts/install-tools.sh # pinned, checksum-verified tools into ~/.local/bin
make validate build test scan sbom chart-test
The template builds a tiny static HTTP server (darkhttpd) so that every step runs before
you put your own software in. make build needs the base image digest (next step). Until
it is filled in, give the build a base by digest: BASE_IMAGE=<name:tag@sha256:...>, or
BASE_IMAGE=$(scripts/base-image.sh --build <checkout of commonlayer-org/base>), which builds the
base project and serves it by digest from a registry on 127.0.0.1. In CI, set the
repository variable CL_BASE_REPOSITORY to commonlayer-org/base: validate.yml then checks out
and builds the base project the same way.
2. Fill in commonlayer.yaml
The manifest is how the platform discovers and validates the project. It is validated
against schemas/commonlayer-v1.schema.json and by rules beyond the schema (exactly one
primary and one secondary maintainer, a SHA-256 for every supported line, platforms from
linux/amd64 and linux/arm64, channels pointing at declared lines). Fill in:
metadata: slug (^[a-z][a-z0-9-]{1,38}[a-z0-9]$), title, summary, a seeded category (web-server,database,cache,message-broker,search,monitoring,identity,ci-cd,object-storage,language-runtime,cms,base-image), keywords,licenseof the build configuration (Apache-2.0).upstream: name, homepage, repository, SPDX license of the upstream application.maintainers: CommonLayer usernames asprimaryandsecondary.versions[]: each supported line with the release tarball URL and its SHA-256, verified against upstream's checksum file or signature.images[]: Dockerfile and context, platforms,basewith the digest ofregistry.commonlayer.org/base:<tag>, user10001:10001, ports, volumes,readOnlyRootFilesystem, deviations.charts[],security(contact, policy document, scanners, gate, VEX file, signing identity),build(SBOM formats, provenance, andcomponents: the software you build from source, see below),release(tag pattern, channels),registry(source repository on GHCR in lower case, destinations).
Declare what you build from source. Scanners only see what the SBOM lists, and SBOM
tools often do not recognise a binary built from source. List each such component in
build.components with its purl (pkg:generic/<name>@{version}) and the CPEs the NVD
data uses for it (look them up with grype db search --pkg <name>); fromUpstream: true
takes the version from the image label and the source URL and SHA-256 from versions[].
make sbom adds the declarations to both SBOMs and make scan scans the image and the
SBOM. Only Grype matches declared components; Trivy does not
(ADR-0039). The
template, nginx, Valkey and PostgreSQL manifests are worked examples; each project's
docs/SECURITY.md says what its scanners see.
The signing identity for a standalone repository built with the template's reusable
workflows is https://github.com/<owner>/<repo>/.github/workflows/*@refs/tags/v*, with
issuer https://token.actions.githubusercontent.com
(ADR-0026).
Validate it locally with make validate, or against the platform's own rules with
POST /api/v1/manifest/validate (public), which takes {"content": "<the YAML>"} and
returns the issues with paths and codes.
The base image has not been published to registry.commonlayer.org yet, so no released
base digest exists. Until it does, every first-party manifest leaves
images[].base.digest empty and builds against a base built from the base project; the
release workflows refuse to release with an empty base digest. The one-time order (release
the base, fill the digests, set CL_BASE_IMAGE, then release the projects and the
platform) is in the base project's README (commonlayer-org/base; its tree is under
projects/base here until that repository exists).
3. Replace the sample software
image/Dockerfile: keep the stage structure, theARG UPSTREAM_*arguments that mirror the manifest, the labels,USER 10001:10001and theHEALTHCHECK. Download the source withimage/build/fetch-source.sh(checksum, optional OpenPGP verification); add runtime libraries withimage/build/add-runtime-packages.sh.image/entrypoint.sh: keep the entrypoint helper library and<VAR>_FILEsupport for every secret variable.tests/*.sh,tests/run.env, the chart and its unit tests.
Run make validate build test scan sbom chart-test until it passes. make test checks the
image standard (non-root UID, read-only root filesystem, health check, graceful stop,
labels, no package manager or network tools, no setuid files) plus your project tests.
make scan fails on findings at or above security.gate that have a fix and no VEX
statement. The kind install test (make chart-test-kind) needs a working kind cluster; it
does not run in the build sandbox and runs in the projects workflow on GitHub runners
(green for the template and nginx).
4. Apply as a maintainer
Both maintainers apply on /dashboard/maintainer/apply (POST /api/v1/maintainers/apply),
accepting the maintenance agreement shown there, and are approved by a platform admin on
/admin/maintainers. See the maintainer handbook.
5. Connect the repository
Dashboard → Onboarding (/dashboard/onboarding) starts the connection
(GET /api/v1/github/app/install). GitHub asks on which account or organization to
install the CommonLayer App and for which repositories; choose the account that owns the
repository and select it. GitHub then returns you to the portal.
The installation is bound to your CommonLayer account only after GitHub confirms, through your own authorization, that you can access it; your GitHub account is linked to your CommonLayer account at the same time. One GitHub account belongs to one CommonLayer account: an account that is already linked elsewhere, or a second GitHub account for you, is refused and nothing is bound.
What the App may do, and what it never does:
| Permission | Why |
|---|---|
| Administration (read and write) | to invite the maintainers the project appointed as collaborators, and to remove the access CommonLayer granted when an assignment ends. It changes no settings and never removes the owner or anyone it did not add |
| Contents (read) | the manifest, Dockerfiles, workflows and charts for the scan; the source backups |
| Metadata, Actions, Pull requests (read) | repository identity; observing builds and contributions |
| Issues (read and write, optional) | notifications as GitHub issues, if you choose that channel |
CommonLayer never pushes to your repository. Without Administration the repository is observed: scanned and backed up, but maintainers are not managed, and the project cannot be approved while the platform requires managed repositories (the default).
The picker (GET /api/v1/github/repositories) lists the repositories of your
installations and marks those you administer. Only an administrator of a repository, as
GitHub reports it, can onboard it (POST /api/v1/onboarding).
6. Read the onboarding scan
Starting an onboarding queues a scan. It is static: the platform reads the tree and
files through the GitHub API and executes nothing from the repository. The rules are the
image standard, the
chart standard and the template's structure, as a
catalogue (GET /api/v1/repository-rules): connection, structure, manifest, image,
chart, CI, template and repository settings. The project template passes every rule, so
a repository built from it starts close to compliant.
The report lists every result with the rule id, the standard it enforces, a message and
the file and line. The outcome is compliant, compliant_with_deviations (deviations
you declared in the manifest, with reasons) or non_compliant.
- A failing required rule blocks the proposal. Fix it in the repository; a push to
the default branch rescans by itself, or ask for a scan
(
POST /api/v1/onboarding/{id}/scan). - A failing advisory rule is shown and does not block.
- A rule you cannot meet for a good reason is declared as a deviation in the manifest where the standard allows it. A required rule can otherwise only be waived by a platform reviewer, with a written reason that is shown publicly on the project.
The same rules run again after every push and at least daily once the project is connected (the guardrail check). A project whose required rules start failing is shown as such on its public page, and its maintainers are notified.
7. Propose the project
When the onboarding is ready, propose the project from its page
(POST /api/v1/onboarding/{id}/propose). The project is created in state proposed
from the manifest on the default branch, with the repository as its verified primary
repository and the scan attached; you become its project admin and administer it under
/dashboard/projects/{slug}. Proposing with the plain form (POST /api/v1/projects)
is refused while the platform requires managed repositories
(projects.onboarding_required).
Submit it for review from the project's overview (POST /api/v1/projects/{slug}/submit);
a platform reviewer approves or rejects it on /admin/projects. Approval needs the
managed access level, a finished scan and no failing required rule that was not waived.
Until approval the project is visible only to its members and reviewers.
The platform syncs the manifest from the verified primary repository only (automatically on every push to the default branch), because the manifest carries the signing identity.
8. First release
Push the tag v<version>-r1 (release.tagPattern). The template's
release.yml validates, builds each platform natively and merges the index by digest,
generates and attests SPDX and CycloneDX SBOMs per platform digest, signs with cosign
keyless, attests SLSA v1 build provenance with GitHub's artifact attestation, publishes
the chart as an OCI artifact with the released image digest filled in, submits every
digest to CommonLayer and creates a GitHub release.
Registration (scripts/register.sh) posts each digest to
POST /api/v1/projects/{slug}/submissions and polls GET /api/v1/submissions/{id} until the
submission is promoted, or prints the failed checks and fails the job. The script
authenticates with CL_API_TOKEN (an API token with artifacts:submit) when that variable
is set, otherwise with a GitHub Actions OIDC token for audience commonlayer. The
template's publish.yml runs it with OIDC only (id-token: write; no token secret is
passed). The platform accepts the OIDC token when its repository_id and owner/name
match a verified repository of the project. Set the repository variable
COMMONLAYER_API_URL to the API base including /api/v1 when not using the default.
For an API token instead, create a CI service account on the project's Members page
(/dashboard/projects/{slug}/members, role maintainer) and give it a token with the
scope artifacts:submit, as the publish drill does. A service account can submit; it can
never approve anything.
What has and has not run:
- A release rehearsal against a throwaway local registry exercised push by digest, the index merge, release SBOMs and chart publication (WP-20).
- The publish drill (WP-70g, locally) ran the platform side for real: nginx built with
the project's scripts, SBOMs, a cosign signature and attestations made with a key,
submission with a CI service-account token, verification, scans with real Trivy and
Grype databases, mirroring and promotion, then anonymous pull and
cosign verifyfrom the CommonLayer registry, and the chart. See publish drill for what it does not prove. - Keyless signing, GitHub's provenance attestation and OIDC registration need GitHub and a deployed platform; the template's release workflow has never run (validated with actionlint and shellcheck only). The OIDC path of the platform is covered by the acceptance test of workflow A with a test verifier.
- The first-party manifests name their own repositories (
ghcr.io/commonlayer-org/<project>, signing identityhttps://github.com/commonlayer-org/<project>/.github/workflows/*@refs/tags/v*) since WP-83; a release fromcommonlayer-org/<project>can push and be verified. An export for another repository rewrites both (scripts/export-project.sh <name> <dir> --repository owner/name).
9. Verify promotion
The submission moves through received → validating → verified → mirroring → promoted
or ends rejected with the failed checks of the
verification policy. The project's Submissions
page (/dashboard/projects/{slug}/submissions) shows each step and every check. A
rejection lists every failed check; fix the cause and release a new revision (-r2).
Re-submitting the same digest after a transport failure resumes the existing submission
and runs the scan and evaluation again. Requested tags shaped like a release tag
(<version>-r<n>) must be the submission's own.
After promotion, the public digest page (/digests/{digest}, GET /api/v1/digests/{digest}) shows the checks,
signatures, attestations, SBOMs and scans, and
GET /api/v1/digests/{digest}/verify-instructions gives the exact cosign commands. Pull
from the CommonLayer registry and run them; see
verifying artifacts.
Trying it locally with your own project directory
With the development stack (make up, make dev, COMMONLAYER__GITHUB__PROVIDER=development
as in .env.example) the platform has a development GitHub, and a project directory of
your checkout can stand in for the repository you have not created yet:
- Create the directory from the template:
cp -r templates/project-template projects/myappand fill inprojects/myapp/commonlayer.yaml(step 2; the maintainers must be CommonLayer users of your local data set, for example yourself andadmin). - Sign in to the portal and open the installation page (Dashboard → Onboarding →
"Install the GitHub App"; in development it is
/dev/github/install). Make the directory a repository of your development account (POST /api/v1/dev/github/repositorieswith{"directory": "projects/myapp"};GET /api/v1/dev/github/accountslists the directories that qualify), then install the App on your account. - Start the onboarding of
<username>-gh/myappand read the scan (step 6). The repository's files are read from the directory when they are read, as they are (a directory without.githubhas no workflows, and the CI rules fail). Its manifest's registry source and signing identity are shown as the development repository's (a manifest written for your real repository names that one); every other finding is real. - Fix the files in the directory, then push (
POST /api/v1/dev/github/repositories/{id}/push): the directory's content becomes a new commit and the scan reruns. An unchanged directory has nothing to push. - Propose the project (step 7) once the scan is
ready.
The directory must be directly under projects/ (or be templates/project-template) and
contain a commonlayer.yaml; everything is development data and never leaves your machine.
In the Compose stack the checkout is mounted read-only at /src
(github.development.repository_root).
Your repository stays yours
- Access. With the managed level CommonLayer keeps the project's appointed
maintainers as collaborators of the repository. A maintainer without a linked GitHub
account is shown as pending until they link one. The project's Repositories page
(
/dashboard/projects/{slug}/repositories) shows who was granted access and why. - Source backups. Every release, a weekly schedule and the opening of a succession
take a backup: a verified
git bundleof every branch and tag with a list of the refs. The public project page shows the last backup. Issues, pull requests, wikis and Git LFS objects are not part of it. - Guardrails. The rule check of step 6 keeps running. It reports; it does not block your pushes or your releases in your repository. What it guards is the project's standing on CommonLayer.
- Stepping back. If the account that owns the repository leaves the project, a
succession case asks for a transfer of the repository to a remaining maintainer or
their organization within a grace period (30 days by default). GitHub keeps the
repository's identity across a transfer, so history, links and CI follow. The case
closes when an appointed maintainer other than the leaving person administers the
repository. If no transfer happens, the project continues from the latest backup in a
new repository, which must match the backup before it becomes the project's
repository; your original stays where it is, untouched. The template's
docs/MAINTENANCE.mddescribes both paths.
After the first release
- Findings, tasks and the monthly evaluation start with the first promoted supported digest; see the maintainer handbook.
- A project admin configures funding (target and allocation policy) on the project's
Funding page (
/dashboard/projects/{slug}/funding); see the funding model. - Keep
versions[]current: an upstream release ahead of the latest released version of a supported line creates arelease_publishtask and counts againstrelease.activity.