diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 549bf81..f92e6ec 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,4 +1,4 @@ -name: Docker hub +name: Build & deploy on: push: tags: diff --git a/README.md b/README.md index 49cbb0d..3085305 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,21 @@ + +Allure Logo + + +Docker Logo + + +Firebase Logo + + +# Allure Docker Deploy + +--- + ![Deployment](https://github.com/cybersokari/allure-docker-deploy/actions/workflows/deploy.yaml/badge.svg?branch=main) ![](https://img.shields.io/docker/pulls/sokari/allure-docker-deploy) -# Allure Docker Deploy -_An easy-to-use Docker solution for sharing and backing up Allure test reports_ + +_An easy-to-use serverless Docker solution for sharing and backing up Allure test reports_ This [Docker image](https://hub.docker.com/r/sokari/allure-docker-deploy) lets you share [Allure test reports](https://allurereport.org/) seamlessly via an ephemeral URL. It also backs up all report history and retries to Firebase Cloud Storage, enabling unique URLs for each test run and previewing previous reports. diff --git a/assets/allure-logo.png b/assets/allure-logo.png new file mode 100644 index 0000000..0e420e2 Binary files /dev/null and b/assets/allure-logo.png differ diff --git a/assets/docker-logo.png b/assets/docker-logo.png new file mode 100644 index 0000000..600476c Binary files /dev/null and b/assets/docker-logo.png differ diff --git a/assets/firebase-logo.png b/assets/firebase-logo.png new file mode 100644 index 0000000..4cfb7da Binary files /dev/null and b/assets/firebase-logo.png differ diff --git a/worker/app/cloud-storage.ts b/worker/app/cloud-storage.ts index 0c164e1..13913ac 100644 --- a/worker/app/cloud-storage.ts +++ b/worker/app/cloud-storage.ts @@ -34,7 +34,7 @@ export class CloudStorage { validation: process.env.DEBUG !== 'true', destination: `${storageHomeDir}/${destinationFilePath}`, }); - counter.incrementFilesUploaded() + await counter.incrementFilesUploaded() } catch (error) { console.error(`Failed to upload ${filePath}:`, error); } diff --git a/worker/app/counter.ts b/worker/app/counter.ts index b0a4b4a..d36f6c6 100644 --- a/worker/app/counter.ts +++ b/worker/app/counter.ts @@ -1,14 +1,24 @@ +import { Mutex } from 'async-mutex'; class Counter { private startTime: number | null = null; private processed = 0 private uploaded = 0 - incrementFilesProcessed() { - this.processed ++ + // Mutex for protecting critical sections + private mutex = new Mutex(); + + async incrementFilesProcessed(): Promise { + await this.mutex.runExclusive(() => { + this.processed++; + }); } - incrementFilesUploaded() { - this.uploaded++ + + async incrementFilesUploaded(): Promise { + await this.mutex.runExclusive(() => { + this.uploaded++; + }); } + get filesUploaded(){ return this.uploaded } diff --git a/worker/app/report-builder.ts b/worker/app/report-builder.ts index a35adde..661bc8a 100644 --- a/worker/app/report-builder.ts +++ b/worker/app/report-builder.ts @@ -1,7 +1,7 @@ import {REPORTS_DIR, STAGING_PATH} from "../index"; const allure = require('allure-commandline') -import {deploy} from "./site-builder"; +import {changePermissionsRecursively, createFirebaseJson, publishToFireBaseHosting} from "./site-builder"; import * as path from "node:path"; import * as fs from 'fs/promises' import counter from "./counter"; @@ -17,10 +17,13 @@ class ReportBuilder { public setTtl() { clearTimeout(this.timeOut) - this.timeOut = setTimeout(this.generateAndHost, this.ttl * 1000) + this.timeOut = setTimeout(async () => { + await this.generate() + return await this.host() + }, this.ttl * 1000) } - public async generateAndHost() { + public async generate() { // History files can exist in reports directory in WATCH_MODE // due to multiple call to generateAndHost, so we try to move @@ -53,7 +56,7 @@ class ReportBuilder { let success = false await new Promise((resolve, reject) => { generation.on('exit', async function (exitCode: number) { - success = exitCode == 0 + success = exitCode === 0 if (success) { resolve('success') } else { @@ -62,14 +65,6 @@ class ReportBuilder { } }) }) - if (success){ - try { - return await deploy() - } catch (e) { - console.log(`Hosting deployment failed: ${e}`) - } - } - return null } // Move from '/allure-results' mount to staging @@ -79,7 +74,7 @@ class ReportBuilder { const destinationFilePath = path.join(STAGING_PATH, path.basename(file)); await fs.mkdir(path.dirname(destinationFilePath), {recursive: true});// recursive, don't throw await fs.copyFile(file, destinationFilePath); - counter.incrementFilesProcessed() + await counter.incrementFilesProcessed() } catch (e) { console.warn(`Failed to move ${path.basename(file)} to staging area: ${e}`) } @@ -88,6 +83,13 @@ class ReportBuilder { return this } + public async host() { + await createFirebaseJson() + // Grant execution permission to website files + await changePermissionsRecursively(REPORTS_DIR, 0o755) + return await publishToFireBaseHosting() + } + } export default new ReportBuilder() \ No newline at end of file diff --git a/worker/app/site-builder.ts b/worker/app/site-builder.ts index 5fae8bd..3cb392f 100644 --- a/worker/app/site-builder.ts +++ b/worker/app/site-builder.ts @@ -10,7 +10,7 @@ import counter from "./counter"; const exec = util.promisify(require('child_process').exec) -async function createFirebaseJson() { +export async function createFirebaseJson() { const hosting = { "hosting": { "public": ".", @@ -30,7 +30,7 @@ async function createFirebaseJson() { } } -async function changePermissionsRecursively(dirPath: string, mode: fsSync.Mode) { +export async function changePermissionsRecursively(dirPath: string, mode: fsSync.Mode) { await fs.chmod(dirPath, mode); const files = await fs.readdir(dirPath); @@ -47,7 +47,7 @@ async function changePermissionsRecursively(dirPath: string, mode: fsSync.Mode) } } -async function publishToFireBaseHosting() { +export async function publishToFireBaseHosting() { if (process.env.DEBUG === 'true') { console.warn('DEBUG=true: Skipping live deployment') return @@ -118,12 +118,6 @@ export function writeGitHubSummary({summaryPath = '', url = ''}) { } } -export async function deploy() { - await createFirebaseJson() - // Grant execution permission to website files - await changePermissionsRecursively(REPORTS_DIR, 0o755) - return await publishToFireBaseHosting() -} diff --git a/worker/app/util.ts b/worker/app/util.ts index 79bd164..97db4fe 100644 --- a/worker/app/util.ts +++ b/worker/app/util.ts @@ -46,16 +46,19 @@ export async function getAllFiles(dirPath: string): Promise { export function validateWebsiteExpires(expires: string): boolean { // Check if input is empty if (!expires) { - console.error('Error: WEBSITE_EXPIRES cannot be empty'); return false; } + if(expires.trim().length > 3){ + return false; + } + + // Regex to validate format: number followed by h/d/w const validFormatRegex = /^(\d+)([hdw])$/; const match = expires.match(validFormatRegex); if (!match) { - console.error('Error: Invalid WEBSITE_EXPIRES format. Use format like 12h, 7d, or 2w'); return false; } diff --git a/worker/index.ts b/worker/index.ts index 26c0674..3b0fb08 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -73,7 +73,8 @@ function main(): void { ReportBuilder.stageFiles(await getAllFiles(MOUNTED_PATH)), cloudStorage?.stageRemoteFiles() ]) - const url = await ReportBuilder.generateAndHost() + await ReportBuilder.generate() + const url = await ReportBuilder.host() if(keepHistory){ await cloudStorage?.uploadHistory() }