Do Zero à Produção - Gere tudo com um único botão
Do Zero à Produção - Gere tudo com um único botão
Como engenheiros de software, tentamos automatizar o máximo possível. Essa é a razão pela qual construímos nossa plataforma de integração contínua e implantação contínua. Mostrarei como você pode utilizar GitHub Actions, GitHub Pages, DocFx e, claro, .NET para criar seu próprio pipeline que, com um clique, faz o seguinte:
- Execute testes e construa seu aplicativo
- Solte o aplicativo, por exemplo, para nuget
- Criar uma versão no GitHub com notas de versão
- Atualize a documentação utilizando GitHub Pages e DocFx
Então vamos passo a passo. No final vou postar o link para o repositório se você quiser apenas usar isso. Você verá como configurar o repositório, como gerar a documentação automaticamente e como podemos liberar tudo com um único botão sob demanda.
Crie o Repositório
O primeiro passo não é novo para a maioria de nós. De qualquer forma, isso tem que ser feito, então vamos para o GitHub e criar um novo repositório.
Eu usei o .gitignore padrão mais o README.md padrão. Como licença eu escolho o MIT como sempre escolho isso. Todos os meus repositórios públicos são gratuitos para uso em projetos privados e empresariais. Mas sinta-se à vontade para usar a licença que melhor lhe convier. Agora apenas clonamos o repositório para nosso PC local. Eu sou um usuário pesado do git cli, portanto, você verá muitos exemplos de linha de comando do git em vez de GUI. Por favor, pegue a ferramenta com a qual você se sente mais confortável.
git clone https://github.com/linkdotnet/deployment-template.git e lá você tem seu novo repositório.
A estrutura de pastas
Agora antes de começar a encher o repositório com live eu quero mostrar a estrutura de pastas que vou usar ao longo deste post. Sinta-se à vontade para adaptá-lo às suas necessidades. Primeiro vou criar 4 novas pastas no diretório raiz
- .github onde as ações do GitHub ficarão
- docs onde nossa documentação ficará (feita por DocFx)
- src onde nosso código de produção ficará
- testes onde nosso código de teste ficará
Nossa biblioteca e projeto de teste
Agora é hora de criar uma nova solução, incluindo um projeto de biblioteca e um projeto de teste. Vou utilizar a linha de comando para isso. Sinta-se à vontade para usar seu IDE favorito.
No diretório raiz, usarei os seguintes comandos:
- dotnet new sln - Cria nossa solução. Se você não fornecer nenhum argumento, a solução terá o mesmo nome do diretório em que foi criada. Com -n você pode passar o nome da solução assim: dotnew new sln -n MyFancySolution .
- dotnet new classlib --output src/MyLibrary irá gerar nossa biblioteca de classes chamada "MyLibrary" no diretório src.
- dotnet sln add src/MyLibrary/ adicionará o projeto recém-adicionado à nossa solução.
- dotnet new xunit --output tests/MyLibraryTests criará nosso projeto de teste xUnit em tests/MyLibraryTests. Você também pode usar o NUnit ou qualquer outro framework de teste.
- dotnet sln add tests/MyLibraryTests adicionará o projeto de teste à nossa solução.
Primeiro passo feito! Você pode confirmar seu trabalho e enviá-lo para o GitHub.
Opcional - StyleCop e SonarAnalyzers
Esta parte é opcional e você não precisa usá-la, mas eu uso os dois pacotes em quase todos os projetos que uso. Os analisadores StyleCop, bem como os analisadores SonarAnalyters.CSharp, verificam seu código e avisam se houver algum problema em potencial. Mas, além disso, eles também podem impor alguns estilos de codificação, o que é muito bom. Outra grande vantagem é que você pode configurá-los para gerar um erro se uma API pública não estiver documentada. Eu recomendo fortemente que eles os usem, veremos mais tarde por que o último ponto é muito importante.
Esses dois pacotes são um pouco especiais para mim: eu os quero literalmente em todos os projetos, até os adiciono aos meus projetos de teste. Portanto, uso o arquivo Directory.Build.props para que todos os meus arquivos csproj instalem automaticamente esses dois pacotes.
Então eu vou adicionar esses arquivos. Se você quiser, vá para o repositório e copie e cole esses 3 arquivos:
- Directory.Build.props
- stylecop.analyzers.ruleset
- stylecop.json
A predefinição que eu uso é uma boa mistura. Sinta-se à vontade para adotar as regras.
A biblioteca
Até agora nossa biblioteca está bem vazia e não temos testes. Não tão grande. Então vamos criar uma nova classe de exemplo que tem uma função pública. Vou usar a seguinte função:
namespace MyLibrary;
/// <summary>
/// Can print "Hello World" to the user.
/// </summary>
public class HelloWorldPrinter
{
/// <summary>
/// Returns "Hello World" <paramref name="times"/> on the Console.
/// </summary>
/// <param name="times">How many times Hello World should be displayed.</param>
/// <returns>"Hello World" multiple times delimited by a newline.</returns>
public string GetHellWorld(int times)
{
var hello = Enumerable.Range(0, times).Select(_ => new string("Hello World"));
return string.Join(Environment.NewLine, hello);
}
}
Como você vê, criei um novo HelloWorldPrinter que apenas retorna "Hello World" n vezes. Mas também importante temos uma documentação. Isso será muito importante em alguns passos. Eu o encorajaria a documentar seu código (API pública em sua biblioteca, não tudo!).
Adicione também um pequeno teste:
using MyLibrary;
using Xunit;
namespace MyLibraryTests;
public class UnitTest1
{
[Fact]
public void ShouldReturnHelloWorldThreeTimes()
{
const string expected = @"Hello World
Hello World
Hello World";
var sut = new HelloWorldPrinter();
var str = sut.GetHellWorld(3);
Assert.Equal(expected, str);
}
}
Vamos cometer isso e seguir em frente.
A primeira ação: verifique a compilação em commits
Agora temos nosso primeiro teste com a primeira lógica. De agora em diante, queremos construir e testar nosso aplicativo com cada commit. Para isso vamos criar uma ação no GitHub. Dentro da pasta .github, crie uma nova subpasta chamada workflows. E aí criamos nossa primeira ação: dotnet.yml. Você pode nomeá-lo como desejar. Não vou entrar muito em detalhes sobre como as ações funcionam. Você precisa de uma pequena compreensão de como eles funcionam, mas não se preocupe. Aqui e aqui para iniciantes pode ajudá-lo. Achei-os bastante auto-explicativos:
name: .NET
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
include-prerelease: true
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore -c Release
- name: Test
run: dotnet test -c Release --no-build
O que ele faz: em cada solicitação push para main ou pull que segmenta main, execute as seguintes etapas:
- Faça check-out do nosso repositório para o GitHub Action Agent
- Restaure todas as nossas dependências
- Construir nosso aplicativo
- E faça os testes
Vamos confirmar isso e ir para a guia Ações em seu repositório.
E toda vez que a compilação falhar, um dos analisadores vir um problema ou um de nossos testes falhar, você receberá uma indicação (e até um e-mail, se configurado). Você pode criar até mesmo um crachá de status que você pode colocar em seu README.md. Parece algo como isto: .NET.
CHANGELOG. md
Esta é a primeira parte da documentação. Quando lançamos, não queremos escrever notas de lançamento, queremos que elas sejam geradas automaticamente e colocadas no GitHub. Para isso usaremos um arquivo chamado CHANGELOG.md que colocaremos na raiz do repositório. Esse não é um arquivo especial você pode nomeá-lo como quiser, nós o tornamos especial depois ??.
A estrutura desse arquivo é por convenção. A ideia é do keepachangelog. Quando você adiciona um novo recurso ou uma correção, nós o colocamos na seção Não lançados. Assim que criamos uma versão, aproveitamos essas informações.
CHANGELOG.md
# Changelog
A nice description could be here.
<!-- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) -->
## [Unreleased]
### Added
- `HelloWorldPrinter` which creates the user
### Fixed
- Made the world a bit better!
A ideia é simples, uma vez que lançamos, transformamos a parte não lançada na versão atual e a marcamos como tal. Portanto, o release deve ter o seguinte conteúdo:
### Added
- `HelloWorldPrinter` which creates the user
### Fixed
- Made the world a bit better!
E nosso arquivo CHANGELOG.md deve ficar assim depois
## [Unreleased]
## [1.0.0] - 2022-04-10
### Added
- `HelloWorldPrinter` which creates the user
### Fixed
- Made the world a bit better!
Como isso é feito eu vou mostrar mais tarde.
DocFx - Documente nossa aplicação e API
DocFx é um gerador de documentos estáticos. No nosso caso, levará nossos projetos C# e algumas remarcações e criará um site. Condições perfeitas para a nossa página GitHub gratuita, onde podemos hospedar exatamente esses conteúdos.
Antes de criarmos qualquer documentação, precisamos instalar o DocFx. Existem várias maneiras. Eu gosto de instalá-lo via chocolate, mas existem opções diferentes. Vou apenas vincular o guia de início rápido se você precisar de uma configuração diferente. Com chocolatey, que é um gerenciador de pacotes para Windows, você pode simplesmente instalá-lo através do powershell com direitos elevados: choco install docfx. Reinicie seu console e você poderá inserir o seguinte comando em seu shell: docfx --version e deverá obter a versão atual do DocFx. Uma vez que temos isso, podemos configurar nosso projeto.
Na pasta doc podemos criar docfx com o seguinte comando:
docfx init -q -o site --apiSourceFolder ../../src/
- docfx init criará o projeto.
- -q significa bastante, então não temos muita saída do console, você pode omitir este parâmetro se desejar.
- -o site esta será a saída. Nesse caso, isso significaria que todo o conteúdo do documento está em docs/site. -_ --apiSourceFolder ../../src/_ damos ao docfx uma dica de onde está nosso src. Isso é relativo ao arquivo docfx.json recém-criado que fica em docs/site.
Agora você pode entrar na nova pasta do site e fazer uma pequena alteração no docfx.json. Altere a primeira parte de "arquivos" de "src/.csproj" para apenas ".csproj". A razão é simples, você pode, em teoria, hospedar todo o seu código dentro da pasta src que foi criada pelo docfx. Eu pessoalmente não sou um grande fã disso, mas isso é com você.
{
"metadata": [
{
"src": [
{
"files": [
"**.csproj"
],
"src": "../../src/"
}
],
Agora estamos prontos e podemos dar uma olhada em nossa documentação. Para isso, siga novamente um diretório até docs e execute o seguinte comando: docfx site/docfx.json. Isso compilará todos os arquivos e gerará a documentação da API. E depois chame _docfx serve site/site. Isso servirá a página da Web em localhost:8080. Deve ser algo assim.
Na parte superior você também tem a Documentação da API que deve ser criada automaticamente. Agora, espero que tenha feito sentido para você aplicarmos a documentação pública da API. Por conveniência, eu adicionaria um script cmd ou bash que faz esses dois comandos juntos na pasta docs:
docfx site/docfx.json
docfx serve site/_site
Não entrarei em detalhes sobre como escrever a documentação, mas fornecerei este link onde você poderá obter mais informações. Agora é o momento perfeito para comprometer nosso trabalho e continuar a jornada. Faça uma pausa se desejar ??.
Criar a página do GitHub
Agora que temos a documentação em vigor, podemos pensar onde e como podemos implantar isso. Na melhor das hipóteses, podemos fazer isso sob demanda ou sempre que tivermos um lançamento. Nós não necessariamente queremos isso em cada commit porque então mostraríamos ao usuário informações novas ou alteradas que não fazem sentido para ele neste momento.
Para começar, queremos criar 2 novas ramificações. Um chamamos de estável e outro chamamos de páginas gh. Este último é bastante óbvio. Usamos esse branch para servir nossa documentação. estável será nosso espelho da versão mais recente ou da versão mais recente com documentação atualizada. Agora, por que temos dois ramos? Um principal e um estável. Imagine que você só quer alterar sua documentação sem enviar conteúdo real ou liberar algo, isso seria difícil com um branch. Portanto, temos estável. Se queremos apenas atualizar a documentação, mesclamos as alterações em estável (stable) e principal (main) (também podemos automatizar isso).
Depois de criarmos nossas ramificações, vamos para a página de configurações da seção GitHub páginas. Lá selecione como fonte (Source) o branch gh-páginas. Você notará que imediatamente uma ação é iniciada.
Depois, você pode clicar no link oferecido a você acima da Fonte que você alterou alguns segundos atrás. Agora isso não parece tão ruim, mas não é o conteúdo que queremos ter.
A parte que falta é a seguinte: Toda vez que alguém ou alguma coisa empurra para estável nós queremos recompilar nossa documentação. E fazemos o mesmo que fizemos localmente anteriormente. Então, vamos para a pasta github/workflow e adicione um novo arquivo yml. Vou chamá-lo de docs.yml. A ação irá:
- Confira nosso repositório
- Baixar docx
- Executar docfx
- Publique as alterações no branch gh-pages
name: Docs
on:
push:
branches:
- stable
workflow_dispatch:
jobs:
generate-docs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET 6.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Setup DocFX
uses: crazy-max/ghaction-chocolatey@v1
with:
args: install docfx
- name: DocFX Build
working-directory: docs
run: docfx site\docfx.json
continue-on-error: false
- name: Publish
if: github.event_name == 'push'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: docs/site/_site
force_orphan: true
Agora em relação aos segredos.GITHUB_TOKEN: Isso é predefinido e você não precisa definir isso. Vamos aprender/usar segredos "reais" daqui a pouco. Certifique-se de que tudo o que você faz está no branch principal. Mal vamos interagir com o branch estável. Ao enviar essas alterações, você pode acessar as ações do Github e selecionar nosso fluxo de trabalho do Documentos recém-definido por meio do fluxo de trabalho Executar. Depois de um tempo, você pode verificar novamente sua página do GitHub e pronto, você verá uma página familiar. O do DocFx e nossa API.
Viva, nós fizemos isso. Nós chegamos muito longe agora. Muitos processos são automatizados, mas podemos fazer mais!
Mesclar automaticamente o estável para o principal
O próximo fluxo de trabalho fará o backmerge automaticamente de quaisquer alterações de estável para principal. Se a mesclagem não for bem-sucedida, criaremos um novo problema no mesmo repositório para que o autor esteja ciente do problema e tenha que resolver o conflito manualmente.
Crie um novo arquivo de fluxo de trabalho (em .github/workflows) chamado backmerge.yml com o seguinte conteúdo:
name: Back merge to main
on:
push:
branches:
- stable
workflow_dispatch:
jobs:
back-merge:
if: github.event.pull_request.merged == true
timeout-minutes: 2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set Git config
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Merge master back to dev
run: |
git fetch --unshallow
git checkout main
git pull
git merge --no-ff origin/stable -m "Auto-merge stable back to main"
git push
- name: Create issue if failed
if: failure()
uses: JasonEtco/create-an-issue@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
filename: .github/backmerge-failed.md
update_existing: true
A última parte é um pouco interessante. Se falharmos, criamos um novo problema. Como template eu defini um arquivo em .github/backmerge-failed.md. Este será o conteúdo do problema criado pela ação do GitHub. Eu uso um conteúdo muito simples como este:
---
title: Back merge from stable to main failed
labels: input needed
---
The back merge from stable to main failed. Please investigate the GitHub Action and resolve the conflict manually.
O título será usado como título da edição e também poderá atribuir etiquetas automaticamente.
Defina algumas informações básicas do NuGet em nosso csproj
Estamos tão perto. Fique comigo, estamos quase na linha de chegada. A próxima coisa importante é que precisamos de um ID exclusivo para nossa biblioteca ou aplicativo. Você pode fazer isso via Visual Studio ou editar diretamente seu csproj. Eu uso JetBrains Rider para isso:
Agora defina a versão para algo muito baixo como 0.0.1 e construa o projeto. Certifique-se também de gerar um pacote nuget pelo menos uma vez. Precisamos disso mais tarde para gerar a chave de API do NuGet. Depois de construir o projeto e ter um pacote nuget, remova a seguinte linha novamente < GeneratePackageOnBuild>true</ GeneratePackageOnBuild>. Caso contrário, isso pode levar a problemas no final.
Obtendo alguns segredos!
Agora precisamos de 2 segredos para adicionar ao nosso repositório: Nossa API NuGet para que possamos publicar pacotes e um Token de Acesso Pessoal.
NuGet
Vá até o Nuget para criar uma nova chave.
Como não temos um pacote agora, temos que usar o upload de nosso pacote criado anteriormente com uma versão muito baixa. Não se preocupe, podemos deslistá-lo diretamente depois, mas as chaves de API têm como escopo os pacotes. Portanto, se seu pacote não for conhecido, não podemos criar nele. Portanto, basta carregar o pacote nuget criado anteriormente.
Após carregarmos o pacote, podemos gerar diretamente o token:
Depois vá ao seu repositório e adicione a chave NuGet como segredo:
Token de acesso pessoal
Expliquei anteriormente que você já tem algum tipo de token de acesso chamado secrets.GITHUB_TOKEN que podemos usar para ações, mas eles têm um grande problema: imagine que você tem uma ação que envia algumas alterações para nosso branch estável. Não seria bom que nossa ação doc pegasse isso e enviasse as alterações para o branch gh-pages? Bem, isso só funciona com um PAT curto do Personal Access Token. Para isso, você deve acessar o site do Token e gerar um novo token. Apenas a parte do repo deve ser marcada para a ação que escreveremos.
Copie o Token conforme mostrado abaixo:
E por último crie um novo segredo em nosso repositório com o conteúdo do nosso PAT. Portanto, devemos ter 2 segredos em nosso repositório.
Criar uma versão
A última parte chegou. Reunimos todas as informações e criamos um release. Agora a ideia é simples, temos duas entradas: A nova versão que o usuário deve fornecer. Algo como 1.0.0 ou 1.2.0-beta.2. Usaremos esta versão nos seguintes locais:
- Alteramos *Unreleased em nosso CHANGELOG.md para a versão fornecida
- O pacote NuGet terá esta versão
- O lançamento terá esta versão
- Vamos criar uma tag com essa versão
A outra entrada é se tivermos um pré-lançamento. Há apenas um lugar onde isso é importante: a página de lançamento do GitHub. O NuGet não se importa com esse sinalizador. O NuGet usa o controle de versão semântico para determinar se um pacote é um pré-lançamento ou não. Basicamente tudo que tem uma string após o terceiro dígito é considerado um pré-lançamento (1.0.0-rc.1).
Além disso, quando nosso fluxo de trabalho é concluído, ele aciona automaticamente as atualizações da documentação à medida que mesclamos as alterações para estável também. Perfeito! Estamos no controle total quando lançamos, mas uma vez que decidimos fazê-lo, tudo é totalmente automatizado.
name: create-release
on:
workflow_dispatch:
inputs:
versionIncrement:
description: 'The new version. For example: 1.1.0'
required: true
default: ''
prerelease:
description: 'Is this a pre-release?'
type: boolean
required: false
default: false
jobs:
release:
name: Publish new release
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
token: ${{ secrets.PAT }}
persist-credentials: true
fetch-depth: 0
- name: Setup dotnet
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x
- name: Test
run: dotnet test -c Release --no-build
- name: Get changelog entries
id: changelog
uses: mindsers/changelog-reader-action@v2
with:
version: Unreleased
path: ./CHANGELOG.md
- name: Update CHANGELOG file
uses: thomaseizinger/keep-a-changelog-new-release@1.2.1
with:
version: ${{ github.event.inputs.versionIncrement }}
- name: Set git config
run: |
git config --local user.email "linkdotnet@action.com"
git config --local user.name "LinkDotNet Bot"
- name: Commit changes and push changes
run: |
git add CHANGELOG.md
git commit -m "Update Changelog.md for ${{github.event.inputs.versionIncrement}} release" git push origin main
git push origin main
- name: Create release on GitHub
uses: thomaseizinger/create-release@1.0.0
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
tag_name: v${{ github.event.inputs.versionIncrement }
target_commitish: ${{ env.RELEASE_COMMIT_HASH }}
name: v${{ github.event.inputs.versionIncrement }}
body: ${{ steps.changelog.outputs.changes }}
draft: false
prerelease: ${{ github.event.inputs.prerelease }}
- name: Create release package
run: |
dotnet pack -c RELEASE -p:PackageVersion=${{ github.event.inputs.versionIncrement }} -o ${GITHUB_WORKSPACE}/packages
- name: Upload to nuget
run: |
dotnet nuget push ${GITHUB_WORKSPACE}/packages/*.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate --no-symbols
- name: Merge main to stable
run: |
git fetch
git checkout stable
git pull
git merge --no-ff -X theirs origin/main -m "Updating to newest release"
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PAT }}
branch: stable
force: true
Alguns detalhes aqui:
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.PAT }}
branch: stable
force: true
Estamos usando nosso PAT para que todas as ações, conforme explicado acima, sejam acionadas.
- name: Create release on GitHub
uses: thomaseizinger/create-release@1.0.0
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
tag_name: v${{ github.event.inputs.versionIncrement }}
target_commitish: ${{ env.RELEASE_COMMIT_HASH }}
name: v${{ github.event.inputs.versionIncrement }}
body: ${{ steps.changelog.outputs.changes }}
draft: false
prerelease: ${{ github.event.inputs.prerelease }}
Isso criará o lançamento com a tag definida como nossa variável. Vamos adicionar v em frente a ele ficar com a "norma". Também podemos ver que usamos o conteúdo de CHANGELOG.md para preencher o corpo da nossa página de lançamento.
Resultado
Se você confirmar este arquivo e vá para o painel de ações.
Depois de executado com sucesso, podemos ver que temos um novo commit no main, além de uma nova entrada na seção de lançamento do nosso repositório:
Vamos dar uma olhada no nosso CHANGELOG.md:
# Changelog
A nice description could be here.
<!-- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) -->
## [Unreleased]
## [1.0.0] - 2022-04-10
### Added
- `HelloWorldPrinter` which creates the user
### Fixed
- Made the world a bit better!
[Unreleased]: https://github.com/linkdotnet/deployment-template/compare/1.0.0...HEAD
[1.0.0]: https://github.com/linkdotnet/deployment-template/compare/902a59583c17b4e0c437e156c038bd25ac2958f0...1.0.0
Também perfeito! A última coisa importante é que nosso pacote foi publicado no NuGet:
Isso é perfeito! Também podemos colocar uma marca de seleção por trás dessa tarefa. E é basicamente isso. Com apenas um clique de botão podemos liberar um pacote, criar as informações e tags no GitHub e atualizar nossa documentação também. Claro que você pode adotar esses fluxos de trabalho como desejar.
Recursos
O template GitHub Repository pode ser encontrado aqui