You already have a build job that produces a wheel, and a private repository that should receive it. The mistake is to let a second job rebuild from a branch and call the result the same release. Move the file that was tested, name it by digest, and keep the upload credential on the inside. The walkthrough below follows one artifact from a public CI runner to an internal publisher.

Build where the source already lives

Run the build in the job that already has the source, the toolchain and the test suite. Nothing about a private repository requires that job to reach it. Install the build frontend in the environment the project already uses, then produce both distribution formats if the project ships both.

python -m build --sdist --wheel --outdir dist
python -m twine check dist/*

Keep the output directory small and disposable. The only things worth carrying forward are the files the next job will publish, plus their hashes. Everything else, including virtual environments and caches, can be rebuilt from the recorded source revision. A lean artifact is also easier to review, because every file in it is a file someone can inspect.

Record what produced them: the package name and version from the file names, the source revision, and the workflow run that created them. A version string alone does not identify a file, because the same version can be built twice from different commits. Write the revision next to the file names in the artifact description, and treat that record as part of the handoff.

Move bytes, not instructions to rebuild

A second build is a second product. Timestamps, bundled dependencies and tool versions can all change the bytes, so a rebuild needs its own test evidence and its own review. Promote the reviewed files instead.

Upload the distribution files to the artifact store your CI platform provides, and give the internal job a reference to that specific artifact rather than a branch name.

sha256sum dist/*.whl dist/*.tar.gz
# keep the digests beside the run id in the artifact description

If your platform supports signed provenance, ask it to attest the artifact and later verify the expected repository, workflow and source revision. GitHub's attestation documentation describes claims of that shape. A valid signature is origin evidence, not a vulnerability scan and not an authorization decision.

Verify on the inside before uploading

The internal job starts by checking that the file it received is the file that was approved. Compare digests against the record from the build job, not against a value recomputed from whatever arrived.

sha256sum -c digests.txt
python -m twine check dist/*

Then read the metadata. The package name, the version and the declared dependencies should match the review you approved. A wheel whose name looks right but whose dependency pins changed is a different package wearing the same label. If the metadata moved, treat the artifact as unapproved and find out which build produced it.

If the digests do not match, stop. Do not upload, and do not repair the file by rebuilding it in place; that erases the evidence that something changed in transit. A mismatch means the artifact store, the download step or the record is wrong, and each of those deserves an explanation before a release is published.

Handle the duplicate-version answer

Private repositories usually refuse to overwrite an existing version, and that refusal is a feature. When an upload fails because the version exists, inspect the repository's record for that version and compare the files it holds with the digests you intended to publish.

An existing version can mean an earlier upload succeeded and the response was lost, or it can mean someone published different content under the same number.

Do not delete the version, and do not invent a new number to get past the error. Decide with the release owner whether the stored content is acceptable, then record the outcome. If the content differs, you have a versioning problem, not a pipeline problem.

Keep the upload credential on one side

Only the internal publisher needs repository credentials, and only for the length of the upload. Scope the token to the target repository and to upload operations, keep it in the internal system's secret store, and leave it out of the build job entirely. If the build job can publish, then anyone who can change the build can publish.

Restrict which internal workflows may start the publisher, too. A manual trigger with a required approval is a reasonable default for releases; a push to any branch is not.

The finished release should leave three things behind: the distribution files, their digests with the source revision, and a repository record showing the same version.

Ũhoro ũhĩtĩte