OXYGENHUB
◆ v0.9.7 oxygenhub / templates / repo_settings.html
33.4 KiB · HTML · last changed A push reuses a job's pass on the same state (closes #265) 15 days ago source blame history raw
1
{% extends "base.html" %}
2
{% import "webhooks_panel.html" as w %}
3
{% import "assistants_panel.html" as ai %}
4
{% import "variables_panel.html" as va %}
5
{% import "secrets_panel.html" as se %}
6
{% import "repo_tabs.html" as m %}
7
{% block title %}{% if section != Section::General %}{{ section.title() }} · {% endif %}Settings · {{ repo.owner }}/{{ repo.name }}{% endblock %}
8
{% block bodyattrs %} data-repo="{{ repo.url }}"{% endblock %}
9
{% block tabs %}{% call m::tabs(repo, "settings") %}{% endblock %}
10
{% block content %}
11
<div class="head"><h1>Settings <small>{{ repo.owner }}/{{ repo.name }}</small></h1></div>
12
<div class="settings-layout">
13
<nav class="settings-nav" aria-label="Settings"{% if section == Section::General %} data-settings-anchors{% endif %}>
14
  {% for (group, pages) in nav %}<div class="settings-group">
15
    <h2>{{ group }}</h2>
16
    <ul>{% for p in pages %}<li><a href="{{ repo.url }}/settings/{{ p.slug() }}" data-slug="{{ p.slug() }}"{% if *p == section %} class="on" aria-current="page"{% endif %}>{{ p.title() }}</a></li>{% endfor %}</ul>
17
  </div>{% endfor %}
18
</nav>
19
<div class="sections settings-body">
20
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %}
21
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %}
22
{% match section %}
23
{% when Section::General %}
24
<form class="card wide" id="general" method="post" action="{{ repo.url }}/settings">
25
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
26
  <h2>General</h2>
27
  <label for="description">Description</label><input type="text" id="description" name="description" value="{{ row.description }}">
28
  <label for="topics">Topics</label><input type="text" id="topics" name="topics" value="{{ row.topics }}" placeholder="rust version-control retro">
29
  <p class="small mute">Separated by spaces or commas; letters, digits and dashes. People find repositories by them under Explore.</p>
30
  <label for="default_bookmark">Default bookmark</label><input type="text" id="default_bookmark" name="default_bookmark" value="{{ row.default_bookmark }}">
31
  <label>Visibility{% if role != crate::auth::Role::Admin %} <span class="mute">(admins only)</span>{% endif %}</label>
32
  <div class="choice">
33
    <label><input type="radio" name="visibility" value="public"{% if row.visibility == "public" %} checked{% endif %}> public</label>
34
    <label><input type="radio" name="visibility" value="internal"{% if row.visibility == "internal" %} checked{% endif %}> internal</label>
35
    <label><input type="radio" name="visibility" value="private"{% if row.visibility == "private" %} checked{% endif %}> private</label>
36
  </div>
37
  <button type="submit">SAVE</button>
38
</form>
39
{% when Section::Landing %}
40
<form class="card wide" id="landing" method="post" action="{{ repo.url }}/settings/requirements">
41
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
42
  <h2>Landing requirements</h2>
43
  <label for="required_approvals">Sealed approvals required</label><input type="number" id="required_approvals" name="required_approvals" min="0" value="{{ requirements.required_approvals }}">
44
  <div class="choice">
45
    <label><input type="checkbox" name="require_owners" value="1"{% if requirements.require_owners %} checked{% endif %}> code owners from <code>.oxyowners</code> must approve</label>
46
    <label><input type="checkbox" name="allow_conflicts" value="1"{% if requirements.allow_conflicts %} checked{% endif %}> allow landing with conflicts</label>
47
  </div>
48
  <button type="submit">SAVE</button>
49
</form>
50
{% when Section::PushProtection %}
51
<section class="panel" id="push-protection">
52
  <h2>Push protection</h2>
53
  <p>A push that adds what looks like a credential (cloud keys, tokens, private keys) is refused before any of it is stored, naming the file, line and a fingerprint for each. Only what a push adds is scanned, up to a budget; the push log says when a large push was not scanned in full. What the repository already holds is scanned in the background (Found in history, below).</p>
54
  <form class="card" method="post" action="{{ repo.url }}/settings/push-protection">
55
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
56
    <label class="check"><input type="checkbox" name="protect"{% if row.push_protection %} checked{% endif %}> Refuse pushes that add possible secrets</label>
