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
22 changes: 12 additions & 10 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,23 +127,21 @@ jobs:
with:
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: never
runCmd: pnpm tsx scripts/tasks/features/dotnet/publishImage.ts --component ${{ matrix.component }} --version ${{ matrix.version }} --channel "$CHANNEL" --install-dir .ci/feature
runCmd: pnpm tsx scripts/tasks/features/dotnet/publishImage.ts --component ${{ matrix.component }} --version ${{ matrix.key }} --channel "$CHANNEL" --install-dir .ci/feature
env: |
CHANNEL=${{ github.head_ref || github.ref_name }}
GH_TOKEN=${{ github.token }}

commit-tags:
release-tags:
needs:
- plan
- image
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.plan.result == 'success' && needs.image.result == 'success' && needs.plan.outputs.matrix != ''
- verify
if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.image.result == 'success' || needs.image.result == 'skipped') && needs.verify.result == 'success' }}
permissions:
contents: write
packages: read
packages: write
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v7
Expand All @@ -155,24 +153,28 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ github.token }}

- name: Tag the published pair
- name: Apply the release tags
uses: devcontainers/ci@v0.3
with:
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: never
runCmd: pnpm tsx scripts/tasks/features/dotnet/tagPublished.ts --component ${{ matrix.component }} --version ${{ matrix.version }}
runCmd: pnpm tsx scripts/tasks/features/dotnet/releaseTags.ts --channel "$CHANNEL" ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && '--git-tags' || '' }}
env: |
CHANNEL=${{ github.head_ref || github.ref_name }}
GH_TOKEN=${{ github.token }}

verify:
needs:
- plan
- image
if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.image.result == 'success' || needs.image.result == 'skipped') }}
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- ubuntu-24.04-arm
scenario:
- sdk
- runtime
Expand Down
25 changes: 15 additions & 10 deletions .github/workflows/ripgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,23 +69,21 @@ jobs:
with:
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: never
runCmd: pnpm tsx scripts/tasks/features/ripgrep/publishImage.ts --version ${{ matrix.version }} --channel "$CHANNEL"
runCmd: pnpm tsx scripts/tasks/features/ripgrep/publishImage.ts --version ${{ matrix.key }} --channel "$CHANNEL"
env: |
CHANNEL=${{ github.head_ref || github.ref_name }}
GH_TOKEN=${{ github.token }}

commit-tags:
release-tags:
needs:
- plan
- image
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.plan.result == 'success' && needs.image.result == 'success' && needs.plan.outputs.matrix != ''
- verify
if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.image.result == 'success' || needs.image.result == 'skipped') && needs.verify.result == 'success' }}
permissions:
contents: write
packages: read
packages: write
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v7
Expand All @@ -97,21 +95,28 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ github.token }}

- name: Tag the published version
- name: Apply the release tags
uses: devcontainers/ci@v0.3
with:
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: never
runCmd: pnpm tsx scripts/tasks/features/ripgrep/tagPublished.ts --version ${{ matrix.version }}
runCmd: pnpm tsx scripts/tasks/features/ripgrep/releaseTags.ts --channel "$CHANNEL" ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && '--git-tags' || '' }}
env: |
CHANNEL=${{ github.head_ref || github.ref_name }}
GH_TOKEN=${{ github.token }}

verify:
needs:
- plan
- image
if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.image.result == 'success' || needs.image.result == 'skipped') }}
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- ubuntu-24.04-arm
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v7
Expand Down
24 changes: 13 additions & 11 deletions .github/workflows/uv.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Uv"
name: "uv"

on:
pull_request:
Expand Down Expand Up @@ -127,23 +127,21 @@ jobs:
with:
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: never
runCmd: pnpm tsx scripts/tasks/features/uv/publishImage.ts --component ${{ matrix.component }} --version ${{ matrix.version }} --channel "$CHANNEL" --install-dir .ci/feature
runCmd: pnpm tsx scripts/tasks/features/uv/publishImage.ts --component ${{ matrix.component }} --version ${{ matrix.key }} --channel "$CHANNEL" --install-dir .ci/feature
env: |
CHANNEL=${{ github.head_ref || github.ref_name }}
GH_TOKEN=${{ github.token }}

commit-tags:
release-tags:
needs:
- plan
- image
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.plan.result == 'success' && needs.image.result == 'success' && needs.plan.outputs.matrix != ''
- verify
if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.image.result == 'success' || needs.image.result == 'skipped') && needs.verify.result == 'success' }}
permissions:
contents: write
packages: read
packages: write
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- name: Checkout (GitHub)
uses: actions/checkout@v7
Expand All @@ -155,24 +153,28 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ github.token }}

- name: Tag the published pair
- name: Apply the release tags
uses: devcontainers/ci@v0.3
with:
cacheFrom: ghcr.io/${{ github.repository }}/devcontainer
push: never
runCmd: pnpm tsx scripts/tasks/features/uv/tagPublished.ts --component ${{ matrix.component }} --version ${{ matrix.version }}
runCmd: pnpm tsx scripts/tasks/features/uv/releaseTags.ts --channel "$CHANNEL" ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && '--git-tags' || '' }}
env: |
CHANNEL=${{ github.head_ref || github.ref_name }}
GH_TOKEN=${{ github.token }}

verify:
needs:
- plan
- image
if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.image.result == 'success' || needs.image.result == 'skipped') }}
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
runner:
- ubuntu-latest
- ubuntu-24.04-arm
scenario:
- binaries
- python
Expand Down
35 changes: 32 additions & 3 deletions features/dotnet/Build/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ sealed class Artifact

