Skip to content
CommonLayer
Documentation menu
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, license of the build configuration (Apache-2.0).
  • upstream: name, homepage, repository, SPDX license of the upstream application.
  • maintainers: CommonLayer usernames as primary and secondary.
  • 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, base with the digest of registry.commonlayer.org/base:<tag>, user 10001:10001, ports, volumes, readOnlyRootFilesystem, deviations.
  • charts[], security (contact, policy document, scanners, gate, VEX file, signing identity), build (SBOM formats, provenance, and components: 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, the ARG UPSTREAM_* arguments that mirror the manifest, the labels, USER 10001:10001 and the HEALTHCHECK. Download the source with image/build/fetch-source.sh (checksum, optional OpenPGP verification); add runtime libraries with image/build/add-runtime-packages.sh.
  • image/entrypoint.sh: keep the entrypoint helper library and <VAR>_FILE support 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:

PermissionWhy
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 verify from 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 identity https://github.com/commonlayer-org/<project>/.github/workflows/*@refs/tags/v*) since WP-83; a release from commonlayer-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:

  1. Create the directory from the template: cp -r templates/project-template projects/myapp and fill in projects/myapp/commonlayer.yaml (step 2; the maintainers must be CommonLayer users of your local data set, for example yourself and admin).
  2. 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/repositories with {"directory": "projects/myapp"}; GET /api/v1/dev/github/accounts lists the directories that qualify), then install the App on your account.
  3. Start the onboarding of <username>-gh/myapp and read the scan (step 6). The repository's files are read from the directory when they are read, as they are (a directory without .github has 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.
  4. 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.
  5. 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 bundle of 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.md describes 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 a release_publish task and counts against release.activity.