628ab153a8
## What
Makes the app-installation version gate **workspace-scoped**. App
installation now validates a manifest's `engines.twenty` requirement
against the version the **target workspace has actually finished
upgrading to**, instead of the instance/server's inferred version.
## Why
The server binary and a given workspace's migration state can diverge.
In a multi-workspace deployment the instance can already report version
`X` while an individual workspace still hasn't completed its
workspace-scoped upgrade commands for `X` (it's mid-upgrade or a
migration failed). Gating on the instance version let an app that
requires `X` install into a workspace whose schema/metadata is
effectively still at `X-1`, which can break the app. The requirement
should be checked against what the *workspace* has completed, not what
the server reports.
## How
- **`UpgradeStatusService.getWorkspaceCompletedVersion(workspaceId)`**
(new): resolves the last fully-completed upgrade version for a workspace
by reading its upgrade cursor and walking the upgrade sequence:
- Returns the cursor's version when the cursor sits on the **last step
of its version segment** and its status is `completed`.
- Otherwise walks backwards to the previous fully-completed version
segment.
- Returns `null` when the cursor is missing, not found in the sequence,
or otherwise uninterpretable.
- **`ApplicationVersionValidationService`**:
- Adds `validateWorkspaceCompatibility({ requiredServerVersion,
workspaceId })`.
- Extracts the shared semver logic into a private
`validateVersionAgainstRange({ version, requiredVersionRange, scope })`
and makes error messages scope-aware (workspace vs. instance).
`validateServerCompatibility` is preserved and now delegates to it.
- New failure reason `INVALID_WORKSPACE_VERSION`.
- **`ApplicationInstallService`** now calls
`validateWorkspaceCompatibility` with the `workspaceId` instead of
`validateServerCompatibility`.
- **Exception plumbing**: new
`ApplicationExceptionCode.INVALID_WORKSPACE_VERSION`, surfaced as a
`UserInputError` (`BAD_USER_INPUT`) with a user-friendly message ("This
workspace's upgrade state could not be determined…"). The
tarball/registration path maps it onto the existing
`INVALID_SERVER_VERSION` registration code.
## Notes
- **Publishing (app registration) is intentionally not
workspace-gated.** The tarball/registration path
(`ApplicationTarballService`) still uses the instance-level
`validateServerCompatibility` check, not the new workspace-scoped one.
Publishing an app is not tied to any particular workspace's upgrade
state, so there is no workspace version to check at that point — the
workspace-completed-version gate only applies when installing an app
into a specific workspace.
## Testing
- Unit tests for `ApplicationVersionValidationService`
(`validateServerCompatibility` + new `validateWorkspaceCompatibility`)
covering: no requirement, invalid semver range, satisfied/unsatisfied
ranges, and the uninterpretable-cursor case.
- Unit tests for `UpgradeStatusService.getWorkspaceCompletedVersion`
against a three-segment mock upgrade sequence (multi-command version,
instance-only version, workspace-terminated version).
- New integration suite
`failing-app-installation-workspace-version.integration-spec.ts` (+
snapshots) exercising the real install flow: rejects installation when
the workspace hasn't completed the required version, and when the
workspace's upgrade cursor can't be interpreted. Adds a
`create-app-tarball.util.ts` test helper.
57 lines
2.2 KiB
TypeScript
57 lines
2.2 KiB
TypeScript
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
|
import { buildBaseManifest } from 'test/integration/metadata/suites/application/utils/build-base-manifest.util';
|
|
import { cleanupApplicationAndAppRegistration } from 'test/integration/metadata/suites/application/utils/cleanup-application-and-app-registration.util';
|
|
import { createAppTarball } from 'test/integration/metadata/suites/application/utils/create-app-tarball.util';
|
|
import { uploadAppTarball } from 'test/integration/metadata/suites/application/utils/upload-app-tarball.util';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
|
// The upload flow runs cache-lock retries with real delays, so fake timers
|
|
// would hang it — mirror the other application suites.
|
|
jest.setTimeout(120000);
|
|
|
|
describe('Publish application is gated by the instance server version', () => {
|
|
const createdApplicationUniversalIdentifiers: string[] = [];
|
|
|
|
beforeAll(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
for (const universalIdentifier of createdApplicationUniversalIdentifiers) {
|
|
await cleanupApplicationAndAppRegistration({
|
|
applicationUniversalIdentifier: universalIdentifier,
|
|
});
|
|
}
|
|
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
it('rejects publishing when the app requires a server version the instance does not satisfy', async () => {
|
|
const universalIdentifier = uuidv4();
|
|
const roleId = uuidv4();
|
|
|
|
const tarball = await createAppTarball({
|
|
'manifest.json': JSON.stringify(
|
|
buildBaseManifest({ appId: universalIdentifier, roleId }),
|
|
),
|
|
'package.json': JSON.stringify({
|
|
name: `test-server-version-gate-${universalIdentifier}`,
|
|
version: '1.0.0',
|
|
// Require a server version far above any real instance version so the
|
|
// instance-level compatibility check always rejects it.
|
|
engines: { twenty: '>=999.0.0' },
|
|
}),
|
|
});
|
|
|
|
createdApplicationUniversalIdentifiers.push(universalIdentifier);
|
|
|
|
const { errors } = await uploadAppTarball({
|
|
tarballBuffer: tarball,
|
|
universalIdentifier,
|
|
expectToFail: true,
|
|
});
|
|
|
|
expectOneNotInternalServerErrorSnapshot({ errors });
|
|
});
|
|
});
|