From d5a8a936c199712b0a8bc663873ee205271f9d57 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 10 Feb 2026 12:28:42 -0600 Subject: [PATCH] Add telemetry tracking for deprecated set-output and save-state commands (#4221) --- src/Runner.Worker/ActionCommandManager.cs | 22 +++++++++ src/Runner.Worker/GlobalContext.cs | 2 + src/Test/L0/Worker/ActionCommandManagerL0.cs | 50 ++++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/src/Runner.Worker/ActionCommandManager.cs b/src/Runner.Worker/ActionCommandManager.cs index 2a487df3d..77d6a4c5f 100644 --- a/src/Runner.Worker/ActionCommandManager.cs +++ b/src/Runner.Worker/ActionCommandManager.cs @@ -318,6 +318,17 @@ namespace GitHub.Runner.Worker context.AddIssue(issue, ExecutionContextLogOptions.Default); } + if (!context.Global.HasDeprecatedSetOutput) + { + context.Global.HasDeprecatedSetOutput = true; + var telemetry = new JobTelemetry + { + Type = JobTelemetryType.ActionCommand, + Message = "DeprecatedCommand: set-output" + }; + context.Global.JobTelemetry.Add(telemetry); + } + if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName)) { throw new Exception("Required field 'name' is missing in ##[set-output] command."); @@ -353,6 +364,17 @@ namespace GitHub.Runner.Worker context.AddIssue(issue, ExecutionContextLogOptions.Default); } + if (!context.Global.HasDeprecatedSaveState) + { + context.Global.HasDeprecatedSaveState = true; + var telemetry = new JobTelemetry + { + Type = JobTelemetryType.ActionCommand, + Message = "DeprecatedCommand: save-state" + }; + context.Global.JobTelemetry.Add(telemetry); + } + if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName)) { throw new Exception("Required field 'name' is missing in ##[save-state] command."); diff --git a/src/Runner.Worker/GlobalContext.cs b/src/Runner.Worker/GlobalContext.cs index da45c010d..6d4494843 100644 --- a/src/Runner.Worker/GlobalContext.cs +++ b/src/Runner.Worker/GlobalContext.cs @@ -31,5 +31,7 @@ namespace GitHub.Runner.Worker public JObject ContainerHookState { get; set; } public bool HasTemplateEvaluatorMismatch { get; set; } public bool HasActionManifestMismatch { get; set; } + public bool HasDeprecatedSetOutput { get; set; } + public bool HasDeprecatedSaveState { get; set; } } } diff --git a/src/Test/L0/Worker/ActionCommandManagerL0.cs b/src/Test/L0/Worker/ActionCommandManagerL0.cs index 3a1f8f70f..906f446fc 100644 --- a/src/Test/L0/Worker/ActionCommandManagerL0.cs +++ b/src/Test/L0/Worker/ActionCommandManagerL0.cs @@ -457,6 +457,8 @@ namespace GitHub.Runner.Common.Tests.Worker new SetEnvCommandExtension(), new WarningCommandExtension(), new AddMaskCommandExtension(), + new SetOutputCommandExtension(), + new SaveStateCommandExtension(), }; foreach (var command in commands) { @@ -499,5 +501,53 @@ namespace GitHub.Runner.Common.Tests.Worker }; } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void SetOutputCommand_EmitsTelemetryOnce() + { + using (TestHostContext hc = CreateTestContext()) + { + _ec.Object.Global.JobTelemetry = new List(); + var reference = string.Empty; + _ec.Setup(x => x.SetOutput(It.IsAny(), It.IsAny(), out reference)); + + // First set-output should add telemetry + Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::set-output name=foo::bar", null)); + Assert.Single(_ec.Object.Global.JobTelemetry); + Assert.Equal(JobTelemetryType.ActionCommand, _ec.Object.Global.JobTelemetry[0].Type); + Assert.Equal("DeprecatedCommand: set-output", _ec.Object.Global.JobTelemetry[0].Message); + Assert.True(_ec.Object.Global.HasDeprecatedSetOutput); + + // Second set-output should not add another telemetry entry + Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::set-output name=foo2::bar2", null)); + Assert.Single(_ec.Object.Global.JobTelemetry); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void SaveStateCommand_EmitsTelemetryOnce() + { + using (TestHostContext hc = CreateTestContext()) + { + _ec.Object.Global.JobTelemetry = new List(); + _ec.Setup(x => x.IsEmbedded).Returns(false); + _ec.Setup(x => x.IntraActionState).Returns(new Dictionary()); + + // First save-state should add telemetry + Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::save-state name=foo::bar", null)); + Assert.Single(_ec.Object.Global.JobTelemetry); + Assert.Equal(JobTelemetryType.ActionCommand, _ec.Object.Global.JobTelemetry[0].Type); + Assert.Equal("DeprecatedCommand: save-state", _ec.Object.Global.JobTelemetry[0].Message); + Assert.True(_ec.Object.Global.HasDeprecatedSaveState); + + // Second save-state should not add another telemetry entry + Assert.True(_commandManager.TryProcessCommand(_ec.Object, "::save-state name=foo2::bar2", null)); + Assert.Single(_ec.Object.Global.JobTelemetry); + } + } + } }