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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
issuer: https://token.actions.githubusercontent.com

subject_pattern: repo:DataDog/dd-trace-dotnet:.*

claim_pattern:
event_name: (schedule|workflow_dispatch)
ref: refs/heads/master
ref_protected: "true"
job_workflow_ref: DataDog/dd-trace-dotnet/\.github/workflows/auto_bump_smoke_test_docker_images\.yml@refs/heads/master

permissions:
contents: write
pull_requests: write
15 changes: 0 additions & 15 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,6 @@ updates:
cooldown:
default-days: 2

# Docker images used in smoke tests - Dependabot updates pinned digests
- package-ecosystem: "docker-compose"
directory: "/tracer/build/_build/SmokeTests"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "area:dependabot"
groups:
smoke-test-images:
patterns:
- "*"
cooldown:
default-days: 2

- package-ecosystem: "github-actions"
directories:
- "/"
Expand Down
77 changes: 77 additions & 0 deletions .github/workflows/auto_bump_smoke_test_docker_images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Auto bump smoke test docker images

on:
schedule:
- cron: "0 0 * * 1" # Every Monday at midnight
- cron: "0 0 * * 4" # Every Thursday at midnight
workflow_dispatch:

jobs:
bump_smoke_test_docker_images:
runs-on: windows-latest
permissions:
actions: read # read secrets
id-token: write # Required for dd-octo-sts authentication

steps:
- name: Support longpaths
run: git config --system core.longpaths true

- name: Get dd-octo-sts token
uses: DataDog/dd-octo-sts-action@96a25462dbcb10ebf0bfd6e2ccc917d2ab235b9a # v1.0.4
id: octo-sts
with:
scope: DataDog/dd-trace-dotnet
policy: self.auto_bump_smoke_test_docker_images.create-pr

- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
global-json-file: global.json

- name: "Regenerating docker image versions"
run: .\tracer\build.ps1 UpdateSmokeTestImageDigests --PackageVersionCooldownDays 2

- name: Read cooldown report
id: cooldown
if: always()
shell: pwsh
run: |
$report = ""
$reportPath = ".nuke/temp/smoke_test_image_cooldown_report.md"
if (Test-Path $reportPath) {
$report = Get-Content $reportPath -Raw
}
"report<<EOF" >> $env:GITHUB_OUTPUT
$report >> $env:GITHUB_OUTPUT
"EOF" >> $env:GITHUB_OUTPUT

- name: Create Pull Request
id: pr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ steps.octo-sts.outputs.token }}
branch: "bot/smoke-test-docker-image-bump"
commit-message: "[Smoke Test Docker Image Bump]"
delete-branch: true
base: master
title: "[Smoke Test Docker Image Bump] Updating docker image tags "
labels: "area:dependabot,area:test-apps,dependencies"
body: |
Updates the docker images used for smoke tests.

${{ steps.cooldown.outputs.report }}

- name: Send Slack notification about generating failure
if: failure()
uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1
with:
# This data can be any valid JSON from a previous step in the GitHub Action
payload: |
{
"github_url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBOOK_URL_GENERATEPACKAGEVERSIONS }}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Typo, this is missing a H

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, so is the secret in GitHub though πŸ™ˆ πŸ˜‚

15 changes: 15 additions & 0 deletions tracer/build/_build/Build.SmokeTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using Nuke.Common;
using static Nuke.Common.IO.FileSystemTasks;
using Logger = Serilog.Log;
Expand Down Expand Up @@ -25,4 +26,18 @@ await SmokeTests.SmokeTestRunner.RunSmokeTestAsync(
Version,
GetDotnetSdkVersion(RootDirectory));
});

Target UpdateSmokeTestImageDigests => _ => _
.Description("Queries each registry and updates pinned sha256 digests in smoke-test-images.docker-compose.yml, respecting a 2-day cooldown")
.Unlisted()
.Executes(async () =>
{
var composeFile = TracerDirectory / "build" / "_build" / "SmokeTests" / "smoke-test-images.docker-compose.yml";
var reportPath = TemporaryDirectory / "smoke_test_image_cooldown_report.md";
var cooldown = PackageVersionCooldownDays.HasValue
? TimeSpan.FromDays(PackageVersionCooldownDays.Value)
: TimeSpan.FromDays(0);
using var updater = new SmokeTests.SmokeTestImageDigestUpdater(cooldown);
await updater.UpdateAsync(composeFile, reportPath);
});
}
142 changes: 142 additions & 0 deletions tracer/build/_build/SmokeTests/CooldownReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// <copyright file="CooldownReport.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace SmokeTests;

/// <summary>
/// Collects smoke-test image digests that were skipped by the cooldown filter
/// and renders them as a markdown report for inclusion in the PR body.
/// </summary>
public class CooldownReport
{
readonly TimeSpan _cooldown;
readonly List<CooldownEntry> _cooldownEntries = new();
readonly List<FailureEntry> _failureEntries = new();

public CooldownReport(TimeSpan cooldown)
{
_cooldown = cooldown;
}

public bool HasEntries => _cooldownEntries.Count > 0 || _failureEntries.Count > 0;

public IReadOnlyList<CooldownEntry> Entries => _cooldownEntries;

public IReadOnlyList<FailureEntry> Failures => _failureEntries;

public void Add(CooldownEntry entry)
{
_cooldownEntries.Add(entry);
}

public void AddFailure(FailureEntry entry)
{
_failureEntries.Add(entry);
}

public string ToMarkdown()
{
if (!HasEntries)
{
return string.Empty;
}

var sb = new StringBuilder();

if (_cooldownEntries.Count > 0)
{
sb.AppendLine("## Smoke Test Image Digest Cooldown Report");
sb.AppendLine();
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.");
sb.AppendLine("They will be picked up automatically by a future run once they age out of the cooldown window.");
sb.AppendLine();
sb.AppendLine("| Image | Current pinned | Available digest | Published | Age |");
sb.AppendLine("|-------|----------------|------------------|-----------|-----|");

foreach (var entry in _cooldownEntries)
{
var published = entry.PublishedDate?.UtcDateTime.ToString("yyyy-MM-dd") ?? "unknown";
var age = entry.PublishedDate.HasValue
? FormatAge(DateTimeOffset.UtcNow - entry.PublishedDate.Value)
: "?";

sb.AppendLine($"| `{entry.Image}` | `{Shorten(entry.CurrentDigest)}` | `{Shorten(entry.AvailableDigest)}` | {published} | {age} |");
}
}

if (_failureEntries.Count > 0)
{
if (_cooldownEntries.Count > 0)
{
sb.AppendLine();
}
sb.AppendLine("## Smoke Test Image Digest Failures");
sb.AppendLine();
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.");
sb.AppendLine();
sb.AppendLine("| Image | Error |");
sb.AppendLine("|-------|-------|");

foreach (var entry in _failureEntries)
{
sb.AppendLine($"| `{entry.Image}` | {EscapeTableCell(entry.ErrorMessage)} |");
}
}

return sb.ToString();
}

public async Task SaveToFile(string path)
{
var markdown = ToMarkdown();
if (!string.IsNullOrEmpty(markdown))
{
// Saved to disk so it can later be fed into the PR description of the automation workflow.
await File.WriteAllTextAsync(path, markdown);
}
}

static string EscapeTableCell(string value)
{
// Collapse newlines and escape the pipe so a single error string can't blow up the table.
return value
.Replace("\r", " ")
.Replace("\n", " ")
.Replace("|", "\\|");
}

static string FormatAge(TimeSpan age)
{
if (age.TotalDays >= 1)
{
return $"{(int)age.TotalDays}d";
}
return $"{(int)age.TotalHours}h";
}

static string Shorten(string digest)
{
var colon = digest.IndexOf(':');
var hex = colon >= 0 ? digest[(colon + 1)..] : digest;
return hex.Length > 12 ? hex.Substring(0, 12) : hex;
}

public record CooldownEntry(
string Image,
string CurrentDigest,
string AvailableDigest,
DateTimeOffset? PublishedDate);

public record FailureEntry(
string Image,
string ErrorMessage);
}
Loading
Loading