From 6990f1efe54b67a66e502e11014265e3bb77881e Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Fri, 24 Jan 2025 14:39:16 +0000 Subject: [PATCH 1/8] linuxsettings: implement default settings for Linux --- docs/enterprise-config.md | 31 ++++++- .../Interop/Linux/LinuxConfigParserTests.cs | 57 ++++++++++++ .../Interop/Linux/LinuxSettingsTests.cs | 47 ++++++++++ src/shared/Core/CommandContext.cs | 2 +- src/shared/Core/Constants.cs | 1 + .../Core/Interop/Linux/LinuxConfigParser.cs | 62 +++++++++++++ .../Core/Interop/Linux/LinuxSettings.cs | 93 +++++++++++++++++++ .../Core/Interop/MacOS/MacOSSettings.cs | 2 +- .../Core/Interop/Windows/WindowsSettings.cs | 2 +- src/shared/Core/Settings.cs | 2 +- 10 files changed, 294 insertions(+), 5 deletions(-) create mode 100644 src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs create mode 100644 src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs create mode 100644 src/shared/Core/Interop/Linux/LinuxConfigParser.cs create mode 100644 src/shared/Core/Interop/Linux/LinuxSettings.cs diff --git a/docs/enterprise-config.md b/docs/enterprise-config.md index 97544a33f..d0cf30aba 100644 --- a/docs/enterprise-config.md +++ b/docs/enterprise-config.md @@ -88,7 +88,36 @@ $ defaults read git-credential-manager configuration ## Linux -Default configuration setting stores has not been implemented. +Default settings values come from the `/etc/git-credential-manager/config.d` +directory. Each file in this directory represents a single settings dictionary. + +All files in this directory are read at runtime and merged into a single +collection of settings, in the order they are read from the file system. +To provide a stable ordering, it is recommended to prefix each filename with a +number, e.g. `42-my-settings`. + +The format of each file is a simple set of key/value pairs, separated by an +`=` sign, and each line separated by a line-feed (`\n`, LF) character. +Comments are identified by a `#` character at the beginning of a line. + +For example: + +```text +# /etc/git-credential-manager/config.d/00-example1 +credential.noguiprompt=0 +``` + +```text +# /etc/git-credential-manager/config.d/01-example2 +credential.trace=true +credential.traceMsAuth=true +``` + +All settings names and values are the same as the [Git configuration][config] +reference. + +> Note: These files are read once at startup. If changes are made to these files +they will not be reflected in an already running process. [environment]: environment.md [config]: configuration.md diff --git a/src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs b/src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs new file mode 100644 index 000000000..b31a3be30 --- /dev/null +++ b/src/shared/Core.Tests/Interop/Linux/LinuxConfigParserTests.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using GitCredentialManager.Interop.Linux; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests.Interop.Linux; + +public class LinuxConfigParserTests +{ + [Fact] + public void LinuxConfigParser_Parse() + { + const string contents = + """ + # + # This is a config file complete with comments + # and empty.. + + # lines, as well as lines with.. + # + # only whitespace (like above ^), and.. + invalid lines like this one, not a comment + # Here's the first real properties: + core.overrideMe=This is the first config value + baz.specialChars=I contain special chars like = in my value # this is a comment + # and let's have with a comment that also contains a = in side + # + core.overrideMe=This is the second config value + bar.scope.foo=123456 + core.overrideMe=This is the correct value + ###### comments that start ## with whitespace and extra ## inside + strings.one="here we have a dq string" + strings.two='here we have a sq string' + strings.three= 'here we have another sq string' # have another sq string + strings.four="this has 'nested quotes' inside" + strings.five='mixed "quotes" the other way around' + strings.six='this has an \'escaped\' set of quotes' + """; + + var expected = new Dictionary + { + ["core.overrideMe"] = "This is the correct value", + ["bar.scope.foo"] = "123456", + ["baz.specialChars"] = "I contain special chars like = in my value", + ["strings.one"] = "here we have a dq string", + ["strings.two"] = "here we have a sq string", + ["strings.three"] = "here we have another sq string", + ["strings.four"] = "this has 'nested quotes' inside", + ["strings.five"] = "mixed \"quotes\" the other way around", + ["strings.six"] = "this has an \\'escaped\\' set of quotes", + }; + + var parser = new LinuxConfigParser(new NullTrace()); + + Assert.Equal(expected, parser.Parse(contents)); + } +} \ No newline at end of file diff --git a/src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs b/src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs new file mode 100644 index 000000000..7b9d7e893 --- /dev/null +++ b/src/shared/Core.Tests/Interop/Linux/LinuxSettingsTests.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using GitCredentialManager.Interop.Linux; +using GitCredentialManager.Tests.Objects; +using Xunit; + +namespace GitCredentialManager.Tests.Interop.Linux; + +public class LinuxSettingsTests +{ + [LinuxFact] + public void LinuxSettings_TryGetExternalDefault_CombinesFiles() + { + var env = new TestEnvironment(); + var git = new TestGit(); + var trace = new NullTrace(); + var fs = new TestFileSystem(); + + var utf8 = EncodingEx.UTF8NoBom; + + fs.Directories = new HashSet + { + "/", + "/etc", + "/etc/git-credential-manager", + "/etc/git-credential-manager/config.d" + }; + + const string config1 = "core.overrideMe=value1"; + const string config2 = "core.overrideMe=value2"; + const string config3 = "core.overrideMe=value3"; + + fs.Files = new Dictionary + { + ["/etc/git-credential-manager/config.d/01-first"] = utf8.GetBytes(config1), + ["/etc/git-credential-manager/config.d/02-second"] = utf8.GetBytes(config2), + ["/etc/git-credential-manager/config.d/03-third"] = utf8.GetBytes(config3), + }; + + var settings = new LinuxSettings(env, git, trace, fs); + + bool result = settings.TryGetExternalDefault( + "core", null, "overrideMe", out string value); + + Assert.True(result); + Assert.Equal("value3", value); + } +} diff --git a/src/shared/Core/CommandContext.cs b/src/shared/Core/CommandContext.cs index d3ef1dbf6..b5bade471 100644 --- a/src/shared/Core/CommandContext.cs +++ b/src/shared/Core/CommandContext.cs @@ -148,7 +148,7 @@ public CommandContext() gitPath, FileSystem.GetCurrentDirectory() ); - Settings = new Settings(Environment, Git); + Settings = new LinuxSettings(Environment, Git, Trace, FileSystem); } else { diff --git a/src/shared/Core/Constants.cs b/src/shared/Core/Constants.cs index 4777b0cf8..93346984b 100644 --- a/src/shared/Core/Constants.cs +++ b/src/shared/Core/Constants.cs @@ -15,6 +15,7 @@ public static class Constants public const string AuthorityIdAuto = "auto"; public const string GcmDataDirectoryName = ".gcm"; + public const string LinuxAppDefaultsDirectoryPath = "/etc/git-credential-manager/config.d"; public const string MacOSBundleId = "git-credential-manager"; public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf"); diff --git a/src/shared/Core/Interop/Linux/LinuxConfigParser.cs b/src/shared/Core/Interop/Linux/LinuxConfigParser.cs new file mode 100644 index 000000000..1caa918fc --- /dev/null +++ b/src/shared/Core/Interop/Linux/LinuxConfigParser.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace GitCredentialManager.Interop.Linux; + +public class LinuxConfigParser +{ +#if NETFRAMEWORK + private const string SQ = "'"; + private const string DQ = "\""; + private const string Hash = "#"; +#else + private const char SQ = '\''; + private const char DQ = '"'; + private const char Hash = '#'; +#endif + + private static readonly Regex LineRegex = new(@"^\s*(?[a-zA-Z0-9\.-]+)\s*=\s*(?.+?)\s*(?:#.*)?$"); + + private readonly ITrace _trace; + + public LinuxConfigParser(ITrace trace) + { + EnsureArgument.NotNull(trace, nameof(trace)); + + _trace = trace; + } + + public IDictionary Parse(string content) + { + var result = new Dictionary(GitConfigurationKeyComparer.Instance); + + IEnumerable lines = content.Split(['\n'], StringSplitOptions.RemoveEmptyEntries); + + foreach (string line in lines) + { + // Ignore empty lines or full-line comments + var trimmedLine = line.Trim(); + if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(Hash)) + continue; + + var match = LineRegex.Match(trimmedLine); + if (!match.Success) + { + _trace.WriteLine($"Invalid config line format: {line}"); + continue; + } + + string key = match.Groups["key"].Value; + string value = match.Groups["value"].Value; + + // Remove enclosing quotes from the value, if any + if ((value.StartsWith(DQ) && value.EndsWith(DQ)) || (value.StartsWith(SQ) && value.EndsWith(SQ))) + value = value.Substring(1, value.Length - 2); + + result[key] = value; + } + + return result; + } +} diff --git a/src/shared/Core/Interop/Linux/LinuxSettings.cs b/src/shared/Core/Interop/Linux/LinuxSettings.cs new file mode 100644 index 000000000..4f00420fd --- /dev/null +++ b/src/shared/Core/Interop/Linux/LinuxSettings.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Avalonia.Markup.Xaml.MarkupExtensions; + +namespace GitCredentialManager.Interop.Linux; + +public class LinuxSettings : Settings +{ + private readonly ITrace _trace; + private readonly IFileSystem _fs; + + private IDictionary _extConfigCache; + + /// + /// Reads settings from Git configuration, environment variables, and defaults from the + /// /etc/git-credential-manager.d app configuration directory. + /// + public LinuxSettings(IEnvironment environment, IGit git, ITrace trace, IFileSystem fs) + : base(environment, git) + { + EnsureArgument.NotNull(trace, nameof(trace)); + EnsureArgument.NotNull(fs, nameof(fs)); + + _trace = trace; + _fs = fs; + + PlatformUtils.EnsureLinux(); + } + + protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value) + { + value = null; + + _extConfigCache ??= ReadExternalConfiguration(); + + string name = string.IsNullOrWhiteSpace(scope) + ? $"{section}.{property}" + : $"{section}.{scope}.{property}"; + + // Check if the setting exists in the configuration + if (!_extConfigCache?.TryGetValue(name, out value) ?? false) + { + // No property exists (or failed to read config) + return false; + } + + _trace.WriteLine($"Default setting found in app configuration directory: {name}={value}"); + return true; + } + + private IDictionary ReadExternalConfiguration() + { + try + { + // Check for system-wide config files in /etc/git-credential-manager/config.d and concatenate them together + // in alphabetical order to form a single configuration. + const string configDir = Constants.LinuxAppDefaultsDirectoryPath; + if (!_fs.DirectoryExists(configDir)) + { + // No configuration directory exists + return null; + } + + // Get all the files in the configuration directory + IEnumerable files = _fs.EnumerateFiles(configDir, "*"); + + // Read the contents of each file and concatenate them together + var combinedFile = new StringBuilder(); + foreach (string file in files) + { + using Stream stream = _fs.OpenFileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new StreamReader(stream); + string contents = reader.ReadToEnd(); + combinedFile.Append(contents); + combinedFile.Append('\n'); + } + + var parser = new LinuxConfigParser(_trace); + + return parser.Parse(combinedFile.ToString()); + } + catch (Exception ex) + { + // Reading defaults is not critical to the operation of the application + // so we can ignore any errors and just log the failure. + _trace.WriteLine("Failed to read default setting from app configuration directory."); + _trace.WriteException(ex); + return null; + } + } +} \ No newline at end of file diff --git a/src/shared/Core/Interop/MacOS/MacOSSettings.cs b/src/shared/Core/Interop/MacOS/MacOSSettings.cs index 3ef2c8247..9b7677a72 100644 --- a/src/shared/Core/Interop/MacOS/MacOSSettings.cs +++ b/src/shared/Core/Interop/MacOS/MacOSSettings.cs @@ -19,7 +19,7 @@ public MacOSSettings(IEnvironment environment, IGit git, ITrace trace) PlatformUtils.EnsureMacOS(); } - protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) + protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value) { value = null; diff --git a/src/shared/Core/Interop/Windows/WindowsSettings.cs b/src/shared/Core/Interop/Windows/WindowsSettings.cs index abdd9ee0e..888e9aa7d 100644 --- a/src/shared/Core/Interop/Windows/WindowsSettings.cs +++ b/src/shared/Core/Interop/Windows/WindowsSettings.cs @@ -17,7 +17,7 @@ public WindowsSettings(IEnvironment environment, IGit git, ITrace trace) PlatformUtils.EnsureWindows(); } - protected override bool TryGetExternalDefault(string section, string scope, string property, out string value) + protected internal override bool TryGetExternalDefault(string section, string scope, string property, out string value) { value = null; diff --git a/src/shared/Core/Settings.cs b/src/shared/Core/Settings.cs index 0e24ce9a3..af3dcf99c 100644 --- a/src/shared/Core/Settings.cs +++ b/src/shared/Core/Settings.cs @@ -489,7 +489,7 @@ public IEnumerable GetSettingValues(string envarName, string section, st /// Configuration property name. /// Value of the configuration setting, or null. /// True if a default setting has been set, false otherwise. - protected virtual bool TryGetExternalDefault(string section, string scope, string property, out string value) + protected internal virtual bool TryGetExternalDefault(string section, string scope, string property, out string value) { value = null; return false; From 3e404d3055c9aa97918036b55e3aecaf5274828e Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 14 Jan 2026 10:14:02 +0000 Subject: [PATCH 2/8] release: ifdef compile ESRP steps Move from a runtime `condition` on the ESRP steps to a YAML parse-time condition so that no usage of the `esrp*ConnectionName` variables exist when the esrp parameter is false. This means we can avoid the need to approve runs of this workflow in the internal secure environment when not accessing ESRP (for example, when debugging and testing the release process). Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 696 ++++++++++++++++++----------------- 1 file changed, 350 insertions(+), 346 deletions(-) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index aaf5e2d43..9c0a36e60 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -157,46 +157,46 @@ extends: includeRootFolder: false archiveType: zip archiveFile: '$(Build.ArtifactStagingDirectory)\symbols\gcm-${{ dim.runtime }}-$(version)-symbols.zip' - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)\payload' - pattern: | - **/*.exe - **/*.dll - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "https://www.microsoft.com", - "FileDigest": "/fd SHA256", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + displayName: 'Sign payload' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)\payload' + pattern: | + **/*.exe + **/*.dll + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} } - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + ] - task: PowerShell@2 displayName: 'Build installers' inputs: @@ -209,46 +209,47 @@ extends: -p:PayloadPath="$(Build.ArtifactStagingDirectory)\payload" ` -p:OutputPath="$(Build.ArtifactStagingDirectory)\installers" ` -p:RuntimeIdentifier="${{ dim.runtime }}" - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign installers' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)\installers' - pattern: '**/*.exe' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "https://www.microsoft.com", - "FileDigest": "/fd SHA256", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) + displayName: 'Sign installers' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)\installers' + pattern: '**/*.exe' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} } - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + ] - task: ArchiveFiles@2 - displayName: 'Archive signed payload' + displayName: 'Archive payload' inputs: rootFolderOrFile: '$(Build.ArtifactStagingDirectory)\payload' includeRootFolder: false @@ -311,105 +312,105 @@ extends: archiveType: tar tarCompression: gz archiveFile: '$(Build.ArtifactStagingDirectory)/symbols/gcm-${{ dim.runtime }}-$(version)-symbols.tar.gz' - - task: AzureKeyVault@2 - displayName: 'Download developer certificate' - inputs: - azureSubscription: '$(esrpMIConnectionName)' - keyVaultName: '$(esrpKeyVaultName)' - secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity' - - task: Bash@3 - displayName: 'Import developer certificate' - inputs: - targetType: inline - script: | - # Create and unlock a keychain for the developer certificate - security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain - security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain - security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain + - ${{ if eq(parameters.esrp, true) }}: + - task: AzureKeyVault@2 + displayName: 'Download developer certificate' + inputs: + azureSubscription: '$(esrpMIConnectionName)' + keyVaultName: '$(esrpKeyVaultName)' + secretsFilter: 'mac-developer-certificate,mac-developer-certificate-password,mac-developer-certificate-identity' + - task: Bash@3 + displayName: 'Import developer certificate' + inputs: + targetType: inline + script: | + # Create and unlock a keychain for the developer certificate + security create-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain + security default-keychain -s $(Agent.TempDirectory)/buildagent.keychain + security unlock-keychain -p pwd $(Agent.TempDirectory)/buildagent.keychain - echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12 - echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password + echo $(mac-developer-certificate) | base64 -D > $(Agent.TempDirectory)/cert.p12 + echo $(mac-developer-certificate-password) > $(Agent.TempDirectory)/cert.password - # Import the developer certificate - security import $(Agent.TempDirectory)/cert.p12 \ - -k $(Agent.TempDirectory)/buildagent.keychain \ - -P "$(mac-developer-certificate-password)" \ - -T /usr/bin/codesign + # Import the developer certificate + security import $(Agent.TempDirectory)/cert.p12 \ + -k $(Agent.TempDirectory)/buildagent.keychain \ + -P "$(mac-developer-certificate-password)" \ + -T /usr/bin/codesign - # Clean up the cert file immediately after import - rm $(Agent.TempDirectory)/cert.p12 + # Clean up the cert file immediately after import + rm $(Agent.TempDirectory)/cert.p12 - # Set ACLs to allow codesign to access the private key - security set-key-partition-list \ - -S apple-tool:,apple:,codesign: \ - -s -k pwd \ - $(Agent.TempDirectory)/buildagent.keychain - - task: Bash@3 - displayName: 'Developer sign payload files' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload + # Set ACLs to allow codesign to access the private key + security set-key-partition-list \ + -S apple-tool:,apple:,codesign: \ + -s -k pwd \ + $(Agent.TempDirectory)/buildagent.keychain + - task: Bash@3 + displayName: 'Developer sign payload files' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/tosign/payload - # Copy the files that need signing (Mach-o executables and dylibs) - pushd $(Build.ArtifactStagingDirectory)/payload - find . -type f -exec file --mime {} + \ - | sed -n '/mach/s/: .*//p' \ - | while IFS= read -r f; do - rel="${f#./}" - tgt="$(Build.ArtifactStagingDirectory)/tosign/payload/$rel" - mkdir -p "$(dirname "$tgt")" - cp -- "$f" "$tgt" - done - popd + # Copy the files that need signing (Mach-o executables and dylibs) + pushd $(Build.ArtifactStagingDirectory)/payload + find . -type f -exec file --mime {} + \ + | sed -n '/mach/s/: .*//p' \ + | while IFS= read -r f; do + rel="${f#./}" + tgt="$(Build.ArtifactStagingDirectory)/tosign/payload/$rel" + mkdir -p "$(dirname "$tgt")" + cp -- "$f" "$tgt" + done + popd - # Developer sign the files - ./src/osx/Installer.Mac/codesign.sh \ - "$(Build.ArtifactStagingDirectory)/tosign/payload" \ - "$(mac-developer-certificate-identity)" \ - "$PWD/src/osx/Installer.Mac/entitlements.xml" - # ESRP code signing for macOS requires the files be packaged in a zip file for submission - - task: ArchiveFiles@2 - displayName: 'Archive files for signing' - inputs: - rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload' - includeRootFolder: false - archiveType: zip - archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip' - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'payload.zip' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401337-Apple", - "OperationCode": "MacAppDeveloperSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "Hardening": "Enable" + # Developer sign the files + ./src/osx/Installer.Mac/codesign.sh \ + "$(Build.ArtifactStagingDirectory)/tosign/payload" \ + "$(mac-developer-certificate-identity)" \ + "$PWD/src/osx/Installer.Mac/entitlements.xml" + # ESRP code signing for macOS requires the files be packaged in a zip file for submission + - task: ArchiveFiles@2 + displayName: 'Archive files for signing' + inputs: + rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/tosign/payload' + includeRootFolder: false + archiveType: zip + archiveFile: '$(Build.ArtifactStagingDirectory)/tosign/payload.zip' + - task: EsrpCodeSigning@5 + displayName: 'Sign payload' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/tosign' + pattern: 'payload.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } } - } - ] - # Extract signed files, overwriting the unsigned files, ready for packaging - - task: Bash@3 - displayName: 'Extract signed payload files' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload + ] + # Extract signed files, overwriting the unsigned files, ready for packaging + - task: Bash@3 + displayName: 'Extract signed payload files' + inputs: + targetType: inline + script: | + unzip -uo $(Build.ArtifactStagingDirectory)/tosign/payload.zip -d $(Build.ArtifactStagingDirectory)/payload - task: Bash@3 displayName: 'Build component package' inputs: @@ -428,94 +429,95 @@ extends: --version="$(version)" \ --runtime="${{ dim.runtime }}" \ --package-path="$(Build.ArtifactStagingDirectory)/pkg" \ - --output="$(Build.ArtifactStagingDirectory)/installers-presign/gcm-${{ dim.runtime }}-$(version).pkg" - # ESRP code signing for macOS requires the files be packaged in a zip file first - - task: Bash@3 - displayName: 'Prepare installer package for signing' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign - cd $(Build.ArtifactStagingDirectory)/installers-presign - zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers-presign.zip *.pkg - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign installer package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'installers-presign.zip' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401337-Apple", - "OperationCode": "MacAppDeveloperSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "Hardening": "Enable" + --output="$(Build.ArtifactStagingDirectory)/installers/gcm-${{ dim.runtime }}-$(version).pkg" + - ${{ if eq(parameters.esrp, true) }}: + # ESRP code signing for macOS requires the files be packaged in a zip file first + - task: Bash@3 + displayName: 'Prepare installer package for signing' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/tosign + cd $(Build.ArtifactStagingDirectory)/installers + zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg + - task: EsrpCodeSigning@5 + displayName: 'Sign installer package' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/tosign' + pattern: 'installers.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppDeveloperSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "Hardening": "Enable" + } } - } - ] - # Extract signed installer, overwriting the unsigned installer - - task: Bash@3 - displayName: 'Extract signed installer package' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers-presign.zip -d $(Build.ArtifactStagingDirectory)/installers - - task: Bash@3 - displayName: 'Prepare installer package for notarization' - inputs: - targetType: inline - script: | - mkdir -p $(Build.ArtifactStagingDirectory)/tosign - cd $(Build.ArtifactStagingDirectory)/installers - zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Notarize installer package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/tosign' - pattern: 'installers.zip' - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401337-Apple", - "OperationCode": "MacAppNotarize", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "BundleId": "com.microsoft.gitcredentialmanager" + ] + # Extract signed installer, overwriting the unsigned installer + - task: Bash@3 + displayName: 'Extract signed installer package' + inputs: + targetType: inline + script: | + unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers + - task: Bash@3 + displayName: 'Prepare installer package for notarization' + inputs: + targetType: inline + script: | + mkdir -p $(Build.ArtifactStagingDirectory)/tosign + cd $(Build.ArtifactStagingDirectory)/installers + # Remove previous installers.zip to avoid any confusion + rm -f $(Build.ArtifactStagingDirectory)/tosign/installers.zip + zip -rX $(Build.ArtifactStagingDirectory)/tosign/installers.zip *.pkg + - task: EsrpCodeSigning@5 + displayName: 'Notarize installer package' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/tosign' + pattern: 'installers.zip' + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401337-Apple", + "OperationCode": "MacAppNotarize", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "BundleId": "com.microsoft.gitcredentialmanager" + } } - } - ] - # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload - - task: Bash@3 - displayName: 'Extract signed and notarized installer package' - inputs: - targetType: inline - script: | - unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers + ] + # Extract signed and notarized installer pkg files, overwriting the unsigned files, ready for upload + - task: Bash@3 + displayName: 'Extract signed and notarized installer package' + inputs: + targetType: inline + script: | + unzip -uo $(Build.ArtifactStagingDirectory)/tosign/installers.zip -d $(Build.ArtifactStagingDirectory)/installers - task: ArchiveFiles@2 - displayName: 'Archive signed payload' + displayName: 'Archive payload' inputs: rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/payload' includeRootFolder: false @@ -595,32 +597,32 @@ extends: mkdir -p $(Build.ArtifactStagingDirectory)/installers mv $(Build.ArtifactStagingDirectory)/pkg/tar/*.tar.gz $(Build.ArtifactStagingDirectory)/installers mv $(Build.ArtifactStagingDirectory)/pkg/deb/*.deb $(Build.ArtifactStagingDirectory)/installers - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign Debian package' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/installers' - pattern: | - **/*.deb - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-453387-Pgp", - "OperationCode": "LinuxSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + displayName: 'Sign Debian package' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/installers' + pattern: | + **/*.deb + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-453387-Pgp", + "OperationCode": "LinuxSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] - task: Bash@3 displayName: 'Collect artifacts for publishing' inputs: @@ -672,46 +674,47 @@ extends: arguments: | -Configuration Release ` -Output "$(Build.ArtifactStagingDirectory)/nupkg" - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign payload' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/nupkg' - pattern: | - **/*.exe - **/*.dll - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "https://www.microsoft.com", - "FileDigest": "/fd SHA256", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) + displayName: 'Sign payload' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/nupkg' + pattern: | + **/*.exe + **/*.dll + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "https://www.microsoft.com", + "FileDigest": "/fd SHA256", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} } - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + ] - task: PowerShell@2 displayName: 'Create NuGet packages' inputs: @@ -722,33 +725,34 @@ extends: -Version "$(version)" ` -PackageRoot "$(Build.ArtifactStagingDirectory)/nupkg" ` -Output "$(Build.ArtifactStagingDirectory)/packages" - - task: EsrpCodeSigning@5 - condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) - displayName: 'Sign NuGet packages' - inputs: - connectedServiceName: '$(esrpAppConnectionName)' - useMSIAuthentication: true - appRegistrationClientId: '$(esrpClientId)' - appRegistrationTenantId: '$(esrpTenantId)' - authAkvName: '$(esrpKeyVaultName)' - authSignCertName: '$(esrpSignReqCertName)' - serviceEndpointUrl: '$(esrpEndpointUrl)' - folderPath: '$(Build.ArtifactStagingDirectory)/packages' - pattern: | - **/*.nupkg - **/*.snupkg - useMinimatch: true - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "ToolName": "sign", - "ToolVersion": "1.0", - "Parameters": {} - } - ] + - ${{ if eq(parameters.esrp, true) }}: + - task: EsrpCodeSigning@5 + condition: and(succeeded(), eq('${{ parameters.esrp }}', true)) + displayName: 'Sign NuGet packages' + inputs: + connectedServiceName: '$(esrpAppConnectionName)' + useMSIAuthentication: true + appRegistrationClientId: '$(esrpClientId)' + appRegistrationTenantId: '$(esrpTenantId)' + authAkvName: '$(esrpKeyVaultName)' + authSignCertName: '$(esrpSignReqCertName)' + serviceEndpointUrl: '$(esrpEndpointUrl)' + folderPath: '$(Build.ArtifactStagingDirectory)/packages' + pattern: | + **/*.nupkg + **/*.snupkg + useMinimatch: true + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "ToolName": "sign", + "ToolVersion": "1.0", + "Parameters": {} + } + ] - stage: release displayName: 'Release' From d273e554039840fdcf77008b5d9729ce51aab3d6 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 14 Jan 2026 12:27:01 +0000 Subject: [PATCH 3/8] linux/pack.sh: use standard filename format All other platforms use the package filename format: gcm(user)-$RUNTIME-$VERSION.$EXT But the Linux packages have been set to: gcm-$RUNTIME.$VERSION.$EXT Let's standardise on the `.` separator between runtime and version. Signed-off-by: Matthew John Cheetham --- src/linux/Packaging.Linux/pack.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/linux/Packaging.Linux/pack.sh b/src/linux/Packaging.Linux/pack.sh index 821d66ea1..2b3999267 100755 --- a/src/linux/Packaging.Linux/pack.sh +++ b/src/linux/Packaging.Linux/pack.sh @@ -84,12 +84,12 @@ if test -z "$RUNTIME"; then fi TAROUT="$OUTPUT_ROOT/tar" -TARBALL="$TAROUT/gcm-$RUNTIME.$VERSION.tar.gz" -SYMTARBALL="$TAROUT/gcm-$RUNTIME.$VERSION-symbols.tar.gz" +TARBALL="$TAROUT/gcm-$RUNTIME-$VERSION.tar.gz" +SYMTARBALL="$TAROUT/gcm-$RUNTIME-$VERSION-symbols.tar.gz" DEBOUT="$OUTPUT_ROOT/deb" DEBROOT="$DEBOUT/root" -DEBPKG="$DEBOUT/gcm-$RUNTIME.$VERSION.deb" +DEBPKG="$DEBOUT/gcm-$RUNTIME-$VERSION.deb" mkdir -p "$DEBROOT" # Set full read, write, execute permissions for owner and just read and execute permissions for group and other From de3b137862a400ea9aadada3f62aafdf157ec534 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 22 Jan 2026 12:44:51 +0000 Subject: [PATCH 4/8] release: cleanup CodeSignSummary.md files from Windows zips Remove the cruft code signing summary file from the Windows payload zips. There's nothing interesting in this file anyway. Signed-off-by: Matthew John Cheetham --- .azure-pipelines/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.azure-pipelines/release.yml b/.azure-pipelines/release.yml index 9c0a36e60..797a42d73 100644 --- a/.azure-pipelines/release.yml +++ b/.azure-pipelines/release.yml @@ -197,6 +197,12 @@ extends: "Parameters": {} } ] + - task: PowerShell@2 + displayName: 'Clean up code signing artifacts' + inputs: + targetType: inline + script: | + Remove-Item "$(Build.ArtifactStagingDirectory)\payload\CodeSignSummary-*.md" - task: PowerShell@2 displayName: 'Build installers' inputs: From faaa3b13e9b60ce5b0970eaf58e79448bd8e9df0 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 22 Jan 2026 12:54:01 +0000 Subject: [PATCH 5/8] Installer.Windows: fix up installer output path When building locally the gcm(user)-$RID-$VERSION.exe installer files were being written to the PayloadPath, rather than the OutputPath. Also, since we're now specifying a RID for all invocations of the project file, we should disable the appending of the RID to the OutputPath variable - we already have the RID as part of the PayloadPath (for keeping binaries separated by RID) and the resulting installer filenames also contain the RID anyway. Signed-off-by: Matthew John Cheetham --- .github/workflows/continuous-integration.yml | 2 +- src/windows/Installer.Windows/Installer.Windows.csproj | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7bb45f26a..94f275915 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,7 +51,7 @@ jobs: shell: bash run: | mkdir -p artifacts/bin - mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}/gcm*.exe artifacts/ + mv out/windows/Installer.Windows/bin/Release/net472/gcm*.exe artifacts/ mv out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }} artifacts/bin/ cp out/windows/Installer.Windows/bin/Release/net472/${{ matrix.runtime }}.sym/* artifacts/bin/${{ matrix.runtime }}/ diff --git a/src/windows/Installer.Windows/Installer.Windows.csproj b/src/windows/Installer.Windows/Installer.Windows.csproj index eae3631f0..ec678fe5f 100644 --- a/src/windows/Installer.Windows/Installer.Windows.csproj +++ b/src/windows/Installer.Windows/Installer.Windows.csproj @@ -13,8 +13,11 @@ net472 false false - $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier) + $(PlatformOutPath)Installer.Windows\bin\$(Configuration)\net472\$(RuntimeIdentifier)\ 6.3.1 + + false From 2d4143e40e93e2e9b4d5cec2af8e6174836c1cfc Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 22 Jan 2026 13:05:17 +0000 Subject: [PATCH 6/8] windows/layout.ps1: more robust handling of inputs We assume the input paths given to the Windows layout.ps1 scripts do not have a trailing slash - we should make sure this is the case before proceeding and doing path-math with that. Signed-off-by: Matthew John Cheetham --- src/windows/Installer.Windows/layout.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/windows/Installer.Windows/layout.ps1 b/src/windows/Installer.Windows/layout.ps1 index 3fc43ab36..3b1624896 100644 --- a/src/windows/Installer.Windows/layout.ps1 +++ b/src/windows/Installer.Windows/layout.ps1 @@ -1,6 +1,10 @@ # Inputs param ([Parameter(Mandatory)] $Configuration, [Parameter(Mandatory)] $Output, $RuntimeIdentifier, $SymbolOutput) +# Trim trailing slashes from output paths +$Output = $Output.TrimEnd('\','/') +$SymbolOutput = $SymbolOutput.TrimEnd('\','/') + Write-Output "Output: $Output" # Determine a runtime if one was not provided From 010818d81d5ea28e54a5543a6a4f72e0d43b5b72 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Tue, 3 Feb 2026 15:50:32 +0000 Subject: [PATCH 7/8] linuxsettings: fix bug reading linux defaults If there is no GCM configuration defaults directory on Linux, we had been inadvertently returning `null` for the setting value! Fix the issue by explictly returning `false` (no setting found) to `TryGetExternalDefault` rather than `true` (setting found). Signed-off-by: Matthew John Cheetham --- src/shared/Core/Interop/Linux/LinuxSettings.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/shared/Core/Interop/Linux/LinuxSettings.cs b/src/shared/Core/Interop/Linux/LinuxSettings.cs index 4f00420fd..af4c578a8 100644 --- a/src/shared/Core/Interop/Linux/LinuxSettings.cs +++ b/src/shared/Core/Interop/Linux/LinuxSettings.cs @@ -35,14 +35,17 @@ protected internal override bool TryGetExternalDefault(string section, string sc _extConfigCache ??= ReadExternalConfiguration(); + if (_extConfigCache is null) + return false; // No external config found (or failed to read) + string name = string.IsNullOrWhiteSpace(scope) ? $"{section}.{property}" : $"{section}.{scope}.{property}"; // Check if the setting exists in the configuration - if (!_extConfigCache?.TryGetValue(name, out value) ?? false) + if (!_extConfigCache.TryGetValue(name, out value)) { - // No property exists (or failed to read config) + // No property exists return false; } @@ -90,4 +93,4 @@ private IDictionary ReadExternalConfiguration() return null; } } -} \ No newline at end of file +} From 78e3c589f890b815eec0b17877230245b1abb0c5 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Wed, 4 Feb 2026 09:03:27 +0000 Subject: [PATCH 8/8] VERSION: bump to 2.7.1 Signed-off-by: Matthew John Cheetham --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 21b5059f4..ba4940bb1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.7.0.0 +2.7.1.0