OXYGENHUB
# 806615f974c0 oxygenhub / templates / admin_runner_updates.html
10.7 KiB · HTML · last changed Runner updates in waves: a later wave takes a version once the waves before it proved it; an error stops it (refs andymccall/oxygenflow#84) 8 days ago source blame history raw
8501fd58 Andy McCall ↶ 1
{% extends "base.html" %}
2
{% import "admin_nav.html" as an %}
dc5c6429 Andy McCall ↶ 3
{% import "runner_update_fields.html" as ruf %}
4
8501fd58 Andy McCall ↶ 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">
b6dfcb61 Andy McCall ↶ 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>
1ec29691 Andy McCall ↶ 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>
8501fd58 Andy McCall ↶ 32
  <button type="submit">Save</button>
33
</form>
faf5246c Andy McCall ↶ 34
{% if !holds.is_empty() %}
35
<section class="panel strip warn" id="holds">
36
  <h2>Held builds</h2>
1ec29691 Andy McCall ↶ 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>
faf5246c Andy McCall ↶ 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 }}">
1ec29691 Andy McCall ↶ 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>
faf5246c Andy McCall ↶ 41
  </form>
42
  {% endfor %}
43
</section>
44
{% endif %}
8501fd58 Andy McCall ↶ 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>
dc5c6429 Andy McCall ↶ 49
  <div class="pol-row">{% call ruf::policy_fields(site, true) %}</div>
8501fd58 Andy McCall ↶ 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 }}">
dc5c6429 Andy McCall ↶ 59
    <strong>{{ o.key }}</strong>{% call ruf::policy_fields(o, false) %}<button type="submit">Save</button>
8501fd58 Andy McCall ↶ 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">
dc5c6429 Andy McCall ↶ 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>
8501fd58 Andy McCall ↶ 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">
1ec29691 Andy McCall ↶ 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>
8501fd58 Andy McCall ↶ 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 }}">
dc5c6429 Andy McCall ↶ 77
      {% call ruf::policy_fields(gv.policy, false) %}<button type="submit">Save settings</button>
8501fd58 Andy McCall ↶ 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>
1ec29691 Andy McCall ↶ 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>
8501fd58 Andy McCall ↶ 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>
1ec29691 Andy McCall ↶ 102
    <label class="pol">Wave <input type="number" name="wave" min="1" max="9" size="3" title="Blank: after every wave"></label>
8501fd58 Andy McCall ↶ 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 }}">
dc5c6429 Andy McCall ↶ 121
      {% call ruf::policy_fields(rv.policy, false) %}<button type="submit">set</button></form></td>
8501fd58 Andy McCall ↶ 122
  </tr>
123
  {% endfor %}
124
  </table></div>
125
  {% endif %}
126
</section>
127
</div>
128
</div>
129
{% endblock %}