57
    <button type="submit">SAVE</button>
58
  </form>
59
  <h3>Recent findings</h3>
60
  {% if push_findings.is_empty() %}<p class="mute small">No refused push is waiting on a decision.</p>{% else %}
61
  <p class="small mute">From refused pushes, newest first. Allow a finding only when it is not a real secret (an example key, a test fixture): a push adding that same value then passes.</p>
62
  <table class="small">
63
  <tr><th>When</th><th>Who</th><th>Where</th><th>What</th><th>Fingerprint</th><th>Allow, because…</th></tr>
64
  {% for f in push_findings %}
65
  <tr><td><time title="{{ f.at_ms|when_i }}">{{ f.at_ms|ago_i }}</time></td><td>{% if f.pusher.is_empty() %}<span class="mute">anonymous</span>{% else %}{{ f.pusher }}{% endif %}{% if !f.via.is_empty() %} <span class="mute">via {{ f.via }}</span>{% endif %}</td><td class="px">{{ f.path }}:{{ f.line }}</td><td>{{ f.description }}<br><code class="mute">{{ f.excerpt }}</code></td><td><code>{{ f.fingerprint }}</code></td>
66
  <td><form class="tiny" method="post" action="{{ repo.url }}/settings/secret-bypass"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="fingerprint" value="{{ f.fingerprint }}"><input type="text" name="reason" aria-label="Reason" placeholder="not a secret: …" required><button type="submit">allow</button></form></td></tr>
67
  {% endfor %}
68
  </table>{% endif %}
69
  <form class="card" method="post" action="{{ repo.url }}/settings/secret-bypass">
70
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
71
    <div class="row">
72
      <div><label for="bypass-fingerprint">Fingerprint</label><input type="text" id="bypass-fingerprint" name="fingerprint" placeholder="16 hex digits from the refusal" required></div>
73
      <div><label for="bypass-reason">Reason</label><input type="text" id="bypass-reason" name="reason" placeholder="not a secret: …" required></div>
74
    </div>
75
    <button type="submit">ALLOW</button>
76
  </form>
77
  <h3 id="history-scan">Found in history</h3>
78
  <p class="small mute">Every change reachable from this repository's bookmarks is scanned with the same rules in the background, a budget at a time: its whole history once, then what each push adds.
79
  {% if !history_scan_on %}The history scan is off on this hub (<code>history_scan_limit = 0</code> under <code>[limits]</code>).
80
  {% else if !row.push_protection %}History is not scanned while push protection is off.
81
  {% else if let Some(s) = history_scan %}{% if s.requested_ms.is_some() && s.target_heads.is_empty() %}A scan of the whole history is waiting to start.{% else if !s.target_heads.is_empty() %}Scanning: {{ s.position }} of {{ s.pending }} changes{% if s.requested_ms.is_some() %} (asked for){% endif %}.{% else if let Some(t) = s.finished_ms %}Scanned up to the bookmarks <time title="{{ t|when_i }}">{{ t|ago_i }}</time>.{% else %}Scanned up to the bookmarks.{% endif %} {{ s.changes }} changes ({{ s.bytes|bytes }}) scanned in all.{% if !s.error.is_empty() %} The last pass failed: {{ s.error }}{% endif %}
82
  {% else %}Not scanned yet: the scan reaches every repository soon after the hub starts.{% endif %}</p>
83
  {% if history_scan_on && row.push_protection %}<form class="tiny" method="post" action="{{ repo.url }}/settings/secret-scan"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit">SCAN HISTORY NOW</button></form>{% endif %}
84
  {% if history_findings.is_empty() %}<p class="mute small">Nothing open: no possible secret was found in history, or each was allowed or resolved.</p>{% else %}
85
  <p class="small mute">Values already in history: removing them from the files now leaves them in every earlier change, so a real one must be <strong>revoked or rotated</strong>, then marked resolved, with how. One that is not a secret (an example key, a test fixture) can be allowed instead, as for a push.</p>
86
  <table class="small">
87
  <tr><th>Where</th><th>What</th><th>Fingerprint</th><th>Found</th><th>Allow, because…</th><th>Resolved, because…</th></tr>
88
  {% for f in history_findings %}
