# Bringing issues in from a file

2026-08-11

Phase 7b. Somebody has a tracker already, or a spreadsheet, and wants it here.

## 🚨 An import cannot be synchronous, and that breaks the bulk-change rule

The bulk-change note argued for doing the work in the request, because that is
the only way the person gets told what actually happened. Five thousand rows
cannot be done in a request, so that argument does not survive contact with
this.

The resolution is not to give up the report — it is to **make the report a
thing rather than a flash message.** Every import is a row: what file, who ran
it, how many rows, how many became issues, how many were refused and why. It
outlives the session, which a flash message does not, and it is the same record
whether the import ran from a queue or from the command line.

That also means the report can be *read again next week*, which is when
somebody actually asks what happened.

## Two phases, and the first one changes nothing

**Analyse** reads the file, applies the mapping, and reports what *would*
happen — how many rows would create an issue, how many would be refused, and
the reason for each refusal, grouped.

**Commit** does it.

🚨 **Analyse is the default.** Bulk change could show the person every row they
were about to touch; five thousand rows cannot be shown, so the shape is shown
instead. An importer whose first action is to write is one where the first
anybody learns of a bad mapping is afterwards.

## 🚨 A row that does not validate is refused, never guessed

A CSV saying `Opened` when the tracker has `Open` is a row somebody has to look
at. Guessing — matching loosely, creating the missing status, falling back to a
default — turns an import into a silent edit of what the data meant.

So: unknown status, unknown priority, unknown type, unknown assignee are all
refusals, and each says which row and which value. The fix is to correct the
file or add the status, and both are things a person should do knowingly.

Only the title is required. Everything else missing is a default, because a
blank cell is an absence and absence is a fact the tracker can represent.

## 🚨 An import can be undone, and the keys still are not reused

Every issue an import creates is recorded against that import, so "undo this
import" is a question the database can answer — which is exactly what a bulk
delete lacked and why bulk delete was not built.

But undo does **not** give back the issue numbers. `CONV-41` through `CONV-98`
stay used, and the next issue is `CONV-99`. A gap in the numbering is the price
of the guarantee that a key means one thing forever, and it is a price worth
paying: the alternative is that a key somebody pasted into a commit message
last week now points at something else.

## Re-running an import

A row may carry an **external reference** — its id in wherever it came from.
Where one is given and an issue in this project already has it, the row is
**skipped and reported**, not updated.

🚨 Skipping rather than updating, and that is a decision rather than laziness.
"Update the issue to match the file" is a *sync*, and a sync overwrites work
done here since the last import — someone's triage, someone's assignment — with
whatever the other system still believes. That is a different feature with a
different risk, and it should be asked for rather than arrived at.

## Where it runs

Two entry points, one service:

- `php tools/convoro tracker:import` — for a large file, and for anybody who
  has shell access. No timeout, no upload limit, and the report is printed.
- The queue, from an upload. The request stores the file and pushes a job; the
  report is read from the imports list.

🚨 **Never in the request itself.** That is the standing rule, and here it is
not even close: a five-thousand-row import is minutes of work.

## Not matched, deliberately

- **Importing from Jira's own export format.** A CSV is a CSV; a mapping screen
  handles their column names as well as anyone's. A format-specific importer is
  a promise to track somebody else's format.
- **Importing comments and attachments.** They belong to a row that is not a
  row — one issue, many comments — and a flat file cannot say so without a
  convention this would then own. Worth doing when it is asked for, with its
  own note.
- **Creating statuses and types as it goes.** See above: that is guessing with
  extra steps.
