Рубрики
Uncategorized

Рабочие процессы одобрения с действиями GitHub

В последнее время я делал кучу работы с действиями GitHub, из развертывания функций Azure To … Tagged с DevOps, учебником, Opensource, Github.

Я делал кучу работы с Действия GitHub Недавно от Развертывание функций Azure к Ремонт моего блога Но каждая из этих рабочих процессов была довольно прямой, просто построить и развернуть все с одним рабочим процессом.

С моим последним проектом Fsharp. Cosmosdb Я хотел использовать действия GitHub, но рабочий процесс, который я хочу, это немного сложнее. Для других проектов OSS, таких как dotnet-delice Рабочий процесс работает так: я нажимаю владелец Он будет компилировать приложение, создать пакеты Nuget, а затем ждать, чтобы я одобрил релиз, прежде чем нажать Nuget, создавая релиз Github и пометить правильное Commit. Это дает мне уровень контроля от случайных толкаков к Мастер И я с этим справляюсь через Лазурный трубопровод Что поддерживает простое одобрение потока, нажав кнопку «Утвердить».

Но на данный момент действия GitHub не имеют функциональности, чтобы делать одобрения, поэтому я создал меня собственным! Если вы просто хотите увидеть последние части вот построить рабочий процесс и Отпустите рабочий процесс , но вы захотите прочитать, чтобы понять, как они работают. 😊

Определение нашего рабочего процесса

Идея этого рабочего процесса — это то, что я думаю, что я думаю, что довольно распространена в проектах с открытым исходным кодом, я хочу иметь сборку и упаковку в качестве одного рабочего процесса, а затем эти активы были доступны для людей, которые будут потреблять и тестировать, затем на основе обратной связи (выпуск Это хорошо или нет) Это будет «продвигается» к официальному репозитории пакета, создается релиз GitHub, коммиты отмечены, все такое дело.

Сборка будет довольно прямо вперед, я использую Подделка Чтобы сценарировать рабочий процесс сборки, и я использую изменение изменений в следующем Держите изменяемый файл определить выпуск и его детали. Работа выглядит так:

jobs:
    build:
        runs-on: ubuntu-latest
        steps:
            - name: Checkout
              uses: actions/checkout@master

            - name: Setup Dotnet ${{ env.DOTNET_VERSION }}
              uses: actions/setup-dotnet@v1
              with:
                  dotnet-version: ${{ env.DOTNET_VERSION }}

            - name: Restore dotnet tools
              run: dotnet tool restore

            - name: Generate packages
              run: dotnet fake run ./build.fsx --target Release

Теперь у меня есть свои артефакты, я хочу настроить некоторые метаданные, которые будут доступны, такие как номер версии из ChangeLog. Для этого я создал специальную поддельную задачу:

let getChangelog() =
    let changelog = "CHANGELOG.md" |> Changelog.load
    changelog.LatestEntry

Target.create "SetVersionForCI" (fun _ ->
    let changelog = getChangelog()
    printfn "::set-env name=package_version::%s" changelog.NuGetVersion)

Обратите внимание, как это делает printfn. :: set-env ? Вот как вы создаете свои собственные переменные среды, и она удобно работает из любого места, который пишет на stdout Отказ

С этим прочитаем мы можем добавить его в рабочий процесс:

- name: Set Version
  run: dotnet fake run ./build.fsx --target SetVersionForCI

- name: Create version file
  run: echo ${{ env.package_version }} >> ${{ env.OUTPUT_PATH }}/version.txt

- name: Publish release packages
    uses: actions/upload-artifact@v1

Одобрения через проблемы GitHub

Когда я думал о том, как делать одобрения, я думал: «Что в Github вы бы использовали для обсуждения и одобрения?» И есть очевидный ответ, проблемы! Моя мысль заключается в том, что если я смогу автоматически автоматизировать создание проблемы и метить ее соответствующим образом, я могу использовать Действия GitHub Trigger Выпуск помечен мониторировать для определенной этикетки, чтобы выключить их. В моем случае я собираюсь иметь ярлык Одобрено освобождение И как только эта метка применяется, я хочу запустить рабочий процесс, чтобы освободить пакеты.

