В этой статье мы рассмотрим, каким образом можно использовать GitHub Actions для создания конвейера непрерывной интеграции (CI — continuous integration), который автоматически тестирует, проверяет и анализирует ваш код Go.

Для выполнения таких проверок в своих сольных проектах я обычно создаю пре-коммитный хук Git, но для командных проектов или работы с открытым исходным кодом, где у вас нет полного контроля над всеми средами разработки, использование CI-процессов является отличным способом, чтобы привлечь внимание к потенциальной проблеме и помочь выявить ошибки до того, как они просочатся в систему контроля версий или даже продакшн.

Если ваш репозиторий уже размещен на GitHub, то вам будет очень легко и удобно использовать их встроенные функции без необходимости задействовать какие-либо дополнительные сторонние инструменты или сервисы.

Чтобы продемонстрировать, как это работает, я подготовил для вас это пошаговое руководство.

Если хотите повторять изложенные шаги вслед за руководством, то вам нужно будет создать новый репозиторий и клонировать его на свой локальный компьютер. В рамках этого руководства я буду использовать приватный репозиторий alexedwards/example.

$ git clone git@github.com:alexedwards/example.git
Cloning into 'example'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

Затем давайте создадим простое приложение Go вместе с (непроходимым) тестом, как показано в следующем примере:

$ cd example/
$ touch main.go main_test.go
$ go mod init github.com/alexedwards/example
File: main.go
package main

import "fmt"

func main() {
    msg := sayHello("Alice")
    fmt.Println(msg)
}

func sayHello(name string) string {
    return fmt.Sprintf("Hi %s", name)
}
File: main_test.go
package main

import "testing"

func Test_sayHello(t *testing.T) {
    name := "Bob"
    want := "Hello Bob"

    if got := sayHello(name); got != want {
        t.Errorf("hello() = %q, want %q", got, want)
    }
}

Если вы запустите это приложение, оно должно правильно скомпилироваться и вывести "Hi Alice", но выполнив go test . мы получим FAIL, как вы можете наблюдать ниже:

$ go test .
--- FAIL: Test_sayHello (0.00s)
  main_test.go:10: hello() = "Hi Bob", want "Hello Bob"
FAIL
FAIL	github.com/alexedwards/example	0.002s
FAIL

Создание файла рабочего процесса

Следующее, что мы хотим создать - это файл рабочего процесса (workflow file), который описывает, что именно мы хотим делать в наших CI-проверках, и когда мы хотим, чтобы они выполнялись. По соглашению этот файл должен храниться в каталоге .github/workflow в корне вашего репозитория и должен иметь формат YAML

Давайте создадим этот каталог вместе с файлом рабочего процесса audit.yml .

$ mkdir -p .github/workflows
$ touch .github/workflows/audit.yml

Есть отличное введение в синтаксис файла рабочего процесса, а также набор шаблонов для разных языков и сред, которые можно использовать в качестве отправной точки.

А пока давайте перейдем к обновлению файла рабочего процесса, чтобы он выглядел как указано ниже:

File: .github/workflows/audit.yml
name: Audit

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:

  audit:
    runs-on: ubuntu-20.04
    steps:
    - uses: actions/checkout@v2

    - name: Set up Go
      uses: actions/setup-go@v2
      with:
        go-version: 1.17

    - name: Verify dependencies
      run: go mod verify

    - name: Build
      run: go build -v ./...

    - name: Run go vet
      run: go vet ./...

    - name: Install staticcheck
      run: go install honnef.co/go/tools/cmd/staticcheck@latest

    - name: Run staticcheck
      run: staticcheck ./...

    - name: Install golint
      run: go install golang.org/x/lint/golint@latest

    - name: Run golint
      run: golint ./...

    - name: Run tests
      run: go test -race -vet=off ./...

Давайте пробежимся по нему и разберем, что делают разные части этого файла.

  • Сначала мы используем ключевое слово on, чтобы определить, когда мы хотим запускать рабочий процесс. В данном случае я настроил рабочий процесс так, чтобы он запускался, когда делается новый коммит в ветку main или производится пул-запрос.

  • Затем мы используем ключевое слово jobs , чтобы определить список джобов (jobs, задач), которые должны быть запущены. На данный момент наш рабочий процесс содержит только один джоб под именем audit, но вы можете определить несколько джобов, если хотите, и (по умолчанию) они будут выполняться параллельно.

  • Для каждого джоба будет выделен отдельный раннер (runner). По сути, это виртуальная машина, которая будет выполнять шаги (steps) джоба. В приведенном выше файле мы используем ключевое слово run-on, чтобы указать, что мы хотим, чтобы раннер использовал Ubuntu 20.04 в качестве базовой ОС, но доступны и другие операционные системы. Также стоит отметить, что в раннере уже предустановлено множество полезного программного обеспечения и инструментов.

  • На первом шаге нашего джоба audit мы используем ключевое слово uses для выполнения общедоступный экшена actions/checkout@v2. Этот экшен выгрузит код из репозитория нашего проекта в раннер, чтобы следующие шаги получили к нему доступ.

  • Затем мы используем экшн actions/setup-go@v2, чтобы установить Go версии 1.17 на раннер.

  • Как только это будет сделано, в оставшихся шагах мы используем ключевое слово run для выполнения определенных команд в раннере. В этом случае мы билдим наш код, а затем проверяем его, используя стандартные команды go build|vet|test и инструменты golint и staticcheck .

