feat(health-checks): enhance health check configuration and UI components
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 52s
CD / quality (push) Successful in 1m55s
CD / publish (push) Successful in 1m54s
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 5s
quality / changes (push) Successful in 5s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 52s
CD / quality (push) Successful in 1m55s
CD / publish (push) Successful in 1m54s
- Introduced a new HealthProviderToggle component to manage health check providers (local/cloudflare) in the UI. - Updated health check configuration to include additional parameters such as retries and consecutive success/failure counts. - Improved the service edit and group edit sheets to support the new health check provider options. - Enhanced documentation to clarify the use of ACTIONS_PAT and GITEA_TOKEN for wiki updates. This commit improves the health check management experience and expands the configuration options for better service monitoring.
This commit is contained in:
+2
-2
@@ -35,13 +35,13 @@ Runner: `ubuntu-latest`, Docker для **docker-check** (PR) и **publish** (CD)
|
|||||||
|
|
||||||
Повтор упавшего **publish** (тег уже есть, bake нет): detect берёт `v*` на `HEAD` и всё равно пушит образы. Подробнее: [docs/releasing.md](../docs/releasing.md#перезапуск-упавшего-job-publish).
|
Повтор упавшего **publish** (тег уже есть, bake нет): detect берёт `v*` на `HEAD` и всё равно пушит образы. Подробнее: [docs/releasing.md](../docs/releasing.md#перезапуск-упавшего-job-publish).
|
||||||
|
|
||||||
Job **update-wiki** идёт **параллельно** publish (не блокирует образы): при diff `docs/Home.md` копирует файл в wiki-репозиторий. Clone/push идут с `Authorization: Basic oauth2:<token>` — после clone git вырезает токен из `origin`, без header Gitea отвечает `Repository not found` (часто на внутреннем `GITEA_INSTANCE_URL` раннера). Секрет: **`GITEA_TOKEN`**, fallback **`ACTIONS_PAT`**.
|
Job **update-wiki** идёт **параллельно** publish (не блокирует образы): при diff `docs/Home.md` копирует файл в wiki-репозиторий. Clone/push идут на публичный **`https://git.shx.one`** (не внутренний `gitea.server_url` / `192.168.x.x:3000`): Gitea `ROOT_URL` совпадает с Host, иначе `git-receive-pack` wiki отвечает `Repository not found`. Токен в URL `https://oauth2:<PAT>@…/*.wiki.git` — Gitea на неаутентифицированный wiki push даёт **404, не 401**, поэтому `http.extraHeader` / ASKPASS не срабатывают. Секрет: **`ACTIONS_PAT`**, fallback **`GITEA_TOKEN`**.
|
||||||
|
|
||||||
### Секреты
|
### Секреты
|
||||||
|
|
||||||
**`ACTIONS_PAT`**: push tags, releases, Container Registry. Для git tag fallback: `gitea.token`. Push OCI — **только PAT** (у job token Gitea нет права packages).
|
**`ACTIONS_PAT`**: push tags, releases, Container Registry. Для git tag fallback: `gitea.token`. Push OCI — **только PAT** (у job token Gitea нет права packages).
|
||||||
|
|
||||||
**`GITEA_TOKEN`**: clone/push wiki.
|
**`GITEA_TOKEN`**: опциональный wiki-only PAT (fallback, если нет `ACTIONS_PAT`).
|
||||||
|
|
||||||
### Теги образов
|
### Теги образов
|
||||||
|
|
||||||
|
|||||||
+15
-10
@@ -40,21 +40,26 @@ jobs:
|
|||||||
- name: Update and push Wiki content
|
- name: Update and push Wiki content
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
if: steps.check_changes.outputs.changed == 'true'
|
||||||
env:
|
env:
|
||||||
WIKI_TOKEN: ${{ secrets.GITEA_TOKEN || secrets.ACTIONS_PAT }}
|
# ACTIONS_PAT уже пишет git (tags/releases). GITEA_TOKEN — опциональный
|
||||||
SERVER_URL: ${{ gitea.server_url }}
|
# wiki-only PAT; если он задан без write, Gitea отвечает 404, не 403.
|
||||||
|
WIKI_TOKEN: ${{ secrets.ACTIONS_PAT || secrets.GITEA_TOKEN }}
|
||||||
|
# Не gitea.server_url: на runner это внутренний http://192.168.x.x:3000,
|
||||||
|
# а ROOT_URL = git.shx.one — git-receive-pack wiki тогда даёт 404.
|
||||||
|
GITEA_PUBLIC_URL: https://git.shx.one
|
||||||
REPO: ${{ gitea.repository }}
|
REPO: ${{ gitea.repository }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [ -z "${WIKI_TOKEN:-}" ]; then
|
if [ -z "${WIKI_TOKEN:-}" ]; then
|
||||||
echo "GITEA_TOKEN / ACTIONS_PAT is empty — cannot push wiki"
|
echo "ACTIONS_PAT / GITEA_TOKEN is empty — cannot push wiki"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# runner GITEA_INSTANCE_URL часто внутренний (http://192.168.x.x:3000).
|
PUBLIC_URL="${GITEA_PUBLIC_URL%/}"
|
||||||
# git после clone вырезает userinfo из origin → push без токена даёт 404
|
TOKEN_ENC="$(python3 -c 'import urllib.parse,os; print(urllib.parse.quote(os.environ["WIKI_TOKEN"], safe=""))')"
|
||||||
# «Repository not found». Authorization header переживает insteadOf/sanitize.
|
WIKI_URL="${PUBLIC_URL}/${REPO}.wiki.git"
|
||||||
WIKI_URL="${SERVER_URL}/${REPO}.wiki.git"
|
# Gitea на неаутентифицированный wiki push отвечает 404, не 401 —
|
||||||
AUTH_HEADER="Authorization: Basic $(printf '%s' "oauth2:${WIKI_TOKEN}" | base64 | tr -d '\n')"
|
# extraHeader/ASKPASS не помогают: токен должен быть в URL с первого запроса.
|
||||||
git -c http.extraHeader="${AUTH_HEADER}" clone "${WIKI_URL}" cfdm.wiki
|
AUTH_INSTEAD="url.https://oauth2:${TOKEN_ENC}@${PUBLIC_URL#https://}/.insteadOf=${PUBLIC_URL}/"
|
||||||
|
GIT_TERMINAL_PROMPT=0 git -c "${AUTH_INSTEAD}" clone "${WIKI_URL}" cfdm.wiki
|
||||||
cp docs/Home.md cfdm.wiki/Home.md
|
cp docs/Home.md cfdm.wiki/Home.md
|
||||||
cd cfdm.wiki
|
cd cfdm.wiki
|
||||||
git config user.name "Gitea Actions"
|
git config user.name "Gitea Actions"
|
||||||
@@ -65,7 +70,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
git commit -m "docs: Update Wiki from main repository"
|
git commit -m "docs: Update Wiki from main repository"
|
||||||
git -c http.extraHeader="${AUTH_HEADER}" push origin HEAD
|
GIT_TERMINAL_PROMPT=0 git -c "${AUTH_INSTEAD}" push origin HEAD
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: [quality]
|
needs: [quality]
|
||||||
|
|||||||
@@ -17,11 +17,15 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@cfdm/ui/components/select'
|
} from '@cfdm/ui/components/select'
|
||||||
import { Switch } from '@cfdm/ui/components/switch'
|
import { Switch } from '@cfdm/ui/components/switch'
|
||||||
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
|
import { ButtonGroup } from '@cfdm/ui/components/button-group'
|
||||||
import { FieldGroup } from '@cfdm/ui/components/field'
|
import { FieldGroup } from '@cfdm/ui/components/field'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/reui/alert'
|
||||||
import { cn } from '@cfdm/ui/lib/utils'
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
|
|
||||||
export type LbMode = 'round_robin' | 'failover' | 'weighted'
|
export type LbMode = 'round_robin' | 'failover' | 'weighted'
|
||||||
export type HealthCheckType = 'tcp' | 'http'
|
export type HealthCheckType = 'tcp' | 'http'
|
||||||
|
export type HealthProvider = 'local' | 'cloudflare'
|
||||||
|
|
||||||
export interface HealthCheckConfig {
|
export interface HealthCheckConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -32,7 +36,11 @@ export interface HealthCheckConfig {
|
|||||||
interval_sec: number
|
interval_sec: number
|
||||||
timeout_ms: number
|
timeout_ms: number
|
||||||
verify_tls: boolean
|
verify_tls: boolean
|
||||||
provider?: 'local' | 'cloudflare'
|
provider: HealthProvider
|
||||||
|
method?: string | null
|
||||||
|
retries?: number
|
||||||
|
consecutive_fails?: number
|
||||||
|
consecutive_successes?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LbAndHealthConfig extends HealthCheckConfig {
|
export interface LbAndHealthConfig extends HealthCheckConfig {
|
||||||
@@ -50,6 +58,47 @@ const healthCheckTypes = [
|
|||||||
{ value: 'http', label: 'HTTP' },
|
{ value: 'http', label: 'HTTP' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
const cloudflareTypes = [
|
||||||
|
{ value: 'tcp', label: 'TCP' },
|
||||||
|
{ value: 'http', label: 'HTTP' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export function HealthProviderToggle({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
value: HealthProvider
|
||||||
|
onChange: (next: HealthProvider) => void
|
||||||
|
id?: string
|
||||||
|
}) {
|
||||||
|
const provider = value || 'local'
|
||||||
|
return (
|
||||||
|
<ButtonGroup className="w-full" id={id}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
variant={provider === 'local' ? 'secondary' : 'outline'}
|
||||||
|
aria-pressed={provider === 'local'}
|
||||||
|
onClick={() => onChange('local')}
|
||||||
|
>
|
||||||
|
Local
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1"
|
||||||
|
variant={provider === 'cloudflare' ? 'secondary' : 'outline'}
|
||||||
|
aria-pressed={provider === 'cloudflare'}
|
||||||
|
onClick={() => onChange('cloudflare')}
|
||||||
|
>
|
||||||
|
Cloudflare
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface HealthCheckConfigFieldsProps {
|
interface HealthCheckConfigFieldsProps {
|
||||||
value: LbAndHealthConfig
|
value: LbAndHealthConfig
|
||||||
onChange: (next: LbAndHealthConfig) => void
|
onChange: (next: LbAndHealthConfig) => void
|
||||||
@@ -120,6 +169,7 @@ export function HealthCheckConfigFields({
|
|||||||
className={rowClass}
|
className={rowClass}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
modal={false}
|
||||||
value={value.lb_mode}
|
value={value.lb_mode}
|
||||||
onValueChange={(v) => patch({ lb_mode: (v ?? 'round_robin') as LbMode })}
|
onValueChange={(v) => patch({ lb_mode: (v ?? 'round_robin') as LbMode })}
|
||||||
>
|
>
|
||||||
@@ -144,21 +194,26 @@ export function HealthCheckConfigFields({
|
|||||||
compact
|
compact
|
||||||
className={rowClass}
|
className={rowClass}
|
||||||
>
|
>
|
||||||
<Select
|
<HealthProviderToggle
|
||||||
|
id={`${idPrefix}-provider`}
|
||||||
value={value.provider ?? 'local'}
|
value={value.provider ?? 'local'}
|
||||||
onValueChange={(v) =>
|
onChange={(provider) =>
|
||||||
patch({ provider: (v ?? 'local') as 'local' | 'cloudflare' })
|
patch({
|
||||||
|
provider,
|
||||||
|
enabled: provider === 'cloudflare' ? true : value.enabled,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<SelectTrigger id={`${idPrefix}-provider`} className="w-full">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="local">Local</SelectItem>
|
|
||||||
<SelectItem value="cloudflare">Cloudflare</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
{value.provider === 'cloudflare' ? (
|
||||||
|
<Alert>
|
||||||
|
<AlertTitle>Cloudflare Health Checks</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Поля соответствуют официальному API зоны. Если план не позволяет Health
|
||||||
|
Checks, API вернёт ошибку — останется Local. Workers не используются.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<SettingRow
|
<SettingRow
|
||||||
title="Health-check"
|
title="Health-check"
|
||||||
@@ -188,6 +243,7 @@ export function HealthCheckConfigFields({
|
|||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<FormFieldSimple label="Тип" htmlFor={`${idPrefix}-type`}>
|
<FormFieldSimple label="Тип" htmlFor={`${idPrefix}-type`}>
|
||||||
<Select
|
<Select
|
||||||
|
modal={false}
|
||||||
value={value.type}
|
value={value.type}
|
||||||
onValueChange={(v) => patch({ type: (v ?? 'tcp') as HealthCheckType })}
|
onValueChange={(v) => patch({ type: (v ?? 'tcp') as HealthCheckType })}
|
||||||
>
|
>
|
||||||
@@ -195,7 +251,10 @@ export function HealthCheckConfigFields({
|
|||||||
<SelectValue placeholder="Тип" />
|
<SelectValue placeholder="Тип" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{healthCheckTypes.map((item) => (
|
{(value.provider === 'cloudflare'
|
||||||
|
? cloudflareTypes
|
||||||
|
: healthCheckTypes
|
||||||
|
).map((item) => (
|
||||||
<SelectItem key={item.value} value={item.value}>
|
<SelectItem key={item.value} value={item.value}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -289,6 +348,57 @@ export function HealthCheckConfigFields({
|
|||||||
/>
|
/>
|
||||||
</FormFieldSimple>
|
</FormFieldSimple>
|
||||||
</div>
|
</div>
|
||||||
|
{value.provider === 'cloudflare' ? (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<FormFieldSimple
|
||||||
|
label="Retries"
|
||||||
|
htmlFor={`${idPrefix}-retries`}
|
||||||
|
hint="Cloudflare retries"
|
||||||
|
>
|
||||||
|
<CompactNumberField
|
||||||
|
id={`${idPrefix}-retries`}
|
||||||
|
value={value.retries ?? 2}
|
||||||
|
min={0}
|
||||||
|
max={10}
|
||||||
|
placeholder="2"
|
||||||
|
onValueChange={(retries) => patch({ retries: retries ?? 2 })}
|
||||||
|
/>
|
||||||
|
</FormFieldSimple>
|
||||||
|
<FormFieldSimple
|
||||||
|
label="Successes"
|
||||||
|
htmlFor={`${idPrefix}-successes`}
|
||||||
|
hint="consecutive_successes"
|
||||||
|
>
|
||||||
|
<CompactNumberField
|
||||||
|
id={`${idPrefix}-successes`}
|
||||||
|
value={value.consecutive_successes ?? 2}
|
||||||
|
min={1}
|
||||||
|
max={20}
|
||||||
|
placeholder="2"
|
||||||
|
onValueChange={(consecutive_successes) =>
|
||||||
|
patch({ consecutive_successes: consecutive_successes ?? 2 })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormFieldSimple>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{value.provider === 'cloudflare' && isHttp ? (
|
||||||
|
<FormFieldSimple label="HTTP method" htmlFor={`${idPrefix}-method`}>
|
||||||
|
<Select
|
||||||
|
modal={false}
|
||||||
|
value={value.method ?? 'GET'}
|
||||||
|
onValueChange={(v) => patch({ method: v ?? 'GET' })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`${idPrefix}-method`} className="w-full">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="GET">GET</SelectItem>
|
||||||
|
<SelectItem value="HEAD">HEAD</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormFieldSimple>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ interface BindingHealthConfig {
|
|||||||
interval_sec: number
|
interval_sec: number
|
||||||
timeout_ms: number
|
timeout_ms: number
|
||||||
verify_tls: boolean
|
verify_tls: boolean
|
||||||
|
provider: 'local' | 'cloudflare'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceBindingDraft {
|
export interface ServiceBindingDraft {
|
||||||
@@ -79,6 +80,7 @@ const defaultHealth: BindingHealthConfig = {
|
|||||||
interval_sec: 30,
|
interval_sec: 30,
|
||||||
timeout_ms: 3000,
|
timeout_ms: 3000,
|
||||||
verify_tls: false,
|
verify_tls: false,
|
||||||
|
provider: 'local',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceEditSheetProps {
|
interface ServiceEditSheetProps {
|
||||||
@@ -112,6 +114,7 @@ function toBindingDrafts(service: ServiceView): ServiceBindingDraft[] {
|
|||||||
interval_sec: binding.health_check_interval_sec,
|
interval_sec: binding.health_check_interval_sec,
|
||||||
timeout_ms: binding.health_check_timeout_ms,
|
timeout_ms: binding.health_check_timeout_ms,
|
||||||
verify_tls: binding.health_check_verify_tls ?? false,
|
verify_tls: binding.health_check_verify_tls ?? false,
|
||||||
|
provider: 'local',
|
||||||
},
|
},
|
||||||
target_ip_weights: binding.target_ip_weights ?? {},
|
target_ip_weights: binding.target_ip_weights ?? {},
|
||||||
target_ip_priorities: binding.target_ip_priorities ?? {},
|
target_ip_priorities: binding.target_ip_priorities ?? {},
|
||||||
@@ -328,6 +331,7 @@ export function ServiceEditSheet({
|
|||||||
interval_sec: next.interval_sec,
|
interval_sec: next.interval_sec,
|
||||||
timeout_ms: next.timeout_ms,
|
timeout_ms: next.timeout_ms,
|
||||||
verify_tls: next.verify_tls,
|
verify_tls: next.verify_tls,
|
||||||
|
provider: next.provider ?? 'local',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: item,
|
: item,
|
||||||
@@ -650,6 +654,7 @@ export function ServiceEditSheet({
|
|||||||
interval_sec: binding.health.interval_sec,
|
interval_sec: binding.health.interval_sec,
|
||||||
timeout_ms: binding.health.timeout_ms,
|
timeout_ms: binding.health.timeout_ms,
|
||||||
verify_tls: binding.health.verify_tls,
|
verify_tls: binding.health.verify_tls,
|
||||||
|
provider: binding.health.provider ?? 'local',
|
||||||
}}
|
}}
|
||||||
onChange={(next) => handleBindingHealthChange(index, next)}
|
onChange={(next) => handleBindingHealthChange(index, next)}
|
||||||
lbModeLabel="Режим балансировки"
|
lbModeLabel="Режим балансировки"
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ const defaultLbHealth: LbAndHealthConfig = {
|
|||||||
interval_sec: 30,
|
interval_sec: 30,
|
||||||
timeout_ms: 3000,
|
timeout_ms: 3000,
|
||||||
verify_tls: false,
|
verify_tls: false,
|
||||||
|
provider: 'local',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ServiceGroupEditSheet({
|
export function ServiceGroupEditSheet({
|
||||||
@@ -93,6 +94,7 @@ export function ServiceGroupEditSheet({
|
|||||||
interval_sec: group.health_check_interval_sec,
|
interval_sec: group.health_check_interval_sec,
|
||||||
timeout_ms: group.health_check_timeout_ms,
|
timeout_ms: group.health_check_timeout_ms,
|
||||||
verify_tls: group.health_check_verify_tls,
|
verify_tls: group.health_check_verify_tls,
|
||||||
|
provider: 'local',
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
form.reset({ name: '', type: 'custom', domain: null })
|
form.reset({ name: '', type: 'custom', domain: null })
|
||||||
|
|||||||
@@ -10,14 +10,8 @@ import { FormFieldSimple } from '@/components/form-field'
|
|||||||
import { LoadingButton } from '@/components/loading-button'
|
import { LoadingButton } from '@/components/loading-button'
|
||||||
import { Button } from '@cfdm/ui/components/button'
|
import { Button } from '@cfdm/ui/components/button'
|
||||||
import { Input } from '@cfdm/ui/components/input'
|
import { Input } from '@cfdm/ui/components/input'
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@cfdm/ui/components/select'
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/reui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/reui/alert'
|
||||||
|
import { HealthProviderToggle } from '@/components/health-check-config-fields'
|
||||||
import {
|
import {
|
||||||
createOriginHealthCheck,
|
createOriginHealthCheck,
|
||||||
listOriginHealthChecks,
|
listOriginHealthChecks,
|
||||||
@@ -125,20 +119,11 @@ export function ServiceHealthPage() {
|
|||||||
<Input id="hc-name" {...form.register('name')} />
|
<Input id="hc-name" {...form.register('name')} />
|
||||||
</FormFieldSimple>
|
</FormFieldSimple>
|
||||||
<FormFieldSimple label="Провайдер" htmlFor="hc-provider">
|
<FormFieldSimple label="Провайдер" htmlFor="hc-provider">
|
||||||
<Select
|
<HealthProviderToggle
|
||||||
|
id="hc-provider"
|
||||||
value={provider}
|
value={provider}
|
||||||
onValueChange={(value) =>
|
onChange={(next) => form.setValue('provider', next)}
|
||||||
form.setValue('provider', (value as 'local' | 'cloudflare') ?? 'local')
|
/>
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="hc-provider">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="local">Local</SelectItem>
|
|
||||||
<SelectItem value="cloudflare">Cloudflare</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormFieldSimple>
|
</FormFieldSimple>
|
||||||
{provider === 'cloudflare' ? (
|
{provider === 'cloudflare' ? (
|
||||||
<Alert>
|
<Alert>
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ Workflows: [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml), [.gitea/work
|
|||||||
|
|
||||||
Fallback для **git tag**: `gitea.token`, если PAT недоступен. Push образов в Container Registry — **только `ACTIONS_PAT`** (у job token Gitea нет права packages).
|
Fallback для **git tag**: `gitea.token`, если PAT недоступен. Push образов в Container Registry — **только `ACTIONS_PAT`** (у job token Gitea нет права packages).
|
||||||
|
|
||||||
Wiki: секрет **`GITEA_TOKEN`** (fallback `ACTIONS_PAT`) для clone/push `*.wiki.git`. Push идёт с HTTP `Authorization`, потому что git после clone вырезает токен из remote URL.
|
Wiki: секрет **`ACTIONS_PAT`** (fallback `GITEA_TOKEN`) для clone/push `*.wiki.git` на `https://git.shx.one` (не внутренний `GITEA_INSTANCE_URL` раннера). Токен передаётся в URL (`oauth2:<PAT>`): Gitea на неаутентифицированный wiki push отвечает 404, а не 401.
|
||||||
|
|
||||||
## Источник правды для версии
|
## Источник правды для версии
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user