From dd9e58ddaa87cb0819596809656ce0f4004ae82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20=C5=9Awiderski?= Date: Thu, 11 Jun 2026 13:38:01 +0200 Subject: [PATCH] [ACS-11927] init acs aps scripts update terraform (#11976) * [ACS-11927] updated init-acs and aps scripts * [ACS-11927] copilot review fixes 1 * [ACS-11927] handled error catches * [ACS-11927] last error handled * [ACS-11927] caught errors stringyfied * [ACS-11927] scripts made more robust * [ACS-11927] copilot review fixes 2 * [ACS-11927] eslint fixes 1 * [ACS-11927] eslint fixes 2 * ci:force * ci:force * [ACS-11927] copilot review fixes 2 --- lib/cli/scripts/init-acs-env.ts | 409 +++++++++++++++++++++----------- lib/cli/scripts/init-aps-env.ts | 227 +++++++++++------- 2 files changed, 412 insertions(+), 224 deletions(-) diff --git a/lib/cli/scripts/init-acs-env.ts b/lib/cli/scripts/init-acs-env.ts index aa38024241..7a10ce31c3 100755 --- a/lib/cli/scripts/init-acs-env.ts +++ b/lib/cli/scripts/init-acs-env.ts @@ -28,12 +28,16 @@ interface InitAcsEnvArgs { username?: string; password?: string; } + const MAX_RETRY = 10; -let counter = 0; -const TIMEOUT = 6000; +const RETRY_DELAY_MS = 6000; const ACS_DEFAULT = require('./resources').ACS_DEFAULT; let alfrescoJsApi: AlfrescoApi; +let nodesApi: NodesApi; +let uploadApi: UploadApi; +let sharedlinksApi: SharedlinksApi; +let favoritesApi: FavoritesApi; /** * Init ACS environment command @@ -46,7 +50,6 @@ Usage: init-acs-env [options] Initialize ACS environment Options: - -v, --version Output the version number --host Remote environment host --clientId SSO client (default: "alfresco") -p, --password Password @@ -56,11 +59,6 @@ Options: exit(0); } - if (argv.includes('-v') || argv.includes('--version')) { - console.log('0.1.0'); - exit(0); - } - const { values } = parseArgs({ args: argv.slice(2), options: { @@ -92,160 +90,293 @@ Options: await checkEnv(opts); - logger.info(`***** Step initialize ACS *****`); + logger.info('***** Step initialize ACS *****'); await initializeDefaultFiles(); } /** - * Setup default files + * Initialize default files. Creates the e2e folder and ensures each file + * exists with its required state (locked, shared, favorite). + * Idempotent: only creates/modifies what is missing. */ async function initializeDefaultFiles() { - const e2eFolder = ACS_DEFAULT.e2eFolder; - const parentFolder = await createFolder(e2eFolder.name, '-my-'); - const parentFolderId = parentFolder.entry.id; + const e2eFolderName: string = ACS_DEFAULT.e2eFolder.name; - for (let j = 0; j < ACS_DEFAULT.files.length; j++) { - const fileInfo = ACS_DEFAULT.files[j]; - switch (fileInfo.action) { - case 'UPLOAD': { - await uploadFile(fileInfo.name, parentFolderId); - break; - } - case 'LOCK': { - const fileToLock = await uploadFile(fileInfo.name, parentFolderId); - await lockFile(fileToLock.entry.id); - break; - } - case 'SHARE': { - const fileToShare = await uploadFile(fileInfo.name, parentFolderId); - await shareFile(fileToShare.entry.id); - break; - } - case 'FAVORITE': { - const fileToFav = await uploadFile(fileInfo.name, parentFolderId); - await favoriteFile(fileToFav.entry.id); - break; - } - default: { - logger.error('No action found for file ', fileInfo.name, parentFolderId); - break; - } - } - } -} - -/** - * Create folder - * - * @param folderName folder name - * @param parentId parent folder id - */ -async function createFolder(folderName: string, parentId: string) { - let createdFolder: NodeEntry; - const body = { - name: folderName, - nodeType: 'cm:folder' - }; + let parentFolder: NodeEntry; try { - createdFolder = await new NodesApi(alfrescoJsApi).createNode(parentId, body, { overwrite: true }); + parentFolder = await withRetry(() => ensureFolder(e2eFolderName, '-my-'), `ensure folder ${e2eFolderName}`); + } catch (error: any) { + logger.warn(`Skipping file initialization: test-data folder could not be created: ${formatError(error)}`); + return; + } - logger.info(`Folder ${folderName} was created`); - } catch (err) { - if (err.status === 409) { - const relativePath = `/${folderName}`; - createdFolder = await new NodesApi(alfrescoJsApi).getNode('-my-', { relativePath }); - } + const parentFolderId = getEntryId(parentFolder, `folder ${e2eFolderName}`); + + for (const fileInfo of ACS_DEFAULT.files) { + await withRetry(() => processFile(fileInfo, parentFolderId), `initialize ${fileInfo.name}`); } - return createdFolder; } /** - * Upload file - * - * @param fileName file name - * @param fileDestination destination path + * Process a single file: upload if missing, then apply its action (lock/share/favorite). + * @param fileInfo file descriptor from ACS_DEFAULT + * @param fileInfo.name file name + * @param fileInfo.action action to apply (LOCK, SHARE, FAVORITE) + * @param parentFolderId parent folder node id */ -async function uploadFile(fileName: string, fileDestination: string): Promise { +async function processFile(fileInfo: { name: string; action: string }, parentFolderId: string) { + const existingNode = await findNodeByRelativePath(parentFolderId, fileInfo.name); + + let nodeId: string; + if (existingNode?.entry?.id) { + logger.info(`File ${fileInfo.name} already exists, verifying required state.`); + nodeId = existingNode.entry.id; + } else { + const createdNode = await uploadFile(fileInfo.name, parentFolderId); + nodeId = getEntryId(createdNode, `file ${fileInfo.name}`); + } + + switch (fileInfo.action) { + case 'LOCK': + await ensureLocked(nodeId, fileInfo.name, existingNode?.entry?.isLocked); + break; + case 'SHARE': + await ensureShared(nodeId, fileInfo.name); + break; + case 'FAVORITE': + await ensureFavorite(nodeId, fileInfo.name); + break; + default: + break; + } +} + +/** + * Ensure a folder exists under the given parent. Creates it if missing. + * Handles 409 conflict (race condition) by fetching the existing folder. + * @param folderName folder name + * @param parentId parent node id + * @returns the folder node entry + */ +async function ensureFolder(folderName: string, parentId: string): Promise { + const existingFolder = await findNodeByRelativePath(parentId, folderName); + + if (existingFolder?.entry?.id) { + if (!existingFolder.entry.isFolder) { + throw new Error( + `Cannot use ${folderName} as test-data folder: a non-folder node with that name already exists (nodeType: ${existingFolder.entry.nodeType}, id: ${existingFolder.entry.id}).` + ); + } + logger.info(`Folder ${folderName} already exists.`); + return existingFolder; + } + + try { + const createdFolder = await nodesApi.createNode(parentId, { name: folderName, nodeType: 'cm:folder' }, { overwrite: true }); + logger.info(`Folder ${folderName} was created`); + return createdFolder; + } catch (error: any) { + if (error?.status === 409) { + const conflictingFolder = await findNodeByRelativePath(parentId, folderName); + + if (conflictingFolder?.entry?.id) { + if (!conflictingFolder.entry.isFolder) { + throw new Error( + `Cannot use ${folderName} as test-data folder: a non-folder node with that name already exists (nodeType: ${conflictingFolder.entry.nodeType}, id: ${conflictingFolder.entry.id}).` + ); + } + logger.info(`Folder ${folderName} already exists.`); + return conflictingFolder; + } + } + + throw new Error(`Failed to ensure folder ${folderName}: ${formatError(error)}`); + } +} + +/** + * Find a node by relative path under a parent. Returns null if not found (404). + * @param parentId parent node id + * @param fileName relative path / file name + * @returns the node entry or null if not found + */ +async function findNodeByRelativePath(parentId: string, fileName: string): Promise { + try { + return await nodesApi.getNode(parentId, { relativePath: `/${fileName}`, include: ['isLocked'] }); + } catch (error: any) { + if (error?.status === 404) { + return null; + } + + throw new Error(`Failed to fetch ${fileName}: ${formatError(error)}`); + } +} + +/** + * Upload a file to the given destination folder. + * @param fileName file name + * @param destinationId destination folder node id + * @returns the uploaded node entry + */ +async function uploadFile(fileName: string, destinationId: string): Promise { const filePath = `../resources/content/${fileName}`; const file = createReadStream(path.join(__dirname, filePath)); - let uploadedFile: NodeEntry; + try { - uploadedFile = await new UploadApi(alfrescoJsApi).uploadFile(file, '', fileDestination, null, { + const uploadedFile = await uploadApi.uploadFile(file, '', destinationId, null, { name: fileName, nodeType: 'cm:content', renditions: 'doclib', overwrite: true }); logger.info(`File ${fileName} was uploaded`); - } catch (err) { - logger.error(`Failed to upload file with error: `, err); + return uploadedFile; + } catch (error: any) { + throw new Error(`Failed to upload ${fileName}: ${formatError(error)}`); } - return uploadedFile; } /** - * Lock file node - * + * Ensure a file is locked. Skips if already locked. * @param nodeId node id + * @param fileName file name (for logging) + * @param isAlreadyLocked whether the node is already locked */ -async function lockFile(nodeId: string): Promise { - const data = { - type: 'ALLOW_OWNER_CHANGES' - }; +async function ensureLocked(nodeId: string, fileName: string, isAlreadyLocked = false) { + if (isAlreadyLocked) { + logger.info(`File ${fileName} is already locked.`); + return; + } + try { - const result = await new NodesApi(alfrescoJsApi).lockNode(nodeId, data); - logger.info('File was locked'); - return result; - } catch (error) { - logger.error('Failed to lock file with error: ', error); - return null; + await nodesApi.lockNode(nodeId, { type: 'ALLOW_OWNER_CHANGES' }); + logger.info(`File ${fileName} was locked`); + } catch (error: any) { + throw new Error(`Failed to lock ${fileName}: ${formatError(error)}`); } } /** - * Share file node - * + * Ensure a file is shared. Handles 409 (already shared) gracefully. * @param nodeId node id + * @param fileName file name (for logging) */ -async function shareFile(nodeId: string) { - const data = { - nodeId - }; +async function ensureShared(nodeId: string, fileName: string) { try { - await new SharedlinksApi(alfrescoJsApi).createSharedLink(data); - logger.info('File was shared'); - } catch (error) { - logger.error('Failed to share file with error: ', error); - } -} - -/** - * Favorite file node - * - * @param nodeId node id - */ -async function favoriteFile(nodeId: string) { - const data = { - target: { - ['file']: { - guid: nodeId - } + await sharedlinksApi.createSharedLink({ nodeId }); + logger.info(`File ${fileName} was shared`); + } catch (error: any) { + if (error?.status === 409) { + logger.info(`File ${fileName} is already shared.`); + return; } - }; - try { - await new FavoritesApi(alfrescoJsApi).createFavorite('-me-', data); - logger.info('File was add to favorites'); - } catch (error) { - logger.error('Failed to add the file to favorites with error: ', error); + + throw new Error(`Failed to share ${fileName}: ${formatError(error)}`); } } /** - * Check environment state - * - * @param opts command options + * Ensure a file is favorite. Handles 409 (already favorite) gracefully. + * @param nodeId node id + * @param fileName file name (for logging) */ -async function checkEnv(opts: InitAcsEnvArgs) { +async function ensureFavorite(nodeId: string, fileName: string) { + try { + await favoritesApi.createFavorite('-me-', { + target: { + file: { + guid: nodeId + } + } + }); + logger.info(`File ${fileName} was added to favorites`); + } catch (error: any) { + if (error?.status === 409) { + logger.info(`File ${fileName} is already a favorite.`); + return; + } + + throw new Error(`Failed to favorite ${fileName}: ${formatError(error)}`); + } +} + +/** + * Extract entry id from a node response. Throws if missing. + * @param nodeEntry node entry response + * @param label label for error message + * @returns the node id + */ +function getEntryId(nodeEntry: NodeEntry, label: string): string { + const nodeId = nodeEntry?.entry?.id; + + if (nodeId) { + return nodeId; + } + + throw new Error(`Missing ACS response entry for ${label}.`); +} + +/** + * Format an error for logging. + * @param error error object + * @returns formatted error string + */ +function formatError(error: any): string { + if (!error) { + return 'Unknown error'; + } + + if (typeof error === 'string') { + return error; + } + + try { + return error?.message || error?.stack || JSON.stringify(error); + } catch { + return 'Unknown error (unable to serialize)'; + } +} + +/** + * Retry wrapper for transient failures. + * @param fn async function to execute + * @param label label for logging + * @param maxAttempts maximum retry attempts + * @returns the result of the function + */ +async function withRetry(fn: () => Promise, label: string, maxAttempts = 3): Promise { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (error: any) { + if (attempt === maxAttempts) { + logger.error(`${label}: failed after ${maxAttempts} attempts: ${formatError(error)}`); + throw error; + } + + logger.warn(`${label}: attempt ${attempt} failed, retrying in ${RETRY_DELAY_MS / 1000}s: ${formatError(error)}`); + await wait(RETRY_DELAY_MS); + } + } + + throw new Error(`${label}: exhausted all ${maxAttempts} attempts`); +} + +/** + * Async delay. + * @param ms milliseconds to wait + * @returns a promise that resolves after the delay + */ +function wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Check environment state and authenticate. Retries on transient failures. + * @param opts command options + * @param attempt current attempt number + */ +async function checkEnv(opts: InitAcsEnvArgs, attempt = 1) { try { alfrescoJsApi = new AlfrescoApi({ provider: 'ALL', @@ -260,31 +391,29 @@ async function checkEnv(opts: InitAcsEnvArgs) { }, contextRoot: 'alfresco' }); + await alfrescoJsApi.login(opts.username, opts.password); - } catch (e) { - if (e.error.code === 'ETIMEDOUT') { - logger.error('The env is not reachable. Terminating'); - exit(1); + + nodesApi = new NodesApi(alfrescoJsApi); + uploadApi = new UploadApi(alfrescoJsApi); + sharedlinksApi = new SharedlinksApi(alfrescoJsApi); + favoritesApi = new FavoritesApi(alfrescoJsApi); + } catch (error: any) { + const errorCode = error?.error?.code; + + if (errorCode === 'ETIMEDOUT' || error?.status === 504) { + logger.warn('Login attempt timed out or received a gateway error, environment may still be starting up.'); + } else { + logger.error('Login error, environment down or inaccessible.'); } - logger.error('Login error environment down or inaccessible'); - counter++; - if (MAX_RETRY === counter) { + + if (attempt >= MAX_RETRY) { logger.error('Give up'); exit(1); - } else { - logger.error(`Retry in 1 minute attempt N ${counter}`); - sleep(TIMEOUT); - await checkEnv(opts); } + + logger.warn(`Retry in ${RETRY_DELAY_MS / 1000} seconds, attempt ${attempt}`); + await wait(RETRY_DELAY_MS); + await checkEnv(opts, attempt + 1); } } - -/** - * Perform a delay - * - * @param delay timeout in milliseconds - */ -function sleep(delay: number) { - const start = new Date().getTime(); - while (new Date().getTime() < start + delay) {} -} diff --git a/lib/cli/scripts/init-aps-env.ts b/lib/cli/scripts/init-aps-env.ts index b1e7a3ab21..87428bd606 100755 --- a/lib/cli/scripts/init-aps-env.ts +++ b/lib/cli/scripts/init-aps-env.ts @@ -40,8 +40,7 @@ interface InitApsEnvArgs { license?: string; } const MAX_RETRY = 10; -let counter = 0; -const TIMEOUT = 6000; +const RETRY_DELAY_MS = 6000; const TENANT_DEFAULT_ID = 1; const TENANT_DEFAULT_NAME = 'default'; const CONTENT_DEFAULT_NAME = 'adw-content'; @@ -60,7 +59,6 @@ Usage: init-aps-env [options] Initialize APS environment Options: - -v, --version Output the version number --host Remote environment host --clientId SSO client (default: "alfresco") -p, --password Password @@ -71,11 +69,6 @@ Options: exit(0); } - if (argv.includes('-v') || argv.includes('--version')) { - console.log('0.1.0'); - exit(0); - } - const { values } = parseArgs({ args: argv.slice(2), options: { @@ -111,6 +104,14 @@ Options: await checkEnv(opts); + const e2eAppReady = await ensureE2eApplicationDeployed(); + if (e2eAppReady) { + logger.info(`APS environment already initialized (terraform). Skipping.`); + return; + } + + await alfrescoJsApi.login(opts.username, opts.password); + logger.info(`***** Step 1 - Check License *****`); let licenceUploaded = false; @@ -146,25 +147,24 @@ Options: logger.info(`***** Step 4 - Create users *****`); const users = await getDefaultApsUsersFromRealm(opts); if (tenantId && users && users.length > 0) { - for (let i = 0; i < users.length; i++) { - await createUsers(tenantId, users[i]); + for (const user of users) { + await createUsers(tenantId, user); } - for (let i = 0; i < users.length; i++) { - logger.info('Impersonate user: ' + users[i].username); - await alfrescoJsApi.login(users[i].username, 'password'); - await authorizeUserToContentRepo(opts, users[i]); + for (const user of users) { + logger.info('Impersonate user: ' + user.username); + await alfrescoJsApi.login(user.username, 'password'); + await authorizeUserToContentRepo(opts, user); - const defaultUser = 'hruser'; - if (users[i].username.includes(defaultUser)) { - logger.info(`***** Step initialize APS apps for user ${defaultUser} *****`); + if (user.username.includes('hruser')) { + logger.info(`***** Step initialize APS apps for user hruser *****`); await initializeDefaultApps(); } } } else { logger.info('Something went wrong. Was not able to create the users'); } - } catch (error) { - logger.error(`Aps something went wrong. Tenant id ${tenantId}`, error); + } catch (error: any) { + logger.error(`Aps something went wrong. Tenant id ${tenantId}: ${formatError(error)}`); exit(1); } } else { @@ -173,15 +173,56 @@ Options: } } +/** + * Ensure e2e-Application is deployed for hruser. + * If the app is already present, returns true (skip full init). + * If hruser can log in but the app is missing, imports, publishes and deploys it. + * Returns false only if hruser cannot log in or deployment fails. + * @returns `true` if app is deployed, otherwise `false` + */ +async function ensureE2eApplicationDeployed(): Promise { + try { + await alfrescoJsApi.login('hruser', 'password'); + const runtimeAppDefinitionsApi = new RuntimeAppDefinitionsApi(alfrescoJsApi); + const availableApps = await runtimeAppDefinitionsApi.getAppDefinitions(); + const e2eApp = availableApps.data?.filter((app) => app.name?.includes('e2e-Application')); + if (e2eApp && e2eApp.length > 0) { + logger.info(`e2e-Application is already deployed for hruser`); + return true; + } + logger.info(`e2e-Application not found for hruser - uploading and deploying it now`); + const appDefinition = await importPublishApp('e2e-Application'); + if (appDefinition?.appDefinition?.id) { + await deployApp(appDefinition.appDefinition.id); + const verifyApps = await runtimeAppDefinitionsApi.getAppDefinitions(); + const deployed = verifyApps.data?.some((app) => app.name?.includes('e2e-Application')); + if (deployed) { + logger.info(`e2e-Application successfully deployed for hruser`); + return true; + } + logger.info(`e2e-Application deployment could not be verified - proceeding with full initialization`); + return false; + } + logger.info(`Failed to import/deploy e2e-Application for hruser - proceeding with full initialization`); + return false; + } catch (error: any) { + logger.info(`Unable to verify APS state for hruser - proceeding with initialization: ${formatError(error)}`); + return false; + } +} + /** * Initialise default applications */ async function initializeDefaultApps() { - for (let x = 0; x < ACTIVITI_APPS.apps.length; x++) { - const appInfo = ACTIVITI_APPS.apps[x]; + for (const appInfo of ACTIVITI_APPS.apps) { const isDeployed = await isDefaultAppDeployed(appInfo.name); - if (isDeployed !== undefined && !isDeployed) { - const appDefinition = await importPublishApp(`${appInfo.name}`); + if (!isDeployed) { + const appDefinition = await importPublishApp(appInfo.name); + if (!appDefinition?.appDefinition?.id) { + logger.error(`Failed to import app ${appInfo.name}, skipping deployment.`); + continue; + } await deployApp(appDefinition.appDefinition.id); } else { logger.info(`***** App ${appInfo.name} already deployed *****`); @@ -190,11 +231,11 @@ async function initializeDefaultApps() { } /** - * Check environment - * + * Check environment state and authenticate. Retries on transient failures. * @param opts command options + * @param attempt current attempt number */ -async function checkEnv(opts: InitApsEnvArgs) { +async function checkEnv(opts: InitApsEnvArgs, attempt = 1) { try { alfrescoJsApi = new AlfrescoApi({ provider: 'ALL', @@ -210,27 +251,28 @@ async function checkEnv(opts: InitApsEnvArgs) { } }); await alfrescoJsApi.login(opts.username, opts.password); - } catch (e) { - if (e.error.code === 'ETIMEDOUT') { - logger.error('The env is not reachable. Terminating'); - exit(1); + } catch (error: any) { + const errorCode = error?.error?.code; + + if (errorCode === 'ETIMEDOUT' || error?.status === 504) { + logger.warn('Login attempt timed out or received a gateway error, environment may still be starting up.'); + } else { + logger.error('Login error, environment down or inaccessible.'); } - logger.info('Login error environment down or inaccessible'); - counter++; - if (MAX_RETRY === counter) { + + if (attempt >= MAX_RETRY) { logger.error('Give up'); exit(1); - } else { - logger.error(`Retry in 1 minute attempt N ${counter}`); - sleep(TIMEOUT); - await checkEnv(opts); } + + logger.warn(`Retry in ${RETRY_DELAY_MS / 1000} seconds, attempt ${attempt}`); + await wait(RETRY_DELAY_MS); + await checkEnv(opts, attempt + 1); } } /** * Check if the default tenant is present - * * @param tenantId tenant id * @param tenantName tenant name * @returns `true` if tenant is found, otherwise `false` @@ -241,7 +283,7 @@ async function hasDefaultTenant(tenantId: number, tenantName: string): Promise { const availableApps = await runtimeAppDefinitionsApi.getAppDefinitions(); const defaultApp = availableApps.data?.filter((app) => app.name?.includes(appName)); return defaultApp && defaultApp.length > 0; - } catch (error) { - logger.error(`Aps app failed to import/Publish!`); + } catch (error: any) { + logger.error(`Failed to check if ${appName} is deployed: ${formatError(error)}`); return false; } } /** * Import and publish the application - * * @param appName application name + * @returns the app definition result */ -async function importPublishApp(appName: string): Promise { +async function importPublishApp(appName: string): Promise { const appNameExtension = `../resources/${appName}.zip`; logger.info(`Import app ${appNameExtension}`); const pathFile = path.join(__dirname, appNameExtension); @@ -371,15 +412,14 @@ async function importPublishApp(appName: string): Promise { try { @@ -415,13 +455,13 @@ async function hasLicense(opts: InitApsEnvArgs): Promise { ['application/json'], ['application/json'] ); - if (license && license.status === 'valid') { + if (license?.status === 'valid') { logger.info(`Aps has a valid License!`); return true; } logger.info(`Aps does NOT have a valid License!`); return false; - } catch (error) { + } catch { logger.error(`Aps not able to check the license`); return false; } @@ -429,8 +469,8 @@ async function hasLicense(opts: InitApsEnvArgs): Promise { /** * Get default users from the realm - * * @param opts command options + * @returns array of default APS users or null */ async function getDefaultApsUsersFromRealm(opts: InitApsEnvArgs) { try { @@ -449,18 +489,18 @@ async function getDefaultApsUsersFromRealm(opts: InitApsEnvArgs) { const apsDefaultUsers = users.filter((user) => usernamesOfApsDefaultUsers.includes(user.username)); logger.info(`Keycloak found ${apsDefaultUsers.length} users`); return apsDefaultUsers; - } catch (error) { - logger.error(`APS: not able to fetch user: ${error.message}`); + } catch (error: any) { + logger.error(`APS: not able to fetch user: ${formatError(error)}`); return null; } } /** * Validate that ACS repo for Activiti is present - * * @param opts command options * @param tenantId tenant id * @param contentName content service name + * @returns `true` if content repo is present, otherwise `false` */ async function isContentRepoPresent(opts: InitApsEnvArgs, tenantId: number, contentName: string): Promise { try { @@ -476,18 +516,18 @@ async function isContentRepoPresent(opts: InitApsEnvArgs, tenantId: number, cont ['application/json'] ); return !!contentRepos.data.find((repo) => repo.name === contentName); - } catch (error) { - logger.error(`APS: not able to create content: ${error.message}`); - return null; + } catch (error: any) { + logger.error(`APS: not able to check content repo: ${formatError(error)}`); + return false; } } /** * Add content service with basic auth - * * @param opts command options * @param tenantId tenant id * @param name content name + * @returns the created content repo */ async function addContentRepoWithBasic(opts: InitApsEnvArgs, tenantId: number, name: string) { logger.info(`Create Content with name ${name} and basic auth`); @@ -517,14 +557,13 @@ async function addContentRepoWithBasic(opts: InitApsEnvArgs, tenantId: number, n ); logger.info(`Content created!`); return content; - } catch (error) { - logger.error(`APS: not able to create content: ${error.message}`); + } catch (error: any) { + logger.error(`APS: not able to create content: ${formatError(error)}`); } } /** * Authorize activiti user to ACS repo - * * @param opts command options * @param user user object */ @@ -551,17 +590,17 @@ async function authorizeUserToContentRepo(opts: InitApsEnvArgs, user: any) { } } return; - } catch (error) { - logger.error(`APS: not able to authorize content: ${error.message}`); + } catch (error: any) { + logger.error(`APS: not able to authorize content: ${formatError(error)}`); } } /** * Authorize user with content using basic auth - * * @param opts command options * @param username username * @param contentId content id + * @returns the authorized content */ async function authorizeUserToContentWithBasic(opts: InitApsEnvArgs, username: string, contentId: string) { logger.info(`Authorize ${username} on contentId: ${contentId} in basic auth`); @@ -580,15 +619,15 @@ async function authorizeUserToContentWithBasic(opts: InitApsEnvArgs, username: s ); logger.info(`User authorized!`); return content; - } catch (error) { - logger.error(`APS: not able to authorize content: ${error.message}`); + } catch (error: any) { + logger.error(`APS: not able to authorize content: ${formatError(error)}`); } } /** * Download APS license file - * * @param apsLicensePath path to license file + * @returns `true` if download succeeded, otherwise `false` */ async function downloadLicenseFile(apsLicensePath: string) { const args = [`s3`, `cp`, apsLicensePath, `./`]; @@ -606,11 +645,31 @@ async function downloadLicenseFile(apsLicensePath: string) { } /** - * Perform a delay - * - * @param delay timeout in milliseconds + * Format an error for logging. + * @param error error object + * @returns formatted error string */ -function sleep(delay: number) { - const start = new Date().getTime(); - while (new Date().getTime() < start + delay) {} +function formatError(error: any): string { + if (!error) { + return 'Unknown error'; + } + + if (typeof error === 'string') { + return error; + } + + try { + return error?.message || error?.stack || JSON.stringify(error); + } catch { + return 'Unknown error (unable to serialize)'; + } +} + +/** + * Async delay. + * @param ms milliseconds to wait + * @returns a promise that resolves after the delay + */ +function wait(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); }