Publicar no App Platform
Fluxo: construir e testar no sandbox → tornar implantável → propor o plano e obter aprovação → enviar para o GitHub → criar o aplicativo no App Platform → aguardar ACTIVE → verificar a URL.
Nunca crie um repositório, faça push ou crie/atualize/exclua um aplicativo antes que o usuário tenha aprovado o plano na etapa 2. Um pedido como "publique isso" não é essa aprovação. Construir e testar no sandbox não precisa de aprovação.
Nunca use as ações publish, docker ou doctl – nenhuma delas funciona neste computador. Tudo passa por duas credenciais compartilhadas, que você chama através do broker de credenciais do núcleo. Você nunca vê os tokens.
Use um sandbox_id explícito para cada comando e selecione a credencial em execute.credentials para qualquer comando que chame o broker (digitalocean, github ou github-git). O núcleo fornece AGENT_CREDENTIAL_TOKEN apenas para esses comandos. Não use rm -rf; crie um diretório novo com mktemp -d.
O que o computador tem
Node 24 + npm, Python 3.12 + pip, git, gh (não logado; não o use), curl, gcc, make. Sem docker, doctl, jq, wget, unzip. Analise JSON com node -e ou python3 -c.
Credenciais (o que cada uma pode fazer)
| Credencial | Host | Permitido |
|---|---|---|
digitalocean | api.digitalocean.com | GET/POST/PUT/DELETE em /v2/apps e /v2/regions |
github | api.github.com | GET/POST/PATCH em /user e /repos/jnaidu-do |
github-git | github.com (git push/clone) | GET/POST em /jnaidu-do/ |
Tudo é criado sob a conta compartilhada do GitHub jnaidu-do (uma credencial de toda a implantação, não a conta do próprio usuário). Diga isso no plano. Se o usuário quiser a própria conta, informe que isso ainda não foi configurado. Um 403 path_not_allowed / method_not_allowed significa que a requisição está fora disso – não tente variações; informe ao usuário que não é permitido.
Helper do broker
Cole isto uma vez por comando (o estado do shell não persiste). Ele imprime o status HTTP no stderr e o corpo da resposta no stdout:
bk() { # bk <credential> <METHOD> <url> [json-body]
p=$(node -e 'const [c,m,u,b]=process.argv.slice(1);console.log(JSON.stringify({credential:c,method:m,url:u,headers:{accept:"application/json","content-type":"application/json"},...(b?{body:b}:{})}))' "$1" "$2" "$3" "${4:-}")
curl -fsS -X POST "$AGENT_API_URL/v1/credentials/broker" -H "x-agent-capability: $AGENT_CREDENTIAL_TOKEN" \
-H "content-type: application/json" -d "$p" |
node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const r=JSON.parse(s);console.error("HTTP "+r.status);console.log(r.body)})'
}
1. Tornar implantável (verifique antes de enviar)
O aplicativo é executado como um contêiner normal por trás do App Platform, não neste sandbox.
- Escute em
$PORT, vinculado a0.0.0.0(o App Platform definePORT; padrão 8080). Nunca codifique outra porta. - HTML/CSS/JS simples ou um framework que compila para arquivos estáticos (Vite, CRA, Astro): implante como um site estático (
build_command,output_dir). Prefira isso a executar um servidor para uma página estática. - Qualquer coisa com um servidor: um serviço com
build_command,run_command,http_port. Layouts padrão funcionam sem configuração: Node precisa depackage.jsoncom um scriptstart; Python precisa derequirements.txte um comando de execução (ex.:gunicorn app:app --bind 0.0.0.0:$PORT). UmDockerfileno repositório também funciona (definadockerfile_path), mas você não pode testar a compilação aqui, então mantenha simples. - Sem disco persistente. Qualquer coisa escrita no sistema de arquivos é perdida a cada implantação. Não use arquivos SQLite, uploads para disco ou arquivos JSON para estado. Se o aplicativo precisar de um banco de dados, adicione um Postgres gerenciado (veja abaixo); caso contrário, mantenha-o sem estado.
- Execute localmente primeiro:
npm install, inicie comPORT=8080ecurl localhost:8080. Publique apenas o que funciona. - Adicione um
.gitignorecomnode_modulesetc. Não confirme segredos. Nunca imprima tokens.
Se o aplicativo usar Postgres
O ${db.DATABASE_URL} do App Platform inclui sslmode=require, e o pg do Node então rejeita o CA gerenciado (SELF_SIGNED_CERT_IN_CHAIN). Remova sslmode da URL e defina ssl: { rejectUnauthorized: false }:
function normalizeDatabaseUrl(raw) {
const u = new URL(raw);
u.searchParams.delete("sslmode"); u.searchParams.delete("ssl");
return u.toString();
}
const pool = new Pool({ connectionString: normalizeDatabaseUrl(process.env.DATABASE_URL), ssl: { rejectUnauthorized: false } });
Adicione "databases": [{"name":"db","engine":"PG","production":false,"version":"16"}] à especificação e {"key":"DATABASE_URL","scope":"RUN_TIME","value":"${db.DATABASE_URL}"} a cada componente que precisar. Repita a primeira conexão por ~30 s: o banco de dados ainda pode estar iniciando. Adicione um banco de dados apenas se o aplicativo realmente precisar (custa extra).
2. Propor o plano e perguntar (pare aqui até eles dizerem sim)
Depois que o aplicativo compilar e executar localmente, pare e envie ao usuário um plano curto, depois aguarde uma resposta. Não execute nenhuma chamada bk, git push ou criação de repositório/app até que eles aprovem. Inclua:
- O que você construiu e que foi executado localmente (uma linha).
- As etapas que você seguirá, em ordem, cada uma com o que toca:
- Criar repositório GitHub
jnaidu-do/<nome>— público, na conta compartilhadajnaidu-do(não a conta do próprio usuário) - Enviar os arquivos do aplicativo para ele com git (liste o que está incluído; confirme que não há segredos)
- Criar aplicativo App Platform
<nome>na regiãonyc: um site estático (sem custo por alguns) ou um serviço (basic-xxs, cerca de $5/mês) — diga qual e o custo - Aguardar a compilação (3–5 min), depois fornecer a URL pública
- Criar repositório GitHub
- O que se torna público: o repositório e o site ao vivo são legíveis por qualquer pessoa na internet.
- O que você não fará sem perguntar novamente: enviar mais alterações, editar a especificação ou excluir qualquer coisa.
Termine com uma pergunta direta, ex.: "Devo criar o repositório e o aplicativo como acima? Você também pode alterar o nome, tornar o repositório privado (isso precisa que o GitHub App do DO tenha acesso) ou dizer não." Ofereça alternativas se relevante (estático vs serviço, nome diferente). Se recusarem, forneça os arquivos ou o código pronto para repositório e pare.
Pergunte novamente, separadamente, antes de: qualquer git push posterior (atualizações), alterar a especificação do aplicativo e excluir o aplicativo.
3. Enviar para o GitHub
Use o nome proposto na etapa 2 (qm-<short-kebab-name>) (minúsculas). Se o nome já existir (422), adicione um sufixo curto.
# criar o repositório (público: o App Platform pode então cloná-lo sem autenticação extra)
bk github POST https://api.github.com/user/repos '{"name":"qm-hello-site","private":false}'
# enviar a partir de um diretório limpo e novo contendo o aplicativo (sem node_modules)
cd "$(mktemp -d)" && cp -r /path/to/app/. . && git init -q -b main && git add . &&
git -c user.name=QM -c [email protected] commit -qm "Initial commit" &&
git remote add origin "$AGENT_API_URL/v1/credentials/git/github-git/jnaidu-do/qm-hello-site.git" &&
git -c http.extraHeader="x-agent-capability: $AGENT_CREDENTIAL_TOKEN" push origin HEAD:refs/heads/main
Atualizações posteriores: commit e push da mesma forma (HEAD:refs/heads/main), mas apenas depois de informar ao usuário o que mudou e obter um sim — cada push é uma nova aprovação, não coberta pelo primeiro plano.
4. Criar o aplicativo App Platform
bk digitalocean POST https://api.digitalocean.com/v2/apps '{"spec": {
"name": "qm-hello-site",
"region": "nyc",
"services": [{
"name": "web",
"github": {"repo": "jnaidu-do/qm-hello-site", "branch": "main", "deploy_on_push": true},
"build_command": "npm install",
"run_command": "npm start",
"http_port": 8080,
"instance_size_slug": "basic-xxs",
"instance_count": 1
}]
}}'
Em vez de services, para site estático: "static_sites": [{"name":"web","github":{...},"build_command":"npm run build","output_dir":"dist"}] (HTML simples: omita build_command, output_dir é "/"; para um aplicativo de página única adicione "catchall_document":"index.html").
A resposta contém o id do aplicativo. Mantenha-o. Nomes de aplicativos são letras minúsculas, dígitos, traços, 2–32 caracteres.
Se a criação falhar com um erro de acesso ao repositório do GitHub ("does not have access to the repository"): o GitHub App do DigitalOcean não tem acesso a este repositório. Diga ao usuário para abrir https://github.com/apps/digitalocean/installations/select_target e conceder acesso aos repositórios jnaidu-do, depois avise que está feito; repita a mesma requisição. Se não puderem, recorra a uma fonte de clone público genérica ("git": {"repo_clone_url": "https://github.com/jnaidu-do/qm-hello-site.git", "branch": "main"}) e avise que atualizações então precisam de uma reimplantação (veja abaixo).
5. Aguardar ACTIVE
Uma compilação real leva de 3 a 5 minutos. Pesquise a cada 30 s e avise ao usuário que está aguardando:
bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID/deployments?per_page=1" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const d=JSON.parse(s).deployments[0];console.log(d.id,d.phase,d.progress?.success_steps+"/"+d.progress?.total_steps)})'
Fases: PENDING_BUILD → BUILDING → DEPLOYING → ACTIVE. ERROR ou CANCELED significa que falhou; vá para "Quando falhar". Pare de pesquisar após cerca de 12 minutos e informe ao usuário onde está.
6. Relatar a URL
bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).app.live_url))'
Tente curl -s -o /dev/null -w '%{http_code}' <live_url> — espere 200. Este computador muitas vezes não consegue alcançar novos sites *.ondigitalocean.app (a conexão falha). Se o curl falhar mas a implantação estiver ACTIVE, isso não é uma falha de implantação: diga que não foi possível verificar daqui e peça ao usuário para abrir a URL. Forneça ao usuário a URL completa https://…ondigitalocean.app e a URL do repositório. O aplicativo é público na internet.
Quando falhar
Leia os logs você mesmo; não peça ao usuário por eles. Obtenha uma URL de log e depois a acesse:
bk digitalocean GET "https://api.digitalocean.com/v2/apps/$APP_ID/deployments/$DEPLOYMENT_ID/logs?type=BUILD"
# o corpo tem "live_url" (ou "historic_urls"); acesse essa URL com curl (sem autenticação)
type é BUILD, DEPLOY ou RUN (sites estáticos têm apenas BUILD). Combine o sinal:
| Sinal | Correção |
|---|---|
Missing script: start / comando não encontrado | build_command / run_command errado, ou nenhum script start |
| Contêiner sai, verificação de saúde falha | não está ouvindo em $PORT / não vinculado a 0.0.0.0, ou falha na inicialização |
SELF_SIGNED_CERT_IN_CHAIN | Helper SSL do Postgres ausente (veja acima) |
Cannot find module | dependência ausente em package.json, ou node_modules confirmado incompatível |
| Vite "Blocked request. This host is not allowed" | defina preview: { allowedHosts: true } (ou sirva o dist compilado como site estático) |
| Build "skipped" / ACTIVE em segundos | a fonte não mudou; faça um commit real |
Informe ao usuário a causa raiz (a linha exata do log) e a correção que você propõe, e pergunte antes de enviar a correção ou alterar a especificação — cada push e cada alteração de especificação precisa de seu próprio sim. Após a aprovação, corrija o código, faça commit e push (deploy_on_push reimplanta automaticamente) e então pesquise novamente. Para uma alteração de especificação, envie PUT /v2/apps/$APP_ID com {"spec": {...}} (obtenha a especificação atual primeiro com GET /v2/apps/$APP_ID e edite-a). Para uma fonte git genérica, force uma recompilação com POST /v2/apps/$APP_ID/deployments com corpo {"force_build":true}. Não exclua e recrie o aplicativo para corrigir uma falha — isso muda a URL. Corrija adiante. Desista após três tentativas diferentes falhas e relate o que tentou. Ler logs não precisa de aprovação.
Excluindo
Apenas quando o usuário pedir explicitamente, e nomeie o aplicativo primeiro. DELETE /v2/apps/$APP_ID remove o aplicativo. O repositório no GitHub é deixado para trás (esta credencial não pode excluir repositórios); avise.
Relatório
Termine com: a URL ao vivo, a URL do repositório e uma linha sobre como atualizá-lo ("peça-me para mudar X e eu enviarei" ).