Skip to content

Commit 276cb7d

Browse files
authored
Run the docker-compose version tagging update manually instead of using Dependabot (#8506)
## Summary of changes - Dependabot sucks - I guess we'll do it ourselves ## Reason for change tl;dr; [Dependabot continues to be terrible at its job](https://github.com/DataDog/dd-trace-dotnet/actions/runs/24776168750/job/72494856573#step:3:1525) <details><summary>🤖 explanation</summary> <p> ``` Root cause The dependabot job fails on the first image it checks (dotnet/aspnet 2.1-stretch-slim) with DockerRegistry2::NotFound — Image not found at mcr.microsoft.com, and because the whole group shares a single job, one failure kills the run. The actual HTTP call that 404s (from the proxy log): HEAD https://mcr.microsoft.com/v2/dotnet/aspnet/blobs/sha256:80ddf066edeeb7e489b23c3c90d8e5dd741037d4a4875150c1dec777fd24cc76 → 404 That digest is the one we pinned in smoke-test-images.docker-compose.yml:15. I verified against MCR: ┌───────────────────────────────────────────────────┬────────────────────────────────────┐ │ Endpoint │ Result │ ├───────────────────────────────────────────────────┼────────────────────────────────────┤ │ HEAD /v2/dotnet/aspnet/manifests/sha256:80ddf066… │ 200 (it's a valid manifest digest) │ ├───────────────────────────────────────────────────┼────────────────────────────────────┤ │ HEAD /v2/dotnet/aspnet/blobs/sha256:80ddf066… │ 404 │ └───────────────────────────────────────────────────┴────────────────────────────────────┘ So the digest is fine — the problem is Dependabot is querying the wrong endpoint for it. Why Dependabot does this The stack trace leads to apply_cooldown → publication_detail → get_tag_publication_details (docker/update_checker.rb:346 → 362 → 380). That method: 1. Calls client.digest(repo, tag) which does a HEAD /manifests/<tag> and returns the Docker-Content-Digest header — that's the manifest digest. 2. Then does client.dohead "v2/#{repo}/blobs/#{digest}" and reads Last-Modified from the response to know when the image was published (so it can apply the 2-day cooldown). Step 2 assumes the manifest digest is also accessible via the /blobs/ endpoint. MCR doesn't do that cross-storage, so every cooldown lookup 404s. The docker-compose ecosystem is the only one of ours where we enabled cooldown and target MCR, so it's the only one this surfaces on. This is a bug in dependabot-core's Docker update checker, not in our config per se. ``` </p> </details> We could either remove the cool down (not acceptable) or we could try to fix the bug in dependabot (not worth the hassle), or we can just do it ourselves (what I settled on) ## Implementation details Told 🤖 to "implement it properly" and it did a good enough job 😄 It handles getting an anonymous token for the docker hub registry and managing the cool down, but there _is_ a possibility that we could hit rate limits. For now I'm inclined to just "meh" over it, as these _should_ be very rare, because most of the requests don't count towards it, and it shouldn't be an issue for mcr. One slight issue is the way the tagging works we can't get "all the recent new images", so that we can update to the latest image _outside_ the cooldown. We get the latest, see if it's in the cooldown, and if not, don't update. In the pathological case, we could end up never updating. In practice, that shouldn't be an issue for the images we're using ## Test coverage We can't actually test this properly until it's merged, but I ran a test locally, and it output the following (abbreviated) ``` ╬════════════════════════════════ ║ UpdateSmokeTestImageDigests ╬═══════════════════════ ​ 09:46:16 [INF] ──────────────────────────────────────────────────────────── 09:46:16 [INF] Updating pinned digests in C:\repos\dd-trace-dotnet-5\tracer\build\_build\SmokeTests\smoke-test-images.docker-compose.yml 09:46:16 [INF] ──────────────────────────────────────────────────────────── 09:46:16 [INF] Found 96 pinned images to evaluate 09:46:16 [DBG] Evaluating mcr.microsoft.com/dotnet/aspnet:2.1-alpine3.12 09:46:16 [INF] Unchanged mcr.microsoft.com/dotnet/aspnet:2.1-alpine3.12 (digest 43ff07984cea) 09:46:16 [DBG] Evaluating mcr.microsoft.com/dotnet/aspnet:2.1-stretch-slim ... 09:46:33 [DBG] Evaluating andrewlock/dotnet-ubuntu:25.04-9.0 09:46:33 [INF] Unchanged andrewlock/dotnet-ubuntu:25.04-9.0 (digest 0e5c1ae3e68d) 09:46:33 [INF] Rewrote C:\repos\dd-trace-dotnet-5\tracer\build\_build\SmokeTests\smoke-test-images.docker-compose.yml with 10 digest update(s) 09:46:33 [INF] Summary - updated: 10, cooldown: 8, unchanged: 78, failed: 0 09:46:33 [INF] Image digest cooldown report saved to C:\repos\dd-trace-dotnet-5\.nuke\temp\smoke_test_image_cooldown_report.md ​ ╬══════════════════════ ║ Errors & Warnings ╬═════════════ ​ [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/aspnet:8.0-bookworm-slim: new digest f88c77644f4c is only 28h old (< 48h cooldown); keeping current pin f24d74e8185b [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim: new digest 4e07e00025f1 is only 28h old (< 48h cooldown); keeping current pin d6c93f1bd94f [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/aspnet:10.0-noble: new digest 55e37c7795bf is only 41h old (< 48h cooldown); keeping current pin ccdca44cd4f2 [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled: new digest f43461f1774d is only 41h old (< 48h cooldown); keeping current pin 1191b4891ae8 [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-composite: new digest a6fe6804ff21 is only 41h old (< 48h cooldown); keeping current pin 168f51340812 [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/aspnet:10.0-windowsservercore-ltsc2022: new digest 17c963b25ddb is only 41h old (< 48h cooldown); keeping current pin 31eccd550269 [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim: new digest f9ddb8a31ae9 is only 28h old (< 48h cooldown); keeping current pin dbbdd47fae7a [WRN] UpdateSmokeTestImage: Cooldown mcr.microsoft.com/dotnet/sdk:10.0-noble: new digest 8a90a473da52 is only 41h old (< 48h cooldown); keeping current pin f061e5a7532b ``` And seems to work 🤞 ## Other details I'd _like_ to update the pipeline to always run _all_ smoke tests for these PRs, but I'll move that to a separate PR so as not to block this, as might require some faffing https://datadoghq.atlassian.net/browse/APMLP-1282
1 parent 33380a2 commit 276cb7d

7 files changed

Lines changed: 814 additions & 25 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
issuer: https://token.actions.githubusercontent.com
2+
3+
subject_pattern: repo:DataDog/dd-trace-dotnet:.*
4+
5+
claim_pattern:
6+
event_name: (schedule|workflow_dispatch)
7+
ref: refs/heads/master
8+
ref_protected: "true"
9+
job_workflow_ref: DataDog/dd-trace-dotnet/\.github/workflows/auto_bump_smoke_test_docker_images\.yml@refs/heads/master
10+
11+
permissions:
12+
contents: write
13+
pull_requests: write

.github/dependabot.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,6 @@ updates:
102102
cooldown:
103103
default-days: 2
104104

105-
# Docker images used in smoke tests - Dependabot updates pinned digests
106-
- package-ecosystem: "docker-compose"
107-
directory: "/tracer/build/_build/SmokeTests"
108-
schedule:
109-
interval: "weekly"
110-
labels:
111-
- "dependencies"
112-
- "area:dependabot"
113-
groups:
114-
smoke-test-images:
115-
patterns:
116-
- "*"
117-
cooldown:
118-
default-days: 2
119-
120105
- package-ecosystem: "github-actions"
121106
directories:
122107
- "/"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Auto bump smoke test docker images
2+
3+
on:
4+
schedule:
5+
- cron: "0 0 * * 1" # Every Monday at midnight
6+
- cron: "0 0 * * 4" # Every Thursday at midnight
7+
workflow_dispatch:
8+
9+
jobs:
10+
bump_smoke_test_docker_images:
11+
runs-on: windows-latest
12+
permissions:
13+
actions: read # read secrets
14+
id-token: write # Required for dd-octo-sts authentication
15+
16+
steps:
17+
- name: Support longpaths
18+
run: git config --system core.longpaths true
19+
20+
- name: Get dd-octo-sts token
21+
uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4
22+
id: octo-sts
23+
with:
24+
scope: DataDog/dd-trace-dotnet
25+
policy: self.auto_bump_smoke_test_docker_images.create-pr
26+
27+
- name: Checkout
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
29+
30+
- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
31+
with:
32+
global-json-file: global.json
33+
34+
- name: "Regenerating docker image versions"
35+
run: .\tracer\build.ps1 UpdateSmokeTestImageDigests --PackageVersionCooldownDays 2
36+
37+
- name: Read cooldown report
38+
id: cooldown
39+
if: always()
40+
shell: pwsh
41+
run: |
42+
$report = ""
43+
$reportPath = ".nuke/temp/smoke_test_image_cooldown_report.md"
44+
if (Test-Path $reportPath) {
45+
$report = Get-Content $reportPath -Raw
46+
}
47+
"report<<EOF" >> $env:GITHUB_OUTPUT
48+
$report >> $env:GITHUB_OUTPUT
49+
"EOF" >> $env:GITHUB_OUTPUT
50+
51+
- name: Create Pull Request
52+
id: pr
53+
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
54+
with:
55+
token: ${{ steps.octo-sts.outputs.token }}
56+
branch: "bot/smoke-test-docker-image-bump"
57+
commit-message: "[Smoke Test Docker Image Bump]"
58+
delete-branch: true
59+
base: master
60+
title: "[Smoke Test Docker Image Bump] Updating docker image tags "
61+
labels: "area:dependabot,area:test-apps,dependencies"
62+
body: |
63+
Updates the docker images used for smoke tests.
64+
65+
${{ steps.cooldown.outputs.report }}
66+
67+
- name: Send Slack notification about generating failure
68+
if: failure()
69+
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
70+
with:
71+
# This data can be any valid JSON from a previous step in the GitHub Action
72+
payload: |
73+
{
74+
"github_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
75+
}
76+
env:
77+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBOOK_URL_GENERATEPACKAGEVERSIONS }}

