| 1 | {{define "app_releases"}}{{template "top" .}} |
| 2 | {{if .Flash}}<p class="banner">{{.Flash}}</p>{{end}} |
| 3 | {{if .FlashErr}}<p class="banner error">{{.FlashErr}}</p>{{end}} |
| 4 | {{template "appnav" .}} |
| 5 | <nav class="tabs" aria-label="{{.DeploymentsCopy.Title}}"> |
| 6 | <a href="{{.Nav}}/apps/{{.AppName}}/deployments">{{.DeploymentsCopy.Title}}</a> |
| 7 | <a href="{{.Nav}}/apps/{{.AppName}}/releases" aria-current="page">{{.DeploymentsCopy.History}}</a> |
| 8 | </nav> |
| 9 | <h2>{{.DeploymentsCopy.History}}</h2> |
| 10 | {{template "accountreviewbanner" .}} |
| 11 | |
| 12 | {{with .ReleaseSummary}} |
| 13 | <section class="pagehead" aria-label="Release status"> |
| 14 | <h2>{{$.StatusCopy.ObservedLabel}}</h2> |
| 15 | {{if not .Deployment.HasRoutes}} |
| 16 | <p>{{$.StatusCopy.NoInstances}}</p> |
| 17 | {{else if not .Deployment.HasReportable}} |
| 18 | <p>—</p> |
| 19 | {{else if not .Deployment.HasReportedVersion}} |
| 20 | <p>{{$.StatusCopy.NoVersion}}</p> |
| 21 | {{else}} |
| 22 | <p>{{.Deployment.ReportingText}}</p> |
| 23 | <ul>{{range .Deployment.Versions}}<li><code>{{.Version}}</code></li>{{end}}</ul> |
| 24 | {{if .Deployment.UnknownCount}}<p>{{$.StatusCopy.Partial}}</p>{{end}} |
| 25 | {{end}} |
| 26 | {{if .NextVersion}} |
| 27 | <p>{{.CandidateText}}</p> |
| 28 | {{if and $.CanPromote $.IsOwner}} |
| 29 | <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/promote"> |
| 30 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 31 | <input type="hidden" name="channel" value="stable"> |
| 32 | <input type="hidden" name="version" value="{{.NextVersion}}"> |
| 33 | <input type="hidden" name="back" value="releases"> |
| 34 | <button type="submit" class="btn">Make {{.NextVersion}} live</button> |
| 35 | </form> |
| 36 | {{else if $.CanPromote}} |
| 37 | <p class="small">Only an account owner can make it live.</p> |
| 38 | {{end}} |
| 39 | {{end}} |
| 40 | </section> |
| 41 | {{end}} |
| 42 | |
| 43 | {{if .RetentionKeep}} |
| 44 | <p class="small">Retention: the newest {{.RetentionKeep}} releases are kept; older releases not promoted, tagged, or recently shipped are pruned automatically.</p> |
| 45 | {{end}} |
| 46 | |
| 47 | {{if .Versions}} |
| 48 | <details class="reveal" open> |
| 49 | <summary>All versions ({{len .Versions}})</summary> |
| 50 | <div class="tablewrap"> |
| 51 | <table> |
| 52 | <tr><th>Version</th><th>Label</th><th>Kind</th><th>Built</th>{{if and .CanPromote (not .Detail.OneChannelPipeline)}}<th>Promote</th>{{end}}</tr> |
| 53 | {{range .Versions}} |
| 54 | <tr id="version-{{.Version}}"> |
| 55 | <td><code>{{.Version}}</code>{{if .Tag}} <span class="chip chip-env">{{.Tag}}</span>{{end}}</td> |
| 56 | {{if .Err}} |
| 57 | <td colspan="3" class="error">{{.Err}}</td> |
| 58 | {{else}} |
| 59 | <td>{{if .Label}}{{.Label}}{{end}}{{if .Notes}} |
| 60 | <details><summary>notes</summary><pre class="changelog">{{.Notes}}</pre></details> |
| 61 | {{end}}{{if and (not .Label) (not .Notes)}}—{{end}}</td> |
| 62 | <td>{{.Kind}}</td> |
| 63 | <td>{{if .BuiltAt}}{{.BuiltAt}}{{else}}—{{end}}</td> |
| 64 | {{end}} |
| 65 | {{if and $.CanPromote (not $.Detail.OneChannelPipeline)}}<td> |
| 66 | <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/promote"> |
| 67 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 68 | <input type="hidden" name="version" value="{{.Version}}"> |
| 69 | <input type="hidden" name="back" value="releases"> |
| 70 | <select name="channel" aria-label="Channel to promote {{.Version}} to"> |
| 71 | {{range $.PromoteChannels}}<option value="{{.}}">{{.}}</option>{{end}} |
| 72 | </select> |
| 73 | <button type="submit" class="btn-sm">Promote</button> |
| 74 | </form> |
| 75 | </td>{{end}} |
| 76 | </tr> |
| 77 | {{end}} |
| 78 | </table> |
| 79 | </div> |
| 80 | </details> |
| 81 | {{else}} |
| 82 | <p>Nothing shipped yet. Ship your first release from your repo:</p> |
| 83 | <pre><code>carlos ship --app {{.Detail.App}} --version $(git rev-parse --short HEAD) --target linux-arm64 <binary> |
| 84 | carlos ship --app {{.Detail.App}} --kind static --version v1 <site-dir></code></pre> |
| 85 | <p class="small">Releases appear here once shipped, and get promoted to channels from this page.</p> |
| 86 | {{end}} |
| 87 | |
| 88 | {{if .Detail.OneChannelPipeline}} |
| 89 | <details class="reveal"> |
| 90 | <summary>Test versions and rolling back</summary> |
| 91 | {{template "solorollback" .}} |
| 92 | {{template "canaries" .}} |
| 93 | {{template "apptags" .}} |
| 94 | </details> |
| 95 | {{else}} |
| 96 | <details class="reveal"> |
| 97 | <summary>Release pipeline — channels, test versions, and the promotion ladder</summary> |
| 98 | {{template "ladder" .}} |
| 99 | |
| 100 | <h2>Channels</h2> |
| 101 | {{if .Detail.Channels}} |
| 102 | <div class="tablewrap"> |
| 103 | <table> |
| 104 | <tr><th>Channel</th><th>Version</th><th>Promoted</th><th>Goes live</th><th></th></tr> |
| 105 | {{range .Detail.Channels}} |
| 106 | <tr> |
| 107 | <td>{{.Name}}</td> |
| 108 | {{if .Err}} |
| 109 | <td colspan="3" class="error">{{.Err}}</td> |
| 110 | {{else}} |
| 111 | <td><code>{{.Version}}</code>{{if .Tag}} <span class="chip chip-env">{{.Tag}}</span>{{end}}</td> |
| 112 | <td><span title="{{fmtTime .PromotedAt}}">{{ago .PromotedAt}}</span></td> |
| 113 | <td>{{if .BakedUntil.IsZero}}live now{{else}}in {{until .BakedUntil}}{{end}}{{if .FastTrack}} · delay skipped{{end}}</td> |
| 114 | {{end}} |
| 115 | <td>{{if and $.CanPromote .HasHistory (not .Err) (or $.Detail.Pipeline $.IsOwner (ne .Name "stable"))}} |
| 116 | <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/rollback"> |
| 117 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 118 | <input type="hidden" name="channel" value="{{.Name}}"> |
| 119 | <input type="hidden" name="back" value="releases"> |
| 120 | <button type="submit" class="btn-sm btn-danger">Roll back</button> |
| 121 | </form> |
| 122 | {{end}}</td> |
| 123 | </tr> |
| 124 | {{end}} |
| 125 | </table> |
| 126 | </div> |
| 127 | <p class="small">Go-live times are estimates — each copy keeps its own clock. Rollbacks skip the delay.</p> |
| 128 | {{else}}<p class="small">No channels yet — ship a release, then promote it below to create the first one.</p>{{end}} |
| 129 | |
| 130 | {{template "canaries" .}} |
| 131 | {{template "apptags" .}} |
| 132 | </details> |
| 133 | {{end}} |
| 134 | {{if .Detail.OneChannelPipeline}} |
| 135 | <p class="small">This app has one channel — <code>carlos deploy</code> ships straight to it, with no |
| 136 | promotion step in between. <a href="{{.Nav}}/apps/{{.Detail.App}}/pipeline">Set up a pipeline</a> to |
| 137 | add more.</p> |
| 138 | {{end}} |
| 139 | {{if not .CanPromote}}<p class="small">Deploys are unavailable on this console — it has no signing key.</p>{{end}} |
| 140 | {{template "bottom" .}}{{end}} |
| 141 | |
| 142 | {{/* The canary section and the tag list are NOT channel vocabulary, so both |
| 143 | app shapes get them: canary/* lives outside the pipeline entirely and is |
| 144 | always allowed (release pipelines v2 §2), and a tag names a release, not |
| 145 | a rung. Shared through one definition so the two shapes cannot drift into |
| 146 | different canary controls. */}} |
| 147 | {{define "canaries"}}<h2>Canaries</h2> |
| 148 | {{if .Detail.Canaries}} |
| 149 | <div class="tablewrap"> |
| 150 | <table> |
| 151 | <tr><th>Name</th><th>Channel</th><th>Version</th><th>Created</th><th>URL</th><th></th></tr> |
| 152 | {{range .Detail.Canaries}} |
| 153 | <tr> |
| 154 | <td>{{.Name}}</td> |
| 155 | <td><code>{{.Channel}}</code></td> |
| 156 | {{if .Err}} |
| 157 | <td colspan="3" class="error">{{.Err}}</td> |
| 158 | {{else}} |
| 159 | <td><code>{{.Version}}</code></td> |
| 160 | <td><span title="{{fmtTime .PromotedAt}}">{{ago .PromotedAt}}</span></td> |
| 161 | <td>{{if .Hosts}}{{range .Hosts}}<a href="https://{{.}}">{{.}}</a> {{end}}{{else}}no host yet{{end}}</td> |
| 162 | {{end}} |
| 163 | <td>{{if $.CanPromote}} |
| 164 | <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/canary/retire"> |
| 165 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 166 | <input type="hidden" name="name" value="{{.Name}}"> |
| 167 | <input type="hidden" name="back" value="releases"> |
| 168 | <button type="submit" class="btn-sm btn-danger">Retire</button> |
| 169 | </form> |
| 170 | {{end}}</td> |
| 171 | </tr> |
| 172 | {{end}} |
| 173 | </table> |
| 174 | </div> |
| 175 | {{else}}<p class="small">No test versions — a test version is a private copy serving one chosen build immediately, with no safety delay.</p>{{end}} |
| 176 | {{if and .CanPromote .Versions}} |
| 177 | <form class="signin" method="post" action="{{.Base}}/apps/{{.Detail.App}}/canary"> |
| 178 | <input type="hidden" name="csrf" value="{{.CSRF}}"> |
| 179 | <input type="hidden" name="back" value="releases"> |
| 180 | <input type="text" name="name" placeholder="rc-2"> |
| 181 | <select name="version"> |
| 182 | {{range .Versions}}{{if not .Err}}<option value="{{.Version}}">{{.Version}}</option>{{end}}{{end}} |
| 183 | </select> |
| 184 | <button type="submit" class="btn-sm">Create canary</button> |
| 185 | </form> |
| 186 | <p class="small">Creates <code>canary/<name></code> — a test version serving the chosen build immediately, no delay. It gets its own URL from an address that follows it:</p> |
| 187 | {{if .AttachEnable}}<pre><code>{{.AttachEnable}} |
| 188 | {{.AttachCmd}}</code></pre> |
| 189 | <p class="small">This app has no addresses yet, so the first line sets one up under this account's platform subdomain. It comes live within a couple of minutes.</p> |
| 190 | {{else}}<pre><code>{{.AttachCmd}}</code></pre> |
| 191 | <p class="small">The address comes live within a couple of minutes.</p> |
| 192 | {{end}} |
| 193 | {{end}}{{end}} |
| 194 | |
| 195 | {{define "apptags"}}{{if .Tags}} |
| 196 | <h2>Tags</h2> |
| 197 | {{range .Tags}} |
| 198 | <details> |
| 199 | <summary><code>{{.Version}}</code> — <code>{{.Release}}</code>{{if .Actor}} · {{.Actor}}{{end}}</summary> |
| 200 | <pre class="changelog">{{.Changelog}}</pre> |
| 201 | </details> |
| 202 | {{end}} |
| 203 | {{end}}{{end}} |
| 204 | |
| 205 | {{/* The one-channel app's rollback control. A rollback is not channel |
| 206 | vocabulary — it is the pointer write that puts the previous build back, |
| 207 | and a one-channel app is exactly the app with no promotion step to fall |
| 208 | back on when a bad build lands. It names no channel on screen (the name |
| 209 | rides in the hidden field the door needs), draws no table and offers no |
| 210 | promote; the ladder stays hidden. RollbackChannels is empty until the |
| 211 | channel actually has somewhere to go back to. */}} |
| 212 | {{define "solorollback"}}{{if .CanPromote}}{{with .Detail.RollbackChannels}} |
| 213 | <h2>Rolling back</h2> |
| 214 | <p class="small">Puts the previous build back in front of everyone straight away — rollbacks skip the safety delay. You'll be asked to confirm, and told which build it lands on.</p> |
| 215 | {{range .}} |
| 216 | <form method="post" action="{{$.Base}}/apps/{{$.Detail.App}}/rollback"> |
| 217 | <input type="hidden" name="csrf" value="{{$.CSRF}}"> |
| 218 | <input type="hidden" name="channel" value="{{.Name}}"> |
| 219 | <input type="hidden" name="back" value="releases"> |
| 220 | <button type="submit" class="btn-sm btn-danger">Roll back</button> |
| 221 | </form> |
| 222 | {{end}} |
| 223 | {{end}}{{end}}{{end}} |
| 224 | |