feat: enable app deployments (#7769)

This commit is contained in:
Laurin 2026-03-04 15:18:45 +01:00 committed by GitHub
parent c4a6a6f8de
commit ee2785c4cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 117 additions and 60 deletions

View file

@ -0,0 +1,19 @@
---
'@graphql-hive/apollo': minor
'@graphql-hive/core': minor
'@graphql-hive/yoga': minor
---
The persisted documents feature is now stable. Please use the `persistedDocuments` configuration option instead of `experimental__persistedDocuments`.
```diff
useHive({
- experimental__persistedDocuments: {
+ persistedDocuments: {
cdn: {
endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/<target_id>',
accessToken: '<cdn_access_token>'
},
},
})
```

View file

@ -0,0 +1,5 @@
---
'hive': minor
---
App deployments are now stable and enabled for all organizations by default.

View file

@ -190,11 +190,11 @@ export function createHive(clientOrOptions: HivePluginOptions, ctx?: GraphQLServ
version,
...clientOrOptions.agent,
},
experimental__persistedDocuments: clientOrOptions.experimental__persistedDocuments
persistedDocuments: clientOrOptions.persistedDocuments
? {
...clientOrOptions.experimental__persistedDocuments,
...clientOrOptions.persistedDocuments,
layer2Cache: (() => {
const userL2Config = clientOrOptions.experimental__persistedDocuments?.layer2Cache;
const userL2Config = clientOrOptions.persistedDocuments?.layer2Cache;
if (persistedDocumentsCache) {
return {
cache: persistedDocumentsCache,
@ -306,7 +306,7 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Apollo
let persistedDocumentError: GraphQLError | null = null;
let persistedDocumentHash: string | undefined;
if (hive.experimental__persistedDocuments) {
if (hive.persistedDocuments) {
if (
context.request.http?.body &&
typeof context.request.http.body === 'object' &&
@ -319,7 +319,7 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Apollo
const contextValue = isLegacyV3
? (context as any).context
: (context as any).contextValue;
const document = await hive.experimental__persistedDocuments.resolve(
const document = await hive.persistedDocuments.resolve(
context.request.http.body.documentId,
{ waitUntil: contextValue?.waitUntil },
);
@ -360,7 +360,7 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Apollo
}
} else if (
false ===
(await hive.experimental__persistedDocuments.allowArbitraryDocuments({
(await hive.persistedDocuments.allowArbitraryDocuments({
headers: {
get(name: string) {
return context.request.http?.headers?.get(name) ?? null;

View file

@ -39,7 +39,7 @@ test('use persisted documents (GraphQL over HTTP "documentId")', async () => {
plugins: [
useHive({
token: 'token',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -96,7 +96,7 @@ test('persisted document not found (GraphQL over HTTP "documentId")', async () =
plugins: [
useHive({
token: 'token',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -152,7 +152,7 @@ test('arbitrary options are rejected with allowArbitraryDocuments=false (GraphQL
plugins: [
useHive({
token: 'token',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -205,7 +205,7 @@ test('arbitrary options are allowed with allowArbitraryDocuments=true (GraphQL o
plugins: [
useHive({
token: 'token',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -301,7 +301,7 @@ test('usage reporting for persisted document', async () => {
enabled: true,
debug: false,
token: 'brrrt',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -376,7 +376,7 @@ test('handles validation errors from malformed document IDs', async () => {
plugins: [
useHive({
token: 'token',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',

View file

@ -31,7 +31,7 @@ export function createHive(options: HivePluginOptions): HiveClient {
const logger = resolveLoggerFromConfigOptions(options).child({ module: 'hive' });
let enabled = options.enabled ?? true;
if (enabled === false && !options.experimental__persistedDocuments) {
if (enabled === false && !options.persistedDocuments) {
logger.debug('Plugin is not enabled.');
}
@ -48,11 +48,11 @@ export function createHive(options: HivePluginOptions): HiveClient {
const usage = createUsage(mergedOptions);
const schemaReporter = createReporting(mergedOptions);
const persistedDocuments = options.experimental__persistedDocuments
const persistedDocuments = options.persistedDocuments
? createPersistedDocuments({
...options.experimental__persistedDocuments,
...options.persistedDocuments,
logger,
fetch: options.experimental__persistedDocuments.fetch,
fetch: options.persistedDocuments.fetch,
})
: null;
@ -243,7 +243,7 @@ export function createHive(options: HivePluginOptions): HiveClient {
collectSubscriptionUsage: usage.collectSubscription,
createInstrumentedSubscribe,
createInstrumentedExecute,
experimental__persistedDocuments: persistedDocuments,
persistedDocuments,
};
}

View file

@ -41,7 +41,7 @@ export interface HiveClient {
createInstrumentedExecute(executeImpl: any): any;
createInstrumentedSubscribe(executeImpl: any): any;
dispose(): Promise<void>;
experimental__persistedDocuments: null | {
persistedDocuments: null | {
resolve(
documentId: string,
context?: { waitUntil?: (promise: Promise<void> | void) => void },
@ -265,10 +265,10 @@ export type HivePluginOptions = OptionalWhenFalse<
*/
autoDispose?: boolean | NodeJS.Signals[];
/**
* Experimental persisted documents configuration.
* Persisted documents configuration.
*
**/
experimental__persistedDocuments?: PersistedDocumentsConfiguration;
persistedDocuments?: PersistedDocumentsConfiguration;
},
'enabled',
'token'
@ -454,7 +454,7 @@ export type PersistedDocumentsConfiguration = {
* await redis.connect();
*
* const hive = createHive({
* experimental__persistedDocuments: {
* persistedDocuments: {
* cdn: { endpoint: '...', accessToken: '...' },
* layer2Cache: {
* cache: {

View file

@ -221,8 +221,8 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin
: undefined,
});
void hive.info();
const experimentalPersistedDocs = hive.experimental__persistedDocuments;
if (experimentalPersistedDocs) {
const persistedDocuments = hive.persistedDocuments;
if (persistedDocuments) {
addPlugin(
usePersistedOperations({
extractPersistedOperationId(body, request) {
@ -241,7 +241,7 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin
async getPersistedOperation(key, _request, context) {
let document: string | null;
try {
document = await experimentalPersistedDocs.resolve(key, {
document = await persistedDocuments.resolve(key, {
waitUntil: context.waitUntil,
});
} catch (error) {
@ -273,7 +273,7 @@ export function useHive(clientOrOptions: HiveClient | HivePluginOptions): Plugin
return document;
},
allowArbitraryOperations(request) {
return experimentalPersistedDocs.allowArbitraryDocuments(request);
return persistedDocuments.allowArbitraryDocuments(request);
},
customErrors: {
keyNotFound() {

View file

@ -20,7 +20,7 @@ test('use persisted documents (GraphQL over HTTP "documentId")', async () => {
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -70,7 +70,7 @@ test('use persisted documents (GraphQL over HTTP "documentId") real thing', asyn
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -124,7 +124,7 @@ test('persisted document not found (GraphQL over HTTP "documentId")', async () =
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -177,7 +177,7 @@ test('malformed document ID returns validation error instead of 500 (GraphQL ove
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -223,7 +223,7 @@ test('document ID with wrong number of parts returns validation error', async ()
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -269,7 +269,7 @@ test('arbitrary options are rejected with allowArbitraryDocuments=false (GraphQL
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -313,7 +313,7 @@ test('arbitrary options are allowed with allowArbitraryDocuments=true (GraphQL o
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -369,7 +369,7 @@ test('use persisted documents for SSE GET (GraphQL over HTTP "documentId")', asy
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -441,7 +441,7 @@ test('use persisted documents for subscription over SSE (GraphQL over HTTP "docu
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -502,7 +502,7 @@ test('usage reporting for persisted document', async () => {
enabled: true,
debug: false,
token: 'brrrt',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -596,7 +596,7 @@ test('usage reporting for persisted document (subscription)', async () => {
enabled: true,
debug: false,
token: 'brrrt',
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -696,7 +696,7 @@ test('deduplication of parallel requests resolving the same document from CDN',
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -750,7 +750,7 @@ test('usage reporting with batch execution and persisted documents', async () =>
useHive({
token: 'foo',
enabled: true,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -853,7 +853,7 @@ test('L2 cache with waitUntil from yoga context', () => {
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',
@ -922,7 +922,7 @@ test('L2 cache hit skips CDN fetch', async () => {
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'http://artifacts-cdn.localhost',
accessToken: 'foo',

View file

@ -64,7 +64,7 @@ The GraphQL API for GraphQL Hive.
| `INTEGRATION_GITHUB` | No | Whether the GitHub integration is enabled | `1` (enabled) or `0` (disabled) |
| `INTEGRATION_GITHUB_GITHUB_APP_ID` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app id. | `123` |
| `INTEGRATION_GITHUB_GITHUB_APP_PRIVATE_KEY` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app private key. | `letmein1` |
| `FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED` | No | Whether app deployments should be enabled for every organization. | `1` (enabled) or `0` (disabled) |
| `FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED` | No | Whether app deployments should be enabled for every organization. | `1` (enabled **default**) or `0` (disabled) |
| `FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED` | No | Whether schema proposals should be enabled for every organization. | `1` (enabled) or `0` (disabled) |
| `S3_AUDIT_LOG` | No (audit log uses default S3 if not configured) | Whether audit logs should be stored on another S3 bucket than the artifacts. | `1` (enabled) or `0` (disabled) |
| `S3_AUDIT_LOG_ENDPOINT` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 endpoint. | `http://localhost:9000` |

View file

@ -38,7 +38,10 @@ const EnvironmentModel = zod.object({
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
),
FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED: emptyString(
zod.union([zod.literal('1'), zod.literal('0')]).optional(),
zod
.union([zod.literal('1'), zod.literal('0')])
.optional()
.default('1'),
),
FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED: emptyString(
zod.union([zod.literal('1'), zod.literal('0')]).optional(),

View file

@ -222,7 +222,7 @@ export const graphqlHandler = (options: GraphQLHandlerOptions): RouteHandlerMeth
exclude: ['readiness'],
}
: false,
experimental__persistedDocuments: options.hivePersistedDocumentsConfig
persistedDocuments: options.hivePersistedDocumentsConfig
? {
cdn: {
endpoint: options.hivePersistedDocumentsConfig.cdnEndpoint,

View file

@ -38,7 +38,7 @@ await redis.connect()
const yoga = createYoga({
plugins: [
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/<target_id>',
accessToken: '<cdn_access_token>'

View file

@ -279,6 +279,23 @@ export function PlansTable({ className }: { className?: string }) {
</PlansTableCell>
</tr>
<tr>
<PlansTableCell>
<TextLink href="/docs/schema-registry/app-deployments" target="_blank">
Manage Persisted Documents
</TextLink>
</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Hobby">
{YES}
</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Pro">
{YES}
</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Enterprise">
{YES}
</PlansTableCell>
</tr>
<TableSubheaderRow
icon={<UsageIcon />}
title="Analytics, Monitoring & Metrics"
@ -350,6 +367,26 @@ export function PlansTable({ className }: { className?: string }) {
</PlansTableCell>
</tr>
<tr>
<PlansTableCell>
<TextLink
href="/docs/schema-registry/app-deployments#schema-checks-and-affected-app-deployments"
target="_blank"
>
Persisted Document breaking change detection
</TextLink>
</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Hobby">
{YES}
</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Pro">
{YES}
</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Enterprise">
{YES}
</PlansTableCell>
</tr>
<tr>
<PlansTableCell>Schema check retention</PlansTableCell>
<PlansTableCell activePlan={activePlan} plan="Hobby">

View file

@ -400,11 +400,11 @@ Hive client supports resolving persisted documents. For getting started please r
#### Basic Configuration
For enabling you need to provide the `experimental__persistedDocuments` as follows.
For enabling you need to provide the `persistedDocuments` as follows.
```ts filename="Hive Persisted Documents Basic Configuration"
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
// replace <target_id> and <cdn_access_token> with your values
endpoint: [
@ -425,7 +425,7 @@ Schema Registry. If you want to allow arbitrary documents to be executed, you ca
```typescript filename="Hive Persisted Documents Allow Arbitrary Documents" {8}
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
// replace <target_id> and <cdn_access_token> with your values
endpoint: [
@ -443,7 +443,7 @@ Furthermore, you can also allow arbitrary documents based on the incoming HTTP r
```typescript filename="Hive Persisted Documents Allow Arbitrary Documents Based on Request" {8-9}
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
// replace <target_id> and <cdn_access_token> with your values
endpoint: [
@ -466,7 +466,7 @@ the `cache` option.
```typescript filename="Hive Persisted Documents Cache Size" {8-9}
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
// replace <target_id> and <cdn_access_token> with your values
endpoint: [

View file

@ -7,13 +7,6 @@ import { Callout, ContactTextLink, Tabs } from '@theguild/components'
# App Deployments
<Callout>
<div>
App deployments are currently in preview. If you would like to try them out, please{' '}
<ContactTextLink>reach out to us</ContactTextLink>
</div>
</Callout>
App deployments are a way to group and publish your GraphQL operations as a single app version to
the Hive Registry. This allows you to keep track of your different app versions, their operations
usage, and performance.
@ -444,7 +437,7 @@ const yoga = createYoga({
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
// replace <target_id> and <cdn_access_token> with your values
endpoint: [
@ -482,7 +475,7 @@ const yoga = createYoga({
plugins: [
useHive({
enabled: false,
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/<target_id>',
accessToken: '<cdn_access_token>'
@ -533,7 +526,7 @@ const server = new ApolloServer({
schema,
plugins: [
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
// replace <target_id> and <cdn_access_token> with your values
endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/<target_id>',
@ -567,7 +560,7 @@ const server = new ApolloServer({
schema,
plugins: [
useHive({
experimental__persistedDocuments: {
persistedDocuments: {
cdn: {
endpoint: 'https://cdn.graphql-hive.com/artifacts/v1/<target_id>',
accessToken: '<cdn_access_token>'