Integrate SP auctions for app deployment #2

Merged
nabarun merged 42 commits from ng-integrate-auction into main 2024-10-18 12:37:01 +00:00
17 changed files with 238 additions and 4 deletions
Showing only changes of commit 614405a2f4 - Show all commits

View File

@ -472,6 +472,11 @@ export class Registry {
return this.registry.resolveNames([name]); return this.registry.resolveNames([name]);
} }
async getAuctionData(auctionId: string): Promise<any> {
const auction = this.registry.getAuctionsByIds([auctionId]);
return auction;
}
getLrn(appName: string): string { getLrn(appName: string): string {
assert(this.registryConfig.authority, "Authority doesn't exist"); assert(this.registryConfig.authority, "Authority doesn't exist");
return `lrn://${this.registryConfig.authority}/applications/${appName}`; return `lrn://${this.registryConfig.authority}/applications/${appName}`;

View File

@ -69,6 +69,13 @@ export const createResolvers = async (service: Service): Promise<any> => {
) => { ) => {
return service.getDomainsByProjectId(projectId, filter); return service.getDomainsByProjectId(projectId, filter);
}, },
getAuctionStatus: async (
_: any,
{ auctionId }: { auctionId: string },
) => {
return service.getAuctionStatus(auctionId);
},
}, },
// TODO: Return error in GQL response // TODO: Return error in GQL response

View File

@ -22,6 +22,13 @@ enum DeploymentStatus {
Deleting Deleting
} }
enum AuctionStatus {
AuctionStatusCommitPhase
AuctionStatusRevealPhase
AuctionStatusExpired
AuctionStatusCompleted
}
enum DomainStatus { enum DomainStatus {
Live Live
Pending Pending
@ -199,6 +206,7 @@ type Query {
environmentVariables(projectId: String!): [EnvironmentVariable!] environmentVariables(projectId: String!): [EnvironmentVariable!]
projectMembers(projectId: String!): [ProjectMember!] projectMembers(projectId: String!): [ProjectMember!]
searchProjects(searchText: String!): [Project!] searchProjects(searchText: String!): [Project!]
getAuctionStatus(auctionId: String!): AuctionStatus!
domains(projectId: String!, filter: FilterDomainsInput): [Domain] domains(projectId: String!, filter: FilterDomainsInput): [Domain]
} }

View File

@ -1267,4 +1267,11 @@ export class Service {
): Promise<boolean> { ): Promise<boolean> {
return this.db.updateUser(user, data); return this.db.updateUser(user, data);
} }
async getAuctionStatus(
auctionId: string
): Promise<boolean> {
const auctions = await this.registry.getAuctionData(auctionId);
return auctions[0].status;
}
} }

View File

@ -0,0 +1,94 @@
import { useCallback, useEffect, useState } from 'react';
import { Project } from 'gql-client';
import { CheckRoundFilledIcon, GlobeIcon, LoadingIcon } from 'components/shared/CustomIcon';
import { useGQLClient } from 'context/GQLClientContext';
import { Tag } from 'components/shared';
const CHECK_AUCTION_STATUS_INTERVAL = 2000;
export const AuctionData = ({
project
}: {
project: Project
}) => {
const [isAuctionCompleted, setIsAuctionCompleted] = useState<boolean>(true);
const client = useGQLClient();
const getIconByAuctionStatus = (isCompleted: Boolean) => {
return isCompleted ? <CheckRoundFilledIcon /> : <LoadingIcon className="animate-spin" />
};
const checkAuctionStatus = async () => {
const result = await client.getAuctionStatus(project.auctionId);
if (result) {
setIsAuctionCompleted(true);
}
};
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
if (!isAuctionCompleted) {
checkAuctionStatus();
intervalId = setInterval(checkAuctionStatus, CHECK_AUCTION_STATUS_INTERVAL);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [isAuctionCompleted]);
const renderAuctionStatus = useCallback(
(className?: string) => {
return (
<div className={className}>
<Tag
leftIcon={getIconByAuctionStatus(isAuctionCompleted)}
size="xs"
>
{isAuctionCompleted ? 'Auction Completed' : 'Auction ongoing'}
</Tag>
</div>
);
},
[isAuctionCompleted],
);
return (
<>
<div className="flex justify-between items-center py-3 text-sm">
<div className="flex items-center text-elements-high-em gap-2">
<GlobeIcon />
<span>Auction data</span>
</div>
{/* AUCTION STATUS */}
<div className="w-[10%] max-w-[110px] hidden md:flex h-fit">
{renderAuctionStatus('w-[10%] max-w-[110px] hidden md:flex h-fit')}
</div>
</div>
<div className="ml-4 mb-2">
<p className="text-elements-low-em text-sm">
Auction Id: {project.auctionId}
</p>
<p className="text-elements-low-em text-sm mt-2">
Deployer LRNs:
</p>
<div>
{project.deployerLrn.map((lrn, index) => (
<p key={index} className="text-elements-low-em text-sm">
{lrn}
</p>
))}
</div>
</div>
</>
);
};

