💡 A imagem de destaque mostra um pipeline típico de CI/CD em ação, parcialmente desenhado por OpenAI DALL-E, mas neste artigo vamos desenvolver algo benéfico

Este é um tutorial relativamente curto sobre como desenvolver, testar e implantar suas extensões de CI para GitHub Actions, Azure Pipelines e CircleCI a partir de um único monorepo e é baseado na experiência de criar as extensões de CI do Qodana.

Comece com os templates oficiais

Vamos escolher a pilha de tecnologia para nossas extensões de CI.

OK, eu não vou escolher. Vou apenas dizer por que usei TypeScript e node.js para as extensões.

Vantagens de usar actions baseadas em JS

  • Mais flexível do que abordagens baseadas em bash/Dockerfile
  • Escrever testes é relativamente simples

Desvantagens

  • JavaScript

Então vamos escrever uma action baseada em TypeScript!

GitHub Actions

Achei a documentação do GitHub Actions mais fácil de ler do que a do Azure, então recomendo começar escrevendo e testando suas extensões no GitHub usando o template oficial actions/typescript-action. O template mencionado fornece um bom ponto de partida; não vou repetir os passos aqui. Brinque com ele, escreva algumas coisas simples e depois volte aqui para os próximos passos.

Azure Pipelines

O GitHub Actions é construído sobre a infraestrutura do Azure, então portar sua GitHub action para o Azure Pipelines deve ser relativamente fácil.

Então,

  • a “action” torna-se a “task”
  • é empacotada de forma um pouco diferente, distribuída e instalada de outra maneira

E a definição de uma task task.json é a mesma que a de uma action action.yml.

Por exemplo, tendo o seguinte action.yml:

name: 'Your name here'
description: 'Provide a description here'
author: 'Your name or organization here'
inputs:
  milliseconds: # change this
    required: true
    description: 'input description here'
    default: 'default value if applicable'
runs:
  using: 'node16'
  main: 'dist/index.js'

“Facilmente” se traduz para a seguinte task do Azure:

{
  "$schema": "https://raw.githubusercontent.com/Microsoft/azure-pipelines-task-lib/master/tasks.schema.json",
  "id": "822d6cb9-d4d1-431b-9513-e7db7d718a49",
  "name": "YourTaskNameHere",
  "friendlyName": "Your name here",
  "description": "Provide a description here",
  "helpMarkDown": "Provide a longer description here",
  "author": "Your name or organization here",
  "version": {
    "Major": 1,
    "Minor": 0,
    "Patch": 0
  },
  "instanceNameFormat": "YourTaskNameHere",
  "inputs": [
    {
      "name": "milliseconds",
      "type": "string",
      "label": "label name here",
      "defaultValue": "default value if applicable",
      "required": true,
      "helpMarkDown": "input description here"
    }
  ],
  "execution": {
    "Node10": {
      "target": "index.js"
    }
  }
}

A partir de um exemplo tão simples, pode-se ver por que sugeri começar com GitHub Actions. Mas vamos continuar.

Para começar a desenvolver sua nova task brilhante do Azure Pipelines, sugiro apenas copiar o diretório da action e então implementar os passos da documentação oficial do Azure – é bem direto.

  1. Crie vss-extension.json
  2. Crie task.json e coloque-o no diretório dist (na verdade é melhor nomeá-lo após o nome da task)
  3. Se você usou algum método de @actions/core ou @actions/github em sua action, você precisa substituí-los pelos métodos correspondentes de azure-pipelines-task-lib (por exemplo, core.getInput tl.getInput)

A API de azure-pipelines-task-lib é similar a @actions/core e outras bibliotecas @actions/*. Por exemplo, temos um método para obter os parâmetros de entrada:

export function getInputs(): Inputs {
  return {
    milliseconds: core.getInput('milliseconds'),
  }
}

E o mesmo para Azure Pipelines:

export function getInputs(): Inputs {
  return {
    milliseconds: tl.getInput('milliseconds'),
  }
}

Para casos mais reais, sinta-se à vontade para explorar nossa base de código do Qodana GitHub Actions utils e Azure Pipelines task utils.

Crie o monorepo

Vamos usar npm workspaces para gerenciar o monorepo. Coloque seu código de action e task em subdiretórios (por exemplo, github) do seu monorepo recém-criado. E então crie um arquivo package.json no diretório raiz.

{
  "name": "@org/ci",
  "version": "1.0.0",
  "description": "Common code for CI extensions",
  "license": "Apache-2.0",
  "workspaces": [
    "github",
    "azure"
  ],
  "devDependencies": {
    "typescript": "latest",
    "eslint": "latest",
    "eslint-plugin-github": "latest",
    "eslint-plugin-jest": "latest",
    "prettier": "latest",
    "ts-node": "latest"
  }
}
 

Então a estrutura do monorepo fica assim:

...
├── action.yaml
├── github/
├── azure/
└── package.json

Após implementar a configuração do workspace, você pode executar tasks e actions do diretório raiz. Por exemplo, para executar a task build do diretório github, você pode usar o seguinte comando:

npm run -w github build

Compartilhe código entre actions e tasks

A parte mais valiosa do uso da abordagem de monorepo começa aqui: você pode compartilhar o código entre suas actions e tasks.

Vamos fazer os seguintes passos:

  1. Criar um diretório common na raiz do monorepo, um subprojeto para código compartilhado
  2. Atualizar as configurações do compilador tsconfig.json de todos os subdiretórios para builds adequados do projeto

Primeiro, vamos criar o tsconfig base – tsconfig.base.json com as configurações base que serão usadas em todos os subprojetos:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "composite": true
  },
  "exclude": ["node_modules", "**/*.test.ts", "*/lib/**"]
}

Então crie um tsconfig.json simples na raiz do projeto:

{
  "references": [
    { "path": "common" },
    { "path": "azure" },
    { "path": "github" }
  ],
  "files": []
}

Então common/tsconfig.json:

{
  "extends": "../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./lib",
    "rootDir": "."
  },
  "files": ["include your files here or use typical include/exclude patterns"]
}

E finalmente, atualize os arquivos tsconfig.json nos subprojetos (eles são basicamente os mesmos, por exemplo, github/tsconfig.json):

{
  "extends": "../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./lib",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../common" }
  ]
}

Agora você pode usar o código compartilhado do diretório common em suas actions e tasks. Por exemplo, temos um arquivo qodana.ts no diretório common que contém a função getQodanaUrl que retorna a URL para a ferramenta CLI do Qodana. E nós a usamos tanto em actions quanto em tasks.

CleanShot 2023-06-18 at 16 54 11@2x

Construa e publique

Você já tem workflows do GitHub do template configurados para publicar suas actions nos releases do seu repositório. Para releases automatizados, usamos GH CLI, e temos um script simples que publica um changelog nos releases do repositório:

#!/usr/bin/env bash
previous_tag=0
for current_tag in $(git tag --sort=-creatordate)
do
 
if [ "$previous_tag" != 0 ];then
    printf "## Changelog\n"
    git log ${current_tag}...${previous_tag} --pretty=format:'* %h %s' --reverse | grep -v Merge
    printf "\n"
    break
fi
previous_tag=${current_tag}
done

E o workflow do GitHub que o executa:

name: 'Release'
on:
  push:
    tags:
      - '*'
permissions:
  contents: write
 
jobs:
  github:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - run: |
          ./changelog.sh > changelog.md
          gh release create ${GITHUB_REF##*/} -F changelog.md
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Para releases de tasks do Azure Pipelines, você pode usar a abordagem oficial do Azure. Mas também pode fazer o mesmo na infraestrutura do GitHub Actions, já que a ferramenta de publicação deles pode ser instalada em qualquer lugar. Então, no nosso caso, é resolvido por um simples job de workflow do GitHub:

  azure:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set Node.js 12.x
        uses: actions/[email protected]
        with:
          node-version: 12.x
      - name: Install dependencies
        run: npm ci && cd vsts/QodanaScan && npm ci && npm i -g tfx-cli
      - name: Package and publish
        run: |
          cd vsts && npm run azure
          mv JetBrains.qodana-*.vsix qodana.vsix
          tfx extension publish --publisher JetBrains --vsix qodana.vsix -t $AZURE_TOKEN
        env:
          AZURE_TOKEN: ${{ secrets.AZURE_TOKEN }}

Com essa configuração, cada release acontece automaticamente em cada push de tag.

git tag -a v1.0.0 -m "v1.0.0" && git push origin v1.0.0
CleanShot 2023-06-18 at 16 55 34@2x

CircleCI?

Ah, sim, este artigo mencionou o orb do CircleCI também… A configuração do CircleCI é direta, mas não suporta extensões TypeScript, então você precisa empacotar seu código em uma imagem Docker ou um binário e executá-lo lá. A única razão para estar incluído neste post é que construímos nosso orb com a abordagem de monorepo, que funciona bem.

Implemente o template oficial de orb e coloque-o em seu monorepo, então a estrutura fica assim:

...
├── action.yaml
├── github/
├── azure/
├── src/            # código-fonte do orb aqui
└── package.json

E lembre-se de fazer commit do diretório .circleci/ no seu repositório para fazer o CircleCI lint, testar e publicar seu orb.

CleanShot 2023-06-18 at 16 49 57@2x