Skip to content

Conversation

@heckerdj
Copy link

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:

  • Public PyPI is blocked by firewall
  • Internal repositories (Nexus, Artifactory, etc.) are used for security-scanned packages
  • Custom package indices need to be configured

Changes

  • action.yml: Added three new optional inputs:

    • pypi-url: URL of the custom PyPI repository
    • pypi-username: Username for authentication
    • pypi-password: Password or token for authentication
  • src/utils.ts: Implemented configurePipRepository() function that:

    • Creates pip.conf (Linux/macOS) or pip.ini (Windows) with proper configuration
    • Embeds credentials securely in the repository URL
    • Automatically masks passwords in logs using core.setSecret()
    • Handles missing or partial credentials gracefully
  • 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:

    • Configuration with URL only
    • Configuration with credentials
    • Empty URL handling
    • Partial credential warnings
    • Directory creation
  • README.md: Added documentation with usage examples and security notes

Usage Example

steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
  with:
    python-version: '3.13'
    pypi-url: ${{ secrets.PYPI_REPO_URL }}
    pypi-username: ${{ secrets.PYPI_USER }}
    pypi-password: ${{ secrets.PYPI_PASSWORD }}
- run: pip install -r requirements.txt

Testing

All unit tests pass successfully:

  • ✅ Creates pip config file with URL only
  • ✅ Creates pip config file with credentials
  • ✅ Does nothing when pypiUrl is not provided
  • ✅ Warns when only username is provided
  • ✅ Warns when only password is provided
  • ✅ Creates config directory if it does not exist

Fixes #814

- 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
@heckerdj heckerdj requested a review from a team as a code owner December 16, 2025 16:41
Copilot AI review requested due to automatic review settings December 16, 2025 16:41
Copy link
Contributor

Copilot AI left a 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.

heckerdj and others added 2 commits December 19, 2025 08:08
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +383 to +486
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();
});
});
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Author

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}`
);
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
);
);
throw error instanceof Error
? error
: new Error(
`Failed to create pip config file at ${configPath}: ${error}`
);

Copilot uses AI. Check for mistakes.
Comment on lines +493 to +494
core.setFailed(
`Failed to create pip config file at ${configPath}: ${error}`
Copy link

Copilot AI Dec 19, 2025

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.

Suggested change
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}`

Copilot uses AI. Check for mistakes.
Comment on lines +433 to +437
export async function configurePipRepository(
pypiUrl: string,
username?: string,
password?: string
): Promise<void> {
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +445 to +450
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);
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.

// Write the config file
try {
fs.writeFileSync(configPath, configContent, {encoding: 'utf8'});
Copy link

Copilot AI Dec 19, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Author

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

heckerdj and others added 3 commits December 20, 2025 13:34
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>
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.

Optional ability to set the pypi repository and credentials to be used by pip on the runner

1 participant