View File

@ -36,6 +36,8 @@ const deployment: Deployment = {
url: 'https://deploy1.example.com', url: 'https://deploy1.example.com',
environment: Environment.Production, environment: Environment.Production,
isCurrent: true, isCurrent: true,
auctionId: '7553538436710373822151221341b43f577e07b0525d083cc9b2de98890138a1',
deployerLrn: 'lrn://deepstack-test4/deployers/webapp-deployer-api.test4.wireitin.com',
status: DeploymentStatus.Ready, status: DeploymentStatus.Ready,
createdBy: { createdBy: {
id: 'user1', id: 'user1',

View File

@ -21,6 +21,7 @@ import { Activity } from 'components/projects/project/overview/Activity';
import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo'; import { OverviewInfo } from 'components/projects/project/overview/OverviewInfo';
import { relativeTimeMs } from 'utils/time'; import { relativeTimeMs } from 'utils/time';
import { Domain, DomainStatus } from 'gql-client'; import { Domain, DomainStatus } from 'gql-client';
import { AuctionData } from 'components/projects/project/overview/Activity/AuctionData';
const COMMITS_PER_PAGE = 4; const COMMITS_PER_PAGE = 4;
@ -136,6 +137,7 @@ const OverviewTabPanel = () => {
</a> </a>
</div> </div>
</div> </div>
<AuctionData project={project}/>
<OverviewInfo label="Domain" icon={<GlobeIcon />}> <OverviewInfo label="Domain" icon={<GlobeIcon />}>
{liveDomain ? ( {liveDomain ? (
<Tag type="positive" size="xs" leftIcon={<CheckRoundFilledIcon />}> <Tag type="positive" size="xs" leftIcon={<CheckRoundFilledIcon />}>

View File

@ -102,6 +102,8 @@ export const deployment0: Deployment = {
domain: domain0, domain: domain0,
commitMessage: 'Commit Message', commitMessage: 'Commit Message',
createdBy: user, createdBy: user,
auctionId: '7553538436710373822151221341b43f577e07b0525d083cc9b2de98890138a1',
deployerLrn: 'lrn://deepstack-test4/deployers/webapp-deployer-api.test4.wireitin.com',
}; };
export const project: Project = { export const project: Project = {
@ -119,6 +121,8 @@ export const project: Project = {
organization: organization, organization: organization,
template: 'Template', template: 'Template',
members: [member], members: [member],
auctionId: '7553538436710373822151221341b43f577e07b0525d083cc9b2de98890138a1',
deployerLrn: ['lrn://deepstack-test4/deployers/webapp-deployer-api.test4.wireitin.com', 'lrn://wireitin/deployers/webapp-deployer-api.wireitin.com'],
webhooks: ['beepboop'], webhooks: ['beepboop'],
icon: 'Icon', icon: 'Icon',
subDomain: 'SubDomain', subDomain: 'SubDomain',

View File

@ -18,6 +18,12 @@ declare enum DeploymentStatus {
Error = "Error", Error = "Error",
Deleting = "Deleting" Deleting = "Deleting"
} }
declare enum AuctionStatus {
AuctionStatusCommitPhase = "commit",
AuctionStatusRevealPhase = "reveal",
AuctionStatusExpired = "expired",
AuctionStatusCompleted = "completed"
}
declare enum DomainStatus { declare enum DomainStatus {
Live = "Live", Live = "Live",
Pending = "Pending" Pending = "Pending"
@ -55,6 +61,8 @@ type Deployment = {
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
url?: string; url?: string;
deployerLrn: string;
auctionId?: string;
environment: Environment; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
status: DeploymentStatus; status: DeploymentStatus;
@ -112,6 +120,8 @@ type Project = {
description: string; description: string;
template: string; template: string;
framework: string; framework: string;
deployerLrn: string[];
auctionId: string;
webhooks: string[]; webhooks: string[];
members: ProjectMember[]; members: ProjectMember[];
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];
@ -294,6 +304,7 @@ declare class GQLClient {
getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>; getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>;
authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>; authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>;
unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>; unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>;
getAuctionStatus(auctionId: string): Promise<AuctionStatus>;
} }
export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuctionData, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User }; export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuctionData, AuctionStatus, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User };

View File

@ -18,6 +18,12 @@ declare enum DeploymentStatus {
Error = "Error", Error = "Error",
Deleting = "Deleting" Deleting = "Deleting"
} }
declare enum AuctionStatus {
AuctionStatusCommitPhase = "commit",
AuctionStatusRevealPhase = "reveal",
AuctionStatusExpired = "expired",
AuctionStatusCompleted = "completed"
}
declare enum DomainStatus { declare enum DomainStatus {
Live = "Live", Live = "Live",
Pending = "Pending" Pending = "Pending"
@ -55,6 +61,8 @@ type Deployment = {
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
url?: string; url?: string;
deployerLrn: string;
auctionId?: string;
environment: Environment; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
status: DeploymentStatus; status: DeploymentStatus;
@ -112,6 +120,8 @@ type Project = {
description: string; description: string;
template: string; template: string;
framework: string; framework: string;
deployerLrn: string[];
auctionId: string;
webhooks: string[]; webhooks: string[];
members: ProjectMember[]; members: ProjectMember[];
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];
@ -294,6 +304,7 @@ declare class GQLClient {
getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>; getDomains(projectId: string, filter?: FilterDomainInput): Promise<GetDomainsResponse>;
authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>; authenticateGitHub(code: string): Promise<AuthenticateGitHubResponse>;
unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>; unauthenticateGithub(): Promise<UnauthenticateGitHubResponse>;
getAuctionStatus(auctionId: string): Promise<AuctionStatus>;
} }
export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuctionData, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User }; export { type AddDomainInput, type AddDomainResponse, type AddEnvironmentVariableInput, type AddEnvironmentVariablesResponse, type AddProjectFromTemplateInput, type AddProjectFromTemplateResponse, type AddProjectInput, type AddProjectMemberInput, type AddProjectMemberResponse, type AddProjectResponse, type AuctionData, AuctionStatus, type AuthenticateGitHubResponse, type DeleteDeploymentResponse, type DeleteDomainResponse, type DeleteProjectResponse, type Deployment, DeploymentStatus, type Domain, DomainStatus, Environment, type EnvironmentVariable, type FilterDomainInput, GQLClient, type GetDeploymentsResponse, type GetDomainsResponse, type GetEnvironmentVariablesResponse, type GetOrganizationsResponse, type GetProjectMembersResponse, type GetProjectResponse, type GetProjectsInOrganizationResponse, type GetUserResponse, type GraphQLConfig, type Organization, type OrganizationMember, type OrganizationProject, Permission, type Project, type ProjectMember, type RedeployToProdResponse, type RemoveEnvironmentVariableResponse, type RemoveProjectMemberResponse, Role, type RollbackDeploymentResponse, type SearchProjectsResponse, type UnauthenticateGitHubResponse, type UpdateDeploymentToProdResponse, type UpdateDomainInput, type UpdateDomainResponse, type UpdateEnvironmentVariableInput, type UpdateEnvironmentVariableResponse, type UpdateProjectInput, type UpdateProjectMemberInput, type UpdateProjectMemberResponse, type UpdateProjectResponse, type User };

View File

@ -40,6 +40,7 @@ var __async = (__this, __arguments, generator) => {
// src/index.ts // src/index.ts
var src_exports = {}; var src_exports = {};
__export(src_exports, { __export(src_exports, {
AuctionStatus: () => AuctionStatus,
DeploymentStatus: () => DeploymentStatus, DeploymentStatus: () => DeploymentStatus,
DomainStatus: () => DomainStatus, DomainStatus: () => DomainStatus,
Environment: () => Environment, Environment: () => Environment,
@ -182,6 +183,8 @@ query ($projectId: String!) {
commitHash commitHash
commitMessage commitMessage
url url
auctionId
deployerLrn
environment environment
isCurrent isCurrent
status status
@ -269,6 +272,11 @@ query ($projectId: String!, $filter: FilterDomainsInput) {
} }
} }
`; `;
var getAuctionStatus = import_client.gql`
query ($auctionId: String!) {
getAuctionStatus(auctionId: $auctionId)
}
`;
// src/mutations.ts // src/mutations.ts
var import_client2 = require("@apollo/client"); var import_client2 = require("@apollo/client");
@ -714,6 +722,17 @@ var GQLClient = class {
return data; return data;
}); });
} }
getAuctionStatus(auctionId) {
return __async(this, null, function* () {
const { data } = yield this.client.query({
query: getAuctionStatus,
variables: {
auctionId
}
});
return data;
});
}
}; };
// src/types.ts // src/types.ts
@ -741,6 +760,13 @@ var DeploymentStatus = /* @__PURE__ */ ((DeploymentStatus2) => {
DeploymentStatus2["Deleting"] = "Deleting"; DeploymentStatus2["Deleting"] = "Deleting";
return DeploymentStatus2; return DeploymentStatus2;
})(DeploymentStatus || {}); })(DeploymentStatus || {});
var AuctionStatus = /* @__PURE__ */ ((AuctionStatus2) => {
AuctionStatus2["AuctionStatusCommitPhase"] = "commit";
AuctionStatus2["AuctionStatusRevealPhase"] = "reveal";
AuctionStatus2["AuctionStatusExpired"] = "expired";
AuctionStatus2["AuctionStatusCompleted"] = "completed";
return AuctionStatus2;
})(AuctionStatus || {});
var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => { var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
DomainStatus2["Live"] = "Live"; DomainStatus2["Live"] = "Live";
DomainStatus2["Pending"] = "Pending"; DomainStatus2["Pending"] = "Pending";
@ -748,6 +774,7 @@ var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
})(DomainStatus || {}); })(DomainStatus || {});
// Annotate the CommonJS export names for ESM import in node: // Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = { 0 && (module.exports = {
AuctionStatus,
DeploymentStatus, DeploymentStatus,
DomainStatus, DomainStatus,
Environment, Environment,

