@@ -10,6 +10,7 @@ import (
10
10
"net/http"
11
11
"net/http/httputil"
12
12
"os"
13
+ "path/filepath"
13
14
"regexp"
14
15
"strings"
15
16
"time"
@@ -188,6 +189,7 @@ func resourceHerokuBuildCreate(d *schema.ResourceData, meta interface{}) error {
188
189
opts .Buildpacks = buildpacks
189
190
}
190
191
192
+ var checksum string
191
193
if v , ok := d .GetOk ("source" ); ok {
192
194
vL := v .([]interface {})
193
195
@@ -213,24 +215,31 @@ func resourceHerokuBuildCreate(d *schema.ResourceData, meta interface{}) error {
213
215
if err != nil {
214
216
return fmt .Errorf ("Error stating build source path %s: %s" , path , err )
215
217
}
216
-
217
- if fileInfo .IsDir () {
218
+ // The checksum is "relaxed" for source directories, and not performed on the tarball, but instead purely filenames & contents.
219
+ // This allows empemeral runtimes like Terraform Cloud to have a "stable" checksum for a source directory that will be cloned fresh each time.
220
+ // The trade-off, is that the checksum is non-standard, and should not be passed to Heroku as a build parameter.
221
+ useRelaxedChecksum := fileInfo .IsDir ()
222
+ if useRelaxedChecksum {
218
223
// Generate tarball from the directory
219
224
tarballPath , err = generateSourceTarball (path )
220
225
if err != nil {
221
226
return fmt .Errorf ("Error generating build source tarball %s: %s" , path , err )
222
227
}
223
228
defer cleanupSourceFile (tarballPath )
229
+ checksum , err = checksumSourceRelaxed (path )
230
+ if err != nil {
231
+ return fmt .Errorf ("Error calculating relaxed checksum for directory source %s: %s" , path , err )
232
+ }
224
233
} else {
225
234
// or simply use the path to the file
226
235
tarballPath = path
236
+ checksum , err = checksumSource (tarballPath )
237
+ if err != nil {
238
+ return fmt .Errorf ("Error calculating checksum for tarball source %s: %s" , tarballPath , err )
239
+ }
227
240
}
228
241
229
242
// Checksum, create, & upload source archive
230
- checksum , err := checksumSource (tarballPath )
231
- if err != nil {
232
- return fmt .Errorf ("Error calculating checksum for build source %s: %s" , tarballPath , err )
233
- }
234
243
newSource , err := client .SourceCreate (context .TODO ())
235
244
if err != nil {
236
245
return fmt .Errorf ("Error creating source for build: %s" , err )
@@ -240,7 +249,9 @@ func resourceHerokuBuildCreate(d *schema.ResourceData, meta interface{}) error {
240
249
return fmt .Errorf ("Error uploading source for build to %s: %s" , newSource .SourceBlob .PutURL , err )
241
250
}
242
251
opts .SourceBlob .URL = & newSource .SourceBlob .GetURL
243
- opts .SourceBlob .Checksum = & checksum
252
+ if ! useRelaxedChecksum {
253
+ opts .SourceBlob .Checksum = & checksum
254
+ }
244
255
} else if v , ok = sourceArg ["url" ]; ok && v != "" {
245
256
s := v .(string )
246
257
opts .SourceBlob .URL = & s
@@ -271,6 +282,8 @@ func resourceHerokuBuildCreate(d *schema.ResourceData, meta interface{}) error {
271
282
}
272
283
273
284
d .SetId (build .ID )
285
+ // Capture the checksum, to diff changes in the local source directory.
286
+ d .Set ("local_checksum" , checksum )
274
287
275
288
build , err = client .BuildInfo (context .TODO (), app , build .ID )
276
289
if err != nil {
@@ -321,31 +334,30 @@ func resourceHerokuBuildCustomizeDiff(ctx context.Context, diff *schema.Resource
321
334
if err != nil {
322
335
return fmt .Errorf ("Error stating build source path %s: %s" , path , err )
323
336
}
324
-
325
- if fileInfo . IsDir () {
326
- // To diff this generates a tarball of the source directory for calculating the current "local_checksum", same function call as in resourceHerokuBuildCreate
327
- tarballPath , err = generateSourceTarball (path )
337
+ useRelaxedChecksum := fileInfo . IsDir ()
338
+ var realChecksum string
339
+ if useRelaxedChecksum {
340
+ realChecksum , err = checksumSourceRelaxed (path )
328
341
if err != nil {
329
- return fmt .Errorf ("Error generating build source tarball %s: %s" , path , err )
342
+ return fmt .Errorf ("Error calculating relaxed checksum for directory source %s: %s" , path , err )
330
343
}
331
- defer cleanupSourceFile (tarballPath )
332
344
} else {
333
345
// or simply use the path to the file
334
346
tarballPath = path
347
+ realChecksum , err = checksumSource (tarballPath )
348
+ if err != nil {
349
+ return fmt .Errorf ("Error calculating checksum for tarball source %s: %s" , tarballPath , err )
350
+ }
335
351
}
336
352
337
- // Calculate & diff the "local_checksum" SHA256
338
- realChecksum , err := checksumSource (tarballPath )
339
- if err == nil {
340
- oldChecksum , newChecksum := diff .GetChange ("local_checksum" )
341
- log .Printf ("[DEBUG] Diffing source: old '%s', new '%s', real '%s'" , oldChecksum , newChecksum , realChecksum )
342
- if newChecksum != realChecksum {
343
- if err := diff .SetNew ("local_checksum" , realChecksum ); err != nil {
344
- return fmt .Errorf ("Error updating source archive checksum: %s" , err )
345
- }
346
- if err := diff .ForceNew ("local_checksum" ); err != nil {
347
- return fmt .Errorf ("Error forcing new source resource: %s" , err )
348
- }
353
+ oldChecksum , newChecksum := diff .GetChange ("local_checksum" )
354
+ log .Printf ("[DEBUG] Diffing source: old '%s', new '%s', real '%s'" , oldChecksum , newChecksum , realChecksum )
355
+ if newChecksum != realChecksum {
356
+ if err := diff .SetNew ("local_checksum" , realChecksum ); err != nil {
357
+ return fmt .Errorf ("Error updating source archive checksum: %s" , err )
358
+ }
359
+ if err := diff .ForceNew ("local_checksum" ); err != nil {
360
+ return fmt .Errorf ("Error forcing new source resource: %s" , err )
349
361
}
350
362
}
351
363
}
@@ -414,6 +426,63 @@ func checksumSource(filePath string) (string, error) {
414
426
return checksum , nil
415
427
}
416
428
429
+ func checksumSourceRelaxed (sourcePath string ) (string , error ) {
430
+ hash := sha256 .New ()
431
+
432
+ info , err := os .Stat (sourcePath )
433
+ if err != nil {
434
+ return "" , fmt .Errorf ("Error stating source path '%s' for checksum %w" , sourcePath , err )
435
+ }
436
+
437
+ var baseDir string
438
+ if info .IsDir () {
439
+ baseDir = filepath .Base (sourcePath )
440
+ }
441
+
442
+ err = filepath .Walk (sourcePath , func (path string , info os.FileInfo , walkErr error ) error {
443
+ var walkPath string
444
+ if baseDir != "" {
445
+ walkPath = filepath .ToSlash (filepath .Join (baseDir , strings .TrimPrefix (path , sourcePath )))
446
+ } else {
447
+ walkPath = info .Name ()
448
+ }
449
+
450
+ if walkErr != nil {
451
+ return fmt .Errorf ("Error walking '%s' for checksum: %w" , walkPath , walkErr )
452
+ }
453
+
454
+ // Write each path name to the hash, so that if things are renamed, they're ensured to change checksum.
455
+ fmt .Fprint (hash , walkPath + "\n " )
456
+ log .Printf ("[DEBUG] hash ← %s (name)" , walkPath )
457
+
458
+ // Skip checksumming unless the file has data/contents.
459
+ if info .IsDir () || ! info .Mode ().IsRegular () {
460
+ return nil
461
+ }
462
+
463
+ // Read the file into the hasher.
464
+ file , err := os .Open (path )
465
+ if err != nil {
466
+ return fmt .Errorf ("Error opening file '%s' for checksum: %w" , info .Name (), err )
467
+ }
468
+ defer file .Close ()
469
+ b , err := io .Copy (hash , file )
470
+ if err != nil {
471
+ return fmt .Errorf ("Error reading file '%s' for checksum: %w" , info .Name (), err )
472
+ }
473
+ log .Printf ("[DEBUG] hash ← %v (bytes)" , b )
474
+
475
+ return nil
476
+ })
477
+ if err != nil {
478
+ return "" , err
479
+ }
480
+
481
+ checksum := fmt .Sprintf ("SHA256:%x" , hash .Sum (nil ))
482
+ log .Printf ("[DEBUG] hash sum → %v" , checksum )
483
+ return checksum , nil
484
+ }
485
+
417
486
func setBuildState (d * schema.ResourceData , build * heroku.Build , appName string ) error {
418
487
d .Set ("app" , appName )
419
488
@@ -448,8 +517,6 @@ func setBuildState(d *schema.ResourceData, build *heroku.Build, appName string)
448
517
if v := build .SourceBlob .URL ; v != "" {
449
518
source ["url" ] = v
450
519
}
451
- } else {
452
- d .Set ("local_checksum" , build .SourceBlob .Checksum )
453
520
}
454
521
if v := build .SourceBlob .Version ; v != nil {
455
522
source ["version" ] = * v
0 commit comments