Создание проблем с действиями GitHub

Если вы посмотрите на Действия Marketplace Есть много действий по созданию проблемы, но у меня будет несколько странных требований, поэтому я решил построить свои собственные (также, я не построил свои собственные действия, чтобы это был еще один хороший шанс учиться). Это (а другие, которые мы будем строить) являются частью моего Git Reppo, а не на рынке, поэтому они будут жить в ..Github/Действия Папка, наряду с рабочими процессами, и они будут написаны в Teadercript.

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

Поскольку мы будем работать с проблемами GitHub, нам понадобится токен доступа, который удобно доступен в виде секретной переменной секреты. Github_token. И я собираюсь пройти еще два аргумента, идентификатор текущего действия ( github.run_id ) и версия выпуска ( env.package_version ).

Начнем с создания наших пустых действий:

import * as core from "@actions/core";
import * as github from "@actions/github";
import * as fs from "fs";

async function run() {}

run();

И теперь мы можем начать заполнять Беги Функция:

async function run() {
    const token = core.getInput("token");

    const octokit = new github.GitHub(token);
    const context = github.context;
}

Это дает нам доступ к API GitHub через октокит . Теперь я хочу, чтобы изменяемость, как я хочу бросить, что в тело вопроса, которую мы создаем (поэтому, одобряя, я могу отработать, что находится в выпуске):

async function run() {
    const token = core.getInput("token");

    const octokit = new github.GitHub(token);
    const context = github.context;

    const changelog = fs.readFileSync("./.nupkg/changelog.md", {
        encoding: "UTF8",
    });
}

Примечание. Этот файл создается одним из моих поддельных задач и содержит только текущая версия ChangeLog, не полная история, как корень CHANGELOG.md содержит.

Теперь, чтобы создать проблему:

async function run() {
    const token = core.getInput("token");

    const octokit = new github.GitHub(token);
    const context = github.context;

    const changelog = fs.readFileSync("./.nupkg/changelog.md", {
        encoding: "UTF8",
    });

    const newIssue = await octokit.issues.create({
        ...context.repo,
        labels: [`awaiting-review`, "release-candidate"],
        title: `Release ${core.getInput("package-version")} ready for review`,
        body: `# :rocket: Release ${core.getInput(
            "package-version"
        )} ready for review

## Changelog

--------

${changelog}
    `,
    });
}

Потому что у нас есть context.repo Чтобы дать нам информацию о текущем репо GitHub, я просто распространяю ( ... context.repo ) что на вход otcokit.issues.create. а затем дать ему еще несколько предметов информации, этикетки Ожидание - Обзор и релиз-кандидат , название и тело, которое содержит изменяемый файл. Эти этикетки полезны для меня создавать фильтры в вопросах GitHub, и я могу искать их в будущем рабочем процессе.

И теперь мы закончили, пришло время подключить его к нашему рабочему процессу сборки:

- name: Prepare create release issue action
  uses: actions/setup-node@v1
  with:
      node-version: "12.x"

- name: Building Action
  run: npm i && npm run build
  working-directory: ./.github/actions/create-issue

- name: Create Release Issue
  uses: ./.github/actions/create-issue
  with:
      token: ${{ secrets.GITHUB_TOKEN }}
      action-id: ${{ github.run_id }}
      package-version: ${{ env.package_version }}

Так как я решил сделать эти типографы Я должен добавить 2 дополнительных шага в рабочий процесс, один на настройку Node.js и один для компиляции действия, но важный материал находится в 3-м действии. Как это местное действие использовать Указывает на каталог, в котором оно живет, который является абсолютным путем от корня Git Reppo (Так что вам не нужно использовать .gitub/Действия , но я люблю держать их всех вместе).

И там мы идем, рабочий процесс создает нашу проблему (да, это закрыто, потому что я уже одобрил его):

Утверждение релизов

