Skip to content

feat: add export#2

Merged
tbinna merged 1 commit into
mainfrom
add-export
Jun 12, 2026
Merged

feat: add export#2
tbinna merged 1 commit into
mainfrom
add-export

Conversation

@tbinna

@tbinna tbinna commented Jun 5, 2026

Copy link
Copy Markdown
Member

Closes #1

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced ifj export command to export Intercom-for-Jira data with support for source/target Jira Cloud credentials, explicit space selection, and flexible configuration via flags, environment variables, or .env files.
    • Added ifj inspect command to validate exported artifacts and display summary statistics.
    • Automatic output path numbering prevents overwriting existing exports.
  • Documentation

    • Comprehensive CLI usage guide with authentication and configuration precedence details.
    • Artifact format specification for export outputs.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements a complete ifj Intercom-for-Jira migration CLI using Effect, adding export and inspect commands with Jira Cloud integration, artifact compression and validation, runtime config resolution, comprehensive testing, and documentation.

Changes

ifj CLI export and inspect tool

Layer / File(s) Summary
Error and warning primitives
src/errors.ts, src/warnings.ts
Application error codes/class with structured context and exit codes; error message formatting; warning codes, collector with capping, and formatting utilities.
Configuration validation and precedence resolution
src/shared/config/*
Jira Cloud URL validation (HTTPS, atlassian.net), trimmed/redacted string schemas, space list normalization, export/inspect config contracts, environment variable mapping with .env file fallback and defaults.
Artifact schemas and record types
src/shared/artifact/artifact.model.ts
Discriminated union of manifest/spaceConfiguration/workItemConversationLinks record types; artifact file extension; ArtifactCounts aggregation interface.
Artifact reader and writer
src/shared/artifact/artifact.{path,reader,writer}.ts
Gzipped JSON Lines I/O with manifest-position validation, blank-line rejection, schema encode/decode with contextual errors; path collision resolution and atomic write via temp sibling file.
Conversation link normalization
src/shared/app/app.model.ts
Conversation ID deduplication via ReadonlySet; current Jira property schema (count + conversationIds); legacy array-of-objects migration with transformation.
Jira REST API client
src/shared/jira/{jira.model,jira.client,retry.util}.ts
API request/response schemas, basic-auth header construction, retry policy with rate-limit parsing and exponential backoff, status classification, paginated project/work-item search, approximate count, permission checks.
Jira integration for export
src/export/jira.service.ts
Admin verification, space discovery with Intercom config, explicit space validation, approximate linked work-item count, per-space work-item/link-property streaming.
Inspect service
src/inspect/inspect.{model,service,formatter}.ts
Artifact inspection via record folding, manifest presence check, distinct space tracking, record and ID aggregation, human-readable and JSON summary formatting.
Export orchestration
src/export/export.{service,model,formatter}.ts
Scope resolution, permission/count approximation with warnings, per-space config/link record writing, legacy link property decoding with fallback, malformed/empty property warning/skipping, mutable counter tracking, atomic artifact write, summary generation.
CLI command handlers
src/cli/cli.service.ts
Inspect and export subcommands with schema validation, config fallback, JSON formatting selection, service invocation and output routing.
CLI executable entrypoint
src/ifj.ts, src/index.ts
Node.js main entry via NodeRuntime.runMain; error handling with stderr logging and exit codes; removal of previous greet export.
Comprehensive test suite
src/**/*.test.ts
Artifact reader/writer roundtrips, Jira client auth/retry/pagination, conversation-link schema normalization, export orchestration with mocked Jira, inspect aggregation, CLI command flows with env config fallback.
Build and development configuration
package.json, tsconfig.json, tsconfig.build.json, vitest.config.ts, eslint.config.js
TypeScript emit to dist/, narrowed src include, test file exclusions, bin entry for ifj, added @effect/platform-node and @effect/vitest dependencies, test.exclude for dist/node_modules.
Documentation and ignore rules
README.md, docs/artifact.md, .gitignore, .prettierignore
README with CLI commands, authentication, flags, environment variables, and examples; artifact schema invariants and contract documentation; .env and prd/ ignore rules.
GitHub Actions validation workflow
.github/workflows/validation.yml
pnpm v10.33.0, Node.js 24, format/lint/typecheck/test/build checks on pull_request/push/workflow_dispatch.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add export' is clear and specific, accurately describing the main feature addition of the export command for the ifj CLI.
Linked Issues check ✅ Passed The PR implements all major requirements from issue #1: export/inspect commands, artifact format with manifest/records, schema validation, Jira authentication, configuration precedence, URL canonicalization, output path handling, conversation-link normalization, retry logic, warning collection, and comprehensive tests/documentation.
Out of Scope Changes check ✅ Passed All changes are directly aligned with implementing the export/inspect CLI and supporting infrastructure (error handling, models, services, tests, config, documentation, and build tooling). No unrelated or out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
package.json (1)

19-20: 💤 Low value

Beta dependencies in production package.

Both effect and @effect/platform-node are beta versions (4.0.0-beta.78). Beta releases may have breaking changes, stability issues, or incomplete features. Consider whether this is appropriate for production use or if the package should remain marked as "private": true until dependencies stabilize.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 19 - 20, package.json currently pins beta releases
for "effect" and "`@effect/platform-node`" (4.0.0-beta.78); replace or mitigate
these by either upgrading to a stable release, pinning to a known-good non-beta
version, or revert to a stable dependency set and mark the package "private":
true until you intentionally adopt the beta. Locate the dependencies named
"effect" and "`@effect/platform-node`" and change their versions to a stable tag
(or remove them if unused), or set "private": true in package.json to prevent
accidental production publishing if you must keep the betas for development.
README.md (1)

27-27: 💤 Low value

