Skip to content

Commit a55f6b9

Browse files
committed
feat(deploy): reconcile DNS on Hetzner deploys via the configured provider
Hetzner deploys had no DNS step (Route53 reconciliation only ran on the AWS path), so domains had to be pointed by hand. After a successful compute deploy, resolve a DNS provider per-domain from the environment (Porkbun/Cloudflare/ Route53) using ts-cloud's detectDnsProvider and upsert A records (apex + www) to the box IP. Best-effort and idempotent — a DNS hiccup won't fail the release.
1 parent 47fbdf7 commit a55f6b9

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

  • storage/framework/core/buddy/src/commands

storage/framework/core/buddy/src/commands/deploy.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,15 @@ async function runHetznerDeploy(args: {
823823
},
824824
})
825825

826+
// Reconcile DNS for every site that declares a public domain. Hetzner deploys
827+
// historically had NO DNS step (Route53 reconciliation only ran on the AWS
828+
// path), so domains had to be pointed by hand. We now resolve a DNS provider
829+
// per-domain via ts-cloud's factory (Porkbun/Route53/Cloudflare/GoDaddy from
830+
// env) and upsert A records → the box IP. Non-fatal: a DNS hiccup shouldn't
831+
// fail an otherwise-successful release.
832+
if (ok)
833+
await reconcileHetznerDns(sites, ip, log)
834+
826835
console.log('')
827836
if (ok) {
828837
await outro(`Deployed to Hetzner. Your site is live at http://${ip}:3000`, { startTime, useSeconds: true })
@@ -834,6 +843,64 @@ async function runHetznerDeploy(args: {
834843
}
835844
}
836845

846+
/**
847+
* Point every site's public domain (apex + `www`) at the Hetzner box via the
848+
* appropriate DNS provider. Providers are resolved per-domain from the
849+
* environment (Porkbun, Route53, Cloudflare, GoDaddy) using ts-cloud's
850+
* `detectDnsProvider`, so whichever registrar actually hosts the zone is used.
851+
* Idempotent (upsert) and best-effort — failures are logged, not thrown.
852+
*/
853+
async function reconcileHetznerDns(sites: Record<string, any>, ip: string, logger: typeof log): Promise<void> {
854+
// Collect the apex domains declared by sites (skip loopback/domain-less sites).
855+
const domains = new Set<string>()
856+
for (const site of Object.values(sites)) {
857+
if (site?.domain && typeof site.domain === 'string')
858+
domains.add(site.domain.replace(/^www\./, ''))
859+
}
860+
if (domains.size === 0)
861+
return
862+
863+
// Candidate provider configs, built from whatever credentials are present.
864+
const providerConfigs: any[] = []
865+
if (process.env.PORKBUN_API_KEY && process.env.PORKBUN_SECRET_KEY)
866+
providerConfigs.push({ provider: 'porkbun', apiKey: process.env.PORKBUN_API_KEY, secretKey: process.env.PORKBUN_SECRET_KEY })
867+
if (process.env.CLOUDFLARE_API_TOKEN)
868+
providerConfigs.push({ provider: 'cloudflare', apiToken: process.env.CLOUDFLARE_API_TOKEN })
869+
if (process.env.AWS_ACCESS_KEY_ID || process.env.AWS_PROFILE)
870+
providerConfigs.push({ provider: 'route53' })
871+
872+
if (providerConfigs.length === 0) {
873+
logger.warn('DNS: no DNS provider credentials found (PORKBUN_API_KEY/…); skipping DNS reconciliation.')
874+
for (const d of domains)
875+
logger.info(` Point manually: A ${d}${ip} and A www.${d}${ip}`)
876+
return
877+
}
878+
879+
const { detectDnsProvider } = await import('@stacksjs/ts-cloud') as any
880+
logger.info('Reconciling DNS records...')
881+
882+
for (const domain of domains) {
883+
try {
884+
const provider = await detectDnsProvider(domain, providerConfigs)
885+
if (!provider) {
886+
logger.warn(` DNS: no configured provider can manage ${domain} — point it manually: A ${domain}${ip}`)
887+
continue
888+
}
889+
for (const sub of ['', 'www']) {
890+
const fqdn = sub ? `${sub}.${domain}` : domain
891+
const res = await provider.upsertRecord(domain, { name: sub, type: 'A', content: ip, ttl: 600 })
892+
if (res?.success === false)
893+
logger.warn(` DNS: ${fqdn}${ip} failed: ${res.error || 'unknown error'}`)
894+
else
895+
logger.success(` DNS: ${fqdn}${ip} (${provider.name})`)
896+
}
897+
}
898+
catch (err: any) {
899+
logger.warn(` DNS: ${domain} reconciliation failed: ${err?.message || err}`)
900+
}
901+
}
902+
}
903+
837904
/**
838905
* Build an OCI container image for each site using pantry's native, daemon-less
839906
* builder — no Docker dependency. The image is built from the framework's

0 commit comments

Comments
 (0)