GitHub Actions ile Deno Deploy Otomatik Deployment
Deno Deploy’u ilk keşfettiğimde aklıma şu geldi: “Sonunda bir edge platform var ki beni JavaScript build tooling cehennemiyle boğmuyor.” Webpack config dosyalarıyla, node_modules klasörüyle, transpile adımlarıyla uğraşmak yerine TypeScript kodunu doğrudan çalıştırabilen, global edge ağında saniyeler içinde deploy eden bir platform. Ve bunu GitHub Actions ile birleştirince gerçekten keyifli bir CI/CD pipeline ortaya çıkıyor. Bu yazıda sıfırdan production’a giden tüm süreci, gerçek hayattan aldığım senaryolarla ve karşılaştığım pitfall’larla birlikte anlatacağım.
Deno Deploy Nedir, Neden Kullanmalıyız?
Deno Deploy, Cloudflare Workers veya Vercel Edge Functions’a benzer şekilde çalışan bir edge computing platformu. Ama birkaç kritik farkı var. Deno runtime kullandığı için Node.js’e özgü module system karmaşasından kurtuluyorsunuz. TypeScript’i native destekliyor, yani tsc çalıştırmak zorunda değilsiniz. Kodunuz V8 Isolate’lerde çalışıyor, bu da cold start süresini neredeyse sıfıra indiriyor.
Pratik avantajları şöyle sıralayabilirim:
- Zero cold start: V8 Isolate mimarisi sayesinde ilk istek bile milisaniyeler içinde cevaplanıyor
- Global dağıtım: 35+ edge lokasyonu, kodunuz otomatik olarak kullanıcıya en yakın noktadan çalışıyor
- Native TypeScript: Ayrı bir build adımı yok
- Web standart API’ları: Fetch, WebCrypto, URL gibi API’lar direkt kullanılabiliyor
- Ücretsiz tier: Küçük projeler için oldukça cömert
Bununla birlikte sınırlamalarını da söylemek lazım. Filesystem erişimi yok, uzun süren işlemler için uygun değil (request timeout’ları mevcut), ve bazı Node.js API’ları henüz desteklenmiyor. API gateway, webhook handler, SSR uygulaması, edge middleware gibi use case’ler için biçilmiş kaftan.
Ön Hazırlık: Hesaplar ve Tokenlar
Başlamadan önce şunların hazır olması gerekiyor:
- Deno Deploy hesabı (deno.com/deploy)
- GitHub hesabı ve bir repository
- Deno CLI (lokal geliştirme için)
Deno CLI kurulumu:
# Linux/macOS
curl -fsSL https://deno.land/install.sh | sh
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
# Kurulumu doğrula
deno --version
Deno Deploy token almak için dashboarda gidip Settings > Access Tokens bölümünden yeni token oluşturuyorsunuz. Bu token’ı GitHub repository’nizin Settings > Secrets and Variables > Actions altına DENO_DEPLOY_TOKEN adıyla eklemeniz gerekiyor.
Proje Yapısı: Gerçek Bir API Servisi
Teorik örnekler yerine gerçek bir şey yapalım. Basit bir REST API servisi kuracağız. Webhook alan, işleyen ve bir KV store’a yazan bir uygulama.
Proje dizin yapısı:
mkdir deno-edge-api && cd deno-edge-api
# Dizin yapısı
# .
# ├── .github/
# │ └── workflows/
# │ └── deploy.yml
# ├── src/
# │ ├── main.ts
# │ ├── routes/
# │ │ ├── health.ts
# │ │ └── webhook.ts
# │ └── middleware/
# │ └── auth.ts
# ├── tests/
# │ └── health_test.ts
# └── deno.json
deno.json dosyası projemizin kalbi:
{
"tasks": {
"dev": "deno run --allow-net --allow-env --watch src/main.ts",
"test": "deno test --allow-net --allow-env tests/",
"lint": "deno lint src/",
"fmt": "deno fmt src/ tests/"
},
"lint": {
"rules": {
"exclude": ["no-explicit-any"]
}
},
"fmt": {
"lineWidth": 100,
"singleQuote": true
},
"imports": {
"std/": "https://deno.land/[email protected]/",
"hono": "https://deno.land/x/[email protected]/mod.ts",
"hono/": "https://deno.land/x/[email protected]/"
}
}
Ana uygulama dosyası src/main.ts:
import { Hono } from 'hono';
import { logger } from 'hono/middleware';
import { cors } from 'hono/middleware';
import { healthRouter } from './routes/health.ts';
import { webhookRouter } from './routes/webhook.ts';
import { authMiddleware } from './middleware/auth.ts';
const app = new Hono();
// Global middleware
app.use('*', logger());
app.use('/api/*', cors({
origin: Deno.env.get('ALLOWED_ORIGINS')?.split(',') ?? ['*'],
}));
// Auth middleware sadece webhook endpoint'i icin
app.use('/api/webhook/*', authMiddleware);
// Route'lari bagliyoruz
app.route('/health', healthRouter);
app.route('/api/webhook', webhookRouter);
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Route bulunamadi', path: c.req.path }, 404);
});
// Hata handler
app.onError((err, c) => {
console.error('Uygulama hatasi:', err.message);
return c.json({ error: 'Sunucu hatasi' }, 500);
});
Deno.serve(app.fetch);
src/middleware/auth.ts:
import type { Context, Next } from 'hono';
export async function authMiddleware(c: Context, next: Next) {
const authHeader = c.req.header('Authorization');
const webhookSecret = Deno.env.get('WEBHOOK_SECRET');
if (!webhookSecret) {
console.error('WEBHOOK_SECRET environment variable tanimli degil!');
return c.json({ error: 'Sunucu yapilandirma hatasi' }, 500);
}
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Yetkilendirme basligı eksik' }, 401);
}
const token = authHeader.slice(7);
// Timing-safe karsilastirma icin WebCrypto kullanimiyoruz
const encoder = new TextEncoder();
const tokenBytes = encoder.encode(token);
const secretBytes = encoder.encode(webhookSecret);
const tokenKey = await crypto.subtle.importKey(
'raw', secretBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
);
const signature = await crypto.subtle.sign('HMAC', tokenKey, tokenBytes);
const isValid = token === webhookSecret;
if (!isValid) {
return c.json({ error: 'Gecersiz token' }, 403);
}
await next();
}
GitHub Actions Workflow Dosyası
Asıl konumuza gelelim. .github/workflows/deploy.yml dosyası:
name: Deno Deploy CI/CD
on:
push:
branches:
- main
- 'release/**'
pull_request:
branches:
- main
env:
DENO_VERSION: v1.38.0
jobs:
# Once test ve lint
quality:
name: Kalite Kontrol
runs-on: ubuntu-latest
steps:
- name: Kodu cek
uses: actions/checkout@v4
- name: Deno kur
uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- name: Bagimliliklari cache'le
uses: actions/cache@v3
with:
path: ~/.cache/deno
key: deno-${{ runner.os }}-${{ hashFiles('**/deno.json', '**/deno.lock') }}
restore-keys: |
deno-${{ runner.os }}-
- name: Format kontrolu
run: deno fmt --check src/ tests/
- name: Lint
run: deno lint src/
- name: Testleri calistir
run: deno test --allow-net --allow-env --coverage=coverage/ tests/
env:
WEBHOOK_SECRET: test-secret-for-ci
ALLOWED_ORIGINS: http://localhost:3000
- name: Coverage raporu
run: deno coverage coverage/ --lcov --output=coverage.lcov
- name: Tip kontrolu
run: deno check src/main.ts
# Sadece main branch'e push'ta production deploy
deploy-production:
name: Production Deploy
runs-on: ubuntu-latest
needs: quality
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
url: https://deno-edge-api.deno.dev
steps:
- name: Kodu cek
uses: actions/checkout@v4
- name: Deno Deploy'a gonder
uses: denoland/deployctl@v1
with:
project: deno-edge-api
entrypoint: src/main.ts
token: ${{ secrets.DENO_DEPLOY_TOKEN }}
# PR'lar icin preview deploy
deploy-preview:
name: Preview Deploy
runs-on: ubuntu-latest
needs: quality
if: github.event_name == 'pull_request'
steps:
- name: Kodu cek
uses: actions/checkout@v4
- name: Preview deploy
uses: denoland/deployctl@v1
with:
project: deno-edge-api
entrypoint: src/main.ts
token: ${{ secrets.DENO_DEPLOY_TOKEN }}
- name: PR'a yorum ekle
uses: actions/github-script@v7
if: always()
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Deno Deploy Preview')
);
const body = `## Deno Deploy Preview
Preview URL'iniz hazir! PR icindeki degisiklikler preview ortaminda test edilebilir.
**Commit:** `${{ github.sha }}`
**Branch:** `${{ github.head_ref }}``;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
Environment Variables ve Secret Yonetimi
Deno Deploy’da environment variable yönetimi biraz farklı işliyor. Dashboard üzerinden veya deployctl CLI ile set edebilirsiniz.
# deployctl ile environment variable set etme
deployctl env set WEBHOOK_SECRET=super-gizli-deger --project=deno-edge-api
# Mevcut env variable'lari listele
deployctl env list --project=deno-edge-api
# Bir env variable'i sil
deployctl env unset WEBHOOK_SECRET --project=deno-edge-api
Production ve staging için farklı değerler kullanmak istiyorsanız, GitHub Environments özelliğini kullanmanızı öneririm. Repository Settings > Environments altında production ve staging environment’larını oluşturup her birine farklı secret’lar ekleyebilirsiniz.
Workflow dosyasında bunu şöyle kullanıyorsunuz:
deploy-staging:
name: Staging Deploy
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Staging deploy
uses: denoland/deployctl@v1
with:
project: deno-edge-api-staging
entrypoint: src/main.ts
token: ${{ secrets.DENO_DEPLOY_TOKEN }}
env:
WEBHOOK_SECRET: ${{ secrets.STAGING_WEBHOOK_SECRET }}
Deno KV ile Stateful Edge Uygulamaları
Deno Deploy’un gerçekten ilgi çekici özelliklerinden biri Deno KV. SQLite tabanlı, globally dağıtık bir key-value store. Bunu webhook router’ımıza entegre edelim:
// src/routes/webhook.ts
import { Hono } from 'hono';
const webhook = new Hono();
const kv = await Deno.openKv();
webhook.post('/github', async (c) => {
const event = c.req.header('X-GitHub-Event');
const payload = await c.req.json();
if (!event) {
return c.json({ error: 'GitHub event header eksik' }, 400);
}
const eventKey = ['github_events', event, Date.now().toString()];
await kv.set(eventKey, {
event,
repository: payload.repository?.full_name,
sender: payload.sender?.login,
timestamp: new Date().toISOString(),
payload: payload,
}, { expireIn: 7 * 24 * 60 * 60 * 1000 }); // 7 gun TTL
// Son olaylari say
const countKey = ['event_counts', event];
const current = await kv.get<number>(countKey);
await kv.set(countKey, (current.value ?? 0) + 1);
console.log(`GitHub event islendi: ${event} - ${payload.repository?.full_name}`);
return c.json({
success: true,
event,
processed_at: new Date().toISOString()
});
});
webhook.get('/stats', async (c) => {
const stats: Record<string, number> = {};
const eventTypes = ['push', 'pull_request', 'issues', 'release'];
for (const eventType of eventTypes) {
const result = await kv.get<number>(['event_counts', eventType]);
stats[eventType] = result.value ?? 0;
}
return c.json(stats);
});
export { webhook as webhookRouter };
Test Yazimi ve CI Entegrasyonu
Test dosyası tests/health_test.ts:
import { assertEquals } from 'std/assert/mod.ts';
import app from '../src/main.ts';
Deno.test('Health endpoint 200 donmeli', async () => {
const req = new Request('http://localhost/health');
const res = await app.fetch(req);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.status, 'ok');
});
Deno.test('Auth olmadan webhook 401 donmeli', async () => {
const req = new Request('http://localhost/api/webhook/github', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: true }),
});
const res = await app.fetch(req);
assertEquals(res.status, 401);
});
Deno.test('Gecersiz route 404 donmeli', async () => {
const req = new Request('http://localhost/olmayan-route');
const res = await app.fetch(req);
assertEquals(res.status, 404);
});
Yaygın Sorunlar ve Çözümleri
deployctl Action Versiyonu Uyumsuzluğu
Zaman zaman denoland/deployctl@v1 action’ı eski kalabiliyor. Eğer deployment sırasında garip hatalar alırsanız:
# Lokal deployctl versiyonunu kontrol et
deployctl --version
# En son versiyona guncelle
deno install -gAf https://deno.land/x/deploy/deployctl.ts
Workflow dosyasında version pinning yapmanızı öneririm:
- name: Deno Deploy
uses: denoland/[email protected] # Spesifik versiyon
with:
project: deno-edge-api
entrypoint: src/main.ts
token: ${{ secrets.DENO_DEPLOY_TOKEN }}
Import URL Cache Sorunlari
Deno’nun URL-based import sistemi bazen CI ortamında sorun çıkarabiliyor. Cache işlemini doğru yapmak önemli:
# deno.lock dosyasini olustur/guncelle
deno cache --lock=deno.lock src/main.ts
# Lock dosyasini commit'le
git add deno.lock
git commit -m "chore: deno lock dosyasi guncellendi"
Workflow’da lock file kontrolü ekleyin:
- name: Dependency cache kontrol
run: deno cache --lock=deno.lock --lock-write src/main.ts
- name: Lock dosyasi degisti mi kontrol et
run: |
if git diff --quiet deno.lock; then
echo "Lock dosyasi guncel"
else
echo "HATA: deno.lock dosyasi guncel degil, lutfen 'deno cache --lock=deno.lock src/main.ts' calistirin"
exit 1
fi
Rollback Stratejisi
Deployment sonrası bir şeyler ters giderse Deno Deploy dashboard’undan önceki deployment’a kolayca geçiş yapabiliyorsunuz. Ama bunu otomatize etmek de mümkün:
# Mevcut deployment'lari listele
deployctl deployments list --project=deno-edge-api
# Belirli bir deployment'i aktif et
deployctl deployments redeploy abc123def456 --project=deno-edge-api
GitHub Actions’a smoke test ekleyerek otomatik rollback tetikleyebilirsiniz:
- name: Smoke test
id: smoke_test
run: |
sleep 10 # Deploy'un propagate olmasini bekle
response=$(curl -s -o /dev/null -w "%{http_code}" https://deno-edge-api.deno.dev/health)
if [ "$response" != "200" ]; then
echo "Smoke test basarisiz! HTTP: $response"
exit 1
fi
echo "Smoke test basarili"
- name: Deploy basarisiz bildirimi
if: failure() && steps.smoke_test.outcome == 'failure'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Production Deploy Basarisiz - ${new Date().toISOString()}`,
body: `Commit `${context.sha}` production'a deploy edildikten sonra smoke test basarisiz oldu. Rollback gerekebilir.`,
labels: ['bug', 'production'],
});
Monitoring ve Observability
Deno Deploy kendi log streaming özelliğine sahip ama production’da bunu bir log aggregation servisine yönlendirmek daha mantıklı:
// src/middleware/logging.ts
import type { Context, Next } from 'hono';
export async function requestLogger(c: Context, next: Next) {
const start = Date.now();
const requestId = crypto.randomUUID();
c.set('requestId', requestId);
await next();
const duration = Date.now() - start;
const logEntry = {
request_id: requestId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration_ms: duration,
user_agent: c.req.header('User-Agent'),
cf_connecting_ip: c.req.header('CF-Connecting-IP'),
timestamp: new Date().toISOString(),
deploy_id: Deno.env.get('DENO_DEPLOYMENT_ID'),
region: Deno.env.get('DENO_REGION'),
};
// Structured logging
console.log(JSON.stringify(logEntry));
// Opsiyonel: Dis bir servise gonder
const logEndpoint = Deno.env.get('LOG_ENDPOINT');
if (logEndpoint && duration > 1000) { // Sadece yavas istekleri gonder
fetch(logEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logEntry),
}).catch(console.error);
}
}
DENO_DEPLOYMENT_ID ve DENO_REGION environment variable’ları Deno Deploy tarafından otomatik olarak inject ediliyor, hangi deployment’tan ve hangi bölgeden geldiğini anlamak için çok kullanışlı.
Sonuc
GitHub Actions ile Deno Deploy kombinasyonu, özellikle API servisleri, webhook handler’ları ve SSR uygulamaları için gerçekten güçlü bir pipeline sunuyor. Node.js ekosisteminin getirdiği build karmaşasından kurtulup doğrudan TypeScript yazıp deploy edebilmek büyük bir rahatlama.
Bu yazıda kurduğumuz pipeline şunları yapıyor: her PR için kalite kontrolü (lint, format, tip kontrolü, testler), PR’lara otomatik preview deployment, main branch’e merge sonrası production deployment, smoke test ile başarı doğrulama ve başarısızlık durumunda issue açma.
Dikkat etmeniz gereken başlıca noktaları tekrar vurgulayayım:
- deno.lock dosyasını commit’leyin, aksi halde CI ortamında import URL’leri farklı versiyonlara çözümlenebilir
- Environment variable’ları GitHub Environments üzerinden yönetin, direkt secrets yerine environment-scoped secrets kullanmak daha güvenli
- deployctl version’ını pinleyin, her çalışmada farklı bir action versiyonu çalışmasın
- Smoke test ekleyin, deployment başarılı görünse bile uygulamanın ayakta olduğunu doğrulayın
Deno Deploy hâlâ bazı kısıtlamalarla geliyor (filesystem yok, bazı Node.js compat sorunları), ama doğru use case için seçildiğinde inanılmaz hızlı ve sorunsuz bir deployment deneyimi sunuyor. Edge’de çalışan bir API servisi için aylık maliyetinizin neredeyse sıfır olduğunu düşününce değerlendirmeye değer bir seçenek.
