Skip to content

feat: add circleci as trusted publisher#19349

Open
meeech wants to merge 26 commits intopypi:mainfrom
meeech:add-cci-trusted-publisher-take-2
Open

feat: add circleci as trusted publisher#19349
meeech wants to merge 26 commits intopypi:mainfrom
meeech:add-cci-trusted-publisher-take-2

Conversation

@meeech
Copy link

@meeech meeech commented Jan 20, 2026

#13888

Related PRs:

pypi/pypi-attestations#166 Add CircleCI to pypi-attestations
meeech#1 Stacked pr off this one - integrates attestations. Question: would it be better to just merge this into have the one pr? I was trying to be mindful of making things easier to review.

I have tried to keep commits bite sized. I am open to split this up into multiple prs if that preferred (though not sure where the 'fault lines' would be for this since its all a bit related - forms, model, etc? let me know

MUST

  • we must reject any publish where the ssh-rerun claim is true. like we don't even allow the exchange for an api token to do the publish ✅

while i worked with a LLM to generate this PR, i (the human) have reviewed all this code. (amp for the curious)
tbf it did a good job doing all the boilerplate.

Basically I try to adhere to https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md

Copy link
Author

@meeech meeech Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q:

when comparing with GH form, there looks like there's some more stuff we could do.

eg; they have a call to hit the api to lookup the owner name. We could do some stuff like that, hit CCI API to fetch project name, org names, but not sure its worth it? We could silently fail if its not a OSS project. (if we get auth rejected)

Also, I can see the GH form has provision for API key, but I'm not yet clear where the user would provide that? not seeing it in the warehouse ui.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I think will do a follow up pr around adding something similar to look up a project by uuid and fill in some other info (when its a public project). let me know if you think its a bad idea to make it a follow up pr as opposed to taking care of it now? fwict it will be a simple migration to add a few more fields

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this to prevent resurrection attacks (i.e., a new GitHub repo created with the same owner & repository name would have a different repository ID). If CircleCI permits re-using the unique fields being used for the publisher here, then we should do the same thing.

I think we should just do that as part of this PR, otherwise we will end up in a state where we have IDs for new projects going forwards but not ones added before the follow-up PR.

The API key is not the user's key, but our key for making these requests to the GitHub API.

{% macro circleci_form(request, pending_circleci_publisher_form) %}
<p>
{% trans href="https://circleci.com/docs/openid-connect-tokens/" %}
Read more about CircleCI's OpenID Connect support <a href="{{ href }}">here</a>.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: prep page about this form for CCI guides, lets add a link to that as well. though its a chicken egg issue i suspect. but if we can get this deployed to staging warehouse, can then build a guide and do a followup pr to add the link before hitting prod

class_="form-group__field",
aria_describedby="circleci_org_id-errors")
}}
<p class="form-group__help-text">{% trans %}The CircleCI organization ID (UUID) that owns the project{% endtrans %}</p>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need more guidance here where to find this info? i am leaning to no, since we will have a guide we can link to in cci docs that can go in depth

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed theres space for a guide on pypi docs as well, so between those 2 it should be sufficient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something users are aware of? Or should we be getting it via API based on the organization name?

@meeech meeech force-pushed the add-cci-trusted-publisher-take-2 branch from a2568a4 to 44d7223 Compare January 22, 2026 04:49
@meeech
Copy link
Author

meeech commented Jan 27, 2026

milestone:

the 0.0.1.dev87 was published from
workflow https://app.circleci.com/workflow/0af1fd55-41dd-4bf8-9706-c632647f83f2

to my local pypi

image

@meeech meeech force-pushed the add-cci-trusted-publisher-take-2 branch from 6ec2327 to 9fe9b93 Compare January 27, 2026 02:50
@meeech
Copy link
Author

meeech commented Jan 27, 2026

image

@meeech meeech force-pushed the add-cci-trusted-publisher-take-2 branch from 5b980b7 to 459fa6e Compare February 5, 2026 04:05
@meeech meeech force-pushed the add-cci-trusted-publisher-take-2 branch from 459fa6e to 669a67a Compare February 7, 2026 15:32
@meeech meeech marked this pull request as ready for review February 7, 2026 16:32
@meeech meeech requested a review from a team as a code owner February 7, 2026 16:32
meeech and others added 8 commits February 18, 2026 12:11
Add CircleCIPublisher and PendingCircleCIPublisher models following
the existing OIDC publisher pattern. CircleCI identifies projects by
circleci_org_id and circleci_project_id (UUIDs from OIDC claims).

