Skip to content

Commit 125c687

Browse files
authored
feat: add graph deploy --ifps-hash option (#1353)
* feat: add graph deploy --ifps-hash option * re-pin
1 parent c7cf89c commit 125c687

File tree

3 files changed

+116
-60
lines changed

3 files changed

+116
-60
lines changed

.changeset/heavy-ducks-sparkle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphprotocol/graph-cli': minor
3+
---
4+
5+
Add a new `--ipfs-hash` flag to `graph deploy` allowing to deploy a subgraph that is already compiled and uploaded to IPFS.

packages/cli/src/command-helpers/data-sources.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import yaml from 'js-yaml';
12
import immutable from 'immutable';
23
import { loadManifest } from '../migrations/util/load-manifest';
34
import Protocol from '../protocols';
@@ -12,6 +13,17 @@ export const fromFilePath = async (manifestPath: string) => {
1213
return dataSources.concat(templates);
1314
};
1415

16+
// Loads manifest from file path and returns all:
17+
// - data sources
18+
// - templates
19+
// In a single list.
20+
export function fromManifestString(manifest: string) {
21+
// TODO: can we make it typesafe?
22+
const { dataSources = [], templates = [] } = (yaml.safeLoad(manifest) || {}) as unknown as any;
23+
24+
return dataSources.concat(templates);
25+
}
26+
1527
const extractDataSourceByType = (
1628
manifest: immutable.Map<any, any>,
1729
dataSourceType: string,

packages/cli/src/commands/deploy.ts

Lines changed: 99 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { URL } from 'url';
33
import { Args, Command, Flags, ux } from '@oclif/core';
44
import { print, prompt } from 'gluegun';
55
import { identifyDeployKey } from '../command-helpers/auth';
6-
import { createCompiler } from '../command-helpers/compiler';
6+
import { appendApiVersionForGraph, createCompiler } from '../command-helpers/compiler';
77
import * as DataSourcesExtractor from '../command-helpers/data-sources';
88
import { DEFAULT_IPFS_URL } from '../command-helpers/ipfs';
99
import { createJsonRpcClient } from '../command-helpers/jsonrpc';
@@ -12,6 +12,7 @@ import { chooseNodeUrl } from '../command-helpers/node';
1212
import { validateStudioNetwork } from '../command-helpers/studio';
1313
import { assertGraphTsVersion, assertManifestApiVersion } from '../command-helpers/version';
1414
import Protocol from '../protocols';
15+
import { create } from 'ipfs-http-client';
1516

1617
const headersFlag = Flags.custom<Record<string, string>>({
1718
summary: 'Add custom headers that will be used by the IPFS HTTP client.',
@@ -67,6 +68,10 @@ export default class DeployCommand extends Command {
6768
char: 'i',
6869
default: DEFAULT_IPFS_URL,
6970
}),
71+
'ipfs-hash': Flags.string({
72+
summary: 'IPFS hash of the subgraph manifest to deploy.',
73+
required: false,
74+
}),
7075
headers: headersFlag(),
7176
'debug-fork': Flags.string({
7277
summary: 'ID of a remote subgraph whose store will be GraphQL queried.',
@@ -111,6 +116,7 @@ export default class DeployCommand extends Command {
111116
'debug-fork': debugFork,
112117
network,
113118
'network-file': networkFile,
119+
'ipfs-hash': ipfsHash,
114120
},
115121
} = await this.parse(DeployCommand);
116122

@@ -138,16 +144,6 @@ export default class DeployCommand extends Command {
138144
])
139145
.then(({ product }) => product as string));
140146

141-
try {
142-
const dataSourcesAndTemplates = await DataSourcesExtractor.fromFilePath(manifest);
143-
144-
for (const { network } of dataSourcesAndTemplates) {
145-
validateStudioNetwork({ studio, product, network });
146-
}
147-
} catch (e) {
148-
this.error(e, { exit: 1 });
149-
}
150-
151147
const { node } = chooseNodeUrl({
152148
product,
153149
studio,
@@ -158,56 +154,9 @@ export default class DeployCommand extends Command {
158154
this.error('No Graph node provided');
159155
}
160156

161-
let protocol;
162-
try {
163-
// Checks to make sure deploy doesn't run against
164-
// older subgraphs (both apiVersion and graph-ts version).
165-
//
166-
// We don't want the deploy to run without these conditions
167-
// because that would mean the CLI would try to compile code
168-
// using the wrong AssemblyScript compiler.
169-
await assertManifestApiVersion(manifest, '0.0.5');
170-
await assertGraphTsVersion(path.dirname(manifest), '0.25.0');
171-
172-
const dataSourcesAndTemplates = await DataSourcesExtractor.fromFilePath(manifest);
173-
174-
protocol = Protocol.fromDataSources(dataSourcesAndTemplates);
175-
} catch (e) {
176-
this.error(e, { exit: 1 });
177-
}
178-
179-
if (network) {
180-
const identifierName = protocol.getContract()!.identifierName();
181-
await updateSubgraphNetwork(manifest, network, networkFile, identifierName);
182-
}
183-
184157
const isStudio = node.match(/studio/);
185158
const isHostedService = node.match(/thegraph.com/) && !isStudio;
186159

187-
const compiler = createCompiler(manifest, {
188-
ipfs,
189-
headers,
190-
outputDir,
191-
outputFormat: 'wasm',
192-
skipMigrations,
193-
blockIpfsMethods: isStudio || undefined, // Network does not support publishing subgraphs with IPFS methods
194-
protocol,
195-
});
196-
197-
// Exit with an error code if the compiler couldn't be created
198-
if (!compiler) {
199-
this.exit(1);
200-
return;
201-
}
202-
203-
// Ask for label if not on hosted service
204-
let versionLabel = versionLabelFlag;
205-
if (!versionLabel && !isHostedService) {
206-
versionLabel = await ux.prompt('Which version label to use? (e.g. "v0.0.1")', {
207-
required: true,
208-
});
209-
}
210-
211160
const requestUrl = new URL(node);
212161
const client = createJsonRpcClient(requestUrl);
213162

@@ -228,6 +177,14 @@ export default class DeployCommand extends Command {
228177
client.options.headers = { Authorization: 'Bearer ' + deployKey };
229178
}
230179

180+
// Ask for label if not on hosted service
181+
let versionLabel = versionLabelFlag;
182+
if (!versionLabel && !isHostedService) {
183+
versionLabel = await ux.prompt('Which version label to use? (e.g. "v0.0.1")', {
184+
required: true,
185+
});
186+
}
187+
231188
// eslint-disable-next-line @typescript-eslint/no-this-alias -- request needs it
232189
const self = this;
233190

@@ -259,8 +216,8 @@ export default class DeployCommand extends Command {
259216
'\nYou may need to create it at https://thegraph.com/explorer/dashboard.';
260217
} else {
261218
errorMessage += `
262-
Make sure to create the subgraph first by running the following command:
263-
$ graph create --node ${node} ${subgraphName}`;
219+
Make sure to create the subgraph first by running the following command:
220+
$ graph create --node ${node} ${subgraphName}`;
264221
}
265222
}
266223

@@ -291,11 +248,93 @@ $ graph create --node ${node} ${subgraphName}`;
291248
print.info('\nSubgraph endpoints:');
292249
print.info(`Queries (HTTP): ${queries}`);
293250
print.info(``);
251+
process.exit(0);
294252
}
295253
},
296254
);
297255
};
298256

257+
// we are provided the IPFS hash, so we deploy directly
258+
if (ipfsHash) {
259+
// Connect to the IPFS node (if a node address was provided)
260+
const ipfsClient = create({ url: appendApiVersionForGraph(ipfs.toString()), headers });
261+
262+
// Fetch the manifest from IPFS
263+
const manifestBuffer = ipfsClient.cat(ipfsHash);
264+
let manifestFile = '';
265+
for await (const chunk of manifestBuffer) {
266+
manifestFile += chunk.toString();
267+
}
268+
269+
if (!manifestFile) {
270+
this.error(`Could not find subgraph manifest at IPFS hash ${ipfsHash}`, { exit: 1 });
271+
}
272+
273+
await ipfsClient.pin.add(ipfsHash);
274+
275+
try {
276+
const dataSourcesAndTemplates = DataSourcesExtractor.fromManifestString(manifestFile);
277+
278+
for (const { network } of dataSourcesAndTemplates) {
279+
validateStudioNetwork({ studio, product, network });
280+
}
281+
} catch (e) {
282+
this.error(e, { exit: 1 });
283+
}
284+
285+
await deploySubgraph(ipfsHash);
286+
return;
287+
}
288+
289+
try {
290+
const dataSourcesAndTemplates = await DataSourcesExtractor.fromFilePath(manifest);
291+
292+
for (const { network } of dataSourcesAndTemplates) {
293+
validateStudioNetwork({ studio, product, network });
294+
}
295+
} catch (e) {
296+
this.error(e, { exit: 1 });
297+
}
298+
299+
let protocol;
300+
try {
301+
// Checks to make sure deploy doesn't run against
302+
// older subgraphs (both apiVersion and graph-ts version).
303+
//
304+
// We don't want the deploy to run without these conditions
305+
// because that would mean the CLI would try to compile code
306+
// using the wrong AssemblyScript compiler.
307+
await assertManifestApiVersion(manifest, '0.0.5');
308+
await assertGraphTsVersion(path.dirname(manifest), '0.25.0');
309+
310+
const dataSourcesAndTemplates = await DataSourcesExtractor.fromFilePath(manifest);
311+
312+
protocol = Protocol.fromDataSources(dataSourcesAndTemplates);
313+
} catch (e) {
314+
this.error(e, { exit: 1 });
315+
}
316+
317+
if (network) {
318+
const identifierName = protocol.getContract()!.identifierName();
319+
await updateSubgraphNetwork(manifest, network, networkFile, identifierName);
320+
}
321+
322+
const compiler = createCompiler(manifest, {
323+
ipfs,
324+
headers,
325+
outputDir,
326+
outputFormat: 'wasm',
327+
skipMigrations,
328+
blockIpfsMethods: isStudio || undefined, // Network does not support publishing subgraphs with IPFS methods
329+
protocol,
330+
});
331+
332+
// Exit with an error code if the compiler couldn't be created
333+
if (!compiler) {
334+
this.exit(1);
335+
return;
336+
}
337+
299338
if (watch) {
300339
await compiler.watchAndCompile(async ipfsHash => {
301340
if (ipfsHash !== undefined) {

0 commit comments

Comments
 (0)