Draft: Add the Swift registry list-releases endpoint
What does this MR do and why?
Swift developers can't use GitLab as a package registry today. Swift has had an official registry API since 2021, and this is the first piece of implementing it.
This MR adds one endpoint — the first thing a Swift client asks for:
GET /api/v4/projects/:id/packages/swift/:scope/:name
In plain terms: "what versions of this package do you have?" Everything else a client does, fetching metadata, downloading the source, resolving dependencies, starts from that answer, which is why it's the sensible thing to build first.
To make that work, the MR also adds a swift package type and a Packages::Swift::Package model. The model itself needed no new table: package_type is a plain Ruby enum over a column that already exists, and a Swift package identifier fits in the existing name column.
There is one migration, for the swift_max_file_size plan limit. It lands here rather than with the upload endpoint because spec/models/packages/package_spec.rb walks every entry in Packages::Package.package_types and asks the plan limits for the matching <type>_max_file_size. Adding swift to that enum is what makes the column mandatory, so it cannot wait for the MR that will first read it. The column is also exposed through the admin plan limits API, since spec/requests/api/admin/plan_limits_spec.rb requires every column to be either exposed there or on its deferral list.
Four details in the response are dictated by the Swift specification rather than by GitLab convention, so they're worth calling out:
- Every response carries a
Content-Version: 1header, errors included. This is the Swift registry's own API version, separate from GitLab's/api/v4. - Errors are a different shape than usual. Swift clients expect RFC 7807 "problem details":
application/problem+jsonwith adetailkey, not GitLab's normal{"message": ...}. If we used our usual format the client wouldn't understand the error. - A
Linkheader points at the highest-precedence release, so a client that just wants "the latest" doesn't have to sort versions itself. - A
.jsonsuffix on the URL is accepted. Section 3.5 lets a client ask for JSON explicitly, soGET /mona/LinkedList.jsonnames the same package asGET /mona/LinkedList. The suffix is stripped before anything reads the name, including the canonical path, so both spellings return the same body and the samelatest-versionheader. A package name cannot contain a dot, so there is no real name the stripping can eat into.
Releases are ordered by semantic version precedence rather than by the version column. A database sort compares strings, which puts 1.9.0 above 1.10.0 and 1.2.0-beta.1 above 1.2.0, and the latest-version header inherits whatever the first row is. That ordering cannot be expressed in SQL without the semver_major/minor/patch columns packages_packages does not have, so execute returns an array and the finder is listed in spec/support/finder_collection_allowlist.yml, next to the other finders that reduce in Ruby.
Everything sits behind package_registry_swift_support, which is off by default, so none of this is reachable yet.
Download, manifest, release metadata, identifier lookup and publishing each come in their own MR. So do event tracking and the UI. The full breakdown is on the issue.
References
- Issue: #12233
- Swift registry specification: https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md
- @trizzi confirmed on the issue that we should focus on project-level endpoints first, which is what this is.
- The file structure follows
lib/api/cargo_project_packages.rband the guidance indoc/development/packages/new_format_development.md. pipeline:skip-check-migrationsis applied.db:check-migrationsreports thatai_events_counts, a partitioned table this MR does not touch, moves position in the regenerated schema. This started after 229162fc squashed migrations up to v18.11.0-ee on 2026-10-02 and rewrotedb/init_structure.sql.doc/development/database/dbcheck-migrations-job.mdnames an ordering complaint about an unrelated object as a known false positive and prescribes this label.pipeline:skip-router-syncis applied, for unrelated snapshot drift in the HTTP Router manifest.
Things needing a Validation
Swift identifies a package as scope.name: for example mona.LinkedList. I'm storing that whole string in packages_packages.name, validating each half against the patterns the specification gives, and matching case-insensitively (the spec says both halves are case-insensitive).
That choice is why this MR needs no new table.
The alternative would be to treat the scope as the project's namespace instead. I don't have a strong view, but every later MR inherits whichever we pick, so it's much cheaper to change now than after publishing and downloading are built on top of it. Happy to redo it if you'd prefer the other approach.
Three other things worth a maintainer's view:
The finder is on the finder_collection_allowlist because semantic version ordering has to happen in Ruby. That is a convention deviation rather than something I would decide alone. The alternative is approximating semver in SQL the way SemanticVersionable#order_by_semantic_version_desc does, which keeps the relation but is an approximation of prerelease ordering and would pull in a database review.
With a prerelease present, the highest-precedence release is the prerelease, so latest-version can point at 2.0.0-beta. That is what section 4.1 says literally, but most registries exclude prereleases from "latest". Worth settling before the download MR builds on it.
Content-Version and the problem-details error shape apply only after the feature flag check, so a 401, a 403, a flag-off response and a project that cannot be found all use GitLab's usual shape. I have left that as it is: with the flag off the registry does not exist, and answering in the Swift error format would advertise an API we are not serving. Happy to change it if you read the spec differently.
Differences
Before: GitLab has no Swift package registry. Pointing swift package-registry set at a GitLab project gives you nothing to talk to.
After: With the feature flag enabled, a project serves a Swift registry at /api/v4/projects/:id/packages/swift/. Asking it about a package returns the versions available in that project, ordered by semantic version precedence, with the headers and error format Swift clients expect, whether or not the client appends .json to the URL. With the flag off, which is the default, nothing changes anywhere.
How to set up and validate locally
- Turn the flag on:
Feature.enable(:package_registry_swift_support) - Create a few packages, including versions a string sort would order wrongly:
%w[1.0.0 1.2.0 1.9.0 1.10.0 2.0.0-beta].each do |version|
create(:swift_package, project: project, name: 'mona.LinkedList', version: version)
end- Ask for it:
curl --include --header "Authorization: Bearer <token>" \
"http://gdk.test:3000/api/v4/projects/<id>/packages/swift/mona/LinkedList"You should get a releases object keyed by version in the order 2.0.0-beta, 1.10.0, 1.9.0, 1.2.0, 1.0.0, a Content-Version: 1 header, and a Link header ending in rel="latest-version" pointing at 2.0.0-beta. If 1.9.0 appears above 1.10.0, the ordering has regressed to a string sort.
- Append
.jsonto the same URL. The body should be byte-for-byte what step 3 returned, release URLs included, and thelatest-versionheader should be the same. A release URL containing.jsonwould mean the suffix leaked into the canonical path. - Try a package that doesn't exist. You should get a
404withContent-Type: application/problem+jsonand a body of{"detail": "package not found"}, not GitLab's usual error shape.
Running the specs:
bundle exec rspec spec/models/packages/swift/package_spec.rb \
spec/finders/packages/swift/package_finder_spec.rb \
spec/requests/api/swift_project_packages_spec.rbChecklist
- Project-level endpoints only, as agreed on the issue. Group and instance levels are explicitly out of scope for now.
- Behind a WIP feature flag that's off by default, so nothing is reachable until someone enables it.
- Follows the Swift specification for the response shape, the
Content-Versionheader, thelatest-versionlink, the problem-details error format, and the optional.jsonsuffix. - Scope and name validated with the specification's own patterns and matched case-insensitively. The patterns are bounded and consume one character per step, so they can't backtrack badly.
- One migration, for the
swift_max_file_sizeplan limit, explained above. The column is exposed through the admin plan limits API and in the generated API reference and OpenAPI document. - Query logic lives in a finder rather than in the API file. Release ordering is done in Ruby on semantic version precedence, with the finder on the allowlist as a result.
- Generated files refreshed for the new package type and route: the GraphQL reference and both introspection documents,
db/docs/packages_packages.yml, the Cells routing manifest, the OpenAPI document and the granular token docs. The.jsonsuffix changes no route pattern, so nothing needed regenerating for it. - Specs cover the release listing and its ordering, including versions a string sort gets wrong, both headers, the not-found and invalid-identifier errors, case-insensitive lookup, the
.jsonsuffix returning the same response as the bare URL, the flag being off, and the auth matrix for public and private projects, including deploy tokens and job tokens. - RuboCop passes and every changed Ruby file parses.
- Kept to one reviewable slice, with the rest broken out on the issue.
-
pipeline:skip-check-migrationsandpipeline:skip-router-syncapplied, both for drift unrelated to this change. See References.