mirror of
https://github.com/selfxyz/self.git
synced 2026-01-10 07:08:10 -05:00
* 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.
328 lines
9.3 KiB
JavaScript
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,
|
|
};
|