From 55bd9f51632b4a6d91148d465a4b697f2f86a652 Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Sun, 26 Feb 2017 22:20:00 +0100 Subject: [PATCH 1/6] Add access to TeamCity build parameters --- src/app/FakeLib/TeamCityHelper.fs | 430 ++++++++++++++++++++++++++---- 1 file changed, 374 insertions(+), 56 deletions(-) diff --git a/src/app/FakeLib/TeamCityHelper.fs b/src/app/FakeLib/TeamCityHelper.fs index 99b9a292441..35524274c99 100644 --- a/src/app/FakeLib/TeamCityHelper.fs +++ b/src/app/FakeLib/TeamCityHelper.fs @@ -3,7 +3,7 @@ module Fake.TeamCityHelper /// Encapsulates special chars -let inline EncapsulateSpecialChars text = +let inline EncapsulateSpecialChars text = text |> replace "|" "||" |> replace "'" "|'" @@ -15,15 +15,15 @@ let inline EncapsulateSpecialChars text = let scrub = RemoveLineBreaks >> EncapsulateSpecialChars /// Send message to TeamCity -let sendToTeamCity format message = - if buildServer = TeamCity then +let sendToTeamCity format message = + if buildServer = TeamCity then message |> scrub |> sprintf format |> fun m -> postMessage (LogMessage(m, true)) /// Send message to TeamCity -let sendStrToTeamCity s = +let sendStrToTeamCity s = if buildServer = TeamCity then postMessage (LogMessage(RemoveLineBreaks s, true)) /// Open Named Block @@ -38,114 +38,114 @@ let sendTeamCityError error = sendToTeamCity "##teamcity[buildStatus status='FAI /// Sends an NUnit results filename to TeamCity let sendTeamCityNUnitImport path = sendToTeamCity "##teamcity[importData type='nunit' file='%s']" path -/// Sends an FXCop results filename to TeamCity +/// Sends an FXCop results filename to TeamCity let sendTeamCityFXCopImport path = sendToTeamCity "##teamcity[importData type='FxCop' path='%s']" path -/// Sends an JUnit Ant task results filename to TeamCity +/// Sends an JUnit Ant task results filename to TeamCity let sendTeamCityJUnitImport path = sendToTeamCity "##teamcity[importData type='junit' path='%s']" path -/// Sends an Maven Surefire results filename to TeamCity +/// Sends an Maven Surefire results filename to TeamCity let sendTeamCitySurefireImport path = sendToTeamCity "##teamcity[importData type='surefire' path='%s']" path -/// Sends an MSTest results filename to TeamCity +/// Sends an MSTest results filename to TeamCity let sendTeamCityMSTestImport path = sendToTeamCity "##teamcity[importData type='mstest' path='%s']" path -/// Sends an Google Test results filename to TeamCity +/// Sends an Google Test results filename to TeamCity let sendTeamCityGTestImport path = sendToTeamCity "##teamcity[importData type='gtest' path='%s']" path -/// Sends an Checkstyle results filename to TeamCity +/// Sends an Checkstyle results filename to TeamCity let sendTeamCityCheckstyleImport path = sendToTeamCity "##teamcity[importData type='checkstyle' path='%s']" path -/// Sends an FindBugs results filename to TeamCity +/// Sends an FindBugs results filename to TeamCity let sendTeamCityFindBugsImport path = sendToTeamCity "##teamcity[importData type='findBugs' path='%s']" path -/// Sends an JSLint results filename to TeamCity +/// Sends an JSLint results filename to TeamCity let sendTeamCityJSLintImport path = sendToTeamCity "##teamcity[importData type='jslint' path='%s']" path -/// Sends an ReSharper inspectCode.exe results filename to TeamCity +/// Sends an ReSharper inspectCode.exe results filename to TeamCity let sendTeamCityReSharperInspectCodeImport path = sendToTeamCity "##teamcity[importData type='ReSharperInspectCode' path='%s']" path -/// Sends an FxCop inspection results filename to TeamCity +/// Sends an FxCop inspection results filename to TeamCity let sendTeamCityFxCopImport path = sendToTeamCity "##teamcity[importData type='FxCop' path='%s']" path -/// Sends an PMD inspections results filename to TeamCity +/// Sends an PMD inspections results filename to TeamCity let sendTeamCityPmdImport path = sendToTeamCity "##teamcity[importData type='pmd' path='%s']" path -/// Sends an PMD Copy/Paste Detector results filename to TeamCity +/// Sends an PMD Copy/Paste Detector results filename to TeamCity let sendTeamCityPmdCpdImport path = sendToTeamCity "##teamcity[importData type='pmdCpd' path='%s']" path -/// Sends an ReSharper dupfinder.exe results filename to TeamCity +/// Sends an ReSharper dupfinder.exe results filename to TeamCity let sendTeamCityDotNetDupFinderImport path = sendToTeamCity "##teamcity[importData type='DotNetDupFinder' path='%s']" path -/// Sends an dotcover, partcover, ncover or ncover3 results filename to TeamCity +/// Sends an dotcover, partcover, ncover or ncover3 results filename to TeamCity [] let sendTeamCityDotNetCoverageImport path = sendToTeamCity "##teamcity[importData type='dotNetCoverage' path='%s']" path type TeamCityDotNetCoverageTool = | DotCover | PartCover | NCover | NCover3 with override x.ToString() = match x with | DotCover -> "dotcover" | PartCover -> "partcover" | NCover -> "ncover" | NCover3 -> "ncover3" -/// Sends an dotcover, partcover, ncover or ncover3 results filename to TeamCity -let sendTeamCityDotNetCoverageImportForTool path (tool : TeamCityDotNetCoverageTool) = +/// Sends an dotcover, partcover, ncover or ncover3 results filename to TeamCity +let sendTeamCityDotNetCoverageImportForTool path (tool : TeamCityDotNetCoverageTool) = sprintf "##teamcity[importData type='dotNetCoverage' tool='%s' path='%s']" (string tool |> scrub) (path |> scrub) |> sendStrToTeamCity /// Sends the full path to the dotCover home folder to override the bundled dotCover to TeamCity let sendTeamCityDotCoverHome = sendToTeamCity "##teamcity[dotNetCoverage dotcover_home='%s']" - + /// Sends the full path to NCover installation folder to TeamCity let sendTeamCityNCover3Home = sendToTeamCity "##teamcity[dotNetCoverage ncover3_home='%s']" /// Sends arguments for the NCover report generator to TeamCity let sendTeamCityNCover3ReporterArgs = sendToTeamCity "##teamcity[dotNetCoverage ncover3_reporter_args='%s']" - + /// Sends the path to NCoverExplorer to TeamCity let sendTeamCityNCoverExplorerTool = sendToTeamCity "##teamcity[dotNetCoverage ncover_explorer_tool='%s']" - + /// Sends additional arguments for NCover 1.x to TeamCity let sendTeamCityNCoverExplorerToolArgs = sendToTeamCity "##teamcity[dotNetCoverage ncover_explorer_tool_args='%s']" - + /// Sends the value for NCover /report: argument to TeamCity let sendTeamCityNCoverReportType : int -> unit = string >> sendToTeamCity "##teamcity[dotNetCoverage ncover_explorer_report_type='%s']" - + /// Sends the value for NCover /sort: argument to TeamCity let sendTeamCityNCoverReportOrder : int -> unit = string >> sendToTeamCity "##teamcity[dotNetCoverage ncover_explorer_report_order='%s']" - + /// Send the PartCover xslt transformation rules (Input xlst and output files) to TeamCity let sendTeamCityPartCoverReportXslts : seq -> unit = Seq.map (fun (xslt, output) -> sprintf "%s=>%s" xslt output) >> Seq.map EncapsulateSpecialChars >> String.concat "|n" >> sprintf "##teamcity[dotNetCoverage partcover_report_xslts='%s']" - >> sendStrToTeamCity + >> sendStrToTeamCity /// Starts the test case. -let StartTestCase testCaseName = +let StartTestCase testCaseName = sendToTeamCity "##teamcity[testStarted name='%s' captureStandardOutput='true']" testCaseName /// Finishes the test case. -let FinishTestCase testCaseName (duration : System.TimeSpan) = - let duration = +let FinishTestCase testCaseName (duration : System.TimeSpan) = + let duration = duration.TotalMilliseconds |> round |> string - sprintf "##teamcity[testFinished name='%s' duration='%s']" (EncapsulateSpecialChars testCaseName) duration + sprintf "##teamcity[testFinished name='%s' duration='%s']" (EncapsulateSpecialChars testCaseName) duration |> sendStrToTeamCity -/// Ignores the test case. -let IgnoreTestCase name message = +/// Ignores the test case. +let IgnoreTestCase name message = StartTestCase name - sprintf "##teamcity[testIgnored name='%s' message='%s']" (EncapsulateSpecialChars name) + sprintf "##teamcity[testIgnored name='%s' message='%s']" (EncapsulateSpecialChars name) (EncapsulateSpecialChars message) |> sendStrToTeamCity -/// Ignores the test case. -let IgnoreTestCaseWithDetails name message details = +/// Ignores the test case. +let IgnoreTestCaseWithDetails name message details = IgnoreTestCase name (message + " " + details) /// Finishes the test suite. -let FinishTestSuite testSuiteName = +let FinishTestSuite testSuiteName = EncapsulateSpecialChars testSuiteName |> sendToTeamCity "##teamcity[testSuiteFinished name='%s']" /// Starts the test suite. -let StartTestSuite testSuiteName = +let StartTestSuite testSuiteName = EncapsulateSpecialChars testSuiteName |> sendToTeamCity "##teamcity[testSuiteStarted name='%s']" /// Reports the progress. @@ -159,7 +159,7 @@ let ReportProgressFinish message = EncapsulateSpecialChars message |> sendToTeam /// Create the build status. /// [omit] -let buildStatus status message = +let buildStatus status message = sprintf "##teamcity[buildStatus '%s' text='%s']" (EncapsulateSpecialChars status) (EncapsulateSpecialChars message) /// Reports the build status. @@ -175,33 +175,27 @@ let PublishArticfact path = PublishArtifact path let SetBuildNumber buildNumber = EncapsulateSpecialChars buildNumber |> sendToTeamCity "##teamcity[buildNumber '%s']" /// Reports a build statistic. -let SetBuildStatistic key value = - sprintf "##teamcity[buildStatisticValue key='%s' value='%s']" (EncapsulateSpecialChars key) +let SetBuildStatistic key value = + sprintf "##teamcity[buildStatisticValue key='%s' value='%s']" (EncapsulateSpecialChars key) (EncapsulateSpecialChars value) |> sendStrToTeamCity /// Reports a parameter value -let SetTeamCityParameter name value = - sprintf "##teamcity[setParameter name='%s' value='%s']" (EncapsulateSpecialChars name) +let SetTeamCityParameter name value = + sprintf "##teamcity[setParameter name='%s' value='%s']" (EncapsulateSpecialChars name) (EncapsulateSpecialChars value) |> sendStrToTeamCity /// Reports a failed test. -let TestFailed name message details = - sprintf "##teamcity[testFailed name='%s' message='%s' details='%s']" (EncapsulateSpecialChars name) +let TestFailed name message details = + sprintf "##teamcity[testFailed name='%s' message='%s' details='%s']" (EncapsulateSpecialChars name) (EncapsulateSpecialChars message) (EncapsulateSpecialChars details) |> sendStrToTeamCity /// Reports a failed comparison. -let ComparisonFailure name message details expected actual = - sprintf - "##teamcity[testFailed type='comparisonFailure' name='%s' message='%s' details='%s' expected='%s' actual='%s']" - (EncapsulateSpecialChars name) (EncapsulateSpecialChars message) (EncapsulateSpecialChars details) +let ComparisonFailure name message details expected actual = + sprintf + "##teamcity[testFailed type='comparisonFailure' name='%s' message='%s' details='%s' expected='%s' actual='%s']" + (EncapsulateSpecialChars name) (EncapsulateSpecialChars message) (EncapsulateSpecialChars details) (EncapsulateSpecialChars expected) (EncapsulateSpecialChars actual) |> sendStrToTeamCity -/// Gets the recently failed tests -let getRecentlyFailedTests() = appSetting "teamcity.tests.recentlyFailedTests.file" |> ReadFile - -/// Gets the changed files -let getChangedFilesInCurrentBuild() = appSetting "teamcity.build.changedFiles.file" |> ReadFile - /// The Version of the TeamCity server. This property can be used to determine the build is run within TeamCity. let TeamCityVersion = environVarOrNone "TEAMCITY_VERSION" @@ -212,10 +206,334 @@ let TeamCityProjectName = environVarOrNone "TEAMCITY_PROJECT_NAME" let TeamCityBuildConfigurationName = environVarOrNone "TEAMCITY_BUILDCONF_NAME" /// Is set to true if the build is a personal one. -let TeamCityBuildIsPersonal = +let TeamCityBuildIsPersonal = match environVarOrNone "BUILD_IS_PERSONAL" with | Some _ -> true | None -> false /// The Build number assigned to the build by TeamCity using the build number format or None if it's not on TeamCity. let TeamCityBuildNumber = environVarOrNone "BUILD_NUMBER" + +module private JavaPropertiesFile = + open System.Text + open System.IO + open System.Globalization + + type PropertiesFileEntry = + | Comment of text : string + | KeyValue of key : string * value : string + + module private Parser = + type CharReader = unit -> char option + + let inline (|IsWhitespace|_|) c = + match c with + | Some c -> if c = ' ' || c = '\t' || c = '\u00ff' then Some c else None + | None -> None + + type IsEof = + | Yes = 1y + | No = 0y + + let rec readToFirstChar (c: char option) (reader: CharReader) = + match c with + | IsWhitespace _ -> + readToFirstChar (reader ()) reader + | Some '\r' + | Some '\n' -> + None, IsEof.No + | Some _ -> c, IsEof.No + | None -> None, IsEof.Yes + + let inline (|EscapeSequence|_|) c = + match c with + | Some c -> + if c = 'r' || c = 'n' || c = 'u' || c = 'f' || c = 't' || c = '"' || c = ''' || c = '\\' then + Some c + else + None + | None -> None + + let inline isHex c = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') + + let readEscapeSequence (c: char) (reader: CharReader) = + match c with + | 'r' -> '\r' + | 'n' -> '\n' + | 'f' -> '\f' + | 't' -> '\t' + | 'u' -> + match reader(), reader(), reader(), reader() with + | Some c1, Some c2, Some c3, Some c4 when isHex c1 && isHex c2 && isHex c3 && isHex c4 -> + let hex = System.String([|c1;c2;c3;c4|]) + let value = System.UInt16.Parse(hex, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture) + char value + | _ -> + failwith "Invalid unicode escape" + | _ -> c + + let inline readKey (c: char option) (reader: CharReader) (buffer: StringBuilder) = + let rec recurseEnd (result: string) = + match reader () with + | Some ':' + | Some '=' + | IsWhitespace _ -> recurseEnd result + | Some '\r' + | Some '\n' -> result, false, None, IsEof.No + | None -> result, false, None, IsEof.Yes + | Some c -> result, true, Some c, IsEof.No + let rec recurse (c: char option) (buffer: StringBuilder) (escaping: bool) = + match c with + | EscapeSequence c when escaping -> + let realChar = readEscapeSequence c reader + recurse (reader()) (buffer.Append(realChar)) false + | Some ' ' -> recurseEnd (buffer.ToString()) + | Some ':' + | Some '=' when not escaping -> recurseEnd (buffer.ToString()) + | Some '\r' + | Some '\n' -> buffer.ToString(), false, None, IsEof.No + | None -> buffer.ToString(), false, None, IsEof.Yes + | Some '\\' -> recurse (reader ()) buffer true + | Some c -> recurse (reader ()) (buffer.Append(c)) false + + recurse c buffer false + + let rec readComment (reader: CharReader) (buffer: StringBuilder) = + match reader () with + | Some '\r' + | Some '\n' -> + Some (Comment (buffer.ToString())), IsEof.No + | None -> + Some(Comment (buffer.ToString())), IsEof.Yes + | Some c -> + readComment reader (buffer.Append(c)) + + let inline readValue (c: char option) (reader: CharReader) (buffer: StringBuilder) = + let rec recurse (c: char option) (buffer: StringBuilder) (escaping: bool) (cr: bool) (lineStart: bool) = + match c with + | EscapeSequence c when escaping -> + let realChar = readEscapeSequence c reader + recurse (reader()) (buffer.Append(realChar)) false false false + | Some '\r' + | Some '\n' -> + if escaping || (cr && c = Some '\n') then + recurse (reader ()) buffer false (c = Some '\r') true + else + buffer.ToString(), IsEof.No + | None -> + buffer.ToString(), IsEof.Yes + | Some _ when lineStart -> + let firstChar, _ = readToFirstChar c reader + recurse firstChar buffer false false false + | Some '\\' -> recurse (reader ()) buffer true false false + | Some c -> + recurse (reader()) (buffer.Append(c)) false false false + + recurse c buffer false false true + + let rec readLine (reader: CharReader) (buffer: StringBuilder) = + match readToFirstChar (reader ()) reader with + | Some '#', _ + | Some '!', _ -> + readComment reader (buffer.Clear()) + | Some firstChar, _ -> + let key, hasValue, c, isEof = readKey (Some firstChar) reader (buffer.Clear()) + let value, isEof = + if hasValue then + // We know that we aren't at the end of the buffer, but readKey can return None if it didn't need the next char + let firstChar = match c with | Some c -> Some c | None -> reader () + readValue firstChar reader (buffer.Clear()) + else + "", isEof + Some (KeyValue(key, value)), isEof + | None, isEof -> None, isEof + + let inline textReaderToReader (reader: TextReader) = + let buffer = [| '\u0000' |] + fun () -> + let eof = reader.Read(buffer, 0, 1) = 0 + if eof then None else Some (buffer.[0]) + + let parseWithReader reader = + let buffer = StringBuilder(255) + let mutable isEof = IsEof.No + + seq { + while isEof <> IsEof.Yes do + let line, isEofAfterLine = readLine reader buffer + match line with + | Some line -> yield line + | None -> () + isEof <- isEofAfterLine + } + + let parseTextReader (reader: TextReader) = + let reader = Parser.textReaderToReader reader + Parser.parseWithReader reader + +/// TeamCity build parameters +/// See [Predefined Build Parameters documentation](https://confluence.jetbrains.com/display/TCD10/Predefined+Build+Parameters) for more information +module TeamCityBuildParameters = + open System + open System.IO + + let private get (fileName: string option) = + match fileName with + | Some fileName when (fileName <> null) && (fileExists fileName) -> + use stream = File.OpenRead(fileName) + use reader = new StreamReader(stream) + + reader + |> JavaPropertiesFile.parseTextReader + |> Seq.choose(function + | JavaPropertiesFile.Comment _ -> None + | JavaPropertiesFile.KeyValue(k, v) -> Some (k,v)) + |> Map.ofSeq + | _ -> + Map.empty + + let private systemFile = Environment.GetEnvironmentVariable("TEAMCITY_BUILD_PROPERTIES_FILE") + let private system = lazy(get (Some systemFile)) + + /// Get all system parameters + let getAllSystem () = system.Value + + /// Get the value of a system parameter by name + let tryGetSystem name = system.Value |> Map.tryFind name + + let private configurationFile = lazy (tryGetSystem "teamcity.configuration.properties.file") + let private configuration = lazy (get configurationFile.Value) + + /// Get all configuration parameters + let getAllConfiguration () = configuration.Value + + /// Get the value of a configuration parameter by name + let tryGetConfiguration name = configuration.Value |> Map.tryFind name + + let private runnerFile = lazy (tryGetSystem "teamcity.runner.properties.file") + let private runner = lazy (get runnerFile.Value) + + /// Get all runner parameters + let getAllRunner () = runner.Value + + /// Get the value of a runner parameter by name + let tryGetRunner name = runner.Value |> Map.tryFind name + + let private all = lazy ( + if buildServer = TeamCity then + seq { + // Environment variables are available using 'env.foo' syntax in TeamCity configuration + for pair in System.Environment.GetEnvironmentVariables() do + let pair = pair :?> System.Collections.DictionaryEntry + let key = pair.Key :?> string + let value = pair.Value :?> string + yield sprintf "env.%s" key, value + + // Runner variables aren't available in TeamCity configuration so we choose an arbitrary syntax of 'runner.foo' + for pair in runner.Value do yield sprintf "runner.%s" pair.Key, pair.Value + + // System variables are prefixed with 'system.' as in TeamCity configuration + for pair in system.Value do yield sprintf "system.%s" pair.Key, pair.Value + + for pair in configuration.Value do yield pair.Key, pair.Value + } + |> Map.ofSeq + else + Map.empty) + + /// Get all parameters + /// System ones are prefixed with 'system.', runner ones with 'runner.' and environment variables with 'env.' + let getAll () = all.Value + + /// Get the value of a parameter by name + /// System ones are prefixed with 'system.', runner ones with 'runner.' and environment variables with 'env.' + let tryGet name = all.Value |> Map.tryFind name + +/// Get files changed between builds in TeamCity +module TeamCityChangedFiles = + /// The type of change that occured + type ModificationType = + | FileChanged + | FileAdded + | FileRemoved + | FileNotChanged + | DirectoryChanged + | DirectoryAdded + | DirectoryRemoved + + /// Describe a change between builds + type FileChange = { + /// Path of the file that changed, relative to the current checkout directory ('system.teamcity.build.checkoutDir') + filePath: string + /// Type of modification for the file + modificationType: ModificationType + /// + revision: string option } + + let private getFileChanges' () = + match TeamCityBuildParameters.tryGetSystem "teamcity.build.changedFiles.file" with + | Some file when fileExists file -> + Some [ + for line in ReadFile file do + let split = line.Split(':') + if split.Length = 3 then + let filePath = split.[0] + let modificationType = + match split.[1].ToUpperInvariant() with + | "CHANGED" -> FileChanged + | "ADDED" -> FileAdded + | "REMOVED" -> FileRemoved + | "NOT_CHANGED" -> FileNotChanged + | "DIRECTORY_CHANGED" -> DirectoryChanged + | "DIRECTORY_ADDED" -> DirectoryAdded + | "DIRECTORY_REMOVED" -> DirectoryRemoved + | _ -> failwithf "Unknown change type: %s" (split.[1]) + let revision = + match split.[2] with + | "" -> None + | revision -> Some revision + + yield { filePath = filePath; modificationType = modificationType; revision = revision } + else + failwithf "Unable to split change line: %s" line + ] + | _ -> None + + let private fileChanges = lazy (getFileChanges' ()) + + /// Changed files (since previous build) that are included in this build + /// See [the documentation](https://confluence.jetbrains.com/display/TCD10/Risk+Tests+Reordering+in+Custom+Test+Runner) for more information + let get () = fileChanges.Value + +let private getRecentlyFailedTests' () = + match TeamCityBuildParameters.tryGetSystem "teamcity.tests.recentlyFailedTests.file" with + | Some file when fileExists file -> Some(ReadFile file) + | _ -> None + +let private recentlyFailedTests = lazy (getRecentlyFailedTests' ()) + +/// Name of recently failing tests +/// See [the documentation](https://confluence.jetbrains.com/display/TCD10/Risk+Tests+Reordering+in+Custom+Test+Runner) for more information +let getTeamCityRecentlyFailedTests () = recentlyFailedTests.Value + +/// Get the branch of the main VCS root +let getTeamCityBranch () = TeamCityBuildParameters.tryGetConfiguration "vcsroot.branch" + +/// Get the display name of the branch as shown in TeamCity +/// See [the documentation](https://confluence.jetbrains.com/display/TCD10/Working+with+Feature+Branches#WorkingwithFeatureBranches-branchSpec) for more information +let getTeamCityBranchName () = + match TeamCityBuildParameters.tryGetConfiguration "teamcity.build.branch" with + | Some _ as branch -> branch + | None -> TeamCityBuildParameters.tryGetConfiguration "vcsroot.branch" + +/// Get if the current branch is the one configured as default +let getTeamCityBranchIsDefault () = + if buildServer = TeamCity then + match TeamCityBuildParameters.tryGetConfiguration "teamcity.build.branch.is_default" with + | Some "true" -> true + | Some _ -> false + | None -> + // When only one branch is configured, TeamCity doesn't emit this parameter + getTeamCityBranch().IsSome + else + false From cd992d31debf860def5c82c0625e78bc300a4ea9 Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Mon, 27 Feb 2017 00:07:31 +0100 Subject: [PATCH 2/6] Add a teamCityBlock helper --- src/app/FakeLib/TeamCityHelper.fs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/FakeLib/TeamCityHelper.fs b/src/app/FakeLib/TeamCityHelper.fs index 35524274c99..f26350ae131 100644 --- a/src/app/FakeLib/TeamCityHelper.fs +++ b/src/app/FakeLib/TeamCityHelper.fs @@ -32,6 +32,13 @@ let sendOpenBlock = sendToTeamCity "##teamcity[blockOpened name='%s']" /// Close Named Block let sendCloseBlock = sendToTeamCity "##teamcity[blockClosed name='%s']" +/// Open Named Block that will be closed when the block is disposed +/// Usage: `use __ = teamCityBlock "My Block"` +let teamCityBlock name = + sendOpenBlock name + { new System.IDisposable + with member __.Dispose() = sendCloseBlock name } + /// Sends an error to TeamCity let sendTeamCityError error = sendToTeamCity "##teamcity[buildStatus status='FAILURE' text='%s']" error From 354a7b8314906797f83627e9f05b8466546ca3b2 Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Mon, 27 Feb 2017 00:12:06 +0100 Subject: [PATCH 3/6] Advanced TC, first draft --- help/pics/teamcity/loghierarchy.png | Bin 0 -> 3956 bytes help/pics/teamcity/loghierarchy2.png | Bin 0 -> 4381 bytes help/pics/teamcity/versionnumber.png | Bin 0 -> 3528 bytes help/pics/teamcity/versionnumber2.png | Bin 0 -> 3246 bytes help/teamictyadvanced.md | 83 ++++++++++++++++++++++++++ help/templates/template.cshtml | 1 + 6 files changed, 84 insertions(+) create mode 100644 help/pics/teamcity/loghierarchy.png create mode 100644 help/pics/teamcity/loghierarchy2.png create mode 100644 help/pics/teamcity/versionnumber.png create mode 100644 help/pics/teamcity/versionnumber2.png create mode 100644 help/teamictyadvanced.md diff --git a/help/pics/teamcity/loghierarchy.png b/help/pics/teamcity/loghierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..6af7bf4d5b78e136164a7c81be49b323a297b818 GIT binary patch literal 3956 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 L%Ba-vM&!Q$XwGUZ literal 0 HcmV?d00001 diff --git a/help/pics/teamcity/loghierarchy2.png b/help/pics/teamcity/loghierarchy2.png new file mode 100644 index 0000000000000000000000000000000000000000..27667f2686d5fa342393ee20e033829a8fa83492 GIT binary patch literal 4381 zcmZWtXH-*JxD5_Xs&qjRq;~{GBmp7}qCsjXp$pQBAfeY_1C$nOqy-$Bw9t_NqJR_u zL4gojXwq9C5Tz+ExMt?9dGE))_d92;@1Aqd{`Nlm+!sa$cbFOY7ytkOGvux|6ab(C zQsSrd=P37-lv`&HDnIBQEdXYae~r?hcGc9^1ORH17*8B%DDCr)?ppc*04$wn7gdkX z8z)L9yZ>zqe-j@U{~!mrGvE%~+1cCA&Bx!)jGmHmAq}FfX&P*|l{@wL+YP4MBbH@Z zy2Y;!^*Kzs$EvAj0aQlya{Yx7Lqk?V^tU)Ms|kynU<4H)8-P7m&5V=*Og$39pE4X? z1ndB9uz}KBEK~rzV?WMBoRjebqv{>Z``J_W+4qOl7h|i4L}DIz;)wd!Nw_h0M&_SS zAATh1-S7z&YjI9&eGz(e8c7HyiQ%-_cS0`01$ulk*d=AMCs+1F+tq_G!P1IlY^*tApiUq{ZD}ZD6J8#1HxS=B+_nP*Md- z*=Q5!oB<+|#6xS=TFr*DsqA9Mi$v?mi16s8Ey@WTsG)0H__lbzG$W7i|C$D>MrI^< zIjmQDymSoB>b^Xm#xk%V0$d>a+_-vY8OKw8ZZ&p}Lv@3Qu~SL*-avkRW;vQU`bpGy z*|pE$f%97#rOL=%rg=`*O-h!qU$jj-LCqS|%8`zeyY2q&9%e?gZcs|+k%eF~ zp>AAu8P@|9)5p=zT(-tgOQpuufi{r@(Kg=NE4u+IR!cfr=L?iS+~VXaz*6mj6jT(@aJ3XAuJikFMn4=RpzIAYtqM^)XzXE2$_>eWHzxd4QKmn=p)~-(lWnOC^Q`%JDV!`@`AjM^N>4aNKGA}!Zh_T8|2 zVr(@&1y_cI*cLf8Stf9xppqQP!f#q#oBnfR>p3C^hud-EnwSL^!)o(ke6DKV6lD8W zQbxHkyP|B->Id~7YjC}08~^HV%IrYlyyib=zoc%Ab+{;29fLjztgb=F#n?KbFKxm~ z`utVJ!vl5a#fMALO}+L5!*DrNZrcqY3ZJK^-sOFA=s3b-MOx4Vec85d zQjASkXe-;fm(X{04;T=AjnjFmI@Qu6x&2kwBA>iqK_Y!vtuB685Epu+aL;UrcPGC& z&>U)N_FAlLc_(i60atn>-zLy?QTm+KbJ`TMW(@=-l^lpS!!{1un$y_yF~FIFCW^AJ z5ow01YPNL71EShDmKXr9sNppBmxN$|Myme=b_%dvp*tF|ykPk7j7k&v>m7)4@?tcd zY6IH!w6l?V2}qoPTK9e|GfWxVM3z%vg{w{~%Hh8!3X>|;FISM} zQ**9Je5w*H;}IQNs=4M-hhIlBT3BK#JS(gV$61^73dLqb9DDuWrr}{u3U5c^@1y8a zJ=^0-*PX~kispVrdI~P)&{t`845AjiU1lfIV_m_?s)HJ3by+Ri0in6X9AhD%Am`nR z$tyh7XXwKon`(nIWvhO^D_?&er5y16PW4AGh=aG%ml8ECa=oFgh?N_5p{LmvAg~Jz zM0&QqWC~Q?A68uPQ-}uHk4)mx9yo|3_ClRl4R)zu2N~g2Kr$S27l$k4gz1?0V^=@u zI~COq#)dEt$>xjL^|0KJHe*!YUzBFQ5f?&0C{9z+fe_u8)at>2M$sE@!#OVp^#(K> zTr(z``VP4A0QApfwjD&zc8DO;%h zFJQ5)goE;o7sSi^=eRCIYKVBY{AqZui|~=8wCqh+PWhypnwJ76i{vnlgqK z{wZ&-h>hm5A7yheG+(pIXtKJ$8-^eErqQtNsb9~@^XAHPH7QieTCX%d^l_65{t{ZI zGU_hQo<8&mM8L3MB)uXYb{TdKhIxH6AU|M}DuRK?_jpHd&l3tp`y9rh5+I z`~ggp47)n<-Z{Iz zPro+^w^O}$0wyp4_nmn<5=A(C#ksMKPbf=MhwwL!qKGFttTs3K1 zxA5urqo$JziPWX^_d#5tL)($N272y=$W1ktwhr$07h+0?YO9o!U-fg5x%O=42QB4Z zn!fz3leS<7f7_?3Fz_D2gvGOlr7XTG z)WtsyF~><)1pZ3G(S3=20{U0tI5V<8tQ=U7C)HiB=S53W_6X-{+LqDtGq1Q7n@>Kg`GAlz!HWL{p7x&# z_#KQ}V-~I$g-ydx=8D2Jg&g=wr0Qh0LCjzMls@g2YG+ODK7+n_q?j<0Hj!>JNVwgW zPljyD<1s7ypePZ&5<0t>Ig=jZ+)Rd3_p$8|;mP$=w^;o{0JafzaAaLOakI~2MR4Zp zi0MSd8H_|-_tkD7Xofs3CxlmDo0JK8YM{5SzKr+*xgL-3{ZKTvNXW?t9Y$aEX`*t-zdKJC&4Y zzZqD5@a#7O6O4e$bw93r%S1#xA29@@Ab%vT_5*8xpNqaR4uYSCS%w6O4L?2&yGtMn z{v@`r6V3xC63qJA*OKfZ4$&(Ww9Gzs2?Y_4{*XB#nAtf|h`4T!bZ2(+r}y*zJkg4l z!}GmsV*Sl47}1vv{scv^DB=g6CU-E6lxmK&@Ws@8;P>oTR@TSY@~dOF!Cu^Z>yD-3 z(El>BiC(pR&>I6~$vwRr7#6C7d%%PGLHyR&uK2(F{2AcXQ)Io$_}Z`X=cmyWub|I@ zoyE{fHr#ETN_fqZW8X+rLr}nvxS$8R1x5qf%()!{n{rInMP37lw5ywp{(|?ozRB9s zi~Y)0YRWF@RZce5U38@L#dSV+`CG}e%B;Y|*&wbx>q?#PE;8k5%$(rauYX+$t=s>( zsV!6XJUyT``&@9WL|;IPBX32!e1JTC#yBZA^jwj#PFF;-*e@2op4jP zm0bQ*6>~;f@cNOVdZtS5c6f1K$r5`rjEX3>Z(b_CPuy9UH9v?~8M}sP<CMG4KK^qP5Er9JQKela&#_Em+vt7YD* zR*|ah{ezpIIsaS%Lti-nmEA&Nj$mMXZhPo*Qi2D6x3r!rp`)e0VT1JrHml>nK5Sy5 z6s!Ac8A(Y z@C67sxcE4{`Kr^Rc~kbAbcS^CQHscM2Co(`SS9x+%8JIAP=n@NcLvW!h+vm}w4Kgx zOttYW6=;)qiWl7YJjZXfjBe3ZD&0a#^ae+2gp!It`0_{n@UZ;z69TwU`?3}31~he*Bv3HpqCwkH#pvIL522Wi7)$*`>O;&b2Ny9)DX4zcXT_`Y zuBI|osNL&&XrMP@r~-SzzxQbKI@^R+KhDBXSSK!8tw4;S0k^3G*{!p@Yt!1vzV!EL zz;FY}NI4DcN-K@yzTE4{8G3hdJ4z^|v=}y$*<%bb=6wPD5Z>qSm#$q=>?LT1Z9spl zUj6HjA06b9;7hjhK#Ky(_s1x7mObYSg+Fxv#}&43`kVJSK++opuI4GdW94A0*+!PX zJIjTZSVBL5yLb;a(>nAEP1I{B`R>?^^u6POJU!HN0%))2kz?gptkv=Qr5nl`#cKLC zRo+p`T}|KvEbEfAnoNf=8VP~vWe?(0SK}zqsXb)=96u-=)9+^^EK`;-FUCH|b{uc8 zMzNfF7zdh*0ovoKX3ZI5m@%G{V~m>1UITkqTfBkus2t>?$*32@xA5k*{)e-(Wjw0k zf#`vFFLgii3+}3{_lT^F%9L2{3>1gRbqNl(FVfeM6~hfq9;HLulz00C*{0jB+W=&7 zb(Ax~yiUVD`SV+o{W)agCENEK;SB$^3eVmnPU!>=Gw7QLgWZ(B7XZj@18t0!{geLz D!L@lR literal 0 HcmV?d00001 diff --git a/help/pics/teamcity/versionnumber.png b/help/pics/teamcity/versionnumber.png new file mode 100644 index 0000000000000000000000000000000000000000..4064a0e6688ed5bc83430cdb7dab4c353b1d3ec2 GIT binary patch literal 3528 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 I8V-^F0cP5^rT_o{ literal 0 HcmV?d00001 diff --git a/help/pics/teamcity/versionnumber2.png b/help/pics/teamcity/versionnumber2.png new file mode 100644 index 0000000000000000000000000000000000000000..3f756b62ba6670b127d03e8b62b2e5e1b3f81839 GIT binary patch literal 3246 zcmZ`+XEYm*7S}d56}wg&HDc9@f2~?Ui4`?MYt*h$6tPDuYDCSN_3T|Uc8SscC9$fd zK~ZXxprS_Y_IdA|_ddP%;f~)u=bm$aAMQe%n&>ez@G?+QQ8B~xb;3p6+}8~m?C9@GrRVSJ>J#AZi?p|-yG&scf$3;jgxLQm_ygzL;CX)rKQ7u$JFNU$Q6(lK8-^t*%>IxO*@)IF;Hrr+{K9Z2FLNgw6^RGB? z_>>UvuoPJq=oecZbWBg88YM@y=yR}WJ%@kNMwvPCXl}pId-?1*bU}6gY*r8jN)XXf z6Qpx`U<#*0S#wOVqd;8Lhc7{mx#TZXjq2SeEqzbEpYE=M@y0jRP9kK* zk&Wu`ulps*^G-^X8~WdD$%Q4|&r1mXQ;RQ29+9)$4{Q2|XETF6 zlksBy9tOMe0dKCH?uGVfd16jICpKQKcKoB<)neD!D|IriJxe#bFE;B3nxFlTb$26? zxF%B@vB0W;e?}0@4CZ|XRoH6Vn>L-zh#U@n^dh4KT+MDh+M2~wO>pAgCNsxG`q&?82T!fn-TFm~1% zCsVZpn_;McG~Q}viL65%+5ADnEy$HfUwA*4a%@NPNK#Klf}(=&!Q=7z$<8Fl^%7VdNKaKD!oq zu-kdRD%`^b(fMXvwW+N>r~P2b#}p)A+Zg6C|K|gHprpJ7oa3aVwGWP{={U21|214# z!kJ#DR{xRn%mcprk(8*l@W`(&223tuB964t+L3xAxMo} z=PVW3dv1iP8=>PYg42Dj%g|0kDC}dzM**E2uv*Y|JR*>ON_&5oRgegN$yR${eIk4I zG<99OSZOcMRQ@|%(=P&rMLEl8edogI9;45xS98S0f?@$#qqi`E_V-08Vv)y9!Lx-M z5@a>2EX@_n5(4ZEXN>L5-Ge-`Cvy%59`6wRf9;p*grVTgi*382Cs00`jlEwbYjsSt z5x3(4dX})yatk`Uf84gTFXQ4fW|6rk$5=x;;6Us6ciNg?dm45Ab0DIFb)dM)EHhd9 zZDco>>C@c&^6(1x7n@Z9fKKtzcxu5qgdA^IYpEtB&GY5eS-^9kHUoWIUK}X0!!&|= z;u?_i1U0@Y!E;!ehNu@BA0-iP>&QfjRlMEVp0iHQ! z%+%XHxB(bFL}`Fl6*od|y85R9rIl~h!NQj%XBCAAZg@ij$*^3jwP!D=JFY?RM7Lt+ zV+dN*1%-ub;X&jt6*1QePtH#QreA`-DXdxs?ueN_y-`$d>hkj45VD|E&Fp~Av!jl= z3^5kJtVvC~Gd>NRqw4}VlR>_3ogHe$t@d5&qz3Jay*ZL5t%|GKS0sHMeFGU(g4DHu zSC`zC-ZucoOh6;rCUOhvzV@Ami%!kwIh~DMQ@5b_aw5A<$Y#2JN5NvAo%OerR19bF zLw@+#2kGVJNS%oyuec9i-b9)q%Kq0s`U?^3{gxz3XJby$6l*qN`%XSL%n zHLyX$vEaGyiK{Pi6zU({;+7H-o;rcE!h|nu#vbMfDy||i*rW4pJ>9?w^6=g@5|MlW zU@feF(oY%2fAbNZPAn)(pK>5BEk(_&NR%&v*~=jkJDB2#Cu5c$@7jZ^F_4^=WUFJE z3G)V5rCvwVS4(5ZPl_VjHKRFi%5PL`!ORCp6i`U?Pi}8;LedhQi!E8-sLRB(IV5si z#5E0l*nd0-#O3XU$xDq%3%Jyjxl97Y(!o-?k#!?_Hlt}jh28&RKVUe0O=p2uZ>Rxh z4`$K__51xaPgQ(~mDvC`IMcf$pVpYK;P#q7=QYN|_raAa9+R*I13j!Cf(#4nklW}IYHFL}h2 z6!In$)<$x)OMrE|Y05G-a8q#g%CZV~n<1cGp&}Vo*}F*+V~ggWF}Su$!*=op*r%fi zPtsoTLW8hWRkj^oGCrjG;x0*~Ub`p#^?X9(Y%JT5cyxqI43ted^4#C)L2XrsdiR@% zw6A1wOYBgKs>i*Lar==6r+MBWh*FeZ5AL#7Z?M&J_N$pJd;3L&3hF4iPXe8_`3VHA zhqit#UZFzxkgR$AK#-l4%H>>Yp>&=58GpQV?e`#*Q#zr%yIN z{*I_-cyk48auC*hE|>|JcHHMai4#1m8)zt>ko1H9;lYl&ZTT^oB<8J%P2h*L^jKh| zqjDsCM;o~H6Z3mib>_jm5Xm7Lw7m>-+xErB&heZo?G~1t>qKkV&8g(msYN@5fd|bK z)+R8JhDX{4%&PB&=MOBnFX}m-*-)&r)yI9oYveYT46vA2bpN*kITgnYj(}PIjA7?A zR$QVc+J#j4HG|UaHyU^sGTRmxmfxj$bG_DxwR@ds_&CdBQ|fQ@SjHm=I_m63Mtc6? z(cwXIGIP)*yS`hRdUmZ~><>HIp^r(l5jy#Pw$}2TlE^V6jadpbP z7c-|`Os40hS;J)|2Mf7cVpcO%h}1GxA~dusK=sL^=VsRe7|>{T{L2qW_>+J2=HeTh=T$ z_git#Jubxw_VG86X4oeNd7+yHcAKPPQqY3ByExWGHU6>+PIR8{dQP`D>{x(3+I#L6ceT|A zRpGT^qL-1RCf<_FM?!zRz}9JeHHs-0^Sa}rzvWPcfr{krC|f06PWzAFv{U@Ld)4IY3;|R6ppFCcxdchmK*)|H-RnT^b_iZTrHb0ZOPa<#YGZA z5!MuiRWl`qa+c2D6`0y( zCx}PO6)ed@bYG~&(i`TmG!;a?uXC993{Qi41ov?-{yUaV`u=ru|G)7gqI~cCmZ@1+ V{8|3rX_xN=6-?Jervd7K`X|?JJRAT3 literal 0 HcmV?d00001 diff --git a/help/teamictyadvanced.md b/help/teamictyadvanced.md new file mode 100644 index 00000000000..754cce5620d --- /dev/null +++ b/help/teamictyadvanced.md @@ -0,0 +1,83 @@ +# Advanced TeamCity usage + +As can be seen on the [TeamCity](teamcity.md) page FAKE is really easy to setup in TeamCity, +it also support some advanced scenarios to integrate even deeper with it. + +## Displaying blocks in the log + +By default each Target already is displayed as a collapsible block in the log file : + +![Target blocks](pics/teamcity/loghierarchy.png "Target blocks") + +But blocks can be created in targets to separate operations more +cleanly : + +```fsharp +let printHello name = + use __ = teamCityBlock (sprintf "Hello %s" name) + printfn "Hello %s !" name + +Target "Default" (fun () -> + printHello "Fake" + printHello "TeamCity" +) +``` +![Custom blocks](pics/teamcity/loghierarchy2.png "Custom blocks") + +## Reporting artifacts + +While TeamCity has a [great configurability](https://confluence.jetbrains.com/display/TCD10/Build+Artifact) +in terms of artifacts, nothing beats specifying them in code. + +FAKE scripts also have the advantage of being versioned along the rest of your code, avoiding the need to +keep complex artifact configurations when you need to support a new branch along with old ones or the need +to configure artifacts in each build if you have multiple builds on the same repository. + +```fsharp +Target "NuGet" (fun () -> + Paket.Pack (fun p -> { p with OutputPath = artifactsDir }) + + !! (artifactsDir "*.nupkg") + |> Seq.iter(PublishArtifact) +) +``` + +## Customizing version numbers + +Each build is assigned a build number in TeamCity that is available as `TeamCityBuildNumber` from FAKE +and that is shown in the TeamCity dashboard : + +![Default version numbers](pics/teamcity/versionnumber.png "Default version numbers") + +But TeamCity also support that builds customize their version number by reporting it directly, using this +feature from FAKE is simple and when coupled with other parameters reported by TeamCity can allow complex +versioning schemes. + +This code read versions from a release notes file and if TeamCity is detected label versions as pre-release +when they come from a branch that isn't the default one or from a personal build : + +```fsharp +// Placed outside any Target +let releaseNotes = + let fromFile = ReleaseNotesHelper.LoadReleaseNotes ("Release Notes.md") + if buildServer = TeamCity then + let buildNumber = int (defaultArg TeamCityBuildNumber "0") + let asmVer = System.Version.Parse(fromFile.AssemblyVersion) + let asmVer = System.Version(asmVer.Major, asmVer.Minor, buildNumber) + let prerelease = + if TeamCityBuildIsPersonal then "-personal" + else if getTeamCityBranchIsDefault () then "" else "-branch" + let nugetVersion = asmVer.ToString() + prerelease + + ReleaseNotesHelper.ReleaseNotes.New(asmVer.ToString(), nugetVersion, fromFile.Date, fromFile.Notes) + else + fromFile + +SetBuildNumber releaseNotes.NugetVersion +``` + +![Custom version numbers](pics/teamcity/versionnumber2.png "Custom version numbers") + +## Reporting test results + +TODO diff --git a/help/templates/template.cshtml b/help/templates/template.cshtml index 2716d50ecd2..d85be9c099f 100644 --- a/help/templates/template.cshtml +++ b/help/templates/template.cshtml @@ -69,6 +69,7 @@
  • Creating custom tasks
  • Soft dependencies
  • TeamCity integration
  • +
  • TeamCity integration (Advanced)
  • Running canopy tests
  • Octopus Deploy
  • TypeScript support
  • From 9a7735d73f7ba70778a6b8908980701028f69602 Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Mon, 27 Feb 2017 00:12:36 +0100 Subject: [PATCH 4/6] Correct link in template --- help/templates/template.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help/templates/template.cshtml b/help/templates/template.cshtml index d85be9c099f..3383631bed8 100644 --- a/help/templates/template.cshtml +++ b/help/templates/template.cshtml @@ -69,7 +69,7 @@
  • Creating custom tasks
  • Soft dependencies
  • TeamCity integration
  • -
  • TeamCity integration (Advanced)
  • +
  • TeamCity integration (Advanced)
  • Running canopy tests
  • Octopus Deploy
  • TypeScript support
  • From 86cf83de07cc36949a05384ef91dbd50f9e981ce Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Mon, 27 Feb 2017 22:19:59 +0100 Subject: [PATCH 5/6] Add "Reporting test results" to advanced TC --- ...eamictyadvanced.md => teamcityadvanced.md} | 29 +++++++++++++++++-- help/templates/template.cshtml | 2 +- 2 files changed, 27 insertions(+), 4 deletions(-) rename help/{teamictyadvanced.md => teamcityadvanced.md} (76%) diff --git a/help/teamictyadvanced.md b/help/teamcityadvanced.md similarity index 76% rename from help/teamictyadvanced.md rename to help/teamcityadvanced.md index 754cce5620d..fbcd38c2c9c 100644 --- a/help/teamictyadvanced.md +++ b/help/teamcityadvanced.md @@ -35,9 +35,9 @@ to configure artifacts in each build if you have multiple builds on the same rep ```fsharp Target "NuGet" (fun () -> - Paket.Pack (fun p -> { p with OutputPath = artifactsDir }) + Paket.Pack (fun p -> { p with OutputPath = "." }) - !! (artifactsDir "*.nupkg") + !! "*.nupkg" |> Seq.iter(PublishArtifact) ) ``` @@ -80,4 +80,27 @@ SetBuildNumber releaseNotes.NugetVersion ## Reporting test results -TODO +In addition to artifacts, TeamCity also allow to report test results that will be +visible in the dashboard directly from the build. + +Each test runner has a specific function to send it's result that can be found in the +[TeamCityHelper API](apidocs/fake-teamcityhelper.html) like here for NUnit : + +```fsharp +Target "Tests" (fun () -> + testDlls + |> NUnit(fun p -> + { p with + OutputFile = outputFile + // If the build fails immediately the + // test results will never be reported + ErrorLevel = DontFailBuild + }) + + sendTeamCityNUnitImport outputFile +) +``` + +*Note:* NUnit version 3 is a special case as it directly support TeamCity and it's +enough to set `TeamCity = (BuildServer = TeamCity)` in +[it's configuration](apidocs/fake-testing-nunit3-nunit3params.html). diff --git a/help/templates/template.cshtml b/help/templates/template.cshtml index 3383631bed8..3d2d3ee50b5 100644 --- a/help/templates/template.cshtml +++ b/help/templates/template.cshtml @@ -69,7 +69,7 @@
  • Creating custom tasks
  • Soft dependencies
  • TeamCity integration
  • -
  • TeamCity integration (Advanced)
  • +
  • TeamCity integration (Advanced)
  • Running canopy tests
  • Octopus Deploy
  • TypeScript support
  • From 06f1f18ee62c7b9040074dbc00817ac2938d60d3 Mon Sep 17 00:00:00 2001 From: Julien Roncaglia Date: Mon, 27 Feb 2017 22:52:41 +0100 Subject: [PATCH 6/6] Fixes getBuildConfig & getProject in TC REST helpers --- src/app/FakeLib/TeamCityRESTHelper.fs | 30 +++++----- src/app/FakeLib/XMLHelper.fs | 84 ++++++++++++++------------- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/app/FakeLib/TeamCityRESTHelper.fs b/src/app/FakeLib/TeamCityRESTHelper.fs index 7379fa95840..75b9fde5b97 100644 --- a/src/app/FakeLib/TeamCityRESTHelper.fs +++ b/src/app/FakeLib/TeamCityRESTHelper.fs @@ -6,27 +6,27 @@ module Fake.TeamCityRESTHelper let prepareURL restURL (serverURL : string) = serverURL.Trim '/' + restURL /// Returns the REST version of the TeamCity server -let getRESTVersion serverURL username password = +let getRESTVersion serverURL username password = serverURL |> prepareURL "/httpAuth/app/rest/version" |> REST.ExecuteGetCommand username password /// Record type which stores VCSRoot properties -type VCSRoot = +type VCSRoot = { URL : string Properties : Map VCSName : string Name : string } /// Record type which stores Build properties -type Build = +type Build = { ID : string Number : string Status : string WebURL : string } /// Record type which stores Build configuration properties -type BuildConfiguration = +type BuildConfiguration = { ID : string Name : string WebURL : string @@ -36,7 +36,7 @@ type BuildConfiguration = Builds : Build seq } /// Record type which stores TeamCity project properties -type Project = +type Project = { ID : string Name : string Description : string @@ -45,40 +45,44 @@ type Project = BuildConfigs : string seq } /// [omit] -let getFirstNode serverURL username password url = +let getFirstNode serverURL username password url = serverURL |> prepareURL url |> REST.ExecuteGetCommand username password |> XMLDoc |> DocElement +let private parseBooleanOrFalse s = + let ok, parsed = System.Boolean.TryParse s + if ok then parsed else false + /// Gets information about a build configuration from the TeamCity server. -let getBuildConfig serverURL username password id = +let getBuildConfig serverURL username password id = sprintf "/httpAuth/app/rest/buildTypes/id:%s" id |> getFirstNode serverURL username password - |> parse "buildType" (fun n -> + |> parse "buildType" (fun n -> { ID = getAttribute "id" n Name = getAttribute "name" n Description = getAttribute "description" n WebURL = getAttribute "webUrl" n - Paused = getAttribute "paused" n |> System.Boolean.Parse + Paused = getAttribute "paused" n |> parseBooleanOrFalse ProjectID = parseSubNode "project" (getAttribute "id") n Builds = [] }) /// Gets informnation about a project from the TeamCity server. -let getProject serverURL username password id = +let getProject serverURL username password id = sprintf "/httpAuth/app/rest/projects/id:%s" id |> getFirstNode serverURL username password - |> parse "project" (fun n -> + |> parse "project" (fun n -> { ID = getAttribute "id" n Name = getAttribute "name" n Description = getAttribute "description" n WebURL = getAttribute "webUrl" n - Archived = getAttribute "archived" n |> System.Boolean.Parse + Archived = getAttribute "archived" n |> parseBooleanOrFalse BuildConfigs = parseSubNode "buildTypes" getChilds n |> Seq.map (getAttribute "id") }) /// Gets all projects on the TeamCity server. -let getProjects serverURL username password = +let getProjects serverURL username password = getFirstNode serverURL username password "/httpAuth/app/rest/projects" |> parse "projects" getChilds |> Seq.map (getAttribute "id") diff --git a/src/app/FakeLib/XMLHelper.fs b/src/app/FakeLib/XMLHelper.fs index ad98dc5081b..45948f400ad 100644 --- a/src/app/FakeLib/XMLHelper.fs +++ b/src/app/FakeLib/XMLHelper.fs @@ -11,88 +11,90 @@ open System.Xml.XPath open System.Xml.Xsl /// Reads a value from a XML document using a XPath -let XMLRead failOnError (xmlFileName : string) nameSpace prefix xPath = - try +let XMLRead failOnError (xmlFileName : string) nameSpace prefix xPath = + try let document = new XPathDocument(xmlFileName) let navigator = document.CreateNavigator() let manager = new XmlNamespaceManager(navigator.NameTable) if prefix <> "" && nameSpace <> "" then manager.AddNamespace(prefix, nameSpace) let expression = XPathExpression.Compile(xPath, manager) - seq { + seq { match expression.ReturnType with - | XPathResultType.Number | XPathResultType.Boolean | XPathResultType.String -> + | XPathResultType.Number | XPathResultType.Boolean | XPathResultType.String -> yield navigator.Evaluate(expression).ToString() - | XPathResultType.NodeSet -> + | XPathResultType.NodeSet -> let nodes = navigator.Select(expression) while nodes.MoveNext() do yield nodes.Current.Value | _ -> failwith <| sprintf "XPath-Expression return type %A not implemented" expression.ReturnType } - with exn -> + with exn -> if failOnError then failwithf "XMLRead error:\n%s" exn.Message else Seq.empty /// Reads a value from a XML document using a XPath /// Returns if the value is an int and the value -let XMLRead_Int failOnError xmlFileName nameSpace prefix xPath = - let headOrDefault def seq = +let XMLRead_Int failOnError xmlFileName nameSpace prefix xPath = + let headOrDefault def seq = if Seq.isEmpty seq then def else Seq.head seq XMLRead failOnError xmlFileName nameSpace prefix xPath |> Seq.map Int32.TryParse - |> (fun seq -> + |> (fun seq -> if failOnError then Seq.head seq else headOrDefault (false, 0) seq) /// Creates a XmlWriter which writes to the given file name -let XmlWriter(fileName : string) = +let XmlWriter(fileName : string) = let writer = new XmlTextWriter(fileName, null) writer.WriteStartDocument() writer /// Writes an XML comment to the given XmlTextWriter -let XmlComment comment (writer : XmlTextWriter) = +let XmlComment comment (writer : XmlTextWriter) = writer.WriteComment comment writer /// Writes an XML start element to the given XmlTextWriter -let XmlStartElement name (writer : XmlTextWriter) = +let XmlStartElement name (writer : XmlTextWriter) = writer.WriteStartElement name writer /// Writes an XML end element to the given XmlTextWriter -let XmlEndElement(writer : XmlTextWriter) = +let XmlEndElement(writer : XmlTextWriter) = writer.WriteEndElement() writer /// Writes an XML attribute to current element of the given XmlTextWriter -let XmlAttribute name value (writer : XmlTextWriter) = +let XmlAttribute name value (writer : XmlTextWriter) = writer.WriteAttributeString(name, value.ToString()) writer /// Writes an CData element to the given XmlTextWriter -let XmlCDataElement elementName data (writer : XmlTextWriter) = +let XmlCDataElement elementName data (writer : XmlTextWriter) = XmlStartElement elementName writer |> ignore writer.WriteCData data XmlEndElement writer /// Gets the attribute with the given name from the given XmlNode -let getAttribute (name : string) (node : #XmlNode) = node.Attributes.[name].Value +let getAttribute (name : string) (node : #XmlNode) = + let attribute = node.Attributes.[name] + if attribute <> null then attribute.Value else null /// Gets a sequence of all child nodes for the given XmlNode -let getChilds (node : #XmlNode) = - seq { +let getChilds (node : #XmlNode) = + seq { for x in node.ChildNodes -> x } /// Gets the first sub node with the given name from the given XmlNode -let getSubNode name node = +let getSubNode name node = getChilds node |> Seq.filter (fun x -> x.Name = name) |> Seq.head /// Parses a XmlNode -let parse name f (node : #XmlNode) = +let parse name f (node : #XmlNode) = if node.Name = name then f node else failwithf "Could not parse %s - Node was %s" name node.Name @@ -100,9 +102,9 @@ let parse name f (node : #XmlNode) = let parseSubNode name f = getSubNode name >> parse name f /// Loads the given text into a XmlDocument -let XMLDoc text = +let XMLDoc text = if isNullOrEmpty text then null - else + else let xmlDocument = new XmlDocument() xmlDocument.LoadXml text xmlDocument @@ -111,23 +113,23 @@ let XMLDoc text = let DocElement(doc : XmlDocument) = doc.DocumentElement /// Replaces text in the XML document specified by a XPath expression. -let XPathReplace xpath value (doc : XmlDocument) = +let XPathReplace xpath value (doc : XmlDocument) = let node = doc.SelectSingleNode xpath if node = null then failwithf "XML node '%s' not found" xpath - else + else node.Value <- value doc /// Replaces the inner text of an xml node in the XML document specified by a XPath expression. -let XPathReplaceInnerText xpath innerTextValue (doc : XmlDocument) = +let XPathReplaceInnerText xpath innerTextValue (doc : XmlDocument) = let node = doc.SelectSingleNode xpath if node = null then failwithf "XML node '%s' not found" xpath - else + else node.InnerText <- innerTextValue doc /// Selects a xml node value via XPath from the given document -let XPathValue xpath (namespaces : #seq) (doc : XmlDocument) = +let XPathValue xpath (namespaces : #seq) (doc : XmlDocument) = let nsmgr = XmlNamespaceManager(doc.NameTable) namespaces |> Seq.iter nsmgr.AddNamespace let node = doc.DocumentElement.SelectSingleNode(xpath, nsmgr) @@ -135,63 +137,63 @@ let XPathValue xpath (namespaces : #seq) (doc : XmlDocument) = else node.InnerText /// Replaces text in a XML file at the location specified by a XPath expression. -let XmlPoke (fileName : string) xpath value = +let XmlPoke (fileName : string) xpath value = let doc = new XmlDocument() doc.Load fileName XPathReplace xpath value doc |> fun x -> x.Save fileName /// Replaces the inner text of an xml node in a XML file at the location specified by a XPath expression. -let XmlPokeInnerText (fileName : string) xpath innerTextValue = +let XmlPokeInnerText (fileName : string) xpath innerTextValue = let doc = new XmlDocument() doc.Load fileName XPathReplaceInnerText xpath innerTextValue doc |> fun x -> x.Save fileName /// Replaces text in a XML document specified by a XPath expression, with support for namespaces. -let XPathReplaceNS xpath value (namespaces : #seq) (doc : XmlDocument) = +let XPathReplaceNS xpath value (namespaces : #seq) (doc : XmlDocument) = let nsmgr = XmlNamespaceManager(doc.NameTable) namespaces |> Seq.iter nsmgr.AddNamespace let node = doc.SelectSingleNode(xpath, nsmgr) if node = null then failwithf "XML node '%s' not found" xpath - else + else node.Value <- value doc /// Replaces inner text in a XML document specified by a XPath expression, with support for namespaces. -let XPathReplaceInnerTextNS xpath innerTextValue (namespaces : #seq) (doc : XmlDocument) = +let XPathReplaceInnerTextNS xpath innerTextValue (namespaces : #seq) (doc : XmlDocument) = let nsmgr = XmlNamespaceManager(doc.NameTable) namespaces |> Seq.iter nsmgr.AddNamespace let node = doc.SelectSingleNode(xpath, nsmgr) if node = null then failwithf "XML node '%s' not found" xpath - else + else node.InnerText <- innerTextValue doc /// Replaces text in a XML file at the location specified by a XPath expression, with support for namespaces. -let XmlPokeNS (fileName : string) namespaces xpath value = +let XmlPokeNS (fileName : string) namespaces xpath value = let doc = new XmlDocument() doc.Load fileName XPathReplaceNS xpath value namespaces doc |> fun x -> x.Save fileName /// Replaces inner text of an xml node in a XML file at the location specified by a XPath expression, with support for namespaces. -let XmlPokeInnerTextNS (fileName : string) namespaces xpath innerTextValue = +let XmlPokeInnerTextNS (fileName : string) namespaces xpath innerTextValue = let doc = new XmlDocument() doc.Load fileName XPathReplaceInnerTextNS xpath innerTextValue namespaces doc |> fun x -> x.Save fileName /// Loads the given text into a XslCompiledTransform. -let XslTransformer text = +let XslTransformer text = if isNullOrEmpty text then null - else + else let xslCompiledTransform = new XslCompiledTransform() XMLDoc(text) |> xslCompiledTransform.Load xslCompiledTransform /// Transforms a XmlDocument using a XslCompiledTransform. /// ## Parameters -/// +/// /// - `xsl` - The XslCompiledTransform which should be applied. /// - `doc` - The XmlDocument to transform. -let XslTransform (xsl : XslCompiledTransform) (doc : XmlDocument) = +let XslTransform (xsl : XslCompiledTransform) (doc : XmlDocument) = use memoryStream = new MemoryStream() use textWriter = new XmlTextWriter(memoryStream, new UTF8Encoding(false)) use writer = System.Xml.XmlWriter.Create(textWriter, xsl.OutputSettings) @@ -206,10 +208,10 @@ let XslTransform (xsl : XslCompiledTransform) (doc : XmlDocument) = /// Transforms a XML file using a XSL stylesheet file. /// ## Parameters -/// +/// /// - `stylesheetUri` - The Uri for the XSL stylesheet file. /// - `fileName` - The XML file to transform. -let XmlTransform (stylesheetUri : string) (fileName : string) = +let XmlTransform (stylesheetUri : string) (fileName : string) = let doc = new XmlDocument() doc.Load fileName let xsl = new XslCompiledTransform()