mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
merge: resolve conflicts with origin/main (auto-fix formatting)
This commit is contained in:
commit
6f1a334150
22 changed files with 921 additions and 910 deletions
|
|
@ -54,7 +54,7 @@ bunx wrangler dev # Start local dev server
|
||||||
### Secrets (`.dev.vars` for local, Worker Secrets for production)
|
### Secrets (`.dev.vars` for local, Worker Secrets for production)
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|-----------------------------|------------------------------------------------------------------------------------------------|
|
| --------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from GitHub |
|
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from GitHub |
|
||||||
| `GITEA_WEBHOOK_SECRET` | Webhook secret from Gitea (required only to receive Gitea webhooks) |
|
| `GITEA_WEBHOOK_SECRET` | Webhook secret from Gitea (required only to receive Gitea webhooks) |
|
||||||
| `GITHUB_APP_ID` | GitHub App ID (used by the App install flow to resolve the installing account) |
|
| `GITHUB_APP_ID` | GitHub App ID (used by the App install flow to resolve the installing account) |
|
||||||
|
|
|
||||||
|
|
@ -258,12 +258,7 @@ if (view.value === null) {
|
||||||
throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: false });
|
throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { logs, loading: logsLoading, error: logsError, load: loadLogs } = useSendLogs();
|
||||||
logs,
|
|
||||||
loading: logsLoading,
|
|
||||||
error: logsError,
|
|
||||||
load: loadLogs,
|
|
||||||
} = useSendLogs();
|
|
||||||
const {
|
const {
|
||||||
entries: auditEntries,
|
entries: auditEntries,
|
||||||
loading: auditLoading,
|
loading: auditLoading,
|
||||||
|
|
|
||||||
|
|
@ -322,4 +322,3 @@ function save(): void {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,10 @@ const updated = computed(() => "2026-08-01");
|
||||||
const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en);
|
const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en);
|
||||||
const q = (p: string): string => `${p}?lang=${lang.value}`;
|
const q = (p: string): string => `${p}?lang=${lang.value}`;
|
||||||
const altLink = computed(() =>
|
const altLink = computed(() =>
|
||||||
q(props.active === "terms" ? "/terms" : "/privacy").replace(`lang=${lang.value}`, `lang=${altLang.value}`),
|
q(props.active === "terms" ? "/terms" : "/privacy").replace(
|
||||||
|
`lang=${lang.value}`,
|
||||||
|
`lang=${altLang.value}`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
useHead({ title: `${props.title} · WebHooker` });
|
useHead({ title: `${props.title} · WebHooker` });
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,11 @@
|
||||||
v-for="it in items"
|
v-for="it in items"
|
||||||
:key="it.label"
|
:key="it.label"
|
||||||
class="flex items-center gap-4 rounded-[14px] border border-border bg-surface px-5 py-[18px] text-text no-underline shadow-card transition-all duration-150 hover:-translate-y-0.5 hover:border-border-strong hover:shadow-card-hover"
|
class="flex items-center gap-4 rounded-[14px] border border-border bg-surface px-5 py-[18px] text-text no-underline shadow-card transition-all duration-150 hover:-translate-y-0.5 hover:border-border-strong hover:shadow-card-hover"
|
||||||
:class="it.primary ? 'border-accent bg-accent shadow-none hover:border-accent hover:shadow-accent-lg' : ''"
|
:class="
|
||||||
|
it.primary
|
||||||
|
? 'border-accent bg-accent shadow-none hover:border-accent hover:shadow-accent-lg'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
:href="it.href"
|
:href="it.href"
|
||||||
:target="it.external ? '_blank' : undefined"
|
:target="it.external ? '_blank' : undefined"
|
||||||
:rel="it.external ? 'noopener noreferrer' : undefined"
|
:rel="it.external ? 'noopener noreferrer' : undefined"
|
||||||
|
|
@ -68,7 +72,9 @@ const config = useRuntimeConfig();
|
||||||
const lang = computed(() => (route.query.lang === "en" ? "en" : "zh"));
|
const lang = computed(() => (route.query.lang === "en" ? "en" : "zh"));
|
||||||
const altLang = computed(() => (lang.value === "zh" ? "en" : "zh"));
|
const altLang = computed(() => (lang.value === "zh" ? "en" : "zh"));
|
||||||
const repo = computed(() => (config.public.repoUrl as string) || DEFAULT_REPO);
|
const repo = computed(() => (config.public.repoUrl as string) || DEFAULT_REPO);
|
||||||
const docsBase = computed(() => ((config.public.docsUrl as string) || DEFAULT_DOCS).replace(/\/+$/, ""));
|
const docsBase = computed(() =>
|
||||||
|
((config.public.docsUrl as string) || DEFAULT_DOCS).replace(/\/+$/, ""),
|
||||||
|
);
|
||||||
|
|
||||||
const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en);
|
const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en);
|
||||||
|
|
||||||
|
|
@ -119,7 +125,8 @@ const ICON_PATHS: Record<string, string> = {
|
||||||
docs: '<path d="M4 4a2 2 0 0 1 2-2h7l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z"/><path d="M13 2v5h5"/><path d="M8 12h8M8 16h6"/>',
|
docs: '<path d="M4 4a2 2 0 0 1 2-2h7l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z"/><path d="M13 2v5h5"/><path d="M8 12h8M8 16h6"/>',
|
||||||
github:
|
github:
|
||||||
'<path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.36 1.09 2.94.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2z"/>',
|
'<path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.36 1.09 2.94.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2z"/>',
|
||||||
terms: '<path d="M9 12h6M9 16h6M9 8h2"/><path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"/>',
|
terms:
|
||||||
|
'<path d="M9 12h6M9 16h6M9 8h2"/><path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"/>',
|
||||||
privacy:
|
privacy:
|
||||||
'<path d="M12 2l7 3v6c0 5-3.5 8.5-7 10-3.5-1.5-7-5-7-10V5l7-3z"/><path d="M9 12l2 2 4-4"/>',
|
'<path d="M12 2l7 3v6c0 5-3.5 8.5-7 10-3.5-1.5-7-5-7-10V5l7-3z"/><path d="M9 12l2 2 4-4"/>',
|
||||||
login:
|
login:
|
||||||
|
|
@ -143,4 +150,3 @@ useHead({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
This page is the reference for secrets and the Web UI. Core concepts live in dedicated pages:
|
This page is the reference for secrets and the Web UI. Core concepts live in dedicated pages:
|
||||||
|
|
||||||
| Topic | Page |
|
| Topic | Page |
|
||||||
|---------------------------------------------------|----------------------------------------------------------------------|
|
| ------------------------------------------------- | ------------------------------------------------------------------ |
|
||||||
| Routes, targets, `fallback` / `stop`, role pings | [Routes & Targets](./routes) |
|
| Routes, targets, `fallback` / `stop`, role pings | [Routes & Targets](./routes) |
|
||||||
| Groups, roles, invites, self sign-up, log channel | [Groups & Access Control](./groups) |
|
| Groups, roles, invites, self sign-up, log channel | [Groups & Access Control](./groups) |
|
||||||
| Webhook providers, per-group ingress, custom | [Webhook Ingress & Tenancy](./ingress) |
|
| Webhook providers, per-group ingress, custom | [Webhook Ingress & Tenancy](./ingress) |
|
||||||
| KV / D1 key layout | [Storage Layout](./storage) |
|
| KV / D1 key layout | [Storage Layout](./storage) |
|
||||||
| Filters (pattern syntax reference) | [Filter Types](#filter-types) below / [Filter Tutorial](./filters) |
|
| Filters (pattern syntax reference) | [Filter Types](#filter-types) below / [Filter Tutorial](./filters) |
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@ WebHooker requires several secrets to function. For local development, store the
|
||||||
### Required Secrets
|
### Required Secrets
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|-------------------------|--------------------------------------------------------------------------|
|
| ----------------------- | ------------------------------------------------------------------------ |
|
||||||
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
| `GITHUB_WEBHOOK_SECRET` | Webhook secret from your GitHub App settings |
|
||||||
| `GITEA_WEBHOOK_SECRET` | Webhook secret from your Gitea instance (only to receive Gitea webhooks) |
|
| `GITEA_WEBHOOK_SECRET` | Webhook secret from your Gitea instance (only to receive Gitea webhooks) |
|
||||||
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
| `GITHUB_CLIENT_ID` | OAuth client ID from App settings |
|
||||||
|
|
@ -35,7 +35,7 @@ WebHooker requires several secrets to function. For local development, store the
|
||||||
### Optional Secrets
|
### Optional Secrets
|
||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------|
|
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
||||||
| `DISCORD_PUBLIC_KEY` | Discord application public key (Developer Portal) — required for interactions | Unset → interactions return `401` |
|
| `DISCORD_PUBLIC_KEY` | Discord application public key (Developer Portal) — required for interactions | Unset → interactions return `401` |
|
||||||
| `DISCORD_APPLICATION_ID` | Discord application id; auto-resolved when omitted | Auto-resolved |
|
| `DISCORD_APPLICATION_ID` | Discord application id; auto-resolved when omitted | Auto-resolved |
|
||||||
| `TELEGRAM_WEBHOOK_SECRET` | Secret token for `POST /telegram/webhook` verification (X-Telegram-Bot-Api-Secret-Token) | Disabled (no verification) |
|
| `TELEGRAM_WEBHOOK_SECRET` | Secret token for `POST /telegram/webhook` verification (X-Telegram-Bot-Api-Secret-Token) | Disabled (no verification) |
|
||||||
|
|
@ -67,7 +67,7 @@ All management endpoints (`/admin/api/*`) are documented in the [Admin API](../a
|
||||||
See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
|
See the [Filter Tutorial](./filters) for a hands-on guide with worked examples.
|
||||||
|
|
||||||
| Type | Matches | Example |
|
| Type | Matches | Example |
|
||||||
|-----------|----------------------|------------------------------------|
|
| --------- | -------------------- | ---------------------------------- |
|
||||||
| `event` | GitHub event name | `push`, `pull_*`, `pull_request` |
|
| `event` | GitHub event name | `push`, `pull_*`, `pull_request` |
|
||||||
| `repo` | Repository full name | `org/repo`, `org/*` |
|
| `repo` | Repository full name | `org/repo`, `org/*` |
|
||||||
| `actor` | Sender login | `username`, `[bot]`, `*[bot]` |
|
| `actor` | Sender login | `username`, `[bot]`, `*[bot]` |
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,14 @@ The route above matches both `push` and `pull_request` events.
|
||||||
|
|
||||||
Every filter type shares the same three pattern forms:
|
Every filter type shares the same three pattern forms:
|
||||||
|
|
||||||
| Pattern | Meaning |
|
| Pattern | Meaning |
|
||||||
| ---------------------- | -------------------------------------------------------------- |
|
| ---------------------- | ------------------------------------------------------------- |
|
||||||
| `plain text` | Field filters: **exact** match. `keyword`: search anywhere. |
|
| `plain text` | Field filters: **exact** match. `keyword`: search anywhere. |
|
||||||
| `*` / `?` | **Glob wildcards** — `*` any run, `?` one char. |
|
| `*` / `?` | **Glob wildcards** — `*` any run, `?` one char. |
|
||||||
| `/regular expression/` | Compiled as a **regular expression** (case-insensitive flag). |
|
| `/regular expression/` | Compiled as a **regular expression** (case-insensitive flag). |
|
||||||
|
|
||||||
- On field filters (`event`/`repo`/`actor`/`action`/`branch`), plain text and globs match the whole value; on `keyword` they search anywhere in the payload.
|
- On field filters (`event`/`repo`/`actor`/`action`/`branch`), plain text and globs match the whole value; on `keyword` they search anywhere in the payload.
|
||||||
- Regexes always search: `/^feat/` matches values *starting* with `feat`, `/feat/` matches anywhere.
|
- Regexes always search: `/^feat/` matches values _starting_ with `feat`, `/feat/` matches anywhere.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
|
|
@ -259,5 +259,5 @@ Forward workflow runs that ended in failure on any branch, to a `#ci` channel:
|
||||||
- **A `//`-wrapped invalid regex never matches.** Unlike plain text, an unwrapped invalid pattern is matched literally — wrap patterns only when they are real regular expressions.
|
- **A `//`-wrapped invalid regex never matches.** Unlike plain text, an unwrapped invalid pattern is matched literally — wrap patterns only when they are real regular expressions.
|
||||||
- **An `action` filter on an action-less event never matches.** Check the event has an `action` field first (see [Filter Compatibility](../events/supported#filter-compatibility)).
|
- **An `action` filter on an action-less event never matches.** Check the event has an `action` field first (see [Filter Compatibility](../events/supported#filter-compatibility)).
|
||||||
- **`branch` on an event without a branch never matches.** A `branch` filter on an `issues` event will always be false. Use `keyword` if you need branch-like matching there.
|
- **`branch` on an event without a branch never matches.** A `branch` filter on an `issues` event will always be false. Use `keyword` if you need branch-like matching there.
|
||||||
- **`keyword` searches everything.** Because it scans the whole payload, a pattern like `"fix"` can match commit messages, issue titles, *and* repository names. Be as specific as possible.
|
- **`keyword` searches everything.** Because it scans the whole payload, a pattern like `"fix"` can match commit messages, issue titles, _and_ repository names. Be as specific as possible.
|
||||||
- **Forgetting `exclude` semantics.** `exclude: true` negates the whole filter — one non-matching value in an array does not "block" the route; the negated filter matches only when *none* of the values match.
|
- **Forgetting `exclude` semantics.** `exclude: true` negates the whole filter — one non-matching value in an array does not "block" the route; the negated filter matches only when _none_ of the values match.
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ curl http://localhost:8787/health
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
| Script | Description |
|
| Script | Description |
|
||||||
|------------------------|---------------------------------------------|
|
| ---------------------- | ------------------------------------------- |
|
||||||
| `bun run dev` | Start Nuxt dev server (HMR) |
|
| `bun run dev` | Start Nuxt dev server (HMR) |
|
||||||
| `bun run build` | Production build (cloudflare_module preset) |
|
| `bun run build` | Production build (cloudflare_module preset) |
|
||||||
| `bun run deploy` | Deploy to Cloudflare |
|
| `bun run deploy` | Deploy to Cloudflare |
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
本页是密钥与 Web 控制台的参考。核心概念在独立页面中说明:
|
本页是密钥与 Web 控制台的参考。核心概念在独立页面中说明:
|
||||||
|
|
||||||
| 主题 | 页面 |
|
| 主题 | 页面 |
|
||||||
|--------------------------------------------------|----------------------------------------------------------------------|
|
| ------------------------------------------- | ------------------------------------------------------- |
|
||||||
| 路由、目标、`fallback` / `stop`、身份组提醒 | [路由与目标](./routes) |
|
| 路由、目标、`fallback` / `stop`、身份组提醒 | [路由与目标](./routes) |
|
||||||
| 分组、角色、邀请、自助注册、日志频道 | [分组与访问控制](./groups) |
|
| 分组、角色、邀请、自助注册、日志频道 | [分组与访问控制](./groups) |
|
||||||
| Webhook 提供方、分组入口、自定义 webhook | [Webhook 接入与租户隔离](./ingress) |
|
| Webhook 提供方、分组入口、自定义 webhook | [Webhook 接入与租户隔离](./ingress) |
|
||||||
| KV / D1 键布局 | [存储布局](./storage) |
|
| KV / D1 键布局 | [存储布局](./storage) |
|
||||||
| 过滤器(模式语法参考) | 下方[过滤器类型](#过滤器类型) / [过滤器教程](./filters) |
|
| 过滤器(模式语法参考) | 下方[过滤器类型](#过滤器类型) / [过滤器教程](./filters) |
|
||||||
|
|
||||||
## 密钥
|
## 密钥
|
||||||
|
|
||||||
|
|
@ -16,14 +16,14 @@ WebHooker 的运行需要若干密钥。本地开发时放入 `.dev.vars`,生
|
||||||
|
|
||||||
### 必需密钥
|
### 必需密钥
|
||||||
|
|
||||||
| 变量 | 说明 |
|
| 变量 | 说明 |
|
||||||
|-------------------------|-------------------------------------------------------------------|
|
| ----------------------- | ----------------------------------------------------------- |
|
||||||
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 webhook 密钥 |
|
| `GITHUB_WEBHOOK_SECRET` | GitHub App 设置中的 webhook 密钥 |
|
||||||
| `GITEA_WEBHOOK_SECRET` | Gitea 实例的 webhook 密钥(仅接收 Gitea webhook 时需要) |
|
| `GITEA_WEBHOOK_SECRET` | Gitea 实例的 webhook 密钥(仅接收 Gitea webhook 时需要) |
|
||||||
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
| `GITHUB_CLIENT_ID` | App 设置中的 OAuth 客户端 ID |
|
||||||
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
| `GITHUB_CLIENT_SECRET` | App 设置中的 OAuth 客户端密钥 |
|
||||||
| `DISCORD_TOKEN` | Discord 机器人 Token |
|
| `DISCORD_TOKEN` | Discord 机器人 Token |
|
||||||
| `TELEGRAM_TOKEN` | Telegram 机器人 Token(BotFather 获取)—— Telegram 路由必需 |
|
| `TELEGRAM_TOKEN` | Telegram 机器人 Token(BotFather 获取)—— Telegram 路由必需 |
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> `GITHUB_APP_ID` 与 `GITHUB_PRIVATE_KEY`(PKCS#8 PEM)用于 GitHub App **安装流程**
|
> `GITHUB_APP_ID` 与 `GITHUB_PRIVATE_KEY`(PKCS#8 PEM)用于 GitHub App **安装流程**
|
||||||
|
|
@ -33,19 +33,19 @@ WebHooker 的运行需要若干密钥。本地开发时放入 `.dev.vars`,生
|
||||||
|
|
||||||
### 可选密钥
|
### 可选密钥
|
||||||
|
|
||||||
| 变量 | 说明 | 默认值 |
|
| 变量 | 说明 | 默认值 |
|
||||||
|-----------------------------|--------------------------------------------------------------------------------------------------|-------------------------|
|
| --------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||||
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
|
| `DISCORD_PUBLIC_KEY` | Discord 应用的公钥(开发者门户获取),交互功能必需 | 未设置时交互返回 401 |
|
||||||
| `DISCORD_APPLICATION_ID` | Discord 应用 ID;省略时自动获取 | 自动获取 |
|
| `DISCORD_APPLICATION_ID` | Discord 应用 ID;省略时自动获取 | 自动获取 |
|
||||||
| `TELEGRAM_WEBHOOK_SECRET` | `POST /telegram/webhook` 验签密钥(X-Telegram-Bot-Api-Secret-Token) | 未设置时不校验 |
|
| `TELEGRAM_WEBHOOK_SECRET` | `POST /telegram/webhook` 验签密钥(X-Telegram-Bot-Api-Secret-Token) | 未设置时不校验 |
|
||||||
| `TELEGRAM_RICH_HEADER_HOST` | 外部 rich-header 服务的基础 URL;未设置时使用内置 `GET /api/richheader` 提供 Telegram 头像卡片 | 内置 `/api/richheader` |
|
| `TELEGRAM_RICH_HEADER_HOST` | 外部 rich-header 服务的基础 URL;未设置时使用内置 `GET /api/richheader` 提供 Telegram 头像卡片 | 内置 `/api/richheader` |
|
||||||
| `BASE_URL` | OAuth 回调的公共 URL | `http://localhost:8787` |
|
| `BASE_URL` | OAuth 回调的公共 URL | `http://localhost:8787` |
|
||||||
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID(或登录名),逗号分隔 | 未设置时 WebUI 关闭 |
|
| `ADMIN_USER_IDS` | 允许访问 WebUI 的 GitHub 用户 ID(或登录名),逗号分隔 | 未设置时 WebUI 关闭 |
|
||||||
| `ALLOW_SELF_SIGNUP` | 开启(`1`/`true`)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 | 关闭 |
|
| `ALLOW_SELF_SIGNUP` | 开启(`1`/`true`)后,没有任何分组权限的 GitHub 用户首次登录会自动获得个人分组而非 403 | 关闭 |
|
||||||
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数 | `90` |
|
| `AUDIT_RETENTION_DAYS` | 定时清理时审计日志的保留天数 | `90` |
|
||||||
| `NUXT_PUBLIC_DOCS_URL` | 落地页使用的文档站 URL(客户端运行时配置) | 落地页默认值 |
|
| `NUXT_PUBLIC_DOCS_URL` | 落地页使用的文档站 URL(客户端运行时配置) | 落地页默认值 |
|
||||||
| `NUXT_PUBLIC_REPO_URL` | 落地页使用的 GitHub 仓库 URL | 落地页默认值 |
|
| `NUXT_PUBLIC_REPO_URL` | 落地页使用的 GitHub 仓库 URL | 落地页默认值 |
|
||||||
| `NUXT_PUBLIC_LEGAL_CONTACT` | `/terms` 与 `/privacy` 页面展示的联系方式 | 未设置时显示占位文本 |
|
| `NUXT_PUBLIC_LEGAL_CONTACT` | `/terms` 与 `/privacy` 页面展示的联系方式 | 未设置时显示占位文本 |
|
||||||
|
|
||||||
## Web 控制台
|
## Web 控制台
|
||||||
|
|
||||||
|
|
@ -65,14 +65,14 @@ WebHooker 在 `/admin` 提供内置配置控制台,可在浏览器中管理路
|
||||||
|
|
||||||
实操指南见[过滤器教程](./filters),包含完整示例。
|
实操指南见[过滤器教程](./filters),包含完整示例。
|
||||||
|
|
||||||
| 类型 | 匹配对象 | 示例 |
|
| 类型 | 匹配对象 | 示例 |
|
||||||
|-----------|------------------|--------------------------------------|
|
| --------- | ---------------- | ---------------------------------- |
|
||||||
| `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` |
|
| `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` |
|
||||||
| `repo` | 仓库全名 | `org/repo`, `org/*` |
|
| `repo` | 仓库全名 | `org/repo`, `org/*` |
|
||||||
| `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` |
|
| `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` |
|
||||||
| `action` | 事件操作 | `opened`, `closed`, `published` |
|
| `action` | 事件操作 | `opened`, `closed`, `published` |
|
||||||
| `branch` | 分支名称 | `main`, `feature-?`, `/^release-/` |
|
| `branch` | 分支名称 | `main`, `feature-?`, `/^release-/` |
|
||||||
| `keyword` | 载荷正文中的文本 | `deploy`, `/fix\s+\d+/` |
|
| `keyword` | 载荷正文中的文本 | `deploy`, `/fix\s+\d+/` |
|
||||||
|
|
||||||
### 过滤器行为
|
### 过滤器行为
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@
|
||||||
|
|
||||||
所有过滤器类型共享以下三种模式写法:
|
所有过滤器类型共享以下三种模式写法:
|
||||||
|
|
||||||
| 模式 | 含义 |
|
| 模式 | 含义 |
|
||||||
| -------------- | ----------------------------------------------------------------- |
|
| -------------- | --------------------------------------------------------------- |
|
||||||
| `纯文本` | 字段过滤器:**完全相等**匹配;`keyword`:在载荷中任意位置搜索。 |
|
| `纯文本` | 字段过滤器:**完全相等**匹配;`keyword`:在载荷中任意位置搜索。 |
|
||||||
| `*` / `?` | **通配符(glob)**——`*` 任意长度、`?` 恰好一个字符。 |
|
| `*` / `?` | **通配符(glob)**——`*` 任意长度、`?` 恰好一个字符。 |
|
||||||
| `/正则表达式/` | 按**正则表达式**编译(忽略大小写标志)。 |
|
| `/正则表达式/` | 按**正则表达式**编译(忽略大小写标志)。 |
|
||||||
|
|
||||||
- 字段过滤器(`event`/`repo`/`actor`/`action`/`branch`)的纯文本与通配符匹配整个值;`keyword` 则在载荷中任意位置搜索。
|
- 字段过滤器(`event`/`repo`/`actor`/`action`/`branch`)的纯文本与通配符匹配整个值;`keyword` 则在载荷中任意位置搜索。
|
||||||
- 正则表达式始终是搜索语义:`/^feat/` 匹配以 `feat` **开头**的值,`/feat/` 匹配任意位置出现 `feat` 的值。
|
- 正则表达式始终是搜索语义:`/^feat/` 匹配以 `feat` **开头**的值,`/feat/` 匹配任意位置出现 `feat` 的值。
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
import defaultNitroErrorHandler, {
|
import defaultNitroErrorHandler, { defineNitroErrorHandler } from "nitropack/runtime/error";
|
||||||
defineNitroErrorHandler,
|
|
||||||
} from "nitropack/runtime/error";
|
|
||||||
import { setResponseStatus } from "h3";
|
import { setResponseStatus } from "h3";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ async function readJsonBody(event: H3Event): Promise<Record<string, unknown> | n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function userOctokit(event: H3Event, userId: string): Promise<Awaited<ReturnType<typeof getUserOctokit>>> {
|
async function userOctokit(
|
||||||
|
event: H3Event,
|
||||||
|
userId: string,
|
||||||
|
): Promise<Awaited<ReturnType<typeof getUserOctokit>>> {
|
||||||
return getUserOctokit(userId, cfEnv(event).KV);
|
return getUserOctokit(userId, cfEnv(event).KV);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,16 +126,7 @@ export async function apiClose(event: H3Event): Promise<Record<string, unknown>>
|
||||||
export async function apiReact(event: H3Event): Promise<Record<string, unknown>> {
|
export async function apiReact(event: H3Event): Promise<Record<string, unknown>> {
|
||||||
const userId = await bearerUserId(event);
|
const userId = await bearerUserId(event);
|
||||||
const body = await readJsonBody(event);
|
const body = await readJsonBody(event);
|
||||||
const reactions = [
|
const reactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] as const;
|
||||||
"+1",
|
|
||||||
"-1",
|
|
||||||
"laugh",
|
|
||||||
"confused",
|
|
||||||
"heart",
|
|
||||||
"hooray",
|
|
||||||
"rocket",
|
|
||||||
"eyes",
|
|
||||||
] as const;
|
|
||||||
if (
|
if (
|
||||||
!body ||
|
!body ||
|
||||||
!isNonEmptyString(body.owner) ||
|
!isNonEmptyString(body.owner) ||
|
||||||
|
|
@ -151,14 +145,7 @@ export async function apiReact(event: H3Event): Promise<Record<string, unknown>>
|
||||||
repo: body.repo,
|
repo: body.repo,
|
||||||
issue_number: body.issueNumber,
|
issue_number: body.issueNumber,
|
||||||
content: body.reaction as
|
content: body.reaction as
|
||||||
| "+1"
|
"+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
|
||||||
| "-1"
|
|
||||||
| "laugh"
|
|
||||||
| "confused"
|
|
||||||
| "heart"
|
|
||||||
| "hooray"
|
|
||||||
| "rocket"
|
|
||||||
| "eyes",
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error({ err }, "Failed to create reaction");
|
log.error({ err }, "Failed to create reaction");
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,10 @@ export async function adminInvite(event: H3Event): Promise<void> {
|
||||||
}
|
}
|
||||||
const session = await getAdminSession(env.KV, getHeader(event, "cookie"));
|
const session = await getAdminSession(env.KV, getHeader(event, "cookie"));
|
||||||
if (!session) {
|
if (!session) {
|
||||||
await sendRedirect(event, `/auth/github?redirect=${encodeURIComponent(`/admin/invite?token=${token}`)}`);
|
await sendRedirect(
|
||||||
|
event,
|
||||||
|
`/auth/github?redirect=${encodeURIComponent(`/admin/invite?token=${token}`)}`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await acceptInvite(env.KV, token, session.userId, session.login);
|
const result = await acceptInvite(env.KV, token, session.userId, session.login);
|
||||||
|
|
@ -480,8 +483,7 @@ export async function adminApiGroupsPut(event: H3Event): Promise<Record<string,
|
||||||
const members = g.members ?? normalizeGroupMembers(g);
|
const members = g.members ?? normalizeGroupMembers(g);
|
||||||
const stillMine = members.some(
|
const stillMine = members.some(
|
||||||
(m) =>
|
(m) =>
|
||||||
m.role === "owner" &&
|
m.role === "owner" && identityMatches([m.login], auth.session.userId, auth.session.login),
|
||||||
identityMatches([m.login], auth.session.userId, auth.session.login),
|
|
||||||
);
|
);
|
||||||
const otherOwner = ownerCount(members) > 1;
|
const otherOwner = ownerCount(members) > 1;
|
||||||
if (!stillMine && !otherOwner) {
|
if (!stillMine && !otherOwner) {
|
||||||
|
|
@ -648,7 +650,10 @@ export async function adminApiLogs(event: H3Event): Promise<Record<string, unkno
|
||||||
}
|
}
|
||||||
|
|
||||||
/** GET /admin/api/logs/:id */
|
/** GET /admin/api/logs/:id */
|
||||||
export async function adminApiLogsById(event: H3Event, id: number): Promise<Record<string, unknown>> {
|
export async function adminApiLogsById(
|
||||||
|
event: H3Event,
|
||||||
|
id: number,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
const auth = await requireAnyAccess(event);
|
const auth = await requireAnyAccess(event);
|
||||||
const env = cfEnv(event);
|
const env = cfEnv(event);
|
||||||
if (!Number.isInteger(id) || id <= 0) return respondError(event, 400, "Invalid log id");
|
if (!Number.isInteger(id) || id <= 0) return respondError(event, 400, "Invalid log id");
|
||||||
|
|
@ -783,7 +788,10 @@ export async function adminGroupInvitesGet(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** DELETE /admin/api/invites/:token */
|
/** DELETE /admin/api/invites/:token */
|
||||||
export async function adminInviteDelete(event: H3Event, token: string): Promise<Record<string, unknown>> {
|
export async function adminInviteDelete(
|
||||||
|
event: H3Event,
|
||||||
|
token: string,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
await requireAnyAccess(event);
|
await requireAnyAccess(event);
|
||||||
const env = cfEnv(event);
|
const env = cfEnv(event);
|
||||||
const invite = await getInvite(env.KV, token);
|
const invite = await getInvite(env.KV, token);
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,7 @@ export function requireGroup(event: H3Event, groupId: string): GroupAccess {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Requires at least `min` role in the group (owner|admin|viewer). */
|
/** Requires at least `min` role in the group (owner|admin|viewer). */
|
||||||
export function requireGroupRole(
|
export function requireGroupRole(event: H3Event, groupId: string, min: GroupRole): GroupAccess {
|
||||||
event: H3Event,
|
|
||||||
groupId: string,
|
|
||||||
min: GroupRole,
|
|
||||||
): GroupAccess {
|
|
||||||
const access = requireGroup(event, groupId);
|
const access = requireGroup(event, groupId);
|
||||||
if (!access.ok) return access;
|
if (!access.ok) return access;
|
||||||
const auth = currentAuth(event);
|
const auth = currentAuth(event);
|
||||||
|
|
@ -98,7 +94,6 @@ export async function bearerUserId(event: H3Event): Promise<string> {
|
||||||
if (!auth?.startsWith("Bearer "))
|
if (!auth?.startsWith("Bearer "))
|
||||||
throw createError({ statusCode: 401, statusMessage: "Missing authorization" });
|
throw createError({ statusCode: 401, statusMessage: "Missing authorization" });
|
||||||
const userId = await findUserIdByToken(env.KV, auth.slice(7));
|
const userId = await findUserIdByToken(env.KV, auth.slice(7));
|
||||||
if (!userId)
|
if (!userId) throw createError({ statusCode: 401, statusMessage: "Invalid or expired token" });
|
||||||
throw createError({ statusCode: 401, statusMessage: "Invalid or expired token" });
|
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -157,8 +157,11 @@ export async function handleInstallPage(event: H3Event): Promise<string | void>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const accountLogin =
|
const accountLogin =
|
||||||
(await getInstallationAccount(env.GITHUB_APP_ID ?? "", env.GITHUB_PRIVATE_KEY ?? "", installationId)) ??
|
(await getInstallationAccount(
|
||||||
"";
|
env.GITHUB_APP_ID ?? "",
|
||||||
|
env.GITHUB_PRIVATE_KEY ?? "",
|
||||||
|
installationId,
|
||||||
|
)) ?? "";
|
||||||
const groups = await loadGroups(env.KV);
|
const groups = await loadGroups(env.KV);
|
||||||
const scope = resolveScope(env, groups, session.userId, session.login);
|
const scope = resolveScope(env, groups, session.userId, session.login);
|
||||||
const owned = groups.filter((g) => roleAt(scope, g.id) === "owner");
|
const owned = groups.filter((g) => roleAt(scope, g.id) === "owner");
|
||||||
|
|
@ -216,8 +219,11 @@ export async function handleInstallBind(event: H3Event): Promise<void> {
|
||||||
|
|
||||||
// Default: auto-create a dedicated inst-{id} group.
|
// Default: auto-create a dedicated inst-{id} group.
|
||||||
const accountLogin =
|
const accountLogin =
|
||||||
(await getInstallationAccount(env.GITHUB_APP_ID ?? "", env.GITHUB_PRIVATE_KEY ?? "", installationId)) ??
|
(await getInstallationAccount(
|
||||||
"";
|
env.GITHUB_APP_ID ?? "",
|
||||||
|
env.GITHUB_PRIVATE_KEY ?? "",
|
||||||
|
installationId,
|
||||||
|
)) ?? "";
|
||||||
const group = await ensureInstallationGroup(env.KV, installationId, accountLogin);
|
const group = await ensureInstallationGroup(env.KV, installationId, accountLogin);
|
||||||
if (!group) {
|
if (!group) {
|
||||||
await sendRedirect(event, "/admin?error=install");
|
await sendRedirect(event, "/admin?error=install");
|
||||||
|
|
@ -248,7 +254,10 @@ export async function handleInstallBind(event: H3Event): Promise<void> {
|
||||||
adminIds: [...new Set([...(group.adminIds ?? []), session.login])],
|
adminIds: [...new Set([...(group.adminIds ?? []), session.login])],
|
||||||
};
|
};
|
||||||
const all = await loadGroups(env.KV);
|
const all = await loadGroups(env.KV);
|
||||||
await saveGroups(env.KV, all.map((g) => (g.id === group.id ? updated : g)));
|
await saveGroups(
|
||||||
|
env.KV,
|
||||||
|
all.map((g) => (g.id === group.id ? updated : g)),
|
||||||
|
);
|
||||||
await recordAudit(env.DB, {
|
await recordAudit(env.DB, {
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
actorId: session.userId,
|
actorId: session.userId,
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,7 @@ export async function processWebhook(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** h3 wrapper for `POST /webhook` / `POST /webhook/:groupId`. */
|
/** h3 wrapper for `POST /webhook` / `POST /webhook/:groupId`. */
|
||||||
export async function handleWebhookRequest(
|
export async function handleWebhookRequest(event: H3Event, tenantId?: string): Promise<unknown> {
|
||||||
event: H3Event,
|
|
||||||
tenantId?: string,
|
|
||||||
): Promise<unknown> {
|
|
||||||
const contentLength = Number(getHeader(event, "content-length") ?? 0);
|
const contentLength = Number(getHeader(event, "content-length") ?? 0);
|
||||||
if (contentLength > MAX_BODY_SIZE) {
|
if (contentLength > MAX_BODY_SIZE) {
|
||||||
setResponseStatus(event, 413);
|
setResponseStatus(event, 413);
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ export default <Partial<Config>>{
|
||||||
card: "var(--shadow)",
|
card: "var(--shadow)",
|
||||||
"card-hover": "0 8px 24px -12px rgba(15, 23, 42, 0.25)",
|
"card-hover": "0 8px 24px -12px rgba(15, 23, 42, 0.25)",
|
||||||
"accent-lg": "0 12px 28px -10px var(--accent)",
|
"accent-lg": "0 12px 28px -10px var(--accent)",
|
||||||
"drawer": "-16px 0 48px rgba(15, 23, 42, 0.12)",
|
drawer: "-16px 0 48px rgba(15, 23, 42, 0.12)",
|
||||||
"modal": "0 12px 40px rgba(15, 23, 42, 0.25)",
|
modal: "0 12px 40px rgba(15, 23, 42, 0.25)",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
rise: {
|
rise: {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
import { describe, it, expect } from "bun:test";
|
import { describe, it, expect } from "bun:test";
|
||||||
import {
|
import { adminGroupRename, adminGroupRoutesGet, adminApiMe } from "../server/lib/web/admin";
|
||||||
adminGroupRename,
|
|
||||||
adminGroupRoutesGet,
|
|
||||||
adminApiMe,
|
|
||||||
} from "../server/lib/web/admin";
|
|
||||||
import { createAdminSession, adminCookie } from "../server/lib/web/session";
|
import { createAdminSession, adminCookie } from "../server/lib/web/session";
|
||||||
import { loadGroups } from "../server/lib/web/groups";
|
import { loadGroups } from "../server/lib/web/groups";
|
||||||
import { loadRoutes } from "../server/lib/config";
|
import { loadRoutes } from "../server/lib/config";
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ describe("message title spec", () => {
|
||||||
expect(msg.title).toBe(
|
expect(msg.title).toBe(
|
||||||
"acme/widget: Pushed 1 commit to [`main`](https://github.com/acme/widget/tree/main)",
|
"acme/widget: Pushed 1 commit to [`main`](https://github.com/acme/widget/tree/main)",
|
||||||
);
|
);
|
||||||
expect(msg.description).toBe("[View comparison](https://github.com/acme/widget/compare/abc...def)");
|
expect(msg.description).toBe(
|
||||||
|
"[View comparison](https://github.com/acme/widget/compare/abc...def)",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pull_request title is repo#number: title", () => {
|
it("pull_request title is repo#number: title", () => {
|
||||||
|
|
@ -101,7 +103,9 @@ describe("message title spec", () => {
|
||||||
sender,
|
sender,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(msg.title).toBe("acme/widget: [CI — success](https://github.com/acme/widget/actions/runs/42)");
|
expect(msg.title).toBe(
|
||||||
|
"acme/widget: [CI — success](https://github.com/acme/widget/actions/runs/42)",
|
||||||
|
);
|
||||||
expect(msg.fields![1].value).toBe("✅ build");
|
expect(msg.fields![1].value).toBe("✅ build");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -122,7 +126,9 @@ describe("message title spec", () => {
|
||||||
sender,
|
sender,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(queued.title).toBe("acme/widget: [CI — queued](https://github.com/acme/widget/actions/runs/42)");
|
expect(queued.title).toBe(
|
||||||
|
"acme/widget: [CI — queued](https://github.com/acme/widget/actions/runs/42)",
|
||||||
|
);
|
||||||
expect(queued.fields![0].value).toBe("⏳ queued");
|
expect(queued.fields![0].value).toBe("⏳ queued");
|
||||||
|
|
||||||
const running = formatEvent(
|
const running = formatEvent(
|
||||||
|
|
@ -134,7 +140,9 @@ describe("message title spec", () => {
|
||||||
sender,
|
sender,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(running.title).toBe("acme/widget: [CI — running](https://github.com/acme/widget/actions/runs/42)");
|
expect(running.title).toBe(
|
||||||
|
"acme/widget: [CI — running](https://github.com/acme/widget/actions/runs/42)",
|
||||||
|
);
|
||||||
expect(running.fields![0].value).toBe("🔄 running");
|
expect(running.fields![0].value).toBe("🔄 running");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,10 @@ describe("POST /auth/github/install/bind", () => {
|
||||||
const env = createEnv({ KV: kv });
|
const env = createEnv({ KV: kv });
|
||||||
const sessionId = await createAdminSession(kv, "1001", "alice");
|
const sessionId = await createAdminSession(kv, "1001", "alice");
|
||||||
|
|
||||||
const event = bindEvent(env, adminCookie(sessionId), { installation_id: "555", group: "theirs" });
|
const event = bindEvent(env, adminCookie(sessionId), {
|
||||||
|
installation_id: "555",
|
||||||
|
group: "theirs",
|
||||||
|
});
|
||||||
await handleInstallBind(event);
|
await handleInstallBind(event);
|
||||||
expect(responseStatus(event)).toBe(302);
|
expect(responseStatus(event)).toBe(302);
|
||||||
expect(responseHeader(event, "location")).toBe("/admin?error=forbidden");
|
expect(responseHeader(event, "location")).toBe("/admin?error=forbidden");
|
||||||
|
|
@ -329,7 +332,10 @@ describe("oauth misc", () => {
|
||||||
it("starts the OAuth flow with a state token", async () => {
|
it("starts the OAuth flow with a state token", async () => {
|
||||||
const kv = createMockKV();
|
const kv = createMockKV();
|
||||||
const env = createEnv({ KV: kv, GITHUB_CLIENT_ID: "client-1" });
|
const env = createEnv({ KV: kv, GITHUB_CLIENT_ID: "client-1" });
|
||||||
const event = makeEvent("/auth/github?redirect=/admin", { headers: { accept: "text/html" }, env });
|
const event = makeEvent("/auth/github?redirect=/admin", {
|
||||||
|
headers: { accept: "text/html" },
|
||||||
|
env,
|
||||||
|
});
|
||||||
await handleOAuthStart(event);
|
await handleOAuthStart(event);
|
||||||
expect(responseStatus(event)).toBe(302);
|
expect(responseStatus(event)).toBe(302);
|
||||||
const location = responseHeader(event, "location") ?? "";
|
const location = responseHeader(event, "location") ?? "";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { describe, it, expect, beforeEach } from "bun:test";
|
import { describe, it, expect, beforeEach } from "bun:test";
|
||||||
import { createInvite, getInvite, listInvites, revokeInvite, acceptInvite } from "../server/lib/web/invites";
|
import {
|
||||||
|
createInvite,
|
||||||
|
getInvite,
|
||||||
|
listInvites,
|
||||||
|
revokeInvite,
|
||||||
|
acceptInvite,
|
||||||
|
} from "../server/lib/web/invites";
|
||||||
import { saveGroups, loadGroups } from "../server/lib/web/groups";
|
import { saveGroups, loadGroups } from "../server/lib/web/groups";
|
||||||
import type { Group } from "../server/lib/types";
|
import type { Group } from "../server/lib/types";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -184,22 +184,20 @@ describe("matchRoute", () => {
|
||||||
|
|
||||||
it("matches branch filter with single-char ? wildcard", () => {
|
it("matches branch filter with single-char ? wildcard", () => {
|
||||||
const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "feature-?" }] };
|
const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "feature-?" }] };
|
||||||
expect(
|
expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } })).toBe(
|
||||||
matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-xy" } })).toBe(
|
||||||
matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-xy" } }),
|
false,
|
||||||
).toBe(false);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches branch filter with //-wrapped regex", () => {
|
it("matches branch filter with //-wrapped regex", () => {
|
||||||
const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "/^feat/" }] };
|
const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "/^feat/" }] };
|
||||||
expect(
|
expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } })).toBe(
|
||||||
matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/main" } })).toBe(false);
|
||||||
matchRoute(route, { event: "push", payload: { ref: "refs/heads/main" } }),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats glob special chars literally when not wrapped", () => {
|
it("treats glob special chars literally when not wrapped", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue