Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,25 @@ The Settings tab holds the whole configuration on one screen:
isolation.
- **Autoscaling** and **image auto-update** — optional; see steps below.

Save your **Personal Access Token** from the band at the top (`repo` scope; add
`admin:org` for org runners). It's stored at
`/boot/config/plugins/ci-runner-farm/token` with `chmod 600` and is **never**
written into your plugin config — the **Get a pre-scoped PAT** link opens GitHub
with exactly the right scopes pre-filled.
Save a **classic Personal Access Token** from the band at the top. The selector
opens GitHub with the appropriate least-privilege scope preset. Here, “least
privilege” means the minimum **classic PAT scopes** for the job; it does not
make the credential repository- or organization-scoped. A classic PAT applies
across the token owner's accessible repositories, so use a dedicated service
account/token with only the access this farm needs.

| Use case | PAT scopes |
| --- | --- |
| Repository runners | `repo` |
| Organization runners | `repo`, `admin:org` |
| Either runner type with a private GHCR image | Add `read:packages` |
| Separate registry token for pulling a private GHCR image | `read:packages` |

The runner token is stored at `/boot/config/plugins/ci-runner-farm/token` with
`chmod 600` and is **never** written into the plugin config. A separate
package-only token can be saved in the registry-token field when you do not want
the runner-management token to have package access. If the GitHub organization
uses SSO, authorize each token for that organization.