Важно: Если вы следуете инструкциям, запустите $ git branch --show-current , чтобы проверить имя вашей ветки, прежде чем продолжить. В некоторых случаях ваша ветка может получить имя master вместо main, и в этом случае нужно отредактировать директиву on в audit.yml соответствующим образом.

Теперь, когда все готово, давайте закоммитим все и запушим изменения в ваш репозиторий:

$ git add .
$ git commit -m "Initial commit"
$ git push 

После завершения пуша перейдите в свой репозиторий и выберите вкладку “Actions”. Вы должны увидеть, что рабочий процесс CI 'Audit' запущен, как показано на скриншоте ниже.

Вы можете кликнуть по имени рабочего процесса, чтобы увидеть более подробную информацию во время его выполнения. Где-то через минуту или две вы должны увидеть, что рабочий процесс завершен из-за нашего непроходимого теста.

Кроме того, как владелец репозитория, вы также должны получить уведомление по электронной почте о сбое рабочего процесса, и каждый, кто просматривает репозиторий, увидит красный крестик рядом с коммитом в истории Git.

Исправление кода

Давайте приведем в порядок нашу кодовую базу, подправив sayHello(), чтобы она возвращала правильный вывод, как показано ниже:

File: main.go
package main

import "fmt"

func main() {
	msg := sayHello("Alice")
	fmt.Println(msg)
}

func sayHello(name string) string {
	// Измените это на "Hello %s" вместо "Hi %s".
	return fmt.Sprintf("Hello %s", name)
}

Если хотите, вы можете закоммитить и запушить это изменение…

$ git add .
$ git commit -m "Fix sayHello() to return the correct value"
$ git push

…и тогда вы должны увидеть, что джоб 'Audit' в нашем файле рабочего процесса теперь успешно завершен, и все рядом с ним помечено красивой зеленой галочкой.

Отлично! Теперь все работает как надо, и с этого момента каждый раз, когда кто-то делает push или pull request в ветку main, тесты и проверки будут запускаться автоматически.

Далее вы можете расширить рабочий процесс, чтобы выполнять дополнительные проверки или отправлять дополнительные уведомления, если хотите, или даже модифицировать его, чтобы он действовал как конвейер непрерывного развертывания (continuous deployment - CD), который создает и развертывает ваши бинарники. Чтобы натолкнуть вас на некоторые идеи, вот несколько немного более сложных рабочих процессов из моих собственных проектов:

Если вам понравилась эта статья, вы можете ознакомиться с моим списком рекомендуемых руководств или ознакомиться с моими книгами Let's Go и Let's Go Further, которые научат вас всему, что вам нужно знать о том, как создавать профессиональные веб-приложения и API, готовые к работе в продакшене, с помощью Go.

Часто сражаетесь с линтером? Не понимаете, что он от вас хочет? Вам кажется, что он вас ненавидит? Приглашаем всех желающих на открытый урок «Как подружиться с golangci-lint», на котором постараемся разобраться с этими вопросами.

Комментарии (2)


  1. paramtamtam
    08.02.2022 17:18
    +1

    Спасибо за пост!

    Я бы ещё посоветовал кэшировать зависимости (если это возможно в проекте) дабы не ходить каждый раз в далёкие галактики за ними (на примере простого запуска `go test`):

      go-test:
        name: Unit tests
        runs-on: ubuntu-20.04
        steps:
          - uses: actions/setup-go@v2
            with: {go-version: 1.17}
    
          - uses: actions/checkout@v2
    
          - name: Go modules Cache # Docs: <https://git.io/JfAKn#go---modules>
            uses: actions/cache@v2
            id: go-cache
            with:
              path: ~/go/pkg/mod
              key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
              restore-keys: ${{ runner.os }}-go-
    
          - if: steps.go-cache.outputs.cache-hit != 'true'
            run: go mod download
    
          - name: Run Unit tests
            run: go test -race ./...


    1. Sly_tom_cat
      08.02.2022 19:39
      +2

      Да что вы - про кеширование же можно еще одну статью написать :).