diff --git a/docker-compose.yaml b/docker-compose.yaml index 07eb92c..2b500a2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,7 +27,7 @@ services: WEBSITE_EXPIRES: 12h KEEP_HISTORY: true # Default is true when STORAGE_BUCKET is provided KEEP_RETRIES: true # Default is false - WATCH_MODE: true + WATCH_MODE: false TTL_SECS: 6 volumes: - ./worker:/app #Devs only. This overrides the /app dir diff --git a/worker/app/counter.ts b/worker/app/counter.ts index 4969b72..6c4d84f 100644 --- a/worker/app/counter.ts +++ b/worker/app/counter.ts @@ -33,12 +33,12 @@ class Counter { startTimer(): void { this.startTime = Date.now(); } - getElapsedSeconds(): number { + getElapsedSeconds(): string { if (!this.startTime) { throw "Timer has not been started."; } const elapsed = Date.now() - this.startTime; - return elapsed / 1000 + return (elapsed / 1000).toFixed() } } diff --git a/worker/app/credential.ts b/worker/app/credential.ts index 88c43a2..b27f5f4 100644 --- a/worker/app/credential.ts +++ b/worker/app/credential.ts @@ -6,7 +6,7 @@ import fs from "fs/promises"; * Handles loading and accessing Google Cloud credentials. * Implements a Singleton pattern to ensure only one instance is created. */ -class Credential { +export class Credential { private _projectId: string | null = null; // Holds the project ID private static instance: Credential; // Singleton instance private data: any; // Parsed credentials data @@ -35,15 +35,10 @@ class Credential { * Sets the project ID for later use. */ public async create() { - try { - this.data = JSON.parse( - await fs.readFile(process.env.GOOGLE_APPLICATION_CREDENTIALS!, 'utf8') - ); - this._projectId = this.data.project_id; - } catch (error) { - console.error('Failed to get project_id from Google credentials:', error); - throw error; - } + this.data = JSON.parse( + await fs.readFile(process.env.GOOGLE_APPLICATION_CREDENTIALS!, 'utf8') + ); + this._projectId = this.data.project_id; } } diff --git a/worker/app/notifier.ts b/worker/app/notifier.ts index 80c0869..9fa4486 100644 --- a/worker/app/notifier.ts +++ b/worker/app/notifier.ts @@ -3,6 +3,8 @@ import {cloudStorage, keepHistory, keepRetires, STORAGE_BUCKET, websiteId} from import {WebClient} from '@slack/web-api'; import {StringBuilder} from "./string-builder"; import * as fs from "node:fs"; +import ansiEscapes from "ansi-escapes"; +import chalk from "chalk"; import credential from "./credential"; type SlackCredentials = { @@ -17,7 +19,15 @@ type SlackCredentials = { * Handles notifications related to report generation and file processing. * Supports Slack notifications, GitHub summary updates, and general stats logging. */ -class Notifier { +export class Notifier { + + get dashboardUrl(){ + return new StringBuilder(). + append("https://console.firebase.google.com/project") + .append(`/${(credential.projectId)}`) + .append(`/storage/${STORAGE_BUCKET}/files`) + .toString() + } /** * Sends a message to a Slack channel with details about the report. @@ -43,7 +53,7 @@ class Notifier { "elements": [ { "type": "mrkdwn", - "text": ":file_folder: *Files uploaded:* 54" + "text": `:file_folder: *Files uploaded:* ${counter.filesUploaded}` } ] }, @@ -52,7 +62,7 @@ class Notifier { "elements": [ { "type": "mrkdwn", - "text": ":mag: *Files processed:* 49" + "text": `:mag: *Files processed:* ${counter.filesProcessed}` } ] },) @@ -62,7 +72,7 @@ class Notifier { "elements": [ { "type": "mrkdwn", - "text": ":stopwatch: *Duration:* 11.206 seconds" + "text": `:stopwatch: *Duration:* ${counter.getElapsedSeconds()} seconds` } ] }) @@ -83,8 +93,6 @@ class Notifier { }) } if(cloudStorage){ - - const firebaseDashboardUrl = `https://console.firebase.google.com/project/${credential.projectId}/storage/${STORAGE_BUCKET}/files` blocks.push({ "type": "actions", "elements": [ @@ -95,7 +103,7 @@ class Notifier { "text": "View files in storage", "emoji": true }, - "url": firebaseDashboardUrl + "url": this.dashboardUrl } ] }) @@ -129,7 +137,7 @@ class Notifier { * Includes the report link, processing stats, and duration. * @param data - Contains the report URL and file path for summary */ - public async printGithubSummary(data: { mountedFilePath: string, url: string | undefined }): Promise { + public async printGithubSummary(data: { mountedFilePath: string, url: string | undefined}): Promise { const lineBreak = '
' const builder = new StringBuilder() builder.append(`**Your Allure report is ready 📈**}`) @@ -139,8 +147,7 @@ class Notifier { .append(lineBreak).append(lineBreak) } if (cloudStorage) { - const firebaseDashboardUrl = `https://console.firebase.google.com/project/${credential.projectId}/storage/${STORAGE_BUCKET}/files` - builder.append(`**[View files](${firebaseDashboardUrl})**`) + builder.append(`**[View files](${this.dashboardUrl})**`) .append(lineBreak).append(lineBreak) builder.append(`📂 Files uploaded: ${counter.filesUploaded}`) @@ -179,6 +186,18 @@ class Notifier { console.log('STORAGE_BUCKET is not provided, KEEP_HISTORY and KEEP_RETRIES disabled'); } } + + printSummaryToConsole(data: {url: string | null}): void { + if(data.url){ + console.log('Allure test report URL') + console.log(ansiEscapes.link(chalk.blue(data.url), data.url)); + } + if(cloudStorage){ + const dashboardUrl = this.dashboardUrl + console.log('View files in Storage') + console.log(ansiEscapes.link(chalk.blue(dashboardUrl), dashboardUrl)); + } + } } -export default new Notifier(); \ No newline at end of file + diff --git a/worker/app/report-builder.ts b/worker/app/report-builder.ts index 68302a2..30043b8 100644 --- a/worker/app/report-builder.ts +++ b/worker/app/report-builder.ts @@ -6,6 +6,7 @@ import * as fs from 'fs/promises' import counter from "./counter"; import pLimit from 'p-limit'; import {publishToFireBaseHosting} from "./util"; +import {Notifier} from "./notifier"; /** * ReportBuilder Class @@ -16,6 +17,7 @@ import {publishToFireBaseHosting} from "./util"; class ReportBuilder { private timeOut: NodeJS.Timeout | undefined private readonly ttl: number + private notifier = new Notifier() constructor() { this.ttl = Number.parseInt(process.env.TTL_SECS ?? '45') @@ -29,7 +31,10 @@ class ReportBuilder { clearTimeout(this.timeOut) this.timeOut = setTimeout(async () => { const path = await this.generate() - await publishToFireBaseHosting(path) + const url = await publishToFireBaseHosting(path) + if (url) { + this.notifier.printSummaryToConsole({url: url}) + } }, this.ttl * 1000) } diff --git a/worker/app/util.ts b/worker/app/util.ts index a764962..6b1d66b 100644 --- a/worker/app/util.ts +++ b/worker/app/util.ts @@ -4,11 +4,9 @@ import * as path from "node:path"; import util from "node:util"; const exec = util.promisify(require('child_process').exec) -import {DEBUG, websiteId} from "../index"; +import {websiteId} from "../index"; import {StringBuilder} from "./string-builder"; import credential from "./credential"; -import ansiEscapes from 'ansi-escapes'; -import chalk from "chalk"; export async function* getAllFilesStream(dir: string): AsyncGenerator { @@ -89,11 +87,11 @@ export async function changePermissionsRecursively(dirPath: string, mode: fsSync * @param configParentDir - Directory containing the hosting configuration * @returns {Promise} - The URL of the deployed site, if successful */ -export async function publishToFireBaseHosting(configParentDir: string): Promise { - if (DEBUG) { - console.warn('DEBUG=true: Skipping live deployment') - return - } +export async function publishToFireBaseHosting(configParentDir: string): Promise { + // if (DEBUG) { + // console.warn('DEBUG=true: Skipping live deployment') + // return + // } const hosting = { "hosting": { "public": ".", @@ -145,9 +143,6 @@ export async function publishToFireBaseHosting(configParentDir: string): Promise if (match && match[2]) { const url = match[2] - - console.log('Allure test report URL') - console.log(ansiEscapes.link(chalk.blue(url), url)); return url as string } else { console.warn('Could not parse URL from hosting.') diff --git a/worker/index.ts b/worker/index.ts index fab5d17..ecadf05 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -5,9 +5,9 @@ import { import ReportBuilder from "./app/report-builder"; import {CloudStorage} from "./app/cloud-storage"; import counter from "./app/counter"; -import notifier from "./app/notifier"; import credential from "./app/credential"; import {BatchProcessor} from "./app/batch-processor"; +import {Notifier} from "./app/notifier"; export const DEBUG = process.env.FIREBASE_STORAGE_EMULATOR_HOST !== undefined || false; export const MOUNTED_PATH = '/allure-results' @@ -22,7 +22,6 @@ export const keepRetires = process.env.KEEP_RETRIES?.toLowerCase() === 'true' export const watchMode = process.env.WATCH_MODE?.toLowerCase() === 'true' || false; export const fileProcessingConcurrency = watchMode ? 5 : 10 - /** * Entry Point * @@ -37,27 +36,29 @@ export function main(): void { console.warn('WEBSITE_ID or STORAGE_BUCKET is required'); return } - // Create it now, so that project_id will be available for - // Site deployment and messaging. - void credential.create() - - if (watchMode) { - const processor = new BatchProcessor() - chokidar.watch('/allure-results', { - ignored: '^(?!.*\\.(json|png|jpeg|jpg|gif|properties|log|webm)$).*$', - persistent: true, - awaitWriteFinish: true, - usePolling: true, - depth: 2, // Limit recursive depth - }).on('add', (filePath: string) => { - processor.add(filePath); - }); - console.log("Watching for file additions..."); - } else { - (async () => { - + (async () => { + // credential must be initialized before starting app + try { + await credential.create() + } catch (error) { + console.error('Failed to process Google credentials: Are you sure you have the correct file?', error); + return + } + if (watchMode) { + const processor = new BatchProcessor() + chokidar.watch('/allure-results', { + ignored: '^(?!.*\\.(json|png|jpeg|jpg|gif|properties|log|webm)$).*$', + persistent: true, + awaitWriteFinish: true, + usePolling: true, + depth: 2, // Limit recursive depth + }).on('add', (filePath: string) => { + processor.add(filePath); + }); + console.log("Watching for file additions..."); + } else { let url - if(websiteId){ + if (websiteId) { // Stage files, generateAndHost then upload history if enabled await Promise.all([ ReportBuilder.stageFiles(getAllFilesStream(MOUNTED_PATH)), @@ -65,7 +66,7 @@ export function main(): void { ]) const path = await ReportBuilder.generate() url = await publishToFireBaseHosting(path) - if(keepHistory){ + if (keepHistory) { await cloudStorage?.uploadHistory() } } @@ -73,21 +74,29 @@ export function main(): void { if (cloudStorage && keepRetires) { await cloudStorage.uploadResults() } + const summaryPath = process.env.GITHUB_STEP_SUMMARY const promises = [] - if(summaryPath){ - promises.push(notifier.printGithubSummary({mountedFilePath: summaryPath, url: url})) + const notifier = new Notifier() + if (summaryPath) { + promises.push(notifier.printGithubSummary({ + mountedFilePath: summaryPath, + url: url + })) } const token = process.env.SLACK_TOKEN; const conversationId = process.env.SLACK_CHANNEL_ID; - if(conversationId && token){ - promises.push(notifier.SendSlackMsg({conversationId: conversationId, token: token, url: url})) + if (conversationId && token) { + promises.push(notifier.SendSlackMsg({ + conversationId: conversationId, + token: token, url: url + })) } await Promise.all(promises) + } + + })() - })() - } - notifier.printStats() } if (require.main === module) {