- CircleCIPublisherMixin with shared logic
- CircleCIPublisher for active publishers
- PendingCircleCIPublisher for pending registration
- Export models and issuer URL from __init__.py
- Register in OIDC_PUBLISHER_CLASSES, OIDC_ISSUER_SERVICE_NAMES,
  and OIDC_ISSUER_ADMIN_FLAGS in utils.py

Note: Column names use circleci_ prefix to avoid collision with
PendingOIDCPublisher.organization_id (PyPI org FK).

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
- Add DISALLOW_CIRCLECI_OIDC admin flag for killswitch control
- Register CircleCI OIDC publisher service factory

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
- CircleCIPublisherBase with circleci_org_id and circleci_project_id fields
- CircleCIPublisherForm for project-level publisher management
- PendingCircleCIPublisherForm for pending publisher registration
- Export forms from __init__.py

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
Add add_circleci_oidc_publisher view to ManageOIDCPublisherViews
for registering CircleCI trusted publishers on projects.

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
Add CircleCI support to pending publisher management in account views
for registering publishers before project creation.

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
Add CircleCI support to organization-level pending publisher management.

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
Add CircleCI publisher form and display sections to:
- Project publishing template
- Account publishing template
- Organization publishing template
- Base manage template (nav update if any)

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
Create circleci_oidc_publishers and pending_circleci_oidc_publishers
tables with circleci_org_id and circleci_project_id columns.

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
meeech and others added 18 commits February 18, 2026 12:11
- CircleCIPublisherFactory and PendingCircleCIPublisherFactory
- Model tests for CircleCIPublisher and PendingCircleCIPublisher
- Form validation tests for CircleCI publisher forms
- View tests for project, account, and organization publishers

Amp-Thread-ID: https://ampcode.com/threads/T-019bb846-115f-75e8-9c39-9694e10df5da
Co-authored-by: Amp <amp@ampcode.com>
Add wtforms.validators.UUID to all CircleCI OIDC publisher form fields:
- circleci_org_id (required)
- circleci_project_id (required)
- pipeline_definition_id (required)
- context_id (optional)

This ensures users submit valid UUIDs matching CircleCI's identifier format.

Also update tests to:
- Include pipeline_definition_id in test data (now required)
- Add test for optional context_id with valid UUID
- Add test for missing pipeline_definition_id
- Add parametrized test for invalid UUID on required fields
- Add test for invalid context_id UUID
Add support for two optional VCS claims in CircleCI OIDC publishers:
- vcs_ref: git ref like 'refs/heads/main'
- vcs_origin: VCS origin like 'github.com/organization-123/repo-1'

These claims allow users to constrain publishers to specific branches
or repositories for additional security.

Changes:
- Add model columns vcs_ref and vcs_origin (nullable)
- Move claims from __unchecked_claims__ to __optional_verifiable_claims__
- Add _check_optional_string helper for simple string matching
- Update __getattr__ to map claim names to attributes
- Update exists(), admin_details, unique constraints, and reify()
- Add form fields for vcs_ref and vcs_origin
- Update all views that create CircleCI publishers
Update the CircleCI OIDC publisher migration to include:
- vcs_ref column (nullable)
- vcs_origin column (nullable)
- Updated unique constraints to include both fields
Update test data in test_oidc_publishers.py to use valid UUID format
for CircleCI fields (circleci_org_id, circleci_project_id,
pipeline_definition_id) and include all required form fields
(context_id, vcs_ref, vcs_origin).

This ensures test data matches the UUID validation requirements
added to the CircleCI form fields.
Add pipeline_definition_id, context_id, vcs_ref, and vcs_origin to
the mocked CircleCI form in test_organizations.py to match the
complete form field requirements.
Add tests for the optional vcs_ref and vcs_origin fields:
- test_validate_with_optional_vcs_ref
- test_validate_with_optional_vcs_origin
- test_validate_with_all_optional_fields
Add comprehensive tests for the new optional VCS claims:

- TestCheckOptionalString: Tests for the _check_optional_string helper
  - Empty ground truth always passes
  - Required value fails without claim
  - Required value matches/doesn't match claim

- TestCircleCIPublisher updates:
  - test_admin_details_with_vcs_ref
  - test_admin_details_with_vcs_origin
  - test_admin_details_with_all_optional_fields
  - Updated test_getattr_maps_claims_to_attributes

- TestPendingCircleCIPublisher updates:
  - Updated test_reify_creates_publisher with vcs fields
  - Updated test_reify_returns_existing_publisher with vcs fields

Also update new_signed_claims helper to accept vcs_ref and vcs_origin.
Add the optional VCS fields to all three CircleCI publisher templates:
- manage/project/publishing.html
- manage/account/publishing.html
- manage/organization/publishing.html

