# Migrate Jira Assets fields to Forge fields

`migrate_forge_custom_fields.py` scans TCID/ST issues that have values in the
legacy TiDB Cloud org or cluster Assets fields, resolves those Assets objects
to TiDB Cloud IDs, retrieves each object through jsm-syncer's internal
ID-based query APIs, and writes the payloads into the new Forge custom fields.

The defaults for the old customer-project fields are:

- org: `customfield_10086`
- cluster: `customfield_10088`

The new field IDs are deliberately not committed because the Forge fields do
not exist yet. Configure them after Jira creates the fields:

```bash
export JIRA_URL=https://tidb.atlassian.net
export JIRA_USERNAME=operator@example.com
export JIRA_API_TOKEN='...'
export JSM_SYNCER_URL=http://jsm-syncer.namespace.svc.cluster.local:8080
export JIRA_FORGE_ORG_FIELD=customfield_12345
export JIRA_FORGE_CLUSTER_FIELD=customfield_12346
```

`JSM_SYNCER_URL` should normally be the internal service URL. The script uses
`GET /api/v1/jsm-syncer/orgs/{org_id}` and
`GET /api/v1/jsm-syncer/clusters/{cluster_id}`, so it does not depend on a Jira
reporter identity or Forge membership checks. Do not expose an unauthenticated
jsm-syncer service publicly.

In particular, the org value preserves the API object fields such as
`tidbcloud_org_id`, `name`, `status`, `support_plan`, `plan`, `jsm_org_id`,
`biz_region`, `time_zone`, and `msp_org` when present. The cluster value
preserves `id`, `name`, `status`, `service_plan`, and `tidbcloud_org_id`; it is
not a serialized JSON string.

Start with one issue in dry-run mode:

```bash
python3 services/jsm-syncer/scripts/migrate_forge_custom_fields.py \
  --issue-key TCID-12345
```

Then scan all matching TCID/ST issues without writing:

```bash
python3 services/jsm-syncer/scripts/migrate_forge_custom_fields.py
```

Apply only after reviewing the JSON-lines output:

```bash
python3 services/jsm-syncer/scripts/migrate_forge_custom_fields.py --apply
```

The default JQL pairs each old and new field: the old field must be non-empty
and its corresponding new field must be empty. The runner also checks the
target values before writing, so repeated runs are idempotent. `--overwrite`
explicitly removes the target-empty JQL condition and replaces existing Forge
values.

The org and cluster migrations run independently. If one Assets object or
jsm-syncer object cannot be found, the script still writes the other field
when it resolved successfully. Partial results use `updated_with_errors` in
apply mode or `would_update_with_errors` in dry-run mode, with failures labeled
under `errors.org` or `errors.cluster`. If neither side resolves, the issue is
not updated. Other issues continue in all cases, and the process returns a
non-zero exit code when any field failed. The final summary also groups these
failures in `error_counts`.

Jira issue search returns only an Assets reference (`workspaceId` and
`objectId`), not the TiDB Cloud business ID. The script always dereferences
that reference through the Assets object API, then reads the `ID` attribute
from the returned object. Both org and cluster objects must store
their TiDB Cloud business ID in the attribute named exactly `ID`; no other
attribute names or all-attribute fallback are used. The non-empty `value` is
used as-is after trimming, so short IDs such as `1` are valid.

The Assets API needs the Jira cloud ID to dereference these values. The script
auto-discovers it through Jira, or it can be set explicitly with
`JIRA_CLOUD_ID` / `--jira-cloud-id`.
