@@ -15,10 +15,11 @@ import (
1515
1616func resourceHerokuAppRelease () * schema.Resource {
1717 return & schema.Resource {
18- Create : resourceHerokuAppReleaseCreate ,
19- Read : resourceHerokuAppReleaseRead ,
20- Update : resourceHerokuAppReleaseUpdate ,
21- Delete : resourceHerokuAppReleaseDelete ,
18+ Create : resourceHerokuAppReleaseCreate ,
19+ Read : resourceHerokuAppReleaseRead ,
20+ Update : resourceHerokuAppReleaseUpdate ,
21+ Delete : resourceHerokuAppReleaseDelete ,
22+ CustomizeDiff : resourceHerokuAppReleaseCustomizeDiff ,
2223
2324 Importer : & schema.ResourceImporter {
2425 State : resourceHerokuAppReleaseImport ,
@@ -33,9 +34,18 @@ func resourceHerokuAppRelease() *schema.Resource {
3334 },
3435
3536 "slug_id" : { // An existing Heroku release cannot be updated so ForceNew is required
36- Type : schema .TypeString ,
37- Required : true ,
38- ForceNew : true ,
37+ Type : schema .TypeString ,
38+ Optional : true ,
39+ ForceNew : true ,
40+ ConflictsWith : []string {"oci_image" },
41+ },
42+
43+ "oci_image" : { // OCI image identifier for Fir generation apps
44+ Type : schema .TypeString ,
45+ Optional : true ,
46+ ForceNew : true ,
47+ ConflictsWith : []string {"slug_id" },
48+ ValidateFunc : validateOCIImage ,
3949 },
4050
4151 "description" : {
@@ -58,6 +68,11 @@ func resourceHerokuAppRelease() *schema.Resource {
5868func resourceHerokuAppReleaseCreate (d * schema.ResourceData , meta interface {}) error {
5969 client := meta .(* Config ).Api
6070
71+ // Validate artifact type for app generation at apply time
72+ if err := validateArtifactForApp (client , d ); err != nil {
73+ return err
74+ }
75+
6176 opts := heroku.ReleaseCreateOpts {}
6277
6378 appName := getAppId (d )
@@ -68,6 +83,12 @@ func resourceHerokuAppReleaseCreate(d *schema.ResourceData, meta interface{}) er
6883 opts .Slug = vs
6984 }
7085
86+ if v , ok := d .GetOk ("oci_image" ); ok {
87+ vs := v .(string )
88+ log .Printf ("[DEBUG] OCI Image: %s" , vs )
89+ opts .OciImage = & vs
90+ }
91+
7192 if v , ok := d .GetOk ("description" ); ok {
7293 vs := v .(string )
7394 log .Printf ("[DEBUG] description: %s" , vs )
@@ -101,6 +122,75 @@ func resourceHerokuAppReleaseCreate(d *schema.ResourceData, meta interface{}) er
101122 return resourceHerokuAppReleaseRead (d , meta )
102123}
103124
125+ // validateArtifactForAppGeneration validates artifact type (slug_id vs oci_image) against the target app's generation
126+ func validateArtifactForAppGeneration (ctx context.Context , diff * schema.ResourceDiff , v interface {}) error {
127+ hasSlug := diff .Get ("slug_id" ).(string ) != ""
128+ hasOci := diff .Get ("oci_image" ).(string ) != ""
129+
130+ // Get the app ID to check its generation
131+ appID := diff .Get ("app_id" ).(string )
132+ if appID == "" {
133+ // During plan phase, the app_id might not be available yet if the app is being created
134+ // in the same configuration. Skip validation for now - it will be caught at apply time.
135+ return nil
136+ }
137+
138+ config := v .(* Config )
139+ client := config .Api
140+
141+ // Fetch app info to determine its generation
142+ app , err := client .AppInfo (ctx , appID )
143+ if err != nil {
144+ // If we can't fetch the app (it might not exist yet during plan), skip validation
145+ // It will be caught at apply time
146+ return nil
147+ }
148+
149+ // Validate artifact type against app generation
150+ return validateArtifactForGeneration (app .Generation .Name , hasSlug , hasOci )
151+ }
152+
153+ // validateArtifactForGeneration validates artifact type compatibility with a specific generation
154+ func validateArtifactForGeneration (generationName string , hasSlug bool , hasOci bool ) error {
155+ switch generationName {
156+ case "cedar" :
157+ if hasOci {
158+ return fmt .Errorf ("cedar generation apps must use slug_id, not oci_image" )
159+ }
160+ if ! hasSlug {
161+ return fmt .Errorf ("cedar generation apps require slug_id" )
162+ }
163+ case "fir" :
164+ if hasSlug {
165+ return fmt .Errorf ("fir generation apps must use oci_image, not slug_id" )
166+ }
167+ if ! hasOci {
168+ return fmt .Errorf ("fir generation apps require oci_image" )
169+ }
170+ default :
171+ // Unknown generation - let the API handle it
172+ return nil
173+ }
174+
175+ return nil
176+ }
177+
178+ // validateArtifactForApp validates artifact type for the target app at apply time
179+ func validateArtifactForApp (client * heroku.Service , d * schema.ResourceData ) error {
180+ hasSlug := d .Get ("slug_id" ).(string ) != ""
181+ hasOci := d .Get ("oci_image" ).(string ) != ""
182+ appID := d .Get ("app_id" ).(string )
183+
184+ // Fetch app info to determine its generation
185+ app , err := client .AppInfo (context .TODO (), appID )
186+ if err != nil {
187+ return fmt .Errorf ("error fetching app info: %s" , err )
188+ }
189+
190+ // Validate artifact type against app generation
191+ return validateArtifactForGeneration (app .Generation .Name , hasSlug , hasOci )
192+ }
193+
104194func resourceHerokuAppReleaseRead (d * schema.ResourceData , meta interface {}) error {
105195 client := meta .(* Config ).Api
106196
@@ -113,7 +203,20 @@ func resourceHerokuAppReleaseRead(d *schema.ResourceData, meta interface{}) erro
113203 }
114204
115205 d .Set ("app_id" , appRelease .App .ID )
116- d .Set ("slug_id" , appRelease .Slug .ID )
206+
207+ // Handle Cedar releases (with slugs)
208+ if appRelease .Slug != nil {
209+ d .Set ("slug_id" , appRelease .Slug .ID )
210+ }
211+
212+ // Handle Fir releases (with OCI images)
213+ for _ , artifact := range appRelease .Artifacts {
214+ if artifact .Type == "oci-image" {
215+ d .Set ("oci_image" , artifact .ID )
216+ break
217+ }
218+ }
219+
117220 d .Set ("description" , appRelease .Description )
118221
119222 return nil
@@ -168,6 +271,26 @@ func resourceHerokuAppReleaseImport(d *schema.ResourceData, meta interface{}) ([
168271 return []* schema.ResourceData {d }, nil
169272}
170273
274+ func resourceHerokuAppReleaseCustomizeDiff (ctx context.Context , diff * schema.ResourceDiff , v interface {}) error {
275+ hasSlug := diff .Get ("slug_id" ).(string ) != ""
276+ hasOci := diff .Get ("oci_image" ).(string ) != ""
277+
278+ // Must have exactly one artifact type
279+ if ! hasSlug && ! hasOci {
280+ return fmt .Errorf ("either slug_id or oci_image must be provided" )
281+ }
282+ if hasSlug && hasOci {
283+ return fmt .Errorf ("slug_id and oci_image are mutually exclusive" )
284+ }
285+
286+ // Validate artifact type compatibility with app generation during plan phase
287+ if err := validateArtifactForAppGeneration (ctx , diff , v ); err != nil {
288+ return err
289+ }
290+
291+ return nil
292+ }
293+
171294func resourceHerokuAppReleaseV0 () * schema.Resource {
172295 return & schema.Resource {
173296 Schema : map [string ]* schema.Schema {
0 commit comments