Salve pessoal! Tudo certo?
Neste artigo quero mostrar como automatizar a geração de changelogs e release notes usando o GitHub Copilot, um problema que todo time de software enfrenta e que dá pra resolver de forma bem elegante.
”Mas o que mudou nessa versão?”
Todo time tem aquele momento tenso antes de uma release: alguém precisa escrever o changelog ou ficar respondendo mensagens no chat / email de: “O que mudou nessa versão?”. Obviamente que os usuários, testers, stakeholders e outros não apenas desejam, mas precisam saber o que mudou, para validar, poder usar, mudarem seus workflows se necessário, etc. O resultado costuma ser um dos dois extremos, principalmente vindo de nós, desenvolvedores:
- Muito técnico:
fix: null ref on PaymentService.ProcessAsync- inútil para stakeholders - Muito vago:
Correções e melhorias gerais- inútil para todo mundo
O problema não é falta de informação, os commits têm tudo que é necessário (nem sempre, mas se existe uma política de commits, validação das mensagens, que é possível via linter, deveria ter). O problema é que transformar 40 commits em um texto legível consome tempo que ninguém tem antes de uma release (ou mesmo no dia-a-dia).
Com o GitHub Copilot, dá pra automatizar essa transformação transformando commits brutos em release notes legíveis, publicadas automaticamente no momento da tag ou branch, dependendo do processo que você e seu time utilizam.
Estratégia
Eu poderia automatizar isso utilizando scripts bash, powershell, customizar mais os arquivos de workflow do GitHub Actions, mas resolvi adotar a estratégia de criar um projeto em .NET, que será utilizado para gerar o release notes, onde além de ter mais controle, tenho mais features para poder enriquecer mais o resultado, seja com formatação, envio de email, etc., de forma simples e rápida.
O fluxo utilizando o projeto que vamos construir é simples:
Mão na massa
Vamos construir o projeto passo a passo. Apesar de disponibilizar o código fonte em meu GitHub, estou trazendo cada trecho de código, estrutura de arquivos, tudo para o artigo para facilitar a leitura daqueles que não estão em um computador com o ambiente configurado para isso.
1. Setup do projeto
Vamos criar nosso projeto, sendo um Console App do .NET, com os comandos abaixo, lembrando que estou utilizando o .NET 10 (recomendado), garantindo que está utilizando o .NET 8 ou superior, já irá funcionar.
Criando o projeto:
dotnet new console -n ChangelogGenerator
cd ChangelogGenerator
dotnet add package GitHub.Copilot.SDK
dotnet add package LibGit2Sharp
Estrutura:
ChangelogGenerator/
├── Services/
│ ├── GitLogReader.cs # Leitor de commits e demais informações do git
│ ├── CopilotSummarizer.cs # Envia os dados para o Copilot e recebe o texto com o release notes
│ └── ReleasePublisher.cs # Publica no GitHub Releases via API
├── Models/
│ └── CommitInfo.cs
├── Program.cs
└── ChangelogGenerator.csproj
2. Modelos
Basicamente temos um modelo para armazenar dados dos commits de forma estruturada, incluindo um enumerador para o tipo de commit, considerando algo como em Conventional Commits
// Models/CommitInfo.cs
namespace ChangelogGenerator.Models;
public record CommitInfo(
string Hash,
string ShortHash,
string Message,
string Author,
DateTimeOffset Date,
CommitType Type // classificado a partir do conventional commit
);
public enum CommitType
{
Feature, // feat:
Fix, // fix:
Breaking, // BREAKING CHANGE ou feat!:
Perf, // perf:
Refactor, // refactor:
Docs, // docs:
Chore, // chore:, ci:, build:
Other
}
3. Lendo o git log
O pacote LibGit2Sharp permite ler o histórico sem depender do binário git instalado na máquina de CI, trabalhando diretamente com a base / arquivos contidos no diretório .git do repositório. Recomendo visitarem o repositório e entenderem melhor como funciona, pois pode aprofundar mais seus conhecimentos sobre o funcionamento do Git.
Basicamente validamos os commits de origem e destino, criando um intervalo para analisar as alterações e aplicamos a devida classificação em cada um para poder trabalhar posteriormente a construção da release notes de forma semântica com o GitHub Copilot SDK.
// Services/GitLogReader.cs
using ChangelogGenerator.Models;
using LibGit2Sharp;
namespace ChangelogGenerator.Services;
public class GitLogReader
{
private readonly string _repoPath;
public GitLogReader(string repoPath)
{
_repoPath = repoPath;
}
public List<CommitInfo> GetCommitsBetweenTags(string fromTag, string toTag)
{
using var repo = new Repository(_repoPath);
var from = repo.Tags[fromTag]?.Target as Commit
?? repo.Tags[fromTag]?.PeeledTarget as Commit;
var to = repo.Tags[toTag]?.Target as Commit
?? repo.Tags[toTag]?.PeeledTarget as Commit;
if (from is null)
throw new ArgumentException($"Tag '{fromTag}' não encontrada no repositório.");
if (to is null)
throw new ArgumentException($"Tag '{toTag}' não encontrada no repositório.");
var filter = new CommitFilter
{
IncludeReachableFrom = to,
ExcludeReachableFrom = from,
SortBy = CommitSortStrategies.Time
};
return repo.Commits
.QueryBy(filter)
.Select(c => new CommitInfo(
Hash: c.Sha,
ShortHash: c.Sha[..7],
Message: c.MessageShort.Trim(),
Author: c.Author.Name,
Date: c.Author.When,
Type: ClassifyCommit(c.MessageShort)
))
.ToList();
}
public List<CommitInfo> GetAllCommitsUpToTag(string tag)
{
using var repo = new Repository(_repoPath);
var to = repo.Tags[tag]?.Target as Commit
?? repo.Tags[tag]?.PeeledTarget as Commit;
if (to is null)
throw new ArgumentException($"Tag '{tag}' não encontrada no repositório.");
var filter = new CommitFilter
{
IncludeReachableFrom = to,
SortBy = CommitSortStrategies.Time
};
return repo.Commits
.QueryBy(filter)
.Select(c => new CommitInfo(
Hash: c.Sha,
ShortHash: c.Sha[..7],
Message: c.MessageShort.Trim(),
Author: c.Author.Name,
Date: c.Author.When,
Type: ClassifyCommit(c.MessageShort)
))
.ToList();
}
public string? GetPreviousTag(string currentTag)
{
using var repo = new Repository(_repoPath);
var tags = repo.Tags
.Where(t => t.FriendlyName != currentTag)
.Select(t => t.FriendlyName)
.Where(name => System.Version.TryParse(name.TrimStart('v'), out _))
.OrderByDescending(name => System.Version.Parse(name.TrimStart('v')))
.ToList();
return tags.FirstOrDefault();
}
private static CommitType ClassifyCommit(string message)
{
var lower = message.ToLowerInvariant();
if (lower.StartsWith("feat!") || lower.Contains("breaking change"))
return CommitType.Breaking;
if (lower.StartsWith("feat"))
return CommitType.Feature;
if (lower.StartsWith("fix"))
return CommitType.Fix;
if (lower.StartsWith("perf"))
return CommitType.Perf;
if (lower.StartsWith("refactor"))
return CommitType.Refactor;
if (lower.StartsWith("docs"))
return CommitType.Docs;
if (lower.StartsWith("chore") || lower.StartsWith("ci") || lower.StartsWith("build"))
return CommitType.Chore;
return CommitType.Other;
}
}
4. Sumarizando com o Copilot
Aqui está o núcleo da automação. A chave é um prompt bem estruturado que instrui o modelo a organizar os commits em seções legíveis.
Aqui estamos utilizando o GitHub Copilot SDK (GitHub.Copilot.SDK). O SDK se comunica com o Copilot CLI via JSON-RPC, gerenciando autenticação e ciclo de vida da sessão automaticamente — sem necessidade de montar requests HTTP manualmente.
// Services/CopilotSummarizer.cs
using System.Text;
using ChangelogGenerator.Models;
using GitHub.Copilot.SDK;
namespace ChangelogGenerator.Services;
public class CopilotSummarizer : IAsyncDisposable
{
private readonly CopilotClient _client;
public CopilotSummarizer(string githubToken)
{
_client = new CopilotClient(new CopilotClientOptions
{
GitHubToken = githubToken
});
}
public async Task StartAsync() => await _client.StartAsync();
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
await _client.DisposeAsync();
}
public async Task<string> GenerateReleaseNotesAsync(
string version,
string productName,
List<CommitInfo> commits,
CancellationToken ct = default)
{
// Para repositórios grandes, sumariza em dois passos
if (commits.Count > 80)
return await SummarizeInChunksAsync(version, productName, commits, ct);
var commitList = FormatCommitsForPrompt(commits);
return await CallCopilotAsync(version, productName, commitList, ct);
}
private static string FormatCommitsForPrompt(List<CommitInfo> commits)
{
var sb = new StringBuilder();
foreach (var commit in commits)
{
var typeLabel = commit.Type switch
{
CommitType.Breaking => "[BREAKING]",
CommitType.Feature => "[feat]",
CommitType.Fix => "[fix]",
CommitType.Perf => "[perf]",
CommitType.Refactor => "[refactor]",
CommitType.Docs => "[docs]",
CommitType.Chore => "[chore]",
_ => "[other]"
};
sb.AppendLine($"- {typeLabel} {commit.Message} ({commit.ShortHash})");
}
return sb.ToString();
}
private async Task<string> SummarizeInChunksAsync(
string version,
string productName,
List<CommitInfo> commits,
CancellationToken ct)
{
const int chunkSize = 60;
var chunks = commits.Chunk(chunkSize).ToList();
var chunkSummaries = new List<string>();
Console.WriteLine($"📦 {commits.Count} commits - processando em {chunks.Count} blocos...");
foreach (var (chunk, index) in chunks.Select((c, i) => (c, i)))
{
Console.WriteLine($" → Bloco {index + 1}/{chunks.Count}");
var commitList = FormatCommitsForPrompt(chunk.ToList());
var chunkSummary = await CallCopilotAsync(
$"{version} (parte {index + 1})", productName, commitList, ct);
chunkSummaries.Add(chunkSummary);
}
// Segunda passagem: consolida os resumos parciais
Console.WriteLine(" → Consolidando resumos...");
var combinedSummaries = string.Join("\n\n---\n\n", chunkSummaries);
return await ConsolidateSummariesAsync(version, productName, combinedSummaries, ct);
}
private async Task<string> CallCopilotAsync(
string version,
string productName,
string commitList,
CancellationToken ct)
{
await using var session = await _client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-4o",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Replace,
Content = $"""
Você é um technical writer especializado em release notes para produtos de software.
Seu objetivo é transformar uma lista de commits git em release notes claras e profissionais.
Produto: {productName}
Diretrizes:
- Agrupe as mudanças em seções: 🚨 Breaking Changes, ✨ Novidades, 🐛 Correções, ⚡ Performance, 🔧 Melhorias Internas
- Omita a seção se não houver itens
- Transforme mensagens técnicas em linguagem clara, mas mantenha os detalhes relevantes
- Mencione o hash do commit entre parênteses ao final de cada item
- Use bullet points
- NÃO invente funcionalidades que não estão nos commits
- Retorne SOMENTE o markdown das release notes, sem texto adicional
"""
}
});
var result = new TaskCompletionSource<string>();
string? responseText = null;
session.On(evt =>
{
switch (evt)
{
case AssistantMessageEvent msg:
responseText = msg.Data.Content;
break;
case SessionIdleEvent:
result.TrySetResult(responseText ?? string.Empty);
break;
case SessionErrorEvent err:
result.TrySetException(new Exception(err.Data.Message));
break;
}
});
await session.SendAsync(new MessageOptions
{
Prompt = $"""
Gere as release notes para a versão {version}.
Commits:
{commitList}
"""
}, ct);
return await result.Task.WaitAsync(ct);
}
private async Task<string> ConsolidateSummariesAsync(
string version,
string productName,
string partialSummaries,
CancellationToken ct)
{
await using var session = await _client.CreateSessionAsync(new SessionConfig
{
OnPermissionRequest = PermissionHandler.ApproveAll,
Model = "gpt-4o",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Replace,
Content = $"Você é um technical writer. Consolide os resumos parciais em uma única release note coesa para o produto {productName}, versão {version}. Elimine duplicatas e mantenha o formato markdown com as seções padrão."
}
}, ct);
var result = new TaskCompletionSource<string>();
string? responseText = null;
session.On(evt =>
{
switch (evt)
{
case AssistantMessageEvent msg:
responseText = msg.Data.Content;
break;
case SessionIdleEvent:
result.TrySetResult(responseText ?? string.Empty);
break;
case SessionErrorEvent err:
result.TrySetException(new Exception(err.Data.Message));
break;
}
});
await session.SendAsync(new MessageOptions { Prompt = partialSummaries }, ct);
return await result.Task.WaitAsync(ct);
}
}
5. Publicando no GitHub Releases
// Services/ReleasePublisher.cs
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace ChangelogGenerator.Services;
public class ReleasePublisher
{
private readonly HttpClient _http;
private readonly string _owner;
private readonly string _repo;
public ReleasePublisher(string githubToken, string owner, string repo)
{
_owner = owner;
_repo = repo;
_http = new HttpClient
{
BaseAddress = new Uri("https://api.github.com/")
};
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", githubToken);
_http.DefaultRequestHeaders.UserAgent.ParseAdd("changelog-generator/1.0");
_http.DefaultRequestHeaders.Accept
.ParseAdd("application/vnd.github+json");
}
public async Task PublishAsync(
string tagName,
string releaseName,
string body,
bool isPrerelease = false,
CancellationToken ct = default)
{
var payload = new
{
tag_name = tagName,
name = releaseName,
body = body,
draft = false,
prerelease = isPrerelease
};
var json = JsonSerializer.Serialize(payload);
// Verifica se já existe um release para essa tag
var existingId = await GetExistingReleaseIdAsync(tagName, ct);
HttpResponseMessage response;
if (existingId is not null)
{
Console.WriteLine($"⚠️ Release já existe (id={existingId}), atualizando...");
var content = new StringContent(json, Encoding.UTF8, "application/json");
response = await _http.PatchAsync(
$"repos/{_owner}/{_repo}/releases/{existingId}", content, ct);
}
else
{
var content = new StringContent(json, Encoding.UTF8, "application/json");
response = await _http.PostAsync(
$"repos/{_owner}/{_repo}/releases", content, ct);
}
response.EnsureSuccessStatusCode();
Console.WriteLine($"✅ Release publicada: https://github.com/{_owner}/{_repo}/releases/tag/{tagName}");
}
private async Task<long?> GetExistingReleaseIdAsync(string tagName, CancellationToken ct)
{
var response = await _http.GetAsync(
$"repos/{_owner}/{_repo}/releases/tags/{tagName}", ct);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct));
return doc.RootElement.GetProperty("id").GetInt64();
}
}
6. Program.cs
// Program.cs
using ChangelogGenerator.Services;
// Variáveis de ambiente (injetadas pela pipeline)
// COPILOT_TOKEN: PAT com acesso ao GitHub Copilot (necessário para o SDK)
// GITHUB_TOKEN: token do Actions, usado apenas para publicar o release
var copilotToken = Environment.GetEnvironmentVariable("COPILOT_TOKEN")
?? Environment.GetEnvironmentVariable("GITHUB_TOKEN")!; // fallback local
var githubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN")!;
var repoOwner = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY_OWNER")!;
var repoName = Environment.GetEnvironmentVariable("GITHUB_REPOSITORY")!.Split('/').Last();
var currentTag = Environment.GetEnvironmentVariable("RELEASE_TAG")!; // ex: v2.5.0
var productName = Environment.GetEnvironmentVariable("PRODUCT_NAME") ?? repoName;
var repoPath = Environment.GetEnvironmentVariable("GITHUB_WORKSPACE") ?? ".";
Console.WriteLine($"🚀 Gerando release notes para {productName} {currentTag}");
var gitReader = new GitLogReader(repoPath);
await using var summarizer = new CopilotSummarizer(copilotToken);
await summarizer.StartAsync(); // inicia o processo do Copilot CLI
var publisher = new ReleasePublisher(githubToken, repoOwner, repoName);
// Encontra a tag anterior automaticamente
var previousTag = gitReader.GetPreviousTag(currentTag);
List<ChangelogGenerator.Models.CommitInfo> commits;
if (previousTag is null)
{
Console.WriteLine("⚠️ Nenhuma tag anterior encontrada. Primeira release — coletando todos os commits.");
commits = gitReader.GetAllCommitsUpToTag(currentTag);
}
else
{
Console.WriteLine($"📋 Coletando commits entre {previousTag} e {currentTag}...");
commits = gitReader.GetCommitsBetweenTags(previousTag, currentTag);
}
Console.WriteLine($" {commits.Count} commit(s) encontrado(s)");
if (commits.Count == 0)
{
Console.WriteLine("⚠️ Nenhum commit entre as tags. Pulando geração.");
return 0;
}
// Detecta se é pre-release (sufixo -alpha, -beta, -rc)
var isPrerelease = currentTag.Contains('-');
Console.WriteLine("✍️ Gerando release notes com Copilot...");
var releaseNotes = await summarizer.GenerateReleaseNotesAsync(
version: currentTag,
productName: productName,
commits: commits);
Console.WriteLine("\n--- Preview das release notes ---");
Console.WriteLine(releaseNotes);
Console.WriteLine("---\n");
Console.WriteLine("📤 Publicando no GitHub Releases...");
await publisher.PublishAsync(
tagName: currentTag,
releaseName: $"{productName} {currentTag}",
body: releaseNotes,
isPrerelease: isPrerelease);
return 0;
Pipeline no GitHub Actions
# .github/workflows/release.yml
name: Release Notes
on:
push:
tags:
- 'v*.*.*'
permissions:
contents: write # necessário para criar releases
jobs:
generate-release-notes:
runs-on: ubuntu-latest
steps:
- name: Checkout (histórico completo para o git log)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.x'
- name: Instalar Copilot CLI
run: gh extension install github/gh-copilot || true
env:
GH_TOKEN: ${{ secrets.COPILOT_TOKEN }}
- name: Build ChangelogGenerator
run: dotnet publish -c Release -o ./changelog-tool
- name: Gerar e publicar release notes
run: dotnet ./changelog-tool/ChangelogGenerator.dll
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # usado para publicar o release
COPILOT_TOKEN: ${{ secrets.COPILOT_TOKEN }} # PAT com acesso ao Copilot
RELEASE_TAG: ${{ github.ref_name }}
PRODUCT_NAME: "Gerador de Release Notes"
GITHUB_WORKSPACE: ${{ github.workspace }}
Existe uma configuração que precisa ser feita no GitHub, pois o GITHUB_TOKEN fornecido automaticamente pelo runtime do GitHub Actions não tem acesso a API do GitHub Copilot. Portanto, precisaremos criar um Access Token com acesso a ela e adicionar na secret COPILOT_TOKEN.
Para isso, faça esses passos:
-
Acesse o menu do seu perfil, no canto direito superior do GitHub (onde tem sua foto de perfil) e clique em Settings.
-
Em Settings, vá na opção do menu lateral esquerdo Developer Settings.
-
Depois vá no menu lateral esquerdo Personal Access Tokens → Fine-grained tokens. Clique no botão Generate new token.
-
Preencha o token com um nome, como
ReleaseNotesToken, configure a data de expiração dele. -
Em Repository Access, eu recomendo selecionar Only selected repositories para controlar melhor o acesso do token. Após isso, selecione o repositório da demonstração que você criou ou fez fork.
-
Em Permissions, clique na aba Account e em seguida Add permissions e selecione as permissões:
- Copilot Chat
- Copilot Requests
-
Clique em Generate token e guarde o token gerado em algum lugar seguro. Se você sair dessa tela e não tiver copiado ele, terá que gerar um novo token depois.
Agora no repositório da demonstração, acesse Settings → Secrets and variables → Actions.
Clique em New repository secret, dê o nome de COPILOT_TOKEN e cole o token gerado no campo Secret. Clique em Add secret.
Pronto, as configurações necessárias para utilizar o SDK do GitHub Copilot foram feitas.
Como testar?
Notem que vamos fazer os testes gerando release notes da nossa própria ferramenta. Em um próximo artigo irei detalhar melhor como estou utilizando essa técnica mantendo o gerador de release notes em um repositório mas utilizando ele no workflow de release de outros repositórios.
No projeto de demonstração, disponível em https://github.com/gustavobigardi/demo-releaes-notes-generator, após você fazer um fork dele para seu repositório, você deve gerar uma nova tag:
-
Clique em Tags.
-
Clique em Create a new release.
-
No release, vá em Choose a tag e clique em Create new tag.
-
Utilize uma versão como v1.0.0, v1.1.1, etc., seguindo o padrão que foi definido na trigger do workflow.yaml
-
Preencha o Release title com a mesma versão da tag e clique em Publish release.
-
Após publicar a Release, vá em Actions e note que nosso workflow foi inicializado. Aguarde a execução dele, inclusive, verifique os logs, veja e investigue o que está sendo executado.
-
Após a execução do Workflow, volte para a parte de código, clique em Tags, na aba Releases ou diretamente na release que aparece normalmente ao lado direito da lista de arquivos do repositório. Note que a Release foi atualizada com as notas de release geradas pela ferramenta, incluindo o título da Release.
⚠️ Aviso:
O GitHub já possui um botão de gerar release notes automático que faz algo semelhante. O propósito dessa ferramenta é permitir maior flexibilidade na produção de release notes, customizando os prompts, forma de construir, formato e inclusive utilizar a ferramenta em outros ferramentas de gestão de código como o Azure DevOps, obviamente, com as devidas modificações que serão necessárias.
Resultado: antes vs depois
Antes (commits brutos)
fix: update release publishing logic to handle existing releases
fix: update environment variable for Copilot CLI installation and usage
chore: remove launch.json from git tracking (contains secrets)
fix: approve all permission requests in session configuration
fix: update .NET version to 10 and improve commit retrieval logic
fix(ci): ignore error when gh copilot is already a built-in command
fix: Corrigindo variável faltando no isntall do Copilot CLI
fix: Corrigindo pipeline incorreta faltando aspas
feat: Adicionando documentação do projeto via markdown em README.md
chore: Adding pipeline to build it on github
feat: Initializing the repository with the structure of the ChangeLog Generator, including the review of release notes using the Github copilot SDK
Depois (release notes geradas)
# Release Notes — v1.0.0 _12 de março de 2026_ --- ## ✨ Novidades - **Lançamento inicial do Gerador de Release > Notes**: estrutura base do projeto criada com > suporte à revisão de release notes via GitHub > Copilot SDK (`356bf7b`) - **Documentação do projeto**: adicionado `README.> md` com documentação completa do projeto em > formato Markdown (`23f77bd`) --- ## 🐛 Correções - Corrigida a lógica de publicação de releases > para tratar corretamente cenários onde a release > já existe (`7babba6`) - Corrigida a variável de ambiente utilizada na > instalação e no uso da Copilot CLI (`a9b61d1`) - Corrigida a configuração de sessão para aprovar > automaticamente todas as solicitações de > permissão necessárias (`aa37da4`) - Atualizada a versão do .NET para 10 e > aprimorada a lógica de recuperação de commits > (`54a1efe`) - Pipeline de CI ajustada para ignorar erro > quando o `gh copilot` já está disponível como > comando nativo (`8d0f179`) - Corrigida variável ausente no script de > instalação da Copilot CLI (`6d3c809`) - Corrigida pipeline com aspas faltando que > causava erro na execução (`ab0c4b0`) --- ## 🔧 Melhorias Internas - Adicionada pipeline de build no GitHub Actions > (`3c218b5`) - Removido `launch.json` do rastreamento do Git > por conter informações sensíveis (`07cc295`)
Repositório da demonstração:
O projeto da demonstração está disponível no Github, em https://github.com/gustavobigardi/demo-releaes-notes-generator
Como se aprofundar mais
O que construímos aqui é a base, trazendo um simples exemplo de como usar o Copilot para automatização de tarefas, mas o SDK oferece muito mais. Algumas direções para evoluir o projeto:
Melhorar a qualidade das release notes
O SystemMessageMode.Replace dá controle total sobre o comportamento do modelo. Alguns ajustes que fazem diferença:
- Seja específico sobre o que não quer: instruir explicitamente o modelo a não inventar funcionalidades é mais eficaz do que deixar implícito — o modelo às vezes “infere” comportamentos a partir de commits de refactor ou chore
- Inclua contexto do produto no system prompt: quanto mais o modelo souber sobre o domínio (e-commerce, SaaS B2B, API pública…), mais precisa será a linguagem das notas
- Pedir markdown puro no system prompt: evita o modelo incluir texto introdutório antes do markdown, que quebraria a publicação automática
Explorar o que o SDK oferece além do chat simples
O SDK não é só uma abstração de HTTP — ele expõe o mesmo runtime do Copilot CLI:
- Ferramentas customizadas (
Tools): você pode registrar funções C# que o modelo invoca durante a sessão usandoAIFunctionFactory.Create. Por exemplo, uma ferramentalookup_issueque busca detalhes de uma issue no Jira ou Azure DevOps para enriquecer a release note com contexto de negócio - Hooks de sessão (
SessionHooks):OnPreToolUseeOnPostToolUsepermitem auditar ou modificar o que o modelo faz, útil para logging em pipelines de CI - Streaming (
Streaming = true): para dar feedback em tempo real no console enquanto a release note é gerada, usando o eventoAssistantMessageDeltaEvent - Múltiplos modelos: o
SessionConfig.Modelaceita qualquer modelo disponível no Copilot CLI (gpt-4o,claude-sonnet-4.5, etc.), o que permite comparar resultados ou usar modelos mais baratos para os chunks intermediários
Ir além das release notes
Com o mesmo projeto, dá para ir mais longe sem muito esforço:
- Resumo de sprint: passar commits de um período (em vez de entre tags) e gerar um report de progresso para o time
- Draft de post de blog: pedir ao modelo para transformar as notas técnicas em um texto mais narrativo para o site do produto
- Notificação automática: com o
ReleasePublishercomo base, adicionar envio para Slack ou Teams usando a release note como corpo da mensagem
Espero que tenham gostado do conteúdo, deixem seus comentários e dúvidas, e nos vemos no próximo artigo!