Skip to content

Add allure-upload command#48

Draft
satvik007 wants to merge 2 commits intomainfrom
feat/allure-upload-v2
Draft

Add allure-upload command#48
satvik007 wants to merge 2 commits intomainfrom
feat/allure-upload-v2

Conversation

@satvik007
Copy link
Collaborator

Summary

  • Add new allure-upload CLI command that parses Allure results directories (*-result.json files) and uploads test results to QA Sphere
  • Implement Allure parser with Zod validation, status mapping (broken→blocked, unknown→passed), TMS link marker extraction, folder derivation from suite labels, and attachment support
  • Refactor integration tests to use per-format expected counts instead of hardcoded values, enabling each format to have natural fixtures

Test plan

  • 28 unit tests for the Allure parser covering all status mappings, folder derivation, TMS links, parameterized tests, attachments, error handling
  • 20 integration tests (existing test suite extended to cover allure-upload alongside junit-upload and playwright-json-upload)
  • All 115 tests pass
  • All checks pass (typecheck + lint + format)

🤖 Generated with Claude Code

satvik007 and others added 2 commits February 9, 2026 20:54
Add a new `allure-upload` CLI command that parses Allure results directories
and uploads test results to QA Sphere. Unlike the existing file-based parsers,
this handles directory input containing `*-result.json` files.

Key features:
- Zod-validated parsing of Allure result JSON files
- Status mapping (broken→blocked, unknown→passed)
- TMS link marker extraction (URL parsing and name regex)
- Folder derivation from suite/parentSuite/feature labels
- Attachment support via source file references
- Parameterized test name display
- Graceful skipping of malformed/invalid files and containers

Also refactors integration tests to use per-format expected counts instead of
hardcoded values, making it easier to add formats with different fixture sizes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 9, 2026

Claude finished @satvik007's task in 4m 53s —— View job


Review complete

  • Read review instructions
  • Gather PR context and read source files
  • Run specialized review subagents (code quality, security, performance, tests)
  • Aggregate findings and post review (6 inline comments)
  • Clean up outdated bot comments (none found)

Submitted a review with 6 inline comments covering 1 high, 1 medium, 2 low, and 2 nit-level findings. The most notable is a path traversal risk via untrusted attachment.source values in Allure JSON files. No performance issues were found — the parser is well-structured and follows existing codebase patterns.

@gemini-code-assist
Copy link

Summary of Changes

Hello @satvik007, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces the allure-upload command to the QAS CLI, enabling users to upload Allure test results to QA Sphere. It includes the implementation of an Allure parser, along with refactored integration tests to accommodate the new format. The changes enhance the CLI's ability to handle different test result formats and improve the overall testing workflow.

Highlights

  • New allure-upload Command: Introduces a new CLI command to parse Allure results directories and upload test results to QA Sphere.
  • Allure Parser Implementation: Implements an Allure parser with Zod validation, status mapping (broken→blocked, unknown→passed), TMS link extraction, folder derivation from suite labels, and attachment support.
  • Integration Tests Refactoring: Refactors integration tests to use per-format expected counts instead of hardcoded values, enabling each format to have natural fixtures.
Changelog
  • AGENTS.md
    • Added CLAUDE.md to the list of agents.
  • CLAUDE.md
    • Updated documentation to reflect the addition of the allure-upload command and the Allure parser.
  • package-lock.json
    • Updated package version to 0.4.6.
  • package.json
    • Updated package version to 0.4.6.
  • src/commands/main.ts
    • Registered the allure-upload command.
  • src/commands/resultUpload.ts
    • Updated command descriptions and examples to include the allure-upload command.
    • Modified the describe property to dynamically describe the input as 'results directory' for allure-upload and 'files' for other types.
  • src/tests/allure-parsing.spec.ts
    • Added unit tests for the Allure results directory parser.
  • src/tests/fixtures/allure/
    • Added new directories and files for Allure test fixtures, including unit tests, empty test suites, matching test cases, missing attachments, missing test cases, and unit test results.
  • src/tests/result-upload.spec.ts
    • Extended integration tests to cover allure-upload alongside junit-upload and playwright-json-upload.
    • Refactored integration tests to use per-format expected counts instead of hardcoded values.
  • src/utils/result-upload/ResultUploadCommandHandler.ts
    • Added allure-upload to the list of supported upload command types.
    • Modified the parser to handle directory-based input for Allure results.
  • src/utils/result-upload/allureParser.ts
    • Implemented the Allure parser with Zod validation, status mapping, TMS link extraction, folder derivation, and attachment support.
