OXYGENHUB
◆ api-unknown-fields 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
19bb31ff Andy McCall ↶ 1
{% extends "base.html" %}
8fb35df7 Andy McCall ↶ 2
{% import "webhooks_panel.html" as w %}
e71846aa Andy McCall ↶ 3
{% import "assistants_panel.html" as ai %}
2a83f530 Andy McCall ↶ 4
{% import "variables_panel.html" as va %}
4ac01a73 Andy McCall ↶ 5
{% import "secrets_panel.html" as se %}
19bb31ff Andy McCall ↶ 6
{% import "repo_tabs.html" as m %}
c7067c7b Andy McCall 7
{% block title %}{% if section != Section::General %}{{ section.title() }} · {% endif %}Settings · {{ repo.owner }}/{{ repo.name }}{% endblock %}
19bb31ff Andy McCall ↶ 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>
c7067c7b Andy McCall 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">
19bb31ff Andy McCall ↶ 20
{% if let Some(n) = notice %}<p class="note">{{ n }}</p>{% endif %}
21
{% if let Some(e) = error %}<p class="error">{{ e }}</p>{% endif %}
c7067c7b Andy McCall 22
{% match section %}
23
{% when Section::General %}
64caed18 Andy McCall ↶ 24
<form class="card wide" id="general" method="post" action="{{ repo.url }}/settings">
19bb31ff Andy McCall ↶ 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 }}">
1553bfc9 Andy McCall ↶ 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>
19bb31ff Andy McCall ↶ 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>
c7067c7b Andy McCall 39
{% when Section::Landing %}
64caed18 Andy McCall ↶ 40
<form class="card wide" id="landing" method="post" action="{{ repo.url }}/settings/requirements">
de505df5 Andy McCall ↶ 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>
c7067c7b Andy McCall 50
{% when Section::PushProtection %}
0ee5c3fb Andy McCall ↶ 51
<section class="panel" id="push-protection">
52
  <h2>Push protection</h2>
cb7b3cd4 Andy McCall ↶ 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>
0ee5c3fb Andy McCall ↶ 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>
cb7b3cd4 Andy McCall ↶ 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 %}
0ee5c3fb Andy McCall ↶ 102
  {% if !secret_bypasses.is_empty() %}
103
  <h3>Allowed</h3>
104
  <table class="small">
cb7b3cd4 Andy McCall ↶ 105
  <tr><th>Fingerprint</th><th>Rule</th><th>Reason</th><th>By</th><th>When</th><th></th></tr>
0ee5c3fb Andy McCall ↶ 106
  {% for b in secret_bypasses %}
cb7b3cd4 Andy McCall ↶ 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>
0ee5c3fb Andy McCall ↶ 109
  {% endfor %}
110
  </table>{% endif %}
111
</section>
c7067c7b Andy McCall 112
{% when Section::GitMirror %}
64caed18 Andy McCall ↶ 113
<form class="card wide" id="git-mirror" method="post" action="{{ repo.url }}/settings/mirror">
3f4b0ea6 Andy McCall ↶ 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>
c7067c7b Andy McCall 120
{% when Section::Checks %}
64caed18 Andy McCall ↶ 121
<form class="card wide" id="checks" method="post" action="{{ repo.url }}/settings/checks">
b64e2537 Andy McCall ↶ 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>
9008bf6c Andy McCall ↶ 127
</form>
c7067c7b Andy McCall 128
{% when Section::Pipelines %}
9008bf6c Andy McCall ↶ 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>
1870e4fa Andy McCall ↶ 133
  {% if site_admin || own_runner || row.flow == "own" %}
9008bf6c Andy McCall ↶ 134
  <div class="choice">
1870e4fa Andy McCall ↶ 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 %}
9008bf6c Andy McCall ↶ 137
    <label><input type="radio" name="flow" value="off"{% if row.flow == "off" %} checked{% endif %}> off</label>
1870e4fa Andy McCall ↶ 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 %}
9008bf6c Andy McCall ↶ 139
  </div>
c7067c7b Andy McCall 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>
9008bf6c Andy McCall ↶ 141
  <button type="submit">SAVE</button>
142
  {% else %}
c7067c7b Andy McCall 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>
9008bf6c Andy McCall ↶ 144
  {% endif %}
b64e2537 Andy McCall ↶ 145
</form>
c7067c7b Andy McCall 146
{% when Section::Runs %}
107eaba5 Andy McCall ↶ 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>
a9f1c0bf Andy McCall ↶ 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>
5588bb36 Andy McCall ↶ 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>
367f34b8 Andy McCall ↶ 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>
c7067c7b Andy McCall 184
{% when Section::Gates %}
4a68f106 Andy McCall ↶ 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>
c7067c7b Andy McCall 192
{% when Section::Diffusion %}
e7158350 Andy McCall ↶ 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>
f4d6ef41 Andy McCall ↶ 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>
e7158350 Andy McCall ↶ 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>
f4d6ef41 Andy McCall ↶ 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>
e7158350 Andy McCall ↶ 210
  <button type="submit">SAVE</button>
211
</form>
c7067c7b Andy McCall 212
{% when Section::Backup %}
d3b048b2 Andy McCall ↶ 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>
c7067c7b Andy McCall 227
{% when Section::Coverage %}
7edaff01 Andy McCall ↶ 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>
c7067c7b Andy McCall 238
{% when Section::Runners %}
7974adb0 Andy McCall ↶ 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>
a7aa76ac Andy McCall 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 %}
7974adb0 Andy McCall ↶ 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>
c7067c7b Andy McCall 251
{% when Section::Webhooks %}
8fb35df7 Andy McCall ↶ 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.") %}
c7067c7b Andy McCall 253
{% when Section::Variables %}
2a83f530 Andy McCall ↶ 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.") %}
c7067c7b Andy McCall 255
{% when Section::Secrets %}
4ac01a73 Andy McCall ↶ 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.") %}
c7067c7b Andy McCall 257
{% when Section::Assistants %}
e71846aa Andy McCall ↶ 258
{% call ai::panel(repo.url ~ "/settings", assistants, shell.csrf, "This repository may ask these, beside the ones the site and its owner provide.") %}
c7067c7b Andy McCall 259
{% when Section::Storage %}
260
{% if let Some(st) = storage %}<section class="panel" id="storage">
1f78655a Andy McCall ↶ 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>
c7067c7b Andy McCall 289
</section>{% endif %}
290
{% when Section::Collaborators %}
64caed18 Andy McCall ↶ 291
<section class="panel" id="collaborators">
19bb31ff Andy McCall ↶ 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>
c7067c7b Andy McCall 310
{% when Section::Advanced %}
d0fbea2e Andy McCall ↶ 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">
66fd9d6b Andy McCall ↶ 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 %}
d0fbea2e Andy McCall ↶ 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>
c7067c7b Andy McCall 357
{% endmatch %}
64caed18 Andy McCall ↶ 358
</div>
359
</div>
19bb31ff Andy McCall ↶ 360
{% endblock %}