高速リリース #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 高速リリース | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version_prefix: | |
| description: "バージョンの接頭辞。空欄なら build/Directory.Build.props の値を使う" | |
| required: false | |
| type: string | |
| default: "" | |
| version_suffix: | |
| description: "バージョンの接尾辞。空欄なら正式版として扱う" | |
| required: false | |
| type: string | |
| default: "" | |
| create_release: | |
| description: "GitHub Release を作成してパッケージを添付する" | |
| required: false | |
| type: boolean | |
| default: true | |
| concurrency: | |
| group: computeweave-release | |
| queue: max | |
| permissions: | |
| contents: read | |
| env: | |
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1" | |
| DOTNET_NOLOGO: "1" | |
| jobs: | |
| validate: | |
| name: バージョン検証 | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| shell: bash | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| version_prefix: ${{ steps.version.outputs.prefix }} | |
| version_suffix: ${{ steps.version.outputs.suffix }} | |
| is_prerelease: ${{ steps.version.outputs.is_prerelease }} | |
| steps: | |
| - name: ソースコードを取得 | |
| uses: actions/checkout@v4 | |
| - name: 実行元を検証 | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| run: | | |
| if [ "${GITHUB_REF_TYPE}" != "branch" ] || [ "${GITHUB_REF_NAME}" != "${DEFAULT_BRANCH}" ]; then | |
| echo "高速リリースは既定ブランチからだけ実行できます。" >&2 | |
| exit 1 | |
| fi | |
| - name: バージョン番号を決定 | |
| id: version | |
| env: | |
| VERSION_PREFIX: ${{ inputs.version_prefix }} | |
| VERSION_SUFFIX: ${{ inputs.version_suffix }} | |
| run: | | |
| PREFIX="${VERSION_PREFIX}" | |
| if [ -z "${PREFIX}" ]; then | |
| PREFIX=$(sed -n 's/.*<VersionPrefix>\(.*\)<\/VersionPrefix>.*/\1/p' build/Directory.Build.props | head -n 1) | |
| fi | |
| if [[ ! "${PREFIX}" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then | |
| echo "バージョンの接頭辞が正しくありません。" >&2 | |
| exit 1 | |
| fi | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "${PREFIX}" | |
| for COMPONENT in "${MAJOR}" "${MINOR}" "${PATCH}"; do | |
| if [ "${#COMPONENT}" -gt 5 ] || { [ "${#COMPONENT}" -eq 5 ] && (( 10#${COMPONENT} > 65534 )); }; then | |
| echo "バージョンの接頭辞がアセンブリバージョンの範囲を超えています。" >&2 | |
| exit 1 | |
| fi | |
| done | |
| SUFFIX="${VERSION_SUFFIX}" | |
| if [ -n "${SUFFIX}" ] && [[ ! "${SUFFIX}" =~ ^[0-9a-z-]+(\.[0-9a-z-]+)*$ ]]; then | |
| echo "バージョンの接尾辞が正しくありません。" >&2 | |
| exit 1 | |
| fi | |
| if [ -n "${SUFFIX}" ]; then | |
| IFS='.' read -ra IDENTIFIERS <<< "${SUFFIX}" | |
| for IDENTIFIER in "${IDENTIFIERS[@]}"; do | |
| if [[ "${IDENTIFIER}" =~ ^[0-9]+$ ]] && [[ ! "${IDENTIFIER}" =~ ^(0|[1-9][0-9]*)$ ]]; then | |
| echo "数値のプレリリース識別子に先頭の0は使用できません。" >&2 | |
| exit 1 | |
| fi | |
| done | |
| fi | |
| if [ -n "${SUFFIX}" ]; then | |
| VERSION="${PREFIX}-${SUFFIX}" | |
| IS_PRERELEASE=true | |
| else | |
| VERSION="${PREFIX}" | |
| IS_PRERELEASE=false | |
| fi | |
| if [ "${#VERSION}" -gt 64 ]; then | |
| echo "バージョンは64文字以内で指定してください。" >&2 | |
| exit 1 | |
| fi | |
| echo "prefix=${PREFIX}" >> "$GITHUB_OUTPUT" | |
| echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=${IS_PRERELEASE}" >> "$GITHUB_OUTPUT" | |
| - name: 決定したバージョン番号を確認 | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| IS_PRERELEASE: ${{ steps.version.outputs.is_prerelease }} | |
| run: | | |
| echo "バージョン : ${VERSION}" | |
| echo "プレリリース : ${IS_PRERELEASE}" | |
| echo "コミット : ${GITHUB_SHA}" | |
| pack: | |
| name: NuGet パッケージの作成 | |
| runs-on: windows-2022 | |
| needs: [validate] | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: ソースコードを取得 | |
| uses: actions/checkout@v4 | |
| - name: .NET SDK をセットアップ | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "10.x" | |
| # Platform と CI_RUNNER_DOTNET_TEST_PLATFORM はどちらも設定しない。 | |
| # ComputeWeave.Dxc はどちらも未設定のときだけネイティブライブラリを runtimes/<RID>/native へ配置する。 | |
| - name: パッケージを作成 | |
| env: | |
| VERSION_PREFIX: ${{ needs.validate.outputs.version_prefix }} | |
| VERSION_SUFFIX: ${{ needs.validate.outputs.version_suffix }} | |
| run: | | |
| for project in \ | |
| src/ComputeWeave.Core/ComputeWeave.Core.csproj \ | |
| src/ComputeWeave/ComputeWeave.csproj \ | |
| src/ComputeWeave.Dxc/ComputeWeave.Dxc.csproj \ | |
| src/ComputeWeave.D3D12MemoryAllocator/ComputeWeave.D3D12MemoryAllocator.csproj | |
| do | |
| dotnet pack "$project" -c Release \ | |
| "-p:VersionPrefix=${VERSION_PREFIX}" \ | |
| "-p:VersionSuffix=${VERSION_SUFFIX}" | |
| done | |
| - name: 作成されたパッケージ一覧を確認 | |
| run: ls -la ./artifacts/ | |
| - name: パッケージ内容を検証 | |
| shell: pwsh | |
| env: | |
| EXPECTED_VERSION: ${{ needs.validate.outputs.version }} | |
| EXPECTED_COMMIT: ${{ github.sha }} | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $expectedPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) | |
| [void]$expectedPackageIds.Add('ComputeWeave.Core') | |
| [void]$expectedPackageIds.Add('ComputeWeave') | |
| [void]$expectedPackageIds.Add('ComputeWeave.Dxc') | |
| [void]$expectedPackageIds.Add('ComputeWeave.D3D12MemoryAllocator') | |
| function Test-PackageSet([string]$filter, [string]$description) { | |
| $actualPackageIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) | |
| $packageFiles = @(Get-ChildItem -LiteralPath artifacts -File -Filter $filter) | |
| if ($packageFiles.Count -ne $expectedPackageIds.Count) { | |
| throw "$descriptionは4個でなければなりません。" | |
| } | |
| foreach ($packageFile in $packageFiles) { | |
| $archive = [System.IO.Compression.ZipFile]::OpenRead($packageFile.FullName) | |
| try { | |
| $nuspecEntries = @($archive.Entries | Where-Object { $_.FullName -like '*.nuspec' -and $_.FullName -notlike '*/*' }) | |
| if ($nuspecEntries.Count -ne 1) { | |
| throw "$($packageFile.Name)のnuspecを一意に特定できません。" | |
| } | |
| $reader = [System.IO.StreamReader]::new($nuspecEntries[0].Open()) | |
| try { | |
| [xml]$document = $reader.ReadToEnd() | |
| } | |
| finally { | |
| $reader.Dispose() | |
| } | |
| } | |
| finally { | |
| $archive.Dispose() | |
| } | |
| $namespaceManager = [System.Xml.XmlNamespaceManager]::new($document.NameTable) | |
| $namespaceManager.AddNamespace('n', $document.DocumentElement.NamespaceURI) | |
| $metadata = $document.SelectSingleNode('/n:package/n:metadata', $namespaceManager) | |
| $packageId = $metadata.SelectSingleNode('n:id', $namespaceManager).InnerText | |
| $packageVersion = $metadata.SelectSingleNode('n:version', $namespaceManager).InnerText | |
| $repositoryCommit = $metadata.SelectSingleNode('n:repository', $namespaceManager).GetAttribute('commit') | |
| if (!$expectedPackageIds.Contains($packageId) -or !$actualPackageIds.Add($packageId)) { | |
| throw "$($packageFile.Name)のパッケージIDが正しくありません。" | |
| } | |
| if ($packageVersion -cne $env:EXPECTED_VERSION) { | |
| throw "$($packageFile.Name)のバージョンが正しくありません。" | |
| } | |
| if ($repositoryCommit -cne $env:EXPECTED_COMMIT) { | |
| throw "$($packageFile.Name)のコミットが正しくありません。" | |
| } | |
| } | |
| if (!$actualPackageIds.SetEquals($expectedPackageIds)) { | |
| throw "必要な$descriptionが揃っていません。" | |
| } | |
| } | |
| Test-PackageSet '*.nupkg' 'NuGetパッケージ' | |
| Test-PackageSet '*.snupkg' 'NuGetシンボルパッケージ' | |
| - name: パッケージをアーティファクトとして保存 | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: artifacts/ | |
| retention-days: 90 | |
| if-no-files-found: error | |
| reserve-tag: | |
| name: リリースタグの予約 | |
| runs-on: ubuntu-latest | |
| needs: [validate, pack] | |
| permissions: | |
| contents: write | |
| defaults: | |
| run: | |
| shell: bash | |
| outputs: | |
| tag_already_existed: ${{ steps.reserve.outputs.tag_already_existed }} | |
| steps: | |
| - name: ソースコードを取得 | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: リリースタグを予約 | |
| id: reserve | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| RELEASE_TAG: v${{ needs.validate.outputs.version }} | |
| TARGET_SHA: ${{ github.sha }} | |
| run: | | |
| verify_reserved_tag() { | |
| git fetch --force origin "refs/tags/${RELEASE_TAG}:refs/computeweave/release-tag" | |
| RESERVED_SHA=$(git rev-parse "refs/computeweave/release-tag^{commit}") | |
| if [ "${RESERVED_SHA}" != "${TARGET_SHA}" ]; then | |
| echo "リリースタグは別のコミットに予約されています。" >&2 | |
| exit 1 | |
| fi | |
| } | |
| get_package_status() { | |
| curl --location --silent --show-error --output /dev/null --write-out '%{http_code}' \ | |
| "${PACKAGE_BASE}$1/${VERSION}/$1.nuspec" | |
| } | |
| get_package_commit() { | |
| curl --fail --location --silent --show-error "${PACKAGE_BASE}$1/${VERSION}/$1.nuspec" \ | |
| | python -c 'import sys, xml.etree.ElementTree as ET; root = ET.parse(sys.stdin).getroot(); print(next((node.get("commit", "") for node in root.iter() if node.tag.rsplit("}", 1)[-1] == "repository"), ""))' | |
| } | |
| git check-ref-format "refs/tags/${RELEASE_TAG}" | |
| git check-ref-format --branch "${DEFAULT_BRANCH}" | |
| git fetch --no-tags --force origin "refs/heads/${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}" | |
| if ! git merge-base --is-ancestor "${TARGET_SHA}" "refs/remotes/origin/${DEFAULT_BRANCH}"; then | |
| echo "リリース対象は既定ブランチに含まれていません。" >&2 | |
| exit 1 | |
| fi | |
| set +e | |
| git ls-remote --exit-code --refs --tags origin "refs/tags/${RELEASE_TAG}" > /dev/null | |
| TAG_STATUS=$? | |
| set -e | |
| if [ "${TAG_STATUS}" -eq 0 ]; then | |
| verify_reserved_tag | |
| TAG_ALREADY_EXISTED=true | |
| elif [ "${TAG_STATUS}" -eq 2 ]; then | |
| TAG_ALREADY_EXISTED=false | |
| else | |
| echo "リリースタグの存在を確認できませんでした。" >&2 | |
| exit 1 | |
| fi | |
| PACKAGE_BASE=$(curl --fail --location --silent --show-error https://api.nuget.org/v3/index.json \ | |
| | python -c 'import json, sys; resources = json.load(sys.stdin)["resources"]; print(next(item["@id"] for item in resources if item["@type"] == "PackageBaseAddress/3.0.0"))') | |
| VERSION="${RELEASE_TAG#v}" | |
| for PACKAGE_ID in computeweave.core computeweave computeweave.dxc computeweave.d3d12memoryallocator; do | |
| PACKAGE_STATUS=$(get_package_status "${PACKAGE_ID}") | |
| if [ "${PACKAGE_STATUS}" = "404" ]; then | |
| continue | |
| fi | |
| if [ "${PACKAGE_STATUS}" != "200" ]; then | |
| echo "NuGet.orgのパッケージ状態を確認できませんでした。" >&2 | |
| exit 1 | |
| fi | |
| if [ "${TAG_ALREADY_EXISTED}" != "true" ]; then | |
| echo "同じバージョンのパッケージがNuGet.orgに存在します。" >&2 | |
| exit 1 | |
| fi | |
| PACKAGE_COMMIT=$(get_package_commit "${PACKAGE_ID}") | |
| if [ "${PACKAGE_COMMIT}" != "${TARGET_SHA}" ]; then | |
| echo "NuGet.orgのパッケージは別のコミットから作成されています。" >&2 | |
| exit 1 | |
| fi | |
| done | |
| if [ "${TAG_ALREADY_EXISTED}" = "true" ]; then | |
| echo "tag_already_existed=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| git tag "${RELEASE_TAG}" "${TARGET_SHA}" | |
| if ! git push origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"; then | |
| verify_reserved_tag | |
| echo "tag_already_existed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag_already_existed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| publish-nuget: | |
| name: NuGet.org へ公開 | |
| runs-on: ubuntu-latest | |
| needs: [validate, pack, reserve-tag] | |
| environment: nuget-release | |
| permissions: | |
| id-token: write | |
| packages: write | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: .NET SDK をセットアップ | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: "10.x" | |
| - name: パッケージをダウンロード | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: ./artifacts | |
| - name: ダウンロードしたファイルを確認 | |
| run: ls -la ./artifacts/ | |
| - name: NuGet Trusted Publishing でログイン | |
| uses: NuGet/login@v1 | |
| id: nuget-login | |
| with: | |
| user: routersys | |
| - name: NuGet.org へパッケージを発行 | |
| env: | |
| ALLOW_DUPLICATES: ${{ needs.reserve-tag.outputs.tag_already_existed == 'true' || github.run_attempt > 1 }} | |
| EXPECTED_COMMIT: ${{ github.sha }} | |
| NUGET_API_KEY: ${{ steps.nuget-login.outputs.NUGET_API_KEY }} | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| run: | | |
| get_package_status() { | |
| curl --location --silent --show-error --output /dev/null --write-out '%{http_code}' \ | |
| "${PACKAGE_BASE}$1/${VERSION}/$1.nuspec" | |
| } | |
| get_package_commit() { | |
| curl --fail --location --silent --show-error "${PACKAGE_BASE}$1/${VERSION}/$1.nuspec" \ | |
| | python -c 'import sys, xml.etree.ElementTree as ET; root = ET.parse(sys.stdin).getroot(); print(next((node.get("commit", "") for node in root.iter() if node.tag.rsplit("}", 1)[-1] == "repository"), ""))' | |
| } | |
| PACKAGE_BASE=$(curl --fail --location --silent --show-error https://api.nuget.org/v3/index.json \ | |
| | python -c 'import json, sys; resources = json.load(sys.stdin)["resources"]; print(next(item["@id"] for item in resources if item["@type"] == "PackageBaseAddress/3.0.0"))') | |
| for PACKAGE_ID in computeweave.core computeweave computeweave.dxc computeweave.d3d12memoryallocator; do | |
| PACKAGE_STATUS=$(get_package_status "${PACKAGE_ID}") | |
| if [ "${PACKAGE_STATUS}" = "404" ]; then | |
| continue | |
| fi | |
| if [ "${PACKAGE_STATUS}" != "200" ]; then | |
| echo "NuGet.orgのパッケージ状態を確認できませんでした。" >&2 | |
| exit 1 | |
| fi | |
| if [ "${ALLOW_DUPLICATES}" != "true" ]; then | |
| echo "同じバージョンのパッケージがNuGet.orgに存在します。" >&2 | |
| exit 1 | |
| fi | |
| PACKAGE_COMMIT=$(get_package_commit "${PACKAGE_ID}") | |
| if [ "${PACKAGE_COMMIT}" != "${EXPECTED_COMMIT}" ]; then | |
| echo "NuGet.orgのパッケージは別のコミットから作成されています。" >&2 | |
| exit 1 | |
| fi | |
| done | |
| if [ "${ALLOW_DUPLICATES}" = "true" ]; then | |
| dotnet nuget push ./artifacts/*.nupkg \ | |
| --api-key "${NUGET_API_KEY}" \ | |
| --source https://api.nuget.org/v3/index.json \ | |
| --skip-duplicate | |
| else | |
| dotnet nuget push ./artifacts/*.nupkg \ | |
| --api-key "${NUGET_API_KEY}" \ | |
| --source https://api.nuget.org/v3/index.json | |
| fi | |
| if [ "${ALLOW_DUPLICATES}" = "true" ]; then | |
| for ATTEMPT in $(seq 1 60); do | |
| ALL_PACKAGES_VERIFIED=true | |
| for PACKAGE_ID in computeweave.core computeweave computeweave.dxc computeweave.d3d12memoryallocator; do | |
| PACKAGE_STATUS=$(get_package_status "${PACKAGE_ID}") | |
| if [ "${PACKAGE_STATUS}" = "404" ]; then | |
| ALL_PACKAGES_VERIFIED=false | |
| continue | |
| fi | |
| if [ "${PACKAGE_STATUS}" != "200" ]; then | |
| echo "NuGet.orgのパッケージ状態を確認できませんでした。" >&2 | |
| exit 1 | |
| fi | |
| PACKAGE_COMMIT=$(get_package_commit "${PACKAGE_ID}") | |
| if [ "${PACKAGE_COMMIT}" != "${EXPECTED_COMMIT}" ]; then | |
| echo "NuGet.orgのパッケージは別のコミットから作成されています。" >&2 | |
| exit 1 | |
| fi | |
| done | |
| if [ "${ALL_PACKAGES_VERIFIED}" = "true" ]; then | |
| exit 0 | |
| fi | |
| sleep 10 | |
| done | |
| echo "NuGet.orgで全パッケージを確認できませんでした。" >&2 | |
| exit 1 | |
| fi | |
| - name: GitHub Packages へも発行 | |
| env: | |
| GITHUB_PACKAGES_API_KEY: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| dotnet nuget push ./artifacts/*.nupkg \ | |
| --api-key "${GITHUB_PACKAGES_API_KEY}" \ | |
| --source https://nuget.pkg.github.com/routersys/index.json \ | |
| --skip-duplicate | |
| continue-on-error: true | |
| github-release: | |
| name: GitHub Release の作成 | |
| runs-on: ubuntu-latest | |
| needs: [validate, pack, publish-nuget] | |
| if: ${{ inputs.create_release }} | |
| permissions: | |
| contents: write | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: ソースコードを取得 | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: パッケージをダウンロード | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: nuget-packages | |
| path: ./artifacts | |
| - name: コミット履歴からリリースノートを生成 | |
| env: | |
| CURRENT_TAG: v${{ needs.validate.outputs.version }} | |
| CURRENT_SHA: ${{ github.sha }} | |
| VERSION: ${{ needs.validate.outputs.version }} | |
| run: | | |
| PREV=$(git tag --merged "${CURRENT_SHA}" --sort=-version:refname \ | |
| | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | |
| | grep -Fxv "${CURRENT_TAG}" \ | |
| | head -n 1 || true) | |
| if [ -n "${PREV}" ]; then | |
| LOG=$(git log "${PREV}..${CURRENT_SHA}" --pretty=format:"- %s (%h)" --no-merges --) | |
| else | |
| LOG=$(git log -n 30 --pretty=format:"- %s (%h)" --no-merges) | |
| fi | |
| { | |
| echo "### 変更内容" | |
| printf '%s\n' "${LOG:-変更履歴を取得できませんでした。}" | |
| echo "" | |
| echo "### インストール" | |
| echo '```sh' | |
| echo "dotnet add package ComputeWeave --version ${VERSION}" | |
| echo '```' | |
| echo "" | |
| echo "### NuGet" | |
| echo "https://www.nuget.org/packages/ComputeWeave/${VERSION}" | |
| echo "" | |
| echo "テストは実行していません。パッケージ作成時のビルドだけを実行しています。" | |
| } > release-notes.md | |
| # タグは GITHUB_TOKEN で作られるため release.yml は起動しない。 | |
| - name: GitHub Release を作成してパッケージを添付 | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| tag_name: v${{ needs.validate.outputs.version }} | |
| name: v${{ needs.validate.outputs.version }} | |
| target_commitish: ${{ github.sha }} | |
| body_path: release-notes.md | |
| prerelease: ${{ needs.validate.outputs.is_prerelease }} | |
| fail_on_unmatched_files: true | |
| files: | | |
| artifacts/*.nupkg | |
| artifacts/*.snupkg |