Mert Özen Her Satırda Daha İleri
Proje Yönetimi Jira ve Proje Araçları — #04

Jira Automation ve GitHub Entegrasyonu: Rule'lar, Smart Value'lar, Kullanım Limiti ve REST API

Mert Özen 15 Kas 2026 17 dk 4 görüntülenme
Jira Automation ve GitHub Entegrasyonu: Rule'lar, Smart Value'lar, Kullanım Limiti ve REST API

Jira automation rule'ları trigger, condition, action ve branch'ten oluşur; 3 Aralık 2026'dan itibaren çalışan her step kullanım bazlı faturalanıyor ve bir kuralın action'ı varsayılan olarak başka kuralı tetiklemiyor. Yazı Bookstore'un dört kuralını, step hesabını, GitHub for Atlassian ve smart commits'i, commit mesajlarını kontrol eden ve REST API v3 ile release notu taslağı çıkaran bir .NET aracını anlatıyor.

Sürüm notu: Bu yazıdaki C# kodu .NET 10 (SDK 10.0.402), Microsoft.Extensions.Configuration.UserSecrets 10.0.15 ve Microsoft.Extensions.Configuration.EnvironmentVariables 10.0.15 ile derlenip çalıştırılmıştır. Automation, GitHub entegrasyonu ve Jira API davranışı Jira Cloud'un 2026-11 itibarıyla güncel belgelerine dayanır (Atlassian Support ve Atlassian Developer, 15 Kasım 2026'da kontrol edildi).

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) ekibi iki automation rule yazıyor. Birincisi: bir PR (pull request; değişikliğin ana branch'e alınması için açılan istek) merge edilince bağlı work item'ı Testing'den Done'a taşı. İkincisi: bir subtask Done'a geçtiğinde, parent'ın bütün subtask'ları bittiyse parent'ı da ilerlet. Emre BOOK-143 subtask'ının PR'ını merge ediyor; birinci kural subtask'ı Done'a alıyor. BOOK-142 story'sinin diğer subtask'ı zaten bitmişti.

Tahmin et: İkinci kural çalışır mı, yani story ilerler mi? Ve bu iki kural ayda yüzlerce kez çalışırsa 3 Aralık 2026'dan sonra faturaya nasıl yansır? Bir de kurumsal satıştan Hakan'ın kendi API token'ıyla kurduğu raporlama script'i var; Hakan şirketten ayrıldığında ne olur?

Automation Rule'un Anatomisi: Trigger, Condition, Action, Branch

Her automation rule bir trigger ile başlıyor: bir work item'ın oluşturulması, bir alanın değişmesi, bir transition, bir zamanlama ya da bağlı geliştirme aracından gelen bir olay (branch oluşturuldu, commit atıldı, PR açıldı, PR merge edildi). Condition'lar kuralın devam edip etmeyeceğine karar veriyor; trigger'ın içine de koşul eklenebiliyor ("yalnız Done'a geçişlerde" gibi). Action'lar asıl işi yapıyor: transition, alan güncelleme, yorum, e-posta. Branch ise kuralın bir bölümünü trigger'daki item yerine ilişkili item'larda çalıştırıyor: parent, subtask'lar ya da bir JQL (Jira Query Language; Jira'nın arama sorgu dili) sorgusunun döndürdükleri. Atlassian'ın yeni sayfaları rule yerine "flow" kelimesini de kullanıyor; ikisi aynı şey.

04-automation-rule-tr.png

Branch'lerle ilgili iki kural belgede açıkça yazıyor. Kuralın ana kolu her zaman trigger'daki item'a uygulanıyor; kural içinde yeni bir item oluşturup ona yorum eklemek istiyorsan bunu oluşturulan item'lar için açılmış bir branch'te yapman gerekiyor. Birden fazla item üzerinde çalışan branch'ler ise paralel çalışıyor ve sıra garantisi yok; bir branch'te yapılan değişikliğe diğerinde güvenemezsin.

Sınırları da bilmek gerek. Bir kural en fazla 65 step içerebiliyor, zamanlanmış bir JQL araması en fazla 999 item döndürebiliyor, Lookup work items action'ı yalnız ilk 100 item'ı kullanıyor ve bir kural kısa sürede kendini ya da başka kuralları 10 kez tetiklerse loop detection devreye girip çalışmayı LOOP durumuyla durduruyor. Aynı anda çalışabilen kural sayısı plana göre değişiyor: Free 5, Standard 10, Premium 20, Enterprise 30.

Bu bölümde resmî belgelerden yararlanılmıştır.

Smart Value'lar

Smart value'lar kuralın içinde değişen veriyi kullanmanı sağlıyor. Biçim çift süslü parantez ve nokta gösterimi: {{issue.key}}, {{issue.summary}}, {{issue.assignee.displayName}}. Fonksiyonlarla zincirlenebiliyorlar: {{now.plusDays(5)}} beş gün sonrası, {{issue.summary.toLowerCase()}} küçük harfli özet. Geliştirme olaylarının kendi smart value'ları var: PR trigger'larında {{pullRequest.title}}, {{pullRequest.url}}, {{pullRequest.sourceBranch}}, {{pullRequest.destinationBranch}}; branch trigger'ında {{branch.name}}.

Bir smart value'nun ne döndürdüğünü görmenin belgede önerilen yolu basit: geçici bir kural kur, trigger olarak Manual trigger from work item, action olarak Log action seç, kuralı bir item üzerinden elle çalıştır ve sonucu audit log'da oku. Log action step sayılmadığı için bu deneme kullanım kotasından da yemiyor.

Bu bölümde resmî belgelerden yararlanılmıştır.

Bookstore'un Dört Kuralı

02. yazıda board'a "PR açılınca In Review, merge'de Done" demiştik. Bookstore ekibi bunu ve iki ek ihtiyacı dört kurala çevirdi:

Rule 1  PR opened: move to In Review
  Trigger    Pull request created
  Condition  Issue fields: Status = In Progress
  Action     Transition work item: Request Review

Rule 2  PR merged: move to Done
  Trigger    Pull request merged
  Condition  Issue fields: Status = Testing
  Action     Transition work item: Pass Test

Rule 3  Last subtask done: move the parent
  Trigger    Work item transitioned (to Done), work type = Subtask
  Branch     Parent
    Condition  Related issues: all subtasks match statusCategory = Done
    Action     Transition work item: Request Review
  Rule details  Allow rule trigger = on

Rule 4  Stale work reminder
  Trigger    Scheduled, every day 09:00
             JQL: project = BOOK AND statusCategory = "In Progress" AND updated <= "-7d"
  Action     Comment on work item:
             {{issue.assignee.displayName}}, {{issue.key}} has not moved for 7 days. Blocked?

Kural 1 ve 2 aynı fikrin iki yüzü: PR olayları item'ı yalnız beklenen status'teyse hareket ettiriyor. Testing'i atlayıp merge edilen bir PR item'ı Done'a almıyor; item In Review'da kalıyor ve board bu durumu herkese gösteriyor. Kural 3 parent'ı Done'a değil In Review'a alıyor, çünkü story'nin bütünü yine review ve test'ten geçmeli; subtask'ların bitmesi story'nin bittiği anlamına gelmiyor. Kural 4'teki updated koşulu, 03. yazıdaki "beş gündür dokunulmamış iş" sorgusunun yedi günlük, ekip çapındaki hâli; amacı kimseyi suçlamak değil, takılmış işi Daily Scrum'dan önce görünür yapmak.

Baştaki sorunun cevabı Kural 3'ün son satırında. Atlassian'ın destek makalesine göre bir kuralın action'ı varsayılan olarak başka bir kuralı tetiklemiyor; bu, kural döngülerini önlemek için. Kural 2 subtask'ı Done'a aldığında Kural 3 ancak Rule details'te Allow rule trigger kutusu işaretliyse çalışıyor. Belgedeki not da ayar yalnız gerçekten başka bir kurala tepki vermesi gereken kurallarda açılsın diyor. Bookstore ekibi bu yüzden kutuyu yalnız Kural 3'te açtı.

Bu bölümde resmî belgelerden yararlanılmıştır.

Kullanım: Step Sayımı ve 3 Aralık 2026

Atlassian'ın kullanım sayfası 3 Aralık 2026'da kullanım bazlı faturalandırmanın başladığını yazıyor. Birim step: çalışan her trigger, her condition, her action bir step; branch ve loop kendileri için bir step artı içlerinde çalışan step'ler. Trigger'ın içine yazılan koşul trigger'ın step'ine dahil, ek step saymıyor. Hiç action çalışmayan ve NO_ACTIONS_PERFORMED ile biten çalışmalar 5 Kasım 2026'dan beri step saymıyor; Log action, Create variable, Re-fetch work item data ve Lookup work items gibi sistem step'leri de sayılmıyor.

Kota organizasyon seviyesinde havuzlanıyor ve her ay yenileniyor. Jira için aylık step hakkı Free planda abonelik başına 150, Standard'da kullanıcı başına 400, Premium'da 750, Enterprise'da 1000. Ek kullanım açıksa aşan her 1.000 step 0,50 dolar olarak faturalanıyor ve admin'lere %80 ve %100'de bildirim gidiyor; ek kullanım kapalıysa %100'e gelindiğinde bütün kurallar yeni döneme kadar duruyor.

Bookstore'un hesabı: Standard planda 8 Jira kullanıcısı ayda 3.200 step demek. Kural 2 her çalışmada 3 step harcıyor (trigger, condition, action); ayda 60 merge edilen PR 180 step. Condition'ı trigger'ın içine taşısaydık belgedeki örnekte olduğu gibi her çalışma 2 step'e inerdi. Kural 4 her gün bir trigger ve bulduğu her item için bir yorum action'ı demek; günde ortalama 4 takılmış item varsa günde 5 step, ayda yaklaşık 150. Bu dört kuralın toplamı kotanın küçük bir kısmı. Pahalı olan, her alan değişikliğinde tetiklenen geniş bir Field value changed kuralı ya da yüzlerce item üzerinde dönen bir loop; kullanım sayfasındaki örnekte üç item üzerinde dönen tek action'lı bir loop bile dört step ediyor.

Hangi kuralın ne harcadığı Jira'da Settings, System, Global automation altındaki Usage ekranında kural kural görülebiliyor; bir kurala tıklayınca audit log'da her çalışmanın kaç step kullandığı çıkıyor.

Bu bölümde resmî belgelerden yararlanılmıştır.

GitHub Entegrasyonu: Key'i Doğru Yere Yazmak

GitHub'ı Jira'ya bağlayan uygulamanın güncel adı GitHub for Atlassian. Kurulum iki kişinin işi: Jira site admin'i uygulamayı kuruyor, GitHub organization owner'ı organizasyonu bağlıyor ve Jira'nın tüm repo'lara mı yoksa seçili repo'lara mı erişeceğini seçiyor. Seçili repo'lar seçildiyse sonradan açılan repo'lar otomatik eklenmiyor; Bookstore ekibi yeni repo açtığında bunu kontrol listesine yazdı.

Bağlantıdan sonra eşleşmenin tek anahtarı work item key'i. Branch adında, commit mesajında ya da PR başlığında key geçerse Jira onu ilgili item'ın Development paneline bağlıyor; key büyük harfle yazılmalı, book-142 eşleşmiyor. Eşleşme push'tan sonra oluşuyor ve birkaç dakika sürebiliyor. Board'daki kartlarda geliştirme ikonları görünüyor (board'da 100'den az item varsa) ve Development sayfası açıksa PR cycle time ve deployment frequency gibi metrikler orada toplanıyor. Bu bölümdeki kurallar Git ve GitHub serisinin 14. ve 15. yazılarındaki PR akışı ve branch stratejisinin üzerine oturuyor:

git switch -c BOOK-142-bulk-order-discount
git commit -m "BOOK-142 Add bulk order discount for institutions"
gh pr create --title "BOOK-142 Bulk order discount for institutions" --body "Jira: BOOK-142"

Bu bölümde resmî belgelerden yararlanılmıştır.

Smart Commits ve Küçük Bir Kontrol

Smart commits commit mesajından Jira'da iş yapmayı sağlıyor. Belgedeki syntax şöyle: önce key, sonra #komut ve argümanları. Üç komut var: #comment yorum ekliyor, #time 1h 30m zaman kaydediyor, başka her #ad o adı taşıyan transition'ı çalıştırıyor. Kurallar sıkı: komut tek satırı aşamıyor, her satırın kendi key'i olmalı; transition adının yalnız ilk boşluğa kadarki kısmına bakılıyor, belirsizlikte tireli tam ad gerekiyor; #resolve ile resolution alanı set edilemiyor. Commit'teki e-posta adresi Jira'da tam olarak bir kullanıcıyla eşleşmeli ve o kullanıcının işlemi yapmaya yetkisi olmalı; eşleşmezse commit yine görünüyor ama komut çalışmıyor. Geçmişi yeniden yazan git push --force ya da squash merge, aynı komutları içeren "yeni" commit'ler ürettiği için komutların ikinci kez çalışmasına yol açabiliyor.

Bu bölümde resmî belgelerden yararlanılmıştır.

Bu kuralların çoğu commit atılmadan kontrol edilebilir. Bookstore.JiraTools'a bir smart-commit komutu ekledik; Bookstore workflow'unun transition adlarını biliyor ve mesajı satır satır belgede yazan kurallara göre inceliyor:

using System.Text.RegularExpressions;

namespace Bookstore.JiraTools;

public sealed record SmartCommitLine(int Number, IReadOnlyList<string> Keys, IReadOnlyList<string> Commands,
    IReadOnlyList<string> Problems);

public static partial class SmartCommit
{
    [GeneratedRegex(@"\b[A-Z]{2,}-\d+\b")]
    private static partial Regex Key();

    [GeneratedRegex(@"\b[a-z]{2,}-\d+\b")]
    private static partial Regex LowercaseKey();

    [GeneratedRegex(@"#(?<name>[A-Za-z][\w-]*)")]
    private static partial Regex Command();

    public static IReadOnlyList<SmartCommitLine> Parse(string message, IReadOnlyList<string> transitions)
    {
        List<SmartCommitLine> result = [];
        string[] lines = message.Split('\n');
        for (int i = 0; i < lines.Length; i++)
        {
            string line = lines[i];
            List<string> keys = [.. Key().Matches(line).Select(m => m.Value)];
            List<string> commands = [.. Command().Matches(line).Select(m => m.Groups["name"].Value)];
            List<string> problems = [];

            if (keys.Count == 0 && LowercaseKey().Match(line) is { Success: true } lower)
                problems.Add($"{lower.Value} is lowercase; Jira will not link it");
            if (keys.Count == 0 && commands.Count > 0)
                problems.Add("commands ignored: no work item key on this line");
            foreach (string command in commands.Where(c => c is not ("comment" or "time")))
                problems.AddRange(CheckTransition(command, transitions));

            if (keys.Count > 0 || commands.Count > 0 || problems.Count > 0)
                result.Add(new SmartCommitLine(i + 1, keys, commands, problems));
        }
        return result;
    }

    // Jira, transition adının ilk boşluktan önceki kısmına bakar;
    // belirsizlikte tireli tam ad ("request-review") birini seçer.
    private static IEnumerable<string> CheckTransition(string command, IReadOnlyList<string> transitions)
    {
        string[] exact = [.. transitions.Where(t => t.Replace(' ', '-').Equals(command, StringComparison.OrdinalIgnoreCase))];
        if (exact.Length == 1)
            yield break;
        string[] byFirstWord = [.. transitions.Where(t => t.Split(' ')[0].Equals(command, StringComparison.OrdinalIgnoreCase))];
        if (byFirstWord.Length == 0)
            yield return $"#{command} matches no transition";
        else if (byFirstWord.Length > 1)
            yield return $"#{command} is ambiguous: {string.Join(", ", byFirstWord)}";
    }
}
using Bookstore.JiraTools;
using Microsoft.Extensions.Configuration;

string[] bookstoreTransitions =
    ["Start Work", "Request Review", "Request Changes", "Approve", "Pass Test", "Fail Test", "Reopen"];

return args switch
{
    ["lint-jql", var path] => LintFilters(path),
    ["smart-commit", var path] => CheckSmartCommits(path, bookstoreTransitions),
    ["release-notes", .. var options] => await WriteReleaseNotesAsync(options),
    _ => Usage(),
};
static int CheckSmartCommits(string path, string[] transitions)
{
    // Mesajlar yalnız "---" içeren bir satırla ayrılır.
    string[] messages = File.ReadAllText(path).Replace("\r\n", "\n").Split("\n---\n");
    int problems = 0;
    for (int m = 0; m < messages.Length; m++)
    {
        Console.WriteLine($"message {m + 1}");
        foreach (SmartCommitLine line in SmartCommit.Parse(messages[m].Trim(), transitions))
        {
            string keys = line.Keys.Count > 0 ? string.Join(" ", line.Keys) : "-";
            string commands = line.Commands.Count > 0 ? string.Join(" ", line.Commands.Select(c => "#" + c)) : "-";
            Console.WriteLine($"  line {line.Number}: keys {keys}, commands {commands}");
            foreach (string problem in line.Problems)
                Console.WriteLine($"    problem: {problem}");
            problems += line.Problems.Count;
        }
    }
    Console.WriteLine($"{messages.Length} messages, {problems} problems");
    return problems > 0 ? 1 : 0;
}

Ekibin yazdığı beş örnek mesaj:

BOOK-142 Add bulk order discount for institutions #comment 10% off from 20 copies
---
book-151 fix VAT rounding on invoice PDF
---
BOOK-153 Keep institutional discount after login #request
---
BOOK-153 BOOK-155 #request-review #time 1h 30m
---
BOOK-147 #comment Approval mail uses the new template
and the approver name in the subject #time 2h

Reponun kökünden değil, proje klasöründen çalıştırınca gerçek çıktı (çıkış kodu 1):

cd tools/Bookstore.JiraTools
dotnet run -- smart-commit ../../jira/commit-messages.txt
message 1
  line 1: keys BOOK-142, commands #comment
message 2
  line 1: keys -, commands -
    problem: book-151 is lowercase; Jira will not link it
message 3
  line 1: keys BOOK-153, commands #request
    problem: #request is ambiguous: Request Review, Request Changes
message 4
  line 1: keys BOOK-153 BOOK-155, commands #request-review #time
message 5
  line 1: keys BOOK-147, commands #comment
  line 2: keys -, commands #time
    problem: commands ignored: no work item key on this line
5 messages, 3 problems

İkinci mesaj küçük harfli key yüzünden hiçbir item'a bağlanmıyor. Üçüncüsü 02. yazıda bilerek bıraktığımız belirsizliğe takılıyor: Request Review ve Request Changes aynı kelimeyle başlıyor. Dördüncüsü doğru yazımı gösteriyor: iki key, tireli tam transition adı ve zaman kaydı. Beşincide ikinci satırın key'i olmadığı için #time komutu yok sayılıyor; belgedeki çok satırlı örneğin aynısı. Bu kontrol bir commit-msg hook'una ya da PR pipeline'ına konabilir; e-posta eşleşmesi ve yetki gibi Jira tarafındaki koşulları ise yalnız Jira bilebilir.

REST API v3 ile Release Notu Taslağı

Son parça, sprint'te biten işlerden bir release notu taslağı çıkaran küçük bir komut. Jira Cloud REST (HTTP üzerinden kaynak odaklı API tarzı) API v3'te kimlik doğrulamanın en basit yolu basic authentication: Atlassian hesabının e-posta adresi ve bir API token. Belge bu yöntemi basit script'ler ve elle yapılan çağrılar için öneriyor; dağıtılacak entegrasyonlar için OAuth 2.0 ile bir uygulama yazılmasını istiyor. API token'lar 15 Aralık 2024'ten beri varsayılan olarak bir yıl sonra sona eriyor ve oluştururken 1 ile 365 gün arasında bir süre seçiliyor. Scope'lu token'lar da var; belgeye göre bunlarla çağrılar site adresine değil https://api.atlassian.com/ex/jira/{cloudId} adresine yapılıyor.

Arama için /rest/api/3/search/jql endpoint'ini kullanıyoruz; eski /rest/api/3/search belgede "kaldırılıyor" olarak işaretli. Yeni endpoint sayfalamayı sayfa numarasıyla değil nextPageToken ile yapıyor ve son sayfada isLast dönüyor. fields parametresiyle yalnız ihtiyaç duyulan alanlar isteniyor. Belge ayrıca son güncellemelerin arama sonuçlarına hemen yansımayabileceğini söylüyor; sprint kapanır kapanmaz değil, birkaç dakika sonra çalıştırmak daha güvenli.

Bu bölümde resmî belgelerden yararlanılmıştır.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>Bookstore.JiraTools</RootNamespace>
    <UserSecretsId>bookstore-jiratools</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.15" />
    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.15" />
  </ItemGroup>

</Project>
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

namespace Bookstore.JiraTools;

public sealed class JiraSearchClient(HttpClient http)
{
    private const string Fields = "summary,issuetype";

    public static HttpClient CreateHttpClient(Uri siteUrl, string email, string apiToken)
    {
        HttpClient http = new() { BaseAddress = siteUrl };
        string basic = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{email}:{apiToken}"));
        http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basic);
        http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        return http;
    }

    public static string BuildSearchPath(string jql, string? nextPageToken = null)
    {
        string path = $"rest/api/3/search/jql?jql={Uri.EscapeDataString(jql)}&fields={Fields}&maxResults=100";
        return nextPageToken is null ? path : $"{path}&nextPageToken={Uri.EscapeDataString(nextPageToken)}";
    }

    public async Task<IReadOnlyList<WorkItem>> SearchAsync(string jql, CancellationToken cancellationToken = default)
    {
        List<WorkItem> items = [];
        string? nextPageToken = null;
        do
        {
            using HttpResponseMessage response = await http.GetAsync(BuildSearchPath(jql, nextPageToken), cancellationToken);
            response.EnsureSuccessStatusCode();
            await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken);
            using JsonDocument json = await JsonDocument.ParseAsync(body, cancellationToken: cancellationToken);
            JsonElement root = json.RootElement;

            foreach (JsonElement issue in root.GetProperty("issues").EnumerateArray())
            {
                JsonElement fields = issue.GetProperty("fields");
                items.Add(new WorkItem(
                    issue.GetProperty("key").GetString()!,
                    fields.GetProperty("issuetype").GetProperty("name").GetString()!,
                    fields.GetProperty("summary").GetString()!));
            }

            bool isLast = root.TryGetProperty("isLast", out JsonElement last) && last.GetBoolean();
            nextPageToken = !isLast && root.TryGetProperty("nextPageToken", out JsonElement token) ? token.GetString() : null;
        }
        while (nextPageToken is not null);
        return items;
    }
}
using System.Text;

