@@ -44,8 +44,7 @@ export default class AppCreate extends Command<typeof AppCreate> {
4444 } ) ,
4545 format : Flags . string ( {
4646 description :
47- 'Storage format version. "v1" (default) uses per-version storage and allows any hash format. "v2" enables cross-version deduplication and requires sha256 hashes.' ,
48- default : 'v1' ,
47+ 'Storage format version. "v1" uses per-version storage and allows any hash format. "v2" enables cross-version deduplication and requires sha256 hashes. Auto-detected from hash format if not specified.' ,
4948 options : [ 'v1' , 'v2' ] ,
5049 } ) ,
5150 } ;
@@ -105,37 +104,40 @@ export default class AppCreate extends Command<typeof AppCreate> {
105104 throw new PersistedOperationsMalformedError ( file ) ;
106105 }
107106
108- // Validate hashes are sha256 and match content (unless v1 format)
109- if ( flags . format !== 'v1' ) {
107+ // Auto-detect format from hash patterns if not explicitly specified
108+ let format : 'v1' | 'v2' ;
109+ if ( flags . format === 'v1' || flags . format === 'v2' ) {
110+ format = flags . format ;
111+ } else {
110112 const sha256Regex = / ^ ( s h a 2 5 6 : ) ? [ a - f 0 - 9 ] { 64 } $ / i;
111- const invalidFormatHashes : string [ ] = [ ] ;
113+ const hashes = Object . keys ( validationResult . data ) ;
114+ const allSha256 = hashes . length > 0 && hashes . every ( hash => sha256Regex . test ( hash ) ) ;
115+ format = allSha256 ? 'v2' : 'v1' ;
116+
117+ if ( format === 'v2' ) {
118+ this . log (
119+ `Detected sha256 hashes — using v2 format for faster uploads and cross-version deduplication.` ,
120+ ) ;
121+ } else {
122+ this . log (
123+ `Hashes are not sha256 — using v1 format. For faster uploads and cross-version deduplication, ` +
124+ `configure your code generator to use sha256 hashes. See https://the-guild.dev/graphql/hive/docs/app-deployments/persisted-documents` ,
125+ ) ;
126+ }
127+ }
128+
129+ // Validate hashes match content for v2 format
130+ if ( format === 'v2' ) {
112131 const mismatchedHashes : Array < { hash : string ; expected : string } > = [ ] ;
113132
114133 for ( const [ hash , body ] of Object . entries ( validationResult . data ) ) {
115- if ( ! sha256Regex . test ( hash ) ) {
116- invalidFormatHashes . push ( hash ) ;
117- } else {
118- // Verify hash matches content
119- const computedHash = createHash ( 'sha256' ) . update ( body ) . digest ( 'hex' ) ;
120- const providedHash = hash . replace ( / ^ s h a 2 5 6 : / i, '' ) . toLowerCase ( ) ;
121- if ( computedHash !== providedHash ) {
122- mismatchedHashes . push ( { hash : providedHash , expected : computedHash } ) ;
123- }
134+ const computedHash = createHash ( 'sha256' ) . update ( body ) . digest ( 'hex' ) ;
135+ const providedHash = hash . replace ( / ^ s h a 2 5 6 : / i, '' ) . toLowerCase ( ) ;
136+ if ( computedHash !== providedHash ) {
137+ mismatchedHashes . push ( { hash : providedHash , expected : computedHash } ) ;
124138 }
125139 }
126140
127- if ( invalidFormatHashes . length > 0 ) {
128- const examples = invalidFormatHashes . slice ( 0 , 3 ) . join ( ', ' ) ;
129- const more =
130- invalidFormatHashes . length > 3 ? ` (and ${ invalidFormatHashes . length - 3 } more)` : '' ;
131- throw new APIError (
132- `Invalid hash format detected: ${ examples } ${ more } \n` +
133- `Hashes must be sha256 (64 hexadecimal characters, optionally prefixed with "sha256:").\n` +
134- `This is required for safe cross-version document deduplication.\n` +
135- `Use --format=v1 to bypass this check (disables cross-version deduplication).` ,
136- ) ;
137- }
138-
139141 if ( mismatchedHashes . length > 0 ) {
140142 const example = mismatchedHashes [ 0 ] ;
141143 const more =
@@ -149,13 +151,17 @@ export default class AppCreate extends Command<typeof AppCreate> {
149151 }
150152 }
151153
154+ const allDocuments = Object . entries ( validationResult . data ) ;
155+ const localHashes = format === 'v2' ? allDocuments . map ( ( [ hash ] ) => hash ) : undefined ;
156+
152157 const result = await this . registryApi ( endpoint , accessToken ) . request ( {
153158 operation : CreateAppDeploymentMutation ,
154159 variables : {
155160 input : {
156161 appName : flags [ 'name' ] ,
157162 appVersion : flags [ 'version' ] ,
158163 target,
164+ hashes : localHashes ,
159165 } ,
160166 } ,
161167 } ) ;
@@ -175,37 +181,12 @@ export default class AppCreate extends Command<typeof AppCreate> {
175181 return ;
176182 }
177183
178- const allDocuments = Object . entries ( validationResult . data ) ;
179184 const totalDocuments = allDocuments . length ;
180185
181- // Fetch existing hashes for delta upload
182- let existingHashes = new Set < string > ( ) ;
183- if ( flags . format !== 'v1' ) {
184- if ( ! target ) {
185- throw new APIError (
186- 'The --target flag is required when using --format=v2 for delta optimization.' ,
187- ) ;
188- }
189- const localHashes = allDocuments . map ( ( [ hash ] ) => hash ) ;
190- const hashesResult = await this . registryApi ( endpoint , accessToken ) . request ( {
191- operation : GetExistingDocumentHashesQuery ,
192- variables : {
193- target,
194- appName : flags [ 'name' ] ,
195- hashes : localHashes ,
196- } ,
197- } ) ;
198-
199- if ( ! hashesResult . target ) {
200- this . logWarning (
201- `Target not found when fetching existing hashes. Delta optimization disabled.` ,
202- ) ;
203- } else {
204- existingHashes = new Set ( hashesResult . target . appDeploymentDocumentHashes ) ;
205- if ( flags . showTiming ) {
206- this . log ( `Found ${ existingHashes . size } existing documents (will skip)` ) ;
207- }
208- }
186+ // Use existing hashes from createAppDeployment response for delta upload
187+ const existingHashes = new Set ( result . createAppDeployment . ok . existingHashes ) ;
188+ if ( flags . showTiming && existingHashes . size > 0 ) {
189+ this . log ( `Found ${ existingHashes . size } existing documents (will skip)` ) ;
209190 }
210191
211192 // Filter out already-existing documents
@@ -241,7 +222,7 @@ export default class AppCreate extends Command<typeof AppCreate> {
241222 appVersion : flags [ 'version' ] ,
242223 documents : buffer ,
243224 format :
244- flags . format === 'v1' ? AppDeploymentFormatType . V1 : AppDeploymentFormatType . V2 ,
225+ format === 'v1' ? AppDeploymentFormatType . V1 : AppDeploymentFormatType . V2 ,
245226 } ,
246227 } ,
247228 } ) ;
@@ -319,6 +300,7 @@ const CreateAppDeploymentMutation = graphql(/* GraphQL */ `
319300 version
320301 status
321302 }
303+ existingHashes
322304 }
323305 error {
324306 message
@@ -358,15 +340,3 @@ const AddDocumentsToAppDeploymentMutation = graphql(/* GraphQL */ `
358340 }
359341 }
360342` ) ;
361-
362- const GetExistingDocumentHashesQuery = graphql ( /* GraphQL */ `
363- query GetExistingDocumentHashes(
364- $target: TargetReferenceInput!
365- $appName: String!
366- $hashes: [String!]!
367- ) {
368- target(reference: $target) {
369- appDeploymentDocumentHashes(appName: $appName, hashes: $hashes)
370- }
371- }
372- ` ) ;
0 commit comments