| 1 |
{% extends "base.html" %} |
| 2 |
{% import "admin_nav.html" as an %} |
| 3 |
{% import "runner_update_fields.html" as ruf %} |
| 4 |
|
| 5 |
{% block title %}Runner updates · Administration{% endblock %} |
| 6 |
{% block content %} |
| 7 |
<div class="head"><h1>Runners <small><a href="/admin">Administration</a></small></h1></div> |
| 8 |
<div class="settings-layout"> |
| 9 |
{% call an::nav(crate::web::admin::AdminSection::RunnerUpdates) %} |
| 10 |
<div class="sections settings-body"> |
| 11 |
<div class="bar"><span class="modes"><a href="/admin/runners">runners</a><a href="/admin/runners/images">images</a><a href="/admin/runners/settings">settings</a><a href="/admin/runners/updates" class="on">updates</a></span></div> |
| 12 |
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %} |
| 13 |
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %} |
| 14 |
<datalist id="update-channels"><option value="stable"><option value="canary"></datalist> |
| 15 |
<section class="panel"> |
| 16 |
<h2>Runners update themselves</h2> |
| 17 |
<p>A runner asks the hub which build it should run, fetches it, checks it as a release, finishes what it holds and swaps itself in; a build that does not reach the hub goes back to the one before. <strong>Off unless chosen.</strong> The site, an owner, a test group and a runner each may set any of the settings below, and the nearest setting wins: a runner's own, then its test groups', then its owner's, then the site's. A runner's <code>flow.toml</code> can still refuse every update (<code>[runner.update] allow = false</code>).</p> |
| 18 |
<p class="small mute">A rollout: turn updates on for a test group (two chosen runners, or 10% of them), watch it, then widen it to 50%, then turn updates on for everyone. A percentage is taken by a stable hash of each runner's id, so it is the same machines every time, and 10% sits inside 50%.</p> |
| 19 |
</section> |
| 20 |
<form class="card wide" method="post" action="/admin/runners/updates/source"> |
| 21 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 22 |
<h2>Where builds come from</h2> |
| 23 |
<label for="update-repo">The repository whose releases carry runner builds</label> |
| 24 |
<input type="text" id="update-repo" name="repo" value="{{ repo }}" placeholder="owner/name, e.g. andymccall/oxygenflow"> |
| 25 |
<p class="small mute">Each release offers one file per machine, <code>flow-linux-x86_64</code>, <code>flow-linux-aarch64</code>, <code>flow-linux-riscv64</code>, <code>flow-windows-x86_64.exe</code>. <em>stable</em> is its newest release, <em>canary</em> its newest including pre-releases; a version (<code>0.3.4</code>) pins it, going back if need be.</p> |
| 26 |
<h3>Waves</h3> |
| 27 |
<p class="small mute">Give test groups a wave (1, 2, …) and a new version reaches them in order: wave 2's runners are offered it once every runner of wave 1 that took it has finished this many jobs on it with none ending in error, or this many hours have passed since its first job there. A job that ends in error (the runner's or the machine's fault, not a test's) on a wave stops the version for everyone until you lift it. Runners in no wave come last.</p> |
| 28 |
<div class="pol-row"> |
| 29 |
<label class="pol">Jobs each <input type="number" name="soak_jobs" min="1" max="1000" size="4" value="{{ soak_jobs }}"></label> |
| 30 |
<label class="pol">or hours <input type="number" name="soak_hours" min="1" max="720" size="4" value="{{ soak_hours }}"></label> |
| 31 |
</div> |
| 32 |
<button type="submit">Save</button> |
| 33 |
</form> |
| 34 |
{% if !holds.is_empty() %} |
| 35 |
<section class="panel strip warn" id="holds"> |
| 36 |
<h2>Held builds</h2> |
| 37 |
<p class="small">A build that rolled back on a runner, or that a wave stopped, is offered to no other until you lift the hold.</p> |
| 38 |
{% for h in holds %} |
| 39 |
<form class="pol-form" method="post" action="/admin/runners/updates/holds/lift"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="version" value="{{ h.version }}"> |
| 40 |
<strong>flow {{ h.version }}</strong><span class="small">{{ h.why }} · <time title="{{ h.at_ms|when_i }}">{{ h.at_ms|ago_i }}</time></span><button type="submit" class="small">LIFT</button> |
| 41 |
</form> |
| 42 |
{% endfor %} |
| 43 |
</section> |
| 44 |
{% endif %} |
| 45 |
<form class="card wide" method="post" action="/admin/runners/updates/policy"> |
| 46 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="site"> |
| 47 |
<h2>The site</h2> |
| 48 |
<p class="small mute">What every runner does unless a nearer level says otherwise: {{ site.summary() }}.</p> |
| 49 |
<div class="pol-row">{% call ruf::policy_fields(site, true) %}</div> |
| 50 |
<button type="submit">Save</button> |
| 51 |
</form> |
| 52 |
<section class="panel" id="owners"> |
| 53 |
<h2>Owners</h2> |
| 54 |
<p class="small mute">For the runners an account or organisation registered for its own repositories.</p> |
| 55 |
{% if owners.is_empty() %}<p class="mute small">No owner sets anything; their runners follow the site.</p>{% endif %} |
| 56 |
{% for o in owners %} |
| 57 |
<form class="pol-form" method="post" action="/admin/runners/updates/policy"> |
| 58 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="owner"><input type="hidden" name="key" value="{{ o.key }}"> |
| 59 |
<strong>{{ o.key }}</strong>{% call ruf::policy_fields(o, false) %}<button type="submit">Save</button> |
| 60 |
</form> |
| 61 |
{% endfor %} |
| 62 |
<form class="pol-form" method="post" action="/admin/runners/updates/policy"> |
| 63 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="owner"> |
| 64 |
<label class="pol">Owner <input type="text" name="key" size="12" placeholder="account or org" required></label>{% call ruf::policy_fields(site, false) %}<button type="submit">Add</button> |
| 65 |
</form> |
| 66 |
</section> |
| 67 |
<section class="panel" id="groups"> |
| 68 |
<h2>Test groups</h2> |
| 69 |
<p class="small mute">A named set of runners: those you choose, or a percentage of the site's runners (or one owner's). Give a group its own settings to try a build on it before the rest.</p> |
| 70 |
{% if groups.is_empty() %}<p class="mute small">No test groups yet.</p>{% endif %} |
| 71 |
{% for gv in groups %}{% let g = gv.g %} |
| 72 |
<div class="card wide update-group"> |
| 73 |
<h3>{{ g.name }} <small class="mute">{{ gv.reach }} · {% if let Some(p) = g.percent %}{{ p }}%{% else %}chosen{% endif %} · {{ gv.members.len()|plural("runner", "runners") }} now{% if let Some(w) = g.wave %} · wave {{ w }}{% endif %}</small></h3> |
| 74 |
<p class="small">{% if gv.members.is_empty() %}<span class="mute">No runners in it.</span>{% else %}{% for m in gv.members %}<span class="lbl">{{ m }}</span> {% endfor %}{% endif %}</p> |
| 75 |
<form class="pol-form" method="post" action="/admin/runners/updates/policy"> |
| 76 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="group"><input type="hidden" name="key" value="{{ g.id }}"> |
| 77 |
{% call ruf::policy_fields(gv.policy, false) %}<button type="submit">Save settings</button> |
| 78 |
</form> |
| 79 |
<details><summary class="small">Who is in it</summary> |
| 80 |
<form method="post" action="/admin/runners/updates/groups/{{ g.id }}"> |
| 81 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 82 |
<label class="pol"><input type="radio" name="mode" value="percent"{% if g.percent.is_some() %} checked{% endif %}> a percentage of {{ gv.reach }}: <input type="number" name="percent" min="1" max="100" size="4" value="{% if let Some(p) = g.percent %}{{ p }}{% endif %}">%</label> |
| 83 |
<label class="pol"><input type="radio" name="mode" value="chosen"{% if g.percent.is_none() %} checked{% endif %}> these runners:</label> |
| 84 |
<div class="pol-row">{% for rv in runners %}{% if g.owner.is_empty() || g.owner == rv.r.scope_owner %}<label class="pol"><input type="checkbox" name="runner" value="{{ rv.r.id }}"{% if gv.chosen.contains(rv.r.id) %} checked{% endif %}> {{ rv.r.name }}</label>{% endif %}{% endfor %}</div> |
| 85 |
<label class="pol">Wave <input type="number" name="wave" min="1" max="9" size="3" value="{% if let Some(w) = g.wave %}{{ w }}{% endif %}" title="Blank: after every wave"></label> |
| 86 |
<button type="submit">Save members</button> |
| 87 |
</form> |
| 88 |
<form class="tiny" method="post" action="/admin/runners/updates/groups/{{ g.id }}/delete" data-confirm="Remove the test group {{ g.name }}? Its runners follow the levels above it."><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button class="danger-btn">remove group</button></form> |
| 89 |
</details> |
| 90 |
</div> |
| 91 |
{% endfor %} |
| 92 |
<form class="card wide" method="post" action="/admin/runners/updates/groups"> |
| 93 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"> |
| 94 |
<h3>New test group</h3> |
| 95 |
<div class="pol-row"> |
| 96 |
<label class="pol">Name <input type="text" name="name" size="14" required placeholder="canary"></label> |
| 97 |
<label class="pol">Of <input type="text" name="owner" size="12" placeholder="the site's runners" title="Blank for the site's runners, or an owner for theirs"></label> |
| 98 |
</div> |
| 99 |
<label class="pol"><input type="radio" name="mode" value="percent"> a percentage: <input type="number" name="percent" min="1" max="100" size="4">%</label> |
| 100 |
<label class="pol"><input type="radio" name="mode" value="chosen" checked> these runners:</label> |
| 101 |
<div class="pol-row">{% for rv in runners %}<label class="pol"><input type="checkbox" name="runner" value="{{ rv.r.id }}"> {{ rv.r.name }}</label>{% endfor %}</div> |
| 102 |
<label class="pol">Wave <input type="number" name="wave" min="1" max="9" size="3" title="Blank: after every wave"></label> |
| 103 |
<button type="submit">Make group</button> |
| 104 |
</form> |
| 105 |
</section> |
| 106 |
<section class="panel" id="runners"> |
| 107 |
<h2>Runners</h2> |
| 108 |
{% if runners.is_empty() %}<p class="mute">No runners yet.</p>{% else %} |
| 109 |
<div class="table"><table> |
| 110 |
<tr><th>Runner</th><th>Runs</th><th>Groups</th><th>Its policy</th><th>Now</th><th>Last said</th><th>Its own settings</th></tr> |
| 111 |
{% for rv in runners %}{% let r = rv.r %} |
| 112 |
<tr> |
| 113 |
<td><a href="/admin/runners/{{ r.id }}"><strong>{{ r.name }}</strong></a>{% if !r.scope_owner.is_empty() %}<br><span class="small mute">{{ r.scope_owner }}'s</span>{% endif %}</td> |
| 114 |
<td class="small">{% if r.version.is_empty() %}<span class="mute">unknown</span>{% else %}flow {{ r.version }}{% endif %}</td> |
| 115 |
<td class="small">{% for g in rv.groups %}<span class="lbl">{{ g }}</span> {% endfor %}</td> |
| 116 |
<td class="small">{{ rv.effective_words() }}</td> |
| 117 |
<td class="small">{{ rv.offer }}</td> |
| 118 |
<td class="small">{% if r.update_note.is_empty() %}<span class="mute">nothing yet</span>{% else %}{{ r.update_note }}{% if let Some(t) = r.update_ms %} <time class="mute" title="{{ t|when_i }}">{{ t|ago_i }}</time>{% endif %}{% endif %}</td> |
| 119 |
<td><form class="pol-form tiny" method="post" action="/admin/runners/updates/policy"> |
| 120 |
<input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="scope" value="runner"><input type="hidden" name="key" value="{{ r.id }}"> |
| 121 |
{% call ruf::policy_fields(rv.policy, false) %}<button type="submit">set</button></form></td> |
| 122 |
</tr> |
| 123 |
{% endfor %} |
| 124 |
</table></div> |
| 125 |
{% endif %} |
| 126 |
</section> |
| 127 |
</div> |
| 128 |
</div> |
| 129 |
{% endblock %} |