File diff suppressed because one or more lines are too long

View File

@ -155,6 +155,8 @@ query ($projectId: String!) {
commitHash commitHash
commitMessage commitMessage
url url
auctionId
deployerLrn
environment environment
isCurrent isCurrent
status status
@ -242,6 +244,11 @@ query ($projectId: String!, $filter: FilterDomainsInput) {
} }
} }
`; `;
var getAuctionStatus = gql`
query ($auctionId: String!) {
getAuctionStatus(auctionId: $auctionId)
}
`;
// src/mutations.ts // src/mutations.ts
import { gql as gql2 } from "@apollo/client"; import { gql as gql2 } from "@apollo/client";
@ -687,6 +694,17 @@ var GQLClient = class {
return data; return data;
}); });
} }
getAuctionStatus(auctionId) {
return __async(this, null, function* () {
const { data } = yield this.client.query({
query: getAuctionStatus,
variables: {
auctionId
}
});
return data;
});
}
}; };
// src/types.ts // src/types.ts
@ -714,12 +732,20 @@ var DeploymentStatus = /* @__PURE__ */ ((DeploymentStatus2) => {
DeploymentStatus2["Deleting"] = "Deleting"; DeploymentStatus2["Deleting"] = "Deleting";
return DeploymentStatus2; return DeploymentStatus2;
})(DeploymentStatus || {}); })(DeploymentStatus || {});
var AuctionStatus = /* @__PURE__ */ ((AuctionStatus2) => {
AuctionStatus2["AuctionStatusCommitPhase"] = "commit";
AuctionStatus2["AuctionStatusRevealPhase"] = "reveal";
AuctionStatus2["AuctionStatusExpired"] = "expired";
AuctionStatus2["AuctionStatusCompleted"] = "completed";
return AuctionStatus2;
})(AuctionStatus || {});
var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => { var DomainStatus = /* @__PURE__ */ ((DomainStatus2) => {
DomainStatus2["Live"] = "Live"; DomainStatus2["Live"] = "Live";
DomainStatus2["Pending"] = "Pending"; DomainStatus2["Pending"] = "Pending";
return DomainStatus2; return DomainStatus2;
})(DomainStatus || {}); })(DomainStatus || {});
export { export {
AuctionStatus,
DeploymentStatus, DeploymentStatus,
DomainStatus, DomainStatus,
Environment, Environment,

File diff suppressed because one or more lines are too long

View File

@ -409,4 +409,15 @@ export class GQLClient {
return data; return data;
} }
async getAuctionStatus(auctionId: string): Promise<types.AuctionStatus> {
const { data } = await this.client.query({
query: queries.getAuctionStatus,
variables: {
auctionId,
},
});
return data;
}
} }