![The Settings tab — GitHub scope and targets, per-runner CPU/memory caps, runner image source, warm caches, Docker-in-Docker, autoscaling, and secure token storage, all on one screen](docs/images/settings.png)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ $defaults = [
'AUTOSCALE'=>'false', 'AUTOSCALE_MIN'=>'2', 'AUTOSCALE_MAX'=>'16', 'AUTOSCALE_MIN_IDLE'=>'2',
'AUTOSCALE_STEP'=>'2', 'AUTOSCALE_INTERVAL'=>'30', 'AUTOSCALE_IDLE_GRACE'=>'5',
];
$patScope = (($cfg['GH_SCOPE'] ?? $defaults['GH_SCOPE']) === 'org') ? 'repo,admin:org' : 'repo';
$patUrl = 'https://github.com/settings/tokens/new?description=Unraid+CI+Runner+Farm&scopes='.$patScope;
/* GitHub Packages currently requires a classic PAT. Preselect the least-privilege
preset that matches the saved runner scope and remote GHCR configuration, while
still letting an operator deliberately choose a different (for example,
package-only) token. */
$saved_scope = $cfg['GH_SCOPE'] ?? $defaults['GH_SCOPE'];
$saved_ghcr = ($cfg['IMAGE_SOURCE'] ?? $defaults['IMAGE_SOURCE']) === 'remote'
&& preg_match('/^(?:https:\/\/)?(?:[a-z0-9-]+\.)?ghcr\.io(?:\/|$)/i', trim($cfg['REGISTRY_SERVER'] ?? $defaults['REGISTRY_SERVER']));
$pat_preset = $saved_scope === 'org' ? ($saved_ghcr ? 'org-ghcr' : 'org') : ($saved_ghcr ? 'repo-ghcr' : 'repo');
/* $defaults is authoritative: the inline $d args at call sites are a fallback
for any key not listed above (there are none today). */
if (!function_exists('crf_g')) { function crf_g($cfg,$k,$d=''){ global $defaults; return htmlspecialchars($cfg[$k] ?? $defaults[$k] ?? $d, ENT_QUOTES); } }
Expand All @@ -50,10 +56,17 @@ if (!function_exists('crf_sel')) { function crf_sel($cfg,$k,$val,$d=''){ global
</span>
</div>
<div class="crfs-tok-input">
<uui-button variant="outline" size="xs" onclick="window.open('<?=$patUrl?>','_blank','noopener')">Get a pre-scoped PAT &#8599;</uui-button>
<span class="crfs-field"><input type="password" id="crf-token" autocomplete="new-password" placeholder="ghp_... (scopes pre-filled)"><button type="button" class="crfs-fx" title="Clear saved token" aria-label="Clear saved token" onclick="crfClearToken()">&times;</button></span>
<select id="crf-pat-preset" aria-label="Classic PAT permission preset">
<option value="repo" <?=$pat_preset === 'repo' ? 'selected' : ''?>>Repository runners &mdash; repo</option>
<option value="org" <?=$pat_preset === 'org' ? 'selected' : ''?>>Organization runners &mdash; repo + admin:org</option>
<option value="repo-ghcr" <?=$pat_preset === 'repo-ghcr' ? 'selected' : ''?>>Repository runners + private GHCR image &mdash; repo + read:packages</option>
<option value="org-ghcr" <?=$pat_preset === 'org-ghcr' ? 'selected' : ''?>>Organization runners + private GHCR image &mdash; repo + admin:org + read:packages</option>
</select>
<uui-button variant="secondary" size="xs" onclick="crfGetPat('crf-pat-preset')">Create classic PAT &#8599;</uui-button>
<span class="crfs-field"><input type="password" id="crf-token" autocomplete="new-password" placeholder="ghp_... (choose a preset first)"><button type="button" class="crfs-fx" title="Clear saved token" aria-label="Clear saved token" onclick="crfClearToken()">&times;</button></span>
<uui-button size="xs" onclick="crfSetToken()">Save token</uui-button>
</div>
<div class="crfs-pat-note">Classic PATs apply to every repository the token owner can access. These presets minimize scopes, not repository or organization reach; use a dedicated service account/token with only the access this farm needs.</div>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</div>
<div class="crfs-tokband crfs-regband" id="crf-remote-auth">
<div class="crfs-tok-status">
Expand All @@ -64,6 +77,7 @@ if (!function_exists('crf_sel')) { function crf_sel($cfg,$k,$val,$d=''){ global
</span>
</div>
<div class="crfs-tok-input">
<uui-button variant="secondary" size="xs" onclick="crfGetPat('packages')">Create GHCR pull PAT &#8599;</uui-button>
<span class="crfs-field"><input type="password" id="crf-registry-token" autocomplete="new-password" placeholder="registry password / token"><button type="button" class="crfs-fx" title="Clear saved registry token" aria-label="Clear saved registry token" onclick="crfClearRegistryToken()">&times;</button></span>
<uui-button size="xs" onclick="crfSetRegistryToken()">Save registry token</uui-button>
</div>
Expand Down Expand Up @@ -400,6 +414,20 @@ function crfTokState(ok){
function crfSetToken(){ const t=document.getElementById('crf-token').value; if(!t)return;
crfPost({action:'set-token',token:t}).then(o=>{crfTokState(!!o.ok);document.getElementById('crf-token').value='';}).catch(e=>{ crfToast&&crfToast('Save failed'); console.error('ci-runner-farm set-token:',e); }); }
function crfClearToken(){ crfPost({action:'clear-token'}).then(()=>crfTokState(false)).catch(e=>console.error('ci-runner-farm clear-token:',e)); }
function crfGetPat(presetOrId){
const preset=document.getElementById(presetOrId), key=preset ? preset.value : presetOrId;
const scopes={
repo:'repo',
org:'repo,admin:org',
'repo-ghcr':'repo,read:packages',
'org-ghcr':'repo,admin:org,read:packages',
packages:'read:packages'
};
const scope=scopes[key];
if(!scope) return;
const params=new URLSearchParams({description:'Unraid CI Runner Farm',scopes:scope});
window.open('https://github.com/settings/tokens/new?'+params.toString(),'_blank','noopener');
}
function crfRegState(ok){
document.getElementById('crf-regtok').textContent = ok?'Registry token configured':'Registry token not set';
document.getElementById('crf-regtok-ball').className='crf-ball '+(ok?'crf-ball-idle':'');
Expand Down Expand Up @@ -487,7 +515,9 @@ if (window.jQuery) jQuery(function($){ if ($.fn.fileTreeAttach) $("#CACHE_ROOT")
.crfs-tok-text{display:flex;flex-direction:column;gap:2px}
.crfs-tok-text strong{font-size:13px;color:var(--text-color)}
.crfs-tok-sub{font-size:11px;color:var(--alt-text-color)}
.crfs-pat-note{font-size:11px;color:var(--alt-text-color);margin-top:6px;line-height:1.4}
.crfs-tok-input{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.crfs-tok-input select{max-width:100%;font-size:12px}
.crfs-tok-input input[type=password]{width:24em;font-size:12px;padding-right:26px!important;box-sizing:border-box}
.crfs-field{position:relative;display:inline-flex;align-items:center}
button.crfs-fx{position:absolute;right:4px;border:none!important;background:none!important;box-shadow:none!important;outline:none;width:auto!important;min-width:0!important;height:auto!important;color:var(--alt-text-color)!important;font-size:16px;line-height:1;cursor:pointer;padding:2px 5px!important;margin:0!important;letter-spacing:0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ IMAGE="" # remote image ref, used when IMAGE_SOURCE
EPHEMERAL="false" # true => runner deregisters after each job
RUN_AS_ROOT="false" # false => jobs run as non-root 'runner' (sudo+docker groups), like
# GitHub-hosted runners. true => jobs run as root (legacy).
ACCESS_TOKEN="" # GitHub PAT (repo scope; +admin:org for org). Stays host-side:
ACCESS_TOKEN="" # GitHub PAT (repo scope; +admin:org for org; +read:packages if reused for private GHCR). Stays host-side:
# runners get a short-lived registration token, never the PAT itself.
SHARE_DOCKER_SOCK="false" # mount host docker.sock for service containers (ignored when DIND=true).
# Off by default: it gives jobs root-equivalent host access — opt in only
Expand Down