Each field includes:
- Label with (optional) indicator
- Placeholder text showing expected format
- Help text explaining the field's purpose

Amp-Thread-ID: https://ampcode.com/threads/T-019be17b-aad4-7691-8ab4-01828eebb650
Co-authored-by: Amp <amp@ampcode.com>
Regenerate migration with correct down_revision (31ac9b5e1e8b) to chain
after upstream 'Add ADMINISTRATIVE to BanReason enum' migration.

Amp-Thread-ID: https://ampcode.com/threads/T-019bfd26-1169-72d6-81c6-b682c6daea4d
Co-authored-by: Amp <amp@ampcode.com>
Add the attestation_identity property to CircleCIPublisherMixin, following
the pattern established by GitHub, GitLab, and Google publishers.

Currently returns None as pypi-attestations library does not yet have a
CircleCIPublisher identity class. Fulcio already supports CircleCI OIDC
tokens, so this prepares the infrastructure for when upstream support
is added.

Refs: sigstore/fulcio#591
Amp-Thread-ID: https://ampcode.com/threads/T-019bfff2-9b8a-7199-ab46-631fcb14f9d3
Co-authored-by: Amp <amp@ampcode.com>
Add tests for parity with GitHub/GitLab model tests:
- all_known_claims() assertion for claim set stability
- Missing required claims raise KeyError
- DB uniqueness constraint (UniqueViolation) test
- verify_url() returns False for CircleCI
- Multi-constraint lookup tests (vcs_ref, vcs_origin)
- Exact InvalidPublisherError message test
- Parametrized optional claim verification tests

Signed-off-by: meeech <4623+meeech@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c003a-2977-70f9-ad05-cce7fb3236a0
Co-authored-by: Amp <amp@ampcode.com>
Add tests for parity with GitHub/GitLab form tests:
- ProjectNameUnavailable*Error variants (Invalid, Existing, Stdlib, Prohibited, Similar)
- Optional field consistency tests for vcs_ref, vcs_origin, context_id
- Parametrized invalid field tests for required UUID fields
- Empty string vs None consistency for optional fields

Signed-off-by: meeech <4623+meeech@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c003a-2977-70f9-ad05-cce7fb3236a0
Co-authored-by: Amp <amp@ampcode.com>
Fill in actual values for optional CircleCI fields (context_id, vcs_ref,
vcs_origin) to match the pattern used by GitHub/GitLab tests which
populate their optional fields with real values.

Signed-off-by: meeech <4623+meeech@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c003a-2977-70f9-ad05-cce7fb3236a0
Co-authored-by: Amp <amp@ampcode.com>
Add pipeline_definition_id, context_id, vcs_ref, and vcs_origin to the
CircleCI trusted publisher display in manage_base.html. Also update test
factories to include vcs_ref and vcs_origin fields.

Amp-Thread-ID: https://ampcode.com/threads/T-019c2939-421d-757c-9608-2f7b1758b9c0
Co-authored-by: Amp <amp@ampcode.com>
ran make resetdb and make initdb, seems fine
@meeech meeech force-pushed the add-cci-trusted-publisher-take-2 branch from 1e6acfb to bdae7b2 Compare February 18, 2026 17:11
@meeech meeech changed the title feat: add circleci as trusted publisher (take2) [wip] feat: add circleci as trusted publisher Feb 18, 2026
@meeech
Copy link
Author

meeech commented Feb 18, 2026

So I think this is ready for review. I left some open questions about process - eg: would it be preferred for me to add in the attestation work into this pr? or better to leave stacked.

Copy link
Member

@di di left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, looks like translations need updated and also the "revises" field for the database migration as we've had additional migrations since you generated it.

Comment on lines +793 to +795
context_id = form.context_id.data or ""
vcs_ref = form.vcs_ref.data or ""
vcs_origin = form.vcs_origin.data or ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these and other instances of or "" should just be changed to use None? Is there a reason this must be a string and not a nullable field?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this to prevent resurrection attacks (i.e., a new GitHub repo created with the same owner & repository name would have a different repository ID). If CircleCI permits re-using the unique fields being used for the publisher here, then we should do the same thing.

I think we should just do that as part of this PR, otherwise we will end up in a state where we have IDs for new projects going forwards but not ones added before the follow-up PR.

The API key is not the user's key, but our key for making these requests to the GitHub API.

class_="form-group__field",
aria_describedby="circleci_org_id-errors")
}}
<p class="form-group__help-text">{% trans %}The CircleCI organization ID (UUID) that owns the project{% endtrans %}</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something users are aware of? Or should we be getting it via API based on the organization name?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments