Skip to content

高速リリース

高速リリース #13

Workflow file for this run

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