Grammar nitpick in flag description.

LanguageTool flagged "required output path" as nonstandard phrasing. While understandable, consider "output path (required)" or "path for output file (required)" for better readability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 27, Update the flag description for `--out PATH` to use
clearer phrasing: replace "required output path. Must end with `.jsonl.gz`."
with something like "output path (required). Must end with `.jsonl.gz`" or "path
for output file (required). Must end with `.jsonl.gz`" so the requirement reads
naturally while preserving the `.jsonl.gz` constraint and the `--out PATH` flag
name.
src/jira.ts (1)

170-178: 🏗️ Heavy lift

Default-scope discovery is fully serialized.

When --space is omitted, this does one project-search call and then awaits each property lookup one by one. On larger Jira tenants that makes startup latency grow linearly with project count and increases rate-limit exposure. A small bounded-concurrency pool for getSpaceConfiguration() would make the common discovery path much more usable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jira.ts` around lines 170 - 178, The current default-scope discovery
serially awaits getSpaceConfiguration for each space returned by listSpaces,
causing linear startup latency; change the loop in the method that builds
configured (using listSpaces, configured, JiraSpace) to run
getSpaceConfiguration(space.key).pipe(Effect.runPromise) in parallel with a
small bounded concurrency (e.g., 4–10 workers) rather than one-by-one,
collecting results and pushing the corresponding space when configuration !==
undefined, then sort and return as before; ensure you preserve the
Effect.runPromise call per task and handle/propagate errors consistently (fail
fast or collect errors) to match existing behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/export.ts`:
- Around line 221-225: The loop calling
jiraClient.searchWorkItemConversationLinks should not let
normalizeConversationLinksProperty throw and abort the export; wrap the call to
normalizeConversationLinksProperty(hit.propertyValue) in a try/catch, and on
catch add a warning (e.g. "MALFORMED_LINK_PROPERTY") with context including
spaceKey, workItemKey: hit.key and the error message, then continue the loop
instead of rethrowing; keep the existing skip for normalized === undefined or
empty arrays unchanged so only malformed shapes cause a warning and skip.

In `@src/jira.ts`:
- Around line 303-345: The requestJson retry loop currently calls fetch()
outside any per-attempt try/catch and has no timeout; wrap each fetch attempt
inside a try/catch and use an AbortController with a per-attempt timeout (clear
the timeout on success) so hung requests are aborted and can be retried; on
catch, build the same context (method, sanitized path, headers from the last
response if available or empty) and if attempt < maxAttempts treat the error as
retryable (use the same backoff via retryAfterMs(...) ?? 250 * 2 ** (attempt -
1) and await sleep(...)) and continue, otherwise throw an AppError (use
"jira.transient" for retryable-network-final-failure or "jira.request" as
appropriate) with the context; ensure helpers referenced (requestJson,
isRetryableStatus, retryAfterMs, sleep, sanitizePath, AppError) are used
consistently.

---

Nitpick comments:
In `@package.json`:
- Around line 19-20: package.json currently pins beta releases for "effect" and
"`@effect/platform-node`" (4.0.0-beta.78); replace or mitigate these by either
upgrading to a stable release, pinning to a known-good non-beta version, or
revert to a stable dependency set and mark the package "private": true until you
intentionally adopt the beta. Locate the dependencies named "effect" and
"`@effect/platform-node`" and change their versions to a stable tag (or remove
them if unused), or set "private": true in package.json to prevent accidental
production publishing if you must keep the betas for development.

In `@README.md`:
- Line 27: Update the flag description for `--out PATH` to use clearer phrasing:
replace "required output path. Must end with `.jsonl.gz`." with something like
"output path (required). Must end with `.jsonl.gz`" or "path for output file
(required). Must end with `.jsonl.gz`" so the requirement reads naturally while
preserving the `.jsonl.gz` constraint and the `--out PATH` flag name.

In `@src/jira.ts`:
- Around line 170-178: The current default-scope discovery serially awaits
getSpaceConfiguration for each space returned by listSpaces, causing linear
startup latency; change the loop in the method that builds configured (using
listSpaces, configured, JiraSpace) to run
getSpaceConfiguration(space.key).pipe(Effect.runPromise) in parallel with a
small bounded concurrency (e.g., 4–10 workers) rather than one-by-one,
collecting results and pushing the corresponding space when configuration !==
undefined, then sort and return as before; ensure you preserve the
Effect.runPromise call per task and handle/propagate errors consistently (fail
fast or collect errors) to match existing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0b19728-f50a-43a1-bd20-1257a7c4855d

📥 Commits

Reviewing files that changed from the base of the PR and between c8ec93a and 352f3f2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .gitignore
  • .prettierignore
  • README.md
  • docs/artifact.md
  • eslint.config.js
  • package.json
  • src/artifact.ts
  • src/cli.ts
  • src/config.ts
  • src/errors.ts
  • src/export.ts
  • src/ifj.ts
  • src/index.ts
  • src/jira.ts
  • src/links.ts
  • src/warnings.ts
  • test/index.test.ts
  • tsconfig.json
  • vitest.config.ts

Comment thread src/export.ts Outdated
Comment thread src/jira.ts Outdated
@tbinna tbinna force-pushed the add-export branch 2 times, most recently from 338c7e9 to 5ec2115 Compare June 6, 2026 07:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/jira.ts (1)

245-305: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Add request timeout and network failure retry.

The requestJson function retries on HTTP 5xx/429 status codes but:

  1. Has no request timeout—hung connections will block indefinitely
  2. Does not retry on network failures (the Effect.mapError at lines 263-269 fails immediately)

Consider adding Effect.timeout around each request attempt and treating network errors as retryable for transient failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jira.ts` around lines 245 - 305, The requestJson implementation currently
can hang and doesn't retry on network errors; wrap the httpClient.get call in an
Effect.timeout using a configured request timeout (e.g., requestTimeoutMillis)
so each attempt fails after the timeout, and change the Effect.mapError handler
around httpClient.get to classify network/transport errors as transient (create
an AppError with a retryable marker) so that when the mapped error occurs you
run the same retry logic as for isRetryableStatus (use attempt < maxAttempts,
Clock.currentTimeMillis, retryDelay and recurse requestJson(path, options,
attempt+1)); keep existing handling for 401/403 and non-retryable errors, and
ensure rateLimitHeaders/context are preserved when creating failure AppError.
🧹 Nitpick comments (2)
src/artifact.ts (1)

384-402: 💤 Low value

Mutation of shared Set in fold accumulator.

accumulateInspectState mutates state.spaceKeys (a Set) directly while spreading other properties. This works correctly in the sequential runFold context but mixes mutable and immutable patterns.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/artifact.ts` around lines 384 - 402, accumulateInspectState currently
mutates the shared Set stored in state.spaceKeys when handling
"spaceConfiguration" and "workItemConversationLinks"; instead create a new Set
copy and add record.spaceKey to it, then return the new InspectState with that
new Set (update spaceKeys in the returned object rather than calling
state.spaceKeys.add), preserving immutability in the accumulator (functions:
accumulateInspectState, InspectState, state.spaceKeys, record.spaceKey).
src/cli.ts (1)

61-67: 💤 Low value

Simplify conditional dependency spreading.

The conditional spreading pattern is verbose. Consider using object filtering or a helper.

