@testream/dotnet-reporter 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,4 +23,4 @@ Run .NET tests and send the results from your codebase to Jira via Testream.
23
23
 
24
24
  This installs the reporter, runs your .NET tests, and uploads results to Testream.
25
25
 
26
- For CLI options and CI examples, see https://testream.github.io/docs/reporters/dotnet.
26
+ For CLI options and CI examples, see https://docs.testream.app/reporters/dotnet.
package/dist/index.js CHANGED
@@ -2164,79 +2164,131 @@ module.exports = toNumber;
2164
2164
 
2165
2165
  Object.defineProperty(exports, "__esModule", ({ value: true }));
2166
2166
  exports.detectCIContext = detectCIContext;
2167
- /**
2168
- * Detect CI context from environment variables
2169
- * Supports: GitHub Actions, GitLab CI, Azure Pipelines, CircleCI, Jenkins, Bitbucket Pipelines
2170
- */
2171
- function detectCIContext() {
2172
- const env = process.env;
2173
- // GitHub Actions
2174
- if (env.GITHUB_ACTIONS === 'true') {
2175
- return {
2176
- branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME || env.GITHUB_REF?.replace('refs/heads/', ''),
2177
- commitSha: env.GITHUB_SHA,
2178
- repositoryUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY
2179
- ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`
2180
- : undefined,
2181
- buildNumber: env.GITHUB_RUN_NUMBER,
2182
- buildUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID
2183
- ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`
2184
- : undefined,
2185
- };
2167
+ function removePrefix(value, prefix) {
2168
+ if (!value) {
2169
+ return undefined;
2186
2170
  }
2187
- // GitLab CI
2188
- if (env.GITLAB_CI === 'true') {
2189
- return {
2190
- branch: env.CI_COMMIT_BRANCH || env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
2191
- commitSha: env.CI_COMMIT_SHA,
2192
- repositoryUrl: env.CI_PROJECT_URL,
2193
- buildNumber: env.CI_PIPELINE_IID,
2194
- buildUrl: env.CI_PIPELINE_URL,
2195
- };
2171
+ return value.startsWith(prefix) ? value.slice(prefix.length) : value;
2172
+ }
2173
+ function readEnvironment() {
2174
+ const runtime = globalThis;
2175
+ return runtime.process?.env ?? {};
2176
+ }
2177
+ function buildGithubRepositoryUrl(env) {
2178
+ if (!env.GITHUB_SERVER_URL || !env.GITHUB_REPOSITORY) {
2179
+ return undefined;
2196
2180
  }
2197
- // Azure Pipelines
2198
- if (env.TF_BUILD === 'True') {
2199
- return {
2200
- branch: env.BUILD_SOURCEBRANCH?.replace('refs/heads/', ''),
2201
- commitSha: env.BUILD_SOURCEVERSION,
2202
- repositoryUrl: env.BUILD_REPOSITORY_URI,
2203
- buildNumber: env.BUILD_BUILDNUMBER,
2204
- buildUrl: env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI && env.SYSTEM_TEAMPROJECT && env.BUILD_BUILDID
2205
- ? `${env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`
2206
- : undefined,
2207
- };
2181
+ return `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`;
2182
+ }
2183
+ function buildGithubBuildUrl(env) {
2184
+ if (!env.GITHUB_SERVER_URL || !env.GITHUB_REPOSITORY || !env.GITHUB_RUN_ID) {
2185
+ return undefined;
2208
2186
  }
2209
- // CircleCI
2210
- if (env.CIRCLECI === 'true') {
2211
- return {
2212
- branch: env.CIRCLE_BRANCH,
2213
- commitSha: env.CIRCLE_SHA1,
2214
- repositoryUrl: env.CIRCLE_REPOSITORY_URL,
2215
- buildNumber: env.CIRCLE_BUILD_NUM,
2216
- buildUrl: env.CIRCLE_BUILD_URL,
2217
- };
2187
+ return `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`;
2188
+ }
2189
+ function detectGitHubBranch(env) {
2190
+ if (env.GITHUB_HEAD_REF) {
2191
+ return env.GITHUB_HEAD_REF;
2218
2192
  }
2219
- // Jenkins
2220
- if (env.JENKINS_URL) {
2221
- return {
2222
- branch: env.GIT_BRANCH?.replace('origin/', ''),
2223
- commitSha: env.GIT_COMMIT,
2224
- repositoryUrl: env.GIT_URL,
2225
- buildNumber: env.BUILD_NUMBER,
2226
- buildUrl: env.BUILD_URL,
2227
- };
2193
+ if (env.GITHUB_REF?.startsWith('refs/heads/')) {
2194
+ return env.GITHUB_REF_NAME ?? removePrefix(env.GITHUB_REF, 'refs/heads/');
2228
2195
  }
2229
- // Bitbucket Pipelines
2230
- if (env.BITBUCKET_BUILD_NUMBER) {
2231
- return {
2232
- branch: env.BITBUCKET_BRANCH,
2233
- commitSha: env.BITBUCKET_COMMIT,
2234
- repositoryUrl: env.BITBUCKET_GIT_HTTP_ORIGIN,
2235
- buildNumber: env.BITBUCKET_BUILD_NUMBER,
2236
- buildUrl: env.BITBUCKET_REPO_FULL_NAME && env.BITBUCKET_BUILD_NUMBER
2237
- ? `https://bitbucket.org/${env.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`
2238
- : undefined,
2239
- };
2196
+ return undefined;
2197
+ }
2198
+ function detectGitHubActionsContext(env) {
2199
+ return {
2200
+ branch: detectGitHubBranch(env),
2201
+ commitSha: env.GITHUB_SHA,
2202
+ repositoryUrl: buildGithubRepositoryUrl(env),
2203
+ buildNumber: env.GITHUB_RUN_NUMBER,
2204
+ buildUrl: buildGithubBuildUrl(env),
2205
+ };
2206
+ }
2207
+ function detectGitLabContext(env) {
2208
+ return {
2209
+ branch: env.CI_COMMIT_BRANCH || env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME,
2210
+ commitSha: env.CI_COMMIT_SHA,
2211
+ repositoryUrl: env.CI_PROJECT_URL,
2212
+ buildNumber: env.CI_PIPELINE_IID,
2213
+ buildUrl: env.CI_PIPELINE_URL,
2214
+ };
2215
+ }
2216
+ function detectAzurePipelinesContext(env) {
2217
+ return {
2218
+ branch: removePrefix(env.BUILD_SOURCEBRANCH, 'refs/heads/'),
2219
+ commitSha: env.BUILD_SOURCEVERSION,
2220
+ repositoryUrl: env.BUILD_REPOSITORY_URI,
2221
+ buildNumber: env.BUILD_BUILDNUMBER,
2222
+ buildUrl: env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI && env.SYSTEM_TEAMPROJECT && env.BUILD_BUILDID
2223
+ ? `${env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`
2224
+ : undefined,
2225
+ };
2226
+ }
2227
+ function detectCircleCIContext(env) {
2228
+ return {
2229
+ branch: env.CIRCLE_BRANCH,
2230
+ commitSha: env.CIRCLE_SHA1,
2231
+ repositoryUrl: env.CIRCLE_REPOSITORY_URL,
2232
+ buildNumber: env.CIRCLE_BUILD_NUM,
2233
+ buildUrl: env.CIRCLE_BUILD_URL,
2234
+ };
2235
+ }
2236
+ function detectJenkinsContext(env) {
2237
+ return {
2238
+ branch: removePrefix(env.GIT_BRANCH, 'origin/'),
2239
+ commitSha: env.GIT_COMMIT,
2240
+ repositoryUrl: env.GIT_URL,
2241
+ buildNumber: env.BUILD_NUMBER,
2242
+ buildUrl: env.BUILD_URL,
2243
+ };
2244
+ }
2245
+ function detectBitbucketContext(env) {
2246
+ return {
2247
+ branch: env.BITBUCKET_BRANCH,
2248
+ commitSha: env.BITBUCKET_COMMIT,
2249
+ repositoryUrl: env.BITBUCKET_GIT_HTTP_ORIGIN,
2250
+ buildNumber: env.BITBUCKET_BUILD_NUMBER,
2251
+ buildUrl: env.BITBUCKET_REPO_FULL_NAME && env.BITBUCKET_BUILD_NUMBER
2252
+ ? `https://bitbucket.org/${env.BITBUCKET_REPO_FULL_NAME}/pipelines/results/${env.BITBUCKET_BUILD_NUMBER}`
2253
+ : undefined,
2254
+ };
2255
+ }
2256
+ const CI_PROVIDER_DETECTORS = [
2257
+ {
2258
+ isMatch: (env) => env.GITHUB_ACTIONS === 'true',
2259
+ detect: detectGitHubActionsContext,
2260
+ },
2261
+ {
2262
+ isMatch: (env) => env.GITLAB_CI === 'true',
2263
+ detect: detectGitLabContext,
2264
+ },
2265
+ {
2266
+ isMatch: (env) => env.TF_BUILD === 'True',
2267
+ detect: detectAzurePipelinesContext,
2268
+ },
2269
+ {
2270
+ isMatch: (env) => env.CIRCLECI === 'true',
2271
+ detect: detectCircleCIContext,
2272
+ },
2273
+ {
2274
+ isMatch: (env) => Boolean(env.JENKINS_URL),
2275
+ detect: detectJenkinsContext,
2276
+ },
2277
+ {
2278
+ isMatch: (env) => Boolean(env.BITBUCKET_BUILD_NUMBER),
2279
+ detect: detectBitbucketContext,
2280
+ },
2281
+ ];
2282
+ /**
2283
+ * Detect CI context from environment variables
2284
+ * Supports: GitHub Actions, GitLab CI, Azure Pipelines, CircleCI, Jenkins, Bitbucket Pipelines
2285
+ */
2286
+ function detectCIContext() {
2287
+ const env = readEnvironment();
2288
+ for (const provider of CI_PROVIDER_DETECTORS) {
2289
+ if (provider.isMatch(env)) {
2290
+ return provider.detect(env);
2291
+ }
2240
2292
  }
2241
2293
  return {};
2242
2294
  }
@@ -2299,6 +2351,7 @@ exports.ensureReportId = ensureReportId;
2299
2351
  exports.uploadTestRun = uploadTestRun;
2300
2352
  exports.mapAttachmentsToTestResults = mapAttachmentsToTestResults;
2301
2353
  exports.uploadArtifacts = uploadArtifacts;
2354
+ const ci_detection_1 = __nccwpck_require__(406);
2302
2355
  const DEFAULT_API_URL = 'https://test-manager-backend.fly.dev';
2303
2356
  function ensureReportId(report) {
2304
2357
  if (typeof report.reportId === 'string' && report.reportId.trim().length > 0) {
@@ -2335,6 +2388,52 @@ function normalizeApiUrl(value) {
2335
2388
  }
2336
2389
  return trimmed.replace(/\/+$/, '');
2337
2390
  }
2391
+ function isReportAlreadyExistsConflict(error) {
2392
+ return error.errorCode === 'report_already_exists';
2393
+ }
2394
+ function formatUploadError(error) {
2395
+ const detail = error.detail || error.title || error.rawBody || 'Unknown error';
2396
+ const codeLabel = error.errorCode ? `, code=${error.errorCode}` : '';
2397
+ return `Upload failed (HTTP ${error.statusCode}${codeLabel}): ${detail}`;
2398
+ }
2399
+ async function parseHttpError(response) {
2400
+ const rawBody = await response.text();
2401
+ const contentType = response.headers.get('content-type') || '';
2402
+ if (contentType.toLowerCase().includes('json') && rawBody.trim().length > 0) {
2403
+ try {
2404
+ const parsed = JSON.parse(rawBody);
2405
+ return {
2406
+ statusCode: response.status,
2407
+ title: parsed.title,
2408
+ detail: parsed.detail || parsed.details || parsed.error,
2409
+ errorCode: parsed.errorCode,
2410
+ rawBody,
2411
+ };
2412
+ }
2413
+ catch {
2414
+ // Fall through to raw body handling
2415
+ }
2416
+ }
2417
+ return {
2418
+ statusCode: response.status,
2419
+ rawBody,
2420
+ };
2421
+ }
2422
+ function resolveUploadContext(options) {
2423
+ const ciContext = (0, ci_detection_1.detectCIContext)();
2424
+ return {
2425
+ commitSha: options.commitSha ?? ciContext.commitSha,
2426
+ branch: options.branch ?? ciContext.branch,
2427
+ repositoryUrl: options.repositoryUrl ?? ciContext.repositoryUrl,
2428
+ buildName: options.buildName,
2429
+ buildNumber: options.buildNumber ?? ciContext.buildNumber,
2430
+ buildUrl: options.buildUrl ?? ciContext.buildUrl,
2431
+ testEnvironment: options.testEnvironment,
2432
+ appName: options.appName,
2433
+ appVersion: options.appVersion,
2434
+ testType: options.testType,
2435
+ };
2436
+ }
2338
2437
  /**
2339
2438
  * Upload test run to Testream backend API
2340
2439
  */
@@ -2342,20 +2441,12 @@ async function uploadTestRun(options) {
2342
2441
  const { report, apiKey } = options;
2343
2442
  const apiUrl = resolveApiUrl(options.apiUrl);
2344
2443
  const reportId = ensureReportId(report);
2444
+ const uploadContext = resolveUploadContext(options);
2345
2445
  // Build type-safe IngestRequest payload
2346
2446
  const ingestPayload = {
2347
2447
  report,
2348
2448
  reportId,
2349
- commitSha: options.commitSha,
2350
- branch: options.branch,
2351
- repositoryUrl: options.repositoryUrl,
2352
- buildName: options.buildName,
2353
- buildNumber: options.buildNumber,
2354
- buildUrl: options.buildUrl,
2355
- testEnvironment: options.testEnvironment,
2356
- appName: options.appName,
2357
- appVersion: options.appVersion,
2358
- testType: options.testType,
2449
+ ...uploadContext,
2359
2450
  };
2360
2451
  // Professional logging
2361
2452
  console.log('Uploading test results...');
@@ -2372,34 +2463,41 @@ async function uploadTestRun(options) {
2372
2463
  }
2373
2464
  catch (error) {
2374
2465
  const errorMessage = error instanceof Error ? error.message : String(error);
2466
+ const connectionError = `Connection failed: ${errorMessage}`;
2467
+ console.error(connectionError);
2375
2468
  return {
2376
2469
  success: false,
2377
2470
  reportId,
2378
- error: `Connection failed: ${errorMessage}`,
2471
+ error: connectionError,
2379
2472
  };
2380
2473
  }
2381
- // Handle 409 (report already exists) - idempotent
2382
- if (response.status === 409) {
2383
- console.warn('Report already exists (workflow may have been re-run)');
2384
- return {
2385
- success: true,
2386
- reportId,
2387
- summary: {
2388
- passed: report.results.summary.passed,
2389
- failed: report.results.summary.failed,
2390
- skipped: report.results.summary.skipped,
2391
- total: report.results.summary.tests,
2392
- },
2393
- alreadyExists: true,
2394
- };
2395
- }
2396
- // Handle other errors
2397
2474
  if (!response.ok) {
2398
- const errorText = await response.text();
2475
+ const parsedError = await parseHttpError(response);
2476
+ if (response.status === 409 && isReportAlreadyExistsConflict(parsedError)) {
2477
+ console.warn('Report already exists (workflow may have been re-run)');
2478
+ return {
2479
+ success: true,
2480
+ reportId,
2481
+ summary: {
2482
+ passed: report.results.summary.passed,
2483
+ failed: report.results.summary.failed,
2484
+ skipped: report.results.summary.skipped,
2485
+ total: report.results.summary.tests,
2486
+ },
2487
+ alreadyExists: true,
2488
+ statusCode: parsedError.statusCode,
2489
+ errorCode: parsedError.errorCode,
2490
+ };
2491
+ }
2492
+ const formattedError = formatUploadError(parsedError);
2493
+ console.error(formattedError);
2399
2494
  return {
2400
2495
  success: false,
2401
2496
  reportId,
2402
- error: `Upload failed (HTTP ${response.status}): ${errorText}`,
2497
+ error: formattedError,
2498
+ statusCode: parsedError.statusCode,
2499
+ errorCode: parsedError.errorCode,
2500
+ errorDetail: parsedError.detail || parsedError.rawBody,
2403
2501
  };
2404
2502
  }
2405
2503
  // Success
@@ -2958,26 +3056,18 @@ async function main() {
2958
3056
  logger.info('='.repeat(60));
2959
3057
  return;
2960
3058
  }
2961
- // Detect CI context from environment
2962
- const ciContext = (0, uploader_1.detectGitContext)();
2963
- // Merge CLI options with auto-detected CI context (CLI takes precedence)
2964
- const branch = options.branch || ciContext.branch;
2965
- const commitSha = options.commitSha || ciContext.commitSha;
2966
- const repositoryUrl = options.repositoryUrl || ciContext.repositoryUrl;
2967
- const buildNumber = options.buildNumber || ciContext.buildNumber;
2968
- const buildUrl = options.buildUrl || ciContext.buildUrl;
2969
3059
  // Upload to API
2970
3060
  const result = await (0, uploader_1.uploadToApi)({
2971
3061
  report,
2972
3062
  apiKey: options.apiKey,
2973
3063
  // Git context
2974
- branch,
2975
- commitSha,
2976
- repositoryUrl,
3064
+ branch: options.branch,
3065
+ commitSha: options.commitSha,
3066
+ repositoryUrl: options.repositoryUrl,
2977
3067
  // Build metadata
2978
3068
  buildName: options.buildName,
2979
- buildNumber,
2980
- buildUrl,
3069
+ buildNumber: options.buildNumber,
3070
+ buildUrl: options.buildUrl,
2981
3071
  // Environment metadata
2982
3072
  testEnvironment: options.testEnvironment,
2983
3073
  appName: options.appName,