namespace Bookstore.JiraTools;

public sealed record WorkItem(string Key, string Type, string Summary);

public static class ReleaseNotes
{
    private static readonly (string Type, string Heading)[] Sections =
        [("Story", "New"), ("Bug", "Fixed"), ("Task", "Other changes")];

    public static string Render(string title, IReadOnlyList<WorkItem> items)
    {
        StringBuilder text = new();
        text.AppendLine($"# {title}");
        foreach ((string type, string heading) in Sections)
        {
            WorkItem[] section = [.. items.Where(i => i.Type == type).OrderBy(i => i.Key, StringComparer.Ordinal)];
            if (section.Length == 0)
                continue;
            text.AppendLine().AppendLine($"## {heading}");
            foreach (WorkItem item in section)
                text.AppendLine($"- {item.Summary} ({item.Key})");
        }
        int skipped = items.Count(i => !Sections.Any(s => s.Type == i.Type));
        if (skipped > 0)
            text.AppendLine().AppendLine($"Left out (other work types): {skipped}");
        return text.ToString();
    }

    public static readonly IReadOnlyList<WorkItem> SampleItems =
    [
        new("BOOK-142", "Story", "Institutions get 10% off orders of 20 or more copies"),
        new("BOOK-147", "Story", "Approvers receive an e-mail when a purchase needs approval"),
        new("BOOK-151", "Bug", "VAT rounding on the invoice PDF is off by 0.01 TRY"),
        new("BOOK-153", "Bug", "Cart total drops the institutional discount after login"),
        new("BOOK-155", "Task", "Index Orders by InstitutionId"),
        new("BOOK-120", "Epic", "Institutional ordering, release 1"),
    ];
}
static async Task<int> WriteReleaseNotesAsync(string[] options)
{
    const string jql = "project = BOOK AND sprint in openSprints() AND statusCategory = Done "
                     + "AND hierarchyLevel = 0 ORDER BY key ASC";
    const string title = "Bookstore release notes (current sprint)";

    if (options.Contains("--sample"))
    {
        Console.Write(ReleaseNotes.Render(title, ReleaseNotes.SampleItems));
        return 0;
    }

    IConfiguration config = new ConfigurationBuilder()
        .AddUserSecrets<Program>(optional: true)
        .AddEnvironmentVariables()
        .Build();
    string? site = config["Jira:SiteUrl"], email = config["Jira:Email"], token = config["Jira:ApiToken"];
    if (site is null || email is null || token is null)
    {
        Console.Error.WriteLine("Set Jira:SiteUrl, Jira:Email and Jira:ApiToken (user secrets or Jira__* variables).");
        return 2;
    }

    Uri siteUrl = new(site.TrimEnd('/') + "/");
    if (options.Contains("--dry-run"))
    {
        Console.WriteLine($"GET {new Uri(siteUrl, JiraSearchClient.BuildSearchPath(jql)).AbsoluteUri}");
        Console.WriteLine($"Authorization: Basic <{email}:token, hidden>");
        return 0;
    }

    using HttpClient http = JiraSearchClient.CreateHttpClient(siteUrl, email, token);
    IReadOnlyList<WorkItem> items = await new JiraSearchClient(http).SearchAsync(jql);
    Console.Write(ReleaseNotes.Render(title, items));
    return 0;
}

JQL 03. yazıdaki kurallara uyuyor: sprint in openSprints(), bitiş için statusCategory = Done ve subtask'ları dışarıda bırakmak için hierarchyLevel = 0. Token koda ya da appsettings dosyasına yazılmıyor; geliştirme makinesinde user secrets'ta, CI'da Jira__ApiToken gibi bir environment variable'da duruyor. Aşağıdaki değerler açıkça sahte örnekler:

dotnet user-secrets set "Jira:SiteUrl" "https://bookstore.atlassian.net"
dotnet user-secrets set "Jira:Email" "ada@example.com"
dotnet user-secrets set "Jira:ApiToken" "fake-api-token-not-real"
Successfully saved Jira:SiteUrl to the secret store.
Successfully saved Jira:Email to the secret store.
Successfully saved Jira:ApiToken to the secret store.

--dry-run ağa çıkmadan gönderilecek isteği gösteriyor; token ekrana yazılmıyor. --sample ise biçimlendirmeyi kod içindeki örnek item'larla deniyor. İkisinin gerçek çıktısı:

dotnet run -- release-notes --dry-run
GET https://bookstore.atlassian.net/rest/api/3/search/jql?jql=project%20%3D%20BOOK%20AND%20sprint%20in%20openSprints%28%29%20AND%20statusCategory%20%3D%20Done%20AND%20hierarchyLevel%20%3D%200%20ORDER%20BY%20key%20ASC&fields=summary,issuetype&maxResults=100
Authorization: Basic <ada@example.com:token, hidden>
dotnet run -- release-notes --sample
# Bookstore release notes (current sprint)

## New
- Institutions get 10% off orders of 20 or more copies (BOOK-142)
- Approvers receive an e-mail when a purchase needs approval (BOOK-147)

## Fixed
- VAT rounding on the invoice PDF is off by 0.01 TRY (BOOK-151)
- Cart total drops the institutional discount after login (BOOK-153)

## Other changes
- Index Orders by InstitutionId (BOOK-155)

Left out (other work types): 1

Örnek listedeki epic bilerek orada: JQL'deki hierarchyLevel = 0 gerçek aramada epic'leri zaten dışarıda bırakıyor, ama biçimlendirme kodu da bilinmeyen bir work type gelirse onu sessizce yutmak yerine sayıyor. Seçeneksiz dotnet run -- release-notes gerçek siteye bağlanıyor; yanıttaki issues, key, fields.summary ve fields.issuetype.name alanları belgede gösterilen yanıt şemasından alındı.

Üretimde Dikkat

Kurallar bir actor adına çalışıyor ve o actor'ın yetkileriyle: belge, JQL condition'ının kural çalışırken kuralı kuran kişinin değil actor'ın yetkileriyle değerlendirildiğini not ediyor. Actor'ın bir space'te yetkisi yoksa kural orada sessizce değil, audit log'a düşen bir hatayla başarısız oluyor; yeni bir space açıldığında kuralların orada da çalıştığını audit log'dan kontrol et. Zamanlanmış kurallar art arda 10 kez Failure ile biterse kendiliğinden kapanıyor; bunu fark etmenin yolu da audit log'a ve Usage ekranına düzenli bakmak.

REST API tarafında token'ın ömrü bir yıl. Bir entegrasyonu kişisel token ile kurmak, bir yıl sonra ya da kişi şirketten ayrıldığında sessizce durması demek. Atlassian bunun için service account sunuyor: bir kişiye bağlı olmayan, organizasyon admin'inin Atlassian Administration'da oluşturduğu ve kendi credential'ı olan hesaplar. Belgeye göre her organizasyona 5 service account ücretsiz ve Jira'ya erişim verilen service account'lar kullanıcı sayısına dahil edilmiyor. Hakan'ın raporlama script'i, baştaki üçüncü sorunun cevabı olarak, bir service account'un scope'lu token'ına taşınmalı.

Sık Düşülen Tuzaklar

İlk tuzak birbirini tetikleyen kurallar. Allow rule trigger iki kuralda birden açılırsa ve biri diğerinin değiştirdiği alanı geri değiştiriyorsa kurallar loop detection'ın 10 tekrarlık sınırına kadar birbirini tetikliyor ve LOOP ile duruyor; bu sınıra kadar çalışan step'ler de kotadan düşüyor. Allow rule trigger'ı yalnız gerçekten gereken kuralda aç ve o kuralın action'ının kendi trigger'ını yeniden tetikleyip tetiklemediğini düşün.

