ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

Azure DevOps(Boards)のWork ItemをOrganization間で移行する

はじめに

あまり無いことだとは思いますが、Azure DevOpsの情報を別Organization(企業)へ移行する必要が生じました。 Azure Reposのソースコードの移行はGitなので簡単に出来たのですが、Azure BoardsのWork Itemについてはなかなか情報が見つかりませんでした。 Azure DevOps CLIを使ってみたり、移行元でQueriesを使い全WorkItemをExcelにエクスポートし、移行先でインポートする方法などもありましたが、添付ファイル(Attachment)の移行が出来なかったり等、色々問題がありそうなので採用出来ず。 いろいろ調べた結果、Azure DevOps Migration ToolsというOSSツールを使用することである程度の移行が出来たため、手順を記録しておきます。

これらの手順を実行する場合は自己責任にてお願いします。

Azure DevOps Migration Tools とは

名前の通り、Azure DevOpsの移行用ツールです。C#で記述されているオープンソースのツールになります。移行対象はAzure Boards(Work Item)がメインですが、Azure Test Plansの移行もbeta版ですが提供されているようです。(2020年10月現在)

Windows専用ツールであり、Windows向けパッケージマネージャであるChocolateyからインストール出来ます。

公式サイトはこちらです。

また、GitHubリポジトリはこちらです。

注意点

このツールは公式サイトに以下の記載がある通り、初心者向けのツールではありません。Azure DevOpsの構造に精通していないと運用は難しいかと思います。

WARNING: This tool is not designed for a novice. This tool was developed to support the scenarios below, and the edge cases that have been encountered by the 30+ contributors from around the Azure DevOps community. You should be comfortable with the TFS/Azure DevOps object model, as well as debugging code in Visual Studio.

こんな記事を書いている私ですが、およそ精通してるとは言えないため、使い方が誤っている可能性は大いにあります...ご了承ください...

何が移行出来た?

  • タイトル
  • State(New, Active, Closed, etc.)
  • 親子関係
    • ただし、移行出来たものもあれば何故か出来なかった(Unparentになった)ものも...
  • コメント
  • 添付ファイル(Attachment)
  • Area
  • Iteration
  • Tags

etc...おおよそのものは移行できるのではないかと思います。

何が移行できなかった?

  • アサイン先(Assigned To)
    • 移行元のユーザのまま。(当たり前ですが)
    • あとでQueriesを使い移行元ユーザを抽出 → 一括で変更は出来たため、そこまで問題無し。
  • ブランチ、Pull Request等のLink情報
    • 移行自体は出来るけど、移行先のLink対象と一致せずエラーになり、結局は自分で設定し直すことに。

設定次第ではもしかしたら出来たかもしれないけど、私の知識では不可能でした。

移行手順

移行先プロジェクトのWork Itemを拡張する

移行先のWork Itemに移行状態を記載するというツールの仕様上、これを記載するためにWork ItemにCustom Fieldを追加する必要があります(※移行先のみ)。 まずはこのCustom Fieldを追加します。

Work Itemを拡張するためのInherited Processを作成する

  • Organization Settings > Boards > Processを開く。
  • Processesタブから、ベースとなるProcess(私の場合は”Agile")の右側の"..."をクリックし、"Create inherited process"を開く。
  • Process name, Descriptionを記述し、”Create process"を選択し、新しいProcessを作成する。

作成したInherited ProcessにField追加

  • 作成したProcessを選択し、"Bug"を選択する。
  • "Layout"タブから"New field"を選択する。
  • 表示されたポップアップで"Create a field"を選択し、以下を入力して"Add field"を選択する。
    • Name: "ReflectedWorkItemID"
    • Type: "Text (single line)"
  • 以上をWork Item Type全て(Epic, Feature, etc.)に適用する(何故か"Bug"に追加しただけで動作してしまったけど、恐らく全部に適用した方が良さそう?)

ここで設定した名前("ReflectedWorkItemID")は後述する設定ファイルに記述する名前になります。

移行先プロジェクトのProcessを作成したInherited Processに変更する

  • Organization Settings > Boards > Processを開く。
  • Processesタブから、移行先のプロジェクトが適用しているProcessを開く。
  • Projectsタブを開き、プロジェクト名右側の"..."をクリックし、"→ Change process"を選択する。
  • 新しく作成したProcessを選択し、保存する。

ちなみにProcessについては以下の記事が分かりやすく書いていただいており参考になります。
https://qiita.com/mstakaha1113/items/2c857e85ed6203d93028

Azure DevOps Migration Toolsのインストール

以下の二通りがあるようです。

  • (recommended)Install from Chocolatey
  • Download the latest release from GitHub and unzip

私は前者のChocolateyを使用しました。

  1. Chocolateyのインストール(https://chocolatey.org/install)
  2. コマンドプロンプトから以下を実行
    • choco install vsts-sync-migrator

成功するとc:\tools\vstssyncmigration\にインストールされ、この中のmigrate.exeを使用して移行します。

設定ファイルの作成

基本的には公式サイトのGetting Startedにサンプルの設定ファイルを含めて記載されています。

  1. コマンドプロンプトを開き、c:\tools\vstssyncmigration\へ移動する
  2. ./migration.exe initを実行
  3. configuration.jsonが作成されるので、それを編集する

私の場合はサンプルの設定ファイルを色々いじった結果、以下のようになりました。

{
  "Version": "11.0",
  "TelemetryEnableTrace": false,
  "workaroundForQuerySOAPBugEnabled": false,
  "Source": {
    "Collection": "https://dev.azure.com/{移行元Organization}",
    "Project": "{移行元プロジェクト名}",
    "ReflectedWorkItemIDFieldName": "ReflectedWorkItemID",
    "AllowCrossProjectLinking": false,
    "PersonalAccessToken": "{移行元で作成したPersonal Access Token(PAT)}",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "Target": {
    "Collection": "https://dev.azure.com/{移行先Organization}",
    "Project": "{移行先プロジェクト名}",
    "ReflectedWorkItemIDFieldName": "ReflectedWorkItemID",
    "AllowCrossProjectLinking": false,
    "PersonalAccessToken": "{移行元で作成したPersonal Access Token(PAT)}",
    "LanguageMaps": {
      "AreaPath": "Area",
      "IterationPath": "Iteration"
    }
  },
  "GitRepoMapping": null,
  "Processors": [
    {
      "ObjectType": "NodeStructuresMigrationConfig",
      "PrefixProjectToNodes": false,
      "Enabled": true
    },
    {
      "ObjectType": "WorkItemMigrationConfig",
      "QueryBit": "AND  [Microsoft.VSTS.Common.ClosedDate] = '' AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan')",
      "OrderBit": "[System.ChangedDate] desc",
      "Enabled": true,
      "LinkMigration": true,
      "AttachmentMigration": true,
      "AttachmentWorkingPath": "c:\\temp\\WorkItemAttachmentWorkingFolder\\",
      "FixHtmlAttachmentLinks": false,
      "SkipToFinalRevisedWorkItemType": false,
      "WorkItemCreateRetryLimit": 5,
      "FilterWorkItemsThatAlreadyExistInTarget": true,
      "PauseAfterEachWorkItem": false,
      "AttachmentMazSize": 480000000,
      "CollapseRevisions": false,
      "LinkMigrationSaveEachAsAdded": false
    }
  ]
}
  • "Version"はインストールしたAzure DevOps Migration Toolsのバージョンを記述
    • ./migration.exe --versionで確認
  • "Source"以下に移行元の情報を記述
  • "Target"以下に移行先の情報を記述
  • "ReflectedWorkItemIDFieldName"にはWork Itemを拡張した際に追加したCustom Fieldに指定した名前("ReflectedWorkItemID")を記述
  • "FieldMaps", "WorkItemTypeDefinition"は不要そうだったので削除
    • 移行元と移行先でFieldやWorkItemTypeが異なる場合のマッピング設定?
  • "Processors"内にそれぞれの"Enabled"はtrueに変更(trueにしないとそのProcessorが起動しない)

移行開始

以下のコマンドで移行ツールを起動します。

./migration.exe --config configuration.json

途中、移行元/移行先のMicrosoftアカウントへのログインが表示されるかもしれませんが、必要に応じてログインすれば続行するかと思います。

まとめ

私は以上の手順で、不完全ではありますが移行が出来ました。

最初に書きましたが、私自身あまりAzure DevOpsに詳しくなく、他のプロジェクトでも同様に移行できるかは全く不明なため、これらの手順を実行する場合はあくまで自己責任にてお願いします。

iOS 端末のモデル識別子を取得する

開発中のアプリで端末の機種を判定したかったのですが、 UIDevice.current.modeliPhoneiPod touch としか返さないため、もう少し詳細な情報を取得する方法を調べました。

今回のコードにより iPhone12,3 等の形式の識別子を取得することが出来ます。

実装コード

今回はUIDeviceのextensionとして実装してみます。

extension UIDevice {
  var modelIdentifier: String {
      var systemInfo = utsname()
      uname(&systemInfo)
      let machineMirror = Mirror(reflecting: systemInfo.machine)
      let identifier = machineMirror.children.reduce("") { identifier, element in
          guard let value = element.value as? Int8, value != 0 else { return identifier }
          return identifier + String(UnicodeScalar(UInt8(value)))
      }
      return identifier
  }
}

検索すると、ここから更にモデル名(iPhone12,3iPhone 11 Pro)にマッピングする記事を見かけますが、個人的にはこの状態で十分だったため止めておきます。

マッピング自体は以下が参考になるかと思います。

Models - The iPhone Wiki (Identifier列を参照)

Expressサーバ上にGatsbyサイトをデプロイしてみた

最近Gatsbyをチマチマ触っているので、Expressサーバ上に載せてみました。 基本的にはExpressの静的ファイル配布機能を使用しているだけですが。

※モバイル畑なので、Web関連はあまり詳しくありません。

プロジェクトの作成

$ gatsby new foo
$ cd foo
$ npm install express

(余談ですが、npm installに --save オプションって不要になってたんですね。)

サーバ側の実装

プロジェクト直下にindex.jsを作成

$ touch index.js

index.jsの編集

const express = require("express")
const app = express()
const port = 3000

// `gatsby build` の結果がpublicディレクトリに出力される
app.use(express.static("public/"))

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`)
})

起動スクリプト

package.jsonの編集

...
  "scripts": {
    "prestart": "gatsby build",
    "start": "node index.js"
  }
...

サーバ起動

$ npm start

以下にアクセスして、サイトが表示されることを確認。

http://localhost:3000

設定アプリ(Settings Bundle)にUUIDを表示する方法

たまにですが、設定アプリに「ユーザーID」といった項目名でUUIDを表示しているアプリを見かけます。 開発中のアプリでも端末識別用のUUIDを設定アプリに表示する必要があったため方法を調べてみたのですが、 予め決まっている固定値を表示する方法がほとんどで、アプリ側で生成したUUIDを表示させるといった情報が意外と見つからなかったため、自分が試して実現した方法を書き記しておこうと思います。

結論

Settings Bundleの値はUserDefaultsで取得出来る、ということはアプリ側から書き込みすれば設定アプリに表示出来るのではと考え、予め設定項目だけ作成しておき、アプリ起動時にUserDefaultsに書き込むことで表示することが出来ました。

※ただ、当然ながらアプリインストール直後で起動していない時点では書き込みが出来ておらずデフォルト値が表示されます。この辺りの解決方法は見つかっていません。

実装手順

Settings.bundle を作成

通常通り、ターゲットのルートディレクトリにSettings.bundleを作成します。

公式の情報はこの辺りでしょうか。 → Implementing an iOS Settings Bundle

設定値の作成

以下のように設定値をRoot.plistに作成します。

  • Item 0: Group
    • Title: "UUID"
  • Item 1: Title
    • Default Value: "xxx"
    • Identifier: "uuid"

f:id:tommy10344:20200717000525p:plain

  • UUIDは長い文字列のため、項目名は専用のGroup(Item 0)のTitleに表示するようにし、Item 1にTitleを表示させずに横幅を伸ばすようにしています。
  • Default Valueは何かしら記載しておかないと項目自体が設定アプリに表示されないようです。(半角スペース(" ")でも項目の表示はされましたが、これ良いのかな...)
  • Item 1のIdentifierの値は後ほどUserDefaultsに書き込む際に使用します。

UserDefaultsに書き込み

今回はUUIDとして UIDevice.current.identifierForVendor を使用しています。

identifierForVendor

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    ...
    
    // UUID取得
    let uuid = UIDevice.current.identifierForVendor!
    // UserDefaultsに書き込み。
    // Settings.bundleの"Identifier"で指定した値をKeyにして書き込む。
    UserDefaults.standard.set(uuid.uuidString, forKey: "uuid")
    
    ...
}

このようにしてアプリを起動後、設定アプリを確認するとUUIDが表示されているかと思います。

f:id:tommy10344:20200717002203p:plain

まとめ

所々微妙な部分があるのですが、この方法でとりあえずは実現出来ました。

またここまでの内容を簡単なサンプルコードとしてGitHubにアップしています。

GitHub - tommy10344/SettingsBundleUUID

TestFlightで「輸出コンプライアンスがありません」を表示されないようにする方法

TestFlightにアプリをアップロードした際に表示されるこの質問、fastlaneでアップロードする際にも表示され、自動でアプリが配信されなくなってしまうのですが、予めプロジェクト設定を追加しておくことで表示されないようにすることが可能です。

設定方法

f:id:tommy10344:20200429020942p:plain

Info.plist から ITSAppUsesNonExemptEncryption (表示名: App Uses Non-Exempt Encrption) を NO に設定します。 この設定により、暗号化を使用していない、もしくは免除の対象になる暗号化のみを使用していることを示します。 (HTTPSや、OSに組み込まれた標準の暗号化については免除の対象になるみたいです)

免除の対象にならないような独自の暗号化を含む場合は「YES」を設定し、輸出コンプライアンス書類を提出してAppleから受け取ったキーを ITSEncryptionExportComplianceCode に設定する必要があるようです。

参考

SwiftUIで共有メニューを表示する

アプリのコンテンツをメールやSNS等の外部サービスに渡す共有メニューをSwiftUIで表示する方法です。

f:id:tommy10344:20191105213901p:plain

共有メニュー表示用Viewの定義

UIKitではUIActivityViewControllerを使用して表示するこの共有メニューですが、現状SwiftUIで特に専用のViewがある訳ではないようです。 そのため、UIViewControllerRepresentableプロトコルに準拠させる形でUIActivityViewControllerをSwiftUI上に表示出来るようにします。

struct ActivityView: UIViewControllerRepresentable {

    let activityItems: [Any]
    let applicationActivities: [UIActivity]?

    func makeUIViewController(
        context: UIViewControllerRepresentableContext<ActivityView>
    ) -> UIActivityViewController {
        return UIActivityViewController(
            activityItems: activityItems,
            applicationActivities: applicationActivities
        )
    }

    func updateUIViewController(
        _ uiViewController: UIActivityViewController,
        context: UIViewControllerRepresentableContext<ActivityView>
    ) {
        // Nothing to do
    }
}

共有メニュー表示

表示の際はsheet()を使用し、先ほど作成したUIViewControllerRepresentableプロトコルに準拠したViewを返すようにすれば共有メニューが表示されます。

@State private var showActivityView: Bool = false

var body: some View {
    ...
    Button(action: {
        self.showActivityView = true
    }) {
        Image(systemName: "square.and.arrow.up")
    }
    .sheet(isPresented: self.$showActivityView) {
        ActivityView(
            activityItems: ["abc"],
            applicationActivities: nil
        )
    }
    ...
}

参考

iOSアプリでSystem Imageを使用する(iOS13以降)

iOS13ではSystem ImageとしてSF Symbolsというものが追加され、UIKitのUIImageやSwiftUIのImageから使用出来るようになりました。

使用方法

それぞれ、以下のようにして名前を指定して表示します。

  • UIKit(UIImage)
UIImage(systemName: "xxx")
  • SwiftUI(Image)
Image(systemName: "xxx")

名前の確認方法

ここに指定する名前は以下のリンクからSF Symbolsアプリをダウンロードし、Macにインストールする事で確認出来ます。

Apple Design Resources - Apple Developer

f:id:tommy10344:20191103141401p:plain

SF Symbolsアプリに表示される各アイコンの下にある名前を引数に指定する事で、そのアイコンを使用する事が出来ます。

例えば以下のアイコンの場合、

f:id:tommy10344:20191103141421p:plain

SwiftUIでは以下のような指定になります。

Image(systemName: "square.and.arrow.up")

参考