-
Notifications
You must be signed in to change notification settings - Fork 690
Add support for custom PyPI repository configuration #1260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add pypi-url, pypi-username, and pypi-password inputs to action.yml - Implement configurePipRepository() function in utils.ts to create pip.conf/pip.ini - Integrate pip configuration into setup-python.ts workflow - Add comprehensive unit tests for pip configuration functionality - Update README.md with usage examples and documentation - Automatically mask credentials in logs for security Fixes actions#814
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for configuring pip to use custom PyPI repositories with authentication, enabling users in enterprise environments to work with internal package indices (Nexus, Artifactory, etc.) when public PyPI is blocked or security-scanned packages are required.
Key changes:
- Added three new optional inputs (
pypi-url,pypi-username,pypi-password) to configure custom PyPI repositories - Implemented
configurePipRepository()function to create platform-specific pip configuration files with embedded credentials - Integrated pip configuration into the setup workflow between cache restoration and package installation
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| action.yml | Defines three new optional inputs for custom PyPI repository configuration |
| src/utils.ts | Implements configurePipRepository() function to create pip config files with credentials |
| src/setup-python.ts | Integrates pip repository configuration into the main setup workflow |
| tests/utils.test.ts | Adds comprehensive unit tests for the pip configuration functionality |
| README.md | Documents the new feature with usage examples and security notes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| describe('configurePipRepository', () => { | ||
| const originalHome = process.env.HOME; | ||
| const originalUserProfile = process.env.USERPROFILE; | ||
| const testHome = path.join(tempDir, 'home'); | ||
|
|
||
| beforeEach(() => { | ||
| // Setup test home directory | ||
| process.env.HOME = testHome; | ||
| process.env.USERPROFILE = testHome; | ||
| if (fs.existsSync(testHome)) { | ||
| fs.rmSync(testHome, {recursive: true, force: true}); | ||
| } | ||
| fs.mkdirSync(testHome, {recursive: true}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| // Cleanup | ||
| if (fs.existsSync(testHome)) { | ||
| fs.rmSync(testHome, {recursive: true, force: true}); | ||
| } | ||
| process.env.HOME = originalHome; | ||
| process.env.USERPROFILE = originalUserProfile; | ||
| }); | ||
|
|
||
| it('creates pip config file with URL only', async () => { | ||
| const pypiUrl = 'https://nexus.example.com/repository/pypi/simple'; | ||
| await configurePipRepository(pypiUrl); | ||
|
|
||
| const configDir = IS_WINDOWS | ||
| ? path.join(testHome, 'pip') | ||
| : path.join(testHome, '.pip'); | ||
| const configFile = IS_WINDOWS ? 'pip.ini' : 'pip.conf'; | ||
| const configPath = path.join(configDir, configFile); | ||
|
|
||
| expect(fs.existsSync(configPath)).toBeTruthy(); | ||
| const content = fs.readFileSync(configPath, 'utf8'); | ||
| expect(content).toContain('[global]'); | ||
| expect(content).toContain(`index-url = ${pypiUrl}`); | ||
| }); | ||
|
|
||
| it('creates pip config file with credentials', async () => { | ||
| const pypiUrl = 'https://nexus.example.com/repository/pypi/simple'; | ||
| const username = 'testuser'; | ||
| const password = 'testpass'; | ||
| await configurePipRepository(pypiUrl, username, password); | ||
|
|
||
| const configDir = IS_WINDOWS | ||
| ? path.join(testHome, 'pip') | ||
| : path.join(testHome, '.pip'); | ||
| const configFile = IS_WINDOWS ? 'pip.ini' : 'pip.conf'; | ||
| const configPath = path.join(configDir, configFile); | ||
|
|
||
| expect(fs.existsSync(configPath)).toBeTruthy(); | ||
| const content = fs.readFileSync(configPath, 'utf8'); | ||
| expect(content).toContain('[global]'); | ||
| const encodedUsername = encodeURIComponent(username); | ||
| const encodedPassword = encodeURIComponent(password); | ||
| expect(content).toContain(`index-url = https://${encodedUsername}:${encodedPassword}@`); | ||
| expect(content).toContain('nexus.example.com/repository/pypi/simple'); | ||
| }); | ||
|
|
||
| it('does nothing when pypiUrl is not provided', async () => { | ||
| await configurePipRepository(''); | ||
|
|
||
| const configDir = IS_WINDOWS | ||
| ? path.join(testHome, 'pip') | ||
| : path.join(testHome, '.pip'); | ||
|
|
||
| expect(fs.existsSync(configDir)).toBeFalsy(); | ||
| }); | ||
|
|
||
| it('warns when only username is provided', async () => { | ||
| const warningMock = jest.spyOn(core, 'warning'); | ||
| const pypiUrl = 'https://nexus.example.com/repository/pypi/simple'; | ||
| const username = 'testuser'; | ||
| await configurePipRepository(pypiUrl, username); | ||
|
|
||
| expect(warningMock).toHaveBeenCalledWith( | ||
| 'Both pypi-username and pypi-password must be provided for authentication. Configuring without credentials.' | ||
| ); | ||
| }); | ||
|
|
||
| it('warns when only password is provided', async () => { | ||
| const warningMock = jest.spyOn(core, 'warning'); | ||
| const pypiUrl = 'https://nexus.example.com/repository/pypi/simple'; | ||
| const password = 'testpass'; | ||
| await configurePipRepository(pypiUrl, undefined, password); | ||
|
|
||
| expect(warningMock).toHaveBeenCalledWith( | ||
| 'Both pypi-username and pypi-password must be provided for authentication. Configuring without credentials.' | ||
| ); | ||
| }); | ||
|
|
||
| it('creates config directory if it does not exist', async () => { | ||
| const pypiUrl = 'https://nexus.example.com/repository/pypi/simple'; | ||
| const configDir = IS_WINDOWS | ||
| ? path.join(testHome, 'pip') | ||
| : path.join(testHome, '.pip'); | ||
|
|
||
| expect(fs.existsSync(configDir)).toBeFalsy(); | ||
| await configurePipRepository(pypiUrl); | ||
| expect(fs.existsSync(configDir)).toBeTruthy(); | ||
| }); | ||
| }); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no test coverage for the error handling path when URL parsing fails or when file writing fails. Consider adding tests that verify the behavior when an invalid URL is provided or when the file system operations fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
| } catch (error) { | ||
| core.setFailed( | ||
| `Failed to create pip config file at ${configPath}: ${error}` | ||
| ); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When core.setFailed() is called, it doesn't throw an error or stop execution. The function continues to execute and returns normally. This means that if the config file write fails, the workflow step will be marked as failed but subsequent operations (like pip install) may still attempt to run. Consider throwing an error after setFailed() or restructuring the error handling to ensure the promise rejects.
| ); | |
| ); | |
| throw error instanceof Error | |
| ? error | |
| : new Error( | |
| `Failed to create pip config file at ${configPath}: ${error}` | |
| ); |
| core.setFailed( | ||
| `Failed to create pip config file at ${configPath}: ${error}` |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error object is being directly interpolated into a string, which may not provide useful information. Error objects should be accessed via their message property or converted to string explicitly for better error reporting.
| core.setFailed( | |
| `Failed to create pip config file at ${configPath}: ${error}` | |
| const errorMessage = | |
| error instanceof Error ? error.message : String(error); | |
| core.setFailed( | |
| `Failed to create pip config file at ${configPath}: ${errorMessage}` |
| export async function configurePipRepository( | ||
| pypiUrl: string, | ||
| username?: string, | ||
| password?: string | ||
| ): Promise<void> { |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function is declared as async but doesn't use any await operations. The async keyword is unnecessary and adds overhead. Either remove the async keyword and change the return type from Promise to void, or if file operations should be asynchronous for consistency, use the async versions of fs methods like fs.promises.writeFile.
| const homeDir = process.env.HOME || process.env.USERPROFILE || ''; | ||
| const configDir = IS_WINDOWS | ||
| ? path.join(homeDir, 'pip') | ||
| : path.join(homeDir, '.pip'); | ||
| const configFile = IS_WINDOWS ? 'pip.ini' : 'pip.conf'; | ||
| const configPath = path.join(configDir, configFile); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If both HOME and USERPROFILE environment variables are undefined or empty, homeDir will be an empty string, resulting in config paths like 'pip/pip.ini' or '.pip/pip.conf' in the current working directory instead of the user's home directory. This could cause pip configuration issues. Consider validating that homeDir is not empty and throwing an error or logging a warning if it cannot be determined.
|
|
||
| // Write the config file | ||
| try { | ||
| fs.writeFileSync(configPath, configContent, {encoding: 'utf8'}); |
Copilot
AI
Dec 19, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The config file is written with destructive writeFileSync, which will overwrite any existing pip configuration. If users have existing pip.conf/pip.ini files with other settings (like trusted-hosts, timeout values, etc.), they will be lost. Consider reading and merging with existing configuration, or at minimum documenting this behavior and warning users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Description
This PR adds the ability to configure pip to use a custom PyPI repository with authentication credentials through the setup-python action. This addresses the need for users working in enterprise environments where:
Changes
action.yml: Added three new optional inputs:
pypi-url: URL of the custom PyPI repositorypypi-username: Username for authenticationpypi-password: Password or token for authenticationsrc/utils.ts: Implemented
configurePipRepository()function that:pip.conf(Linux/macOS) orpip.ini(Windows) with proper configurationcore.setSecret()src/setup-python.ts: Integrated pip configuration into the workflow, running after cache restoration but before package installation
tests/utils.test.ts: Added comprehensive unit tests covering:
README.md: Added documentation with usage examples and security notes
Usage Example
Testing
All unit tests pass successfully:
Fixes #814