GitHub Actions Scheduled Workflow ile Zamanlı Görevler

Eğer CI/CD pipeline’larınızı sadece “birisi push yaptığında çalış” mantığıyla kurduysanız, muhtemelen masanızda bekleyip elle tetiklediğiniz işler hâlâ var demektir. Nightly build’ler, veritabanı yedeklemeleri, eski log dosyalarının temizlenmesi, güvenlik taramaları… Bunların hepsi gece yarısı siz uyurken otomatik olarak çalışabilir. GitHub Actions’ın scheduled workflow özelliği tam olarak bunun için var ve doğru kullanıldığında hayatı ciddi ölçüde kolaylaştırıyor.

Scheduled Workflow Nedir ve Neden Kullanırız

GitHub Actions normalde event-driven çalışır. push, pull_request, release gibi olaylar workflow’u tetikler. Ama bazı işlerin belirli bir zaman diliminde, düzenli aralıklarla çalışması gerekir. Scheduled workflow’lar bu ihtiyacı karşılar.

Teknik olarak bakarsak, GitHub’ın cron tabanlı bir zamanlayıcısı var ve siz bu zamanlayıcıya “her gece saat 02:00’de şunu yap” diyebiliyorsunuz. Underlyingda standart Unix cron syntax’ı kullanılıyor, dolayısıyla cron’a aşina olan herkes hemen adaptasyon sağlayabilir.

Gerçek dünya kullanım senaryoları şunlar oluyor genellikle:

  • Nightly build ve test: Her gece tüm test suite’ini çalıştırıp sabah işe geldiğinde raporları hazır bulmak
  • Dependency güncellemeleri: Haftalık olarak npm, pip veya gem bağımlılıklarını kontrol etmek
  • Veritabanı yedekleme: Üretim ortamının düzenli snapshot’larını almak
  • Performans testleri: Gece saatlerinde yük testi çalıştırmak
  • Güvenlik taramaları: Zafiyet taramalarını mesai saatleri dışına almak
  • Stale issue/PR temizliği: Uzun süredir hareketsiz kalan issue ve PR’ları otomatik etiketlemek veya kapatmak
  • Cache temizliği: Eski artifact ve cache’leri periyodik olarak temizlemek

Cron Syntax’ı ve Temel Yapı

Önce temel bir scheduled workflow nasıl görünür, oradan başlayalım:

name: Nightly Build

on:
  schedule:
    - cron: '0 2 * * *'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run nightly build
        run: |
          echo "Nightly build başlıyor: $(date)"
          make build
          make test

Cron syntax’ı beş alandan oluşur:

  • Dakika: 0-59 arası değer
  • Saat: 0-23 arası değer (UTC bazlı!)
  • Ayın günü: 1-31 arası değer
  • Ay: 1-12 arası değer
  • Haftanın günü: 0-7 arası değer (0 ve 7 Pazar’dır)

Yıldız işareti * “her” anlamına gelir. Birkaç pratik örnek:

  • 0 2 * – Her gece saat 02:00 UTC
  • 0 8 1-5 – Hafta içi her sabah 08:00 UTC
  • 0 0 0 – Her Pazar gece yarısı
  • 0 /6 – Her 6 saatte bir
  • 30 1 1 – Her ayın ilk günü 01:30 UTC

Önemli bir not: GitHub Actions zamanlama tamamen UTC bazında çalışır. Türkiye saati UTC+3 (yaz saatinde UTC+2) olduğundan, Türkiye saatiyle gece 02:00’de bir iş çalıştırmak istiyorsanız cron’a 0 23 * yazmanız gerekir (UTC+3 için: 23:00 UTC = 02:00 TR saati).

Ayrıca GitHub’ın resmi dokümantasyonunda şu uyarı var: Yoğun dönemlerde scheduled workflow’lar planlanandan birkaç dakika geç başlayabilir. Saniyeler içinde tetiklenmesi gereken kritik işler için scheduled workflow’a güvenmemek gerekir.

Gerçek Senaryo 1: Haftalık Dependency Audit

Projenizde npm bağımlılıklarınızın güvenlik açıklarını her Pazartesi sabahı kontrol eden ve sonuçları Slack’e gönderen bir workflow yazalım:

name: Weekly Security Audit

on:
  schedule:
    - cron: '0 6 * * 1'
  workflow_dispatch:

jobs:
  security-audit:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run npm audit
        id: audit
        run: |
          npm audit --json > audit-report.json || true
          VULN_COUNT=$(cat audit-report.json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('metadata',{}).get('vulnerabilities',{}).get('total',0))")
          echo "vuln_count=$VULN_COUNT" >> $GITHUB_OUTPUT
          echo "Toplam zafiyet: $VULN_COUNT"

      - name: Upload audit report
        uses: actions/upload-artifact@v4
        with:
          name: security-audit-report
          path: audit-report.json
          retention-days: 30

      - name: Notify Slack
        if: steps.audit.outputs.vuln_count > 0
        uses: slackapi/[email protected]
        with:
          payload: |
            {
              "text": "Haftalık Güvenlik Raporu",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*${{ github.repository }}* reposunda *${{ steps.audit.outputs.vuln_count }}* güvenlik açığı tespit edildi.n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Raporu görüntüle>"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Burada dikkat ettiğiniz bir şey var mı? workflow_dispatch ekledim. Bu sayede hem zamanlayıcı tetikleyebilir hem de siz istediğiniz an GitHub UI’dan manuel olarak çalıştırabilirsiniz. Scheduled workflow’lara her zaman workflow_dispatch eklemek iyi bir alışkanlık.

Gerçek Senaryo 2: Nightly Database Backup

Production ortamında PostgreSQL veritabanının gece düzenli yedeklerini alan ve S3’e yükleyen bir workflow:

name: Nightly Database Backup

on:
  schedule:
    - cron: '0 23 * * *'
  workflow_dispatch:

env:
  BACKUP_BUCKET: s3://my-company-backups
  BACKUP_PREFIX: postgres/nightly

jobs:
  backup:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: eu-central-1

      - name: Install PostgreSQL client
        run: |
          sudo apt-get update -qq
          sudo apt-get install -y postgresql-client-16

      - name: Create backup
        run: |
          TIMESTAMP=$(date +%Y%m%d_%H%M%S)
          BACKUP_FILE="backup_${TIMESTAMP}.sql.gz"

          echo "Yedek alınıyor: $BACKUP_FILE"

          PGPASSWORD=${{ secrets.DB_PASSWORD }} pg_dump 
            -h ${{ secrets.DB_HOST }} 
            -U ${{ secrets.DB_USER }} 
            -d ${{ secrets.DB_NAME }} 
            --no-password 
            --verbose 
            | gzip > $BACKUP_FILE

          echo "BACKUP_FILE=$BACKUP_FILE" >> $GITHUB_ENV
          echo "Yedek boyutu: $(du -sh $BACKUP_FILE | cut -f1)"

      - name: Upload to S3
        run: |
          aws s3 cp $BACKUP_FILE 
            ${{ env.BACKUP_BUCKET }}/${{ env.BACKUP_PREFIX }}/$BACKUP_FILE 
            --storage-class STANDARD_IA

          echo "S3'e yüklendi: ${{ env.BACKUP_BUCKET }}/${{ env.BACKUP_PREFIX }}/$BACKUP_FILE"

      - name: Cleanup old backups (30 günden eski)
        run: |
          CUTOFF_DATE=$(date -d "30 days ago" +%Y-%m-%d)
          echo "Şu tarihten eski yedekler siliniyor: $CUTOFF_DATE"

          aws s3 ls ${{ env.BACKUP_BUCKET }}/${{ env.BACKUP_PREFIX }}/ 
            | awk -v cutoff="$CUTOFF_DATE" '$1 < cutoff {print $4}' 
            | while read -r file; do
                echo "Siliniyor: $file"
                aws s3 rm "${{ env.BACKUP_BUCKET }}/${{ env.BACKUP_PREFIX }}/$file"
              done

      - name: Notify on failure
        if: failure()
        run: |
          curl -X POST ${{ secrets.PAGERDUTY_WEBHOOK }} 
            -H "Content-Type: application/json" 
            -d "{"message": "KRITIK: Nightly backup basarisiz! Run: ${{ github.run_id }}"}"

Bu workflow’da environment: production kullanımına dikkat edin. GitHub Environments özelliği sayesinde, bu workflow’un üretim ortamı secret’larına erişimi kısıtlayabilir ve isteğe bağlı onay mekanizması kurabilirsiniz.

Birden Fazla Schedule Tanımlamak

Tek bir workflow dosyasında birden fazla schedule tanımlayabilirsiniz. Örneğin, bazı işlerin gündüz daha sık, gece daha seyrek çalışmasını istiyorsanız:

name: Health Check Monitor

on:
  schedule:
    - cron: '*/15 8-18 * * 1-5'
    - cron: '0 */2 * * *'

jobs:
  health-check:
    runs-on: ubuntu-latest
    steps:
      - name: Check API health
        run: |
          RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://api.myapp.com/health)

          if [ "$RESPONSE" != "200" ]; then
            echo "HATA: API yanıt vermiyor! HTTP kodu: $RESPONSE"
            exit 1
          fi

          echo "API sağlıklı. HTTP: $RESPONSE - $(date)"

      - name: Check response time
        run: |
          RESPONSE_TIME=$(curl -s -o /dev/null -w "%{time_total}" https://api.myapp.com/health)
          echo "Yanıt süresi: ${RESPONSE_TIME}s"

          # 3 saniyeden uzun sürüyorsa uyar
          if (( $(echo "$RESPONSE_TIME > 3.0" | bc -l) )); then
            echo "UYARI: Yanıt süresi yüksek: ${RESPONSE_TIME}s"
            exit 1
          fi

Hafta içi mesai saatlerinde (08-18 UTC) her 15 dakikada bir, gece ve hafta sonları ise her 2 saatte bir çalışır.

Matrix ile Paralel Scheduled Jobs

Birden fazla servisi veya ortamı aynı anda kontrol etmek için matrix strategy ile scheduled workflow’u birleştirmek güzel sonuçlar veriyor:

name: Multi-Environment Health Check

on:
  schedule:
    - cron: '0 */4 * * *'
  workflow_dispatch:

jobs:
  check:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        environment:
          - name: staging
            url: https://staging.myapp.com
            expected_version: "2.1"
          - name: production
            url: https://myapp.com
            expected_version: "2.0"
          - name: dr-site
            url: https://dr.myapp.com
            expected_version: "2.0"

    steps:
      - name: Check ${{ matrix.environment.name }}
        run: |
          ENV_NAME="${{ matrix.environment.name }}"
          ENV_URL="${{ matrix.environment.url }}"
          EXPECTED_VER="${{ matrix.environment.expected_version }}"

          echo "Kontrol ediliyor: $ENV_NAME ($ENV_URL)"

          HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$ENV_URL/health")
          APP_VERSION=$(curl -s "$ENV_URL/version" | python3 -c "import sys,json; print(json.load(sys.stdin).get('version','unknown'))" 2>/dev/null || echo "unknown")

          echo "HTTP Kodu: $HTTP_CODE"
          echo "Uygulama versiyonu: $APP_VERSION"

          if [ "$HTTP_CODE" != "200" ]; then
            echo "KRITIK: $ENV_NAME down! HTTP: $HTTP_CODE"
            exit 1
          fi

          echo "$ENV_NAME - OK (version: $APP_VERSION)"

Scheduled Workflow’larda Dikkat Edilmesi Gereken Noktalar

Default Branch Zorunluluğu

Scheduled workflow’lar sadece default branch (genellikle main veya master) üzerindeki workflow dosyasını çalıştırır. Feature branch’inizdeki bir schedule tanımı test ortamında çalışmaz. Bu özellikle yeni bir scheduled workflow geliştirirken can sıkıcı olabilir. Test etmek için workflow_dispatch tetikleyicisini ekleyip manuel olarak çalıştırın, doğruladıktan sonra merge edin.

60 Günlük İnaktivite Kuralı

GitHub, repository’de 60 gün boyunca hiçbir aktivite olmazsa scheduled workflow’ları otomatik olarak devre dışı bırakır ve size bir email gönderir. Açık kaynak veya uzun dönemli bakım projelerinde bu durumla karşılaşabilirsiniz. GitHub, devre dışı bırakmadan önce onay email’i gönderiyor, oradan “Keep enabled” diyerek devam ettirebilirsiniz.

Çakışan Çalışmaları Önlemek

Bir scheduled job, bir önceki çalışması bitmeden tekrar tetiklenirse ne olur? Varsayılan davranış her ikisinin de çalışmasına izin vermektir. Bunu engellemek için concurrency kullanın:

name: Daily Report Generator

on:
  schedule:
    - cron: '0 1 * * *'

concurrency:
  group: daily-report
  cancel-in-progress: false

jobs:
  generate-report:
    runs-on: ubuntu-latest
    steps:
      - name: Generate daily report
        run: |
          echo "Günlük rapor oluşturuluyor..."
          # Uzun süren rapor işlemi
          sleep 3600

cancel-in-progress: false ayarı, yeni bir tetikleme olduğunda önceki çalışmanın bitmesini bekler ve yeni olanı kuyrukta tutar. true yaparsanız önceki çalışmayı iptal eder.

Minimum Çalıştırma Sıklığı

GitHub Actions’ta minimum schedule sıklığı 5 dakikada bir‘dir. /1 * yazsanız bile 5 dakikada bir çalışır. Daha sık çalıştırmanız gerekiyorsa self-hosted runner ile kendi cron altyapınızı kurmanız gerekir.

Conditional Logic ile Akıllı Scheduled Jobs

Bazı durumlarda scheduled workflow’un ne zaman tetiklendiğine göre farklı davranmasını isteyebilirsiniz. Örneğin Pazartesi sabahları tam yedek, diğer günler incremental yedek:

name: Smart Backup Strategy

on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:
    inputs:
      backup_type:
        description: 'Yedek tipi'
        required: false
        default: 'auto'
        type: choice
        options:
          - auto
          - full
          - incremental

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: Determine backup type
        id: backup-type
        run: |
          if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.backup_type }}" != "auto" ]; then
            BACKUP_TYPE="${{ inputs.backup_type }}"
          elif [ "$(date +%u)" == "1" ]; then
            # Pazartesi = tam yedek
            BACKUP_TYPE="full"
          else
            BACKUP_TYPE="incremental"
          fi

          echo "backup_type=$BACKUP_TYPE" >> $GITHUB_OUTPUT
          echo "Seçilen yedek tipi: $BACKUP_TYPE"

      - name: Run full backup
        if: steps.backup-type.outputs.backup_type == 'full'
        run: |
          echo "TAM YEDEK alınıyor..."
          # Full backup komutu

      - name: Run incremental backup
        if: steps.backup-type.outputs.backup_type == 'incremental'
        run: |
          echo "ARTIMSAL YEDEK alınıyor..."
          # Incremental backup komutu