♻️ Optional simplification
-        Effect.flatMap((config) =>
-          runExport(config, {
-            ...(dependencies.jiraClient === undefined
-              ? {}
-              : { jiraClient: dependencies.jiraClient }),
-            ...(dependencies.now === undefined ? {} : { now: dependencies.now }),
-            ...(dependencies.stderr === undefined ? {} : { stderr: dependencies.stderr }),
-          }),
-        ),
+        Effect.flatMap((config) => {
+          const { jiraClient, now, stderr } = dependencies;
+          return runExport(config, {
+            ...(jiraClient && { jiraClient }),
+            ...(now && { now }),
+            ...(stderr && { stderr }),
+          });
+        }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli.ts` around lines 61 - 67, The conditional spreading around runExport
is verbose; simplify by building a cleaned dependencies object before calling
runExport (e.g., take the dependencies object and remove keys with undefined
values) and pass that object as the second argument to runExport; specifically
update the call site referencing runExport and dependencies so you only include
jiraClient, now, stderr if they are defined by creating a filtered/compact
object (using Object.entries/Object.fromEntries or a small helper) and pass that
instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 27: Update the README.md description for the `--out PATH` flag to use
clearer wording: replace "required output path. Must end with `.jsonl.gz`." with
something like "required output file path. Must end with `.jsonl.gz`." (or
"required path for output") so the flag description reads naturally for users.

---

Duplicate comments:
In `@src/jira.ts`:
- Around line 245-305: The requestJson implementation currently can hang and
doesn't retry on network errors; wrap the httpClient.get call in an
Effect.timeout using a configured request timeout (e.g., requestTimeoutMillis)
so each attempt fails after the timeout, and change the Effect.mapError handler
around httpClient.get to classify network/transport errors as transient (create
an AppError with a retryable marker) so that when the mapped error occurs you
run the same retry logic as for isRetryableStatus (use attempt < maxAttempts,
Clock.currentTimeMillis, retryDelay and recurse requestJson(path, options,
attempt+1)); keep existing handling for 401/403 and non-retryable errors, and
ensure rateLimitHeaders/context are preserved when creating failure AppError.

---

Nitpick comments:
In `@src/artifact.ts`:
- Around line 384-402: accumulateInspectState currently mutates the shared Set
stored in state.spaceKeys when handling "spaceConfiguration" and
"workItemConversationLinks"; instead create a new Set copy and add
record.spaceKey to it, then return the new InspectState with that new Set
(update spaceKeys in the returned object rather than calling
state.spaceKeys.add), preserving immutability in the accumulator (functions:
accumulateInspectState, InspectState, state.spaceKeys, record.spaceKey).

In `@src/cli.ts`:
- Around line 61-67: The conditional spreading around runExport is verbose;
simplify by building a cleaned dependencies object before calling runExport
(e.g., take the dependencies object and remove keys with undefined values) and
pass that object as the second argument to runExport; specifically update the
call site referencing runExport and dependencies so you only include jiraClient,
now, stderr if they are defined by creating a filtered/compact object (using
Object.entries/Object.fromEntries or a small helper) and pass that instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 71ba49e5-60b4-4c56-9b44-a07862917678

📥 Commits

Reviewing files that changed from the base of the PR and between 352f3f2 and 338c7e9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .gitignore
  • .prettierignore
  • README.md
  • docs/artifact.md
  • eslint.config.js
  • package.json
  • src/artifact.ts
  • src/cli.ts
  • src/clock.ts
  • src/config.ts
  • src/errors.ts
  • src/export.ts
  • src/ifj.ts
  • src/index.ts
  • src/jira.ts
  • src/links.ts
  • src/output.ts
  • src/warnings.ts
  • test/index.test.ts
  • tsconfig.json
  • vitest.config.ts
✅ Files skipped from review due to trivial changes (4)
  • docs/artifact.md
  • vitest.config.ts
  • .gitignore
  • .prettierignore
🚧 Files skipped from review as they are similar to previous changes (9)
  • eslint.config.js
  • src/ifj.ts
  • src/index.ts
  • tsconfig.json
  • src/errors.ts
  • package.json
  • src/links.ts
  • src/export.ts
  • test/index.test.ts

Comment thread README.md Outdated
@tbinna tbinna force-pushed the add-export branch 2 times, most recently from 70f3671 to e1d67e2 Compare June 8, 2026 07:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jira.ts`:
- Around line 285-292: The paginated loop in searchProjectSpaces mutates the
incoming params via params.append(), causing duplicate query keys across
iterations; fix it by cloning the params at the start of each paginate callback
and using set (or a fresh URLSearchParams) for the pagination keys so they don't
accumulate. Concretely, inside Stream.paginate(...) in searchProjectSpaces
create e.g. const iterParams = new URLSearchParams(params.toString()) and then
call iterParams.set("startAt", String(startAt)), iterParams.set("maxResults",
String(pageSize)), iterParams.set("properties", spaceConfigurationPropertyKey")
before constructing path and context, instead of mutating the original params
with append.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c53a6dae-f294-488f-bef3-17de23df89cf

📥 Commits

Reviewing files that changed from the base of the PR and between 94d66b1 and e1d67e2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .gitignore
  • .prettierignore
  • README.md
  • docs/artifact.md
  • eslint.config.js
  • package.json
  • src/artifact.ts
  • src/cli.ts
  • src/clock.ts
  • src/config.ts
  • src/errors.ts
  • src/export.ts
  • src/ifj.ts
  • src/index.ts
  • src/jira.ts
  • src/links.ts
  • src/output.ts
  • src/warnings.ts
  • test/index.test.ts
  • tsconfig.json
  • vitest.config.ts
✅ Files skipped from review due to trivial changes (4)
  • eslint.config.js
  • .prettierignore
  • .gitignore
  • docs/artifact.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/clock.ts
  • vitest.config.ts
  • tsconfig.json
  • README.md
  • package.json
  • src/ifj.ts
  • src/index.ts
  • src/output.ts
  • src/warnings.ts
  • src/export.ts
  • src/errors.ts
  • src/links.ts

Comment thread src/jira.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/jira.ts (1)

285-291: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop mutating the shared query params across pages.

Each append() keeps the previous value, so later requests can send duplicate startAt, maxResults, and properties keys. That can break both default-space discovery and explicit-space validation.

Suggested fix
 const searchProjectSpaces = (params: URLSearchParams): Stream.Stream<JiraSpace, AppError> =>
   Stream.paginate(0, (startAt) => {
-    params.append("startAt", String(startAt));
-    params.append("maxResults", String(pageSize));
-    params.append("properties", spaceConfigurationPropertyKey);
-
-    const path = `/rest/api/3/project/search?${params.toString()}`;
+    const pageParams = new URLSearchParams(params);
+    pageParams.set("startAt", String(startAt));
+    pageParams.set("maxResults", String(pageSize));
+    pageParams.set("properties", spaceConfigurationPropertyKey);
+
+    const path = `/rest/api/3/project/search?${pageParams.toString()}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jira.ts` around lines 285 - 291, The bug is that searchProjectSpaces is
mutating the shared URLSearchParams 'params' across pages by repeatedly calling
params.append(...) inside Stream.paginate, causing duplicate query keys; fix it
by creating a fresh URLSearchParams copy per page (e.g., new
URLSearchParams(params) or building a new params instance) before appending
startAt, maxResults, and properties so each request has exactly one of each key,
and update the construction of 'path' inside Stream.paginate to use that
per-page params copy (refer to searchProjectSpaces, Stream.paginate, and the
startAt/maxResults/properties append calls).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/export.ts`:
- Around line 223-229: The current catch on jiraClient.discoverConfiguredSpaces
swallows real errors by returning [] which later triggers
export.emptyDefaultScope; instead log the warning but rethrow the original error
so transient Jira failures surface (do not return an empty array). Update the
Effect.catch wrapping jiraClient.discoverConfiguredSpaces (the pipeline that
maps with sortSpaces and calls warn(..., "DEFAULT_SCOPE_DISCOVERY_FAILED", {
reason: warningReason(error) })) to call warn/tap for the warning and then
fail/throw the same error (preserve the original error) rather than piping
Effect.as([]), so callers receive the real failure.

In `@src/jira.ts`:
- Around line 509-513: The pagination logic currently treats a missing
response.isLast as end-of-stream; update the ternary so it only stops when
response.isLast === true or response.nextPageToken is undefined. Concretely, in
the block returning [hits, ...] use a condition like (response.isLast === true
|| response.nextPageToken === undefined) ? Option.none() :
Option.some(response.nextPageToken) so that a missing isLast doesn't prevent
continuing when nextPageToken exists; refer to the variables response, isLast,
and nextPageToken in this change.

---

Duplicate comments:
In `@src/jira.ts`:
- Around line 285-291: The bug is that searchProjectSpaces is mutating the
shared URLSearchParams 'params' across pages by repeatedly calling
params.append(...) inside Stream.paginate, causing duplicate query keys; fix it
by creating a fresh URLSearchParams copy per page (e.g., new
URLSearchParams(params) or building a new params instance) before appending
startAt, maxResults, and properties so each request has exactly one of each key,
and update the construction of 'path' inside Stream.paginate to use that
per-page params copy (refer to searchProjectSpaces, Stream.paginate, and the
startAt/maxResults/properties append calls).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 749cf837-57c3-4518-bcca-a500a13e998c

📥 Commits

Reviewing files that changed from the base of the PR and between e1d67e2 and b6587e9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • .gitignore
  • .prettierignore
  • README.md
  • docs/artifact.md
  • eslint.config.js
  • package.json
  • src/artifact.ts
  • src/cli.ts
  • src/clock.ts
  • src/config.ts
  • src/errors.ts
  • src/export.ts
  • src/ifj.ts
  • src/index.ts
  • src/jira.ts
  • src/links.ts
  • src/output.ts
  • src/warnings.ts
  • test/index.test.ts
  • tsconfig.json
  • vitest.config.ts
💤 Files with no reviewable changes (1)
  • src/index.ts
✅ Files skipped from review due to trivial changes (6)
  • .gitignore
  • .prettierignore
  • package.json
  • eslint.config.js
  • README.md
  • docs/artifact.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • vitest.config.ts
  • src/clock.ts
  • src/ifj.ts
  • src/artifact.ts
  • test/index.test.ts

Comment thread src/export.ts Outdated
Comment thread src/jira.ts Outdated
@tbinna tbinna force-pushed the add-export branch 2 times, most recently from 798f5a3 to 7115e2c Compare June 12, 2026 08:43
@tbinna

tbinna commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai, can you do a full review on this again?

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

@tbinna Sure! I'll kick off a full review of the PR right away.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (9)
.github/workflows/validation.yml (1)

24-24: ⚡ Quick win

Consider pinning actions to commit SHA hashes for supply-chain security.

Using mutable tag references (e.g., @v4) allows the underlying action code to change without explicit updates to this workflow, which could introduce unexpected behavior or supply-chain attacks if a tag is moved or compromised.

Pinning to specific SHA hashes provides immutable references and stronger supply-chain guarantees.

Example SHA-pinned references
       - name: Checkout
-        uses: actions/checkout@v4
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
         with:
           persist-credentials: false

       - name: Setup pnpm
-        uses: pnpm/action-setup@v4
+        uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0

       - name: Setup Node.js
-        uses: actions/setup-node@v4
+        uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0

Note: Verify the specific SHA hashes from the respective action repositories. Comments indicate the tag version for maintainability.

Also applies to: 27-27, 32-32

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/validation.yml at line 24, Replace mutable action tags
with SHA-pinned references: change the "uses: actions/checkout@v4" entry (and
the other similar "uses" entries noted at lines 27 and 32) to the corresponding
commit SHA form (e.g., actions/checkout@<full-commit-sha>) by looking up the
exact commit SHA from the action's repository, update the workflow to use that
SHA, and add a short comment indicating the original tag (e.g., "// was `@v4`")
for maintainability.

Source: Linters/SAST tools

src/shared/artifact/artifact.writer.ts (1)

60-89: 💤 Low value

Redundant cleanup in nested error handlers.

Both writeCompressedArtifact (line 71) and writeArtifactRecords (line 86) remove tempPath on error. Since removeArtifactFile uses force: true and ignores errors, the double removal is harmless but redundant. Consider removing the cleanup from line 71 to simplify the flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/artifact/artifact.writer.ts` around lines 60 - 89, The
writeCompressedArtifact function contains a nested error handler that calls
removeArtifactFile on failure, duplicating cleanup already performed by
writeArtifactRecords via removeTempArtifact; remove the Effect.catch(...)
cleanup block from writeCompressedArtifact so it simply fails with the original
error and lets writeArtifactRecords (which calls removeTempArtifact) perform
cleanup; keep writeCompressedArtifact, writeArtifactRecords, tempSiblingPath,
removeArtifactFile and removeTempArtifact names as reference when locating and
updating the code.
src/shared/artifact/artifact.path.ts (1)

91-101: 💤 Low value

Consider adding a depth limit to recursive numbered-path search.

The findAvailableNumberedOutputPath recursion has no depth limit. If many numbered files already exist, this could theoretically cause a stack overflow. Adding a maximum suffix (e.g., 1000) and failing with a clear error would prevent runaway recursion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/artifact/artifact.path.ts` around lines 91 - 101, The recursive
helper findAvailableNumberedOutputPath can run unbounded; add a max depth (e.g.,
const MAX_SUFFIX = 1000) and before recursing check if suffix >= MAX_SUFFIX and
fail the Effect with a clear AppError (e.g., "Too many numbered artifacts,
aborting") instead of recursing; keep the existing behavior otherwise (check
candidate via statOptional and recurse with suffix+1) so the function stops when
the limit is hit and returns an explicit failure rather than risking stack
overflow.
src/shared/artifact/artifact.model.ts (2)

7-7: ⚡ Quick win

Consider stricter validation for createdAt field.

The createdAt field is typed as Schema.String, which accepts any string. Using Schema.DateTimeUtc or a pattern-constrained string would catch malformed timestamps during artifact validation and improve data quality.

📅 Suggested refinement
 export const ManifestRecord = Schema.Struct({
   type: Schema.Literal("manifest"),
-  createdAt: Schema.String,
+  createdAt: Schema.DateTimeUtc,
   source: Schema.String,
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/artifact/artifact.model.ts` at line 7, The createdAt field
currently uses Schema.String which permits any string; change its schema to a
stricter datetime validator by replacing createdAt: Schema.String with either
createdAt: Schema.DateTimeUtc (preferred) or createdAt:
Schema.String.withPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/) in
the artifact model (symbol: createdAt in artifact.model.ts), then update any
code that constructs artifacts to emit ISO-8601 UTC timestamps and run
validation/tests to ensure no callers supply malformed values.

8-8: ⚡ Quick win

Consider validating the source field as a URL.

The source field is currently a plain Schema.String with no format constraint. Validating it as a URL (e.g., Schema.String.pipe(Schema.url())) would catch malformed source values during artifact inspection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/artifact/artifact.model.ts` at line 8, The artifact model's source
field uses a plain Schema.String; change the schema for the source property to
enforce URL validation by piping it through Schema.url() (i.e., replace the
current source: Schema.String with source: Schema.String.pipe(Schema.url()) in
the artifact model definition inside artifact.model.ts) so malformed source
values are rejected during validation.
src/shared/artifact/artifact.reader.ts (1)

102-117: 💤 Low value

Consider using Ref for recordNumber counter.

The mutable let recordNumber = 0 (line 102) works correctly in this sequential stream context, but using Ref.make(0) and Ref.updateAndGet would be more consistent with Effect's functional style.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/artifact/artifact.reader.ts` around lines 102 - 117, Replace the
mutable counter `let recordNumber = 0` with an Effect-managed Ref so the counter
follows the functional style: create a Ref (e.g., `Ref.make(0)`) before piping
`artifactLineStream(path)`, and inside the Stream.mapEffect use
`Ref.updateAndGet` (or `Ref.modify`/`updateAndGet` equivalent) to atomically
increment and obtain the current record number for `parseArtifactLine(line,
recordNumber)`, leaving the `artifactLineStream`, `Stream.mapEffect`,
`parseArtifactLine`, and `sawManifest` logic unchanged except that you pass the
number read from the Ref instead of the mutable variable.
src/export/export.service.ts (1)

184-184: 💤 Low value

Simplify the Option handling pattern.

The ?? Option.none<unknown>() pattern suggests space.configuration might be Option<unknown> | undefined. If space.configuration is already an Option type, the nullish coalescing is unnecessary. If it's potentially undefined, consider making ExportJiraSpace.configuration consistently Option<unknown> to avoid the mixed pattern.

♻️ Possible simplification if already an Option
-      const configuration = space.configuration ?? Option.none<unknown>();
-      if (Option.isNone(configuration)) {
+      if (Option.isNone(space.configuration)) {
         return Stream.empty;
       }
 
-      const jsonConfiguration = yield* decodeJson(configuration.value);
+      const jsonConfiguration = yield* decodeJson(space.configuration.value);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/export/export.service.ts` at line 184, The nullish coalescing in "const
configuration = space.configuration ?? Option.none<unknown>()" indicates mixed
types; make ExportJiraSpace.configuration consistently an Option<unknown> and
remove the unnecessary "?? Option.none<...>()" usage so you can simply use
"const configuration = space.configuration" (or, if configuration truly can be
undefined, change the ExportJiraSpace type to Option<unknown> and initialize
wherever spaces are constructed). Update references to the symbol
ExportJiraSpace.configuration and the variable configuration in
export.service.ts accordingly to reflect the single Option type.
src/cli/cli.service.ts (1)

66-66: ⚖️ Poor tradeoff

Document or reconsider the 1,000 space limit.

The Flag.between(1, 1_000) constraint caps explicit space selection at 1,000 spaces. While reasonable for most use cases, large Jira instances with extensive space hierarchies might exceed this limit. Consider documenting this limit in user-facing help text or making it configurable if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/cli.service.ts` at line 66, The Flag.between(1, 1_000) hard-caps
explicit space selection at 1,000; either document this limit in the CLI help
text or make it configurable. Update the code around Flag.between to reference a
named constant (e.g., MAX_SPACE_SELECTION_LIMIT) or a new CLI option/env var
(e.g., --max-spaces / process.env.MAX_SPACES) instead of the literal 1_000, and
update the user-facing help/usage string to mention the default limit and how to
override it; ensure Flag.between uses that configurable value and add a short
note in the help text returned by the CLI so users know the cap and how to
change it.
src/shared/jira/jira.model.ts (1)

37-42: Review SchemaGetter withDefault / passthrough usage with Schema.decodeTo (Effect Schema v4)
Effect Schema v4 docs describe SchemaGetter.withDefault as providing a default Effect for missing values and SchemaGetter.passthrough() as an identity getter intended for Schema.decodeTo, so this usage pattern is aligned. If the goal is simply a default for a missing/undefined key, consider the dedicated Schema.withDecodingDefault* helpers for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/jira/jira.model.ts` around lines 37 - 42, The current
JiraPropertiesField uses
Schema.optionalKey(JiraProperties).pipe(Schema.decodeTo(JiraProperties, {
decode: SchemaGetter.withDefault(Effect.succeed({})), encode:
SchemaGetter.passthrough() })) — replace this pattern by using the Effect Schema
v4 decoding-default helpers for clarity: remove the manual Schema.decodeTo +
SchemaGetter.withDefault usage and instead apply the appropriate
Schema.withDecodingDefault* helper on JiraProperties (or the optional key) so
missing/undefined values get the same {} default while keeping encoding
passthrough behavior; update references to JiraPropertiesField,
Schema.optionalKey, Schema.decodeTo, SchemaGetter.withDefault,
SchemaGetter.passthrough and JiraProperties accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/validation.yml:
- Around line 23-24: Update the Checkout step that uses actions/checkout@v4 to
disable credential persistence by adding persist-credentials: false under the
step (the step with name "Checkout" and uses "actions/checkout@v4"); this
prevents GitHub credentials from being left in the workspace and potentially
leaked via artifacts or dependencies.

In `@README.md`:
- Around line 43-45: Update the README entry for the `--out PATH` CLI flag to
state that `--out` is required (no default) and that the tool enforces overwrite
protection (it will error or prompt if the target file exists), replacing the
current text that claims a default `intercom-for-jira-export.jsonl.gz` in the
CWD; mention the exact flag `--out PATH` and the previous default name
`intercom-for-jira-export.jsonl.gz` so readers understand the change and the
overwrite behavior.
- Around line 3-19: The README incorrectly describes an import phase and shared
token reuse for “import and export” while the shipped CLI only provides export
and inspect; update the top-level description sentence to say ifj exports
Intercom for Jira Cloud data and offers inspection tools (e.g., `export` and
`inspect`) instead of “exports ... and then imports,” remove or rework the
Target Jira site section that describes an import user/token (or mark it as
future/optional), and delete the final sentence about using the same token “for
both import and export” so the docs reflect only the exported functionality and
token requirements for export/inspection.

In `@src/export/export.service.ts`:
- Around line 68-75: The current error assumes the empty result came from
default discovery; update the branch after calling resolveSpaces to distinguish
whether the user passed explicit spaces (check config.spaces and its length) vs
the default-discovery path and throw an appropriate AppError/message for each
case (e.g., keep "export.emptyDefaultScope" and message for no discovered
spaces, and add a new code like "export.emptyExplicitScope" with a message that
validation removed all requested spaces), referencing resolveSpaces,
config.spaces, and the AppError construction so the correct error code and
user-facing message are used depending on how spaces were provided.
- Around line 237-244: The code currently swallows decodeLinkProperty failures
by mapping them to Option.none() inside Effect.matchEffect; instead update the
onFailure branch so that a malformed link property aborts the export by
returning an Effect.fail (or rethrowing the decode failure) after calling
warnMalformedLinkProperty, rather than returning Option.none(). Concretely, in
the decode block that populates maybeLinkProperty (using decodeLinkProperty,
Effect.matchEffect, warnMalformedLinkProperty, warnings, spaceKey, hit.key),
replace the onFailure handler that pipes to Effect.as(Option.none()) with a
handler that first invokes warnMalformedLinkProperty (if you still need the
side-effect) and then returns Effect.fail(failure) so the overall effect fails
and triggers export cleanup instead of skipping the record.

In `@src/shared/app/app.model.ts`:
- Around line 3-9: conversationIdsSchema currently uses Schema.String and thus
allows empty or whitespace-only IDs; update conversationIdsSchema (and ensure
conversationIdsFromArraySchema conversion still works) to trim input and reject
empty/whitespace-only strings by composing a trimmed-string schema and a
non-empty/refinement check (e.g., use Schema.String.pipe(Schema.trim()) then
Schema.refine or Schema.minLength(1) / Schema.nonEmpty depending on your schema
API) so any string consisting only of whitespace fails validation before being
included in the ReadonlySet; keep the transform/encode logic in
conversationIdsFromArraySchema unchanged except it should rely on the new
validated conversationIdsSchema.

In `@src/shared/artifact/artifact.path.ts`:
- Around line 62-104: ensureOutputTarget currently auto-numbers existing files
which contradicts the missing --overwrite behavior; add an overwrite boolean
parameter to ensureOutputTarget(outputPath, overwrite: boolean) and change its
logic so that if outputPath exists as a file it returns outputPath when
overwrite===true, otherwise it fails with an AppError (e.g.,
"export.invalidOutput") instead of recursing to findAvailableNumberedOutputPath;
then plumb the new overwrite flag from the CLI: add a --overwrite boolean option
to exportCommand and pass that flag into ArtifactWriterService.write, and update
ArtifactWriterService.write to forward overwrite into ensureOutputTarget; remove
or deprecate the auto-numbering helper (findAvailableNumberedOutputPath) if you
adopt the overwrite semantics.

In `@src/shared/jira/jira.client.ts`:
- Around line 223-228: The current return uses response.isLast to decide
pagination end; instead rely on response.nextPageToken being present—update the
tuple returned from the pagination function (the return that currently
references response.issues and the Option wrapping logic) to ignore
response.isLast and produce Option.none when response.nextPageToken is
null/undefined, otherwise Option.some(Option.some(response.nextPageToken)); keep
the issues array unchanged and only change the pagination token logic so it
continues whenever nextPageToken is provided.

---

Nitpick comments:
In @.github/workflows/validation.yml:
- Line 24: Replace mutable action tags with SHA-pinned references: change the
"uses: actions/checkout@v4" entry (and the other similar "uses" entries noted at
lines 27 and 32) to the corresponding commit SHA form (e.g.,
actions/checkout@<full-commit-sha>) by looking up the exact commit SHA from the
action's repository, update the workflow to use that SHA, and add a short
comment indicating the original tag (e.g., "// was `@v4`") for maintainability.

In `@src/cli/cli.service.ts`:
- Line 66: The Flag.between(1, 1_000) hard-caps explicit space selection at
1,000; either document this limit in the CLI help text or make it configurable.
Update the code around Flag.between to reference a named constant (e.g.,
MAX_SPACE_SELECTION_LIMIT) or a new CLI option/env var (e.g., --max-spaces /
process.env.MAX_SPACES) instead of the literal 1_000, and update the user-facing
help/usage string to mention the default limit and how to override it; ensure
Flag.between uses that configurable value and add a short note in the help text
returned by the CLI so users know the cap and how to change it.

In `@src/export/export.service.ts`:
- Line 184: The nullish coalescing in "const configuration = space.configuration
?? Option.none<unknown>()" indicates mixed types; make
ExportJiraSpace.configuration consistently an Option<unknown> and remove the
unnecessary "?? Option.none<...>()" usage so you can simply use "const
configuration = space.configuration" (or, if configuration truly can be
undefined, change the ExportJiraSpace type to Option<unknown> and initialize
wherever spaces are constructed). Update references to the symbol
ExportJiraSpace.configuration and the variable configuration in
export.service.ts accordingly to reflect the single Option type.

In `@src/shared/artifact/artifact.model.ts`:
- Line 7: The createdAt field currently uses Schema.String which permits any
string; change its schema to a stricter datetime validator by replacing
createdAt: Schema.String with either createdAt: Schema.DateTimeUtc (preferred)
or createdAt:
Schema.String.withPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/) in
the artifact model (symbol: createdAt in artifact.model.ts), then update any
code that constructs artifacts to emit ISO-8601 UTC timestamps and run
validation/tests to ensure no callers supply malformed values.
- Line 8: The artifact model's source field uses a plain Schema.String; change
the schema for the source property to enforce URL validation by piping it
through Schema.url() (i.e., replace the current source: Schema.String with
source: Schema.String.pipe(Schema.url()) in the artifact model definition inside
artifact.model.ts) so malformed source values are rejected during validation.

In `@src/shared/artifact/artifact.path.ts`:
- Around line 91-101: The recursive helper findAvailableNumberedOutputPath can
run unbounded; add a max depth (e.g., const MAX_SUFFIX = 1000) and before
recursing check if suffix >= MAX_SUFFIX and fail the Effect with a clear
AppError (e.g., "Too many numbered artifacts, aborting") instead of recursing;
keep the existing behavior otherwise (check candidate via statOptional and
recurse with suffix+1) so the function stops when the limit is hit and returns
an explicit failure rather than risking stack overflow.

In `@src/shared/artifact/artifact.reader.ts`:
- Around line 102-117: Replace the mutable counter `let recordNumber = 0` with
an Effect-managed Ref so the counter follows the functional style: create a Ref
(e.g., `Ref.make(0)`) before piping `artifactLineStream(path)`, and inside the
Stream.mapEffect use `Ref.updateAndGet` (or `Ref.modify`/`updateAndGet`
equivalent) to atomically increment and obtain the current record number for
`parseArtifactLine(line, recordNumber)`, leaving the `artifactLineStream`,
`Stream.mapEffect`, `parseArtifactLine`, and `sawManifest` logic unchanged
except that you pass the number read from the Ref instead of the mutable
variable.

In `@src/shared/artifact/artifact.writer.ts`:
- Around line 60-89: The writeCompressedArtifact function contains a nested
error handler that calls removeArtifactFile on failure, duplicating cleanup
already performed by writeArtifactRecords via removeTempArtifact; remove the
Effect.catch(...) cleanup block from writeCompressedArtifact so it simply fails
with the original error and lets writeArtifactRecords (which calls
removeTempArtifact) perform cleanup; keep writeCompressedArtifact,
writeArtifactRecords, tempSiblingPath, removeArtifactFile and removeTempArtifact
names as reference when locating and updating the code.

In `@src/shared/jira/jira.model.ts`:
- Around line 37-42: The current JiraPropertiesField uses
Schema.optionalKey(JiraProperties).pipe(Schema.decodeTo(JiraProperties, {
decode: SchemaGetter.withDefault(Effect.succeed({})), encode:
SchemaGetter.passthrough() })) — replace this pattern by using the Effect Schema
v4 decoding-default helpers for clarity: remove the manual Schema.decodeTo +
SchemaGetter.withDefault usage and instead apply the appropriate
Schema.withDecodingDefault* helper on JiraProperties (or the optional key) so
missing/undefined values get the same {} default while keeping encoding
passthrough behavior; update references to JiraPropertiesField,
Schema.optionalKey, Schema.decodeTo, SchemaGetter.withDefault,
SchemaGetter.passthrough and JiraProperties accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a5b69f6-97dd-48b5-9246-debdda9ce463

📥 Commits

Reviewing files that changed from the base of the PR and between c8ec93a and 7115e2c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (49)
  • .github/workflows/validation.yml
  • .gitignore
  • .prettierignore
  • README.md
  • docs/artifact.md
  • eslint.config.js
  • package.json
  • src/cli.test.ts
  • src/cli/cli.service.ts
  • src/cli/index.ts
  • src/errors.ts
  • src/export/export.formatter.ts
  • src/export/export.model.ts
  • src/export/export.service.test.ts
  • src/export/export.service.ts
  • src/export/index.ts
  • src/export/jira.service.test.ts
  • src/export/jira.service.ts
  • src/ifj.ts
  • src/index.ts
  • src/inspect/index.ts
  • src/inspect/inspect.formatter.ts
  • src/inspect/inspect.model.ts
  • src/inspect/inspect.service.test.ts
  • src/inspect/inspect.service.ts
  • src/shared/app/app.model.ts
  • src/shared/app/index.ts
  • src/shared/artifact/artifact.model.ts
  • src/shared/artifact/artifact.path.ts
  • src/shared/artifact/artifact.reader.test.ts
  • src/shared/artifact/artifact.reader.ts
  • src/shared/artifact/artifact.writer.test.ts
  • src/shared/artifact/artifact.writer.ts
  • src/shared/artifact/index.ts
  • src/shared/config/config.model.ts
  • src/shared/config/config.provider.ts
  • src/shared/config/export.config.ts
  • src/shared/config/index.ts
  • src/shared/config/inspect.config.ts
  • src/shared/jira/index.ts
  • src/shared/jira/jira.client.test.ts
  • src/shared/jira/jira.client.ts
  • src/shared/jira/jira.model.ts
  • src/shared/jira/retry.util.ts
  • src/warnings.ts
  • test/index.test.ts
  • tsconfig.build.json
  • tsconfig.json
  • vitest.config.ts
💤 Files with no reviewable changes (2)
  • test/index.test.ts
  • src/index.ts

Comment thread .github/workflows/validation.yml
Comment thread README.md
Comment thread README.md
Comment thread src/export/export.service.ts
Comment thread src/export/export.service.ts
Comment thread src/shared/app/app.model.ts Outdated
Comment thread src/shared/artifact/artifact.path.ts
Comment thread src/shared/jira/jira.client.ts
@tbinna tbinna merged commit 733e17e into main Jun 12, 2026
3 checks passed
@tbinna tbinna deleted the add-export branch June 12, 2026 11:22
@coderabbitai coderabbitai Bot mentioned this pull request Jun 17, 2026
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.

Export and Inspect

1 participant