Это оказалось немного ударом, чем я надеялся, поэтому я надеюсь, что это поможет вам избежать некоторых проблем, которые я ударил с этим шагом. Во-первых, мы используем Выпуск помечен Мероприятие в Действиях GitHub, которые запускают каждый раз, когда вы обследуете проблему, поэтому, если вы используете проблемы, в значительной степени вашу историю действий, скорее всего, станет довольно шумным. Это означает, что вам нужно будет думать о том, как пройти только, когда правильно Добавлена метка, чтобы сделать то, что я создал действие, чтобы проверить, имеет ли проблема определенной этикетки:

import * as core from "@actions/core";
import * as github from "@actions/github";

async function run() {
    const token = core.getInput("token");

    const octokit = new github.GitHub(token);
    const context = github.context;

    if (!context.payload.issue) {
        throw new Error("This should not happen");
    }

    const issue = await octokit.issues.get({
        ...context.repo,
        issue_number: context.payload.issue.number,
    });

    core.setOutput(
        "exists",
        issue.data.labels
            .some((label) => label.name === core.getInput("label"))
            .toString()
    );
}

run();

Действие достаточно прямо вперед, он будет схватить проблему, которая вызвала рабочий процесс из контекста действия и посмотреть, если этикетка передается в действие, присутствовало, и установить параметр выходного сигнала, указывающий его присутствие. Мы будем использовать действие, как так:

- name: Check issue was release issue
  uses: ./.github/actions/check-issue
  id: check-issue
  with:
      token: ${{ secrets.GITHUB_TOKEN }}
      label: release-candidate

Не забудьте установить пакеты и сначала построить действие, я только что пропустил это для краткости здесь.

Проблема в том, что теперь теперь каждое действие после этого нам нужно проверить вывод, чтобы решить, хотите ли мы запустить его, то есть мы добавляем Если: shads.Check-yumpe.outputs.exists каждому действию, которое раздражает. Если кто-то знает, как улучшить, что я все уши!

Получение релиз артефактов

Поскольку фаза сборки создал артефакты, и мы могли бы запустить ряд рабочих процессов, с тех пор нам нужно получить правильно артефакты. В прошлом я использовал Загрузка-артефакт и Скачать-артефакт Чтобы обработать это (и в сборке Workflow, который я использовал Загрузка-артефакт ) Но вот проблема, загрузка ожидает скачать от текущего рабочего процесса , Но я не в рабочем процессе, что артефакт был создан, я в совершенно новом, так как я узнаю, какие артефакты, чтобы получить?

Для этого мы собираемся обновить Создать-выпуск Действие, которое мы создали ранее, чтобы включить идентификатор действия в нем где-то. Изначально я думал сделать это как этикетку, поэтому у вас будет ярлык, как Действие: , но на оживленном хранилище вероятно, что это будет быстро раздражать, так как каждая этикетка — это однометражное использование, и они не удаляются автоматически. Поэтому вместо этого давайте создадим комментарий к проблеме с идентификатором действия. Сразу после того, как мы создали проблему, мы добавим это:

await octokit.issues.createComment({
    ...context.repo,
    issue_number: newIssue.data.number,
    body: `Action: ${core.getInput("action-id")}`,
});

С комментарием добавлено, мы создадим другое действие, чтобы извлечь его, я назвал это Get-Action-ID :

import * as core from "@actions/core";
import * as github from "@actions/github";

async function run() {
    const token = core.getInput("token");

    const octokit = new github.GitHub(token);
    const context = github.context;

    if (!context.payload.issue) {
        throw new Error("This should not happen");
    }

    const comments = await octokit.issues.listComments({
        ...context.repo,
        issue_number: context.payload.issue.number,
    });

    const actionComment = comments.data.find(
        (comment) => comment.body.indexOf("Action: ") >= 0
    );

    if (!actionComment) {
        throw new Error("No comment found that has the right pattern");
    }

    core.setOutput("id", actionComment.body.replace("Action: ", "").trim());
}

run();

Опять же, все происходит в контексте проблемы, поэтому мы знаем, где посмотреть комментарии, которые мы делаем с OctoKit.issues.ListComments И тогда от этого мы посмотрим комментарий, который соответствует шаблону, который мы ожидаем, начать с Действие: . Если это найдено, мы сможем вытащить идентификатор из него и выдвинуть его как выходную переменную!

- name: Get the ID of the Action
  uses: ./.github/actions/get-action-id
  if: steps.check-issue.outputs.exists == 'true'
  id: get-action-id
  with:
      token: ${{ secrets.GITHUB_TOKEN }}

С идентификатором действия в руке мы теперь можем скачать действия, и для этого я решил ленивый и просто написать встроенный скрипт Bash:

- name: Download packages
  if: steps.check-issue.outputs.exists == 'true'
  run: |
      echo ${{ steps.get-action-id.outputs.id }}
      mkdir ${{ env.OUTPUT_PATH }}
      cd ${{ env.OUTPUT_PATH }}
      curl https://api.github.com/repos/aaronpowell/FSharp.CosmosDb/actions/runs/${{ steps.get-action-id.outputs.id }}/artifacts --output artifacts.json
      downloadUrl=$(cat artifacts.json | jq -c '.artifacts[] | select(.name == "packages") | .archive_download_url' | tr -d '"')
      echo $downloadUrl
      curl $downloadUrl --output packages.zip --user octocat:${{ secrets.GITHUB_TOKEN }} --verbose --location
      unzip packages.zip
      ls

Ой, это сложно, давайте сломаемся. Я начинаю с небольшого количества диагностики информации, поэтому я могу посмотреть, какой идентификатор действия есть, а затем создать местоположение, в которое я хочу выбросить файлы. Далее нам нужно получить информацию о артефактах для выпуска:

curl https://api.github.com/repos/aaronpowell/FSharp.CosmosDb/actions/runs/${{ steps.get-action-id.outputs.id }}/artifacts --output artifacts.json

Мы схватываем вывод предыдущего шага и вызов на API GitHub и вернувшимся JSON так:

{
    "total_count": 1,
    "artifacts": [
        {
            "id": 2861674,
            "node_id": "MDg6QXJ0aWZhY3QyODYxNjc0",
            "name": "packages",
            "size_in_bytes": 39715,
            "url": "https://api.github.com/repos/aaronpowell/FSharp.CosmosDb/actions/artifacts/2861674",
            "archive_download_url": "https://api.github.com/repos/aaronpowell/FSharp.CosmosDb/actions/artifacts/2861674/zip",
            "expired": false,
            "created_at": "2020-03-13T03:37:13Z",
            "updated_at": "2020-03-13T03:37:14Z"
        }
    ]
}

Я хочу archive_download_url от артефакта по имени Пакеты и делать то, что я снова сложно и использовал JQ Чтобы найти это:

downloadUrl=$(cat artifacts.json | jq -c '.artifacts[] | select(.name == "packages") | .archive_download_url' | tr -d '"')

Так как это будет » вокруг него я использую тр чтобы разделить их.

Наконец, мы загружаем ZIP-пакет из этого местоположения, используя CURL, но вам нужно аутентифицировать этот запрос, поэтому мы передаем - Прозеро восьмикат: $ {{secrets.github_token}} чтобы скручиваться, а также --локация Чтобы сказать ему, чтобы следить за перенаправлением 302. И с загруженным пакетом мы можем расстегнуть его И я просто бегаю Ls сделать еще больше лесозаготовки.

Публикация на Nuget

С загрузкой пакетов мы можем начать толкать их в различные каналы, давайте начнем с Nuget. Я не чувствовал необходимости использовать третьего действия для этого, так как вам нужно только запустить Dotnet Nuget Push (Примечание. Вам понадобится токен доступа Nuget, поэтому поп-один в ваших секретах), но то, что мне нужно было узнать, было то, что был номер версией, чтобы поставить в путь к файлу при публикации.

К счастью, я создал файл, который я толкнул в список артефактов под названием version.txt который содержит номер версии из CHANGELOG.md . Давайте превратимся в переменную среды:

- name: Get release version
  if: steps.check-issue.outputs.exists == 'true'
  working-directory: ${{ env.OUTPUT_PATH }}
  run: |
      version=$(cat version.txt)
      echo "::set-env name=package_version::$version"

Хороший Ол ‘ кот в помощь. Тогда мы можем настроить среду Dotnet и нажмите Nuget:

- name: Setup Dotnet ${{ env.DOTNET_VERSION }}
  uses: actions/setup-dotnet@v1
  if: steps.check-issue.outputs.exists == 'true'
  with:
      dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Push NuGet Package
  if: steps.check-issue.outputs.exists == 'true'
  working-directory: ${{ env.OUTPUT_PATH }}
  run: |
      dotnet nuget push FSharp.CosmosDb.${{ env.package_version }}.nupkg --api-key ${{ secrets.NUGET_KEY }} --source ${{ env.NUGET_SOURCE }}
      dotnet nuget push FSharp.CosmosDb.Analyzer.${{ env.package_version }}.nupkg --api-key ${{ secrets.NUGET_KEY }} --source ${{ env.NUGET_SOURCE }}

И с этим у нас есть пакеты на ногу.

Режущий релиз

Последнее, что нужно сделать, это создать выпуск GitHub, который будет означать, что мы должны знать, что SHA создается сборка. Первоначально я думал сделать это, добавил его к комментариям вопроса (который я все еще делаю) но тогда я понял, что знаю идентификатор оригинального рабочего процесса Так что я могу просто потянуть метаданные оттуда:

- name: Get Action sha
  if: steps.check-issue.outputs.exists == 'true'
  run: |
      echo ${{ steps.get-action-id.outputs.id }}
      cd ${{ env.OUTPUT_PATH }}
      curl https://api.github.com/repos/aaronpowell/FSharp.CosmosDb/actions/runs/${{ steps.get-action-id.outputs.id }} --output run.json
      action_sha=$(cat run.json | jq -c '.head_sha' | tr -d '"')
      echo "::set-env name=action_sha::$action_sha"

Снова есть немного JQ Разборки вывода, чтобы найти его, но теперь у нас есть полный SHA в переменной окружающей среды, поэтому мы можем создать выпуск, который я определил пользовательское действие для (в основном для того, чтобы соответствовать пути Я Хотите, чтобы это структурировано, но вы можете использовать один с рынка, если вы предпочитаете).

На этот раз давайте сначала посмотрим на использование действия:

- name: Cut GitHub Release
  uses: ./.github/actions/github-release
  if: steps.check-issue.outputs.exists == 'true'
  with:
      token: ${{ secrets.GITHUB_TOKEN }}
      sha: ${{ env.action_sha }}
      version: ${{ env.package_version }}
      path: ${{ env.OUTPUT_PATH }}

Результат работы он увидит, как этот релиз:

Это действие мы создадим релиз для правого SHA, а затем загрузите его файлы (я не передаю в файлы, я их жестко кодирую), так что давайте посмотрим на Беги Функция:

async function run() {
    const token = core.getInput("token");
    const sha = core.getInput("sha");
    const version = core.getInput("version");
    const artifactPath = core.getInput("path");

    const releaseNotes = readFileSync(join(artifactPath, "changelog.md"), {
        encoding: "UTF8",
    });

    const octokit = new github.GitHub(token);
    const context = github.context;

    const release = await octokit.repos.createRelease({
        ...context.repo,
        tag_name: version,
        target_commitish: sha,
        name: `Release ${version}`,
        body: releaseNotes,
    });

    await upload(
        octokit,
        context,
        release.data.upload_url,
        join(artifactPath, `FSharp.CosmosDb.${version}.nupkg`)
    );
    await upload(
        octokit,
        context,
        release.data.upload_url,
        join(artifactPath, `FSharp.CosmosDb.Analyzer.${version}.nupkg`)
    );
}

otcokit.repos.createrease Наш первый главный шаг мы используем текущий контекст для установки информации о репозитории, а затем установить Tag_Name к версии, определенной в нашем изменении изменений и target_commitish вправо SHA, которое создаст для нас Git для нас (приятно!) И тогда мы устанавливаем название и, наконец, тело, я просто впрыскивая изменяемый файл (который заставит меня написать достойный изменение!).