89
  <tr><td class="px">{{ f.path }}:{{ f.line }}{% if !f.change_hash.is_empty() %}<br><span class="mute">in</span> <a class="chip" href="{{ repo.url }}/change/{{ f.change_hash }}">{{ f.change_hash|short }}</a>{% endif %}{% if !f.bookmark.is_empty() %} <span class="mute">on</span> {{ f.bookmark }}{% endif %}</td><td>{{ f.description }}<br><code class="mute">{{ f.excerpt }}</code></td><td><code>{{ f.fingerprint }}</code></td><td><time title="{{ f.at_ms|when_i }}">{{ f.at_ms|ago_i }}</time></td>
90
  <td><form class="tiny" method="post" action="{{ repo.url }}/settings/secret-bypass"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="fingerprint" value="{{ f.fingerprint }}"><input type="text" name="reason" aria-label="Reason to allow" placeholder="not a secret: …" required><button type="submit">allow</button></form></td>
91
  <td><form class="tiny" method="post" action="{{ repo.url }}/settings/secret-resolve"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="finding" value="{{ f.id }}"><input type="text" name="reason" aria-label="How it was resolved" placeholder="revoked: …" required><button type="submit">mark resolved</button></form></td></tr>
92
  {% endfor %}
93
  </table>{% endif %}
94
  {% if !resolved_findings.is_empty() %}
95
  <details><summary class="small">Resolved ({{ resolved_findings.len() }})</summary>
96
  <table class="small">
97
  <tr><th>Where</th><th>What</th><th>Fingerprint</th><th>How</th><th>By</th><th>When</th></tr>
98
  {% for f in resolved_findings %}
99
  <tr><td class="px">{{ f.path }}:{{ f.line }}{% if !f.change_hash.is_empty() %} <a class="chip" href="{{ repo.url }}/change/{{ f.change_hash }}">{{ f.change_hash|short }}</a>{% endif %}</td><td>{{ f.description }}</td><td><code>{{ f.fingerprint }}</code></td><td>{{ f.resolved_reason }}</td><td>{{ f.resolved_by }}</td><td>{% if let Some(t) = f.resolved_ms %}<time title="{{ t|when_i }}">{{ t|ago_i }}</time>{% endif %}</td></tr>
100
  {% endfor %}
101
  </table></details>{% endif %}
102
  {% if !secret_bypasses.is_empty() %}
103
  <h3>Allowed</h3>
104
  <table class="small">
105
  <tr><th>Fingerprint</th><th>Rule</th><th>Reason</th><th>By</th><th>When</th><th></th></tr>
106
  {% for b in secret_bypasses %}
107
  <tr><td><code>{{ b.fingerprint }}</code></td><td>{{ b.rule }}</td><td>{{ b.reason }}</td><td>{{ b.by_name }}</td><td><time title="{{ b.created_ms|when_i }}">{{ b.created_ms|ago_i }}</time></td>
108
  <td><form class="tiny" method="post" action="{{ repo.url }}/settings/secret-bypass/remove"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><input type="hidden" name="fingerprint" value="{{ b.fingerprint }}"><button type="submit" title="Refuse pushes adding this value again">remove</button></form></td></tr>
109
  {% endfor %}
110
  </table>{% endif %}
111
</section>
112
{% when Section::GitMirror %}
113
<form class="card wide" id="git-mirror" method="post" action="{{ repo.url }}/settings/mirror">
114
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
115
  <h2>Git mirror</h2>
116
  <div class="choice"><label><input type="checkbox" name="enabled" value="1"{% if row.git_mirror %} checked{% endif %}> keep a read-only Git mirror of <strong>{{ row.default_bookmark }}</strong></label></div>
117
  <p class="small mute">For tools that only speak Git (CI runners, IDEs): <code>git clone {{ shell.base_url }}{{ repo.url }}.git</code>. Private repositories take a personal access token as the password. Pushing to the mirror is refused.{% if row.git_mirror && !row.git_mirror_state.is_empty() %} Last exported state <code>{{ row.git_mirror_state|short }}</code>.{% endif %}</p>
118
  <button type="submit">SAVE</button>
119
</form>
120
{% when Section::Checks %}
121
<form class="card wide" id="checks" method="post" action="{{ repo.url }}/settings/checks">
122
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
123
  <h2>Required checks</h2>
124
  <label for="required_checks">Check names that must pass on a proposal's projected state (comma-separated)</label><input type="text" id="required_checks" name="required_checks" value="{{ required_checks }}" placeholder="build, test">
