Files
self/app/scripts/setup-private-modules.cjs
Javier Cortejoso 4b09e5b96f Remove personal access token (#1481)
* Refactor NFC scanner tests to use a global variable for platform OS, allowing dynamic switching between iOS and Android during tests. This change improves test isolation and avoids hoisting issues with jest.mock.

* feat: add GitHub App token generation action for self repositories

- Introduced a new action to generate GitHub App tokens for accessing repositories within the selfxyz organization.
- Updated multiple workflows to utilize the new action for token generation, ensuring secure access to private repositories during CI processes.
- Modified Podfile and scripts to support authentication using the generated token, enhancing the cloning of private modules in CI environments.

* chore: enhance CI workflows with Git authentication for CocoaPods

- Updated multiple CI workflows to include a step for configuring Git authentication for CocoaPods, ensuring secure access to private repositories without embedding credentials in URLs.
- Added masking for sensitive tokens in logs to enhance security during CI processes.
- Modified the Podfile to avoid printing authentication details in CI logs, improving overall security practices.

* chore: enhance CI workflows with optional Git authentication configuration

- Added new inputs to the GitHub action for generating GitHub tokens, allowing optional configuration of a ~/.netrc entry for Git authentication.
- Updated multiple CI workflows to utilize the new configuration, improving security and simplifying access to private repositories during builds.
- Removed redundant Git authentication steps from workflows, streamlining the CI process while maintaining secure access to necessary resources.

* chore: update Podfile for secure Git authentication in CI

- Modified the Podfile to enhance security by avoiding the embedding of credentials in URLs for accessing the NFCPassportReader repository during CI processes.
- Added comments to guide developers on using workflow-provided authentication methods, improving overall security practices in the project.
2025-12-12 12:38:23 +01:00

328 lines
9.3 KiB
JavaScript

// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Constants
const SCRIPT_DIR = __dirname;
const APP_DIR = path.dirname(SCRIPT_DIR);
const ANDROID_DIR = path.join(APP_DIR, 'android');
const GITHUB_ORG = 'selfxyz';
const BRANCH = 'main';
const PRIVATE_MODULES = [
{
repoName: 'android-passport-nfc-reader',
localPath: path.join(ANDROID_DIR, 'android-passport-nfc-reader'),
validationFiles: ['app/build.gradle', 'app/src/main/AndroidManifest.xml'],
},
{
repoName: 'react-native-passport-reader',
localPath: path.join(ANDROID_DIR, 'react-native-passport-reader'),
validationFiles: ['android/build.gradle'],
},
];
// Environment detection
// CI is set by GitHub Actions, CircleCI, etc. Check for truthy value
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
const repoToken = process.env.SELFXYZ_INTERNAL_REPO_PAT;
const appToken = process.env.SELFXYZ_APP_TOKEN; // GitHub App installation token
const isDryRun = process.env.DRY_RUN === 'true';
// Platform detection for Android-specific modules
function shouldSetupAndroidModule() {
// In CI, check for platform-specific indicators
if (isCI) {
const platform = process.env.PLATFORM || process.env.INPUT_PLATFORM;
if (platform === 'ios') {
log('Detected iOS platform, skipping Android module setup', 'info');
return false;
}
if (platform === 'android') {
log(
'Detected Android platform, proceeding with Android module setup',
'info',
);
return true;
}
}
// For local development, only setup if Android directory exists and we're likely building Android
if (fs.existsSync(ANDROID_DIR)) {
log('Android directory detected for local development', 'info');
return true;
}
log(
'No Android build context detected, skipping Android module setup',
'warning',
);
return false;
}
function log(message, type = 'info') {
const prefix =
{
info: '🔧',
success: '✅',
warning: '⚠️',
error: '❌',
cleanup: '🗑️',
}[type] || '📝';
console.log(`${prefix} ${message}`);
}
function runCommand(command, options = {}) {
const defaultOptions = {
stdio: isDryRun ? 'pipe' : 'inherit',
cwd: ANDROID_DIR,
encoding: 'utf8',
...options,
};
// Sanitize command for logging to prevent credential exposure
const sanitizedCommand = sanitizeCommandForLogging(command);
try {
if (isDryRun) {
log(`[DRY RUN] Would run: ${sanitizedCommand}`, 'info');
return '';
}
log(`Running: ${sanitizedCommand}`, 'info');
return execSync(command, defaultOptions);
} catch (error) {
log(`Failed to run: ${sanitizedCommand}`, 'error');
log(`Error: ${error.message}`, 'error');
throw error;
}
}
function sanitizeCommandForLogging(command) {
// Replace any https://token@github.com patterns with https://[REDACTED]@github.com
return command.replace(
/https:\/\/[^@]+@github\.com/g,
'https://[REDACTED]@github.com',
);
}
function removeExistingModule(modulePath, repoName) {
if (fs.existsSync(modulePath)) {
log(`Removing existing ${repoName}...`, 'cleanup');
if (!isDryRun) {
// Force remove even if it's a git repo
fs.rmSync(modulePath, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 1000,
});
}
log(`Removed existing ${repoName}`, 'success');
}
}
// some of us connect to github via SSH, others via HTTPS with gh auth
function usingHTTPSGitAuth() {
try {
const authData = runCommand(`gh auth status`, { stdio: 'pipe' });
const authInfo = authData.toString();
return (
authInfo.includes('Logged in to github.com account') &&
authInfo.includes('Git operations protocol: https')
);
} catch {
console.info(
'gh auth status failed, assuming no HTTPS auth -- will try SSH',
);
return false;
}
}
function clonePrivateRepo(repoName, localPath) {
log(`Setting up ${repoName}...`, 'info');
let cloneUrl;
if (isCI && appToken) {
// CI environment with GitHub App installation token
log('CI detected: Using SELFXYZ_APP_TOKEN for clone', 'info');
cloneUrl = `https://x-access-token:${appToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
} else if (isCI && repoToken) {
// CI environment with Personal Access Token
log('CI detected: Using SELFXYZ_INTERNAL_REPO_PAT for clone', 'info');
cloneUrl = `https://${repoToken}@github.com/${GITHUB_ORG}/${repoName}.git`;
} else if (isCI) {
log(
'CI environment detected but no token available - skipping private module setup',
'info',
);
log(
'This is expected for forked PRs or environments without access to private modules',
'info',
);
return false; // Return false to indicate clone was skipped
} else if (usingHTTPSGitAuth()) {
cloneUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
} else {
// Local development with SSH
log('Local development: Using SSH for clone', 'info');
cloneUrl = `git@github.com:${GITHUB_ORG}/${repoName}.git`;
}
// Security: Use quiet mode for credentialed URLs to prevent token exposure
const isCredentialedUrl = isCI && (appToken || repoToken);
const quietFlag = isCredentialedUrl ? '--quiet' : '';
const targetDir = path.basename(localPath);
const cloneCommand = `git clone --branch ${BRANCH} --single-branch --depth 1 ${quietFlag} "${cloneUrl}" "${targetDir}"`;
try {
if (isCredentialedUrl) {
// Security: Run command silently to avoid token exposure in logs
runCommand(cloneCommand, { stdio: 'pipe' });
} else {
runCommand(cloneCommand);
}
log(`Successfully cloned ${repoName}`, 'success');
return true; // Return true to indicate successful clone
} catch (error) {
if (isCI) {
log(
'Clone failed in CI environment. Check SELFXYZ_APP_TOKEN or SELFXYZ_INTERNAL_REPO_PAT permissions.',
'error',
);
} else {
log(
'Clone failed. Ensure you have SSH access to the repository.',
'error',
);
}
throw error;
}
}
function validateSetup(modulePath, validationFiles, repoName) {
for (const file of validationFiles) {
const filePath = path.join(modulePath, file);
if (!fs.existsSync(filePath)) {
throw new Error(`Expected file not found in ${repoName}: ${file}`);
}
}
log(`${repoName} validation passed`, 'success');
}
function setupPrivateModule(module) {
const { repoName, localPath, validationFiles } = module;
log(`Starting setup of ${repoName}...`, 'info');
// Remove existing module
removeExistingModule(localPath, repoName);
// Clone the private repository
const cloneSuccessful = clonePrivateRepo(repoName, localPath);
// If clone was skipped (e.g., in forked PRs), exit gracefully
if (cloneSuccessful === false) {
log(`${repoName} setup skipped - private module not available`, 'warning');
return false;
}
// Security: Remove credential-embedded remote URL after clone
if (isCI && (appToken || repoToken) && !isDryRun) {
scrubGitRemoteUrl(localPath, repoName);
}
// Validate the setup
if (!isDryRun) {
validateSetup(localPath, validationFiles, repoName);
}
log(`${repoName} setup complete!`, 'success');
return true;
}
function setupAndroidPassportReader() {
// Ensure android directory exists
if (!fs.existsSync(ANDROID_DIR)) {
throw new Error(`Android directory not found: ${ANDROID_DIR}`);
}
log(
`Starting setup of ${PRIVATE_MODULES.length} private module(s)...`,
'info',
);
let successCount = 0;
for (const module of PRIVATE_MODULES) {
try {
const success = setupPrivateModule(module);
if (success) {
successCount++;
}
} catch (error) {
log(`Failed to setup ${module.repoName}: ${error.message}`, 'error');
throw error;
}
}
if (successCount === PRIVATE_MODULES.length) {
log('All private modules setup complete!', 'success');
} else if (successCount > 0) {
log(
`Setup complete: ${successCount}/${PRIVATE_MODULES.length} modules cloned`,
'warning',
);
} else {
log(
'No private modules were cloned - this is expected for forked PRs',
'info',
);
}
}
function scrubGitRemoteUrl(modulePath, repoName) {
try {
const cleanUrl = `https://github.com/${GITHUB_ORG}/${repoName}.git`;
const scrubCommand = `cd "${modulePath}" && git remote set-url origin "${cleanUrl}"`;
log(`Scrubbing credential from git remote URL for ${repoName}...`, 'info');
runCommand(scrubCommand, { stdio: 'pipe' });
log(`Git remote URL cleaned for ${repoName}`, 'success');
} catch (error) {
log(
`Warning: Failed to scrub git remote URL for ${repoName}: ${error.message}`,
'warning',
);
// Non-fatal error - continue execution
}
}
// Script execution
if (require.main === module) {
if (!shouldSetupAndroidModule()) {
log('Skipping Android module setup based on platform detection', 'warning');
process.exit(0);
}
try {
setupAndroidPassportReader();
} catch (error) {
log(`Setup failed: ${error.message}`, 'error');
process.exit(1);
}
}
module.exports = {
setupAndroidPassportReader,
removeExistingModule,
PRIVATE_MODULES,
};