Когда речь идет о прикреплении файлов к выпуску, это то, что вам нужно сделать для каждого файла после создания выпуска, так как создание выпуска дает вам upload_url Для того, где файлы должны быть опубликованы. Так как я делаю это несколько раз, я вытащил функцию, чтобы справиться с ним под названием загрузить :

async function upload(
    octokit: github.GitHub,
    context: Context,
    url: string,
    path: string
) {
    let { name, mime, size, file } = fileInfo(path);
    console.log(`Uploading ${name}...`);
    await octokit.repos.uploadReleaseAsset({
        ...context.repo,
        name,
        file,
        url,
        headers: {
            "content-length": size,
            "content-type": mime,
        },
    });
}

Это использует otcokit.repos.uploadreleaseset Функция для отправки файла, и нам нужно предоставить ему размер ( длина содержимого ) и тип MIME ( тип контента ), который я прохожу с функцией, которая захватывает информацию о файлах . FileInfo :

function mimeOrDefault(path: string) {
    return getType(path) || "application/octet-stream";
}

function fileInfo(path: string) {
    return {
        name: basename(path),
        mime: mimeOrDefault(path),
        size: lstatSync(path).size,
        file: readFileSync(path),
    };
}

Чтобы получить тип MIME, я использую MIME Упаковка NPM, но я мог бы сильно закодировать его, так как я в любом случае, что мне тяжело кодируйте файлы, но это было просто привычкой. В противном случае я использую lstatsync. и Readfilesync от узла ФС модуль.

И с тем, что выпуск создан, и пакеты доступны для людей вручную, если они не хотят использовать Nuget по какой-то причине.

Закрытие проблемы

Последнее, что я хотел автоматизировать, — это закрытие проблемы, используемую для управления рабочим процессом. К настоящему времени я был на рулоне создания пользовательских действий, поэтому я создал еще один (я также не нашел, чтобы просто закрыть проблему, все они были за несвежие проблемы или PR, но, возможно, я не выглядел достаточно сложным).

async function run() {
    const token = core.getInput("token");

    const octokit = new github.GitHub(token);
    const context = github.context;

    if (!context.payload.issue) {
        throw new Error("This should not happen");
    }

    await octokit.issues.createComment({
        ...context.repo,
        issue_number: context.payload.issue.number,
        body: core.getInput("message"),
    });

    await octokit.issues.update({
        ...context.repo,
        issue_number: context.payload.issue.number,
        state: "closed",
    });
}

Для удобства мы добавляем комментарий к проблеме, используя предоставленное сообщение, сделанное через otcokit.issues.createComment , а затем обновление статуса выпуска, используя otcokit.issues.update и установка государство: «Закрыто» Отказ Из нашего файла рабочего процесса мы можем использовать это так:

- name: Close issue
  uses: ./.github/actions/close-issue
  if: steps.check-issue.outputs.exists == 'true'
  with:
      token: ${{ secrets.GITHUB_TOKEN }}
      message: |
          The release has been approved and has been

          * Deployed to NuGet
          * Created as a Release on the repo
          * Commit has been tagged

И с этим, как только проблема помечена Одобрено освобождение Мои взаимодействия сделаны!

Вывод

Моя цель в начале было создание рабочего процесса на основе одобрения с действиями GitHub, и я довольно рад, что я смог сделать это. Вы можете найти самые последние (на момент написания) построить и Выпуск Проходит, и если вы посмотрите в закрытые проблемы, вы найдете их там тоже.

Это немного громоблокируется, хотя, без встроенного одобрения поддержки было много пользовательских действий, которые я оказался письмом (мое репо, теперь докладывается 10% кодовой базы, является Typearcript 🤣) Поэтому я надеюсь, что это функция на их дорожной карте.

Кроме того, это не на 100% дурака. На данный момент я не проверяю этикетки правильно, он должен проверить на Одобрено освобождение этикетка, а также релиз-кандидат Потому что, если бы я поставил другой ярлык, это просто пробежит. Но так как я единственный вкладчик здесь, я меньше беспокоюсь об этом в данный момент.

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

Оригинал: «https://dev.to/azure/approval-workflows-with-github-actions-3bob»