Skip to content

Commit 9c362b9

Browse files
authored
Merge pull request #43 from Volgaa/main
Feature: allow releasing to multiple pypi repositories
2 parents 96111a7 + c9f232c commit 9c362b9

File tree

7 files changed

+104
-39
lines changed

7 files changed

+104
-39
lines changed

README.md

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# 📦🐍 semantic-release-pypi
2+
23
[semantic-release](https://semantic-release.gitbook.io/semantic-release/) plugin to publish a python package to PyPI
34

45
<a href="https://www.npmjs.com/package/semantic-release-pypi">
@@ -19,7 +20,8 @@
1920

2021
- `pyproject.toml` based (Recommended)
2122
- `version` will be set inside `pyproject.toml` - [PEP 621](https://peps.python.org/pep-0621/)
22-
- The build backend can be specified inside `pyproject.toml` (defaults to `setuptools`) - [PEP 518](https://peps.python.org/pep-0518/)
23+
- The build backend can be specified inside `pyproject.toml` (defaults
24+
to `setuptools`) - [PEP 518](https://peps.python.org/pep-0518/)
2325

2426
<br />
2527

@@ -30,35 +32,37 @@
3032

3133
## Steps
3234

33-
| Step | Description
34-
| ---- | -----------
35-
| ```verifyConditions``` | <ul><li>verify the environment variable ```PYPI_TOKEN```</li><li>verify ```PYPI_TOKEN``` is authorized to publish on the specified repository</li><li>check if the packages `setuptools`, `wheel` and `twine` are installed</li></ul>
36-
| ```prepare``` | Update the version in `pyproject.toml` (legacy: `setup.cfg`) and create the distribution packages
37-
| ```publish``` | Publish the python package to the specified repository (default: pypi)
35+
| Step | Description
36+
|------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
37+
| ```verifyConditions``` | <ul><li>verify the environment variable ```PYPI_TOKEN```</li><li>verify ```PYPI_TOKEN``` is authorized to publish on the specified repository</li><li>check if the packages `setuptools`, `wheel` and `twine` are installed</li></ul>
38+
| ```prepare``` | Update the version in `pyproject.toml` (legacy: `setup.cfg`) and create the distribution packages
39+
| ```publish``` | Publish the python package to the specified repository (default: pypi)
3840

3941
## Environment variables
4042

41-
| Variable | Description | Required | Default
42-
| -------- | ----------- | ----------- | -----------
43-
| ```PYPI_TOKEN``` | [API token](https://test.pypi.org/help/#apitoken) for PyPI | true |
44-
| ```PYPI_USERNAME``` | Username for PyPI | false | ```__token__```
45-
| ```PYPI_REPO_URL``` | Repo URL for PyPI | false | See [Options](#options)
43+
| Variable | Description | Required | Default
44+
|---------------------|------------------------------------------------------------|----------|-------------------------
45+
| ```PYPI_TOKEN``` | [API token](https://test.pypi.org/help/#apitoken) for PyPI | true |
46+
| ```PYPI_USERNAME``` | Username for PyPI | false | ```__token__```
47+
| ```PYPI_REPO_URL``` | Repo URL for PyPI | false | See [Options](#options)
4648

4749
## Usage
4850

49-
The plugin can be configured in the [**semantic-release** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration). Here is a minimal example:
51+
The plugin can be configured in the [**semantic-release
52+
** configuration file](https://github.com/semantic-release/semantic-release/blob/master/docs/usage/configuration.md#configuration).
53+
Here is a minimal example:
5054

5155
```json
5256
{
5357
"plugins": [
5458
"@semantic-release/commit-analyzer",
5559
"@semantic-release/release-notes-generator",
56-
"semantic-release-pypi",
60+
"semantic-release-pypi"
5761
]
5862
}
5963
```
6064

61-
Note that this plugin modifies the version inside of `pyproject.toml` (legacy: `setup.cfg`).
65+
Note that this plugin modifies the version inside of `pyproject.toml` (legacy: `setup.cfg`).
6266
Make sure to commit `pyproject.toml` using the `@semantic-release/git` plugin, if you want to save the changes:
6367

6468
```json
@@ -70,31 +74,65 @@ Make sure to commit `pyproject.toml` using the `@semantic-release/git` plugin, i
7074
[
7175
"@semantic-release/git",
7276
{
73-
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
74-
"assets": ["pyproject.toml"]
77+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
78+
"assets": [
79+
"pyproject.toml"
80+
]
7581
}
7682
]
7783
]
7884
}
7985
```
8086

8187
Working examples using Github Actions can be found here:
88+
8289
- [semantic-release-pypi-pyproject](https://github.com/abichinger/semantic-release-pypi-pyproject)
8390
- [semantic-release-pypi-setup](https://github.com/abichinger/semantic-release-pypi-setup)
8491

8592
## Options
8693

87-
| Option | Type | Default | Description
88-
| ------ | ---- | ------- | -----------
89-
| ```srcDir``` | str | ```.``` | source directory (defaults to current directory)
90-
| ```distDir``` | str | ```dist``` | directory to put the source distribution archive(s) in, relative to ```srcDir```
91-
| ```repoUrl``` | str | ```https://upload.pypi.org/legacy/``` | The repository (package index) to upload the package to.
92-
| ```pypiPublish``` | bool | ```true``` | Whether to publish the python package to the pypi registry. If false the package version will still be updated.
93-
| ```gpgSign``` | bool | ```false``` | Whether to sign the package using GPG. A valid PGP key must already be installed and configured on the host.
94-
| ```gpgIdentity``` | str | ```null``` | When ```gpgSign``` is true, set the GPG identify to use when signing files. Leave empty to use the default identity.
95-
| ```envDir``` | string \| ```false``` | ```.venv``` | directory to create the virtual environment in, if set to `false` no environment will be created
96-
| ```installDeps``` | bool | ```true``` | wether to automatically install python dependencies
97-
| ```versionCmd``` | string | ```undefined``` | Run a custom command to update the version (e.g. `hatch version ${version}`). `srcDir` is used as working directory. `versionCmd` is required if the version is set [dynamically](https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic)
94+
| Option | Type | Default | Description
95+
|--------------------|-----------------------|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
96+
| ```srcDir``` | str | ```.``` | source directory (defaults to current directory)
97+
| ```distDir``` | str | ```dist``` | directory to put the source distribution archive(s) in, relative to ```srcDir```
98+
| ```repoUrl``` | str | ```https://upload.pypi.org/legacy/``` | The repository (package index) to upload the package to.
99+
| ```repoUsername``` | str | ```__token__``` | The repository username.
100+
| ```repoToken``` | str | | The repository token. It's safer to set via PYPI_TOKEN environment variable.
101+
| ```pypiPublish``` | bool | ```true``` | Whether to publish the python package to the pypi registry. If false the package version will still be updated.
102+
| ```gpgSign``` | bool | ```false``` | Whether to sign the package using GPG. A valid PGP key must already be installed and configured on the host.
103+
| ```gpgIdentity``` | str | ```null``` | When ```gpgSign``` is true, set the GPG identify to use when signing files. Leave empty to use the default identity.
104+
| ```envDir``` | string \| ```false``` | ```.venv``` | directory to create the virtual environment in, if set to `false` no environment will be created
105+
| ```installDeps``` | bool | ```true``` | wether to automatically install python dependencies
106+
| ```versionCmd``` | string | ```undefined``` | Run a custom command to update the version (e.g. `hatch version ${version}`). `srcDir` is used as working directory. `versionCmd` is required if the version is set [dynamically](https://packaging.python.org/en/latest/specifications/pyproject-toml/#dynamic)
107+
108+
## Publishing to multiple repositories
109+
110+
Using `release.config.js` you can read repository credentials from environment variables and publish to multiple
111+
repositories.
112+
113+
```js
114+
module.exports = {
115+
"plugins": [
116+
[
117+
"semantic-release-pypi",
118+
{
119+
"repoUrl": process.env['REPOSITORY_1_URL'],
120+
"repoUsername": process.env['REPOSITORY_1_USERNAME'],
121+
"repoToken": process.env['REPOSITORY_1_TOKEN']
122+
}
123+
],
124+
[
125+
"semantic-release-pypi",
126+
{
127+
"repoUrl": process.env['REPOSITORY_2_URL'],
128+
"repoUsername": process.env['REPOSITORY_2_USERNAME'],
129+
"repoToken": process.env['REPOSITORY_2_TOKEN']
130+
}
131+
]
132+
]
133+
}
134+
135+
```
98136

99137
## Development
100138

lib/default-options.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ export class DefaultConfig {
2828
return this.config.repoUrl ?? 'https://upload.pypi.org/legacy/';
2929
}
3030

31+
public get repoUsername() {
32+
return this.config.repoUsername ?? '__token__';
33+
}
34+
35+
public get repoToken() {
36+
return this.config.repoToken ?? '';
37+
}
38+
3139
public get pypiPublish() {
3240
return this.config.pypiPublish ?? true;
3341
}

lib/publish.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ function publishPackage(
99
srcDir: string,
1010
distDir: string,
1111
repoUrl: string,
12+
repoUsername: string,
13+
repoToken: string,
1214
gpgSign: boolean,
1315
gpgIdentity?: string,
1416
options?: Options,
@@ -37,10 +39,8 @@ function publishPackage(
3739
cwd: srcDir,
3840
env: {
3941
...options?.env,
40-
TWINE_USERNAME: process.env['PYPI_USERNAME']
41-
? process.env['PYPI_USERNAME']
42-
: '__token__',
43-
TWINE_PASSWORD: process.env['PYPI_TOKEN'],
42+
TWINE_USERNAME: repoUsername,
43+
TWINE_PASSWORD: repoToken,
4444
},
4545
},
4646
);
@@ -55,6 +55,8 @@ async function publish(pluginConfig: PluginConfig, context: Context) {
5555
gpgSign,
5656
gpgIdentity,
5757
repoUrl,
58+
repoUsername,
59+
repoToken,
5860
envDir,
5961
} = new DefaultConfig(pluginConfig);
6062

@@ -69,6 +71,8 @@ async function publish(pluginConfig: PluginConfig, context: Context) {
6971
srcDir,
7072
distDir,
7173
process.env['PYPI_REPO_URL'] ?? repoUrl,
74+
process.env['PYPI_USERNAME'] ?? repoUsername,
75+
process.env['PYPI_TOKEN'] ?? repoToken,
7276
gpgSign,
7377
gpgIdentity,
7478
options,

lib/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ export interface PluginConfig {
22
srcDir?: string;
33
distDir?: string;
44
repoUrl?: string;
5+
repoUsername?: string;
6+
repoToken?: string;
57
pypiPublish?: boolean;
68
gpgSign?: boolean;
79
gpgIdentity?: string;

lib/verify.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,31 @@ function assertVersionCmd(pyproject: any, versionCmd?: string) {
9090

9191
async function verify(pluginConfig: PluginConfig, context: Context) {
9292
const { logger } = context;
93-
const { srcDir, setupPath, pypiPublish, repoUrl, versionCmd } =
94-
new DefaultConfig(pluginConfig);
93+
const {
94+
srcDir,
95+
setupPath,
96+
pypiPublish,
97+
repoUrl,
98+
repoUsername,
99+
repoToken,
100+
versionCmd,
101+
} = new DefaultConfig(pluginConfig);
95102

96103
const execaOptions: Options = pipe(context);
97104

98105
if (pypiPublish !== false) {
99-
const username = process.env['PYPI_USERNAME']
100-
? process.env['PYPI_USERNAME']
101-
: '__token__';
102-
const token = process.env['PYPI_TOKEN'];
103106
const repo = process.env['PYPI_REPO_URL'] ?? repoUrl;
107+
const username = process.env['PYPI_USERNAME'] ?? repoUsername;
108+
const token = process.env['PYPI_TOKEN'] ?? repoToken;
104109

105-
assertEnvVar('PYPI_TOKEN');
110+
if (token === '') {
111+
throw new Error(
112+
`Token is not set. Either set PYPI_TOKEN environment variable or repoToken in plugin configuration`,
113+
);
114+
}
106115

107116
logger.log(`Verify authentication for ${username}@${repo}`);
108-
await verifyAuth(repo, username, token!);
117+
await verifyAuth(repo, username, token);
109118
}
110119

111120
if (isLegacyBuildInterface(srcDir)) {

test/util.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ test('test DefaultConfig', async () => {
77
expect(new DefaultConfig({}).srcDir).toBe('.');
88
expect(new DefaultConfig({}).setupPath).toBe('setup.py');
99
expect(new DefaultConfig({}).repoUrl).toBe('https://upload.pypi.org/legacy/');
10+
expect(new DefaultConfig({}).repoUsername).toBe('__token__');
11+
expect(new DefaultConfig({}).repoToken).toBe('');
1012
expect(new DefaultConfig({ distDir: 'mydist' }).distDir).toBe('mydist');
1113
});
1214

test/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ function genPluginArgs(config: PluginConfig) {
129129
distDir: config.distDir ?? `.tmp/${packageName}/dist`,
130130
envDir: config.envDir ?? `.tmp/${packageName}/.venv`,
131131
repoUrl: config.repoUrl ?? 'https://test.pypi.org/legacy/',
132+
repoUsername: config.repoUsername ?? '__token__',
133+
repoToken: config.repoToken ?? '',
132134
};
133135

134136
const context: Context = {

0 commit comments

Comments
 (0)