Merge pull request #126 from fsolla/cursor/fix-vercel-domain-gitbranch-1749
fix(ci) — clear domain gitBranch so CLI prod aliases work
This commit is contained in:
commit
b5303bd8d6
6 changed files with 252 additions and 25 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -264,7 +264,8 @@ jobs:
|
|||
# Domains" is also OFF, --prod only stages *.vercel.app and leaves
|
||||
# pt.jorgesolla.com.br on the previous Current — promote + verify here.
|
||||
node scripts/vercel-ensure-production-alias.mjs "$deploy_url"
|
||||
# CLI alias when Git Integration is disabled (Vercel docs).
|
||||
# Docs: promote staged production, then alias (Git Integration is OFF by design).
|
||||
vercel promote "$deploy_url" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes || true
|
||||
vercel alias set "$deploy_url" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes
|
||||
|
||||
requeue:
|
||||
|
|
|
|||
24
.github/workflows/vercel-promote.yml
vendored
24
.github/workflows/vercel-promote.yml
vendored
|
|
@ -49,11 +49,23 @@ jobs:
|
|||
set -euo pipefail
|
||||
if [ -n "${DEPLOYMENT_REF:-}" ]; then
|
||||
ref="$DEPLOYMENT_REF"
|
||||
node scripts/vercel-ensure-production-alias.mjs "$ref"
|
||||
else
|
||||
node scripts/vercel-ensure-production-alias.mjs --latest
|
||||
fi
|
||||
# Belt-and-suspenders: CLI alias set (docs recommend this when Git Integration is off).
|
||||
if [ -n "${DEPLOYMENT_REF:-}" ]; then
|
||||
vercel alias set "$DEPLOYMENT_REF" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes
|
||||
ref=$(node --input-type=module -e '
|
||||
import { fetchLatestProductionReadyDeployment } from "./scripts/lib/vercel-production-alias.mjs";
|
||||
const d = await fetchLatestProductionReadyDeployment({
|
||||
token: process.env.VERCEL_TOKEN,
|
||||
projectId: process.env.VERCEL_PROJECT_ID,
|
||||
teamId: process.env.VERCEL_ORG_ID,
|
||||
});
|
||||
if (!d) {
|
||||
console.error("no READY production deployment");
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write(d.url ? `https://${d.url}` : d.id);
|
||||
')
|
||||
echo "Latest READY production: $ref"
|
||||
fi
|
||||
node scripts/vercel-ensure-production-alias.mjs "$ref"
|
||||
# Docs: when Auto-assign is/was OFF, prefer `vercel promote` then `alias set`.
|
||||
vercel promote "$ref" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes || true
|
||||
vercel alias set "$ref" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" --yes
|
||||
|
|
|
|||
|
|
@ -81,8 +81,9 @@ Vercel Git builds: `scripts/vercel-ignore-build.sh` skipeia **todas** as branche
|
|||
|
||||
**Dois toggles distintos no dashboard Vercel (não confundir):**
|
||||
|
||||
1. **Git / Branch tracking — não disparar build em push** — o que queremos OFF (já pinado em `vercel.json` `git.deploymentEnabled: false` + ignore script).
|
||||
2. **Auto-assign Custom Production Domains** (Environments → Production → Branch Tracking) — deve ficar **ON**. Se OFF, `vercel deploy --prod` no Actions cria um deployment "staged" que só recebe `*.vercel.app` e **não** move `pt.jorgesolla.com.br`. Mesmo com ON, domínios amarrados só ao Git Integration podem exigir alias explícito — o step pós-deploy `scripts/vercel-ensure-production-alias.mjs` reativa o flag, faz `promote` e `POST /v2/deployments/{id}/aliases`. Emergência sem rebuild: workflow `Vercel promote production`.
|
||||
1. **Git / Branch tracking — não disparar build em push** — o que queremos OFF (já pinado em `vercel.json` `git.deploymentEnabled: false` + ignore script). Desligar “automatic deploy from main” no dashboard é o mesmo eixo — **não** confunda com o item 2.
|
||||
2. **Auto-assign Custom Production Domains** (Environments → Production → Branch Tracking) — deve ficar **ON**. Se OFF, `vercel deploy --prod` no Actions cria um deployment "staged" que só recebe `*.vercel.app` e **não** move `pt.jorgesolla.com.br`.
|
||||
3. **Domains → `pt.jorgesolla.com.br` → Git Branch** — deve ficar **vazio** (Production). Domínio com Git Branch só auto-alia via Git Integration; com Git deploy OFF + CLI `--prod`, o host fica preso no Current antigo. O step pós-deploy `scripts/vercel-ensure-production-alias.mjs` reativa auto-assign, **limpa gitBranch**, faz `promote` + `POST /v2/deployments/{id}/aliases` + `vercel alias set`. Emergência sem rebuild: workflow `Vercel promote production`.
|
||||
|
||||
## Cursor Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ Completed 2026-08-01. Branch `stage` and GitHub Environment `stage` are deleted.
|
|||
- [x] `VERCEL_ORG_ID` / `VERCEL_PROJECT_ID` synced from local `.vercel/project.json` (`vercel link`)
|
||||
- [x] `ci.yml` deploy job green (`vercel@55`, Node `24.x`, alias https://pt.jorgesolla.com.br)
|
||||
- [x] **Git auto-deploy OFF** (`vercel.json` `git.deploymentEnabled: false` + ignore script) — correct for Actions-gated deploys
|
||||
- [x] **Auto-assign Custom Production Domains ON** (Project → Settings → Environments → Production → Branch Tracking). Turning this OFF stages CLI `--prod` deploys without moving `pt.jorgesolla.com.br`; CI now re-enables + `promote`s via `scripts/vercel-ensure-production-alias.mjs`. Emergency: Actions → "Vercel promote production".
|
||||
- [x] **Auto-assign Custom Production Domains ON** (Project → Settings → Environments → Production → Branch Tracking). Turning this OFF stages CLI `--prod` deploys without moving `pt.jorgesolla.com.br`; CI re-enables it.
|
||||
- [x] **Domain Git Branch empty** for `pt.jorgesolla.com.br` (Settings → Domains). A branch binding only moves with Git Integration; with Git OFF, CI clears it then `promote` + `alias set`. Emergency: Actions → "Vercel promote production".
|
||||
- [x] Branch `stage` deleted
|
||||
- [x] GitHub Environment `stage` (and `STAGE_*` secrets) deleted
|
||||
- [x] Dead stage tooling removed (`db:refresh:stage`, `agent:promote`)
|
||||
|
|
|
|||
|
|
@ -181,6 +181,16 @@ const promoteProductionDeployment = async ({
|
|||
return body
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string
|
||||
* gitBranch: string | null
|
||||
* verified: boolean | null
|
||||
* redirect: string | null
|
||||
* customEnvironmentId: string | null
|
||||
* }} ProjectDomainRow
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* token: string
|
||||
|
|
@ -188,7 +198,7 @@ const promoteProductionDeployment = async ({
|
|||
* teamId: string
|
||||
* fetchImpl?: typeof fetch
|
||||
* }} options
|
||||
* @returns {Promise<string[]>}
|
||||
* @returns {Promise<ProjectDomainRow[]>}
|
||||
*/
|
||||
const fetchProjectDomains = async ({ token, projectId, teamId, fetchImpl = fetch }) => {
|
||||
const params = new URLSearchParams({ teamId })
|
||||
|
|
@ -214,8 +224,66 @@ const fetchProjectDomains = async ({ token, projectId, teamId, fetchImpl = fetch
|
|||
}
|
||||
const rows = Array.isArray(body.domains) ? body.domains : Array.isArray(body) ? body : []
|
||||
return rows
|
||||
.map((row) => (row && typeof row.name === 'string' ? normalizeHostname(row.name) : ''))
|
||||
.filter(Boolean)
|
||||
.filter((row) => row && typeof row.name === 'string' && row.name.trim())
|
||||
.map((row) => ({
|
||||
name: normalizeHostname(row.name),
|
||||
gitBranch: typeof row.gitBranch === 'string' && row.gitBranch.trim() ? row.gitBranch : null,
|
||||
verified: typeof row.verified === 'boolean' ? row.verified : null,
|
||||
redirect: typeof row.redirect === 'string' && row.redirect.trim() ? row.redirect : null,
|
||||
customEnvironmentId:
|
||||
typeof row.customEnvironmentId === 'string' && row.customEnvironmentId.trim()
|
||||
? row.customEnvironmentId
|
||||
: null,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Domains with a Git Branch set only auto-alias via Git Integration — CLI `--prod`
|
||||
* deploys (our Actions path) will not move them until the branch binding is cleared.
|
||||
* @param {{
|
||||
* token: string
|
||||
* projectId: string
|
||||
* teamId: string
|
||||
* domain: string
|
||||
* fetchImpl?: typeof fetch
|
||||
* }} options
|
||||
*/
|
||||
const clearProjectDomainGitBranch = async ({
|
||||
token,
|
||||
projectId,
|
||||
teamId,
|
||||
domain,
|
||||
fetchImpl = fetch,
|
||||
}) => {
|
||||
const host = normalizeHostname(domain)
|
||||
const params = new URLSearchParams({ teamId })
|
||||
const response = await fetchImpl(
|
||||
`${VERCEL_API_BASE}/v9/projects/${projectId}/domains/${encodeURIComponent(host)}?${params}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
// JSON null clears the branch binding (returns domain to Production).
|
||||
body: JSON.stringify({ gitBranch: null }),
|
||||
},
|
||||
)
|
||||
const bodyText = await response.text()
|
||||
let body
|
||||
try {
|
||||
body = bodyText ? JSON.parse(bodyText) : {}
|
||||
} catch {
|
||||
throw new Error(`Vercel domain PATCH returned non-JSON (${response.status})`)
|
||||
}
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
typeof body?.error?.message === 'string'
|
||||
? body.error.message
|
||||
: bodyText || `HTTP ${response.status}`
|
||||
throw new Error(`Vercel domain PATCH ${response.status}: ${message}`)
|
||||
}
|
||||
return body
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,13 +317,19 @@ const assignDeploymentAlias = async ({ token, teamId, deploymentId, alias, fetch
|
|||
} catch {
|
||||
body = { raw: bodyText }
|
||||
}
|
||||
if (response.ok) return { ok: true, body }
|
||||
if (response.ok) {
|
||||
const assigned =
|
||||
typeof body?.alias === 'string' ? normalizeHostname(body.alias) : normalizeHostname(alias)
|
||||
return { ok: true, body, assigned, already: false }
|
||||
}
|
||||
const message =
|
||||
typeof body?.error?.message === 'string'
|
||||
? body.error.message
|
||||
: bodyText || `HTTP ${response.status}`
|
||||
// Only treat the exact "already on this deployment" 409 as success — other 409s
|
||||
// (e.g. "domain is not allowed") must fail loudly.
|
||||
if (response.status === 409 && /already assigned to (the )?given deployment/i.test(message)) {
|
||||
return { ok: true, body, already: true }
|
||||
return { ok: true, body, assigned: normalizeHostname(alias), already: true }
|
||||
}
|
||||
throw new Error(`Vercel alias API ${response.status}: ${message}`)
|
||||
}
|
||||
|
|
@ -476,14 +550,50 @@ export const ensureProductionCustomDomain = async ({
|
|||
const projectDomains = await fetchProjectDomains({ token, projectId, teamId, fetchImpl })
|
||||
note(
|
||||
projectDomains.length > 0
|
||||
? `project domains: ${projectDomains.join(', ')}`
|
||||
? `project domains: ${projectDomains
|
||||
.map((d) => {
|
||||
const flags = [
|
||||
d.gitBranch ? `gitBranch=${d.gitBranch}` : null,
|
||||
d.redirect ? `redirect=${d.redirect}` : null,
|
||||
d.customEnvironmentId ? `customEnv=${d.customEnvironmentId}` : null,
|
||||
d.verified === false ? 'unverified' : null,
|
||||
].filter(Boolean)
|
||||
return flags.length > 0 ? `${d.name}(${flags.join(',')})` : d.name
|
||||
})
|
||||
.join(', ')}`
|
||||
: 'project domains: (none)',
|
||||
)
|
||||
if (!projectDomains.includes(normalizeHostname(expectedHost))) {
|
||||
const expected = normalizeHostname(expectedHost)
|
||||
const domainRow = projectDomains.find((d) => d.name === expected)
|
||||
if (!domainRow) {
|
||||
throw new Error(
|
||||
`${expectedHost} is not attached to this Vercel project. Add it under Project → Settings → Domains, then re-run. Domains seen: ${JSON.stringify(projectDomains)}`,
|
||||
`${expectedHost} is not attached to this Vercel project. Add it under Project → Settings → Domains, then re-run. Domains seen: ${JSON.stringify(projectDomains.map((d) => d.name))}`,
|
||||
)
|
||||
}
|
||||
if (domainRow.verified === false) {
|
||||
throw new Error(
|
||||
`${expectedHost} is on the project but not verified — finish DNS/TXT verification in Vercel Domains before aliasing.`,
|
||||
)
|
||||
}
|
||||
if (domainRow.redirect) {
|
||||
note(
|
||||
`warning: ${expectedHost} redirects to ${domainRow.redirect} — alias may not serve app traffic`,
|
||||
)
|
||||
}
|
||||
// Git-branch-bound domains only move with Git Integration deploys. We deploy via
|
||||
// CLI (`vercel deploy --prebuilt --prod`), so clear the binding first.
|
||||
if (domainRow.gitBranch) {
|
||||
note(
|
||||
`${expectedHost} had gitBranch=${domainRow.gitBranch} — clearing so CLI/Actions deploys can own Production`,
|
||||
)
|
||||
await clearProjectDomainGitBranch({
|
||||
token,
|
||||
projectId,
|
||||
teamId,
|
||||
domain: expected,
|
||||
fetchImpl,
|
||||
})
|
||||
}
|
||||
|
||||
const deployment = await fetchDeploymentAliases({
|
||||
token,
|
||||
|
|
@ -517,11 +627,13 @@ export const ensureProductionCustomDomain = async ({
|
|||
promoted: false,
|
||||
aliased: false,
|
||||
autoAssignWasFalse: autoAssign === false,
|
||||
clearedGitBranch: Boolean(domainRow.gitBranch),
|
||||
deployment,
|
||||
steps,
|
||||
}
|
||||
}
|
||||
|
||||
let promoted = false
|
||||
try {
|
||||
note(`promoting ${deployment.id}`)
|
||||
await promoteProductionDeployment({
|
||||
|
|
@ -531,6 +643,7 @@ export const ensureProductionCustomDomain = async ({
|
|||
deploymentId: deployment.id,
|
||||
fetchImpl,
|
||||
})
|
||||
promoted = true
|
||||
} catch (error) {
|
||||
note(`promote skipped: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
|
@ -540,13 +653,22 @@ export const ensureProductionCustomDomain = async ({
|
|||
token,
|
||||
teamId,
|
||||
deploymentId: deployment.id,
|
||||
alias: normalizeHostname(expectedHost),
|
||||
alias: expected,
|
||||
fetchImpl,
|
||||
})
|
||||
const assignedHost = assignResult.assigned ?? expected
|
||||
if (assignedHost !== expected) {
|
||||
throw new Error(
|
||||
`Alias API returned unexpected host ${JSON.stringify(assignedHost)} (wanted ${expected}). body=${JSON.stringify(assignResult.body)}`,
|
||||
)
|
||||
}
|
||||
note(
|
||||
assignResult.already
|
||||
? `alias API: already on this deployment`
|
||||
: `alias API: assigned ${expectedHost}`,
|
||||
: `alias API: assigned ${expectedHost}` +
|
||||
(assignResult.body?.oldDeploymentId
|
||||
? ` (was ${String(assignResult.body.oldDeploymentId)})`
|
||||
: ''),
|
||||
)
|
||||
|
||||
let verified = false
|
||||
|
|
@ -573,16 +695,19 @@ export const ensureProductionCustomDomain = async ({
|
|||
})
|
||||
throw new Error(
|
||||
`After alias assign, ${expectedHost} is still not on deployment ${deployment.id}. ` +
|
||||
`deployment aliases=${JSON.stringify(hosts)}; domain points to=${JSON.stringify(domainTarget)}`,
|
||||
`deployment aliases=${JSON.stringify(hosts)}; domain points to=${JSON.stringify(domainTarget)}; ` +
|
||||
`aliasApiBody=${JSON.stringify(assignResult.body)}. ` +
|
||||
`Check Vercel → Settings → Domains: Git Branch must be empty, and Environments → Production → Auto-assign Custom Production Domains must be ON.`,
|
||||
)
|
||||
}
|
||||
|
||||
note(`verified alias ${expectedHost}`)
|
||||
return {
|
||||
alreadyAssigned: false,
|
||||
promoted: true,
|
||||
promoted,
|
||||
aliased: true,
|
||||
autoAssignWasFalse: autoAssign === false,
|
||||
clearedGitBranch: Boolean(domainRow.gitBranch),
|
||||
deployment,
|
||||
steps,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,12 +59,14 @@ describe('vercelProductionAlias', () => {
|
|||
calls.push(`${method} ${href}`)
|
||||
|
||||
if (href.includes('/v9/projects/') && href.includes('/domains') && method === 'GET') {
|
||||
return jsonResponse(true, 200, { domains: [{ name: 'pt.jorgesolla.com.br' }] })
|
||||
return jsonResponse(true, 200, {
|
||||
domains: [{ name: 'pt.jorgesolla.com.br', verified: true, gitBranch: null }],
|
||||
})
|
||||
}
|
||||
if (href.includes('/v9/projects/') && method === 'GET') {
|
||||
return jsonResponse(true, 200, { autoAssignCustomDomains: false, name: 'jorgesolla' })
|
||||
}
|
||||
if (href.includes('/v9/projects/') && method === 'PATCH') {
|
||||
if (href.includes('/v9/projects/') && method === 'PATCH' && !href.includes('/domains/')) {
|
||||
return jsonResponse(true, 200, { autoAssignCustomDomains: true })
|
||||
}
|
||||
if (href.includes('/v13/deployments/')) {
|
||||
|
|
@ -108,6 +110,7 @@ describe('vercelProductionAlias', () => {
|
|||
return jsonResponse(true, 200, {
|
||||
alias: 'pt.jorgesolla.com.br',
|
||||
uid: 'alias_1',
|
||||
oldDeploymentId: 'dpl_old',
|
||||
created: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
|
@ -125,15 +128,99 @@ describe('vercelProductionAlias', () => {
|
|||
|
||||
expect(result.aliased).toBe(true)
|
||||
expect(result.autoAssignWasFalse).toBe(true)
|
||||
expect(result.clearedGitBranch).toBe(false)
|
||||
expect(calls.some((c) => c.includes('/aliases') && c.startsWith('POST '))).toBe(true)
|
||||
})
|
||||
|
||||
it('ensureProductionCustomDomain clears gitBranch before aliasing CLI deploys', async () => {
|
||||
const calls: string[] = []
|
||||
let aliasHosts = ['jorgesolla-solla.vercel.app']
|
||||
let domainGitBranch: string | null = 'main'
|
||||
const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
const href = String(url)
|
||||
const method = init?.method ?? 'GET'
|
||||
calls.push(`${method} ${href}`)
|
||||
|
||||
if (href.includes('/v9/projects/') && href.includes('/domains') && method === 'GET') {
|
||||
return jsonResponse(true, 200, {
|
||||
domains: [
|
||||
{
|
||||
name: 'pt.jorgesolla.com.br',
|
||||
verified: true,
|
||||
gitBranch: domainGitBranch,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
if (
|
||||
href.includes('/v9/projects/') &&
|
||||
href.includes('/domains/pt.jorgesolla.com.br') &&
|
||||
method === 'PATCH'
|
||||
) {
|
||||
domainGitBranch = null
|
||||
return jsonResponse(true, 200, { name: 'pt.jorgesolla.com.br', gitBranch: null })
|
||||
}
|
||||
if (href.includes('/v9/projects/') && method === 'GET') {
|
||||
return jsonResponse(true, 200, { autoAssignCustomDomains: true, name: 'jorgesolla' })
|
||||
}
|
||||
if (href.includes('/v13/deployments/')) {
|
||||
return jsonResponse(true, 200, {
|
||||
id: 'dpl_test',
|
||||
alias: aliasHosts,
|
||||
readyState: 'READY',
|
||||
})
|
||||
}
|
||||
if (href.includes('/v2/deployments/') && href.includes('/aliases') && method === 'GET') {
|
||||
return jsonResponse(true, 200, {
|
||||
aliases: aliasHosts.map((alias) => ({ alias, uid: alias, created: '' })),
|
||||
})
|
||||
}
|
||||
if (href.includes('/v4/aliases') && method === 'GET') {
|
||||
const assigned = aliasHosts.includes('pt.jorgesolla.com.br')
|
||||
return jsonResponse(true, 200, {
|
||||
aliases: assigned
|
||||
? [{ alias: 'pt.jorgesolla.com.br', deploymentId: 'dpl_test', uid: 'a1', created: '' }]
|
||||
: [],
|
||||
})
|
||||
}
|
||||
if (href.includes('/promote/') && method === 'POST') {
|
||||
return jsonResponse(true, 200, {})
|
||||
}
|
||||
if (href.includes('/aliases') && method === 'POST') {
|
||||
aliasHosts = ['jorgesolla-solla.vercel.app', 'pt.jorgesolla.com.br']
|
||||
return jsonResponse(true, 200, {
|
||||
alias: 'pt.jorgesolla.com.br',
|
||||
uid: 'alias_1',
|
||||
created: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
throw new Error(`unexpected fetch ${method} ${href}`)
|
||||
}) as typeof fetch
|
||||
|
||||
const result = await ensureProductionCustomDomain({
|
||||
token: 'tok',
|
||||
projectId: 'prj_x',
|
||||
teamId: 'team_x',
|
||||
deploymentRef: 'dpl_test',
|
||||
fetchImpl,
|
||||
sleepImpl: async () => {},
|
||||
})
|
||||
|
||||
expect(result.aliased).toBe(true)
|
||||
expect(result.clearedGitBranch).toBe(true)
|
||||
expect(
|
||||
calls.some((c) => c.startsWith('PATCH ') && c.includes('/domains/pt.jorgesolla.com.br')),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('ensureProductionCustomDomain skips work when alias already present', async () => {
|
||||
const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
const href = String(url)
|
||||
const method = init?.method ?? 'GET'
|
||||
if (href.includes('/v9/projects/') && href.includes('/domains') && method === 'GET') {
|
||||
return jsonResponse(true, 200, { domains: [{ name: 'pt.jorgesolla.com.br' }] })
|
||||
return jsonResponse(true, 200, {
|
||||
domains: [{ name: 'pt.jorgesolla.com.br', verified: true }],
|
||||
})
|
||||
}
|
||||
if (href.includes('/v9/projects/') && method === 'GET') {
|
||||
return jsonResponse(true, 200, { autoAssignCustomDomains: true })
|
||||
|
|
|
|||
Loading…
Reference in a new issue