125
  <p class="small mute">Any token with <code>checks:write</code> reports checks with <code>POST {{ shell.base_url }}/api/v1/repos/{{ repo.owner }}/{{ repo.name }}/checks</code>.</p>
126
  <button type="submit">SAVE</button>
127
</form>
128
{% when Section::Pipelines %}
129
<form class="card wide" id="flow" method="post" action="{{ repo.url }}/settings/flow">
130
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
131
  <h2>OxygenFlow</h2>
132
  <p>Pushes to this repository {% if flow_enabled %}<strong>start runs</strong>{% else %}<strong>do not start runs</strong>{% endif %} of its <code>.flow/pipeline.kdl</code>.</p>
133
  {% if site_admin || own_runner || row.flow == "own" %}
134
  <div class="choice">
135
    {% if site_admin %}<label><input type="radio" name="flow" value="on"{% if row.flow == "on" %} checked{% endif %}> on, any runner (the site's too)</label>{% endif %}
136
    {% if site_admin || own_runner %}<label><input type="radio" name="flow" value="own"{% if row.flow == "own" %} checked{% endif %}> on, own runners only</label>{% endif %}
137
    <label><input type="radio" name="flow" value="off"{% if row.flow == "off" %} checked{% endif %}> off</label>
138
    {% if site_admin %}<label><input type="radio" name="flow" value="default"{% if row.flow.is_empty() %} checked{% endif %}> site default ({% if flow_default %}on{% else %}off{% endif %}, from <code>[flow] repos</code> in <code>hub.toml</code>)</label>{% endif %}
139
  </div>
140
  <p class="small mute"><strong>Own runners only</strong>: jobs go only to runners registered for this repository or its owner (Runners), never to the site's. Only site administrators let the site's runners take them. It takes effect at the next push.</p>
141
  <button type="submit">SAVE</button>
142
  {% else %}
143
  <p class="small mute">To run pipelines on your own machines, register a runner for this repository under {% if role == crate::auth::Role::Admin %}<a href="{{ repo.url }}/settings/runners">Runners</a>{% else %}Runners{% endif %}, then choose <strong>on, own runners only</strong> here. A site administrator can also let the site's runners take its jobs.</p>
144
  {% endif %}
145
</form>
146
{% when Section::Runs %}
147
<form class="card wide" id="flow-priority" method="post" action="{{ repo.url }}/settings/flow-priority">
148
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
149
  <h2>OxygenFlow priority</h2>
150
  <p>Runners take higher levels first; within a level, repositories take turns. To rush one run (an incident's fix), expedite it on its page.</p>
151
  <select name="priority" aria-label="Priority">
152
    <option value="default"{% if row.flow_priority.is_empty() %} selected{% endif %}>the default (the organisation's or the site's)</option>
153
    {% if site_admin %}<option value="high"{% if row.flow_priority == "high" %} selected{% endif %}>high</option>{% endif %}
154
    <option value="normal"{% if row.flow_priority == "normal" %} selected{% endif %}>normal</option>
155
    <option value="low"{% if row.flow_priority == "low" %} selected{% endif %}>low</option>
156
  </select>
157
  <button type="submit">SAVE</button>
158
</form>
159
<form class="card wide" id="flow-supersede" method="post" action="{{ repo.url }}/settings/flow-supersede">
160
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
161
  <h2>Newest run wins</h2>
162
  <p>A new push run cancels the older push runs of its bookmark still going, and a proposal's new run its older ones: they test states that have moved on, and each cancelled job says which run superseded it. Runs started by hand, by a schedule, by a tag or as a re-run are never cancelled this way.</p>
163
  <label class="check"><input type="checkbox" name="running"{% if !row.flow_keep_running %} checked{% endif %}> Cancel superseded runs already running too (untick to let them finish and cancel only those still queued)</label>
164
  <button type="submit">SAVE</button>
165
</form>
166
<form class="card wide" id="flow-reuse" method="post" action="{{ repo.url }}/settings/flow-reuse">
167
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
168
  <h2>Reuse passes</h2>
169
  <p>A push run doesn't run a job again when the same job, with the same spec and sandbox, already passed on the very same state, as main does after a proposal lands: the job shows as passed, reused from that run, with its receipt. A job that waits for an approval, deploys to an environment, holds secrets or asks for identity tokens always runs, as does one whose artifacts another job of the run takes.</p>
170
  <label class="check"><input type="checkbox" name="reuse"{% if row.flow_reuse %} checked{% endif %}> Reuse a job's pass on the same state (untick to run every job of every push, for flaky or time-dependent tests)</label>
171
  <button type="submit">SAVE</button>
172
</form>
173
<form class="card wide" id="flow-skip" method="post" action="{{ repo.url }}/settings/flow-skip">
174
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
175
  <h2>Pushing without a run</h2>
176
  <p>A push whose newest change says <code>[skip flow]</code>, <code>[skip ci]</code> or <code>[ci skip]</code> in its message starts no jobs: the run list shows it as skipped, with the change and who pushed it, and the next run still covers what it brought. A pushed tag always runs.</p>
177
  <select name="skip" aria-label="Where a push may skip its run">
178
    <option value="any"{% if row.flow_skip != "others" && row.flow_skip != "never" %} selected{% endif %}>on any bookmark</option>
179
    <option value="others"{% if row.flow_skip == "others" %} selected{% endif %}>on any bookmark but {{ row.default_bookmark }}</option>
180
    <option value="never"{% if row.flow_skip == "never" %} selected{% endif %}>never: every push runs</option>
181
  </select>
182
  <button type="submit">SAVE</button>
183
</form>
184
{% when Section::Gates %}
185
<form class="card wide" id="agent-gates" method="post" action="{{ repo.url }}/settings/agent-gates">
186
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
187
  <h2>Approval gates and agents</h2>
188
  <p>A gate is there so a person decides. By default an agent's token (an agent-marked token, or a keyless agent's grant) cannot approve or reject one here; a decision signed with a person's key (a sealed approval) always can.</p>
189
  <label class="check"><input type="checkbox" name="agents"{% if row.agents_decide_gates %} checked{% endif %}> Let agents' tokens approve and reject this repository's approval gates (each decision still says which agent)</label>
190
  <button type="submit">SAVE</button>
191
</form>
192
{% when Section::Diffusion %}
193
<form class="card wide" id="diffusion" method="post" action="{{ repo.url }}/settings/diffusion">
194
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
195
  <h2>Diffusion</h2>
196
  <p>When a repository this one pins as a subrepo moves on, Diffusion opens a proposal here that moves the pin, tested by this repository's own pipeline, with the upstream's changes listed. By default it never lands anything by itself.</p>
197
  <div class="choice">
198
    <label><input type="radio" name="diffusion" value="green"{% if row.diffusion == "green" %} checked{% endif %}> when the upstream's checks pass</label>
199
    <label><input type="radio" name="diffusion" value="any"{% if row.diffusion == "any" %} checked{% endif %}> on every upstream move</label>
200
    <label><input type="radio" name="diffusion" value="off"{% if row.diffusion == "off" %} checked{% endif %}> off</label>
201
    <label><input type="radio" name="diffusion" value="default"{% if row.diffusion.is_empty() %} checked{% endif %}> site default ({{ diffusion_site }}, from <code>[diffusion] follow</code> in <code>hub.toml</code>)</label>
202
  </div>
203
  <h3>Landing</h3>
204
  <div class="choice">
205
    <label><input type="radio" name="land" value="propose"{% if row.diffusion_land != "auto" %} checked{% endif %}> propose only: a person lands each bump</label>
206
    <label><input type="radio" name="land" value="auto"{% if row.diffusion_land == "auto" %} checked{% endif %}> land by itself when both halves are green: the upstream state it pins passed its own checks and this repository's run on the bump passed</label>
207
  </div>
208
  <p class="small mute">An untested (amber) upstream always waits for a person. A landing by Diffusion still needs this repository's required checks and approvals, and a trust policy that lets the hub's key move the bookmark; otherwise the bump waits, and its timeline says why.</p>
209
  <p class="small mute">A subrepo's <code>.oxymodules</code> entry overrides this with <code>follow = green</code>, <code>any</code> or <code>off</code>; <code>land = auto</code> or <code>propose</code> overrides the landing, <code>target</code> names the bookmark to move (else the one this repository's proposals land on) and <code>watch</code> lists paths whose upstream changes are flagged.</p>
210
  <button type="submit">SAVE</button>
211
</form>
212
{% when Section::Backup %}
213
<form class="card wide" id="backup" method="post" action="{{ repo.url }}/settings/backup">
214
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
215
  <h2>Backup</h2>
216
  {% if let Some(b) = backup_owner %}
217
  <p>{{ row.owner }} backs up {% if b.every == "off" %}only by hand or when a repository asks{% else %}{% if b.every == "hourly" %}every hour{% else %}every day{% endif %}{% endif %} to {% if b.destination == "gcs" %}<code>gs://{{ b.target }}</code>{% else %}a directory on this hub's host{% endif %}. Every repository goes with it unless it says otherwise here.</p>
218
  {% else %}
219
  <p>{{ row.owner }} has no backup set up yet: a user sets one up in <a href="/settings/backups">Settings › Backups</a>, an organisation's owners in its settings. What you choose here applies once it has.</p>
220
  {% endif %}
221
  <div class="choice">
222
    {% for (v, label) in backup_choices() %}<label><input type="radio" name="backup" value="{{ v }}"{% if row.backup == *v %} checked{% endif %}> {{ label }}</label>{% endfor %}
223
  </div>
224
  <p class="small mute">A repository with its own schedule makes its owner's backup run at least that often; each run sends only what changed and keeps every repository, so any run restores everything. Left out, it is in no new run; earlier runs keep it until they are pruned.</p>
225
  <button type="submit">SAVE</button>
226
</form>
227
{% when Section::Coverage %}
228
<form class="card wide" id="coverage" method="post" action="{{ repo.url }}/settings/coverage">
229
  <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
230
  <h2>Coverage</h2>
231
  <p class="small mute">A job's <code>coverage</code> artifact (LCOV) gives each run a <code>coverage</code> check, measured against <strong>{{ row.default_bookmark }}</strong>. Without rules it only reports; with them it fails when they are broken, and a required check named <code>coverage</code> then holds a proposal back.</p>
232
  <div class="row">
233
    <div><label for="cov_min">Fail below this percentage</label><input type="number" id="cov_min" name="min" min="0" max="100" step="0.1" value="{% if let Some(m) = coverage.min %}{{ m }}{% endif %}" placeholder="e.g. 80"></div>
234
    <div><label for="cov_drop">Fail when it falls by more than (points)</label><input type="number" id="cov_drop" name="max_drop" min="0" max="100" step="0.1" value="{% if let Some(m) = coverage.max_drop %}{{ m }}{% endif %}" placeholder="e.g. 0.5"></div>
235
  </div>
236
  <button type="submit">SAVE</button>
237
</form>
238
{% when Section::Runners %}
239
<section class="panel" id="runners">
240
  <h2>Runners</h2>
241
  <p class="small">The OxygenFlow runners that may take this repository's jobs: the site's, {{ row.owner }}'s, and this repository's own.</p>
242
  {% if let Some(t) = runner_token %}<p class="note">Register a runner for this repository with:</p><pre class="snippet">flow runner register {{ shell.base_url }} --token {{ t }} --name NAME</pre>{% endif %}
243
  {% if runners.is_empty() %}<p class="mute small">None yet: runs wait until a runner that serves this repository is added.</p>{% else %}
244
  <div class="table"><table>
245
  <tr><th>Runner</th><th>Status</th><th>Serves</th><th>Machine</th><th></th></tr>
246
  {% for (r, serves, own) in runners %}<tr><td><strong>{{ r.name }}</strong></td><td><span class="runner-status st-{{ r.status() }}">{{ r.status() }}</span></td><td class="small">{{ serves }}</td><td class="small">{% if r.arch.is_empty() %}<span class="mute">unknown</span>{% else %}{{ r.arch }} · {{ r.os }}{% endif %}</td>
247
  <td>{% if *own %}<form class="tiny" method="post" action="{{ repo.url }}/settings/runners/{{ r.id }}/remove" data-confirm="Remove {{ r.name }}? Its credential stops working."><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button class="danger-btn">remove</button></form>{% endif %}</td></tr>{% endfor %}
248
  </table></div>{% endif %}
249
  <form method="post" action="{{ repo.url }}/settings/runners/token"><input type="hidden" name="_csrf" value="{{ shell.csrf }}"><button type="submit">Add a runner for this repository</button></form>