Zamanlı Görevlerde Monitoring ve Alerting

Scheduled workflow’larınızın çalışıp çalışmadığını nasıl takip ediyorsunuz? Manuel kontrol etmek pratik değil. İki yaklaşım işe yarıyor:

Birincisi, GitHub’ın kendi notification ayarlarından “Failed workflows” için email bildirimi aktif etmek. Repository Settings > Notifications bölümünden yapabilirsiniz.

İkincisi, her başarılı çalışmada bir “heartbeat” göndermek. Healthchecks.io veya benzeri bir servisle entegre ederek eğer beklenen heartbeat gelmezse uyarı alabiliriz:

name: Nightly Data Sync

on:
  schedule:
    - cron: '0 3 * * *'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Ping start
        run: |
          curl -s "${{ secrets.HEALTHCHECK_URL }}/start" || true

      - name: Run data sync
        run: |
          echo "Veri senkronizasyonu başlıyor..."
          # Senkronizasyon işlemleri burada

      - name: Ping success
        if: success()
        run: |
          curl -s "${{ secrets.HEALTHCHECK_URL }}" || true

      - name: Ping failure
        if: failure()
        run: |
          curl -s "${{ secrets.HEALTHCHECK_URL }}/fail" || true

Bu pattern’de Healthchecks.io gibi bir servis, beklenen saatte heartbeat gelmezse size email veya SMS atar. Workflow çalıştı ama işler başarısız oldu mu? O da ayrı /fail endpoint’iyle bildiriliyor.

Self-Hosted Runner ile Scheduled Jobs

Veritabanı yedeklemeleri veya iç ağa erişim gerektiren işleri GitHub’ın hosted runner’larında çalıştırmak güvenli değil ya da mümkün olmayabiliyor. Self-hosted runner kullanırken scheduled workflow yapısı aynı kalıyor, sadece runs-on değişiyor:

name: Internal DB Maintenance

on:
  schedule:
    - cron: '0 4 * * 0'

jobs:
  maintenance:
    runs-on: self-hosted
    steps:
      - name: Cleanup old records
        run: |
          psql -h localhost -U dbadmin -d maindb -c 
            "DELETE FROM audit_logs WHERE created_at < NOW() - INTERVAL '90 days';"

      - name: Vacuum analyze
        run: |
          psql -h localhost -U dbadmin -d maindb -c "VACUUM ANALYZE;"

      - name: Reindex
        run: |
          psql -h localhost -U dbadmin -d maindb -c "REINDEX DATABASE maindb;"

Self-hosted runner’ınızın iç ağda olması sayesinde doğrudan veritabanına erişebilir, external network açmanıza gerek kalmaz.

Workflow’ları Test Etmek

Scheduled workflow geliştirirken en büyük can sıkıcı şey test sürecidir. Cron zamanını beklemek pratik değil. Şu adımları izlemek işleri kolaylaştırır:

  • Her zaman workflow_dispatch tetikleyicisi ekle, manuel test için
  • Geliştirme sırasında cron’u /5 * yap (her 5 dakika), merge’den önce gerçek zamana al
  • act aracını kullanarak workflow’ları lokalde test et
