Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Validation

on:
pull_request:
push:
branches:
- "**"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test-and-build:
name: Test and build
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Check formatting
run: pnpm run format:check

- name: Lint
run: pnpm run lint

- name: Typecheck
run: pnpm run typecheck

- name: Test
run: pnpm test

- name: Build
run: pnpm run build
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ coverage/
dist/
node_modules/
*.tsbuildinfo
.idea
.idea
.env
intercom-for-jira-export*.jsonl.gz
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ coverage
dist
node_modules
pnpm-lock.yaml
prd/
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Intercom for Jira Migration CLI

`ifj` is a command line utility to support Cloud-to-Cloud migrations for Intercom for Jira Cloud (ifj). It exports Intercom for Jira Cloud data from a source Jira Cloud site and then imports it into a target site.

## Requirements

### Source Jira site

- **Administrator user:** This user should have access to all Jira spaces on the source site.
- **Jira API token:** Follow the [Atlassian documentation on how to create a scoped Jira API token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/#Create-an-API-token-with-scopes) for the administrator user. Include the following Classic scopes:
- `read:jira-work`

### Target Jira site

- **Administrator user:** This user should have access to all Jira spaces on the target site.
- **Jira API token:** Follow the [Atlassian documentation on how to create a scoped Jira API token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/#Create-an-API-token-with-scopes) for the administrator user. Including the following Classic scope:
- `write:jira-work`

If the user has access to both the source and target sites, you can include both source and target scopes in the same API token and use the same token for both import and export.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Commands

```sh
ifj export --source https://example.atlassian.net --user admin@example.com --api-token "$TOKEN"
ifj inspect intercom-for-jira-export.jsonl.gz
```

### Export

`ifj export` authenticates with Jira Cloud basic auth using an Atlassian account
email and API token. It verifies that the user associated with the API token has Jira global admin permission before exporting.

By default, export discovers spaces that are currently connected to Intercom. If
none are found, export fails with a "nothing to export" message; pass
`--space` to select spaces explicitly. Explicit spaces are validated for
existence and may be exported even when no Intercom configuration exists.

Flags:

- `--source URL`: source Jira Cloud URL
- `--user EMAIL`: Atlassian account email
- `--api-token TOKEN`: Atlassian API token
- `--out PATH`: optional output file path. Must end with `.jsonl.gz`.
Defaults to `intercom-for-jira-export.jsonl.gz` in the current working directory.
If the file exists the app will pick a unique name, e.g., `intercom-for-jira-export1.jsonl.gz`.
- `--space KEY`: optional space key. Repeat to select multiple spaces
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- `--json`: print the final summary as JSON

Environment variables:

- `EXPORT_SOURCE`
- `EXPORT_USER`
- `EXPORT_API_TOKEN`
- `EXPORT_OUT`
- `EXPORT_SPACES`: comma-separated space keys.

Exports are written as compressed JSON Lines files with the `.jsonl.gz`
extension.

### Inspect

`ifj inspect <artifact>` validates a `.jsonl.gz` artifact and prints aggregate
counts:

```sh
ifj inspect migration.jsonl.gz
ifj inspect migration.jsonl.gz --json
```

## Configuration

Configuration precedence is flags, then process environment, then `.env` from
the current working directory, then defaults.
32 changes: 32 additions & 0 deletions docs/artifact.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Artifact Contract

The CLI-owned artifact is compressed UTF-8 JSON Lines. Every line is compact
single-line JSON, and blank lines are invalid.

The first record is required:

```json
{
"type": "manifest",
"createdAt": "2026-06-05T00:00:00.000Z",
"source": "https://example.atlassian.net"
}
```

Data records:

```json
{"type":"spaceConfiguration","spaceKey":"ENG","configuration":{"enabled":true}}
{"type":"workItemConversationLinks","spaceKey":"ENG","workItemKey":"ENG-1","conversationIds":["abc","def"]}
```

Invariants:

- Record types are `manifest`, `spaceConfiguration`, and `workItemConversationLinks`.
- The manifest contains only `type`, `createdAt`, and `source`.
- Records never include Jira numeric space IDs or work-item IDs.
- `configuration` is opaque JSON.
- `conversationIds` are opaque strings, deduplicated in first-seen order during export.
- The writer validates every record before writing.
- The reader validates every record while reading.
- Future import code should consume this shared reader and apply records through idempotent upserts.
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const tsconfigRootDir = path.dirname(fileURLToPath(import.meta.url));
export default tseslint.config(
{
ignores: ["coverage/**", "dist/**", "node_modules/**"],
},
{
linterOptions: {
reportUnusedDisableDirectives: "error",
},
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"type": "module",
"packageManager": "pnpm@10.33.0",
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json",
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "eslint . --max-warnings=0",
"format": "prettier --write .",
"format:check": "prettier --check .",
Expand All @@ -16,10 +16,15 @@
"effect:lsp:patch": "effect-language-service patch"
},
"dependencies": {
"@effect/platform-node": "4.0.0-beta.78",
"effect": "4.0.0-beta.78"
},
"bin": {
"ifj": "./dist/src/ifj.js"
},
"devDependencies": {
"@effect/language-service": "^0.86.2",
"@effect/vitest": "4.0.0-beta.78",
"@eslint/js": "^10.0.1",
"@types/node": "^25.9.1",
"eslint": "^10.4.1",
Expand Down
Loading
Loading