Activity
  • Added new allure-upload CLI command.
  • Implemented Allure parser with Zod validation and status mapping.
  • Refactored integration tests to use per-format expected counts.
  • Added unit tests for the Allure parser.
  • Updated documentation to reflect the new command and parser.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for uploading Allure test results. The changes are well-structured, adding a new allure-upload command, a robust Allure parser with Zod validation, and comprehensive unit tests. The integration tests have also been refactored to be more maintainable and now include Allure, which is a great improvement. I've suggested a few minor refactorings in the new Allure parser for improved readability and a change in the unit tests to enhance portability. Overall, this is a solid contribution.

})

test('Should map unknown status to passed', async () => {
const tmpDir = join('/tmp', 'allure-unknown-test')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a hardcoded /tmp path for temporary directories is not platform-independent and may cause issues on operating systems like Windows. It's better to use os.tmpdir() to get the system's temporary directory path.

You'll need to add import { tmpdir } from 'node:os'; at the top of the file. Please apply this change to all temporary directory creations in this test suite.

Suggested change
const tmpDir = join('/tmp', 'allure-unknown-test')
const tmpDir = join(tmpdir(), 'allure-unknown-test')

Comment on lines +87 to +104
const getFolderFromLabels = (labels: AllureResult['labels']): string => {
if (!labels || labels.length === 0) return ''

const labelMap = new Map<string, string>()
for (const label of labels) {
if (!labelMap.has(label.name)) {
labelMap.set(label.name, label.value)
}
}

return (
labelMap.get('suite') ||
labelMap.get('parentSuite') ||
labelMap.get('feature') ||
labelMap.get('package') ||
''
)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function can be made more concise by using Array.prototype.find() directly instead of first building a Map. This avoids the intermediate data structure and makes the intent clearer.

const getFolderFromLabels = (labels: AllureResult['labels']): string => {
	if (!labels || labels.length === 0) return ''

	const findLabelValue = (name: string) => labels.find((l) => l.name === name)?.value

	return (
		findLabelValue('suite') ||
		findLabelValue('parentSuite') ||
		findLabelValue('feature') ||
		findLabelValue('package') ||
		''
	)
}

Comment on lines +130 to +152
const buildMessage = (
result: AllureResult,
status: ResultStatus,
options: ParserOptions
): string => {
let message = ''
const details = result.statusDetails

if (!details) return message

const includeMessage = !(status === 'passed' && options.skipStdout === 'on-success')
const includeTrace = !(status === 'passed' && options.skipStderr === 'on-success')

if (includeMessage && details.message) {
message += `<h4>Message:</h4><pre><code>${escapeHtml(details.message)}</code></pre>`
}

if (includeTrace && details.trace) {
message += `<h4>Trace:</h4><pre><code>${escapeHtml(details.trace)}</code></pre>`
}

return message
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function can be refactored for better readability by using an array to collect message parts and then joining them at the end. This avoids string concatenation with += and makes the conditions for including each part a bit clearer.

const buildMessage = (
	result: AllureResult,
	status: ResultStatus,
	options: ParserOptions
): string => {
	const details = result.statusDetails
	if (!details) {
		return ''
	}

	const messages: string[] = []
	const isPassed = status === 'passed'

	if (details.message && !(isPassed && options.skipStdout === 'on-success')) {
		messages.push(`<h4>Message:</h4><pre><code>${escapeHtml(details.message)}</code></pre>`)
	}

	if (details.trace && !(isPassed && options.skipStderr === 'on-success')) {
		messages.push(`<h4>Trace:</h4><pre><code>${escapeHtml(details.trace)}</code></pre>`)
	}

	return messages.join('')
}

Comment on lines +221 to +226
const attachmentPaths: string[] = []
if (result.attachments) {
for (const attachment of result.attachments) {
attachmentPaths.push(attachment.source)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code for collecting attachment paths can be simplified into a single line using optional chaining and Array.prototype.map().

		const attachmentPaths = result.attachments?.map((a) => a.source) ?? []

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

Well-structured PR that adds the allure-upload command following existing patterns. The Allure parser is clean, the Zod schemas are thorough, and the integration test refactor to use per-format expected counts is a good improvement. The test coverage is solid with 28 unit tests and 20 integration tests.

Key findings:

Severity Issue Location
High Attachment source paths from untrusted JSON are not validated for path traversal allureParser.ts:220-226
Medium readdirSync on non-existent directory throws raw ENOENT (no user-friendly error) allureParser.ts:169
Low TMS link name regex is broader than the pattern used elsewhere in the codebase allureParser.ts:120
Low No guard against negative duration from corrupted timestamps allureParser.ts:205
Nit z.any() should be z.unknown() for type safety allureParser.ts:67
Nit Weak assertion (toBeGreaterThanOrEqual) should be exact count allure-parsing.spec.ts:19

Not flagged (no issues found): Performance — the parser is efficient with O(n) complexity, correct use of Promise.all for attachment resolution, and appropriate sync I/O for a CLI tool. No performance concerns.

Comment on lines +220 to +226
// Collect attachment file paths
const attachmentPaths: string[] = []
if (result.attachments) {
for (const attachment of result.attachments) {
attachmentPaths.push(attachment.source)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal via untrusted attachment source paths (High)

The attachment.source values are read from untrusted Allure JSON files and passed directly to getAttachments(), which calls path.join(basePath, filePath). A malicious or tampered result file could set source to ../../etc/passwd or similar, causing arbitrary file reads. The contents would then be uploaded to the QA Sphere API.

This is particularly relevant in CI/CD scenarios where Allure results may be generated by untrusted code (e.g., from a pull request).

Consider validating attachment sources at the Zod schema level:

Suggested change
// Collect attachment file paths
const attachmentPaths: string[] = []
if (result.attachments) {
for (const attachment of result.attachments) {
attachmentPaths.push(attachment.source)
}
}
const attachmentPaths: string[] = []
if (result.attachments) {
for (const attachment of result.attachments) {
// Reject paths with directory traversal components
if (attachment.source.includes('..') || attachment.source.includes('/') || attachment.source.includes('\\')) {
console.warn(`Warning: Skipping attachment with suspicious path "${attachment.source}" in ${file}`)
continue
}
attachmentPaths.push(attachment.source)
}
}

Note: the same path traversal concern exists in the existing JUnit XML and Playwright JSON parsers via their attachment path extraction, so this is a pre-existing architectural gap. But worth addressing here while adding new code.

options: ParserOptions
): Promise<TestCaseResult[]> => {
const dirPath = data
const entries = readdirSync(dirPath)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No error handling for non-existent directory (Medium)

readdirSync(dirPath) will throw a raw ENOENT error with a Node.js stack trace if the user provides a non-existent directory path. Unlike the JUnit/Playwright parsers (which receive pre-read file content from ResultUploadCommandHandler), the Allure parser reads the filesystem directly, so directory-not-found errors hit here unhandled.

Suggested change
const entries = readdirSync(dirPath)
const entries = (() => {
try {
return readdirSync(dirPath)
} catch {
throw new Error(`Could not read Allure results directory: ${dirPath}`)
}
})()


// Fall back to regex on link name
if (link.name) {
const match = link.name.match(/\b[A-Z]+-\d+\b/)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TMS link name regex is broader than the project code pattern used elsewhere (Low)

/\b[A-Z]+-\d+\b/ matches any uppercase-letters-dash-digits pattern like HTTP-200, SHA-256, or V-1. The rest of the codebase (ResultUploadCommandHandler.detectProjectCodeFromTCaseNames) uses a stricter pattern: [A-Za-z0-9]{1,5} for the project code and \d{3,} for at least 3 digits.

Consider aligning to reduce false positives:

Suggested change
const match = link.name.match(/\b[A-Z]+-\d+\b/)
const match = link.name.match(/\b[A-Z]{1,5}-\d{3,}\b/)

const result = parsed.data
const status = mapAllureStatus(result.status)
const folder = getFolderFromLabels(result.labels)
const duration = result.stop - result.start
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No guard against negative duration from corrupted timestamps (Low)

If an Allure result file has stop < start (corrupted data), this produces a negative duration. The JUnit parser guards against this with Number.isFinite(timeTakenSeconds) && timeTakenSeconds >= 0, returning null for invalid values.

Suggested change
const duration = result.stop - result.start
const duration = result.stop - result.start
const timeTaken = duration >= 0 ? duration : null

Then use timeTaken instead of duration when constructing the test case result on line 216.

labels: allureLabelSchema.array().nullable().optional(),
links: allureLinkSchema.array().nullable().optional(),
parameters: allureParameterSchema.array().nullable().optional(),
steps: z.any().nullable().optional(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer z.unknown() over z.any() for type safety (Nit)

z.any() infers as TypeScript any, which disables type checking. Since the steps field is not used by the parser, z.unknown() would be safer -- it infers as unknown and requires explicit narrowing before use.

Suggested change
steps: z.any().nullable().optional(),
steps: z.unknown().nullable().optional(),

const testcases = await parseAllureResults(allureBasePath, allureBasePath, defaultOptions)

// Should only parse *-result.json files, not containers or attachments
expect(testcases.length).toBeGreaterThanOrEqual(12)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weak assertion on fixture count (Nit)

The unit-tests directory contains exactly 12 *-result.json files, so this should assert an exact count to catch accidental duplicates or missing parses. The JUnit and Playwright unit tests both assert exact counts.

Suggested change
expect(testcases.length).toBeGreaterThanOrEqual(12)
expect(testcases).toHaveLength(12)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant