Azure DevOpsのパイプラインから自動ツイートさせる方法

こんにちは。

日頃、boiler's Graphics（ https://github.com/dhq-boiler/boiler-s-Graphics ）の開発に勤しんでいるのですが、ただ勤しむだけではダメで、ひとさじのスパイスというか、工夫が必要だったりします。今回の記事では、boiler's GraphicsのCD/CI、継続的インテグレーションの話を簡単に説明した後、本題のパイプラインから自動ツイートさせる方法を述べます。

# azure-pipelines.ymlの作り方

boiler's Graphics では、GitHubのリポジトリにmasterブランチおよびdevelopブランチをプッシュする度に、自動でビルド、テスト、パッケージング、そしてツイートするようになっています。

Azure DevOpsでCD/CIを実現するためには、azure-pipelines.ymlというファイルを書く必要があります。

実は、このazure-pipelines.ymlというファイルはブランチ毎に作ることも可能で、たとえばmasterブランチがプッシュされたら動作するパイプラインとdevelopブランチがプッシュされたら動作するパイプラインというように分けることができます。

この場合、次のようにファイルを分けます。

* azure-pipelines.yml ===============> masterブランチ用
* azure-pipelines_develop.yml =======> developブランチ用

以下のようにしても良いです。

* azure-pipelines_master.yml
* azure-pipelines_develop.yml

名前は自由に決めてもらって構わないです。結局、Azure DevOpsの画面でazure-pipelines.ymlの場所を聞かれるので、そのパイプラインの適切なブランチのazure-pipeline.ymlを選択すればOKです。

Configure your pipelineの画面で"Existing Azure Pipelines YAML file"を選択し、BranchとPathを設定します。

![2021-11-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4734/0f17314d-7cb5-9e5a-531a-e164c6051b29.png)

# azure-pipelines.yml の内容

boiler's Graphics では2つのazure-pipelines.ymlが存在しています。

* /azure_devops/azure-pipelines.yml ==========> masterブランチ用
* /azure_devops/azure-pipelines_develop.yml ==> developブランチ用

それぞれやっていることが違うのでそれぞれ説明していきたいと思います。

## azure-pipelines.yml (masterブランチの場合)

```yml:/azure_devops/azure-pipelines.yml
# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- master

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  disable.coverage.autogenerate: 'true'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: CmdLine@2
  displayName: "NUnit & OpenCover"
  inputs:
    script: >
      packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe
      -register:Path64
      -target:"packages\NUnit.ConsoleRunner.3.12.0\tools\nunit3-console.exe"
      -targetargs:"boilersGraphics.Test.dll"
      -targetdir:"boilersGraphics.Test\bin\$(buildConfiguration)"
      -returntargetcode
      -output:"coverage.xml"
      -filter:"+[boilersGraphics]* +[TsOperationHistory]* -[*]XamlGeneratedNamespace*"

- task: PublishTestResults@2
  displayName: "Publish unit test result"
  condition: always()
  inputs:
    testResultsFormat: 'NUnit'
    testResultsFiles: '**/TestResult.xml'
    failTaskOnFailedTests: false

- task: reportgenerator@4
  condition: always()
  inputs:
    reports: '**\coverage.xml'
    targetdir: 'coveragereport'

- task: PublishCodeCoverageResults@1
  condition: always()
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: 'coveragereport\Cobertura.xml'
    reportDirectory: 'coveragereport\'

# Archive files
# Compress files into .7z, .tar.gz, or .zip
- task: ArchiveFiles@2
  condition: always()
  inputs:
    rootFolderOrFile: '$(System.DefaultWorkingDirectory)\boilersGraphics\bin\Release' 
    includeRootFolder: false
    archiveType: 'zip' # Options: zip, 7z, tar, wim
    tarCompression: 'gz' # Optional. Options: gz, bz2, xz, none
    archiveFile: '$(Build.ArtifactStagingDirectory)/boilersGraphics_$(Build.BuildId).zip' 
    replaceExistingArchive: true 
    #verbose: # Optional
    #quiet: # Optional

# GitHub Release
# Create, edit, or delete a GitHub release
- task: GitHubRelease@0
  condition: always()
  inputs:
    gitHubConnection: dhq-boiler-azure-devops
    repositoryName: '$(Build.Repository.Name)' 
    action: 'create' # Options: create, edit, delete
    target: '$(Build.SourceVersion)' # Required when action == Create || Action == Edit
    tagSource: 'auto' # Required when action == Create# Options: auto, manual
    #tagPattern: # Optional
    #tag: # Required when action == Edit || Action == Delete || TagSource == Manual
    #title: # Optional
    #releaseNotesSource: 'file' # Optional. Options: file, input
    #releaseNotesFile: # Optional
    #releaseNotes: # Optional
    assets: '$(Build.ArtifactStagingDirectory)\boilersGraphics_$(Build.BuildId).zip'
    assetUploadMode: 'delete' # Optional. Options: delete, replace
    isDraft: false # Optional
    isPreRelease: false # Optional
    addChangeLog: true # Optional
    compareWith: 'lastFullRelease' # Required when addChangeLog == True. Options: lastFullRelease, lastRelease, lastReleaseByTag
    #releaseTag: # Required when compareWith == LastReleaseByTag

- powershell: |
   $CI_BUILD_TAG = git describe --tags
   Write-Host "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
  displayName: 'Set the tag name as an environment variable'

- task: CmdLine@2
  displayName: "pip install python-oauth2"
  inputs:
    script: >
      pip install twitter

# Python script
# Run a Python file or inline script
- task: PythonScript@0
  inputs:
    scriptSource: 'inline' # Options: filePath, inline
    #scriptPath: azure_devops/python_script/tweet.py # Required when scriptSource == filePath
    script: >
        from twitter import Twitter, OAuth

        access_token = '$(AccessToken)'

        access_token_secret = '$(AccessTokenSecret)'

        api_key = '$(APIKey)'

        api_secret = '$(APIKeySecret)'
        
        t = Twitter(auth = OAuth(access_token, access_token_secret, api_key, api_secret))

        text = 'https://github.com/dhq-boiler/boiler-s-Graphics/releases/tag/$(CI_BUILD_TAG)'

        statusUpdate = t.statuses.update(status=text)
    #arguments: # Optional
    #pythonInterpreter: # Optional
    #workingDirectory: # Optional
    #failOnStderr: false # Optional
```

trigger で master ブランチを指定してあります。

タスクの概略は以下のとおりです。

1. NugetToolInstaller
1. NuGetCommand ソリューションをリストアします。
1. VSBuild ソリューションをビルドします。
1. CmdLine NUnitテストの実行とOpenCoverによるカバレッジ収集
1. PublishTestResult ユニットテストの結果を発行します
1. reportgenerator カバレッジの結果を発行します
1. PublishCodeCoverageResults　Cobertura.xmlを発行します（嘘かも。詳細は忘れた）
1. ArchiveFiles ビルドした出力ファイル等をzipファイルに圧縮します
1. GitHubRelease GitHubのReleaseを発行します
1. powershell タグ名を取得して$(CI_BUILD_TAG)に格納します
1. CmdLine pythonのtwitterライブラリをインストールします
1. PythonScript ビルド結果をツイートします

ここで解説したいのは10, 11, 12です。
12でツイートする内容としてGitHubのリリースページのURLを作り上げる必要があります。
例：https://github.com/dhq-boiler/boiler-s-Graphics/releases/tag/v4.7.2.6
この例においては"v4.7.2.6"という文字列をどこかから取得しないとツイートの内容を構成できません。なので、10でgit describe --tagsをしてその内容を環境変数に格納するというのをやっているのが以下です。

```yml:azure-pipelines.yml
- powershell: |
   $CI_BUILD_TAG = git describe --tags
   Write-Host "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
  displayName: 'Set the tag name as an environment variable'
```

次に11ではpythonのtwitterライブラリをインストールします。それだけです。

12ではtwitterライブラリを使ってツイートします。
scriptSourceはinlineにする必要があります。
scriptに複数行のpythonコードを書くために、script: >という書き方をします。

```yml:azure-pipelines.yml
# Python script
# Run a Python file or inline script
- task: PythonScript@0
  inputs:
    scriptSource: 'inline' # Options: filePath, inline
    #scriptPath: azure_devops/python_script/tweet.py # Required when scriptSource == filePath
    script: >
        from twitter import Twitter, OAuth

        access_token = '$(AccessToken)'

        access_token_secret = '$(AccessTokenSecret)'

        api_key = '$(APIKey)'

        api_secret = '$(APIKeySecret)'
        
        t = Twitter(auth = OAuth(access_token, access_token_secret, api_key, api_secret))

        text = 'https://github.com/dhq-boiler/boiler-s-Graphics/releases/tag/$(CI_BUILD_TAG)'

        statusUpdate = t.statuses.update(status=text)
    #arguments: # Optional
    #pythonInterpreter: # Optional
    #workingDirectory: # Optional
    #failOnStderr: false # Optional
```

script内では一行置きにコードを書いていますが、これはエラー防止の策でこうなっています。書きたくてこんな間抜けなコードを書いているわけではありません。scriptSourceをinlineに設定した理由はscriptSource = filePathにした時別のpythonファイルを参照する方法では、コード中に環境変数を使えないためです。inlineにしているのは、中のコードで環境変数を使うためです。で、ここでは4つの環境変数を使用しています。\$(AccessToken)、\$(AccessTokenSecret)、\$(APIKey)、\$(APIKeySecret)の4つです。これらは以下の画面で設定できます。

![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4734/3f19b8e2-1b83-0b93-a649-51265e56d46d.png)

![2021-11-04 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4734/64bd88f7-9da0-2fbb-eb49-72b2421010e2.png)

**Keep this value secretにはチェックを入れて下さい。この4つの変数の値はセンシティブな情報です。ファイルに直接書かないで下さい。**

![2021-11-04 (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4734/4362953b-4e35-65c5-76b3-95e2ce774c23.png)

最終的には以下のようなツイートが発行されます。
これはmasterブランチを自動ビルドした時に発行されるツイートです。

<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/aCx4QI4gXQ">https://t.co/aCx4QI4gXQ</a></p>&mdash; boiler&#39;s Graphics ビルドステータス (@boilersGraphics) <a href="https://twitter.com/boilersGraphics/status/1456105892378595330?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## azure-pipelines.yml (developブランチの場合)


```yml:azure-pipelines_develop.yml
# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net

trigger:
- develop

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'
  disable.coverage.autogenerate: 'true'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: CmdLine@2
  displayName: "NUnit & OpenCover"
  inputs:
    script: >
      packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe
      -register:Path64
      -target:"packages\NUnit.ConsoleRunner.3.12.0\tools\nunit3-console.exe"
      -targetargs:"boilersGraphics.Test.dll"
      -targetdir:"boilersGraphics.Test\bin\$(buildConfiguration)"
      -returntargetcode
      -output:"coverage.xml"
      -filter:"+[boilersGraphics]* +[TsOperationHistory]* -[*]XamlGeneratedNamespace*"

- task: PublishTestResults@2
  displayName: "Publish unit test result"
  condition: always()
  inputs:
    testResultsFormat: 'NUnit'
    testResultsFiles: '**/TestResult.xml'
    failTaskOnFailedTests: false

- task: reportgenerator@4
  condition: always()
  inputs:
    reports: '**\coverage.xml'
    targetdir: 'coveragereport'

- task: PublishCodeCoverageResults@1
  condition: always()
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: 'coveragereport\Cobertura.xml'
    reportDirectory: 'coveragereport\'

# Archive files
# Compress files into .7z, .tar.gz, or .zip
- task: ArchiveFiles@2
  condition: always()
  inputs:
    rootFolderOrFile: '$(System.DefaultWorkingDirectory)\boilersGraphics\bin\Release' 
    includeRootFolder: false
    archiveType: 'zip' # Options: zip, 7z, tar, wim
    tarCompression: 'gz' # Optional. Options: gz, bz2, xz, none
    archiveFile: '$(Build.ArtifactStagingDirectory)/boilersGraphics_$(Build.BuildId).zip' 
    replaceExistingArchive: true 
    #verbose: # Optional
    #quiet: # Optional

# GitHub Release
# Create, edit, or delete a GitHub release
- task: GitHubRelease@0
  condition: always()
  inputs:
    gitHubConnection: dhq-boiler-azure-devops
    repositoryName: '$(Build.Repository.Name)' 
    action: 'create' # Options: create, edit, delete
    target: '$(Build.SourceVersion)' # Required when action == Create || Action == Edit
    tagSource: manual # Required when action == Create# Options: auto, manual
    #tagPattern: # Optional
    tag: unstable-$(Build.BuildNumber) # Required when action == Edit || Action == Delete || TagSource == Manual
    #title: # Optional
    #releaseNotesSource: 'file' # Optional. Options: file, input
    #releaseNotesFile: # Optional
    #releaseNotes: # Optional
    assets: '$(Build.ArtifactStagingDirectory)\boilersGraphics_$(Build.BuildId).zip'
    assetUploadMode: 'delete' # Optional. Options: delete, replace
    isDraft: false # Optional
    isPreRelease: true # Optional
    addChangeLog: true # Optional
    compareWith: 'lastFullRelease' # Required when addChangeLog == True. Options: lastFullRelease, lastRelease, lastReleaseByTag
    #releaseTag: # Required when compareWith == LastReleaseByTag

- task: CmdLine@2
  displayName: "pip install python-oauth2"
  inputs:
    script: >
      pip install twitter

# Python script
# Run a Python file or inline script
- task: PythonScript@0
  inputs:
    scriptSource: 'inline' # Options: filePath, inline
    #scriptPath: azure_devops/python_script/tweet.py # Required when scriptSource == filePath
    script: >
        from twitter import Twitter, OAuth

        access_token = '$(AccessToken)'

        access_token_secret = '$(AccessTokenSecret)'

        api_key = '$(APIKey)'

        api_secret = '$(APIKeySecret)'
        
        t = Twitter(auth = OAuth(access_token, access_token_secret, api_key, api_secret))

        text = 'https://github.com/dhq-boiler/boiler-s-Graphics/releases/tag/unstable-$(Build.BuildNumber)'

        statusUpdate = t.statuses.update(status=text)
    #arguments: # Optional
    #pythonInterpreter: # Optional
    #workingDirectory: # Optional
    #failOnStderr: false # Optional

```

trigger で develop ブランチを指定してあります。

タスクの概略は以下のとおりです。

1. NugetToolInstaller
1. NuGetCommand ソリューションをリストアします。
1. VSBuild ソリューションをビルドします。
1. CmdLine NUnitテストの実行とOpenCoverによるカバレッジ収集
1. PublishTestResult ユニットテストの結果を発行します
1. reportgenerator カバレッジの結果を発行します
1. PublishCodeCoverageResults　Cobertura.xmlを発行します（嘘かも。詳細は忘れた）
1. ArchiveFiles ビルドした出力ファイル等をzipファイルに圧縮します
1. GitHubRelease GitHubのReleaseを発行します**(isPreRelease=true, tag=unstable-$(Build.SourceVersion))**
1. powershell タグ名を取得して$(CI_BUILD_TAG)に格納します
1. CmdLine pythonのtwitterライブラリをインストールします
1. PythonScript ビルド結果をツイートします

masterブランチの時と大きく変わらないのですが、少し変更するところがあります。

developブランチで成果物として固めたいものは変わりがないのですが、GitHubReleaseでのリリース名が”unstable-$(Build.BuildNumber)"となります。
そしてリリースはプレリリースとして発行したいので、isPreRelease: trueにします。

12のツイートでは、ツイート内容のリンクの末尾をunstable-$(Build.BuildNumber)にしています。
これでツイートに正しいURLが貼られます。
こんな風に。

<blockquote class="twitter-tweet"><p lang="und" dir="ltr"><a href="https://t.co/B1vjvTeMk3">https://t.co/B1vjvTeMk3</a></p>&mdash; boiler&#39;s Graphics ビルドステータス (@boilersGraphics) <a href="https://twitter.com/boilersGraphics/status/1456105790377545732?ref_src=twsrc%5Etfw">November 4, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# 終わりに

いかがでしたでしょうか。Azure DevOpsはパブリックリポジトリなら無料で利用できる便利なCD/CIサービスなので、皆さんにも使ってもらえるといいなと思っています。

また、締めに宣伝なのですが、現在開発中のboiler's Graphicsも利用者が増えてくれるといいなと思っています。ユーザーの声が聞きたいですが、なかなか機会が得られないので困っています。

# 宣伝！！！

私はベクターグラフィックスドローイングツールboiler's Graphicsを開発しています。コンピューターグラフィックスツールShadowEyeの開発もしています。ユーザー大募集！！！！

https://github.com/dhq-boiler/boiler-s-Graphics

https://qiita.com/dhq_boiler/items/ac033ae363f1e0a80bea

https://qiita.com/dhq_boiler/items/dd2afcc36a5a205872eb

https://github.com/dhq-boiler/ShadowEye

https://qiita.com/dhq_boiler/items/b9fe777737528db1bfe7