tracer/build/_build/Build.SmokeTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using Nuke.Common;
23
using static Nuke.Common.IO.FileSystemTasks;
34
using Logger = Serilog.Log;
@@ -25,4 +26,18 @@ await SmokeTests.SmokeTestRunner.RunSmokeTestAsync(
2526
Version,
2627
GetDotnetSdkVersion(RootDirectory));
2728
});
29+
30+
Target UpdateSmokeTestImageDigests => _ => _
31+
.Description("Queries each registry and updates pinned sha256 digests in smoke-test-images.docker-compose.yml, respecting a 2-day cooldown")
32+
.Unlisted()
33+
.Executes(async () =>
34+
{
35+
var composeFile = TracerDirectory / "build" / "_build" / "SmokeTests" / "smoke-test-images.docker-compose.yml";
36+
var reportPath = TemporaryDirectory / "smoke_test_image_cooldown_report.md";
37+
var cooldown = PackageVersionCooldownDays.HasValue
38+
? TimeSpan.FromDays(PackageVersionCooldownDays.Value)
39+
: TimeSpan.FromDays(0);
40+
using var updater = new SmokeTests.SmokeTestImageDigestUpdater(cooldown);
41+
await updater.UpdateAsync(composeFile, reportPath);
42+
});
2843
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// <copyright file="CooldownReport.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
using System;
8+
using System.Collections.Generic;
9+
using System.IO;
10+
using System.Text;
11+
using System.Threading.Tasks;
12+
13+
namespace SmokeTests;
14+
15+
/// <summary>
16+
/// Collects smoke-test image digests that were skipped by the cooldown filter
17+
/// and renders them as a markdown report for inclusion in the PR body.
18+
/// </summary>
19+
public class CooldownReport
20+
{
21+
readonly TimeSpan _cooldown;
22+
readonly List<CooldownEntry> _cooldownEntries = new();
23+
readonly List<FailureEntry> _failureEntries = new();
24+
25+
public CooldownReport(TimeSpan cooldown)
26+
{
27+
_cooldown = cooldown;
28+
}
29+
30+
public bool HasEntries => _cooldownEntries.Count > 0 || _failureEntries.Count > 0;
31+
32+
public IReadOnlyList<CooldownEntry> Entries => _cooldownEntries;
33+
34+
public IReadOnlyList<FailureEntry> Failures => _failureEntries;
35+
36+
public void Add(CooldownEntry entry)
37+
{
38+
_cooldownEntries.Add(entry);
39+
}
40+
41+
public void AddFailure(FailureEntry entry)
42+
{
43+
_failureEntries.Add(entry);
44+
}
45+
46+
public string ToMarkdown()
47+
{
48+
if (!HasEntries)
49+
{
50+
return string.Empty;
51+
}
52+
53+
var sb = new StringBuilder();
54+
55+
if (_cooldownEntries.Count > 0)
56+
{
57+
sb.AppendLine("## Smoke Test Image Digest Cooldown Report");
58+
sb.AppendLine();
59+
sb.AppendLine($"The following images have newer digests available but were published less than **{(int)_cooldown.TotalDays} day(s)** ago, so the existing pin is retained.");
60+
sb.AppendLine("They will be picked up automatically by a future run once they age out of the cooldown window.");
61+
sb.AppendLine();
62+
sb.AppendLine("| Image | Current pinned | Available digest | Published | Age |");
63+
sb.AppendLine("|-------|----------------|------------------|-----------|-----|");
64+
65+
foreach (var entry in _cooldownEntries)
66+
{
67+
var published = entry.PublishedDate?.UtcDateTime.ToString("yyyy-MM-dd") ?? "unknown";
68+
var age = entry.PublishedDate.HasValue
69+
? FormatAge(DateTimeOffset.UtcNow - entry.PublishedDate.Value)
70+
: "?";
71+
72+
sb.AppendLine($"| `{entry.Image}` | `{Shorten(entry.CurrentDigest)}` | `{Shorten(entry.AvailableDigest)}` | {published} | {age} |");
73+
}
74+
}
75+
76+
if (_failureEntries.Count > 0)
77+
{
78+
if (_cooldownEntries.Count > 0)
79+
{
80+
sb.AppendLine();
81+
}
82+
sb.AppendLine("## Smoke Test Image Digest Failures");
83+
sb.AppendLine();
84+
sb.AppendLine("The following images could not be evaluated this run. The existing pin has been kept. Investigate before the next scheduled run if these persist.");
85+
sb.AppendLine();
86+
sb.AppendLine("| Image | Error |");
87+
sb.AppendLine("|-------|-------|");
88+
89+
foreach (var entry in _failureEntries)
90+
{
91+
sb.AppendLine($"| `{entry.Image}` | {EscapeTableCell(entry.ErrorMessage)} |");
92+
}
93+
}
94+
95+
return sb.ToString();
96+
}
97+
98+
public async Task SaveToFile(string path)
99+
{
100+
var markdown = ToMarkdown();
101+
if (!string.IsNullOrEmpty(markdown))
102+
{
103+
// Saved to disk so it can later be fed into the PR description of the automation workflow.
104+
await File.WriteAllTextAsync(path, markdown);
105+
}
106+
}
107+
108+
static string EscapeTableCell(string value)
109+
{
110+
// Collapse newlines and escape the pipe so a single error string can't blow up the table.
111+
return value
112+
.Replace("\r", " ")
113+
.Replace("\n", " ")
114+
.Replace("|", "\\|");
115+
}
116+
117+
static string FormatAge(TimeSpan age)
118+
{
119+
if (age.TotalDays >= 1)
120+
{
121+
return $"{(int)age.TotalDays}d";
122+
}
123+
return $"{(int)age.TotalHours}h";
124+
}
125+
126+
static string Shorten(string digest)
127+
{
128+
var colon = digest.IndexOf(':');
129+
var hex = colon >= 0 ? digest[(colon + 1)..] : digest;
130+
return hex.Length > 12 ? hex.Substring(0, 12) : hex;
131+
}
132+
133+
public record CooldownEntry(
134+
string Image,
135+
string CurrentDigest,
136+
string AvailableDigest,
137+
DateTimeOffset? PublishedDate);
138+
139+
public record FailureEntry(
140+
string Image,
141+
string ErrorMessage);
142+
}

0 commit comments

Comments
 (0)