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: a forge_installation_xid with no client_key.
  • client_key, shared_secret and base_url become required only for Connect installs, gated by #forge?.
  • forge_installation_xid is validated for length and uniqueness; cloud_id, jira_api_base_url and forge_system_token are required on a native install.
  • #client becomes the single outbound client selector: the Forge system-token client when forge_direct?, otherwise the Connect client. JiraConnect::SyncService loses 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_key and no forge_installation_xid, so #forge? is false for all of them and the three Connect presence validations still apply exactly as before. No persisted row becomes invalid.
  • client_key, encrypted_shared_secret and base_url are already nullable in Postgres, so storing a native install needs no further migration.
  • idx_jira_connect_installations_on_org_id_client_key_uniq is a plain unique index, and Postgres treats NULLs as distinct, so several native installs can coexist in one organization.
  • validates :forge_installation_xid, uniqueness: true uses allow_nil: true, so ActiveRecord skips the lookup for rows without the value. The Connect lifecycle path gains no query.
  • The SyncService change 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.

Edited by Jorge Tomás

Merge request reports

Loading
Loading