diff --git a/help/pics/teamcity/loghierarchy.png b/help/pics/teamcity/loghierarchy.png new file mode 100644 index 00000000000..6af7bf4d5b7 Binary files /dev/null and b/help/pics/teamcity/loghierarchy.png differ diff --git a/help/pics/teamcity/loghierarchy2.png b/help/pics/teamcity/loghierarchy2.png new file mode 100644 index 00000000000..27667f2686d Binary files /dev/null and b/help/pics/teamcity/loghierarchy2.png differ diff --git a/help/pics/teamcity/versionnumber.png b/help/pics/teamcity/versionnumber.png new file mode 100644 index 00000000000..4064a0e6688 Binary files /dev/null and b/help/pics/teamcity/versionnumber.png differ diff --git a/help/pics/teamcity/versionnumber2.png b/help/pics/teamcity/versionnumber2.png new file mode 100644 index 00000000000..3f756b62ba6 Binary files /dev/null and b/help/pics/teamcity/versionnumber2.png differ diff --git a/help/teamcityadvanced.md b/help/teamcityadvanced.md new file mode 100644 index 00000000000..fbcd38c2c9c --- /dev/null +++ b/help/teamcityadvanced.md @@ -0,0 +1,106 @@ +# 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 = "." }) + + !! "*.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 + +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 2716d50ecd2..3d2d3ee50b5 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
  • diff --git a/src/app/FakeLib/TeamCityHelper.fs b/src/app/FakeLib/TeamCityHelper.fs index 99b9a292441..f26350ae131 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 @@ -32,120 +32,127 @@ 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 /// 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 +166,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 +182,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 +213,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 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()