Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
667a15a
Add schema proposal composition background job
jdolle Feb 19, 2026
88f515f
Add pubsub, subscrition, and composition state for proposals
jdolle Feb 24, 2026
e259a34
revert log change
jdolle Feb 24, 2026
0eb9cdd
note
jdolle Feb 24, 2026
ffeef08
Remove unused function
jdolle Feb 24, 2026
49d1a2c
Remove log
jdolle Feb 24, 2026
406062c
Add limits to patch fetching; check targetId
jdolle Feb 24, 2026
d841a41
Linting errors
jdolle Feb 24, 2026
2e274ba
Merge branch 'main' into composition-job
jdolle Feb 24, 2026
f8cafbb
Add composition reason; fix timestamp format
jdolle Feb 24, 2026
e42665b
Remove @hive/api from workflows' dependencies
jdolle Feb 24, 2026
6b65344
Fix type import
jdolle Feb 24, 2026
b2185a3
Update lockfile
jdolle Feb 24, 2026
2c42fd9
Fix generated type
jdolle Feb 24, 2026
9f008c1
Fix workflows deployment
jdolle Feb 24, 2026
699a373
Fix community & env name
jdolle Feb 24, 2026
0dc136e
Basic composition ui; fix names
jdolle Feb 25, 2026
1aa49aa
add key
jdolle Feb 25, 2026
4d00a88
Unify redis logger utilities
jdolle Feb 25, 2026
887918c
Fix packages
jdolle Feb 25, 2026
e26cf7f
Later version of graphile worker
jdolle Feb 25, 2026
1c974ed
Use nullable instead of optional
jdolle Feb 25, 2026
5d954d7
Remove proposal version select
jdolle Feb 25, 2026
201f156
Pass in external composition state
jdolle Feb 25, 2026
8544455
Fix proposal manager permissions; fix workflow redis env var; add an …
jdolle Feb 26, 2026
6e8a950
Add subscription test and more
jdolle Feb 26, 2026
8e4142c
Lint
jdolle Feb 26, 2026
57b33f2
Merge remote-tracking branch 'origin/main' into composition-job
jdolle Feb 26, 2026
24055c3
service heading style tweak
jdolle Feb 26, 2026
016527f
Rename submodule to avoid conflict with actual package
jdolle Feb 26, 2026
748f7e4
Check integrity
jdolle Feb 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions deployment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,17 +136,6 @@ const tokens = deployTokens({
observability,
});

deployWorkflows({
image: docker.factory.getImageId('workflows', imagesTag),
docker,
environment,
postgres,
postmarkSecret,
observability,
sentry,
heartbeat: heartbeatsConfig.get('webhooks'),
});