250
</section>
251
{% when Section::Webhooks %}
252
{% call w::panel(repo.url ~ "/settings", hooks, shell.csrf, "Each delivery is signed with the secret (X-Oxyhub-Signature) and retried with backoff for a day.") %}
253
{% when Section::Variables %}
254
{% call va::panel(repo.url ~ "/settings", variables, shell.csrf, "This repository's jobs get these, beside the ones the site and its owner set.") %}
255
{% when Section::Secrets %}
256
{% call se::panel(repo.url ~ "/settings", secrets, shell.csrf, "This repository's jobs may use these, beside the ones the site and its owner set.") %}
257
{% when Section::Assistants %}
258
{% call ai::panel(repo.url ~ "/settings", assistants, shell.csrf, "This repository may ask these, beside the ones the site and its owner provide.") %}
259
{% when Section::Storage %}
260
{% if let Some(st) = storage %}<section class="panel" id="storage">
261
  <h2>Storage</h2>
262
  <p>This repository's data lives in <code>{{ st.url }}</code>, your bucket. This server keeps a cache and the credentials, sealed.</p>
263
  <dl class="facts">
264
    <dt>Since</dt><dd><time title="{{ st.created_ms|when_i }}">{{ st.created_ms|ago_i }}</time></dd>
265
    <dt>Last check</dt><dd>{% if let Some(t) = st.checked_ms %}<time title="{{ t|when_i }}">{{ t|ago_i }}</time>: {{ st.check_result }}{% else %}<span class="mute">never</span>{% endif %}</dd>
266
  </dl>
267
  <form class="inline" method="post" action="{{ repo.url }}/settings/storage">
268
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
269
    <input type="hidden" name="action" value="check">
270
    <button type="submit" title="Write, read, list and delete under a probe prefix of the bucket, and compare-and-swap a record">CHECK THE BUCKET</button>
271
  </form>
272
  <details>
273
    <summary>Replace the credentials</summary>
274
    <form class="card" method="post" action="{{ repo.url }}/settings/storage">
275
      <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
276
      <input type="hidden" name="action" value="credentials">
277
      <p class="small mute">The new credentials must pass the check before they replace the old ones. Fill the fields for your provider.</p>
278
      {% if st.url.starts_with("s3://") %}<div class="row">
279
        <div><label for="sak">Access key ID</label><input type="text" id="sak" name="access_key_id" autocomplete="off" required></div>
280
        <div><label for="ssk">Secret access key</label><input type="password" id="ssk" name="secret_access_key" autocomplete="new-password" required></div>
281
      </div>{% else if st.url.starts_with("gs://") %}<label for="ssa">Service account key</label><textarea id="ssa" name="service_account_key" rows="3" required spellcheck="false"></textarea>{% else %}<div class="row">
282
        <div><label for="stn">Tenant ID</label><input type="text" id="stn" name="tenant_id" autocomplete="off" required></div>
283
        <div><label for="sci">Client ID</label><input type="text" id="sci" name="client_id" autocomplete="off" required></div>
284
        <div><label for="scs">Client secret</label><input type="password" id="scs" name="client_secret" autocomplete="new-password" required></div>
285
      </div>{% endif %}
286
      <button type="submit">SAVE</button>
287
    </form>
288
  </details>
289
</section>{% endif %}
290
{% when Section::Collaborators %}
291
<section class="panel" id="collaborators">
292
  <h2>Collaborators</h2>
293
  {% if collaborators.is_empty() %}<p class="mute small">Nobody beyond the owner{% if !teams.is_empty() %} and the organisation's members{% endif %}.</p>{% else %}
294
  <table class="small">
295
  {% for c in collaborators %}
296
  <tr><td>{% if let Some(a) = c.account %}<a href="/{{ a.username }}">{{ a.username }}</a>{% else if let Some(t) = c.team %}team {{ t.name }}{% endif %}</td><td>{{ c.role }}</td>
297
  <td><form class="tiny" method="post" action="{{ repo.url }}/settings/collaborators"><input type="hidden" name="_csrf" value="{{ shell.csrf }}">{% if let Some(a) = c.account %}<input type="hidden" name="username" value="{{ a.username }}">{% else if let Some(t) = c.team %}<input type="hidden" name="team" value="{{ t.name }}">{% endif %}<button name="remove" value="1">remove</button></form></td></tr>
298
  {% endfor %}
299
  </table>{% endif %}
300
  <form class="card" method="post" action="{{ repo.url }}/settings/collaborators">
301
    <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
302
    <div class="row">
303
      <div><label for="username">Account</label><input type="text" id="username" name="username"></div>
304
      {% if !teams.is_empty() %}<div><label for="team">or team</label><select id="team" name="team"><option value="">—</option>{% for t in teams %}<option value="{{ t.name }}">{{ t.name }}</option>{% endfor %}</select></div>{% endif %}
