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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir