diff --git a/src/scripts/autoSync.sh b/src/scripts/autoSync.sh new file mode 100755 index 0000000..b980e8a --- /dev/null +++ b/src/scripts/autoSync.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Function to get UTC time from progress.json and display it in CST +get_restart_time() { + local RESTART_AFTER=$(jq -r '.restartAfter' progress.json) + if [ "$RESTART_AFTER" != "0" ]; then + local RESTART_AFTER_SEC=$((RESTART_AFTER / 1000)) + echo "Restart after: $(TZ='America/Chicago' date -r $RESTART_AFTER_SEC "+%Y-%m-%d %H:%M:%S") (CST)" + fi +} + + +# Function to check if condition is met +exit_if_all_checked() { + local LAST_CHECKED=$(jq -r '.lastChecked' progress.json) + if [ "$LAST_CHECKED" -gt "$END" ]; then + echo "All repos synced up to $END, quitting..." + exit 0 + fi +} + +# Function to format seconds into H:M:S +format_time() { + local SECONDS=$1 + printf "%02d:%02d:%02d" $((SECONDS/3600)) $((SECONDS%3600/60)) $((SECONDS%60)) +} + +# Check if destination argument is provided +if [ $# -ne 3 ]; then + echo "Usage: ./autoSync.sh " + echo "Example: ./autoSync.sh adobe-summit-L322/seat 0 100" + exit 1 +fi + +DESTINATION=$1 +START=$2 +END=$3 + +# Validate arguments +if ! [[ "$START" =~ ^[0-9]+$ ]] || ! [[ "$END" =~ ^[0-9]+$ ]]; then + echo "Error: Start and End must be integers" + exit 1 +fi + +if [ "$START" -gt "$END" ]; then + echo "Error: Start cannot be greater than End" + exit 1 +fi + +# Main loop +while true; do + # Check condition to see if we should quit + exit_if_all_checked + + # Get the current restart time and display it + get_restart_time + + # Wait until restartAfter time has passed + RESTART_AFTER=$(jq -r '.restartAfter' progress.json) + RESTART_AFTER_SEC=$((RESTART_AFTER / 1000)) + CURRENT_TIME=$(date +%s) + SLEEP_TIME=$((RESTART_AFTER_SEC - CURRENT_TIME)) + if [ $SLEEP_TIME -gt 0 ]; then + echo "Sleeping for $(format_time $SLEEP_TIME)" + sleep $SLEEP_TIME + fi + + # Run the script synchronously. When the script ends, it will write to progress.json with new restartAfter time + node src/scripts/checkSync.js "$DESTINATION" "$START" "$END" +done diff --git a/src/scripts/checkSync.js b/src/scripts/checkSync.js new file mode 100644 index 0000000..dcc811e --- /dev/null +++ b/src/scripts/checkSync.js @@ -0,0 +1,160 @@ +/** + * This script is designed to check the status of code syncs for a series of repositories. + * Only to be used for housekeeping purposes. Please do not run this script without supervision. + * + * Usage: node checkSync.js + * Example: node checkSync.js adobe-summit-L322/seat 0 100 + */ + +import fs from 'fs' + +const PROGRESS_FILE = 'progress.json' +const WAIT_TIME_MS = 1 * 60 * 60 * 1000 // 1 hour +// the amount of git files we expect the code sync to have to have processed. +// for L322, it seems to be 418. For L321, seems to be 412. +// TODO: this needs to be dynamic, based on the source repo. +const GIT_FILES_TO_CHECK = 412 + +function parseDestinationString (destString) { + const [org, prefix] = destString.split('/') + return { org, prefix } +} + +function padIndex (index) { + return index.toString().padStart(2, '0') +} + +function loadProgress () { + try { + if (fs.existsSync(PROGRESS_FILE)) { + const data = JSON.parse(fs.readFileSync(PROGRESS_FILE, 'utf8')) + return { + lastChecked: data.lastChecked === 0 ? 0 : data.lastChecked || 1, + restartAfter: data.restartAfter || 0 + } + } + } catch (error) { + console.error('Error loading progress:', error) + } + return { lastChecked: 1, restartAfter: 0 } // Default values +} + +function saveProgress (index, restartAfter = 0) { + try { + fs.writeFileSync(PROGRESS_FILE, JSON.stringify({ lastChecked: index, restartAfter })) + } catch (error) { + console.error('Error saving progress:', error) + } +} + +async function checkRepo (destination, index) { + try { + await triggerCodeSync(destination, index) + console.log(`✅ Code sync completed for ${destination}-${padIndex(index)}. Proceeding to the next repo.`) + return index + 1 // Move to the next repo + } catch (error) { + console.error(`🚨 Error during code sync for ${destination}-${padIndex(index)}:`, error) + return index - 1 // Restart from the previous repo + } +} + +async function saveAndExit (destination, index) { + // Determine restart time in CST + const restartTimestamp = Date.now() + WAIT_TIME_MS + const restartTime = new Date(restartTimestamp).toLocaleTimeString('en-US', { timeZone: 'America/Chicago', hour12: true }) + console.log(`🚨 Script exiting. Restart at ${destination}-${padIndex(index)} after ~1 hour at: ${restartTime} CST`) + + saveProgress(index, restartTimestamp) + process.exit(0) +} + +async function triggerCodeSync (destination, seat) { + const { org, prefix } = parseDestinationString(destination) + const postUrl = `https://admin.hlx.page/code/${org}/${prefix}-${padIndex(seat)}/main/*` + console.log(`🔄 Calling Helix Admin Code Sync at: ${postUrl}`) + + try { + const postResponse = await fetch(postUrl, { method: 'POST' }) + + if (postResponse.status !== 202) { + console.error(`No code found for ${destination}-${padIndex(seat)}. Ensure repo exists.`) + saveAndExit(destination, seat) + } + const postData = await postResponse.json() + const detailsUrl = `${postData.links.self}/details` + console.log(`🔍 Checking details at: ${detailsUrl}`) + + await checkPhaseCompletion(detailsUrl, destination, seat) + } catch (error) { + console.error(`Error processing ${destination}-${padIndex(seat)}:`, error) + throw error + } +} + +async function checkPhaseCompletion (detailsUrl, destination, seat, maxRetries = 60, interval = 5000) { + let attempts = 0 + + while (attempts < maxRetries) { + try { + const detailsResponse = await fetch(detailsUrl) + const detailsData = await detailsResponse.json() + if (detailsData.state === 'stopped') { + if (detailsData.progress && detailsData?.progress.processed >= GIT_FILES_TO_CHECK) { + console.log(`✅ ${destination}-${padIndex(seat)} processing completed.`) + return + } else if (detailsData.error.includes('rate limit exceeded')) { + console.log(`⚠️ ${destination}-${padIndex(seat)} processing rate limit exceeded.`) + saveAndExit(destination, seat) + } else { + console.log('Unknown state! Data:', detailsData) + saveAndExit(destination, seat) + } + } else { + console.log(`⏳ ${destination}-${padIndex(seat)} still processing... retrying (${attempts + 1}/${maxRetries}) in ${(interval / 1000)}s.`) + } + } catch (error) { + console.error(`Error fetching details for ${destination}-${padIndex(seat)}:`, error) + } + + attempts++ + await new Promise(resolve => setTimeout(resolve, interval)) // Wait 1s before retrying + } + + await saveAndExit(destination, seat) +} + +async function runChecks (destination, start, end) { + const { lastChecked, restartAfter } = loadProgress() + + // Check if we are running before the allowed restart time + if (Date.now() < restartAfter) { + const restartTime = new Date(restartAfter).toLocaleTimeString('en-US', { timeZone: 'America/Chicago', hour12: true }) + console.error(`❌ Cannot start yet. Please wait until: ${restartTime} CST.`) + process.exit(1) + } + + let index = lastChecked + while (index <= end) { + index = await checkRepo(destination, index) + if (index < start) index = start // Prevent going below start + saveProgress(index) + } + console.log('✅ All repos checked successfully.') +} + +if (process.argv.length !== 5) { + console.log('Usage: node checkSync.js ') + console.log('Example: node checkSync.js adobe-summit-L322/seat 0 100') +} else { + const destination = process.argv[2] + const start = parseInt(process.argv[3]) + const end = parseInt(process.argv[4]) + + if (!Number.isInteger(start) || !Number.isInteger(end)) { + console.error('Start and End must be integers') + } else if (start > end) { + console.log('Start cannot be greater than End.') + } else { + runChecks(destination, start, end) + } +} diff --git a/src/scripts/cloneContent.js b/src/scripts/cloneContent.js new file mode 100644 index 0000000..4ac14ee --- /dev/null +++ b/src/scripts/cloneContent.js @@ -0,0 +1,65 @@ +/** + * This is a script to clone content automatically. + * Only to be used for housekeeping purposes. Please do not run this script without supervision. + * + * Usage: node cloneContent.js + * Example: node cloneContent.js adobe-commerce/adobe-demo-store adobe-summit-L322/seat 0 100 + */ + +import { execSync } from 'child_process' + +function parseRepoString (repoString) { + const [org, repo] = repoString.split('/') + return { org, repo } +} + +function parseDestinationString (destString) { + const [org, prefix] = destString.split('/') + return { org, prefix } +} + +async function cloneContent (source, destination, start, end) { + console.log('Starting to clone content...') + console.log(`Source: ${source}`) + console.log(`Destination: ${destination}`) + + const { org: sourceOrg, repo: sourceRepo } = parseRepoString(source) + const { org: destOrg, prefix: repoPrefix } = parseDestinationString(destination) + + for (let i = start; i <= end; i++) { + const repoNumber = i.toString().padStart(2, '0') + const command = `aio commerce:init --template "${sourceOrg}/${sourceRepo}" --repo "${destOrg}/${repoPrefix}-${repoNumber}" --datasource "" --skipMesh --skipGit` + console.log(`\nExecuting command ${i} of ${end}:`) + console.log(command) + + try { + execSync(command, { stdio: 'inherit' }) + console.log(`Successfully completed iteration ${i}`) + } catch (error) { + console.error(`Error in iteration ${i}:`, error.message) + throw error + } + } +} + +if (process.argv.length !== 6) { + console.log('Usage: node cloneContent.js ') + console.log('Example: node cloneContent.js adobe-commerce/adobe-demo-store adobe-summit-L322/seat 0 100') +} else { + const source = process.argv[2] + const destination = process.argv[3] + const start = parseInt(process.argv[4]) + const end = parseInt(process.argv[5]) + + if (!Number.isInteger(start) || !Number.isInteger(end)) { + console.error('Start and End must be integers') + } else if (start > end) { + console.log('Start cannot be greater than End.') + } else { + cloneContent(source, destination, start, end).then(() => { + console.log('All repos created successfully.') + }).catch((error) => { + console.error('Error creating repos:', error) + }) + } +} diff --git a/src/scripts/createRepos.js b/src/scripts/createRepos.js new file mode 100644 index 0000000..ab7a37d --- /dev/null +++ b/src/scripts/createRepos.js @@ -0,0 +1,60 @@ +/** + * This is a script to create multiple GitHub repositories using a template repository. + * Only to be used for housekeeping purposes. Please do not run this script without supervision. + * + * Usage: node createRepos.js + * Example: node createRepos.js adobe-commerce/adobe-demo-store adobe-summit-L322/seat 0 100 + */ + +import { createRepo } from '../utils/github.js' + +function parseRepoString (repoString) { + const [org, repo] = repoString.split('/') + return { org, repo } +} + +function parseDestinationString (destString) { + const [org, prefix] = destString.split('/') + return { org, prefix } +} + +async function createRepos (source, destination, start, end) { + console.log('Starting to create repos...') + console.log(`Source: ${source}`) + console.log(`Destination: ${destination}`) + + const { org: sourceOrg, repo: sourceRepo } = parseRepoString(source) + const { org: destOrg, prefix: repoPrefix } = parseDestinationString(destination) + + for (let i = start; i <= end; i++) { + const repo = `${repoPrefix}-${i.toString().padStart(2, '0')}` + try { + await createRepo(destOrg, repo, sourceOrg, sourceRepo) + } catch (e) { + console.error(`! Failed to complete run for "${repo}". Skipping.`) + console.error(e) + } + } +} + +if (process.argv.length !== 6) { + console.log('Usage: node createRepos.js ') + console.log('Example: node createRepos.js adobe-commerce/adobe-demo-store adobe-summit-L322/seat 0 100') +} else { + const source = process.argv[2] + const destination = process.argv[3] + const start = parseInt(process.argv[4]) + const end = parseInt(process.argv[5]) + + if (!Number.isInteger(start) || !Number.isInteger(end)) { + console.error('Start and End must be integers') + } else if (start > end) { + console.log('Start cannot be greater than End.') + } else { + createRepos(source, destination, start, end).then(() => { + console.log('All repos created successfully.') + }).catch((error) => { + console.error('Error creating repos:', error) + }) + } +} diff --git a/src/scripts/deleteRepos.js b/src/scripts/deleteRepos.js new file mode 100644 index 0000000..f384589 --- /dev/null +++ b/src/scripts/deleteRepos.js @@ -0,0 +1,73 @@ +/** + * This script deletes all the repositories created by the create-repos.js script. + * Only to be used for housekeeping purposes. Please do not run this script without supervision. + * + * Usage: node deleteRepos.js + * Example: node deleteRepos.js adobe-summit-L322/seat 0 100 + */ + +import childProcess from 'child_process' + +function parseDestinationString (destString) { + const [org, prefix] = destString.split('/') + return { org, prefix } +} + +function runCommand (cmd) { + return new Promise((resolve, reject) => { + childProcess.exec(cmd, (error, stdout, stderr) => { + if (error) { + console.error(stderr) + resolve(false) + } else { + console.log(stdout) + resolve(true) + } + }) + }) +} + +function deleteRepos (destination, start, end) { + console.log('Starting to delete repos...') + console.log(`Destination: ${destination}`) + + const { org: destOrg, prefix: repoPrefix } = parseDestinationString(destination) + + for (let seat = start; seat <= end; seat++) { + const repoName = `${destOrg}/${repoPrefix}-${seat.toString().padStart(2, '0')}` + const command = `gh api \ + --method DELETE \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${repoName}` + + runCommand(command) + .then((success) => { + if (success) { + console.log(`Repo at ${repoName} deleted successfully`) + } else { + console.error(`Failed to delete repo at ${repoName}`) + } + }) + .catch((error) => { + console.error(`Error occurred while running command for ${repoName}: ${error}`) + }) + } +} + +if (process.argv.length !== 5) { + console.log('Usage: node deleteRepos.js ') + console.log('Example: node deleteRepos.js adobe-summit-L322/seat 0 100') +} else { + const destination = process.argv[2] + const start = parseInt(process.argv[3]) + const end = parseInt(process.argv[4]) + + if (!Number.isInteger(start) || !Number.isInteger(end)) { + console.error('Start and End must be integers') + } else if (start > end) { + console.log('Start cannot be greater than End.') + } else { + deleteRepos(destination, start, end) + } +} diff --git a/src/utils/initialization.js b/src/utils/initialization.js index 3c72140..ddd4109 100644 --- a/src/utils/initialization.js +++ b/src/utils/initialization.js @@ -57,6 +57,7 @@ export async function initialization (args, flags) { // TODO: validate template is allowed template = template || await promptSelect('Which template would you like to use?', [ 'adobe-commerce/adobe-demo-store', // ACCS template + 'adobe-commerce/ccdm-demo-store', // CCDM template 'hlxsites/aem-boilerplate-commerce' // PaaS template // 'adobe-rnd/aem-boilerplate-xcom' // UE Template // 'aabsites/citisignal' // TODO: Cannot use citisignal until we resolve how to use templates that use config service as some core files are missing https://magento.slack.com/archives/C085R48U3R7/p1738785011567519