İkincisi kişiye bağlı entegrasyonlar: kişisel API token, kuralın actor'ı olarak seçilmiş bir çalışan, GitHub uygulamasını kendi hesabıyla bağlamış bir geliştirici. Hepsi o kişi gittiğinde kırılıyor. Üçüncüsü smart commit'lerin sessiz başarısızlığı: commit'teki e-posta adresi Jira'daki adresle eşleşmiyorsa komut çalışmıyor ve fark eden olmuyor. Dördüncüsü fazla geniş trigger'lar: her alan değişikliğinde tetiklenen bir kural hem kotayı hem audit log'u dolduruyor; koşulu trigger'ın içine yazmak hem ucuz hem okunaklı.

Küçük Bir Deneme

★☆☆ Ekibinin son on commit mesajını commit-messages.txt biçiminde bir dosyaya koy ve smart-commit komutunu çalıştır. Kaç mesaj hiçbir work item'a bağlanmıyor?

★★☆ Bookstore'un Kural 4'ünü step açısından yeniden tasarla: condition'ları trigger'a taşı, yorumu yalnız hafta içi at ve ayda kaç step harcayacağını belgedeki sayım kurallarıyla hesapla.

★★★ JiraSearchClient'a, yanıttaki her item için status adını da okuyan bir alan ekle ve release notunda Done olmayan bir item gelirse ayrı bir "Kontrol et" bölümünde göster. Testini ağa çıkmadan, örnek item listesiyle yaz.

Bu yazıyla Jira ve Proje Araçları serisi tamamlanıyor. Dört yazıda Jira'yı bir ekranlar ve alanlar yığını olarak değil, ekibin kararlarını taşıyan bir araç olarak kurduk: model ve space türü, workflow ve board, doğru sorular ve otomasyon. Araç değişebilir, isimler yine değişecek; Scrum, Kanban ve Agile serilerinde konuştuğumuz kararlar ise aynı kalıyor. Bir sonraki adımın Jira'nın kendisi değil, ekibinin bu kararları yazıya döküp ortak bir dille konuşması olsun.