const commerce = deployCommerce({
image: docker.factory.getImageId('commerce', imagesTag),
docker,
Expand Down Expand Up @@ -201,6 +190,18 @@ const schemaPolicy = deploySchemaPolicy({
observability,
});

deployWorkflows({
image: docker.factory.getImageId('workflows', imagesTag),
docker,
environment,
postgres,
postmarkSecret,
observability,
sentry,
heartbeat: heartbeatsConfig.get('webhooks'),
schema,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved after the schema instance because i need to reference it.

});

const supertokens = deploySuperTokens(postgres, { dependencies: [dbMigrations] }, environment);
const zendesk = configureZendesk({ environment });
const githubApp = configureGithubApp();
Expand Down
5 changes: 5 additions & 0 deletions deployment/services/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as pulumi from '@pulumi/pulumi';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { ServiceDeployment } from '../utils/service-deployment';
import { Docker } from './docker';
import { Environment } from './environment';
import { Observability } from './observability';
import { Postgres } from './postgres';
import { Schema } from './schema';
import { Sentry } from './sentry';

export class PostmarkSecret extends ServiceSecret<{
Expand All @@ -22,6 +24,7 @@ export function deployWorkflows({
postgres,
observability,
postmarkSecret,
schema,
}: {
postgres: Postgres;
observability: Observability;
Expand All @@ -31,6 +34,7 @@ export function deployWorkflows({
heartbeat?: string;
sentry: Sentry;
postmarkSecret: PostmarkSecret;
schema: Schema;
}) {
return (
new ServiceDeployment(
Expand All @@ -47,6 +51,7 @@ export function deployWorkflows({
? observability.tracingEndpoint
: '',
LOG_JSON: '1',
SCHEMA_ENDPOINT: serviceLocalEndpoint(schema.service),
},
readinessProbe: '/_readiness',
livenessProbe: '/_health',
Expand Down
4 changes: 4 additions & 0 deletions docker/docker-compose.community.yml
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,10 @@ services:
SENTRY_DSN: '${SENTRY_DSN:-}'
PROMETHEUS_METRICS: '${PROMETHEUS_METRICS:-}'
LOG_JSON: '1'
SCHEMA_ENDPOINT: http://schema:3002
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: '${REDIS_PASSWORD}'

usage:
image: '${DOCKER_REGISTRY}usage${DOCKER_TAG}'
Expand Down
1 change: 1 addition & 0 deletions integration-tests/docker-compose.integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ services:
workflows:
environment:
EMAIL_PROVIDER: '${EMAIL_PROVIDER}'
SCHEMA_ENDPOINT: http://schema:3002
LOG_LEVEL: debug
ports:
- '3014:3014'
Expand Down
1 change: 1 addition & 0 deletions integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dockerode": "4.0.8",
"dotenv": "16.4.7",
"graphql": "16.9.0",
"graphql-sse": "2.6.0",
"human-id": "4.1.1",
"ioredis": "5.8.2",
"slonik": "30.4.4",
Expand Down
40 changes: 40 additions & 0 deletions integration-tests/testkit/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ExecutionResult, parse, print } from 'graphql';
import { createClient } from 'graphql-sse';
import { TypedDocumentNode } from '@graphql-typed-document-node/core';
import { sortSDL } from '@theguild/federation-composition';
import { getServiceHost } from './utils';
Expand Down Expand Up @@ -87,3 +88,42 @@ export async function execute<TResult, TVariables>(
},
};
}

export async function subscribe<TResult, TVariables>(
params: {
document: TypedDocumentNode<TResult, TVariables>;
operationName?: string;
authToken?: string;
token?: string;
legacyAuthorizationMode?: boolean;
} & (TVariables extends Record<string, never>
? { variables?: never }
: { variables: TVariables }),
) {
const registryAddress = await getServiceHost('server', 8082);
const client = createClient({
url: `http://${registryAddress}/graphql`,
headers: {
...(params.authToken
? {
authorization: `Bearer ${params.authToken}`,
}
: {}),
...(params.token
? params.legacyAuthorizationMode
? {
'x-api-token': params.token,
}
: {
authorization: `Bearer ${params.token}`,
}
: {}),
},
});

return client.iterate({
operationName: params.operationName,
query: print(params.document),
variables: params.variables ?? {},
});
}
2 changes: 2 additions & 0 deletions integration-tests/testkit/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,13 +766,15 @@ export function initSeed() {
commit: string;
},
contextId?: string,
schemaProposalId?: string,
) {
return await checkSchema(
{
sdl,
service,
meta,
contextId,
schemaProposalId,
},
secret,
);
Expand Down
83 changes: 83 additions & 0 deletions integration-tests/tests/api/proposals/create.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { graphql } from 'testkit/gql';
import { ProjectType, ResourceAssignmentModeType } from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
import { initSeed } from 'testkit/seed';

const CreateProposalMutation = graphql(`
mutation CreateProposalMutation($input: CreateSchemaProposalInput!) {
createSchemaProposal(input: $input) {
ok {
schemaProposal {
id
}
}
error {
message
}
}
}
`);

describe('Schema Proposals', () => {
test.concurrent(
'cannot be proposed without "schemaProposal:modify" permission',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg();
await setFeatureFlag('schemaProposals', true);
const { target } = await createProject(ProjectType.Federation);
const { privateAccessKey: accessKey } = await createOrganizationAccessToken(
{
resources: {
mode: ResourceAssignmentModeType.All,
},
permissions: ['schemaProposal:describe'],
},
ownerToken,
);

const result = await execute({
document: CreateProposalMutation,
variables: {
input: {
target: { byId: target.id },
author: 'Jeff',
title: 'Proposed changes to the schema...',
},
},
authToken: accessKey,
}).then(r => r.expectGraphQLErrors());
},
);

test.concurrent(
'can be proposed successfully with "schemaProposal:modify" permission',
async ({ expect }) => {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg();
await setFeatureFlag('schemaProposals', true);
const { target } = await createProject(ProjectType.Federation);

const { privateAccessKey: accessKey } = await createOrganizationAccessToken({
resources: {
mode: ResourceAssignmentModeType.All,
},
permissions: ['schemaProposal:modify'],
});

const result = await execute({
document: CreateProposalMutation,
variables: {
input: {
target: { byId: target.id },
author: 'Jeff',
title: 'Proposed changes to the schema...',
},
},
authToken: accessKey,
}).then(r => r.expectNoGraphQLErrors());

expect(result.createSchemaProposal.ok?.schemaProposal).toHaveProperty('id');
},
);
});
115 changes: 115 additions & 0 deletions integration-tests/tests/api/proposals/read.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { graphql } from 'testkit/gql';
import { ProjectType, ResourceAssignmentModeType } from 'testkit/gql/graphql';
import { execute } from 'testkit/graphql';
import { initSeed } from 'testkit/seed';

const CreateProposalMutation = graphql(`
mutation CreateProposalMutation($input: CreateSchemaProposalInput!) {
createSchemaProposal(input: $input) {
ok {
schemaProposal {
id
}
}
error {
message
}
}
}
`);

const ReadProposalQuery = graphql(`
query ReadProposalQuery($input: SchemaProposalInput!) {
schemaProposal(input: $input) {
title
description
checks(input: { latestPerService: true }) {
edges {
node {
id
}
}
}
}
}
`);

/**
* Creates a proposal and returns a token with specified permissions
**/
async function setup(input: {
tokenPermissions: string[];
}): Promise<{ accessKey: string; proposalId: string }> {
const { createOrg, ownerToken } = await initSeed().createOwner();
const { createProject, createOrganizationAccessToken, setFeatureFlag } = await createOrg();
await setFeatureFlag('schemaProposals', true);
const { target } = await createProject(ProjectType.Federation);

// create as owner
const result = await execute({
document: CreateProposalMutation,
variables: {
input: {
target: { byId: target.id },
author: 'Jeff',
title: 'Proposed changes to the schema...',
},
},
token: ownerToken,
}).then(r => r.expectNoGraphQLErrors());

const { privateAccessKey: accessKey } = await createOrganizationAccessToken(
{
resources: {
mode: ResourceAssignmentModeType.All,
},
permissions: input.tokenPermissions,
},
ownerToken,
);
const proposalId = result.createSchemaProposal.ok?.schemaProposal.id!;
return { accessKey, proposalId };
}

describe('Schema Proposals', () => {
test.concurrent(
'can read proposal with "schemaProposal:describe" permission',
async ({ expect }) => {
const { accessKey, proposalId } = await setup({
tokenPermissions: ['schemaProposal:describe'],
});

{
const proposal = await execute({
document: ReadProposalQuery,
variables: {
input: {
id: proposalId,
},
},
token: accessKey,
}).then(r => r.expectNoGraphQLErrors());

expect(proposal.schemaProposal?.title).toMatchInlineSnapshot(
`Proposed changes to the schema...`,
);
}
},
);

test.concurrent('cannot read proposal without "schemaProposal:describe" permission', async () => {
const { accessKey, proposalId } = await setup({ tokenPermissions: [] });

{
await execute({
document: ReadProposalQuery,
variables: {
input: {
id: proposalId,
},
},
token: accessKey,
}).then(r => r.expectGraphQLErrors());
}
});
});
Loading
Loading