Allow native Forge installation without Connect credentials
What
Adds forge_installation_xid to jira_connect_installations and the model support that uses it, so a native Forge installation can exist without Connect credentials.
Column (20260817100000): text limited to 255 characters, with a global partial unique index on non-NULL values. The value is the Atlassian Forge installation ARI (ari:cloud:ecosystem::installation/<uuid>), which every Forge Invocation Token carries in app.installationId.
Model:
#forge?identifies a native install: aforge_installation_xidwith noclient_key.client_key,shared_secretandbase_urlbecome required only for Connect installs, gated by#forge?.forge_installation_xidis validated for length and uniqueness;cloud_id,jira_api_base_urlandforge_system_tokenare required on a native install.#clientbecomes the single outbound client selector: the Forge system-token client whenforge_direct?, otherwise the Connect client.JiraConnect::SyncServiceloses its duplicate copy of that branch.
Two predicates answer two different questions:
| Predicate | True when | Used for |
|---|---|---|
#forge? |
forge_installation_xid is present and client_key is blank |
Gates the Connect presence validations (client_key, shared_secret, base_url) and requires cloud_id |
#forge_direct? |
jira_api_base_url and forge_system_token are both present |
Selects the outbound client in #client: the Forge system-token client, otherwise the Connect client |
A Connect-on-Forge install holds both a client_key and the Forge credentials. It is therefore forge_direct? but not forge?, so it keeps the Connect validations and still calls Jira with the system token.
The _xid suffix follows the foreign key naming convention for an identifier that belongs to a third party platform. The column needs no entry in ignored_fk_columns_map, because that list covers columns that end with _id only.
Why
A native Forge install authenticates with a Forge Invocation Token plus the app system token, not with a Connect client_key and shared_secret. The model has to accept that shape before the link flow can create one (!248687, which populates the column).
The Jira site id (cloud_id) cannot identify an installation, so it stays non-unique on purpose:
- A customer can connect several GitLab instances to one Jira site.
- A reinstall can leave a second record for the same site.
- Atlassian changes the Connect tenant key after a Jira data import, so the same site arrives as a new installation.
The Forge installation ARI is unique per installation by construction, because Atlassian issues a new one when the app is installed again. No backfill or dedup is needed, since the column starts empty.
Compatibility
No behaviour changes for existing installations.
- Every existing row has a
client_keyand noforge_installation_xid, so#forge?isfalsefor all of them and the three Connect presence validations still apply exactly as before. No persisted row becomes invalid. client_key,encrypted_shared_secretandbase_urlare already nullable in Postgres, so storing a native install needs no further migration.idx_jira_connect_installations_on_org_id_client_key_uniqis a plain unique index, and Postgres treats NULLs as distinct, so several native installs can coexist in one organization.validates :forge_installation_xid, uniqueness: trueusesallow_nil: true, so ActiveRecord skips the lookup for rows without the value. The Connect lifecycle path gains no query.- The
SyncServicechange is behaviour preserving: master already selected the same two clients inline.
Database
Script used to recreate scenarios on postgres.ai
ALTER TABLE jira_connect_installations ADD COLUMN forge_installation_xid text;
ALTER TABLE jira_connect_installations
ADD CONSTRAINT check_290e4c5650
CHECK ( char_length(forge_installation_xid) <= 255 ) NOT VALID;
CREATE UNIQUE INDEX index_jira_connect_installations_on_forge_installation_xid
ON jira_connect_installations USING btree (forge_installation_xid)
WHERE (forge_installation_xid IS NOT NULL);
UPDATE jira_connect_installations
SET forge_installation_xid = 'ari:cloud:ecosystem::installation/' || gen_random_uuid()
WHERE id IN (SELECT id FROM jira_connect_installations ORDER BY id LIMIT 20000);
-- plant a value you can use as a literal in the EXPLAIN below
UPDATE jira_connect_installations
SET forge_installation_xid = 'ari:cloud:ecosystem::installation/11111111-2222-3333-4444-555555555555'
WHERE id = (SELECT id FROM jira_connect_installations ORDER BY id LIMIT 1);
ANALYZE jira_connect_installations;
SELECT count(*) AS total_rows,
count(forge_installation_xid) AS rows_with_xid,
pg_size_pretty(pg_relation_size('index_jira_connect_installations_on_forge_installation_xid')) AS index_size
FROM jira_connect_installations;
-- (test a) the uniqueness validation Rails emits on save
EXPLAIN (ANALYZE, BUFFERS)
SELECT 1 AS one FROM jira_connect_installations
WHERE forge_installation_xid = 'ari:cloud:ecosystem::installation/11111111-2222-3333-4444-555555555555'
LIMIT 1;
-- (test b) the runtime lookup: resolve the installation from the Forge Invocation Token
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM jira_connect_installations
WHERE forge_installation_xid = 'ari:cloud:ecosystem::installation/11111111-2222-3333-4444-555555555555'
LIMIT 1;
-- (test c) a miss: an ARI no row holds
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM jira_connect_installations
WHERE forge_installation_xid = 'ari:cloud:ecosystem::installation/00000000-0000-0000-0000-000000000000'
LIMIT 1;Query plans for each test scenario:
test a -> https://postgres.ai/console/gitlab/gitlab-production-main/sessions/55206/commands/158730
test b -> https://postgres.ai/console/gitlab/gitlab-production-main/sessions/55206/commands/158731
test c -> https://postgres.ai/console/gitlab/gitlab-production-main/sessions/55206/commands/158732
How to verify
bin/rspec spec/models/jira_connect_installation_spec.rb spec/services/jira_connect/sync_service_spec.rb spec/db/schema_spec.rb -e jira_connect_installations, green in EE and FOSS_ONLY=1.