diff --git a/.github/workflows/deploy_agent.yml b/.github/workflows/deploy_agent.yml index 74b94ccd00..93768aa757 100644 --- a/.github/workflows/deploy_agent.yml +++ b/.github/workflows/deploy_agent.yml @@ -33,6 +33,10 @@ on: description: 'If "true", will run the index-download-site job. If "false", will not.' required: true default: 'true' + update-apm-version: + description: 'If "true", will run the update-apm job. If "false", will not.' + required: true + default: 'true' permissions: contents: read @@ -334,3 +338,56 @@ jobs: run: | curl -i -X POST -H 'Fastly-Key:${{ secrets.FASTLY_TOKEN }}' ${{ secrets.FASTLY_URL }} shell: bash + + update-apm: + name: Update System Configuration Page + runs-on: ubuntu-latest + if: ${{ github.event.inputs.update-apm-version == true }} + steps: + - name: Update system configuration page + run: | + PAYLOAD="{ + \"system_configuration\": { + \"key\": \"dotnet_agent_version\", + \"value\": \"${{ github.event.inputs.agent_version }}\" + } + }" + CONTENT_TYPE='Content-Type: application/json' + + # STAGING + curl -X POST 'https://staging-api.newrelic.com/v2/system_configuration.json' \ + -H "X-Api-Key:${{ secrets.NEW_RELIC_API_KEY_STAGING }}" -i \ + -H "$CONTENT_TYPE" \ + -d "$PAYLOAD" + + # PRODUCTION + curl -X POST 'https://api.newrelic.com/v2/system_configuration.json' \ + -H "X-Api-Key:${{ secrets.NEW_RELIC_API_KEY_PRODUCTION }}" -i \ + -H "$CONTENT_TYPE" \ + -d "$PAYLOAD" + + # EU PRODUCTION + curl -X POST 'https://api.eu.newrelic.com/v2/system_configuration.json' \ + -H "X-Api-Key:${{ secrets.NEW_RELIC_API_KEY_PRODUCTION }}" -i \ + -H "$CONTENT_TYPE" \ + -d "$PAYLOAD" + + publish-release-notes: + needs: [deploy-linux, index-download-site] + if: ${{ github.event.inputs.deploy == 'true' && github.event.inputs.downloadsite == 'true' && github.event.inputs.nuget == 'true' && github.event.inputs.linux == 'true' && github.event.inputs.linux-deploy-to-production == 'true' }} + name: Create and Publish Release Notes + uses: newrelic/newrelic-dotnet-agent/.github/workflows/publish_release_notes.yml@main + with: + agent_version: ${{ github.event.inputs.agent_version }} + run_id: ${{ github.event.inputs.run_id }} + secrets: inherit + + post-deploy: + needs: [deploy-linux, index-download-site] + if: ${{ github.event.inputs.deploy == 'true' && github.event.inputs.downloadsite == 'true' && github.event.inputs.nuget == 'true' && github.event.inputs.linux == 'true' && github.event.inputs.linux-deploy-to-production == 'true' }} + name: Run Post Deploy Workflow + uses: newrelic/newrelic-dotnet-agent/.github/workflows/post_deploy_agent.yml@main + with: + agent_version: ${{ github.event.inputs.agent_version }} + secrets: inherit + diff --git a/.github/workflows/post_deploy_agent.yml b/.github/workflows/post_deploy_agent.yml index 9199de8e2b..4218555786 100644 --- a/.github/workflows/post_deploy_agent.yml +++ b/.github/workflows/post_deploy_agent.yml @@ -1,5 +1,6 @@ name: Post Deploy for the .NET Agent + on: workflow_dispatch: inputs: @@ -24,8 +25,36 @@ env: jobs: - placeholder: - name: Placeholder + validate-apt-repo: + name: Validate APT-based repo + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 + with: + disable-sudo: false + egress-policy: audit + + - name: Validate + run: | + echo 'deb https://apt.newrelic.com/debian/ newrelic non-free' | sudo tee /etc/apt/sources.list.d/newrelic.list + wget -O- https://download.newrelic.com/548C16BF.gpg | sudo apt-key add - + sudo apt-get update + sudo apt-get install newrelic-dotnet-agent + installed_version=$(dpkg -s newrelic-dotnet-agent | grep -i version) + if [ "$AGENT_VERSION" = "$installed_version" ]; then + echo "Versions match." + exit 0 + else + echo "ERROR: Version mismatch: Expected $AGENT_VERSION was $installed_version" + exit 1 + fi + shell: bash + env: + AGENT_VERSION: "Version: ${{ github.event.inputs.agent_version }}" + + validate-yum-repo: + name: Validate YUM-based repo runs-on: ubuntu-latest steps: - name: Harden Runner @@ -34,3 +63,106 @@ jobs: disable-sudo: true egress-policy: audit + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Validate + run: | + cd deploy/validation/validate-yum + + # This will setup the New Relic yum repo and install the agent. + docker build -t localtesting/validateyum:latest . + docker run --name validateyum localtesting/validateyum:latest + installed_version=$(docker logs --tail 1 validateyum) + if [ "$AGENT_VERSION" = "$installed_version" ]; then + echo "Versions match." + exit 0 + else + echo "ERROR: Version mismatch: Expected $AGENT_VERSION was $installed_version" + exit 1 + fi + shell: bash + env: + AGENT_VERSION: "newrelic-dotnet-agent-${{ github.event.inputs.agent_version }}-1.x86_64" + + validate-download-site-s3: + name: Validate S3-hosted Download Site + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 + with: + disable-sudo: true + egress-policy: audit + + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Build and Run S3Validator + run: | + dotnet build --configuration Release "$BUILD_PATH" + "$RUN_PATH/S3Validator" -v $AGENT_VERSION -c $CONFIG_PATH + shell: bash + env: + BUILD_PATH: ${{ github.workspace }}/build/S3Validator/S3Validator.csproj + RUN_PATH: ${{ github.workspace }}/build/S3Validator/bin/Release/net7.0/ + CONFIG_PATH: ${{ github.workspace }}/build/S3Validator/bin/Release/net7.0/config.yml + AGENT_VERSION: ${{ github.event.inputs.agent_version }} + + validate-nuget-packages: + name: Validate NuGet Package Deployment + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 + with: + disable-sudo: true + egress-policy: audit + + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Build and Run NugetValidator + run: | + dotnet build --configuration Release "$BUILD_PATH" + "$RUN_PATH/NugetValidator" -v $AGENT_VERSION -c $CONFIG_PATH + shell: bash + env: + BUILD_PATH: ${{ github.workspace }}/build/NugetValidator/NugetValidator.csproj + RUN_PATH: ${{ github.workspace }}/build/NugetValidator/bin/Release/net7.0/ + AGENT_VERSION: ${{ github.event.inputs.agent_version }} + CONFIG_PATH: ${{ github.workspace }}/build/NugetValidator/bin/Release/net7.0/config.yml + + report-deprecated-nuget-packages: + name: Report Deprecated NuGet Packages + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@cba0d00b1fc9a034e1e642ea0f1103c282990604 # v2.5.0 + with: + disable-sudo: true + egress-policy: audit + + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Build and Run NugetDeprecator + run: | + dotnet build --configuration Release "$BUILD_PATH" + "$RUN_PATH/NugetVersionDeprecator" -c $CONFIG_PATH --github-token ${{ secrets.GITHUB_TOKEN }} --api-key ${{ secrets.NEW_RELIC_API_KEY_PRODUCTION }} + shell: bash + env: + BUILD_PATH: ${{ github.workspace }}/build/NugetVersionDeprecator/NugetVersionDeprecator.csproj + RUN_PATH: ${{ github.workspace }}/build/NugetVersionDeprecator/bin/Release/net7.0/ + CONFIG_PATH: ${{ github.workspace }}/build/NugetVersionDeprecator/bin/Release/net7.0/config.yml diff --git a/.github/workflows/publish_release_notes.yml b/.github/workflows/publish_release_notes.yml index 6a61aebdeb..c2571ae1f9 100644 --- a/.github/workflows/publish_release_notes.yml +++ b/.github/workflows/publish_release_notes.yml @@ -28,3 +28,97 @@ permissions: env: DOTNET_NOLOGO: true + +jobs: + + get-external-artifacts: + name: Get and Publish Deploy Artifacts Locally + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 + with: + disable-sudo: true + egress-policy: audit + + - name: Download Deploy Artifacts + uses: dawidd6/action-download-artifact@246dbf436b23d7c49e21a7ab8204ca9ecd1fe615 # v2.27.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + workflow: all_solutions.yml + run_id: ${{ github.event.inputs.run_id }} + name: deploy-artifacts + path: ${{ github.workspace }} + repo: ${{ github.repository }} + + - name: Upload Deploy Artifacts Locally + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + name: deploy-artifacts + path: ${{ github.workspace }}/build/BuildArtifacts + if-no-files-found: error + + publish-release-notes: + needs: get-external-artifacts + name: Create and Publish Release Notes + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@55d479fb1c5bcad5a4f9099a5d9f37c8857b2845 # v2.4.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + fetch-depth: 0 + + - name: Download Deploy Artifacts + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: deploy-artifacts + path: ${{ github.workspace }}/artifacts + + - name: Set Docs PR Branch Name + run: | + cleaned_branch=$(echo "10.13.0" | sed 's/\./-/g') + echo "branch_name=dotnet-release-$cleaned_branch" + echo "branch_name=dotnet-release-$cleaned_branch" >> $GITHUB_ENV + shell: bash + + - name: Build Release Notes + run: | + dotnet build --configuration Release "$BUILD_PATH" + notes_file=$("$RUN_PATH/ReleaseNotesBuilder" -p "$RUN_PATH/data.yml" -c "$CHANGELOG" -x "$CHECKSUMS" -o "$OUTPUT_PATH") + echo "$notes_file" + echo "notes_file=$notes_file" >> $GITHUB_ENV + shell: bash + env: + BUILD_PATH: ${{ github.workspace }}/build/ReleaseNotesBuilder/ReleaseNotesBuilder.csproj + RUN_PATH: ${{ github.workspace }}/build/ReleaseNotesBuilder/bin/Release/net7.0/ + CHANGELOG: ${{ github.workspace }}/src/Agent/CHANGELOG.md + CHECKSUMS: ${{ github.workspace }}/artifacts/DownloadSite/SHA256/checksums.md + OUTPUT_PATH: ${{ github.workspace }} + + - name: Create branch + uses: dmnemec/copy_file_to_another_repo_action@c93037aa10fa8893de271f19978c980d0c1a9b37 # tag v1.1.1 + env: + API_TOKEN_GITHUB: ${{ secrets.DOTNET_AGENT_GH_TOKEN }} + with: + source_file: "${{ env.notes_file }}" + destination_repo: 'newrelic/docs-website' + destination_folder: 'src/content/docs/release-notes/agent-release-notes/net-release-notes' + user_email: '${{ secrets.BOT_EMAIL }}' + user_name: 'dotnet-agent-team-bot' + destination_branch: 'develop' + destination_branch_create: ${{env.branch_name}} + commit_message: 'chore(.net agent): Add .NET Agent release notes for v${{ github.event.inputs.agent_version }}.' + + - name: Create pull request + run: gh pr create --base "develop" --repo "$REPO" --head "$HEAD" --title "$TITLE" --body "$BODY" + env: + GH_TOKEN: ${{ secrets.DOTNET_AGENT_GH_TOKEN }} + REPO: https://github.com/newrelic/docs-website/ + HEAD: ${{env.branch_name}} + TITLE: ".NET Agent Release Notes for v${{ github.event.inputs.agent_version }}" + BODY: "This is an automated PR generated when the .NET agent is released. Please merge as soon as possible." diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 9a0bb8204c..7da660b31c 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -23,5 +23,5 @@ jobs: with: release-type: go changelog-path: src/Agent/CHANGELOG.md - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.DOTNET_AGENT_GH_TOKEN }} changelog-types: '[{"type":"notice","section":"Notice","hidden":false},{"type":"feat","section":"New Features","hidden":false},{"type":"fix","section":"Fixes","hidden":false},{"type":"security","section":"Security","hidden":false}]' diff --git a/.gitignore b/.gitignore index da7f6643a3..d196073666 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,9 @@ NDependOut/ test-results +#ignore launchSettings +launchSettings.json + # Ignore FullAgent build output src/_build/ Agent/_build/ @@ -158,3 +161,4 @@ tests/TestResults/* /src/Agent/_profilerBuild/x64-Release/NewRelic.Profiler.dll /src/Agent/_profilerBuild/x86-Release/NewRelic.Profiler.dll /tests/Agent/IntegrationTests/ContainerApplications/.env + diff --git a/build/BuildTools.sln b/build/BuildTools.sln index 2789b312c7..dc81444fb0 100644 --- a/build/BuildTools.sln +++ b/build/BuildTools.sln @@ -7,6 +7,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArtifactBuilder", "Artifact EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NewRelic.NuGetHelper", "NewRelic.NuGetHelper\NewRelic.NuGetHelper.csproj", "{94BF8D27-2122-4573-AA79-90B977B40EF3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NugetValidator", "NugetValidator\NugetValidator.csproj", "{C3F69996-5A5F-4836-A485-C270C318C6E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "S3Validator", "S3Validator\S3Validator.csproj", "{648D08B2-E677-4009-A593-D03E0579E859}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReleaseNotesBuilder", "ReleaseNotesBuilder\ReleaseNotesBuilder.csproj", "{0E9152F2-4CA9-4F24-AADF-9B15310C3DFA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NugetVersionDeprecator", "NugetVersionDeprecator\NugetVersionDeprecator.csproj", "{77685AF5-5FD7-483E-B589-BDE4E2F1769C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +29,22 @@ Global {94BF8D27-2122-4573-AA79-90B977B40EF3}.Debug|Any CPU.Build.0 = Debug|Any CPU {94BF8D27-2122-4573-AA79-90B977B40EF3}.Release|Any CPU.ActiveCfg = Release|Any CPU {94BF8D27-2122-4573-AA79-90B977B40EF3}.Release|Any CPU.Build.0 = Release|Any CPU + {C3F69996-5A5F-4836-A485-C270C318C6E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3F69996-5A5F-4836-A485-C270C318C6E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3F69996-5A5F-4836-A485-C270C318C6E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3F69996-5A5F-4836-A485-C270C318C6E9}.Release|Any CPU.Build.0 = Release|Any CPU + {648D08B2-E677-4009-A593-D03E0579E859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {648D08B2-E677-4009-A593-D03E0579E859}.Debug|Any CPU.Build.0 = Debug|Any CPU + {648D08B2-E677-4009-A593-D03E0579E859}.Release|Any CPU.ActiveCfg = Release|Any CPU + {648D08B2-E677-4009-A593-D03E0579E859}.Release|Any CPU.Build.0 = Release|Any CPU + {0E9152F2-4CA9-4F24-AADF-9B15310C3DFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E9152F2-4CA9-4F24-AADF-9B15310C3DFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E9152F2-4CA9-4F24-AADF-9B15310C3DFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E9152F2-4CA9-4F24-AADF-9B15310C3DFA}.Release|Any CPU.Build.0 = Release|Any CPU + {77685AF5-5FD7-483E-B589-BDE4E2F1769C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77685AF5-5FD7-483E-B589-BDE4E2F1769C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77685AF5-5FD7-483E-B589-BDE4E2F1769C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77685AF5-5FD7-483E-B589-BDE4E2F1769C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/build/NugetValidator/Configuration.cs b/build/NugetValidator/Configuration.cs new file mode 100644 index 0000000000..545b953df9 --- /dev/null +++ b/build/NugetValidator/Configuration.cs @@ -0,0 +1,11 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace NugetValidator +{ + public class Configuration + { + [YamlDotNet.Serialization.YamlMember(Alias = "nuget-packages")] + public List Packages { get; set; } + } +} diff --git a/build/NugetValidator/ConsoleNugetLogger.cs b/build/NugetValidator/ConsoleNugetLogger.cs new file mode 100644 index 0000000000..cdec455c91 --- /dev/null +++ b/build/NugetValidator/ConsoleNugetLogger.cs @@ -0,0 +1,21 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using NuGet.Common; + +namespace NugetValidator; + +internal class ConsoleNugetLogger : LoggerBase +{ + public override void Log(ILogMessage message) + { + Console.WriteLine($"{message}"); + } + + public override Task LogAsync(ILogMessage message) + { + Console.WriteLine($"{message}"); + + return Task.CompletedTask; + } +} diff --git a/build/NugetValidator/ExitCode.cs b/build/NugetValidator/ExitCode.cs new file mode 100644 index 0000000000..c046e73d00 --- /dev/null +++ b/build/NugetValidator/ExitCode.cs @@ -0,0 +1,12 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace NugetValidator; + +public enum ExitCode : int +{ + Success = 0, + Error = 1, + FileNotFound = 2, + BadArguments = 160 +} diff --git a/build/NugetValidator/NugetValidator.csproj b/build/NugetValidator/NugetValidator.csproj new file mode 100644 index 0000000000..6bc7a42d39 --- /dev/null +++ b/build/NugetValidator/NugetValidator.csproj @@ -0,0 +1,22 @@ + + + + Exe + net7.0 + default + enable + + + + + PreserveNewest + + + + + + + + + + diff --git a/build/NugetValidator/Options.cs b/build/NugetValidator/Options.cs new file mode 100644 index 0000000000..ec1893eec7 --- /dev/null +++ b/build/NugetValidator/Options.cs @@ -0,0 +1,15 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace NugetValidator; + +public class Options +{ + [Option('v', "version", Required = true, HelpText = "Package Version")] + public string Version { get; set; } + + [Option('c', "config", Default = "config.yml", Required = false, HelpText = "Path to the configuration file.")] + public required string ConfigurationPath { get; set; } +} diff --git a/build/NugetValidator/Program.cs b/build/NugetValidator/Program.cs new file mode 100644 index 0000000000..7b39080285 --- /dev/null +++ b/build/NugetValidator/Program.cs @@ -0,0 +1,113 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NugetValidator; + +internal class Program +{ + private const string RepoUrl = "https://api.nuget.org/v3/index.json"; + + static async Task Main(string[] args) + { + var options = Parser.Default.ParseArguments(args) + .WithParsed(ValidateOptions) + .WithNotParsed(HandleParseError) + .Value; + + var configuration = LoadConfiguration(options.ConfigurationPath); + + var validationFailed = await ValidatePackagesAsync(options, configuration); + + // return code of 0 indicates success + var exitCode = validationFailed ? 1 : 0; + Console.WriteLine($"Exit code: {exitCode}"); + + return exitCode; + } + + static async Task ValidatePackagesAsync(Options options, Configuration configuration) + { + var validationFailed = false; + + try + { + SourceCacheContext cache = new SourceCacheContext(); + SourceRepository repository = Repository.Factory.GetCoreV3(RepoUrl); + + PackageMetadataResource resource = await repository.GetResourceAsync(); + var nugetVersion = NuGetVersion.Parse(options.Version); + + foreach (var packageName in configuration.Packages) + { + Console.WriteLine($"Validating that NuGet package {packageName} with version {options.Version} exists..."); + + List packages = (await resource.GetMetadataAsync( + packageName, + includePrerelease: false, + includeUnlisted: false, + cache, + new ConsoleNugetLogger(), + CancellationToken.None)).ToList(); + + var result = packages.FirstOrDefault(p => + string.Equals(p.Identity.Id, packageName, StringComparison.CurrentCultureIgnoreCase) && + p.Identity.Version == nugetVersion); + + Console.WriteLine($"{(result != null ? "Found" : "Did NOT find")} NuGet package {packageName} with version {options.Version}"); + + validationFailed |= (result == null); + } + } + catch (Exception e) + { + Console.WriteLine($"Unexpected exception: {e}"); + throw; + } + + return validationFailed; + } + + private static Configuration LoadConfiguration(string path) + { + var input = File.ReadAllText(path); + var deserializer = new YamlDotNet.Serialization.Deserializer(); + return deserializer.Deserialize(input); + } + + + private static void ValidateOptions(Options opts) + { + if (string.IsNullOrWhiteSpace(opts.Version) + || string.IsNullOrWhiteSpace(opts.ConfigurationPath)) + { + ExitWithError(ExitCode.BadArguments, "Arguments were empty or whitespace."); + } + + if (!Version.TryParse(opts.Version, out _)) + { + ExitWithError(ExitCode.Error, $"Version provided, '{opts.Version}', was not a valid version."); + } + + if (!File.Exists(opts.ConfigurationPath)) + { + ExitWithError(ExitCode.FileNotFound, $"Configuration file did not exist at {opts.ConfigurationPath}."); + } + } + + private static void HandleParseError(IEnumerable errs) + { + ExitWithError(ExitCode.BadArguments, "Error occurred while parsing command line arguments."); + } + + public static void ExitWithError(ExitCode exitCode, string message) + { + Console.WriteLine(message); + Environment.Exit((int)exitCode); + } + +} diff --git a/build/NugetValidator/config.yml b/build/NugetValidator/config.yml new file mode 100644 index 0000000000..a3c2dd8991 --- /dev/null +++ b/build/NugetValidator/config.yml @@ -0,0 +1,12 @@ +# Copyright 2023 New Relic, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# NuGetPackages +# A list of NuGet package names to validate +nuget-packages: + - NewRelic.Agent + - NewRelic.Agent.Api + - NewRelic.Azure.Websites + - NewRelic.Azure.Websites.x64 + - NewRelicWindowsAzure + diff --git a/build/NugetVersionDeprecator/AgentRelease.cs b/build/NugetVersionDeprecator/AgentRelease.cs new file mode 100644 index 0000000000..7eabe09a59 --- /dev/null +++ b/build/NugetVersionDeprecator/AgentRelease.cs @@ -0,0 +1,14 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using Newtonsoft.Json; + +namespace NugetVersionDeprecator; + +internal class AgentRelease +{ + [JsonProperty("eolDate")] + public DateTime EolDate { get; set; } + [JsonProperty("version")] + public string Version { get; set; } +} diff --git a/build/NugetVersionDeprecator/Configuration.cs b/build/NugetVersionDeprecator/Configuration.cs new file mode 100644 index 0000000000..e3190adb35 --- /dev/null +++ b/build/NugetVersionDeprecator/Configuration.cs @@ -0,0 +1,11 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace NugetVersionDeprecator +{ + public class Configuration + { + [YamlDotNet.Serialization.YamlMember(Alias = "nuget-packages")] + public List Packages { get; set; } + } +} diff --git a/build/NugetVersionDeprecator/ExitCode.cs b/build/NugetVersionDeprecator/ExitCode.cs new file mode 100644 index 0000000000..b3612d0319 --- /dev/null +++ b/build/NugetVersionDeprecator/ExitCode.cs @@ -0,0 +1,12 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace NugetVersionDeprecator; + +public enum ExitCode : int +{ + Success = 0, + Error = 1, + FileNotFound = 2, + BadArguments = 160 +} diff --git a/build/NugetVersionDeprecator/NugetVersionDeprecator.csproj b/build/NugetVersionDeprecator/NugetVersionDeprecator.csproj new file mode 100644 index 0000000000..69c53d445c --- /dev/null +++ b/build/NugetVersionDeprecator/NugetVersionDeprecator.csproj @@ -0,0 +1,30 @@ + + + + Exe + net7.0 + enable + default + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + diff --git a/build/NugetVersionDeprecator/Options.cs b/build/NugetVersionDeprecator/Options.cs new file mode 100644 index 0000000000..4c8c4cf019 --- /dev/null +++ b/build/NugetVersionDeprecator/Options.cs @@ -0,0 +1,21 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace NugetVersionDeprecator; + +public class Options +{ + [Option('t', "test-mode", Required = false, HelpText = "Test mode: Report deprecated packages but don't create GH Issue")] + public bool TestMode { get; set; } + + [Option('c', "config", Default = "config.yml", Required = true, HelpText = "Path to the configuration file.")] + public required string ConfigurationPath { get; set; } + + [Option('g', "github-token", Required = false, HelpText = "The Github token to use when creating new issues. Not required in Test mode")] + public string GithubToken { get; set; } + + [Option('a', "api-key", Required = true, HelpText = "The NewRelic API Key for executing NerdGraph queries")] + public string ApiKey { get; set; } +} diff --git a/build/NugetVersionDeprecator/PackageDeprecationInfo.cs b/build/NugetVersionDeprecator/PackageDeprecationInfo.cs new file mode 100644 index 0000000000..5f8dea6832 --- /dev/null +++ b/build/NugetVersionDeprecator/PackageDeprecationInfo.cs @@ -0,0 +1,8 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +public class PackageDeprecationInfo +{ + public string PackageName { get; set; } + public string PackageVersion { get; set; } +} diff --git a/build/NugetVersionDeprecator/Program.cs b/build/NugetVersionDeprecator/Program.cs new file mode 100644 index 0000000000..640d094f44 --- /dev/null +++ b/build/NugetVersionDeprecator/Program.cs @@ -0,0 +1,207 @@ +// Copyright 2023 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using CommandLine; +using Newtonsoft.Json.Linq; +using NuGet.Common; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; +using Octokit; +using RestSharp; +using Repository = NuGet.Protocol.Core.Types.Repository; + +namespace NugetVersionDeprecator; + +internal class Program +{ + private const string RepoUrl = "https://api.nuget.org/v3/index.json"; + private const string NewRelicUrl = "https://api.newrelic.com/graphql"; + + private const string NerdGraphQueryJson = "{ \"query\": \"{ docs { agentReleases(agentName: DOTNET) { version eolDate } } }\" }"; + + + static async Task Main(string[] args) + { + try + { + var options = Parser.Default.ParseArguments(args) + .WithParsed(ValidateOptions) + .WithNotParsed(HandleParseError) + .Value; + + if (options.TestMode) + Console.WriteLine("**** TEST MODE *** No Github Issues will be created."); + + var configuration = LoadConfiguration(options.ConfigurationPath); + + var nerdGraphResponse = await QueryNerdGraphAsync(options.ApiKey, NewRelicUrl); + + var agentReleases = ParseNerdGraphResponse(DateTime.UtcNow, nerdGraphResponse); + if (agentReleases.Any()) + { + List packagesToDeprecate = new(); + + foreach (var package in configuration.Packages) + { + packagesToDeprecate.AddRange(await GetPackagesToDeprecateAsync(package, agentReleases)); + } + + if (packagesToDeprecate.Any()) + { + var message = ReportPackagesToDeprecate(packagesToDeprecate, agentReleases); + Console.WriteLine(message); + + if (!options.TestMode) + await CreateGhIssueAsync(message, options.GithubToken); + } + } + else + { + Console.WriteLine("No eligible deprecated Agent released found."); + } + + return 0; + } + catch (Exception e) + { + Console.WriteLine(e); + return 1; + } + } + + private static async Task QueryNerdGraphAsync(string apiKey, string url) + { + // Send a NerdGraph query to get a list of all .NET Agent versions + RestResponse response; + using var client = new RestClient(); + + var request = new RestRequest(url, Method.Post); + request.AddStringBody(NerdGraphQueryJson, DataFormat.Json); + request.AddHeader("API-Key", apiKey); + + try + { + response = await client.PostAsync(request); + } + catch (Exception e) + { + throw new Exception("NerdGraph query failed.", e); + } + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"NerdGraph query failed: {response}"); + } + + return response.Content; + } + + private static List ParseNerdGraphResponse(DateTime releaseDate, string nerdGraphResponse) + { + // parse the NerdGraph response -- we want to deserialize data.docs.agentReleases into an array of AgentRelease + var parsedResponse = JObject.Parse(nerdGraphResponse); + var allAgentReleases = parsedResponse.SelectToken("data.docs.agentReleases", false)?.ToObject>(); + if (allAgentReleases == null) + { + throw new Exception($"Unable to parse NerdGraph response: {Environment.NewLine}{nerdGraphResponse}"); + } + + var deprecatedReleases = allAgentReleases.Where(ar => ar.EolDate <= releaseDate).ToList(); + + return deprecatedReleases; + } + + private static string ReportPackagesToDeprecate(List packagesToDeprecate, List agentReleases) + { + var sb = new StringBuilder(); + + sb.AppendLine("The following NuGet packages should be deprecated:"); + foreach (var package in packagesToDeprecate) + { + var eolRelease = agentReleases.Single(ar => ar.Version.StartsWith(package.PackageVersion)); + + sb.AppendLine($" * {package.PackageName} v{package.PackageVersion} (EOL as of {eolRelease.EolDate.ToShortDateString()})"); + } + + return sb.ToString(); + } + + static async Task> GetPackagesToDeprecateAsync(string packageName, List agentReleases) + { + // query NuGet for a current list of non-deprecated versions of all .NET Agent packages + SourceCacheContext cache = new SourceCacheContext(); + SourceRepository repository = Repository.Factory.GetCoreV3(RepoUrl); + PackageMetadataResource resource = await repository.GetResourceAsync(); + var packages = (await resource.GetMetadataAsync( + packageName, + includePrerelease: false, + includeUnlisted: false, + cache, + NullLogger.Instance, + CancellationToken.None)).Cast() + // because of how NuGet query works, we'll get a whole lot of "close" match results - so we have to narrow the response down + // to only those packages where the name matches packageName + .Where(p => + p.DeprecationMetadata is null // not deprecated + && string.Equals(p.Identity.Id, packageName, StringComparison.CurrentCultureIgnoreCase)).ToList(); + + // get the nuget packages with versions matching the list of all agent releases + var currentVersions = agentReleases.Select(ar => NuGetVersion.Parse(ar.Version)).ToList(); + var packagesToDeprecate = packages.Where(p => currentVersions.Contains(p.Version)).ToList(); + + return packagesToDeprecate.Select(p => + new PackageDeprecationInfo() { PackageName = p.PackageId, PackageVersion = p.Version.ToString() }); + } + + static async Task CreateGhIssueAsync(string message, string githubToken) + { + var ghClient = new GitHubClient(new Octokit.ProductHeaderValue("NugetVersionDeprecator")); + var tokenAuth = new Credentials(githubToken); + ghClient.Credentials = tokenAuth; + + + var newIssue = new NewIssue($"chore(NugetDeprecator): Deprecate Nuget packages.") + { + Body = message + }; + + newIssue.Labels.Add("Deprecation"); + newIssue.Labels.Add("Nuget"); + + var issue = await ghClient.Issue.Create("newrelic", "newrelic-dotnet-agent", newIssue); + + Console.WriteLine($"Created new GitHub Issue #{issue.Number} with title {issue.Title}."); + } + + static Configuration LoadConfiguration(string path) + { + var input = File.ReadAllText(path); + var deserializer = new YamlDotNet.Serialization.Deserializer(); + return deserializer.Deserialize(input); + } + + static void ValidateOptions(Options opts) + { + if (!opts.TestMode && string.IsNullOrEmpty(opts.GithubToken)) + { + ExitWithError(ExitCode.BadArguments, "Github token is required when not in Test mode."); + } + if (!File.Exists(opts.ConfigurationPath)) + { + ExitWithError(ExitCode.FileNotFound, $"Configuration file did not exist at {opts.ConfigurationPath}."); + } + } + + static void HandleParseError(IEnumerable errs) + { + ExitWithError(ExitCode.BadArguments, "Error occurred while parsing command line arguments."); + } + + static void ExitWithError(ExitCode exitCode, string message) + { + Console.WriteLine(message); + Environment.Exit((int)exitCode); + } +} diff --git a/build/NugetVersionDeprecator/config.yml b/build/NugetVersionDeprecator/config.yml new file mode 100644 index 0000000000..a3c2dd8991 --- /dev/null +++ b/build/NugetVersionDeprecator/config.yml @@ -0,0 +1,12 @@ +# Copyright 2023 New Relic, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# NuGetPackages +# A list of NuGet package names to validate +nuget-packages: + - NewRelic.Agent + - NewRelic.Agent.Api + - NewRelic.Azure.Websites + - NewRelic.Azure.Websites.x64 + - NewRelicWindowsAzure + diff --git a/build/ReleaseNotesBuilder/ChangelogParser.cs b/build/ReleaseNotesBuilder/ChangelogParser.cs new file mode 100644 index 0000000000..5cf40af5fa --- /dev/null +++ b/build/ReleaseNotesBuilder/ChangelogParser.cs @@ -0,0 +1,146 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace ReleaseNotesBuilder +{ + internal static class ChangelogParser + { + private const string ChangeLogHeader = "# Changelog"; + private const string EndSquareBracket = "]"; + private const string ReleaseVersionPrefix = "## ["; + + /// + /// Parses the changelog.md file, update the ReleaseNotesModel and return the extracted release version. + /// + /// + /// + /// Extracted release version. + public static string Parse(List changelog, ReleaseNotesModel maker) + { + try + { + // Try to confirm that this is a changelog in our format. + if (changelog[0] != ChangeLogHeader) + { + Program.ExitWithError(ExitCode.NotAChangelog, $"The file does not appear to be a changelog file. Has the format change?"); + } + + // starting point for other searches + var currentReleaseIndex = changelog.FindIndex(0, 25, (x) => x.StartsWith(ReleaseVersionPrefix)); + if (currentReleaseIndex == -1) + { + Program.ExitWithError(ExitCode.NotAChangelog, $"The file does not appear to have a release version. Has the format change?"); + } + + var releaseVersion = ParseReleaseVersion(changelog, currentReleaseIndex); + maker.SetReleaseVersion(releaseVersion); + + // upper bound on the searches + var previousReleaseIndex = changelog.FindIndex(currentReleaseIndex + 1, 50, (x) => x.StartsWith(ReleaseVersionPrefix)); + + //Get change type starting points. + var newFeatures = ParseSection(changelog, Program.NewFeaturesSection, currentReleaseIndex, previousReleaseIndex); + maker.AddFeatures(newFeatures); + + var fixes = ParseSection(changelog, Program.FixesSection, currentReleaseIndex, previousReleaseIndex); + maker.AddBugsAndFixes(fixes); + + var security = ParseSection(changelog, Program.SecuritySection, currentReleaseIndex, previousReleaseIndex); + maker.AddSecurity(security); + + var notice = ParseSection(changelog, Program.NoticeSection, currentReleaseIndex, previousReleaseIndex); + maker.AddNotice(notice); + + return releaseVersion; + } + catch (Exception ex) + { + Program.ExitWithError(ExitCode.InvalidData, $"Problem parsing changelog: " + Environment.NewLine + ex.Message); + return string.Empty; + } + } + + private static string ParseReleaseVersion(List changelog, int releaseIndex) + { + var line = changelog[releaseIndex]; + var endIndex = line.IndexOf(EndSquareBracket); + + return line[4..endIndex]; + } + + private static List ParseSection(List changelog, string sectionLabel, int startingIndex, int maxIndex) + { + var maxLines = GetMaxLines(startingIndex, maxIndex); + var sectionIndex = changelog.FindIndex(startingIndex, maxLines, (x) => x.StartsWith(sectionLabel)); + if (sectionIndex == -1 || sectionIndex >= maxIndex) + { + return new List(); + } + + return GetChangeEntries(changelog, sectionIndex, maxIndex); + } + + private static List GetChangeEntries(List changelog, int startingIndex, int maxIndex) + { + var entries = new List(); + var nextSectionIndex = changelog.FindIndex(startingIndex + 1, GetMaxLines(startingIndex, maxIndex), (x) => x.StartsWith(Program.SectionPrefix)); + var sectionEndIndex = nextSectionIndex == -1 ? maxIndex : nextSectionIndex; + var maxLines = GetMaxLines(startingIndex, sectionEndIndex); + for (int i = startingIndex; i < sectionEndIndex; i++) + { + var line = changelog[i]; + if (string.IsNullOrWhiteSpace(line) || !line.StartsWith("* ")) + { + continue; + } + + var frontEntry = GetFrontEntry(line); + entries.Add(new Entry(frontEntry, line)); // we want the entire line for the body as-is - no need to alter it later. + } + + return entries; + } + + private static string GetFrontEntry(string line) + { + var descEndIndex = line.IndexOf("(["); + var bodyEntry = line[2..descEndIndex].Trim(); // drop the "* " + + var sentenceEndIndex = bodyEntry.IndexOf(". ", 0) + 1; + if (sentenceEndIndex <= 0) // account for padding above. + { + sentenceEndIndex = bodyEntry.Length; + } + + var frontEntry = bodyEntry[0..sentenceEndIndex].Trim(); + if (!frontEntry.EndsWith(".")) + { + frontEntry += "."; + } + + return CleanFrontEntry(frontEntry); + } + + private static string CleanFrontEntry(string frontEntry) + { + // Front matter entries are wrapped in single quotes so we need to escape them and remove newlines. + var cleanedEntry = frontEntry.Replace("\'", "\'\'").Replace("\n", "").Replace("\r", ""); + var illegalChars = new[] { '&', '|', ':' }; // must be wrapped in double quotes + foreach (var illegalChar in illegalChars) + { + // Purposefully break the entry removing any double quoted illegal chars + var damagedEntry = cleanedEntry.Replace($"\"{illegalChar}\"", $"{illegalChar}"); + + // Re-wrap the illegal chars and any others that were not wrapped in the original entry + cleanedEntry = damagedEntry.Replace($"{illegalChar}", $"\"{illegalChar}\""); + } + + return cleanedEntry; + } + + private static int GetMaxLines(int startingIndex, int maxIndex) + { + return maxIndex - startingIndex; + } + } +} diff --git a/build/ReleaseNotesBuilder/Entry.cs b/build/ReleaseNotesBuilder/Entry.cs new file mode 100644 index 0000000000..44ae717e39 --- /dev/null +++ b/build/ReleaseNotesBuilder/Entry.cs @@ -0,0 +1,17 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace ReleaseNotesBuilder +{ + public readonly struct Entry + { + public Entry(string front, string body) + { + Body = body; + Front = front; + } + + public string Body { get; } + public string Front { get; } + } +} diff --git a/build/ReleaseNotesBuilder/ExitCode.cs b/build/ReleaseNotesBuilder/ExitCode.cs new file mode 100644 index 0000000000..e7f43d62e8 --- /dev/null +++ b/build/ReleaseNotesBuilder/ExitCode.cs @@ -0,0 +1,14 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace ReleaseNotesBuilder +{ + enum ExitCode : int + { + Success = 0, + FileNotFound = 2, + NotAChangelog = 11, + InvalidData = 13, + BadArguments = 160 + } +} diff --git a/build/ReleaseNotesBuilder/Options.cs b/build/ReleaseNotesBuilder/Options.cs new file mode 100644 index 0000000000..632a9a25b0 --- /dev/null +++ b/build/ReleaseNotesBuilder/Options.cs @@ -0,0 +1,25 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace ReleaseNotesBuilder +{ + public class Options + { + [Option('v', "verbose", Default = false, Required = false, HelpText = "Output the release notes to console.")] + public bool Verbose { get; set; } + + [Option('p', "pdata", Required = true, HelpText = "Path to the persisent data.")] + public required string PersistentData { get; set; } + + [Option('c', "changelog", Required = true, HelpText = "Changelog.md file to process.")] + public required string Changelog { get; set; } + + [Option('x', "checksums", Required = true, HelpText = "checksums.md file to process.")] + public required string Checksums { get; set; } + + [Option('o', "output", Required = true, HelpText = "Where to save the release notes file. Path only, file name is determined by version!")] + public required string Output { get; set; } + } +} diff --git a/build/ReleaseNotesBuilder/PersistentData.cs b/build/ReleaseNotesBuilder/PersistentData.cs new file mode 100644 index 0000000000..523366cfbb --- /dev/null +++ b/build/ReleaseNotesBuilder/PersistentData.cs @@ -0,0 +1,20 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace ReleaseNotesBuilder +{ + public class PersistentData + { + [YamlDotNet.Serialization.YamlMember(Alias = "subject")] + public string? Subject { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "download-link")] + public string? DownloadLink { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "preamble")] + public string? Preamble { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "epilogue")] + public string? Epilogue { get; set; } + } +} diff --git a/build/ReleaseNotesBuilder/Program.cs b/build/ReleaseNotesBuilder/Program.cs new file mode 100644 index 0000000000..634a3dfa11 --- /dev/null +++ b/build/ReleaseNotesBuilder/Program.cs @@ -0,0 +1,114 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace ReleaseNotesBuilder +{ + internal class Program + { + internal const string SectionPrefix = "### "; + internal const string SecuritySection = SectionPrefix + "Security"; + internal const string NewFeaturesSection = SectionPrefix + "New Features"; + internal const string FixesSection = SectionPrefix + "Fixes"; + internal const string NoticeSection = SectionPrefix + "Notice"; + + static void Main(string[] args) + { + var result = Parser.Default.ParseArguments(args) + .WithParsed(ValidateOptions) + .WithNotParsed(HandleParseError); + + var persistentData = LoadPersisentData(result.Value.PersistentData); + var checksums = LoadChecksums(result.Value.Checksums); + var maker = new ReleaseNotesModel(persistentData, checksums); + var changelog = File.ReadAllLines(result.Value.Changelog).ToList(); + var releaseVersion = ChangelogParser.Parse(changelog, maker); + + var releaseNotes = maker.GetDocumentContents(); + + if (result.Value.Verbose) + { + Console.WriteLine(releaseNotes); + } + + // File name format: net-agent-10-13-0.mdx + var version = new Version(releaseVersion); + var filePath = $"{result.Value.Output}{Path.DirectorySeparatorChar}net-agent-{version.Major}-{version.Minor}-{version.Build}.mdx"; + File.WriteAllText(filePath, releaseNotes); + Console.WriteLine(filePath); + } + + private static void ValidateOptions(Options opts) + { + if (string.IsNullOrWhiteSpace(opts.Changelog) + || string.IsNullOrWhiteSpace(opts.PersistentData) + || string.IsNullOrWhiteSpace(opts.Checksums) + || string.IsNullOrWhiteSpace(opts.Output)) + { + ExitWithError(ExitCode.BadArguments, "Arguments were empty or whitespace."); + } + + if (!File.Exists(opts.PersistentData)) + { + ExitWithError(ExitCode.FileNotFound, $"Persistent data file did not exist at {opts.PersistentData}."); + } + + if (!File.Exists(opts.Changelog)) + { + ExitWithError(ExitCode.FileNotFound, $"Changelog file did not exist at {opts.Changelog}."); + } + + if (!File.Exists(opts.Checksums)) + { + ExitWithError(ExitCode.FileNotFound, $"Checksums file did not exist at {opts.Checksums}."); + } + + if (!Path.Exists(opts.Output)) + { + ExitWithError(ExitCode.FileNotFound, $"Output path did not exist at {opts.Output}."); + } + } + + private static PersistentData LoadPersisentData(string path) + { + try + { + var input = File.ReadAllText(path); + var deserializer = new YamlDotNet.Serialization.Deserializer(); + return deserializer.Deserialize(input); + } + catch (Exception ex) + { + ExitWithError(ExitCode.InvalidData, "Error loading persustent data: " + Environment.NewLine + ex.Message); + return new PersistentData(); ; + } + } + + private static string LoadChecksums(string path) + { + try + { + // This file contains exactly what goes into the release notes, no changes needed. + return File.ReadAllText(path); + } + catch (Exception ex) + { + ExitWithError(ExitCode.InvalidData, "Error loading checksums: " + Environment.NewLine + ex.Message); + return string.Empty; + } + + } + + private static void HandleParseError(IEnumerable errs) + { + ExitWithError(ExitCode.BadArguments, "Error occurred while parsing command line arguments."); + } + + public static void ExitWithError(ExitCode exitCode, string message) + { + Console.WriteLine(message); + Environment.Exit((int)exitCode); + } + } +} diff --git a/build/ReleaseNotesBuilder/ReleaseNotesBuilder.csproj b/build/ReleaseNotesBuilder/ReleaseNotesBuilder.csproj new file mode 100644 index 0000000000..4e6ebbe8e3 --- /dev/null +++ b/build/ReleaseNotesBuilder/ReleaseNotesBuilder.csproj @@ -0,0 +1,26 @@ + + + + Exe + net7.0 + 11.0 + enable + enable + + + + + + + + + PreserveNewest + + + + + + + + + \ No newline at end of file diff --git a/build/ReleaseNotesBuilder/ReleaseNotesModel.cs b/build/ReleaseNotesBuilder/ReleaseNotesModel.cs new file mode 100644 index 0000000000..270d543751 --- /dev/null +++ b/build/ReleaseNotesBuilder/ReleaseNotesModel.cs @@ -0,0 +1,196 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; + +namespace ReleaseNotesBuilder +{ + internal class ReleaseNotesModel + { + private const string FrontMatterWrapper = "---"; + private const string FrontMatterSubject = "subject"; + private const string FrontMatterReleaseDate = "releaseDate"; + private const string FrontMatterVersion = "version"; + private const string FrontMatterDownloadLink = "downloadLink"; + private const string FrontMatterFeatures = "features"; + private const string FrontMatterBugs = "bugs"; + private const string FrontMatterSecurity = "security"; + + private readonly string? _subject; + private readonly string? _releaseDate; + private string? _releaseVersion; + private readonly string? _downloadLink; + private readonly List _frontFeatures; + private readonly List _frontBugs; + private readonly List _frontSecurity; + + private readonly string? _preamble; + private readonly List _bodyNewFeatures; + private readonly List _bodyFixes; + private readonly List _bodySecurity; + private readonly List _bodyNotice; + private readonly string _checksums; + private readonly string? _epilogue; + + public ReleaseNotesModel(PersistentData persistentData, string checksums) + { + _frontFeatures = new List(); + _frontBugs = new List(); + _frontSecurity = new List(); + _bodyNewFeatures = new List(); + _bodyFixes = new List(); + _bodySecurity = new List(); + _bodyNotice = new List(); + + _subject = persistentData.Subject; + _releaseDate = DateTime.Now.ToString("yyyy-MM-dd"); + _downloadLink = persistentData.DownloadLink; + _preamble = persistentData.Preamble; + _epilogue = persistentData.Epilogue; + _checksums = checksums; + } + + public string GetDocumentContents() + { + Validate(); + + var builder = new StringBuilder(); + builder.AppendLine(FrontMatterWrapper); + builder.AppendLine($"{FrontMatterSubject}: {_subject}"); + builder.AppendLine($"{FrontMatterReleaseDate}: '{_releaseDate}'"); + builder.AppendLine($"{FrontMatterVersion}: {_releaseVersion}"); + builder.AppendLine($"{FrontMatterDownloadLink}: '{_downloadLink}'"); + builder.AppendLine($"{FrontMatterFeatures}: [{string.Join(',', _frontFeatures)}]"); + builder.AppendLine($"{FrontMatterBugs}: [{string.Join(',', _frontBugs)}]"); + builder.AppendLine($"{FrontMatterSecurity}: [{string.Join(',', _frontSecurity)}]"); + builder.AppendLine(FrontMatterWrapper); + builder.AppendLine(); + + if (!string.IsNullOrWhiteSpace(_preamble)) + { + builder.AppendLine(_preamble); + builder.AppendLine(); + } + + AppendSection(builder, Program.NoticeSection, _bodyNotice); + AppendSection(builder, Program.SecuritySection, _bodySecurity); + AppendSection(builder, Program.NewFeaturesSection, _bodyNewFeatures); + AppendSection(builder, Program.FixesSection, _bodyFixes); + + builder.AppendLine(_checksums); + + if (!string.IsNullOrWhiteSpace(_epilogue)) + { + builder.AppendLine(); + builder.AppendLine(_epilogue); + } + + return builder.ToString(); + } + + private static void AppendSection(StringBuilder builder, string header, List entries) + { + if (entries.Any()) + { + builder.AppendLine(header); + builder.AppendLine(); + foreach (var entry in entries) + { + builder.AppendLine(entry); + } + + builder.AppendLine(); + } + } + + private void Validate() + { + var problems = new List(); + + CheckString(nameof(_subject), _subject); + CheckString(nameof(_releaseDate), _releaseDate); // not validating that date is today since this could be run for previous releases. + CheckString(nameof(_releaseVersion), _releaseVersion); + CheckString(nameof(_downloadLink), _downloadLink); + CheckString(nameof(_checksums), _checksums); + + // Not checking preamble or epilogue since they can be empty. + + CheckList($"{nameof(_frontFeatures)} and {nameof(_bodyNewFeatures)}", _frontFeatures, _bodyNewFeatures); + CheckList($"{nameof(_frontBugs)} and {nameof(_bodyFixes)}", _frontBugs, _bodyFixes); + CheckList($"{nameof(_frontSecurity)} and {nameof(_bodySecurity)}", _frontSecurity, _bodySecurity); + + // _bodyNotice does not need to be checked since it is singlular and optional. + + if (problems.Any()) + { + Program.ExitWithError(ExitCode.InvalidData, + "The following problems occurred building the release notes:" + + Environment.NewLine + " " + + string.Join(Environment.NewLine + " ", problems) + ); + } + + void CheckString(string name, string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + problems.Add($"Data for {name} was null or whitespace."); + } + } + + void CheckList(string name, List front, List body) + { + if (!front.Any() && !body.Any()) + { + return; + } + + if (front.Count != body.Count) + { + problems.Add($"Count for {name} did not match."); + } + } + } + + public void SetReleaseVersion(string releaseVersion) + { + _releaseVersion = releaseVersion; + } + + public void AddFeatures(List entries) + { + foreach (var entry in entries) + { + _frontFeatures.Add($"'{entry.Front}'"); + _bodyNewFeatures.Add(entry.Body); + } + } + + public void AddBugsAndFixes(List entries) + { + foreach (var entry in entries) + { + _frontBugs.Add($"'{entry.Front}'"); + _bodyFixes.Add(entry.Body); + } + } + + public void AddSecurity(List entries) + { + foreach (var entry in entries) + { + _frontSecurity.Add($"'{entry.Front}'"); + _bodySecurity.Add(entry.Body); + } + } + + public void AddNotice(List entries) + { + foreach (var entry in entries) + { + // Notice does not have a front-matter item. + _bodyNotice.Add(entry.Body); + } + } + } +} diff --git a/build/ReleaseNotesBuilder/data.yml b/build/ReleaseNotesBuilder/data.yml new file mode 100644 index 0000000000..02cba19bb4 --- /dev/null +++ b/build/ReleaseNotesBuilder/data.yml @@ -0,0 +1,20 @@ +# Copyright 2020 New Relic, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +subject: .NET agent +download-link: 'https://download.newrelic.com/dot_net_agent/latest_release' + +# This will appear immediately after the front matter and before any notices, features, fixes, or security items. +# Can be empty. +preamble: + +# This will appear immediately after the checksums section and will be the last portion of the file. +# Can be empty. +epilogue: | + ### Updating + + * Follow standard procedures to [update the .NET agent](/docs/agents/net-agent/installation-configuration/update-net-agent). + * If you're using a particularly old agent, review the list of major changes and procedures for [updating legacy .NET agents](/docs/agents/net-agent/troubleshooting/upgrade-legacy-net-agents). + + We recommend updating to the latest agent version as soon as it's available. If you can't upgrade to the latest version, update your agents to a version no more than 90 days old. Read more about [keeping your agent up to date](/docs/new-relic-solutions/new-relic-one/install-configure/update-new-relic-agent). + See the New Relic .NET agent [EOL policy doc](/docs/apm/agents/net-agent/getting-started/net-agent-eol-policy) for information about agent releases and support dates. \ No newline at end of file diff --git a/build/S3Validator/Configuration.cs b/build/S3Validator/Configuration.cs new file mode 100644 index 0000000000..8950f1ae2b --- /dev/null +++ b/build/S3Validator/Configuration.cs @@ -0,0 +1,17 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Validator +{ + public class Configuration + { + [YamlDotNet.Serialization.YamlMember(Alias = "base-url")] + public string? BaseUrl { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "directory-list")] + public List? DirectoryList { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "file-list")] + public List? FileList { get; set; } + } +} diff --git a/build/S3Validator/ExitCode.cs b/build/S3Validator/ExitCode.cs new file mode 100644 index 0000000000..b1bff6ba40 --- /dev/null +++ b/build/S3Validator/ExitCode.cs @@ -0,0 +1,13 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Validator +{ + enum ExitCode : int + { + Success = 0, + Error = 1, + FileNotFound = 2, + BadArguments = 160 + } +} diff --git a/build/S3Validator/FileDetails.cs b/build/S3Validator/FileDetails.cs new file mode 100644 index 0000000000..1f6067122c --- /dev/null +++ b/build/S3Validator/FileDetails.cs @@ -0,0 +1,14 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +namespace S3Validator +{ + public struct FileDetails + { + [YamlDotNet.Serialization.YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlDotNet.Serialization.YamlMember(Alias = "size")] + public long Size { get; set; } + } +} diff --git a/build/S3Validator/Options.cs b/build/S3Validator/Options.cs new file mode 100644 index 0000000000..b4742914b8 --- /dev/null +++ b/build/S3Validator/Options.cs @@ -0,0 +1,16 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace S3Validator +{ + public class Options + { + [Option('v', "version", Required = true, HelpText = "Version to validate.")] + public required string Version { get; set; } + + [Option('c', "config", Default = "config.yml", Required = false, HelpText = "Path to the configuration file.")] + public required string ConfigurationPath { get; set; } + } +} diff --git a/build/S3Validator/Program.cs b/build/S3Validator/Program.cs new file mode 100644 index 0000000000..6a6a350c4b --- /dev/null +++ b/build/S3Validator/Program.cs @@ -0,0 +1,126 @@ +// Copyright 2020 New Relic, Inc. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +using CommandLine; + +namespace S3Validator +{ + internal class Program + { + private const string VersionToken = @"{version}"; + + private static readonly HttpClient _client = new(); + + static void Main(string[] args) + { + var result = Parser.Default.ParseArguments(args) + .WithParsed(ValidateOptions) + .WithNotParsed(HandleParseError); + + var version = result.Value.Version; + var configuration = LoadConfiguration(result.Value.ConfigurationPath); + Validate(version, configuration); + Console.WriteLine("Valid."); + } + + private static void Validate(string version, Configuration configuration) + { + var tasks = new List>(); + foreach (var dir in configuration.DirectoryList!) + { + foreach (var fileDetail in configuration.FileList!) + { + var url = $"{configuration.BaseUrl}/{dir.Replace(VersionToken, version)}/{fileDetail.Name.Replace(VersionToken, version)}"; + var request = new HttpRequestMessage(HttpMethod.Head, url); + request.Options.TryAdd("expectedSize", fileDetail.Size); + tasks.Add(_client.SendAsync(request)); + } + } + + if (!tasks.Any()) + { + ExitWithError(ExitCode.Error, "There was nothing to validate."); + } + + var taskCompleted = Task.WaitAll(tasks.ToArray(), 10000); + if (!taskCompleted) + { + ExitWithError(ExitCode.Error, "Validation timed out waiting for HttpClient requests to complete."); + } + + var isValid = true; + var results = new List(); + foreach (var task in tasks) + { + var status = "Valid"; + + if (!task.Result.IsSuccessStatusCode) + { + isValid = false; + status = task.Result.StatusCode.ToString(); + } + else if (task.Result.Content.Headers.ContentLength < task.Result.RequestMessage?.Options.GetValue("expectedSize")) + { + isValid = false; + status = "FileSize"; + } + + results.Add($"{status,-12}{task.Result.RequestMessage?.RequestUri}"); + } + + if (!isValid) + { + ExitWithError(ExitCode.Error, "Validation failed. Results:" + Environment.NewLine + string.Join(Environment.NewLine, results)); + } + } + + private static void ValidateOptions(Options opts) + { + if (string.IsNullOrWhiteSpace(opts.Version) + || string.IsNullOrWhiteSpace(opts.ConfigurationPath)) + { + ExitWithError(ExitCode.BadArguments, "Arguments were empty or whitespace."); + } + + if (!Version.TryParse(opts.Version, out _)) + { + ExitWithError(ExitCode.Error, $"Version provided, '{opts.Version}', was not a valid version."); + } + + if (!File.Exists(opts.ConfigurationPath)) + { + ExitWithError(ExitCode.FileNotFound, $"Configuration file did not exist at {opts.ConfigurationPath}."); + } + } + + private static Configuration LoadConfiguration(string path) + { + var input = File.ReadAllText(path); + var deserializer = new YamlDotNet.Serialization.Deserializer(); + return deserializer.Deserialize(input); + } + + private static void HandleParseError(IEnumerable errs) + { + ExitWithError(ExitCode.BadArguments, "Error occurred while parsing command line arguments."); + } + + public static void ExitWithError(ExitCode exitCode, string message) + { + Console.WriteLine(message); + Environment.Exit((int)exitCode); + } + } + + public static class Helpers + { + + // Simplfy the TryGetValue into something more usable. + public static T? GetValue(this HttpRequestOptions options, string key) + { + options.TryGetValue(new HttpRequestOptionsKey(key), out var value); + return value; + } + } + +} diff --git a/build/S3Validator/S3Validator.csproj b/build/S3Validator/S3Validator.csproj new file mode 100644 index 0000000000..0219c691e2 --- /dev/null +++ b/build/S3Validator/S3Validator.csproj @@ -0,0 +1,26 @@ + + + + Exe + net7.0 + 11.0 + enable + enable + + + + + + + + + PreserveNewest + + + + + + + + + diff --git a/build/S3Validator/config.yml b/build/S3Validator/config.yml new file mode 100644 index 0000000000..d0f230f03b --- /dev/null +++ b/build/S3Validator/config.yml @@ -0,0 +1,66 @@ +# Copyright 2020 New Relic, Inc. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# BaseUrl +# Should include the full path the to root of the agent folder - no trailing slash. Example: https://download.newrelic.com/dot_net_agent +base-url: https://download.newrelic.com/dot_net_agent + +# DirectoryList +# A list of sub directories to check - no leading or trailing slash. The FileList will be checked in each directory. +# Use '{version}' in places where the version would be found. This will be replaced with the version supplied to the tool on execution. +# Example: 'previous_releases/10.13.0' becomes 'previous_releases/{version}' +directory-list: + - latest_release + - previous_releases/{version} + +# FileList +# Use the relative path, starting at each directory in DirectoryList, to each file and a minimum acceptable size. +# Do not use the exact size - this file should not need to be update just to change sizes under normal circumstances. +# Use '{version}' in places where the version would be found. This will be replaced with the version supplied to the tool on execution. +# Example: 'NewRelicDotNetAgent_10.13.0_x64.msi' becomes 'NewRelicDotNetAgent_{version}_x64.msi' +file-list: + - name: NewRelicDotNetAgent_{version}_x64.msi + size: 13000000 + - name: NewRelicDotNetAgent_{version}_x64.zip + size: 11500000 + - name: NewRelicDotNetAgent_{version}_x86.msi + size: 12500000 + - name: NewRelicDotNetAgent_{version}_x86.zip + size: 11500000 + - name: NewRelicDotNetAgent_x64.msi + size: 13000000 + - name: NewRelicDotNetAgent_x86.msi + size: 12500000 + - name: Readme.txt + size: 1500 + - name: newrelic-dotnet-agent-{version}-1.x86_64.rpm + size: 3000000 + - name: newrelic-dotnet-agent_{version}_amd64.deb + size: 2500000 + - name: newrelic-dotnet-agent_{version}_amd64.tar.gz + size: 3900000 + - name: newrelic-dotnet-agent_{version}_arm64.deb + size: 2100000 + - name: newrelic-dotnet-agent_{version}_arm64.tar.gz + size: 3700000 + - name: SHA256/NewRelicDotNetAgent_{version}_x64.msi.sha256 + size: 58 + - name: SHA256/NewRelicDotNetAgent_{version}_x64.zip.sha256 + size: 58 + - name: SHA256/NewRelicDotNetAgent_{version}_x86.msi.sha256 + size: 58 + - name: SHA256/NewRelicDotNetAgent_{version}_x86.zip.sha256 + size: 58 + - name: SHA256/checksums.md + size: 800 + - name: SHA256/newrelic-dotnet-agent-{version}-1.x86_64.rpm.sha256 + size: 95 + - name: SHA256/newrelic-dotnet-agent_{version}_amd64.deb.sha256 + size: 95 + - name: SHA256/newrelic-dotnet-agent_{version}_amd64.tar.gz.sha256 + size: 95 + - name: SHA256/newrelic-dotnet-agent_{version}_arm64.deb.sha256 + size: 95 + - name: SHA256/newrelic-dotnet-agent_{version}_arm64.tar.gz.sha256 + size: 95 + diff --git a/deploy/validation/validate-yum/Dockerfile b/deploy/validation/validate-yum/Dockerfile new file mode 100644 index 0000000000..5dc6d8056a --- /dev/null +++ b/deploy/validation/validate-yum/Dockerfile @@ -0,0 +1,9 @@ +FROM rockylinux:9.2.20230513 + +RUN yum install wget -y \ + && wget https://download.newrelic.com/548C16BF.gpg -O /etc/pki/rpm-gpg/RPM-GPG-KEY-NewRelic \ + && rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-NewRelic + +COPY --chmod=777 check-version.sh /tmp/ + +ENTRYPOINT ["/tmp/check-version.sh"] diff --git a/deploy/validation/validate-yum/check-version.sh b/deploy/validation/validate-yum/check-version.sh new file mode 100644 index 0000000000..c68180dae0 --- /dev/null +++ b/deploy/validation/validate-yum/check-version.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +cat << REPO | tee "/etc/yum.repos.d/newrelic-dotnet-agent.repo" +[newrelic-dotnet-agent-repo] +name=New Relic .NET Core packages for Enterprise Linux +baseurl=https://yum.newrelic.com/pub/newrelic/el7/\$basearch +enabled=1 +gpgcheck=1 +gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-NewRelic +REPO + +yum install newrelic-dotnet-agent -y + +rpm -q newrelic-dotnet-agent