View File

@ -132,6 +132,8 @@ query ($projectId: String!) {
commitHash commitHash
commitMessage commitMessage
url url
auctionId
deployerLrn
environment environment
isCurrent isCurrent
status status
@ -223,3 +225,9 @@ query ($projectId: String!, $filter: FilterDomainsInput) {
} }
} }
`; `;
export const getAuctionStatus = gql`
query ($auctionId: String!) {
getAuctionStatus(auctionId: $auctionId)
}
`;

View File

@ -25,6 +25,13 @@ export enum DeploymentStatus {
Deleting = "Deleting", Deleting = "Deleting",
} }
export enum AuctionStatus {
AuctionStatusCommitPhase = "commit",
AuctionStatusRevealPhase = "reveal",
AuctionStatusExpired = "expired",
AuctionStatusCompleted = "completed",
}
export enum DomainStatus { export enum DomainStatus {
Live = "Live", Live = "Live",
Pending = "Pending", Pending = "Pending",
@ -66,6 +73,8 @@ export type Deployment = {
commitHash: string; commitHash: string;
commitMessage: string; commitMessage: string;
url?: string; url?: string;
deployerLrn: string;
auctionId?: string;
environment: Environment; environment: Environment;
isCurrent: boolean; isCurrent: boolean;
status: DeploymentStatus; status: DeploymentStatus;
@ -128,6 +137,8 @@ export type Project = {
description: string; description: string;
template: string; template: string;
framework: string; framework: string;
deployerLrn: string[];
auctionId: string;
webhooks: string[]; webhooks: string[];
members: ProjectMember[]; members: ProjectMember[];
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];