This section includes information relating to our Standards that we expect our Community Projects to adhere to.
This is the multi-page printable view of this section. Click here to print.
Project Standards
- 1: GitHub Repository
- 2: Standard Files
- 3: CI Tooling
- 4: Code Quality
- 5: Dependency Management
- 6: Secrets Management
- 7: Pull Request CI & Secrets
- 7.1: Reference Workflows
- 7.2: Ecosystem Guides
1 - GitHub Repository
This page documents the configuration that must be applied to each Project’s GitHub Repository.
The use of must and should indicate whether it is a requirement or strong guideline.
Any setting not explicitly documented below is not considered part of the standards at the time of publication.
General Settings
These settings are found under Settings -> General.
- Require contributors to sign off on web-based commits must be enabled ✅
- Default branch should be named
main - Features:
- Wikis should be disabled ❌
- Issues must be enabled ✅
- Sponsorhips must be disabled ❌
- Preserve this repository should be enabled ✅
- Discussions should be enabled ✅
- Projects should be disbaled ❌
- Pull Requests:
- Allow merge commits should be enabled ✅
- Allow squash merging should be enabled ✅
- Allow rebase merging should be disabled ❌
- Always suggest updating pull request branches should be enabled ✅
- Allow auto-merge should be disabled ❌
- Automatically delete head branches should be enabled ✅
Code and automation
Branches
These settings are found under Settings -> Code and automation -> Branches.
The following Branch protection rules should be applied.
main
- Require a pull request before merging Yes ✅
- Require approvals Yes - 1 ✅
- Dismiss stale pull request approvals when new commits are pushed TBC
- Require review from Code Owners Yes ✅
- Allow specified actors to bypass required pull requests No ❌
- Require status checks to pass before merging Yes ✅
- Require branches to be up to date before merging Yes ✅
- Checks according to Code Quality and Dependency Management must be covered
- Every tier of Pull Request CI & Secrets the Project has must be required - see Required status checks
- Require signed commits Yes ✅
- Allow force pushes No ❌
- Allow deletions No ❌
Required status checks
All tiers must run and pass before a change is merged. A tier that is not required is a tier that does not exist, so every check below is added to the main ruleset.
| Check | Comes from | Applies to |
|---|---|---|
| The Project’s CI result (one job that fails if any other CI job failed) | Tier 1 | Every Project |
Lifecycle / develop | Tier 2 | Every Project |
Tier 3 / ... (the Project names it, for example Tier 3 / PRO acceptance) | Tier 3 | Projects with tests that need a protected resource |
SonarCloud Code Analysis | Code Quality | Every Project |
A few practical points:
- Use one Tier 1 check. A CI workflow should end in a single job that depends on all the others and fails if any of them did not succeed. Require that job, and not each individual job. Then adding or renaming a job does not mean editing the ruleset.
- Tier 2 and Tier 3 report as commit statuses. They run from separate workflows, so they report on the commit by name. Require the status name exactly as the workflow sets it.
- A commit that has not been approved for Tier 3 is blocked. A Tier 3 status is attached to a specific commit. A Pull Request with no status for its latest commit cannot merge until a maintainer approves that commit, which is the intent.
- Branch protection that is configured as a “branch protection rule” works as well as a ruleset. Prefer rulesets, because they can also protect tags.
A Project cannot merge its first Pull Request that adds these workflows if it also requires the checks they produce. Add the workflows first, and require the checks in the ruleset once they have run on main.
Actions
- Fork pull request workflows from outside collaborators set to
Require approval for first-time contributors
Custom Properties
Set both Flagship-Project and Project-Status accordingly.
TO DOCUMENT
Security
Code security and analysis
- Private vulnerability reporting No ❌ - See Reporting Issues
- Dependabot No ❌ - See Dependency Management
- Code Scanning No ❌ - See Code Quality
- Secret scanning TBC
2 - Standard Files
This page outlines a set of required standard files and their contentst that each Project must adhere to.
If you created your GitHub Repository from our Community Template, these files will be created in your repository - you’ll just need to amend content as directed below.
There are a number of files you might think need creating that are not in the Template - see Organization Files.
Files YOU should create
Code Owners
This file must be named CODEOWNERS and be in the
.github folder at the root of your project.
Every repository must define who owns it.
The owners must be the Project’s own maintainers team, @sonatype-nexus-community/[PROJECT] (for example @sonatype-nexus-community/the-cla), so that the people who review changes are named by team and not by individual. See Project Maintainer.
The maintainers team is also the approval authority for Tier 3. Pull Request CI & Secrets lets only a person with the Maintain role approve licensed or otherwise protected tests, and the cleanest way to give that role is through this team. So the team must exist, must be granted Maintain on the repository, and must be the entry in CODEOWNERS. Do not list individuals in CODEOWNERS in place of the team, and do not rely on @sonatype-nexus-community/community-leaders alone: it does hold Maintain on repositories today, but it is the whole community’s leadership, not the people who maintain this Project.
To set this up for a Project that does not have a team yet:
- Create the team
@sonatype-nexus-community/[PROJECT]and add the Project’s maintainers. - Grant the team the Maintain role on the repository.
- Replace the entries in
.github/CODEOWNERSwith the team.
Here are some groups you can reference if appropriate:
| Group | Purpose |
|---|---|
@sonatype-nexus-community/community-leaders | Leaders of the Sonatype Open Source Community |
Template files from the Community Project Template
Contributing
This file must be named CONTRIBUTING.md and be in the root of your project.
This file contains project-specific information aiding others in providing contributions to the project. We recommend using the template and expanding to include your project specific information such as:
- Development Guidelines
- Coding Conventions
- How to test and testing expectations
License
This file must be named LICENSE and be in the root of your project.
This file contains the Open Source license applicable to this project. You can just copy the license from the template repository.
The approved license for Sonatype Community Projects is Apache-2.0.
Readme
This file must be named README.md and be in the root of your project.
This is your projects “shop-window”. We’ve provided a boilerplate template for you. Use this to introduce the project, define it’s purpose, explain how to use it etc…
You should not repeat information contained in other documentation files (such as Contributing), but you should link to other documentation files.
There must be a section The Fine Print at the end of the file as per the template.
Organization Defined Files
The following files are defined at the GitHub Organization level (in this repository) and you DO NOT need to copy or reproduce them in your project.
Code of Conduct
This file defines our conduct to define community standards, signal a welcoming and inclusive project, and outline procedures for handling abuse.
Security Policy
Provides instructions for how to report a security vulnerability in your project by adding a security policy to your repository.
3 - CI Tooling
The accepted standard is to use GitHub Actions for all automated Build, Test, CI and Release functions.
Historically, CircleCI was used, but projects must now make the migration to GitHub Actions as of September 2024.
4 - Code Quality
We require all Sonatype Community Projects to undertake scans by SonarCloud (first party code analysis) and Sonatype Lifecycle (third party dependency analysis) as a minimum.
First Party Code Analysis
We utilise SonarCloud’s Automatic Analysis.
Request your project is added to the Sonatype Nexus Community Sonar Cloud instance by reaching out to the Community Maintainers.
Once configured in SonarCloud, analysis will be automatic. You should configure SonarCloud Analysis as a required check for PRs into your main branch.
Additional configuration can be controlled by through the use of a .sonarcloud.properties file - read more here.
Status Badge
We encourage projects to include a SonarCloud status badge in their readme. An example to add to your README might be as follows:
[](https://sonarcloud.io/summary/new_code?id=sonatype-nexus-community_community-handbook.sonatype.com)
Replace community-handbook.sonatype.com with your repository name.
Dependency Analysis
We utilise a dedicated Cloud instance of Sonatype Lifecycle for Sonatype Community Projects.
To add analysis, you should include something similar to the below GitHub Workflow example below.
name: Continue Integration Checks
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
# Env Vars
env:
LC_APPLICATION_ID: $(echo "${{ github.repository }}" | cut -d '/' -f2)
jobs:
# You might have other jobs to run in parallel here
code_quality:
name: Code Quality
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
# Disabling shallow clone is recommended for improving relevancy of reporting
fetch-depth: 0
# Run any preparation steps here - such as `npm install`
- name: Sonatype Lifecycle Evaluation
uses: sonatype-nexus-community/iq-github-action@master
with:
serverUrl: ${{ secrets.SONATYPE_LIFECYCLE_URL }}
username: ${{ secrets.SONATYPE_LIFECYCLE_USERNAME }}
password: ${{ secrets.SONATYPE_LIFECYCLE_PASSWORD }}
applicationId: ${{ env.LC_APPLICATION_ID }}
stage: Build
target: .
The referenced secrets are provided at a GitHub Organization level.
5 - Dependency Management
We use Sonatype Lifecycle to ensure our Community Projects use only the best open-source dependencies.
Each project should include Sonatype Lifecycle analysis scans during each Pull Request and upon each Release.
You can check out the real world implementation for this handbook - here for Continuous Integration and here for Release.
When implementing your scans, do reference the official Sonatype Lifecycle documentation that relates to the langugages and ecosystems in the project.
Lifecycle stages
Each kind of run evaluates at its own Lifecycle stage:
| Run | Stage |
|---|---|
| Pull Request, or any other branch | develop |
CI / build on main | build |
| A release | release |
Pull Requests from forks cannot use secrets directly. See Pull Request CI & Secrets (draft) for how to evaluate them safely.
Example GitHub Action for Continuous Integration
env:
LC_APPLICATION_ID: community-handbook.sonatype.com # <-- Our standard is to use the GitHub Repository Name
jobs:
release:
...
steps:
...
- name: Sonatype Lifecycle Evaluation
id: evaluate
uses: sonatype/actions/evaluate@v1.0.1
with:
iq-server-url: ${{ vars.SONATYPE_PLATFORM_URL }}
username: ${{ secrets.SONATYPE_LIFECYCLE_USERNAME }}
password: ${{ secrets.SONATYPE_LIFECYCLE_PASSWORD }}
application-id: ${{ env.LC_APPLICATION_ID }}
scan-targets: '.'
stage: build # <!-- Use 'build' for CI on main; use 'develop' for Pull Requests and other branches
...
Example GitHub Action for Release
env:
LC_APPLICATION_ID: community-handbook.sonatype.com # <-- Our standard is to use the GitHub Repository Name
jobs:
release:
...
steps:
...
- name: Sonatype Lifecycle Evaluation
id: evaluate
uses: sonatype/actions/evaluate@v1.0.1
with:
iq-server-url: ${{ vars.SONATYPE_PLATFORM_URL }}
username: ${{ secrets.SONATYPE_LIFECYCLE_USERNAME }}
password: ${{ secrets.SONATYPE_LIFECYCLE_PASSWORD }}
application-id: ${{ env.LC_APPLICATION_ID }}
scan-targets: '.'
stage: release # <!-- Set to 'release' for the Release Workflow
...
6 - Secrets Management
We currently use GitHub Secrets for our Open Source Community projects.
This is great but currently has one known drawback - PRs from Forks cannot access our secrets and as such Continous Integration GitHub Workflows will fail.
See Pull Request CI & Secrets for the proposed standard on running automated checks for Pull Requests from forks without exposing secrets.
7 - Pull Request CI & Secrets
Status: DRAFT - for community and maintainer review. This section defines proposed standards and boundaries. It is available for adoption as a draft (see Adoption and ratification), but nothing here has been run in a real Project yet. The workflow snippets are reference drafts: they have been checked for syntax, but not yet run end to end in GitHub Actions.
The use of must and should indicate whether it is a requirement or a strong guideline.
Contents
- The problem
- Principles
- Overview of the tiers
- Tier 1: Open checks (no secrets)
- Tier 2: Sonatype Lifecycle evaluation
- Tier 3: Protected-resource integration tests
- Trusted refs, releases and tags
- Intermediate branches
- Repository and organization settings
- Workflow hygiene
- Contributor experience
- What Projects must, should and must not do
- Adoption and ratification
- Your tool or ecosystem is not covered
- Decisions recorded
- Open questions for review
- Glossary
In this section:
- Reference workflows: the complete Tier 1 to Tier 2 workflow pair that every ecosystem guide builds on.
- Ecosystem guides: what to prepare, and the Tier 1 snippet to prepare it, for each language.
The problem
Our Projects run two kinds of automation that need credentials:
- Sonatype Lifecycle evaluations, which need credentials for Sonatype Lifecycle.
- Integration tests that need a protected resource: something that is licensed, paid for, rate limited, or whose credentials would be damaging to lose. Many of our Projects test against live, licensed installations of Sonatype’s commercial products. Others have no such tests at all.
Contributors outside the Project’s maintainers work from forks. GitHub deliberately withholds secrets from workflows triggered by fork Pull Requests (see Secrets Management), so these checks fail, or are skipped, for exactly the people we most want to encourage.
The obvious workarounds are dangerous. A Pull Request is untrusted code: its author can change the application, its tests, its build scripts, its dependency manifests and even the workflow files. If untrusted code runs anywhere a secret is available, the secret should be considered leaked. Hiding the value in logs does not help, because code can send a secret elsewhere in any encoding it likes.
This standard exists to give contributors fast, useful feedback while keeping untrusted code and secrets apart.
Principles
These principles drive every decision below. When a Project’s situation is not covered, apply the principles.
- Untrusted code never runs where secrets exist. Any job that executes code from a fork has no secrets, and any job that has secrets does not execute code from a fork.
- Only data crosses the boundary. What passes from the untrusted side to the trusted side is a file that is checked and read, never run.
- Privilege is earned by review, not assumed. Access to secrets follows a maintainer’s explicit decision about a specific commit, never an author’s identity or history.
- Approval is bound to a commit. A review of one commit is not a review of the next one pushed.
- Give as much feedback as possible without secrets. The fewer checks that need a secret, the fewer contributors wait on a maintainer.
- Limit the damage if a secret does leak. Secrets must be narrowly scoped, short lived and easy to rotate.
- Let Lifecycle do the dependency resolution. We hand Sonatype Lifecycle the files its own ecosystem analysis is built to read, rather than generating our own inventory of a Project’s dependencies.
- One standard, many release styles. The standard does not dictate how a Project cuts a release.
Overview of the tiers
| Tier | Purpose | Runs for | Secrets | Contributor wait |
|---|---|---|---|---|
| 1 | Build, lint, unit tests, tests against unprotected resources, and preparing the Lifecycle scan input | Every PR, automatically | None | None |
| 2 | Sonatype Lifecycle evaluation of the prepared input | Every PR, automatically | Lifecycle credentials, held only by trusted code | None |
| 3 | Integration tests that need a protected resource | PRs a maintainer has approved; trusted refs | Protected-resource access, held only by trusted code | Maintainer review |
A contributor’s Pull Request should show a result for Tier 1 and Tier 2 within minutes of opening. Tier 3 shows as waiting for maintainer approval until a maintainer has reviewed the change. A Project with no protected-resource tests has no Tier 3.
flowchart LR
PR([Fork PR opened or updated])
subgraph U["UNTRUSTED: runs PR code, has NO secrets"]
T1["Tier 1<br/>build, lint, tests"]
PREP["Prepare scan input<br/>(files Lifecycle reads natively)"]
end
subgraph T["TRUSTED: runs default-branch code, has secrets"]
T2["Tier 2<br/>Lifecycle evaluation"]
T3["Tier 3<br/>protected-resource tests"]
end
M{{"Maintainer approves<br/>a specific commit"}}
PR --> T1
T1 --> PREP
PREP -. "artifact: DATA only,<br/>never executed" .-> T2
PR --> M
M --> T3The dotted line is the only thing that crosses from the untrusted side to the trusted side. It carries data that is checked but never run.
Tier 1: Open checks (no secrets)
What
Every Project must run the following on every Pull Request, including those from forks, using the pull_request trigger and with no access to secrets:
- Build / compile.
- Linting, formatting and static analysis that does not need credentials.
- Unit tests.
- Integration tests that can run against unprotected resources: software that needs no licence or credentials, such as Nexus Repository Community Edition running in a container, or mocked and recorded responses.
- Preparing the Lifecycle scan input (see Tier 2) and uploading it as an artifact.
Why
- It is the safest tier: a hostile Pull Request can only harm a short-lived, credential-free runner.
- It gives contributors the same feedback as maintainers get, immediately.
- It sets the boundary for Tier 3. Everything that can be tested without a protected resource must be tested here, which keeps the protected tier small.
- It is the right place to run the project’s own build tooling (package managers, build plugins, install scripts), because that tooling is itself untrusted code.
The protected / unprotected test split
Separating tests that need a protected resource is a core part of this standard. Examples of protected resources are a commercial licence (such as a licensed Sonatype installation), a paid cloud account, a private registry, or a signing key. Projects must:
- Identify which tests need a protected resource and which do not.
- Make the split explicit in the test code, using the ecosystem’s native mechanism (build tags, test categories or markers, separate test directories, or an environment flag), not only in CI configuration.
- Ensure that a contributor can run the unprotected set locally with a single documented command and no credentials.
- Ensure the protected set skips cleanly with a clear message when its credentials are absent, instead of failing.
This benefits contributors beyond CI: they can run everything that applies to them on their own machine.
Tier 2: Sonatype Lifecycle evaluation
What
Every Project must evaluate its dependencies with Sonatype Lifecycle on every Pull Request and at release (see Dependency Management). For fork Pull Requests this standard requires the evaluation to be performed by trusted code in a separate workflow from the one that builds the Pull Request.
Why this is harder than it looks
Lifecycle needs credentials. The straightforward approach is to run the scan in the same job that checks out the Pull Request, and that places untrusted code next to the credentials. Two things make a naive fix wrong:
- Switching to
pull_request_targetand checking out the PR head is the classic “pwn request” vulnerability. - Some ecosystems resolve dependencies by running the project’s own build tooling (for example, build plugins or install scripts). Pointing a privileged scan at an untrusted checkout can therefore execute untrusted code, even though nobody intended to “run” the PR.
Approach: prepare unprivileged, evaluate privileged
The work is split in two so that the privileged side never executes anything from the Pull Request:
- Prepare (untrusted, no secrets). In a Tier 1 job, produce the scan input: the files Lifecycle’s own analysis reads for that ecosystem. Depending on the ecosystem this is a lockfile, a generated dependency list, the dependencies themselves, or a built artifact. Upload it as a single workflow artifact.
- Evaluate (trusted, has secrets). A separate workflow, triggered by
workflow_run, downloads the artifact as data, checks it, and runs the standard Sonatype Lifecycle evaluation over it. It then reports the result back to the Pull Request as a check run.
The ecosystem guides say what the scan input is for each language, and the reference workflows show the complete pair.
How the hand-off works on GitHub
The automation depends on how GitHub’s events behave, which is not obvious. These are the mechanics, and the things that go wrong:
sequenceDiagram
autonumber
participant C as Contributor
participant U as Tier 1 workflow<br/>(pull_request, no secrets)
participant A as Artifact store
participant P as Tier 2 workflow<br/>(workflow_run, default branch, secrets)
participant L as Sonatype Lifecycle
C->>U: Open or update PR
U->>U: Build and prepare scan input
U->>A: Upload artifact "lifecycle-scan-input"
U-->>P: workflow_run completed (run id, head SHA)
P->>A: Download by run id, as data
P->>P: Validate size, file count, no symlinks
P->>L: Evaluate (stage: develop)
L-->>P: Policy result and report URL
P-->>C: Check run on the PR's head SHA- The Tier 1 workflow runs on
pull_request. Its last job uploads the scan input withactions/upload-artifact. - The Tier 2 workflow declares
on: workflow_runfor the Tier 1 workflow, withtypes: [completed]. GitHub runs the default branch’s copy of this workflow, with access to secrets, even when the triggering run came from a fork. - Tier 2 downloads the artifact with
actions/download-artifact, passing the triggeringrun-idand a token withactions: read, because the artifact belongs to a different run. - Tier 2 reports the result with a check run on the triggering
head_sha. Aworkflow_runworkflow’s own status is attached to the default branch, not to the Pull Request, so without a check run the contributor never sees the result. Creating the check run needschecks: write.
Things that must be handled, because each is a known way for this pattern to fail or be abused:
| Gotcha | Consequence | Required handling |
|---|---|---|
The PR number is not in the event for fork PRs. In runs we observed, workflow_run.pull_requests is empty when the PR comes from a fork. GitHub documents the same behaviour for check runs and check suites, but not explicitly for workflow_run. | Workflows that rely on it silently do nothing. | Attach the result to head_sha. If the PR number is needed, look it up from the SHA through the API. Never read it from the artifact, which the contributor controls. |
workflow_run only fires for workflow files on the default branch. | The first Tier 2 PR cannot test itself, and changes to the Tier 2 workflow are not exercised by the PR that makes them. | Merge Tier 2 first, then change it with care: it runs with secrets, so changes to it need maintainer review. |
| Tier 1 might have failed or been cancelled. | There may be no artifact, or an incomplete one. | Check the triggering run’s conclusion and the run’s event before downloading. |
| The artifact belongs to another run. | download-artifact cannot find it with defaults. | Pass run-id and github-token explicitly. |
| Branch names, titles and other event fields are attacker-controlled. | Template injection into scripts. | Use only head_sha and numeric ids from the event, and pass anything else through environment variables, never inline in run:. |
Which Lifecycle stage to use
Lifecycle policy is applied per stage, so each kind of run uses its own:
| Run | Stage | Why |
|---|---|---|
| Pull Request, or any other branch | develop | Early feedback for the people changing the code |
CI / build on main | build | The merged code, built as the Project’s pipeline builds it |
| A release (tag push, GitHub Release or manual publish) | release | The code that is being shipped |
The develop evaluation is advisory. The build and release evaluations run on trusted refs, against reviewed code, and are the ones that enforce policy.
Rules for the privileged phase
The evaluation workflow must:
- Never execute, build, install or
sourceanything from the artifact. It is data. - Take the commit SHA from the triggering event, never from files in the artifact.
- Choose the Lifecycle application ID itself (the repository name), and never take it from the artifact.
- Check the artifact before use: a size limit, a file-count limit, and no symbolic links.
- Have no
actions/checkoutof the Pull Request’s head. - Use credentials for a dedicated Lifecycle account with the least privilege that can perform an evaluation, and with no ability to read or change other applications or policies.
- Evaluate at the
developstage for Pull Requests and other branches, against the Project’s own Lifecycle application (see Which Lifecycle stage to use).
What a PR-time result means
The contributor controls what goes into the artifact. A hostile Pull Request could therefore leave a vulnerable dependency out of the scan input and pass. This is true of any scheme where untrusted code prepares the input. It follows that:
- The PR-time result is advisory. It gives contributors early, accurate feedback on honest changes, and it catches most mistakes.
- The authoritative evaluations run on trusted refs (see Trusted refs), against the real checkout of reviewed, merged code: at the
buildstage for CI onmain, and at thereleasestage for a release. Those are the evaluations that enforce policy. - Maintainers reviewing a PR still look at changes to dependency manifests. That is what the
CODEOWNERSguidance below is for.
The remaining exposure
The trusted job runs the Sonatype CLI over files that a contributor produced, while Lifecycle credentials are present. This standard relies on the Sonatype CLI not running build tools, package managers or project code. It does have to read and process the files it is given, so a flaw in that processing, or a deliberately awkward input, is a theoretical route to the credentials. This is a much narrower exposure than running contributor code, and it is reduced by: a dedicated job with no checkout, the size and file-count limits above, a throwaway runner, and an evaluate-only account for a single application. It is not eliminated.
Reporting
The result should appear on the Pull Request as a check run, with a link to the Lifecycle report where the contributor can see it. Contributors should not need a Sonatype account to understand what to fix.
Tier 3: Protected-resource integration tests
What
Tests that need a protected resource run only when a maintainer has approved the specific commit under test, or when the code is already trusted (see Trusted refs).
There can be more than one Tier 3 workflow. A Project with a clustered or high-availability configuration that needs a protected resource, for example, runs that as another Tier 3 workflow, behind the same approval. All of them are required checks (see Required status checks).
A protected resource is anything the Project cannot let arbitrary code use freely: a commercial licence, a paid or quota-limited service, a private registry or environment, or credentials with a wide blast radius. For many of our Projects the protected resource is a licensed Sonatype installation, such as a licensed Nexus Repository or Lifecycle instance. That is an example, not a definition. A Project with no such tests does not have a Tier 3, and does not need a gate.
Why
These are the most sensitive credentials we hold, and the tests are the code a hostile contributor would most want to modify. A human review is the one control that reliably addresses that.
Part A: The approval gate
Projects must gate Tier 3 behind an explicit maintainer action, and the gate must meet all of these conditions:
- It is explicit. A maintainer approves with a label or a comment command, after reading the diff (see Who can approve and Approval mechanisms).
- It has no way round. Tier 3 workflows must not offer a manual start (
workflow_dispatch) that takes a commit or branch, because that would let anyone with Maintain run unreviewed code with the protected resource, bypassing the check on the commit. The only ways in are an approval of one exact commit, or a push to a protected ref. - It is bound to a commit. The workflow records the approved commit SHA and runs exactly that commit. If the Pull Request is updated, approval is revoked automatically and a maintainer must approve again.
- It does not trust history. Having a previous merged Pull Request is not grounds for automatic approval. Accounts get compromised.
- It may auto-approve maintainers. Pull Requests from members of the Project’s maintainer team may skip the gate, as their branches live in the main repository.
- Reviewers know what to look for. Changes to CI configuration, build files, dependency manifests (including module replacement or override directives), test helpers and scripts are the most common ways to extract secrets. Projects should list these in
CODEOWNERSso that a maintainer is always prompted to review them. See Repository and organization settings.
Who can approve
Only a person with Maintain permission or higher on the repository may approve Tier 3 for a Pull Request. This is the Project’s maintainers team, @sonatype-nexus-community/[PROJECT], which is also its CODEOWNERS (see Project Maintainer and Standard Files). Each Project must have that team, with the Maintain role on the repository, and must list it in CODEOWNERS. Membership of the team, with no individual grant, is enough for the check below to see the Maintain role.
The approval workflow must check this itself. GitHub lets anyone with Triage permission apply a label, so a label is not proof of authority. For the person who applied the label or wrote the comment, look up their role with the repository collaborator permission API and read role_name. Do not use the permission field, which reports Maintain as write and so cannot tell Maintain from Write. An approval from anyone below Maintain is ignored, and a label they applied is removed.
Approval mechanisms
A Project may use either of these, and both must meet Who can approve and the commit-binding rule above. The approval belongs to a commit, not to a Pull Request: it is recorded as a commit status on that exact commit, so a Pull Request that is updated has no approval for its new commit until a maintainer gives one.
| Mechanism | How it works | Strength | Weakness |
|---|---|---|---|
Label (for example approved-to-test) | A maintainer applies it. The workflow records the head commit at that moment, and removes the label whenever new commits are pushed. | Visible on the Pull Request. No typing. | The recorded commit is the one at the moment of labelling, which can differ from the one the maintainer read if the contributor pushed in between. |
Comment command (for example /ok-to-test <short-sha>) | A maintainer comments with the commit they reviewed. The workflow refuses if the head commit is different. | The approval names exactly what was read. Easy to audit. | The workflow has to parse the comment. |
Where a Project can choose, the comment command is preferred, because the maintainer states which commit they reviewed.
A Project may additionally keep the protected resource’s secrets in a GitHub Environment with required reviewers. GitHub then withholds the secrets from a run until a reviewer approves it, whatever our workflow logic does. That makes it a backstop for a mistake in the approval workflow. It costs a click on every run, including re-runs, so it is optional rather than part of the standard.
Part B: Don’t give the protected resource to the test code
Even after review, mistakes happen. The strongest protection is that the code under test never holds the real credential. The preferred design (target state):
- A trusted job, running only default-branch code, provisions an ephemeral instance of the protected resource (for example a container, VM or cloud sandbox) and injects the licence or credential there.
- It passes the test job only the instance’s address and credentials generated for that run.
- The test job, which runs the contributor’s code, has no organizational secrets. If it leaks what it was given, the leak is a throwaway instance that is destroyed when the run ends.
Projects that cannot yet do this may run protected tests with real secrets after the approval gate, as an interim step. When they do, they must:
- Use secrets scoped to the repository or to a protected Environment, never organization-wide secrets available to all repositories.
- Use credentials and licences that exist only for CI, are time-limited and are rotated on a schedule.
- Keep the protected job’s permissions minimal.
How this could be realised
“Don’t give the protected resource to the test code” can be built in stages. A Project can stop at any stage, and should say which one it has reached.
- Gated, with scoped secrets. The approval gate above, with the secrets held at repository or Environment scope. This is the interim approach and needs no new infrastructure. The test code can still reach the secrets once it runs, which is why it is only acceptable behind the gate.
- Hardening inside the job. Keep the licence file out of the workspace, give it to the service rather than the tests, and remove it once the service has started. This reduces accidents. It is not separation: on a single runner the tests and the service run on the same machine, and the test process can normally reach the service’s files and environment. Treat it as hardening.
- A separate machine for the protected resource. A trusted job creates the instance of the protected resource somewhere other than the runner that executes the tests. For example, a short-lived container or virtual machine in a cloud account the community controls. It generates credentials for that run only, and passes the test job just the address and those credentials. Whatever the tests do, the licence never reaches them. A step that always runs at the end destroys the instance, and a scheduled clean-up removes any that leak.
Stage 3 needs decisions that are not technical: where the instances run, whose account and budget pays for them, and whether the licence terms allow short-lived instances. A Project that tests against many product versions needs one instance for each version in each run, so the cost grows with the size of the test matrix.
Trusted refs, releases and tags
Projects release in different ways: by merging to main, by pushing a version tag, by publishing a GitHub Release, or through a manual dispatch. This standard does not dictate which. It defines what makes a trigger trusted.
A ref is trusted when only maintainers can create or change it. On a trusted ref, the code has been reviewed and merged, so the authoritative Tier 2 evaluations (build stage for CI on main, release stage for a release, against the real checkout) and Tier 3 may run directly with full access to their secrets, exactly as described in Dependency Management.
| Release style | Why it is trusted | Required controls |
|---|---|---|
Merge to main | Branch protection requires reviewed PRs | Branch ruleset on main |
| Tag pushed manually | Only maintainers may create tags | Tag ruleset restricting creation of release tags (for example v*) to maintainers; many Projects forget this and any writer can create tags by default |
GitHub Release / workflow_dispatch | Only users with write access can trigger | Environment with required reviewers for publishing |
In all cases:
- Publishing credentials (package registries, signing keys) must be Environment secrets with deployment rules restricted to the trusted refs. A feature branch, or an intermediate branch, must not be able to read them.
- Release workflows must not be triggered by a Pull Request event.
- Whatever the trigger, the release workflow should perform the
release-stage Lifecycle evaluation and the full test set, including Tier 3 where the Project has one. CI onmainperforms thebuild-stage evaluation.
Intermediate branches
An intermediate branch is a branch in the main repository that holds a copy of a fork Pull Request’s commit, so that it can be tested by the same push-triggered workflows used for trusted code.
Why consider them
- Some Projects already have a Lifecycle and protected-test workflow for
pushevents, which would need changes to support the Tier 2 / Tier 3 split. - They give maintainers a simple “break-glass” way to run Tier 3 for a PR they have reviewed.
- It lets you apply the trusted-ref controls (protected Environments with branch rules) to a PR.
Why handle them carefully
- A
pushto any branch that matches a workflow trigger will run that workflow with the secrets that trigger can see. If anything other than a human maintainer can create these branches, you have rebuilt the vulnerability. - The branch’s content still came from a contributor. The workflow file on that branch is contributor-controlled, and
pushworkflows run the version on the pushed branch.
Conditions for use
If a Project uses intermediate branches, it must:
- Use a reserved naming scheme, such as
pr/<number>, and a branch ruleset so that only maintainers (or a tightly scoped automation) can create or updatepr/*branches. - Publish only a commit a maintainer has reviewed, recording the reviewed SHA. Mirroring is a result of approval, not a substitute for it.
- Ensure secrets are held in an Environment whose deployment branches are limited to an explicit pattern, and not exposed to
pr/*unless the Project has deliberately decided so. - Prevent the
pr/*branches from triggering release or publishing workflows. - Delete the branch when the Pull Request is closed or merged.
- Make changes to
.github/in a mirrored commit a blocking reason to refuse the mirror, or make the workflow definition come from the default branch instead (for example by calling a reusable workflow at a pinned reference).
Intermediate branches are an optional, break-glass mechanism, not the normal route. The approval gate described in Part A is the standard way to run Tier 3 for a Pull Request and needs fewer moving parts. Use an intermediate branch only when a maintainer has a specific reason, for example a Project whose existing trusted push workflows cannot yet be split.
Repository and organization settings
Actions settings
Each Project must set, under Settings -> Actions -> General:
- Fork pull request workflows from outside collaborators: require approval (at minimum for first-time contributors).
- Send write tokens / secrets to workflows from fork pull requests: disabled.
- Workflow permissions: default to read-only.
Secrets
- Secrets used by Tier 2 and Tier 3 should be repository- or Environment-scoped, not organization-wide. An organization secret is available to every repository allowed to use it, so a mistake in one repository exposes the secret for all of them.
- Environments holding the most sensitive secrets should use deployment branch/tag rules and, where useful, required reviewers.
- Credentials must be dedicated to CI, narrowly permissioned and rotated on a schedule, and the process for emergency revocation must be documented for the Project’s maintainers.
CODEOWNERS
Following CODEOWNERS, Projects should list the files that are most useful to an attacker and so deserve close review, including:
.github/(workflows and actions)- Build and dependency definitions (for example
Makefile,pom.xml,build.gradle,package.json,go.mod) - Test helpers, fixtures and scripts that Tier 3 runs
Workflow hygiene
All Projects should follow these practices in every workflow, as they make the tiers above safer:
- Set
permissions: {}at the top of the workflow and grant extra permissions per job. - Pin third-party actions to a full commit SHA, with the version in a trailing comment.
- Never insert untrusted values (PR titles, branch names, commit messages) directly into
run:scripts. Pass them as environment variables. - Use
persist-credentials: falseonactions/checkout. - Avoid
pull_request_targetunless the job does not check out or run contributor code. - Do not use self-hosted runners for Pull Request workflows from forks.
- Run a workflow linter and a security scanner such as actionlint and zizmor in CI. They detect the specific mistakes this page warns about.
Contributor experience
The aim is that a contributor is never confused about why a check isn’t running, and never blocked by something they could have fixed themselves.
- Tier 1 and 2 results arrive on their own. No approval is needed.
- Tier 3 explains itself. A bot comment or check summary should say that protected-resource tests need a maintainer to approve the change, why this is the case, and roughly how long it takes.
- Contributors can test locally. The
CONTRIBUTINGguide of each Project should explain how to run the unprotected tests locally, for example with a Compose file that starts Nexus Repository Community Edition. - Contributors can bring their own access. A contributor with their own trial licence or account can run the protected tests in their own fork with their own secrets and link the run to the Pull Request. This is optional and useful evidence for maintainers, but never a requirement.
- Maintainers have a response expectation. Projects should aim to review approval requests promptly and say so in their contributor docs.
What Projects must, should and must not do
Must
- Have a maintainers team for the Project, with the Maintain role, named in
CODEOWNERS. - Require every tier the Project has as a status check before merging.
- Run Tier 1 on every Pull Request without secrets.
- Separate tests that need a protected resource from those that do not, in the test code itself.
- Perform Lifecycle evaluation for fork PRs only through trusted code that treats the prepared scan input as data.
- Run the authoritative Lifecycle evaluation on trusted refs.
- Gate protected-resource tests behind a commit-bound maintainer approval.
- Restrict who can create release tags and publishing refs.
- Disable sending secrets to fork PR workflows.
Should
- Scope secrets to repository or Environment rather than the whole organization.
- Use ephemeral, per-run credentials for protected resources.
- Pin actions, restrict token permissions, lint workflows.
- Provide a local way to run unprotected tests.
Must not
- Check out and execute a fork’s code in a job that can read secrets.
- Rely on log masking as a protection.
- Auto-approve a contributor because of their earlier contributions.
- Allow Pull Request events to trigger release or publish workflows.
- Treat the PR-time Lifecycle result as proof that a Pull Request is safe to merge.
Adoption and ratification
This standard moves through two states.
- Draft (now). Once this page is published, the standard is available for adoption. Projects may adopt it early, and early adopters are how we find what does not work. A Project that adopts it should follow it in full, and tell us what it learned (see Your tool or ecosystem is not covered).
- Ratified. Once the standard is ratified it is no longer optional. Every Project must adopt the latest ratified version the next time it is updated, developed or released. A Project does not have to stop and retrofit on the day of ratification, but it cannot ship its next change without bringing its Pull Request CI into line.
The first real-world trial is terraform-provider-sonatyperepo. What we learn there, including anything in these workflows that does not work as written, feeds back into this draft before it is ratified.
Your tool or ecosystem is not covered
The ecosystem guides cover the ecosystems our Projects use today. If yours is not there, or your tooling does something these guides do not allow for, engage with us rather than working around the standard:
- Ask a question or propose an approach in GitHub Discussions.
- Report a gap, or suggest a change to a guide, as a GitHub Issue.
Please say which Project and which ecosystem or tool it concerns. We add guides as Projects need them. Sonatype Lifecycle supports more ecosystems than we have guides for, so a missing guide usually means nobody has asked yet, not that it cannot be done.
While a guide is missing, the principles still apply: prepare the files Lifecycle reads for that ecosystem in a job with no secrets, and let a separate trusted job evaluate them as data.
Decisions recorded
These were considered during drafting. They are recorded so reviewers can see why, and challenge them with evidence.
Scan input is native files, not a generated SBOM
A Software Bill of Materials was the first candidate for the data that crosses the boundary, because it is a standard, inert format. We chose against it as the default.
- What an SBOM would add. A uniform format. It would not add any extra separation, because the boundary is held by “only data crosses”, which is equally true of a lockfile or a built archive.
- What it would cost. Someone has to generate it accurately. Lifecycle resolves dependencies as part of its own analysis, and that is where its accuracy comes from. A third-party generator reproduces that resolution with varying fidelity per ecosystem. Sonatype’s own documentation also lists limits on SBOM ingestion, including that nested components are ignored and that a repeated component keeps only its first entry.
- Generating it with Sonatype’s own tooling does not remove the problem. The Sonatype CLI can produce an accurate SBOM, but only by running with Lifecycle credentials against a server, using a temporary application. Run on the untrusted side, that puts credentials next to contributor code. Run on the trusted side, the SBOM adds nothing, because the same step could have been the evaluation. It also needs an account permitted to create and delete applications, which is more privilege than an evaluation needs.
- What we do instead. The untrusted side prepares the files Lifecycle already reads for that ecosystem, and the trusted side lets Lifecycle do the resolving.
An SBOM remains acceptable for a Project whose ecosystem Lifecycle cannot analyse from prepared files. The reference SBOM that Lifecycle can export from a trusted-ref evaluation is also useful, to measure the difference between any alternative generator and Lifecycle’s own result. We will revisit this decision if the native approach hits a blocking limit.
PR-time evaluation is advisory
See What a PR-time result means.
Pull Requests are evaluated at the develop stage, against the real application
Pull Request evaluations go to the Project’s own Lifecycle application (named after the repository), at the develop stage.
- One application, not a separate one for Pull Requests. We considered a separate application to keep unreviewed Pull Requests out of a Project’s results, and chose against it. Contributors then see the policy the Project actually uses, and the history stays in one place.
- Why
develop. It is the stage for early feedback on Pull Requests and branches. CI onmainusesbuild, and a release usesrelease. - Consequences. The Tier 2 account needs permission to evaluate that one application. The existing Dependency Management page, whose Continuous Integration example uses the
buildstage for every run, will need updating to match when the standard is ratified.
The Sonatype CLI does not run build tools
The design relies on the Sonatype CLI only reading the files it is given: it does not run build tools, package managers or project code. That is why the Tier 2 job can run it over a contributor’s files. See the remaining exposure.
Reusable workflows live in a central Community repository
Where it is sane and safe to do so, the trusted workflow logic (the Tier 2 evaluation, the Tier 3 approval gate, and shared preparation steps) will be provided as reusable workflows in a new central Community repository. It is then hardened and audited once, and each Project calls it with a few lines.
“Sane and safe” means:
- Callers pin the reusable workflow to a commit SHA.
- The central repository has tight access control,
CODEOWNERSand required reviews. - Each Project’s own workflow still defines its own triggers. A reusable workflow does not decide when it runs.
The reference workflows then become the pattern for the callers. The repository’s name and home are still to be decided.
Open questions for review
We want feedback from maintainers, contributors and Sonatype on these before the standard is ratified.
- Protected resources created for each test run. Part B of Tier 3 describes creating a throwaway copy of the protected resource for every test run, so that the contributor’s test code never sees a real credential. It is the safest design, but no Project has built it yet. What would create and destroy the copies, where they would run, what licence they would use, and who would pay are all undecided. How this could be realised sets out the stages. Until it exists, Projects use the interim approach.
- Approval mechanism. The standard allows a label or a comment command, restricted to the people named in Who can approve, and is considering whether to also use a protected Environment. See Approval mechanisms.
Glossary
| Term | Meaning |
|---|---|
| Fork PR | A Pull Request from a contributor’s own copy of the repository. |
| Untrusted code | Any code a person outside the maintainers can change, including code in a PR. |
| Trusted code | Code on a protected default branch or protected tag. |
| Protected resource | Something the Project cannot let arbitrary code use freely, such as a licence, a paid service, or powerful credentials. |
| Scan input | The prepared files, produced by Tier 1, that the Tier 2 Lifecycle evaluation reads. |
pull_request | Runs the PR’s code with a read-only token and no secrets when it comes from a fork. |
pull_request_target | Runs the default branch’s workflow with secrets. Safe only if it never runs the PR’s code. |
workflow_run | Runs a workflow (from the default branch) after another workflow completes. Used to pass data from the unprivileged to the privileged side. |
| Check run | A status attached to a commit, shown on the Pull Request. How Tier 2 reports back. |
| Environment | A GitHub setting that holds secrets together with rules for who and what may use them. |
| Ephemeral instance | A temporary installation of a protected resource created for one test run and destroyed afterwards. |
| Intermediate branch | A branch in the main repository holding a reviewed copy of a PR commit. |
7.1 - Reference Workflows
DRAFT. These workflows implement the Pull Request CI & Secrets standard. Every snippet has been checked to parse as YAML. They have not been linted with actionlint or zizmor, and have not been run end to end in GitHub Actions. Treat them as a design to review, not as tested code.
Contents
- How the pieces fit
- Tier 1: the CI workflow
- Tier 2: the Lifecycle evaluation workflow
- Trusted refs: the authoritative evaluation
- Repository configuration
- Why each part is the way it is
How the pieces fit
| File | Trigger | Has secrets? | Runs PR code? | Job |
|---|---|---|---|---|
.github/workflows/ci.yaml | pull_request, push to main | No | Yes | Build, test, and produce the scan input |
.github/workflows/lifecycle-evaluate.yaml | workflow_run of CI | Yes | No | Evaluate the scan input and report |
.github/workflows/lifecycle-release.yaml | push to main or release tags | Yes | Only reviewed, merged code | Authoritative evaluation |
The name of the first workflow (CI) is referenced by the second, so the two must agree.
Pick your language from the ecosystem guides and put its prepare-scan-input job in ci.yaml. Nothing else in these workflows changes between languages, except where a guide says to set a repository variable.
Tier 1: the CI workflow
This is the skeleton. The build-and-test job stands for your Project’s own build and tests. The prepare-scan-input job comes from the ecosystem guide; the Go version is shown as a worked example.
# .github/workflows/ci.yaml
name: CI
on:
pull_request:
push:
branches: [main]
# No permissions by default. Each job asks only for what it needs.
permissions: {}
jobs:
build-and-test:
name: Build and test
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# ... your build, lint and unprotected tests here ...
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# --- Ecosystem-specific: replace this block with your guide's steps ---
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- name: Prepare scan input
run: |
mkdir scan-input
go list -f '{{define "M"}}{{.Path}} {{.Version}}{{end}}{{with .Module}}
{{if not .Main}}{{if .Replace}}{{template "M" .Replace}}
{{else}}{{template "M" .}}{{end}}{{end}}{{end}}' -compiled -deps ./... | sort -u > scan-input/go.list
cp go.mod scan-input/
# --- End ecosystem-specific ---
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
Requirements for the prepare-scan-input job, whatever the language:
- It must have no secrets and no write permissions.
- It must put everything it wants evaluated in a directory named
scan-input, and upload it as an artifact namedlifecycle-scan-input. - It must not put anything in the artifact that is not a file Lifecycle reads for that ecosystem.
- It must fail if there is nothing to upload (
if-no-files-found: error), so a broken preparation step is visible.
Tier 2: the Lifecycle evaluation workflow
This workflow runs with secrets, so it is the sensitive part of the design. It never checks out the Pull Request, and it never runs anything it downloads.
# .github/workflows/lifecycle-evaluate.yaml
name: Lifecycle Evaluate
on:
workflow_run:
workflows: [CI]
types: [completed]
permissions: {}
jobs:
evaluate:
name: Evaluate scan input
# Only for pull requests, and only when Tier 1 succeeded. Trusted refs are
# handled by lifecycle-release.yaml instead.
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
actions: read # download the artifact from another run
checks: write # report the result on the pull request
env:
# Limits on the downloaded data. Override with repository variables.
MAX_BYTES: ${{ vars.LIFECYCLE_SCAN_MAX_BYTES || 536870912 }}
MAX_FILES: ${{ vars.LIFECYCLE_SCAN_MAX_FILES || 50000 }}
steps:
- name: Download scan input (as data)
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: lifecycle-scan-input
path: scan-input
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Validate scan input
run: |
set -euo pipefail
bytes=$(du -sb scan-input | cut -f1)
files=$(find scan-input -type f | wc -l)
if [ "$bytes" -gt "$MAX_BYTES" ]; then
echo "::error::Scan input is $bytes bytes; the limit is $MAX_BYTES"
exit 1
fi
if [ "$files" -gt "$MAX_FILES" ]; then
echo "::error::Scan input has $files files; the limit is $MAX_FILES"
exit 1
fi
if [ -n "$(find scan-input ! -type f ! -type d -print -quit)" ]; then
echo "::error::Scan input contains something other than regular files and directories"
exit 1
fi
- name: Sonatype Lifecycle Evaluation
id: evaluate
# Do not fail here, so that the result is always reported below.
continue-on-error: true
uses: sonatype/actions/evaluate@b1c823a70e98d3c67478cd72c0128712623455b9 # v1.14.1
with:
iq-server-url: ${{ vars.SONATYPE_PLATFORM_URL }}
username: ${{ secrets.SONATYPE_LIFECYCLE_USERNAME }}
password: ${{ secrets.SONATYPE_LIFECYCLE_PASSWORD }}
# Chosen here, from trusted values. Never read from the artifact.
application-id: ${{ github.event.repository.name }}
scan-targets: ${{ vars.LIFECYCLE_SCAN_TARGETS || 'scan-input' }}
stage: develop # Pull Requests use the develop stage, see the standard
- name: Report result on the pull request
if: ${{ !cancelled() }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
EVALUATE_OUTCOME: ${{ steps.evaluate.outcome }}
REPORT_URL: ${{ steps.evaluate.outputs.report-url }}
with:
script: |
const outcome = process.env.EVALUATE_OUTCOME;
const passed = outcome === 'success';
const summaries = {
success: 'The evaluation passed.',
skipped: 'The scan input was missing or was rejected by validation, so nothing was evaluated.',
};
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'Sonatype Lifecycle (pull request, advisory)',
head_sha: process.env.HEAD_SHA,
status: 'completed',
conclusion: passed ? 'success' : 'failure',
details_url: process.env.REPORT_URL || undefined,
output: {
title: passed ? 'Passed' : 'Needs attention',
summary: summaries[outcome] ??
'The evaluation reported policy violations or did not complete. Open the report for details.',
},
});
What Tier 2 deliberately does not do
- It has no
actions/checkout. The only thing from the Pull Request on the runner is the downloaded artifact. - It does not read the Pull Request number, branch name, title or any file contents to decide what to do. It uses
head_sha, which GitHub sets, and the repository name, which is the Project’s own. - Tier 1 must have succeeded before Tier 2 runs. A contributor whose tests are failing fixes them first. A Project that wants a Lifecycle result even when tests fail can run the
prepare-scan-inputjob in a separate workflow, and pointworkflows:at that one.
Trusted refs: the authoritative evaluation
On a trusted ref (see Trusted refs, releases and tags) the code is reviewed and merged, so the Project can scan the real checkout directly, with secrets, as described in Dependency Management. This evaluation, and not the advisory Pull Request one, is what enforces policy.
# .github/workflows/lifecycle-release.yaml
name: Lifecycle Release Evaluation
on:
push:
branches: [main]
tags: ['v*'] # adjust to your release convention
permissions: {}
jobs:
evaluate:
name: Evaluate (build on main, release on a tag)
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# ... build the project here if your ecosystem is scanned from build output ...
- name: Sonatype Lifecycle Evaluation
uses: sonatype/actions/evaluate@b1c823a70e98d3c67478cd72c0128712623455b9 # v1.14.1
with:
iq-server-url: ${{ vars.SONATYPE_PLATFORM_URL }}
username: ${{ secrets.SONATYPE_LIFECYCLE_USERNAME }}
password: ${{ secrets.SONATYPE_LIFECYCLE_PASSWORD }}
application-id: ${{ github.event.repository.name }}
scan-targets: '.' # or the build output, per the ecosystem guide
# Tags are releases; a push to main is CI. See "Which Lifecycle stage to use".
stage: ${{ startsWith(github.ref, 'refs/tags/') && 'release' || 'build' }}
This workflow is only safe if the refs that trigger it are protected. A branch ruleset on main and a tag ruleset on the release tags are required, as described in the standard. If a Project releases some other way (a manual dispatch, a published GitHub Release), change the on: block to match and keep the protection requirement.
Repository configuration
For these workflows to work as designed, the repository needs:
| Setting | Value | Why |
|---|---|---|
Variable SONATYPE_PLATFORM_URL | The Lifecycle URL | Not secret, but only Tier 2 and the release workflow read it |
Secrets SONATYPE_LIFECYCLE_USERNAME and SONATYPE_LIFECYCLE_PASSWORD | A dedicated, evaluate-only account | Least privilege; see the standard |
| Where those secrets live | Repository or Environment scope, not organization | A mistake then affects one repository |
| Actions setting: send secrets to fork PR workflows | Disabled | Tier 1 must not receive them |
| Actions setting: approval for fork PR workflows | Required for first-time contributors | Limits abuse of runner time |
Optional variable LIFECYCLE_SCAN_TARGETS | A path under scan-input | For ecosystems whose target is a single file (for example Docker) |
Optional variables LIFECYCLE_SCAN_MAX_BYTES, LIFECYCLE_SCAN_MAX_FILES | Larger limits | For ecosystems with big inputs (for example Docker, node_modules) |
The lifecycle-evaluate.yaml workflow must be on the default branch before it takes effect, and changes to it, to ci.yaml’s prepare-scan-input job, and to anything under .github/ should be listed in CODEOWNERS.
Why each part is the way it is
| Choice | Reason |
|---|---|
| Two workflows, not one | The first runs contributor code and has no secrets; the second has secrets and runs none. This is the whole design. |
workflow_run, not pull_request_target | workflow_run runs the default branch’s copy of the workflow and never checks out the Pull Request by default, so there is no code from the PR to run. |
permissions: {} at the top | Nothing gets a permission it did not ask for. |
| Actions pinned to a commit SHA | A tag can be moved; a commit cannot. The version is kept in a trailing comment so updates are easy to review. |
persist-credentials: false | Checkout does not leave the token in .git where later steps could read it. |
| Limits and the file-type check in Tier 2 | The downloaded data is untrusted. A very large or oddly-shaped input is rejected before anything parses it. |
continue-on-error then a check run | A failed evaluation is still reported on the PR. Without the check run, a workflow_run result is attached to the default branch and the contributor never sees it. |
application-id from the repository name | It comes from a value the contributor cannot change, and it matches the existing Dependency Management convention. |
| Trusted-ref workflow separate from Tier 2 | The advisory and authoritative results are different things and should not share a name, a trigger or a failure mode. |
7.2 - Ecosystem Guides
DRAFT. These guides are drawn from Sonatype’s published analysis documentation. The snippets have not been run end to end, and where Sonatype’s documentation is silent the guide says so, rather than guessing.
What these guides are
Under the Pull Request CI & Secrets standard, a Tier 1 job (no secrets) prepares a scan input, and a separate Tier 2 job (with secrets) hands it to Sonatype Lifecycle. Which files Lifecycle can accurately evaluate depends on the language. These guides record, for each one:
- What Lifecycle reads, and so what the scan input must contain.
- A
prepare-scan-inputjob to paste into yourci.yaml(see Reference Workflows). - What accuracy you give up compared with scanning a full build, so that nobody is surprised.
The Tier 2 workflow is the same for every language. Only the preparation differs.
Two kinds of ecosystem
| Kind | Examples | What the scan input is | Accuracy |
|---|---|---|---|
| Manifest | Go, Rust, PHP, Ruby, Python, .NET, R | A lockfile or a dependency list, produced by the language’s own tooling | Good, if the lockfile or list pins exact versions. Transitive dependencies depend on what the file contains. |
| Binary | Java, npm with node_modules, Docker | The built artifact, or the installed dependencies | Best. Lifecycle fingerprints the files themselves, which also finds modified or repackaged dependencies. |
Binary inputs are large. Allow for that in the artifact limits (see Reference Workflows).
The guides
| Ecosystem | Used by | Guide |
|---|---|---|
| Go | Nancy, Cheque, Terraform providers, API clients | Go |
| Java (Maven, Gradle) | Sherlock Trunks, Nexus CasC Plugin, API clients | Java |
| JavaScript (npm, Yarn) | AuditJS, API clients | JavaScript |
| Python | Jake, API clients | Python |
| .NET (NuGet) | No current Project | .NET |
| Rust (Cargo) | Cargo Pants | Rust |
| Ruby (Bundler) | Chelsea | Ruby |
| PHP (Composer) | Bach | PHP |
| R (CRAN) | OysterR | R |
| C/C++ (Conan) | No current Project | C/C++ |
| Docker images | Container image Projects | Docker |
Terraform providers are written in Go, so their Lifecycle scan is the Go guide.
Not yet covered: Dart and Flutter, Swift, Objective-C, Conda, RPM, and Hugging Face. Sonatype supports these. If your Project needs one of them, or uses a tool or ecosystem not listed at all, see Your tool or ecosystem is not covered.
Adding a guide
Each guide follows the same shape so they can be compared: what Lifecycle reads, the prepare-scan-input job, what you give up, and a link to the Sonatype page it came from. Quote Sonatype’s documentation, and say “not documented” where it is silent.
7.2.1 - .NET
DRAFT. Not yet run end to end. No current community Project uses .NET, so this guide is the least exercised. Source: Sonatype’s NuGet Application Analysis.
What Lifecycle reads
Sonatype’s documentation says Lifecycle scans .nupkg files and PE binaries (.dll, .exe and others), as a binary scan, and project files (.csproj, packages.config) as a manifest scan. In manifest mode “only components with an exact version specified are evaluated”, floating versions such as 3.6.* are not supported, and transitive dependencies are not included.
Running dotnet restore is not required, but it is recommended for a more complete analysis. Restoring into a directory (dotnet restore --packages packages) gives a folder of packages to scan.
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Restore packages
run: |
mkdir scan-input
dotnet restore --packages scan-input/packages
find . -name '*.csproj' -not -path './scan-input/*' -exec cp --parents {} scan-input/ \;
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
dotnet restore can run MSBuild targets from the project, which is code from the Pull Request. That is acceptable in Tier 1 and is why it must not run in Tier 2.
Tier 2
Unchanged from the reference workflow.
Trusted-ref evaluation
Restore and scan the same way against the real checkout, at stage: build on main and stage: release for a release. Scanning the published output (dotnet publish) is the closest equivalent of the Java “deployed artifact” advice, but Sonatype’s page does not state this for .NET.
What you give up
- Manifest mode skips transitive dependencies. Restoring into a package folder avoids this.
- Floating versions are not evaluated. Pin exact versions.
.msifiles are not supported.- Advisory only at PR time, as for every ecosystem.
7.2.2 - C/C++
DRAFT. Not yet run end to end. No current community Project uses Conan, so this guide is the least exercised. Source: Sonatype’s C/C++ Application Analysis.
What Lifecycle reads
Sonatype’s documentation says it analyses files named conanfile.txt, conanfile.py or conaninfo.txt, taking dependencies “with an exact version” from the requires and build_requires sections. It describes a scan of “a directory or subdirectories containing” those files. It does not describe a lockfile or a binary scan, and does not say that conan install is needed first.
Note that the Conan support is for the dependencies declared in those files. C and C++ code that is vendored into a repository, or fetched some other way, is not covered.
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Gather scan input
run: |
mkdir scan-input
for f in conanfile.txt conanfile.py conaninfo.txt; do
[ -f "$f" ] && cp "$f" scan-input/
done
test -n "$(ls -A scan-input)"
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
This job runs nothing from the Pull Request. The final test makes the job fail if none of the files exist, because an empty directory would upload nothing.
Tier 2
Unchanged from the reference workflow.
Trusted-ref evaluation
Scan the real Conan files at stage: build on main and stage: release for a release.
What you give up
conanfile.pyis code. Lifecycle reads it as text and does not run it, but be aware that it is a Python file and that reviewers should treat changes to it as code changes.- Exact versions only. Version ranges are not evaluated.
- No binary scan is documented.
- Advisory only at PR time, as for every ecosystem.
7.2.3 - Docker
DRAFT. Not yet run end to end. Container images are the largest scan input, and the one with the most open questions. Source: Sonatype’s Docker Image Analysis.
What Lifecycle reads
Docker is a binary ecosystem. Sonatype’s documentation describes the scan target as an image saved to a tar archive with docker save, which the CLI then accepts directly: for example docker save webgoat/webgoat-7.1:latest > webgoat-7.1.tar. It says the Sonatype CLI “relies on the Docker daemon to package the image as an archive file”.
That is helpful for this standard. If the Tier 1 job builds the image and saves the tar, the Tier 2 job has no need for a Docker daemon, because the CLI is given a file. It also means the Tier 2 job never pulls an image from a registry, so it never needs registry credentials.
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build image
run: docker build --tag scan-candidate:pr .
- name: Save image
run: |
mkdir scan-input
docker save scan-candidate:pr --output scan-input/image.tar
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
A docker build runs the Dockerfile’s instructions, which is code from the Pull Request. That is acceptable in Tier 1.
Tier 2
Two changes from the reference workflow:
- Set the repository variable
LIFECYCLE_SCAN_TARGETStoscan-input/image.tar, so that the scan target is the single archive. - Raise
LIFECYCLE_SCAN_MAX_BYTES. Images are routinely hundreds of megabytes to gigabytes, and the default of 512 MB will reject many.
An image archive is large and contains whole filesystems, so also see the remaining exposure.
Trusted-ref evaluation
Build the image from the real checkout, save it, and scan the archive at stage: build on main and stage: release for a release. A Project that publishes the image should scan the same image it publishes.
What you give up
- Time and storage. An image artifact is expensive to upload and download. Keep
retention-daysshort, and consider building a smaller image for the scan if your Dockerfile has a multi-stage build with a test stage. - Base image drift. The result reflects the base image as pulled at build time, which can change between runs.
- Platforms.
docker saveof a multi-platform build saves one platform. Choose the one you ship, or scan each. - Advisory only at PR time, as for every ecosystem. In particular, a Dockerfile change that adds a layer is exactly what a hostile Pull Request might try, and the image contents are what get scanned, which is a mitigating factor.
7.2.4 - Go
DRAFT. Not yet run end to end. Source: Sonatype’s Go Application Analysis.
What Lifecycle reads
Go is a manifest ecosystem. Lifecycle matches Go components by module path and version. Sonatype’s documentation says:
- A valid
go.modis required for Go module analysis. - The recommended scan target is a
go.listfile generated withgo list. go.sumis also supported, but “may include dependencies that are not used by the application”, so it can report more than your build actually uses.- The Go toolchain does not need to be present when Lifecycle scans the file.
Why go.list and not go.sum
go.sum records every module version your module graph has ever needed, including ones the build does not use. go.list is produced from what the compiler would actually link, so it matches your real dependencies. The tradeoff is that producing it needs the Go toolchain and module downloads, which is why it happens in Tier 1.
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
- name: Generate go.list
run: |
mkdir scan-input
go list -f '{{define "M"}}{{.Path}} {{.Version}}{{end}}{{with .Module}}
{{if not .Main}}{{if .Replace}}{{template "M" .Replace}}
{{else}}{{template "M" .}}{{end}}{{end}}{{end}}' -compiled -deps ./... | sort -u > scan-input/go.list
cp go.mod scan-input/go.mod
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
The go list template is Sonatype’s, verbatim. Two things are ours: the ./... package pattern, which the Sonatype page does not show (its command ends at -deps), and copying go.mod next to the list, because the page says a valid go.mod is required.
To confirm with Sonatype: the exact package pattern the go list command should use. We use ./... so the whole module is covered. A Project with several modules (a go.work workspace) needs a go list per module.
Tier 2
No change from the reference workflow. The default scan-targets of scan-input finds go.list and go.mod.
Trusted-ref evaluation
Scan the real checkout. Run the same go list step, and use scan-targets: 'go.list go.mod' or '.'.
What you give up
- Replace directives and local modules. The template substitutes a
replacetarget’s path and version, which is what you want for a module replacement. Areplacepointing at a local directory has no version, so it cannot be matched. Reviewers should treat newreplacedirectives in a Pull Request as a signal to look closely. - Versions Lifecycle cannot match. Sonatype’s documentation lists these:
+incompatibleversions “may not be identified and can appear as Unknown”; pseudo-versions have “partial support”; arbitrary pre-release versions “are not supported”. - Build tags.
-compiled -depsfollows the build configuration the job runs with. Code behind other build tags or other operating systems is not listed. If you ship for several platforms, rungo listfor each (GOOS,GOARCH) and combine the results withsort -u. - Advisory only at PR time. As for every ecosystem, the contributor produces this file, so the Pull Request result is advisory. See What a PR-time result means.
Terraform providers
terraform-provider-sonatyperepo and terraform-provider-sonatypeiq are Go modules, so this guide applies unchanged. The provider’s docs/ and example Terraform files are not dependencies and are not scanned.
7.2.5 - Java
DRAFT. Not yet run end to end. Source: Sonatype’s Java Application Analysis. Gradle guidance is thinner than Maven, and this guide says where.
What Lifecycle reads
Java is a binary ecosystem. Sonatype’s documentation says Lifecycle scans packaged archives (.ear, .war, .jar, .zip, .tar.gz) and post-build artifacts, using Advanced Binary Fingerprinting, and that “for the best results” the post-build artifact you plan to deploy should be the scan target.
It can also read pom.xml and build.gradle, but that is a manifest scan with a real limit: “transitive dependencies are not automatically resolved and, therefore not included”. It needs the group, artifact and exact version of each direct dependency.
So for an accurate Pull Request scan, the scan input should be the built output. That means the build runs in Tier 1, where running a build is safe.
Tier 1 job: Maven
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Java
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version-file: .java-version # or set java-version: explicitly
cache: maven
- name: Build
run: ./mvnw --batch-mode -DskipTests package
- name: Gather scan input
run: |
mkdir scan-input
cp -r target/*.jar scan-input/ 2>/dev/null || true
cp -r target/*.war scan-input/ 2>/dev/null || true
# A thin jar does not contain its dependencies. Copy them alongside.
./mvnw --batch-mode dependency:copy-dependencies -DoutputDirectory=scan-input/dependencies
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
Notes:
mvn dependency:copy-dependenciesis the command Sonatype’s documentation gives for thin jars.- An uber or shaded jar already contains its dependencies, so the
dependency:copy-dependenciesstep is not needed. Sonatype’s documentation says to “call the index goal of the Maven plugin at the time of building the uber/shaded jar”. Add that to the build step if you produce one. -DskipTestsis for speed here. Your tests run in thebuild-and-testjob.- Use the project’s own wrapper (
./mvnw), so the Maven version is the one the Project uses. A Maven wrapper downloads Maven, which is acceptable in Tier 1 and is why this must not run in Tier 2.
Tier 1 job: Gradle
- name: Build
run: ./gradlew --no-daemon assemble
- name: Gather scan input
run: |
mkdir scan-input
cp -r build/libs/. scan-input/
Not documented: Sonatype’s Java page names build/libs only indirectly and gives no Gradle equivalent of Maven’s dependency:copy-dependencies. For a Gradle project that produces a thin jar, the dependencies are not in build/libs. Until Sonatype documents a recommended approach, a Gradle Project has two choices, and should say which it uses:
- Build a fat jar (for example with the Shadow plugin) and scan that.
- Scan
build.gradleas a manifest, accepting the loss of transitive dependencies. Sonatype’s documentation lists the supported scopes (api,apiElements,compileClasspath,implementation,compileOnly,compileOnlyApi,compile) and says only Groovybuild.gradlefiles are supported, so a Kotlin DSL build is not covered.
The Gradle plugin Project in this community (scan-gradle-plugin) is itself a Gradle plugin and its Pull Requests are a good test case for deciding this.
Tier 2
Mostly unchanged from the reference workflow. Two adjustments:
- Binary inputs are larger than lockfiles. Raise
LIFECYCLE_SCAN_MAX_BYTESif the default of 512 MB is too small, and keep it as low as is workable.
Trusted-ref evaluation
Build the project, then scan the same output (scan-targets: 'target' or 'build/libs') at stage: build on main and stage: release for a release. Do not scan ., because the source tree is a weaker target than the deployed artifact.
What you give up
- Test and provided scope.
dependency:copy-dependenciescopies test-scope dependencies by default. To scan only what ships, add-DincludeScope=runtime. Whether to include test dependencies is a policy decision for the Project. - A build that fails. If the Pull Request does not compile, there is no artifact, so the Pull Request gets no Lifecycle result until it builds. This is consistent with Tier 2 only running after Tier 1 succeeds.
- Advisory only at PR time. A Pull Request could build an artifact that omits a dependency. Maintainers reviewing
pom.xmlandbuild.gradlechanges is the control for that, and the trusted-ref evaluation is the backstop.
Nexus plugin Projects
Nexus Repository plugins (for example the CasC plugin) are built as bundles that run inside Nexus Repository. The bundle is the deployed artifact, so scan it. The Nexus platform itself is provided by the host at runtime and is not in the bundle. Whether it should appear in the scan is a policy decision for the Project.
7.2.6 - JavaScript
DRAFT. Not yet run end to end. Sources: Sonatype’s npm Application Analysis and Preparing to Run npm Application Analysis.
What Lifecycle reads
JavaScript can be scanned two ways, and the choice sets the accuracy:
| Scan input | Mode | Accuracy |
|---|---|---|
package.json plus a lockfile (package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml) | Manifest matching | Good. Evaluates dependencies from metadata only. |
package.json, the lockfile and node_modules | Binary (ABF) | Best. Sonatype recommends this for “the most precise results”. |
Sonatype’s documentation says that when JavaScript files are included, ABF is used and the JavaScript files are the primary identification source, and that “manifest entries without corresponding JavaScript files are ignored”. package.json must come with a package-lock.json.
The binary mode needs node_modules from “a clean production flag build”.
Which should a Project choose
- Manifest is small, fast and cheap to move between jobs. Choose it as the default for Pull Requests.
- Binary matters when you ship or bundle
node_modules, or need to detect modified copies of packages. It is large. Choose it for the trusted-ref evaluation and, if your Project wants it, for Pull Requests too.
Tier 1 job: manifest
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Gather scan input
run: |
mkdir scan-input
cp package.json scan-input/
# Use whichever lockfile your Project has.
cp package-lock.json scan-input/
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
This job does not run npm, so it runs nothing from the Pull Request at all. It is the simplest and safest way to prepare a scan input.
For a monorepo or workspace, copy each package’s package.json into a matching subdirectory of scan-input, so that each lockfile stays next to its manifest.
Tier 1 job: binary
- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Install production dependencies
run: npm install --production
# For Yarn: yarn install --prod
- name: Gather scan input
run: |
mkdir scan-input
cp package.json package-lock.json scan-input/
cp -r node_modules scan-input/node_modules
npm install --production is the command Sonatype’s preparation page gives, as is yarn install --prod. Use these exactly and not npm ci, until Sonatype says the others are equivalent.
Install scripts run here. npm install runs package install scripts. In Tier 1 that is acceptable, because Tier 1 has no secrets. It is the reason this step must never move into Tier 2.
Tier 2
Unchanged from the reference workflow for the manifest mode. For the binary mode, node_modules is large and has many files. Raise LIFECYCLE_SCAN_MAX_BYTES and LIFECYCLE_SCAN_MAX_FILES for this Project, and note that the artifact upload and download will be slow.
Trusted-ref evaluation
Run npm install --production on the real checkout, then scan package.json, the lockfile and node_modules together, at stage: build on main and stage: release for a release.
What you give up
- Manifest mode cannot detect a package whose files were altered, and relies on the lockfile being honest. A lockfile that a Pull Request changed deserves a reviewer’s attention.
- Development dependencies. The production flag means they are excluded in binary mode. If your policy covers build-time dependencies, scan them separately.
- Name matching sunset. Sonatype’s documentation notes that matching unmatched JavaScript files by name ended on 2025-09-10. Only files that match known hashes are counted.
- Advisory only at PR time, as for every ecosystem.
7.2.7 - PHP
DRAFT. Not yet run end to end. Source: Sonatype’s PHP Application Analysis.
What Lifecycle reads
PHP is a manifest ecosystem and the documentation describes one file only: “files named composer.lock (a JSON file generated by PHP composer) will be analyzed”. It does not scan composer.json or the vendor/ directory. Sonatype recommends generating and committing composer.lock before scanning, so the Project must commit composer.lock.
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Gather scan input
run: |
mkdir scan-input
cp composer.lock scan-input/
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
This job runs nothing from the Pull Request, and does not need composer install.
Tier 2
Unchanged from the reference workflow.
Trusted-ref evaluation
Scan the real composer.lock at stage: build on main and stage: release for a release.
What you give up
- Only top-level packages. Sonatype’s documentation says “only top-level dependencies (specified under the ‘packages’ group) are evaluated”. Development dependencies, which Composer records under
packages-dev, are not. - No binary scan is documented for PHP.
- Advisory only at PR time, as for every ecosystem. A Pull Request that edits
composer.lockis changing the thing being scanned.
7.2.8 - Python
DRAFT. Not yet run end to end. Source: Sonatype’s Python Application Analysis.
What Lifecycle reads
Python can be scanned from manifests or from binaries:
| Scan input | Notes |
|---|---|
requirements.txt | Must use == with a version and no wildcards. Variable ranges and environment ranges are ignored. |
poetry.lock | Parsed automatically. For Poetry 1.5.1 and later, development dependencies are excluded when pyproject.toml is present and discoverable. |
Pipfile.lock | Needs IQ CLI 2.0 or later. Only dependencies under the default key are parsed. |
.whl and .tar.gz files | Binary scan of the actual packages. |
Sonatype’s recommendation for the best results is to first create a virtual environment, resolve dependencies with pip install -r requirements.txt, and use pip download or pip wheel to gather the packages as the scan target.
Tier 1 job: Poetry lockfile (manifest)
This is the simplest option and fits a Project that already commits poetry.lock, such as Jake.
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Gather scan input
run: |
mkdir scan-input
cp poetry.lock pyproject.toml scan-input/
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
Include pyproject.toml so that Lifecycle can exclude development dependencies. This job runs nothing from the Pull Request.
Tier 1 job: pip (binary)
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version-file: .python-version # or set python-version: explicitly
- name: Download packages
run: |
python -m venv "$RUNNER_TEMP/venv"
. "$RUNNER_TEMP/venv/bin/activate"
mkdir scan-input
pip download -r requirements.txt -d scan-input
This follows Sonatype’s guidance. Use pip wheel -r requirements.txt -w scan-input instead if you want wheels built from source distributions.
Building runs setup code. pip download of a source distribution, and pip wheel, can execute a package’s build scripts. That is acceptable in Tier 1, which has no secrets, and is the reason this must not happen in Tier 2.
Tier 2
Unchanged from the reference workflow. Binary inputs are larger, so check the limits.
Trusted-ref evaluation
Scan the real poetry.lock and pyproject.toml, or the downloaded packages, at stage: build on main and stage: release for a release.
What you give up
- Unpinned requirements. A
requirements.txtwith ranges is skipped for those lines. Pin versions (for example withpip freeze, or a tool such aspip-compile) before scanning. - A single requirements file. Sonatype’s documentation says other variants of requirements files are ignored. If you split them (
requirements-dev.txt), only the one named exactly is read. - Advisory only at PR time, as for every ecosystem. A Pull Request that edits
poetry.lockis changing the thing being scanned, and a reviewer should look at it.
Not documented
Sonatype’s page does not state what site-packages scans look like, and does not describe a poetry export flow. If a Project needs either, ask Sonatype before relying on it.
7.2.9 - R
DRAFT. Not yet run end to end, and the thinnest guide: Sonatype’s documentation for R is brief. Source: Sonatype’s R (CRAN) Application Analysis.
What Lifecycle reads
The only input Sonatype’s documentation describes is a file named cran-installed.packages, “exported via installed.packages() command in CRAN console”. It does not describe scanning DESCRIPTION, renv.lock or package tarballs. Matching is by package name and version.
That means the scan input has to be generated from an R installation that has the package’s dependencies installed. The Project’s own repository does not contain it.
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up R
uses: r-lib/actions/setup-r@f9a764fea8d5c63df6ef9a5c7795bf7deb5d7e05 # v2.14.0
with:
use-public-rspm: true
- name: Install dependencies and export package list
run: |
mkdir scan-input
Rscript -e 'install.packages("remotes"); remotes::install_deps(dependencies = TRUE)'
Rscript -e 'write.table(installed.packages()[, c("Package", "Version")], file = "scan-input/cran-installed.packages", sep = "\t", quote = FALSE, row.names = FALSE)'
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
Not documented, and not yet verified: Sonatype’s page names the file and the command that produces it but not the exact column layout the scanner expects. The write.table line above is our guess at a sensible export and must be checked against a real scan before this guide is relied on.
OysterR is the Project this applies to. It has a DESCRIPTION file and no lockfile, so it would need this approach.
Tier 2
Unchanged from the reference workflow.
Trusted-ref evaluation
Generate the same file from the real checkout and scan it at stage: build on main and stage: release for a release.
What you give up
- Only what is installed. The scan covers what the Tier 1 job installed, which depends on the repositories and versions available at that moment, so two runs can differ.
- Name and version only. There is no binary fingerprinting for R in Sonatype’s documentation.
- Installing packages runs their code (compilation,
.onLoadhooks in some cases). That is acceptable in Tier 1 and is why this must not run in Tier 2. - Advisory only at PR time, as for every ecosystem.
7.2.10 - Ruby
DRAFT. Not yet run end to end. Source: Sonatype’s Ruby Application Analysis.
What Lifecycle reads
Ruby is a manifest ecosystem, with a binary option. Sonatype’s documentation says it scans Gemfile.lock, .gem files and vendor/bundle. For the lockfile, “components from sections GIT, GEM and PATH and with an exact version will be analyzed”. For the best results it recommends scanning “after either installing the packages from a clean environment or storing the gems in a local cache”.
The Project must commit Gemfile.lock. Gem projects (libraries) often do not, and then there is nothing to scan.
Tier 1 job: lockfile
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Gather scan input
run: |
mkdir scan-input
cp Gemfile.lock scan-input/
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
This job runs nothing from the Pull Request.
Tier 1 job: installed gems (binary)
Add this to scan the gems themselves. It runs bundle, which may execute native extension builds, so it belongs in Tier 1.
- name: Install gems into a local path
run: |
bundle config set --local path vendor/bundle
bundle install
mkdir scan-input
cp Gemfile.lock scan-input/
cp -r vendor/bundle scan-input/vendor-bundle
Not documented: Sonatype’s Ruby page does not say whether the directory has to be named vendor/bundle to be recognised. This snippet renames it to vendor-bundle to avoid a path clash, which has not been tested. Keep the original name if the scan does not pick it up.
Tier 2
Unchanged from the reference workflow.
Trusted-ref evaluation
Scan the real Gemfile.lock, or the installed gems, at stage: build on main and stage: release for a release.
What you give up
- Only exact versions are analysed, so a lockfile entry without one is skipped.
- The Gemfile is not read. The lockfile is the truth, which means a Pull Request that edits it is changing the thing being scanned, and a reviewer should look at it.
- Advisory only at PR time, as for every ecosystem.
7.2.11 - Rust
DRAFT. Not yet run end to end. Source: Sonatype’s Rust Application Analysis.
What Lifecycle reads
Rust is a manifest ecosystem, and the only file Sonatype’s documentation describes is Cargo.lock: “the Cargo coordinate-based matching feature provides the ability to scan and evaluate Rust dependencies found in the Cargo.lock file”. The name and version of each package entry are evaluated. The documentation does not mention Cargo.toml or built binaries.
Because the lockfile is the input, the Project must commit Cargo.lock. Library crates often do not. A Project that does not commit it has nothing to scan, and should either start committing it or generate it in Tier 1 with cargo generate-lockfile (which resolves to the newest versions, not the ones the Project tested against, and is therefore a weaker signal).
Tier 1 job
prepare-scan-input:
name: Prepare Lifecycle scan input
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Gather scan input
run: |
mkdir scan-input
cp Cargo.lock scan-input/
- name: Upload scan input
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lifecycle-scan-input
path: scan-input/
if-no-files-found: error
retention-days: 3
This job runs nothing from the Pull Request. For a workspace with several Cargo.lock files, copy each into its own subdirectory.
Tier 2
Unchanged from the reference workflow.
Trusted-ref evaluation
Scan the real Cargo.lock at stage: build on main and stage: release for a release.
What you give up
- No binary scan is documented. The result is only as honest as
Cargo.lock. A Pull Request that edits the lockfile is changing the thing being scanned, and a reviewer should look at it. - Path and git dependencies may not match published crates. Sonatype’s page does not say how they are handled.
- Advisory only at PR time, as for every ecosystem.