Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,31 @@ jobs:
-p:Version=${{ steps.version.outputs.version }}
--output nupkg

# NuGet's OPC writer gives the core-properties *.psmdcp part a random
# name on each pack run. Normalize that internal path before hashing or
# publishing so otherwise identical .nupkg/.snupkg archives compare
# byte-for-byte. See issue #2756 / DEVELOPER_GUIDE.md.
# NuGet の OPC writer は core-properties の *.psmdcp part に pack ごと
# ランダムな名前を付ける。hash / publish 前に内部 path を正規化し、
# それ以外が同一の .nupkg/.snupkg archive を byte-for-byte で比較可能にする。
# 詳細は issue #2756 / DEVELOPER_GUIDE.md。
- name: Normalize NuGet package metadata part names
run: dotnet run --project tools/CodeIndex.PackageNormalize -- nupkg/*.nupkg nupkg/*.snupkg

- name: Verify normalized NuGet package metadata part names
run: |
set -euo pipefail
for package in nupkg/*.nupkg nupkg/*.snupkg; do
unzip -Z1 "$package" | grep -qx 'package/services/metadata/core-properties/core-properties.psmdcp'
if unzip -Z1 "$package" \
| grep '^package/services/metadata/core-properties/' \
| grep -vx 'package/services/metadata/core-properties/core-properties.psmdcp'; then
echo "Unexpected non-canonical NuGet core-properties part in ${package}." >&2
unzip -Z1 "$package" >&2
exit 1
fi
done

- name: Verify packed NuGet package version
env:
VERSION: ${{ steps.version.outputs.version }}
Expand Down
15 changes: 15 additions & 0 deletions CodeIndex.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.Changelog", "tool
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.TestTelemetry", "tools\CodeIndex.TestTelemetry\CodeIndex.TestTelemetry.csproj", "{4EA94432-F249-46FC-8EB4-99F1EB9933A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeIndex.PackageNormalize", "tools\CodeIndex.PackageNormalize\CodeIndex.PackageNormalize.csproj", "{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -73,6 +75,18 @@ Global
{4EA94432-F249-46FC-8EB4-99F1EB9933A0}.Release|x64.Build.0 = Release|Any CPU
{4EA94432-F249-46FC-8EB4-99F1EB9933A0}.Release|x86.ActiveCfg = Release|Any CPU
{4EA94432-F249-46FC-8EB4-99F1EB9933A0}.Release|x86.Build.0 = Release|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Debug|x64.ActiveCfg = Debug|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Debug|x64.Build.0 = Debug|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Debug|x86.ActiveCfg = Debug|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Debug|x86.Build.0 = Debug|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Release|Any CPU.Build.0 = Release|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Release|x64.ActiveCfg = Release|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Release|x64.Build.0 = Release|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Release|x86.ActiveCfg = Release|Any CPU
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -82,5 +96,6 @@ Global
{A33D0351-ACAC-485B-9517-9ABD6A878ECD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{6A1D4E0B-24F8-4E8E-9E7A-28CC55D3F7AC} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{4EA94432-F249-46FC-8EB4-99F1EB9933A0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{1B9BA505-A3AE-4F70-9E7B-18EB4D3540E4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
19 changes: 19 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ The normal build/test workflow also runs `dotnet list src/CodeIndex/CodeIndex.cs

The release `dotnet publish` (per-RID) and `dotnet pack` (NuGet packaging) steps intentionally do **not** set `RestoreLockedMode=true`. Those steps run runtime-specific restores that legitimately add lock entries that did not exist at solution-restore time (e.g. `net8.0/<rid>` runtime sections, `Microsoft.NET.ILLink.Tasks` for trimming). They still consume locked versions because `RestorePackagesWithLockFile=true` from `Directory.Build.props` forces every restore on the machine to resolve through the lock file. The supply-chain guarantee for `Microsoft.Data.Sqlite` and its `SQLitePCLRaw.*` graph is enforced by the solution-level locked restore that runs first.

After `dotnet pack`, the release workflow runs
`dotnet run --project tools/CodeIndex.PackageNormalize -- nupkg/*.nupkg nupkg/*.snupkg`
before validating, hashing, or publishing NuGet artifacts. NuGet's OPC package
writer generates a random `package/services/metadata/core-properties/*.psmdcp`
part name on each pack run; the normalizer rewrites that part to
`package/services/metadata/core-properties/core-properties.psmdcp`, updates the
matching content-type and relationship references, and gives ZIP entries stable
timestamps. This is the package reproducibility boundary for `.nupkg` and
`.snupkg` archives (#2756).

When you intentionally update a dependency (or add a new direct `PackageReference`), regenerate the lock files locally and commit the diff in the same change:

```bash
Expand Down Expand Up @@ -2030,6 +2040,15 @@ CI(`.github/workflows/dotnet.yml`, `release.yml`, `codeql.yml`)はソリュ

release の `dotnet publish`(RID ごと)と `dotnet pack`(NuGet パッケージング)には意図的に `RestoreLockedMode=true` を設定していません。これらは runtime-specific な restore を走らせ、ソリューション restore 時には存在しなかった lock エントリ(`net8.0/<rid>` 等の runtime section や trimming 用の `Microsoft.NET.ILLink.Tasks`)を正当に追加します。それでも `Directory.Build.props` の `RestorePackagesWithLockFile=true` により、その実行マシン上の全 restore は lock ファイル経由で解決されるため版は固定されたままです。`Microsoft.Data.Sqlite` および `SQLitePCLRaw.*` グラフに対する supply-chain 保証は、先行する solution-level の locked restore で担保されます。

`dotnet pack` 後、release workflow は NuGet artifact の検証、hash、publish の前に
`dotnet run --project tools/CodeIndex.PackageNormalize -- nupkg/*.nupkg nupkg/*.snupkg`
を実行します。NuGet の OPC package writer は
`package/services/metadata/core-properties/*.psmdcp` part 名を pack ごとに
ランダム生成するため、normalizer はその part を
`package/services/metadata/core-properties/core-properties.psmdcp` に書き換え、
対応する content-type / relationship 参照も更新し、ZIP entry timestamp を固定します。
これが `.nupkg` / `.snupkg` archive の package 再現性境界です (#2756)。

依存を意図的に更新する(あるいは直接 `PackageReference` を追加する)場合は、ローカルで lock ファイルを再生成し、同じ変更でコミットしてください:

```bash
Expand Down
18 changes: 18 additions & 0 deletions changelog.d/unreleased/2756.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: fixed
issues:
- 2756
affected:
- .github/workflows/release.yml
- DEVELOPER_GUIDE.md
- tools/CodeIndex.PackageNormalize/PackageNormalizeCli.cs
- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
---

## English

- **NuGet release packages now normalize the random core-properties part name (#2756)** — the release workflow rewrites `.nupkg` and `.snupkg` archives so NuGet's generated `package/services/metadata/core-properties/*.psmdcp` entry uses a stable path before package validation, hashing, and publishing.

## 日本語

- **NuGet release package のランダムな core-properties part 名を正規化するようになりました (#2756)** — release workflow は package 検証、hash、publish の前に `.nupkg` / `.snupkg` archive を書き換え、NuGet が生成する `package/services/metadata/core-properties/*.psmdcp` entry を安定した path に固定します。
1 change: 1 addition & 0 deletions tests/CodeIndex.Tests/CodeIndex.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\CodeIndex\CodeIndex.csproj" />
<ProjectReference Include="..\..\tools\CodeIndex.Changelog\CodeIndex.Changelog.csproj" />
<ProjectReference Include="..\..\tools\CodeIndex.PackageNormalize\CodeIndex.PackageNormalize.csproj" />
<ProjectReference Include="..\..\tools\CodeIndex.TestTelemetry\CodeIndex.TestTelemetry.csproj" />
</ItemGroup>

Expand Down
103 changes: 103 additions & 0 deletions tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
using CodeIndex.PackageNormalize;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;

namespace CodeIndex.Tests;

public class ReleaseWorkflowTests
Expand Down Expand Up @@ -89,6 +94,58 @@ public void ReleaseWorkflow_ValidatesNuGetVersionBeforePublishing()
Assert.DoesNotContain("--skip-duplicate", workflow);
}

// Issue #2756: NuGet emits the core-properties OPC part with a random
// *.psmdcp entry name, so two otherwise identical pack runs can produce
// different .nupkg/.snupkg bytes. The release workflow normalizes that
// implementation detail before hashing and publishing.
// Issue #2756 対応: NuGet は core-properties の OPC part をランダムな
// *.psmdcp entry 名で生成するため、他が同一でも .nupkg/.snupkg の bytes が
// 揺れる。release workflow は hash / publish 前にその実装詳細を正規化する。
[Fact]
public void ReleaseWorkflow_NormalizesNuGetCorePropertiesBeforePublishing()
{
var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "release.yml"));

Assert.Contains("Normalize NuGet package metadata part names", workflow);
Assert.Contains("dotnet run --project tools/CodeIndex.PackageNormalize --", workflow);
Assert.Contains("nupkg/*.nupkg nupkg/*.snupkg", workflow);
Assert.Contains("core-properties/core-properties.psmdcp", workflow);
}

[Fact]
public void PackageNormalizer_RewritesRandomCorePropertiesPartDeterministically()
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RewritesRandomCorePropertiesPartDeterministically));
try
{
var packageA = Path.Combine(projectRoot, "a.nupkg");
var packageB = Path.Combine(projectRoot, "b.nupkg");

CreateMinimalNuGetPackage(packageA, "a1b2c3.psmdcp");
CreateMinimalNuGetPackage(packageB, "f9e8d7.psmdcp");

PackageCorePropertiesNormalizer.NormalizePackage(packageA);
PackageCorePropertiesNormalizer.NormalizePackage(packageB);

Assert.Equal(
Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(packageA))),
Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(packageB))));

using var archive = ZipFile.OpenRead(packageA);
Assert.Contains(archive.Entries, entry => entry.FullName == PackageCorePropertiesNormalizer.CanonicalCorePropertiesPath);
Assert.DoesNotContain(archive.Entries, entry => entry.FullName.EndsWith("a1b2c3.psmdcp", StringComparison.Ordinal));

var contentTypes = ReadZipEntryText(archive, "[Content_Types].xml");
var relationships = ReadZipEntryText(archive, "_rels/.rels");
Assert.Contains("/package/services/metadata/core-properties/core-properties.psmdcp", contentTypes);
Assert.Contains("/package/services/metadata/core-properties/core-properties.psmdcp", relationships);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void ReleaseWorkflow_PublishesOfficialContainerImage()
{
Expand Down Expand Up @@ -124,4 +181,50 @@ private static string GetRepositoryRoot()

throw new InvalidOperationException("Could not locate repository root / リポジトリルートを特定できませんでした");
}

private static void CreateMinimalNuGetPackage(string packagePath, string corePropertiesFileName)
{
var corePropertiesPath = $"package/services/metadata/core-properties/{corePropertiesFileName}";
using var archive = ZipFile.Open(packagePath, ZipArchiveMode.Create);

WriteZipEntry(archive, "[Content_Types].xml", $"""
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<Override PartName="/{corePropertiesPath}" ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
</Types>
""");
WriteZipEntry(archive, "_rels/.rels", $"""
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="R1" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="/{corePropertiesPath}" />
</Relationships>
""");
WriteZipEntry(archive, "cdidx.nuspec", """
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata><id>cdidx</id><version>1.0.0</version></metadata>
</package>
""");
WriteZipEntry(archive, corePropertiesPath, """
<?xml version="1.0" encoding="utf-8"?>
<coreProperties xmlns="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" />
""");
}

private static void WriteZipEntry(ZipArchive archive, string entryName, string content)
{
var entry = archive.CreateEntry(entryName);
using var stream = entry.Open();
using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
writer.Write(content.Replace("\r\n", "\n", StringComparison.Ordinal));
}

private static string ReadZipEntryText(ZipArchive archive, string entryName)
{
var entry = archive.GetEntry(entryName) ?? throw new InvalidOperationException($"Missing ZIP entry: {entryName}");
using var stream = entry.Open();
using var reader = new StreamReader(stream, Encoding.UTF8);
return reader.ReadToEnd();
}
}
8 changes: 7 additions & 1 deletion tests/CodeIndex.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,9 @@
"codeindex.changelog": {
"type": "Project"
},
"codeindex.packagenormalize": {
"type": "Project"
},
"codeindex.testtelemetry": {
"type": "Project"
}
Expand Down Expand Up @@ -2417,9 +2420,12 @@
"codeindex.changelog": {
"type": "Project"
},
"codeindex.packagenormalize": {
"type": "Project"
},
"codeindex.testtelemetry": {
"type": "Project"
}
}
}
}
}
11 changes: 11 additions & 0 deletions tools/CodeIndex.PackageNormalize/CodeIndex.PackageNormalize.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

</Project>
Loading
Loading