305
      <div><label for="role">Role</label><select id="role" name="role"><option value="read">read</option><option value="triage">triage</option><option value="write" selected>write</option><option value="maintain">maintain</option><option value="admin">admin</option></select></div>
306
    </div>
307
    <button type="submit">ADD</button>
308
  </form>
309
</section>
310
{% when Section::Advanced %}
311
<section class="panel danger-zone" id="advanced">
312
  <h2>Advanced</h2>
313
  <p class="small mute">Settings that change what this repository is. Each asks you to confirm; none can be undone from here except archiving.</p>
314
  <div class="danger-row">
315
    <div><strong>Transfer ownership</strong><p class="small mute">Move this repository to a user or an organisation you may create repositories under. Its address changes to <code>/OWNER/{{ repo.name }}</code>; the old address redirects, and <code>oxy</code> remotes keep working.</p></div>
316
    <details class="danger-act"><summary class="danger-btn">Transfer</summary>
317
      <form class="card" method="post" action="{{ repo.url }}/settings/transfer">
318
        <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
319
        <label for="towner">New owner</label><input type="text" id="towner" name="owner" placeholder="an organisation or a user" required>
320
        <label for="tconfirm">Type <code>{{ repo.owner }}/{{ repo.name }}</code> to confirm</label><input type="text" id="tconfirm" name="confirm" required autocomplete="off">
321
        <button type="submit">TRANSFER</button>
322
      </form>
323
    </details>
324
  </div>
325
  <div class="danger-row">
326
    <div><strong>{% if row.archived %}Bring this repository back{% else %}Archive this repository{% endif %}</strong><p class="small mute">{% if row.archived %}It is archived: read-only for everyone, shown with a badge.{% if !row.archived_reason.is_empty() %} Reason: {{ row.archived_reason }}{% endif %}{% else %}Read-only for everyone, including you: no pushes, edits, issues, proposals or comments, until an admin brings it back. Say why, for whoever finds it later.{% endif %}</p></div>
327
    <details class="danger-act"><summary class="danger-btn">{% if row.archived %}Unarchive{% else %}Archive{% endif %}</summary>
328
      <form class="card" method="post" action="{{ repo.url }}/settings/archive">
329
        <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
330
        {% if row.archived %}<input type="hidden" name="action" value="unarchive"><button type="submit">BRING IT BACK</button>{% else %}<input type="hidden" name="action" value="archive">
331
        <label for="areason">Reason (optional)</label><textarea id="areason" name="reason" rows="2" maxlength="500" placeholder="Superseded by …"></textarea>
332
        <button type="submit">ARCHIVE</button>{% endif %}
333
      </form>
334
    </details>
335
  </div>
336
  <div class="danger-row">
337
    <div><strong>Delete wiki data</strong><p class="small mute">{% if repo.wiki %}The wiki's repository moves to the trash with its history; a new first page starts a fresh one.{% else %}This repository has no wiki.{% endif %}</p></div>
338
    {% if repo.wiki %}<details class="danger-act"><summary class="danger-btn">Delete wiki data</summary>
339
      <form class="card" method="post" action="{{ repo.url }}/settings/wiki/delete">
340
        <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
341
        <label for="wconfirm">Type <code>{{ repo.owner }}/{{ repo.name }}</code> to confirm</label><input type="text" id="wconfirm" name="confirm" required autocomplete="off">
342
        <button type="submit">DELETE WIKI DATA</button>
343
      </form>
344
    </details>{% else %}<button class="danger-btn" type="button" disabled>Delete wiki data</button>{% endif %}
345
  </div>
346
  <div class="danger-row">
347
    <div><strong>Delete this repository</strong><p class="small mute">The repository moves to the trash and is removed after the retention period.</p></div>
348
    <details class="danger-act"><summary class="danger-btn">Delete</summary>
349
      <form class="card" method="post" action="{{ repo.url }}/settings/delete">
350
        <input type="hidden" name="_csrf" value="{{ shell.csrf }}">
351
        <label for="dconfirm">Type <code>{{ repo.owner }}/{{ repo.name }}</code> to confirm</label><input type="text" id="dconfirm" name="confirm" required autocomplete="off">
352
        <button type="submit">DELETE</button>
353
      </form>
354
    </details>
355
  </div>
356
</section>
357
{% endmatch %}
358
</div>
359
</div>
360
{% endblock %}