<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
<pclass="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>
<pclass="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>
<pclass="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
<divclass="pol-row">
29
<labelclass="pol">Jobs each <inputtype="number"name="soak_jobs"min="1"max="1000"size="4"value="{{ soak_jobs }}"></label>
<pclass="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() %}<pclass="mute small">No test groups yet.</p>{% endif %}
<pclass="small">{% if gv.members.is_empty() %}<spanclass="mute">No runners in it.</span>{% else %}{% for m in gv.members %}<spanclass="lbl">{{ m }}</span> {% endfor %}{% endif %}</p>
<labelclass="pol"><inputtype="radio"name="mode"value="percent"{% ifg.percent.is_some() %} checked{% endif %}> a percentage of {{ gv.reach }}: <inputtype="number"name="percent"min="1"max="100"size="4"value="{% if let Some(p) = g.percent %}{{ p }}{% endif %}">%</label>
83
<labelclass="pol"><inputtype="radio"name="mode"value="chosen"{% ifg.percent.is_none() %} checked{% endif %}> these runners:</label>
84
<divclass="pol-row">{% for rv in runners %}{% if g.owner.is_empty() || g.owner == rv.r.scope_owner %}<labelclass="pol"><inputtype="checkbox"name="runner"value="{{ rv.r.id }}"{% ifgv.chosen.contains(rv.r.id) %} checked{% endif %}> {{ rv.r.name }}</label>{% endif %}{% endfor %}</div>
<labelclass="pol">Wave <inputtype="number"name="wave"min="1"max="9"size="3"value="{% if let Some(w) = g.wave %}{{ w }}{% endif %}"title="Blank: after every wave"></label>
<formclass="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."><inputtype="hidden"name="_csrf"value="{{ shell.csrf }}"><buttonclass="danger-btn">remove group</button></form>
<labelclass="pol">Of <inputtype="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
<labelclass="pol"><inputtype="radio"name="mode"value="percent"> a percentage: <inputtype="number"name="percent"min="1"max="100"size="4">%</label>
100
<labelclass="pol"><inputtype="radio"name="mode"value="chosen"checked> these runners:</label>
101
<divclass="pol-row">{% for rv in runners %}<labelclass="pol"><inputtype="checkbox"name="runner"value="{{ rv.r.id }}"> {{ rv.r.name }}</label>{% endfor %}</div>