
jenkins-infra/repository-permissions-updater
Developer toolsArtifactory permissions synchronization tool and data set
Overview
Repository Permissions Updater ============================== The Jenkins project hosts Maven artifacts such as core and plugin releases on Artifactory. Its permissions system is independent of GitHub's, and we limit which users (identified by the Jenkins LDAP account, same as wiki and JIRA) are allowed to upload which artifacts. This repository contains both the definitions for Artifactory upload permissions in YAML format, as well as the tool that synchronizes them to Artifactory. These permissions are specifically for uploading artifacts to the Jenkins project's Maven repository. It is independent of GitHub repository permissions. You may have one without the other. Typically, you'll either have both, or just the GitHub repository access. To host a new plugin in the jenkinsci GitHub organization, open a hosting request. See the hosting documentation for more information.
README
Repository Permissions Updater
About
The Jenkins project hosts Maven artifacts such as core and plugin releases on Artifactory.
Its permissions system is independent of GitHub’s, and we limit which users (identified by the Jenkins LDAP account, same as wiki and JIRA) are allowed to upload which artifacts.
This repository contains both the definitions for Artifactory upload permissions in YAML format, as well as the tool that synchronizes them to Artifactory.
Note: These permissions are specifically for uploading artifacts to the Jenkins project’s Maven repository. It is independent of GitHub repository permissions. You may have one without the other. Typically, you’ll either have both, or just the GitHub repository access.
Requesting Hosting
To host a new plugin in the jenkinsci GitHub organization, open a hosting request.
See the hosting documentation for more information.
The Jenkins Hosting Checker comments on every hosting request with the problems it finds, and the hosting team reviews the request once those are fixed. Please run the same checks yourself before opening the request so it can be processed without rounds of fixing and re-checking.
Running the hosting checks locally
Requirements: Java 25 or newer, Maven, and a GitHub token (no scopes needed).
If you have the GitHub CLI logged in, its token is picked up automatically; otherwise set GITHUB_OAUTH (or GITHUB_TOKEN).
The checks read your repository from GitHub, so push your changes first, then:
git clone https://github.com/jenkins-infra/repository-permissions-updater.git
cd repository-permissions-updater
./bin/check-hosting.sh \
--repository-url https://github.com/your-org/your-cool-plugin \
--new-repository-name your-cool-plugin \
--github-user your-github-user \
--jenkins-user your-jenkins-account
--repository-url: the repository to host, as you will enter it in the hosting request.--new-repository-name: the name it will have injenkinsci,-plugin. Defaults to the current repository name.--github-user: GitHub users to have commit permission, comma separated or repeated.--jenkins-user: Jenkins community accounts to have release permission, comma separated or repeated.--no-cd: add this if you will not request automated releases via GitHub Actions.
The first run builds the checker.
After pulling updates to this repository, run with REBUILD=1 to use the latest checks.
Set VERBOSE=1 to see the checker’s full log.
The script exits with 0 when no issues marked Required are found, and 1 when there are issues to fix.
Fix them, push, and run it again.
Using an AI coding agent
The jenkins-hosting-check skill teaches an AI coding agent to run the checks against your plugin and fix what they report.
Install it as a personal skill so it is available while working in your plugin repository.
From your clone of this repository:
# GitHub Copilot and OpenAI Codex read personal skills from ~/.agents/skills
mkdir -p ~/.agents/skills
ln -sfn "$PWD/.claude/skills/jenkins-hosting-check" ~/.agents/skills/jenkins-hosting-check
# Claude Code reads personal skills from ~/.claude/skills
mkdir -p ~/.claude/skills
ln -sfn "$PWD/.claude/skills/jenkins-hosting-check" ~/.claude/skills/jenkins-hosting-check
The symlinks keep the skill up to date when you git pull this repository.
To install a snapshot instead, replace ln -sfn with cp -R.
Claude Code also picks up the skill automatically when run inside this repository.
Then ask the agent to check that your plugin is ready to be hosted.
Requesting Permissions
Prerequisite: You need to have logged in once to Artifactory and Jira with your Jenkins community account (this is the same as the account you would use to login to Jira) before you can be added to a permissions target.
To request upload permissions to an artifact (typically a plugin), file a PR editing the appropriate YAML file, and provide a reference that shows you have commit permissions, or have an existing committer to the plugin comment on your PR, approving it. See this page for more information.
Adoption requests and the authorized unless rejected label
When a PR is labeled authorized unless rejected, it means the request has been authorized and will proceed unless an existing maintainer explicitly objects.
An automated reminder comment is posted on the PR if 14 days pass without the PR being merged or closed.
The 14-day timer is measured from the most recent time the label was applied; removing and re-adding the label resets the timer.
The reminder is sent at most once per label application.
If you are a maintainer of the affected plugin and wish to object, please comment on the pull request.
Managing Permissions
The directory permissions/ contains a set of files, one per plugin or artifact, that define the permissions for the respective artifacts. Files typically have a component, plugin, or pom prefix for organization purposes:
pluginis used for Jenkins plugins.pomis used for parent POMs and everything else consisting of just a POM file.componentis used for everything else, usually libraries.
These prefixes, like the rest of the file name, have no semantic meaning and just help in organizing these files.
Each file contains the following in YAML format:
- A
name(typically mirrored in the file name), this is also theartifactIdof the Maven artifact. - A
githubfield indicating the GitHub organization and repository which is expected to produce these artifacts. - A set of paths, usually just one. These correspond to the full Maven coordinates (
groupIdandartifactId) used for the artifact. Since Jenkins plugins can change group IDs and are still considered the same artifact, multiple entries are possible. - Optional
releaseBlockedflag. When set to true, artifact(s) described in this file cannot be pushed to Artifactory. This is only used in rare circumstances, e.g., by the Jenkins security team if plugin code contains unreleased security issues. - A set of user names (Jenkins community user accounts in LDAP, the same as used for wiki and JIRA) allowed to upload this artifact to Artifactory. This set can be empty, which means nobody is currently allowed to upload the plugin in question (except Artifactory admins). This can happen for plugins that haven’t seen releases in several years, or permission cleanups.
Example file:
---
name: "p4"
github: "jenkinsci/p4-plugin"
paths:
- "org/jenkins-ci/plugins/p4"
developers:
- "p4paul"
p4(lines 2 and 5):artifactIdp4-plugin(line 3): GitHub repository nameorg/jenkins-ci/plugins(line 5):groupId(with slashes replacing periods)p4paul(line 7): Jenkins community account user name
Adding a new plugin
Create a new YAML file similar to existing files.
Adding a new uploader to an existing plugin
Edit the developers list in the YAML file for the plugin.
Remove uploaders from an existing plugin
Remove entries from the developers list.
Deprecating a plugin
See the documentation on jenkins.io. Do not delete YAML files from this repository.
Changing plugin ID
Jenkins cannot handle plugin renames, so if a release of the plugin has been published already, it’s not possible to rename it. A plugin can theoretically be replaced by a new one with a different ID, but this is difficult to get right (e.g., when both plugins are installed at the same time).
If the plugin hasn’t been released yet, you can just rename and edit the existing permissions file, changing the name component.
You may also edit the github component, if you wish to rename the repository.
Changing a plugin’s artifactId
Changing the paths or modifying the `` in the plugin pom.xml is highly discouraged.
Modifying the path will break any Maven dependencies from other plugins.
Altering the artifactId means changing the identifier by which the Jenkins plugin manager differentiates one plugin from others, and will cause chaos for users who have already installed it under the old name.
Multiple plugins in single configuration file
If multiple closely related plugins are located in the same GitHub repository, have the same maintainer and the same issue tracker, they can use a single configuration file. To define multiple plugins in a single file, please do the following:
- Add an entry to
pathsthat includes a*character, this path has to match the paths of all included plugins, but should not be too generic. - Add a new property
extraNames, it should contain the list of names of all the plugins managed by this configuration file other than the main plugin defined byname. All these names must start with the name of the main plugin, followed by-.
Managing Continuous Delivery (JEP-229 CD)
Jenkins plugins and other components can be continuously delivered through a supported process described in JEP-229.
You can enable JEP-229 CD for your component by adding the following to your component’s YAML file:
cd:
enabled: true
For this to work, there needs to be at least one developers listed. If the list of developers is empty or missing entirely (e.g., after the last maintainer steps down), no new releases can be published through JEP-229 CD.
IMPORTANT: When using JEP-229 CD, every committer to your repository can create new releases by merging pull requests. As a result, the list of maintainer accounts maintained in your plugin’s YAML file is no longer the single reference on who can publish new releases. Be sure to check which users have commit access to your repository and remove any that are unexpected before enabling CD, as well as any unexpected deploy keys. Additionally, the users listed in this repository still serve as the contacts for security issues and plugin/component governance questions. For that reason, CD permissions are also only granted to components with at least one maintainer. In particular, the Jenkins security team will not make an effort to reach out to GitHub committers when maintainers (and security contacts, see below) are unresponsive before announcing vulnerabilities without a fix.
Exclusively using JEP-229 CD
By default, enabling JEP-229 CD enables it exclusively; i.e., the listed users will not be able to create new releases, but they remain contacts for security issues and plugin/component governance questions.
It is also possible to disable exclusive JEP-229 CD, in which case both users with commit access and the listed users will be able to create new releases, with the listed users remaining contacts for security issues and plugin/component governance questions:
cd:
enabled: true
exclusive: false
Managing GitHub Permissions (experimental)
RPU is gaining the ability to also manage GitHub repository/team access as code, in addition to Artifactory upload permissions. This is a new, opt-in feature: nothing changes for a component unless its YAML file explicitly enables it.
See docs/rollout-of-github-permissions-sync.md for the staged adoption plan across the 2000+ repos in the org and how GitHub API usage is kept bounded as adoption grows.
Why it’s opt-in, and how writes are gated
The jenkinsci organization has 2000+ repositories and ~2600 teams, so a mistake in an automated
reconciliation could lock maintainers out of their own repositories at scale. To manage that risk:
- A component (or cross-repository team) is only touched by this feature once its YAML file sets
manageGitHubPermissions: true(or, forteams/*.yml,manageGitHubTeam: true). Everything else is left completely alone, so adoption can be gradual. Setting this flag means RPU actively reconciles that component’s/team’s GitHub membership to match itsdeveloperslist — it is not merely a reporting toggle. - Whether a run actually applies those changes, or only computes and logs/reports them, is controlled
independently by the
githubPermissionsDryRunsystem property (-DgithubPermissionsDryRun=...), which defaults totrue(safe/report-only) until explicitly set tofalse. This lets the org run the full read+diff pipeline against real GitHub data for a bake-in period, with zero risk of a write, before anyone deliberately flips the switch to start applying changes. Either way, the computed diff (including whether it was applied) is written tojson/github-permissions-diff.json, published alongside the other index reports. - Removing the last member of a team is allowed and is not treated as a special/blocked case: org owners can always restore access regardless of member count, so there’s no lockout risk that needs guarding against.
- A mutation failure for one login/team (e.g. a login that no longer exists) is logged and recorded in that
team’s
errorsin the report, but does not abort reconciliation of any other opted-in team in the same run. - Just like Artifactory sync, any live call to the GitHub API for this feature only happens in the trusted, post-merge run — never in PR builds. PR builds intentionally run without any credentials (since PRs can come from forks), and there is no GitHub credential that is both genuinely read-only and safe to hand to untrusted, credential-free PR builds (a fine-grained PAT or GitHub App key is still a secret; anonymous reads are rate-limited to 60 requests/hour and don’t reliably expose private team membership). PR builds only perform static YAML validation (schema/format checks) of the fields below — no network calls.
The developers list
Each entry in developers (in both permissions/*.yml and teams/*.yml) can be either:
- a plain string — a Jenkins community (LDAP) id, exactly as before; used for Artifactory permissions. Only
supported when GitHub permissions management is not enabled (see below). A plain string starting with
@(e.g."@core") is a legacy reference to a cross-repositoryteams/*.ymlteam, expanded to that team’s owndevelopersentries; also only supported when GitHub permissions management is not enabled – use the typed{team: ...}form below instead once it is. - a mapping with both
ldapandgithubkeys — ties that developer’s LDAP id to their GitHub login 1-to-1, e.g.{ldap: jglick, github: jglick}. Use this form when the two ids differ, so the GitHub login used for permissions management is explicit rather than assumed. Both keys are required when using this form. - a mapping with only a
githubkey, e.g.{github: someuser}— a developer with a GitHub login but no Jenkins community (LDAP) account at all. Only relevant for GitHub permissions management (never contributes to Artifactory permissions, since there’s no LDAP id to grant them). Useful when backfillingmanageGitHubPermissions/manageGitHubTeamfor a component that already has GitHub collaborators/team members with no obvious LDAP mapping: merge the ones you can confidently tie to an LDAP id as{ldap, github}, and add everyone else as GitHub-only entries so existing access is preserved without guessing. - a mapping with only an
ldapkey, e.g.{ldap: someuser}— the opposite case: a developer who should be excluded from GitHub permissions management entirely (e.g. because their LDAP id isn’t a real GitHub login, belongs to someone else on GitHub, or they simply shouldn’t be granted GitHub team access). Still used for Artifactory permissions as normal. - a mapping with only a
teamkey, e.g.{team: cloudbees-developers}— an explicit, typed reference to a cross-repositoryteams/*.ymlteam, expanded (recursively, so a referenced team can itself reference further teams) into that team’s owndevelopersentries. This is the typed equivalent of the legacy"@team-name"string reference, and – unlike that legacy form – remains usable oncemanageGitHubPermissions/manageGitHubTeamhas banned plain-string entries. The referenced team name is validated to exist (statically, no GitHub API access), and a cyclic chain of team references is rejected.
Once manageGitHubPermissions/manageGitHubTeam is enabled, plain-string entries (including "@team-name"
references) are no longer allowed at all — every developer must use one of the mapping forms above (use
{team: ...} in place of "@team-name"), so it’s explicit (never implicitly assumed) how each developer
maps to GitHub. This is enforced by static (no GitHub API access) validation, so it fails fast on PRs.
All five forms can be freely mixed in the same list.
YAML fields
In a component’s permissions/*.yml file:
developers:
- ldap: "jglick"
github: "jglick"
manageGitHubPermissions: true # opt-in: without this, GitHub logins above and the fields below are ignored
repositoryTeam: "custom-name" # optional: overrides the default " Developers" team name
additionalGitHubTeams: # grants existing cross-repo teams (teams/*.yml) access to this repo
- name: "cloudbees-developers"
role: "push" # one of: pull, triage, push, maintain, admin
In a cross-repository teams/*.yml file:
name: "cloudbees-developers"
developers:
- ldap: "existing-ldap-id"
github: "existing-github-id"
manageGitHubTeam: true
Testing the diff report locally, for one component
The github-sync CLI command computes the same diff sync produces, but on its own — it never touches
Artifactory, so it doesn’t need ARTIFACTORY_TOKEN at all. This makes it convenient to test
manageGitHubPermissions/manageGitHubTeam changes for a single component in isolation:
# GITHUB_TOKEN needs read access to the relevant team(s) - e.g. an org member's token with the read:org scope.
# It additionally needs write (admin:org) access if you disable dry-run below and want to actually apply changes.
export GITHUB_TOKEN=...
# Point definitionsDir at a directory containing just the file(s) you want to test, e.g.:
mkdir -p /tmp/rpu-test/permissions /tmp/rpu-test/teams
cp permissions/plugin-slack.yml /tmp/rpu-test/permissions/
# If the file(s) you copied reference a team (e.g. `{team: "core"}` or legacy `"@core"` in `developers`),
# also copy every `teams/*.yml` file that ends up referenced (including transitively, for team-of-teams),
# or validation will fail with "developers entry references unknown team '...'":
cp teams/core.yml /tmp/rpu-test/teams/
java -DdefinitionsDir=/tmp/rpu-test/permissions \
-DteamsDir=/tmp/rpu-test/teams \
-DgithubDiffOutput=/tmp/rpu-test/github-permissions-diff.json \
-jar target/repository-permissions-updater-*-bin/repository-permissions-updater-*.jar \
github-sync
cat /tmp/rpu-test/github-permissions-diff.json
By default (githubPermissionsDryRun unset, or explicitly -DgithubPermissionsDryRun=true), this only
computes and reports the diff — nothing is added or removed on GitHub. To actually apply the reported
changes, pass -DgithubPermissionsDryRun=false:
java -DdefinitionsDir=/tmp/rpu-test/permissions \
-DteamsDir=/tmp/rpu-test/teams \
-DgithubDiffOutput=/tmp/rpu-test/github-permissions-diff.json \
-DgithubPermissionsDryRun=false \
-jar target/repository-permissions-updater-*-bin/repository-permissions-updater-*.jar \
github-sync
Since the sync only ever considers components/teams that have opted in
(manageGitHubPermissions/manageGitHubTeam: true), you don’t strictly need to copy files out — running
github-sync directly against the real permissions/teams directories (the defaults if -D...Dir is
omitted) is equally safe and only reports on whichever components have currently opted in; copying a single
file out is just a convenient way to be certain you’re looking at exactly one component’s report while
testing.
Managing Security Process
The Jenkins project acts as a primary contact point for security researchers seeking to report security vulnerabilities in Jenkins and Jenkins plugins (learn more).
We generally assign reported issues to maintainers after a basic sanity check (learn more). Through additional metadata in the YAML file described above, you can define who else should be contacted in the event of a report being received and is authorized to make decisions regarding security updates, e.g. scheduling. Add a section like the following to your plugin’s YAML file:
security:
contacts:
jira: some_user_name
Given the above example, we will primarily assign any security issue in Jira to some_user_name.
Regular maintainers are added to the issue as well to give visibility and allow participation/discussion.
This means that specifying a Jira security contact is only useful when it’s an account not already listed as maintainer.
jira is optional.
If you represent a company with dedicated security team that needs to be involved, we recommend you to create a Jira account backed by an email address that the team can access, like a mailing list.
Managing Issue Trackers
The YAML metadata files in this repository also hold information about issue trackers. This is used to make issue trackers more accessible, e.g. by adding them to the UI of Jenkins or on https://plugins.jenkins.io.
Declaring Issue Trackers
The top-level issues key contains a sorted list of issue tracker references in descending order of preference.
For GitHub issues, a GitHub repository must be specified as the value for the github key, and the value must start with jenkinsci/, followed by the repository name.
For Jira, a component name or ID must be specified as the value for the jira key.
The component name is easier to read, while the component ID is resilient in case of component renames.
Either kind of issue tracker supports the report: boolean value that controls whether new issues should be reported in this issue tracker.
The default is true.
A complete example with two trackers:
issues:
- github: 'jenkinsci/configuration-as-code-plugin' # The preferred issue tracker
- jira: '23170' # A secondary issue tracker is the Jira component id 23170 for 'configuration-as-code-plugin'
report: false # No new issues should be reported here
Jira component id can be found at: https://issues.jenkins.io/rest/api/2/project/JENKINS/components
When GitHub Issues is used, there would be some duplicated content in the file (between github and issues entries) which can be resolved by using a YAML reference.
Example:
github: &GH 'jenkinsci/configuration-as-code-plugin' # Declare a reference
issues:
- github: *GH # Use the reference
Migrating from Jira to GitHub issue trackers
Send a pull request to this repository removing the Jira reference and changing it to GitHub. Use this script to automate the required file change and open a draft pull request:
./bin/migrate-to-github.sh permissions/plugin-slack.yml
# Multiple files can be done at once:
./bin/migrate-to-github.sh permissions/plugin-slack.yml permissions/plugin-dark-theme.yml
A maintainer / Jenkins infra team member will then be able to perform the migration on your behalf.
Consuming Issue Trackers
A file issues.index.json is generated when the tool is executed, containing a map from component names to a list of issue trackers.
Only plugins are expected to specify an issue tracker here.
If a plugin does not have a corresponding key in this map, the tool did not consider it for inclusion. If a plugin has a corresponding key in this map but an empty list of issue trackers, no issue trackers are known or supported.
Each issue tracker entry will have the following keys:
type: Currentlyjira(meaning issues.jenkins.io) orgithub(meaning GitHub issues)referencecontains atype-specific identifier string that provides additional information how issues are tracked; for Jira it is the component name or ID and for GitHub Issues it is theorgname/reponameString.viewUrlis a URL to a human-readable overview page. This value may not exist if no valid URL could be determined.reportUrlis a URL where users can report issues. This value may not exist if no valid URL could be determined, or new issues should not be reported in this tracker.
The list is sorted in descending order of preference.
The first issue tracker in the list with a reportUrl should be presented as the primary (or only) option for reporting issues.
Further issue trackers are mostly provided as a reference, e.g. when listing existing issues, although a different issue tracker with reportUrl can be linked if users provide a preference for a specific kind of issue tracker.
Usage
This tool provides a unified CLI with subcommands. See Jenkinsfile for the sync command and GitHub Actions workflows for hosting commands.
Available commands:
RPU_CLI="java -jar target/repository-permissions-updater-*-bin/repository-permissions-updater-*.jar"
$RPU_CLI sync
$RPU_CLI check-hosting
$RPU_CLI precheck-hosting --repository-url --github-user --jenkins-user
$RPU_CLI host
Configuration
The following Java system properties can be used to customize the behavior of the sync command:
dryRun- Set totrueto generate the API payloads without submitting them. No modifications will be executed.development- Set totrueduring tool development to ensure production data is not overridden. This will have the following effects:- Permissions are only granted to deploy to the
snapshotsrepository (rather than bothsnapshotsandreleases) - A different, non-colliding set of prefixes (unless overridden, see below) is used.
- Permissions are only granted to deploy to the
definitionsDir- Path to directory containing permission definitions YAML files, defaults to./permissions.artifactoryUserNamesJsonListUrl- URL to a list containing known Artifactory user names, any permissions assigned to a user not on that list will cause the tool to abortartifactoryApiTempDir- Path to directory (that will be created) where this tool stores Artifactory permissions API JSON payloads, defaults to./json.artifactoryObjectPrefix- Override the prefix for groups and permission targets managed (created, updated, removed) using the tool. If unspecified, the value will begeneratedv2-by default, orgenerateddev-in development mode.artifactoryUrl- URL to Artifactory, defaults tohttps://repo.jenkins-ci.orgartifactoryTokenMinutesValid- How long authentication tokens to Artifactory for CD enabled repos should be valid for, default240(4 hours). Regular script execution frequency needs to be aligned with this.gitHubSecretNamePrefix- Prefix for secrets sent to GitHub repos. If unspecified, the value will beMAVEN_by default, orDEV_MAVEN_in development mode.jiraUserNamesJsonListUrl- URL to a list containing known Jira user names of (potential) maintainers. This is essentially a workaround to reduce the number of individual user lookups via Jira API.
precheck-hosting runs the same checks as check-hosting without a hosting request issue and prints the result instead of commenting.
bin/check-hosting.sh wraps it, see Running the hosting checks locally.
The following Java system properties can be used for the check-hosting command:
debugHosting- Set totrueto enable debug mode.
Environment Variables
The following environment variables are expected to be set:
ARTIFACTORY_USERNAME- Admin user name for ArtifactoryARTIFACTORY_PASSWORD- Corresponding admin password (or API key) for Artifactory admin userGITHUB_USERNAME- GitHub user name for a user with admin access to any CD enabled reposGITHUB_TOKEN- Corresponding token for the user with admin access to any CD enabled repos, requiresreposcope to create/update secretsJIRA_USERNAME- User name (does not need admin permissions) for JiraJIRA_PASSWORD- Corresponding password for Jira user
How It Works
The tool runs three steps in sequence:
- Generate JSON payloads from YAML permission definition files.
- Submit generated JSON payloads to Artifactory.
- Remove all generated permission targets in Artifactory that have no corresponding generated JSON payload file.
Recommended Tools
Try a different keyword or remove a filter.
Install
npx skillfish add jenkins-infra/repository-permissions-updater