sealed class ComponentVersion
{
[JsonPropertyName("aliases")]
public List<string> Aliases { get; init; } = [];

[JsonExtensionData]
public Dictionary<string, JsonElement> Rids { get; init; } = [];
}
Expand All @@ -32,3 +29,35 @@ sealed class FeatureConfig
[JsonPropertyName("components")]
public required Dictionary<string, ComponentConfig> Components { get; init; }
}

sealed record PayloadSection(string Source, string Component, string Key, ComponentVersion Entry);

internal static class Config
{
internal static FeatureConfig Parse(string json, string source)
{
return JsonSerializer.Deserialize<FeatureConfig>(json)
?? throw new BuildException($"{source} deserialized to null.");
}

internal static PayloadSection Section(FeatureConfig config, string source, string component, string key)
{
if (!config.Components.TryGetValue(component, out ComponentConfig? componentConfig))
{
throw new BuildException(
$"component '{component}' not found in {source}; available components: "
+ $"{string.Join(", ", config.Components.Keys)}."
);
}

if (!componentConfig.Versions.TryGetValue(key, out ComponentVersion? entry))
{
throw new BuildException(
$"version '{key}' not found for component '{component}' in {source}; "
+ $"available versions: {string.Join(", ", componentConfig.Versions.Keys)}."
);
}

return new PayloadSection(source, component, key, entry);
}
}
2 changes: 1 addition & 1 deletion features/dotnet/Build/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[assembly: SupportedOSPlatform("linux")]

Option<FileInfo> configOption = new("--config") { Required = true, Description = "Path to the feature config.json." };
Option<string> configOption = new("--config") { Required = true, Description = "Path to the feature config.json." };
Option<string> componentOption = new("--component")
{
Required = true,
Expand Down
44 changes: 7 additions & 37 deletions features/dotnet/Build/Resolution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,26 @@ namespace DevcontainerConfig.Dotnet.Build;

internal static class Resolution
{
internal static async Task<Artifact> ResolveAsync(
FileInfo config,
string component,
string version,
string arch,
CancellationToken cancellationToken
)
internal static Artifact Resolve(PayloadSection section, string arch)
{
string rid = $"linux-{MapArch(arch)}";
FeatureConfig feature = await LoadAsync(config, cancellationToken);

if (!feature.Components.TryGetValue(component, out ComponentConfig? componentConfig))
{
throw new BuildException(
$"component '{component}' not found in {config.FullName}; available components: "
+ $"{string.Join(", ", feature.Components.Keys)}."
);
}

if (!componentConfig.Versions.TryGetValue(version, out ComponentVersion? componentVersion))
{
throw new BuildException(
$"version '{version}' not found for component '{component}' in {config.FullName}; "
+ $"available versions: {string.Join(", ", componentConfig.Versions.Keys)}."
);
}

if (!componentVersion.Rids.TryGetValue(rid, out JsonElement ridEntry))
if (!section.Entry.Rids.TryGetValue(rid, out JsonElement ridEntry))
{
throw new BuildException(
$"rid '{rid}' not found for component '{component}' version '{version}' in "
+ $"{config.FullName}; available rids: {string.Join(", ", componentVersion.Rids.Keys)}."
$"rid '{rid}' not found for component '{section.Component}' version '{section.Key}' in "
+ $"{section.Source}; available rids: {string.Join(", ", section.Entry.Rids.Keys)}."
);
}

Artifact artifact =
ridEntry.Deserialize<Artifact>()
?? throw new BuildException($"artifact for rid '{rid}' is null in {config.FullName}.");
?? throw new BuildException($"artifact for rid '{rid}' is null in {section.Source}.");

if (artifact.Sha512.Length != 128 || !artifact.Sha512.All(char.IsAsciiHexDigit))
{
throw new BuildException(
$"sha512 for component '{component}' version '{version}' rid '{rid}' is not 128 hex " + "characters."
$"sha512 for component '{section.Component}' version '{section.Key}' rid '{rid}' is not 128 hex "
+ "characters."
);
}

Expand All @@ -62,11 +39,4 @@ static string MapArch(string arch)
_ => throw new BuildException($"arch '{arch}' is not one of: amd64, arm64."),
};
}

static async Task<FeatureConfig> LoadAsync(FileInfo config, CancellationToken cancellationToken)
{
await using FileStream stream = config.OpenRead();
return await JsonSerializer.DeserializeAsync<FeatureConfig>(stream, cancellationToken: cancellationToken)
?? throw new BuildException($"{config.FullName} deserialized to null.");
}
}
11 changes: 7 additions & 4 deletions features/dotnet/Build/Staging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ namespace DevcontainerConfig.Dotnet.Build;
internal static class Staging
{
internal static async Task RunAsync(
FileInfo config,
string config,
string component,
string version,
string key,
string arch,
DirectoryInfo outDir,
CancellationToken cancellationToken
)
{
Artifact artifact = await Resolution.ResolveAsync(config, component, version, arch, cancellationToken);
string json = await File.ReadAllTextAsync(config, cancellationToken);
FeatureConfig feature = Config.Parse(json, config);
PayloadSection section = Config.Section(feature, config, component, key);
Artifact artifact = Resolution.Resolve(section, arch);
string tempPath = Path.Join(Path.GetTempPath(), $"dotnet-build-{Guid.NewGuid():N}");
try
{
await Http.DownloadAsync(tempPath, artifact.Url, cancellationToken);
await Checksum.VerifyAsync(tempPath, artifact.Sha512, cancellationToken);
await Tarball.ExtractAsync(tempPath, outDir, cancellationToken);
Layout.Assert(outDir.FullName, component, version);
Layout.Assert(outDir.FullName, section.Component, section.Key);
}
finally
{
Expand Down
Loading
Loading