# act kurulumu (macOS)
brew install act

# Scheduled workflow'u lokalde test etme
act schedule

# Belirli bir job'ı test etme
act schedule -j backup

# Workflow_dispatch ile test
act workflow_dispatch

act her özelliği desteklemiyor ama temel adımları test etmek için yeterli.

Sonuç

Scheduled workflow’lar, GitHub Actions’ın en değer katan ama en az konuşulan özelliklerinden biri. Düzgün kurulmuş bir yapıyla gece yarısı testler çalışıyor, yedekler alınıyor, güvenlik taramaları yapılıyor ve sabah işe geldiğinizde her şey hazır.

Birkaç kritik noktayı tekrar vurgulamak istiyorum: Cron zamanlaması her zaman UTC bazlıdır, her scheduled workflow’a workflow_dispatch eklemek test ve acil durum müdahalesi için şart, uzun süren işlerde concurrency ile çakışmaları engellemek gerekiyor ve monitoring olmadan hiçbir scheduled job “çalışıyor” sayılmaz.

En önemli tavsiye şu: Mevcut crontab dosyanıza ya da Windows Task Scheduler görevlerinize bakın. Bunların kaçı gerçekte bir uygulama deposuyla ilişkili? Bunları ilgili repo’nun workflow’u haline getirmek hem versiyon kontrolü hem de ekip görünürlüğü açısından büyük fayda sağlıyor. Infrastructure as Code mantığını scheduled görevlere de taşımak, “bu iş ne zaman bozuldu ki?” sorusunun cevabını git log’dan bulmanızı mümkün kılıyor.

Bir yanıt yazın

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