示例#1
0
        public async Task <int> LocalRunAsync(CommandSettings command, CancellationToken token)
        {
            Trace.Info(nameof(LocalRunAsync));

            // Warn preview.
            _term.WriteLine("This command is currently in preview. The interface and behavior will change in a future version.");
            if (!command.Unattended)
            {
                _term.WriteLine("Press Enter to continue.");
                _term.ReadLine();
            }

            HostContext.RunMode = RunMode.Local;

            // Resolve the YAML file path.
            string ymlFile = command.GetYml();

            if (string.IsNullOrEmpty(ymlFile))
            {
                string[] ymlFiles =
                    Directory.GetFiles(Directory.GetCurrentDirectory())
                    .Where((string filePath) =>
                {
                    return(filePath.EndsWith(".yml", IOUtil.FilePathStringComparison));
                })
                    .ToArray();
                if (ymlFiles.Length > 1)
                {
                    throw new Exception($"More than one .yml file exists in the current directory. Specify which file to use via the '{Constants.Agent.CommandLine.Args.Yml}' command line argument.");
                }

                ymlFile = ymlFiles.FirstOrDefault();
            }

            if (string.IsNullOrEmpty(ymlFile))
            {
                throw new Exception($"Unable to find a .yml file in the current directory. Specify which file to use via the '{Constants.Agent.CommandLine.Args.Yml}' command line argument.");
            }

            // Load the YAML file.
            var parseOptions = new ParseOptions
            {
                MaxFiles = 10,
                MustacheEvaluationMaxResultLength = 512 * 1024, // 512k string length
                MustacheEvaluationTimeout         = TimeSpan.FromSeconds(10),
                MustacheMaxDepth = 5,
            };
            var pipelineParser = new PipelineParser(new PipelineTraceWriter(), new PipelineFileProvider(), parseOptions);

            Pipelines.Process process = pipelineParser.Load(
                defaultRoot: Directory.GetCurrentDirectory(),
                path: ymlFile,
                mustacheContext: null,
                cancellationToken: HostContext.AgentShutdownToken);
            ArgUtil.NotNull(process, nameof(process));
            if (command.WhatIf)
            {
                return(Constants.Agent.ReturnCode.Success);
            }

            // Verify the current directory is the root of a git repo.
            string repoDirectory = Directory.GetCurrentDirectory();

            if (!Directory.Exists(Path.Combine(repoDirectory, ".git")))
            {
                throw new Exception("Unable to run the build locally. The command must be executed from the root directory of a local git repository.");
            }

            // Get the URL - required if missing tasks.
            string url = command.GetUrl(suppressPromptIfEmpty: true);

            if (string.IsNullOrEmpty(url))
            {
                if (!TestAllTasksCached(process, token))
                {
                    url = command.GetUrl(suppressPromptIfEmpty: false);
                }
            }

            if (!string.IsNullOrEmpty(url))
            {
                // Initialize and store the HTTP client.
                var credentialManager = HostContext.GetService <ICredentialManager>();

                // Get the auth type. On premise defaults to negotiate (Kerberos with fallback to NTLM).
                // Hosted defaults to PAT authentication.
                string defaultAuthType = UrlUtil.IsHosted(url) ? Constants.Configuration.PAT :
                                         (Constants.Agent.Platform == Constants.OSPlatform.Windows ? Constants.Configuration.Integrated : Constants.Configuration.Negotiate);
                string authType = command.GetAuth(defaultValue: defaultAuthType);
                ICredentialProvider provider = credentialManager.GetCredentialProvider(authType);
                provider.EnsureCredential(HostContext, command, url);
                _taskStore.HttpClient = new TaskAgentHttpClient(new Uri(url), provider.GetVssCredentials(HostContext));
            }

            var           configStore = HostContext.GetService <IConfigurationStore>();
            AgentSettings settings    = configStore.GetSettings();

            // Create job message.
            IJobDispatcher jobDispatcher = null;

            try
            {
                jobDispatcher = HostContext.CreateService <IJobDispatcher>();
                foreach (JobInfo job in await ConvertToJobMessagesAsync(process, repoDirectory, token))
                {
                    job.RequestMessage.Environment.Variables[Constants.Variables.Agent.RunMode] = RunMode.Local.ToString();
                    jobDispatcher.Run(job.RequestMessage);
                    Task jobDispatch = jobDispatcher.WaitAsync(token);
                    if (!Task.WaitAll(new[] { jobDispatch }, job.Timeout))
                    {
                        jobDispatcher.Cancel(job.CancelMessage);

                        // Finish waiting on the same job dispatch task. The first call to WaitAsync dequeues
                        // the dispatch task and then proceeds to wait on it. So we need to continue awaiting
                        // the task instance (queue is now empty).
                        await jobDispatch;
                    }
                }
            }
            finally
            {
                if (jobDispatcher != null)
                {
                    await jobDispatcher.ShutdownAsync();
                }
            }

            return(Constants.Agent.ReturnCode.Success);
        }
示例#2
0
        public async Task <int> RunAsync(CommandSettings command, CancellationToken token)
        {
            Trace.Info(nameof(RunAsync));
            var           configStore = HostContext.GetService <IConfigurationStore>();
            AgentSettings settings    = configStore.GetSettings();

            // Store the HTTP client.
            // todo: fix in master to allow URL to be empty and then rebase on master.
            const string DefaultUrl = "http://127.0.0.1/local-runner-default-url";
            string       url        = command.GetUrl(DefaultUrl);

            if (!string.Equals(url, DefaultUrl, StringComparison.Ordinal))
            {
                var    credentialManager     = HostContext.GetService <ICredentialManager>();
                string authType              = command.GetAuth(defaultValue: Constants.Configuration.Integrated);
                ICredentialProvider provider = credentialManager.GetCredentialProvider(authType);
                provider.EnsureCredential(HostContext, command, url);
                _httpClient = new TaskAgentHttpClient(new Uri(url), provider.GetVssCredentials(HostContext));
            }

            // Load the YAML file.
            string yamlFile = command.GetYaml();

            ArgUtil.File(yamlFile, nameof(yamlFile));
            var parseOptions = new ParseOptions
            {
                MaxFiles = 10,
                MustacheEvaluationMaxResultLength = 512 * 1024, // 512k string length
                MustacheEvaluationTimeout         = TimeSpan.FromSeconds(10),
                MustacheMaxDepth = 5,
            };
            var pipelineParser = new PipelineParser(new PipelineTraceWriter(), new PipelineFileProvider(), parseOptions);

            Pipelines.Process process = pipelineParser.Load(
                defaultRoot: Directory.GetCurrentDirectory(),
                path: yamlFile,
                mustacheContext: null,
                cancellationToken: HostContext.AgentShutdownToken);
            ArgUtil.NotNull(process, nameof(process));
            if (command.WhatIf)
            {
                return(Constants.Agent.ReturnCode.Success);
            }

            // Create job message.
            IJobDispatcher jobDispatcher = null;

            try
            {
                jobDispatcher = HostContext.CreateService <IJobDispatcher>();
                foreach (JobInfo job in await ConvertToJobMessagesAsync(process, token))
                {
                    job.RequestMessage.Environment.Variables[Constants.Variables.Agent.RunMode] = RunMode.Local.ToString();
                    jobDispatcher.Run(job.RequestMessage);
                    Task jobDispatch = jobDispatcher.WaitAsync(token);
                    if (!Task.WaitAll(new[] { jobDispatch }, job.Timeout))
                    {
                        jobDispatcher.Cancel(job.CancelMessage);

                        // Finish waiting on the same job dispatch task. The first call to WaitAsync dequeues
                        // the dispatch task and then proceeds to wait on it. So we need to continue awaiting
                        // the task instance (queue is now empty).
                        await jobDispatch;
                    }
                }
            }
            finally
            {
                if (jobDispatcher != null)
                {
                    await jobDispatcher.ShutdownAsync();
                }
            }

            return(Constants.Agent.ReturnCode.Success);
        }
示例#3
0
        private async Task <List <JobInfo> > ConvertToJobMessagesAsync(Pipelines.Process process, string repoDirectory, CancellationToken token)
        {
            // Collect info about the repo.
            string repoName = Path.GetFileName(repoDirectory);
            string userName = await GitAsync("config --get user.name", token);

            string userEmail = await GitAsync("config --get user.email", token);

            string branch = await GitAsync("symbolic-ref HEAD", token);

            string commit = await GitAsync("rev-parse HEAD", token);

            string commitAuthorName = await GitAsync("show --format=%an --no-patch HEAD", token);

            string commitSubject = await GitAsync("show --format=%s --no-patch HEAD", token);

            var jobs      = new List <JobInfo>();
            int requestId = 1;

            foreach (Phase phase in process.Phases ?? new List <IPhase>(0))
            {
                foreach (Job job in phase.Jobs ?? new List <IJob>(0))
                {
                    var builder = new StringBuilder();
                    builder.Append($@"{{
  ""tasks"": [");
                    var steps = new List <ISimpleStep>();
                    foreach (IStep step in job.Steps ?? new List <IStep>(0))
                    {
                        if (step is ISimpleStep)
                        {
                            steps.Add(step as ISimpleStep);
                        }
                        else
                        {
                            var stepsPhase = step as StepsPhase;
                            foreach (ISimpleStep nestedStep in stepsPhase.Steps ?? new List <ISimpleStep>(0))
                            {
                                steps.Add(nestedStep);
                            }
                        }
                    }

                    bool firstStep = true;
                    foreach (ISimpleStep step in steps)
                    {
                        if (!(step is TaskStep))
                        {
                            throw new Exception("Unable to run step type: " + step.GetType().FullName);
                        }

                        var task = step as TaskStep;
                        if (!task.Enabled)
                        {
                            continue;
                        }

                        // Sanity check - the pipeline parser should have already validated version is an int.
                        int taskVersion;
                        if (!int.TryParse(task.Reference.Version, NumberStyles.None, CultureInfo.InvariantCulture, out taskVersion))
                        {
                            throw new Exception($"Unexpected task version format. Expected an unsigned integer with no formatting. Actual: '{taskVersion}'");
                        }

                        TaskDefinition definition = await _taskStore.GetTaskAsync(
                            name : task.Reference.Name,
                            version : taskVersion,
                            token : token);

                        await _taskStore.EnsureCachedAsync(definition, token);

                        if (!firstStep)
                        {
                            builder.Append(",");
                        }

                        firstStep = false;
                        builder.Append($@"
    {{
      ""instanceId"": ""{Guid.NewGuid()}"",
      ""displayName"": {JsonConvert.ToString(!string.IsNullOrEmpty(task.Name) ? task.Name : definition.InstanceNameFormat)},
      ""enabled"": true,
      ""continueOnError"": {task.ContinueOnError.ToString().ToLowerInvariant()},
      ""condition"": {JsonConvert.ToString(task.Condition)},
      ""alwaysRun"": false,
      ""timeoutInMinutes"": {task.TimeoutInMinutes.ToString(CultureInfo.InvariantCulture)},
      ""id"": ""{definition.Id}"",
      ""name"": {JsonConvert.ToString(definition.Name)},
      ""version"": {JsonConvert.ToString(definition.Version.ToString())},
      ""inputs"": {{");
                        bool firstInput = true;
                        foreach (KeyValuePair <string, string> input in task.Inputs ?? new Dictionary <string, string>(0))
                        {
                            if (!firstInput)
                            {
                                builder.Append(",");
                            }

                            firstInput = false;
                            builder.Append($@"
        {JsonConvert.ToString(input.Key)}: {JsonConvert.ToString(input.Value)}");
                        }

                        builder.Append($@"
      }},
      ""environment"": {{");
                        bool firstEnv = true;
                        foreach (KeyValuePair <string, string> env in task.Environment ?? new Dictionary <string, string>(0))
                        {
                            if (!firstEnv)
                            {
                                builder.Append(",");
                            }

                            firstEnv = false;
                            builder.Append($@"
        {JsonConvert.ToString(env.Key)}: {JsonConvert.ToString(env.Value)}");
                        }
                        builder.Append($@"
      }}
    }}");
                    }

                    builder.Append($@"
  ],
  ""requestId"": {requestId++},
  ""lockToken"": ""00000000-0000-0000-0000-000000000000"",
  ""lockedUntil"": ""0001-01-01T00:00:00"",
  ""messageType"": ""JobRequest"",
  ""plan"": {{
    ""scopeIdentifier"": ""00000000-0000-0000-0000-000000000000"",
    ""planType"": ""Build"",
    ""version"": 8,
    ""planId"": ""00000000-0000-0000-0000-000000000000"",
    ""artifactUri"": ""vstfs:///Build/Build/1234"",
    ""artifactLocation"": null
  }},
  ""timeline"": {{
    ""id"": ""00000000-0000-0000-0000-000000000000"",
    ""changeId"": 1,
    ""location"": null
  }},
  ""jobId"": ""{Guid.NewGuid()}"",
  ""jobName"": {JsonConvert.ToString(!string.IsNullOrEmpty(job.Name) ? job.Name : "Build")},
  ""environment"": {{
    ""endpoints"": [
      {{
        ""data"": {{
          ""repositoryId"": ""00000000-0000-0000-0000-000000000000"",
          ""localDirectory"": {JsonConvert.ToString(repoDirectory)},
          ""clean"": ""false"",
          ""checkoutSubmodules"": ""False"",
          ""onpremtfsgit"": ""False"",
          ""fetchDepth"": ""0"",
          ""gitLfsSupport"": ""false"",
          ""skipSyncSource"": ""false"",
          ""cleanOptions"": ""0""
        }},
        ""name"": {JsonConvert.ToString(repoName)},
        ""type"": ""LocalRun"",
        ""url"": ""https://127.0.0.1/vsts-agent-local-runner?directory={Uri.EscapeDataString(repoDirectory)}"",
        ""authorization"": {{
          ""parameters"": {{
            ""AccessToken"": ""dummy-access-token""
          }},
          ""scheme"": ""OAuth""
        }},
        ""isReady"": false
      }}
    ],
    ""mask"": [
      {{
        ""type"": ""regex"",
        ""value"": ""dummy-access-token""
      }}
    ],
    ""variables"": {{");
                    builder.Append($@"
      ""system"": ""build"",
      ""system.collectionId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.culture"": ""en-US"",
      ""system.definitionId"": ""55"",
      ""system.isScheduled"": ""False"",
      ""system.hosttype"": ""build"",
      ""system.jobId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.planId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.timelineId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.taskDefinitionsUri"": ""https://127.0.0.1/vsts-agent-local-runner"",
      ""system.teamFoundationCollectionUri"": ""https://127.0.0.1/vsts-agent-local-runner"",
      ""system.teamProject"": {JsonConvert.ToString(repoName)},
      ""system.teamProjectId"": ""00000000-0000-0000-0000-000000000000"",
      ""build.buildId"": ""1863"",
      ""build.buildNumber"": ""1863"",
      ""build.buildUri"": ""vstfs:///Build/Build/1863"",
      ""build.clean"": """",
      ""build.definitionName"": ""My Build Definition Name"",
      ""build.definitionVersion"": ""1"",
      ""build.queuedBy"": {JsonConvert.ToString(userName)},
      ""build.queuedById"": ""00000000-0000-0000-0000-000000000000"",
      ""build.requestedFor"": {JsonConvert.ToString(userName)},
      ""build.requestedForEmail"": {JsonConvert.ToString(userEmail)},
      ""build.requestedForId"": ""00000000-0000-0000-0000-000000000000"",
      ""build.repository.uri"": ""https://127.0.0.1/vsts-agent-local-runner/_git/{Uri.EscapeDataString(repoName)}"",
      ""build.sourceBranch"": {JsonConvert.ToString(branch)},
      ""build.sourceBranchName"": {JsonConvert.ToString(branch.Split('/').Last())},
      ""build.sourceVersion"": {JsonConvert.ToString(commit)},
      ""build.sourceVersionAuthor"": {JsonConvert.ToString(commitAuthorName)},
      ""build.sourceVersionMessage"": {JsonConvert.ToString(commitSubject)},
      ""AZURE_HTTP_USER_AGENT"": ""VSTS_00000000-0000-0000-0000-000000000000_build_55_1863"",
      ""MSDEPLOY_HTTP_USER_AGENT"": ""VSTS_00000000-0000-0000-0000-000000000000_build_55_1863""");
                    foreach (Variable variable in job.Variables ?? new List <IVariable>(0))
                    {
                        builder.Append($@",
      {JsonConvert.ToString(variable.Name ?? string.Empty)}: {JsonConvert.ToString(variable.Value ?? string.Empty)}");
                    }

                    builder.Append($@"
    }},
    ""systemConnection"": {{
      ""data"": {{
        ""ServerId"": ""00000000-0000-0000-0000-000000000000"",
        ""ServerName"": ""127.0.0.1""
      }},
      ""name"": ""SystemVssConnection"",
      ""url"": ""https://127.0.0.1/vsts-agent-local-runner"",
      ""authorization"": {{
        ""parameters"": {{
          ""AccessToken"": ""dummy-access-token""
        }},
        ""scheme"": ""OAuth""
      }},
      ""isReady"": false
    }}
  }}
}}");
                    string message = builder.ToString();
                    try
                    {
                        jobs.Add(new JobInfo(job, message));
                    }
                    catch
                    {
                        Dump("Job message JSON", message);
                        throw;
                    }
                }
            }

            return(jobs);
        }
示例#4
0
        private async Task <List <JobInfo> > ConvertToJobMessagesAsync(Pipelines.Process process, CancellationToken token)
        {
            var jobs      = new List <JobInfo>();
            int requestId = 1;

            foreach (Phase phase in process.Phases ?? new List <IPhase>(0))
            {
                foreach (Job job in phase.Jobs ?? new List <IJob>(0))
                {
                    var builder = new StringBuilder();
                    builder.Append($@"{{
  ""tasks"": [");
                    var steps = new List <ISimpleStep>();
                    foreach (IStep step in job.Steps ?? new List <IStep>(0))
                    {
                        if (step is ISimpleStep)
                        {
                            steps.Add(step as ISimpleStep);
                        }
                        else
                        {
                            var stepsPhase = step as StepsPhase;
                            foreach (ISimpleStep nestedStep in stepsPhase.Steps ?? new List <ISimpleStep>(0))
                            {
                                steps.Add(nestedStep);
                            }
                        }
                    }

                    bool firstStep = true;
                    foreach (ISimpleStep step in steps)
                    {
                        if (!(step is TaskStep))
                        {
                            throw new Exception("Unable to run step type: " + step.GetType().FullName);
                        }

                        var task = step as TaskStep;
                        if (!task.Enabled)
                        {
                            continue;
                        }

                        TaskDefinition definition = await GetDefinitionAsync(task, token);

                        if (!firstStep)
                        {
                            builder.Append(",");
                        }

                        firstStep = false;
                        builder.Append($@"
    {{
      ""instanceId"": ""{Guid.NewGuid()}"",
      ""displayName"": {JsonConvert.ToString(!string.IsNullOrEmpty(task.Name) ? task.Name : definition.InstanceNameFormat)},
      ""enabled"": true,
      ""continueOnError"": {task.ContinueOnError.ToString().ToLowerInvariant()},
      ""condition"": {JsonConvert.ToString(task.Condition)},
      ""alwaysRun"": false,
      ""timeoutInMinutes"": {task.TimeoutInMinutes.ToString(CultureInfo.InvariantCulture)},
      ""id"": ""{definition.Id}"",
      ""name"": {JsonConvert.ToString(definition.Name)},
      ""version"": {JsonConvert.ToString(GetVersion(definition).ToString())},
      ""inputs"": {{");
                        bool firstInput = true;
                        foreach (KeyValuePair <string, string> input in task.Inputs ?? new Dictionary <string, string>(0))
                        {
                            if (!firstInput)
                            {
                                builder.Append(",");
                            }

                            firstInput = false;
                            builder.Append($@"
        {JsonConvert.ToString(input.Key)}: {JsonConvert.ToString(input.Value)}");
                        }

                        builder.Append($@"
      }},
      ""environment"": {{");
                        bool firstEnv = true;
                        foreach (KeyValuePair <string, string> env in task.Environment ?? new Dictionary <string, string>(0))
                        {
                            if (!firstEnv)
                            {
                                builder.Append(",");
                            }

                            firstEnv = false;
                            builder.Append($@"
        {JsonConvert.ToString(env.Key)}: {JsonConvert.ToString(env.Value)}");
                        }
                        builder.Append($@"
      }}
    }}");
                    }

                    builder.Append($@"
  ],
  ""requestId"": {requestId++},
  ""lockToken"": ""00000000-0000-0000-0000-000000000000"",
  ""lockedUntil"": ""0001-01-01T00:00:00"",
  ""messageType"": ""JobRequest"",
  ""plan"": {{
    ""scopeIdentifier"": ""00000000-0000-0000-0000-000000000000"",
    ""planType"": ""Build"",
    ""version"": 8,
    ""planId"": ""00000000-0000-0000-0000-000000000000"",
    ""artifactUri"": ""vstfs:///Build/Build/1234"",
    ""artifactLocation"": null
  }},
  ""timeline"": {{
    ""id"": ""00000000-0000-0000-0000-000000000000"",
    ""changeId"": 1,
    ""location"": null
  }},
  ""jobId"": ""{Guid.NewGuid()}"",
  ""jobName"": {JsonConvert.ToString(!string.IsNullOrEmpty(job.Name) ? job.Name : "Build")},
  ""environment"": {{
    ""endpoints"": [
      {{
        ""data"": {{
          ""repositoryId"": ""00000000-0000-0000-0000-000000000000"",
          ""rootFolder"": null,
          ""clean"": ""false"",
          ""checkoutSubmodules"": ""False"",
          ""onpremtfsgit"": ""False"",
          ""fetchDepth"": ""0"",
          ""gitLfsSupport"": ""false"",
          ""skipSyncSource"": ""true"",
          ""cleanOptions"": ""0""
        }},
        ""name"": ""gitTest"",
        ""type"": ""TfsGit"",
        ""url"": ""https://127.0.0.1/vsts-agent-local-runner/_git/gitTest"",
        ""authorization"": {{
          ""parameters"": {{
            ""AccessToken"": ""dummy-access-token""
          }},
          ""scheme"": ""OAuth""
        }},
        ""isReady"": false
      }}
    ],
    ""mask"": [
      {{
        ""type"": ""regex"",
        ""value"": ""dummy-access-token""
      }}
    ],
    ""variables"": {{");
                    builder.Append($@"
      ""system"": ""build"",
      ""system.collectionId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.teamProject"": ""gitTest"",
      ""system.teamProjectId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.definitionId"": ""55"",
      ""build.definitionName"": ""My Build Definition Name"",
      ""build.definitionVersion"": ""1"",
      ""build.queuedBy"": ""John Doe"",
      ""build.queuedById"": ""00000000-0000-0000-0000-000000000000"",
      ""build.requestedFor"": ""John Doe"",
      ""build.requestedForId"": ""00000000-0000-0000-0000-000000000000"",
      ""build.requestedForEmail"": ""*****@*****.**"",
      ""build.sourceVersion"": ""55ba1763b74d42a758514b466b7ea931aedbc941"",
      ""build.sourceBranch"": ""refs/heads/master"",
      ""build.sourceBranchName"": ""master"",
      ""system.culture"": ""en-US"",
      ""build.clean"": """",
      ""build.buildId"": ""1863"",
      ""build.buildUri"": ""vstfs:///Build/Build/1863"",
      ""build.buildNumber"": ""1863"",
      ""system.isScheduled"": ""False"",
      ""system.hosttype"": ""build"",
      ""system.teamFoundationCollectionUri"": ""https://127.0.0.1/vsts-agent-local-runner"",
      ""system.taskDefinitionsUri"": ""https://127.0.0.1/vsts-agent-local-runner"",
      ""AZURE_HTTP_USER_AGENT"": ""VSTS_00000000-0000-0000-0000-000000000000_build_55_1863"",
      ""MSDEPLOY_HTTP_USER_AGENT"": ""VSTS_00000000-0000-0000-0000-000000000000_build_55_1863"",
      ""system.planId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.jobId"": ""00000000-0000-0000-0000-000000000000"",
      ""system.timelineId"": ""00000000-0000-0000-0000-000000000000"",
      ""build.repository.uri"": ""https://127.0.0.1/vsts-agent-local-runner/_git/gitTest"",
      ""build.sourceVersionAuthor"": ""John Doe"",
      ""build.sourceVersionMessage"": ""Updated Program.cs""");
                    foreach (Variable variable in job.Variables ?? new List <IVariable>(0))
                    {
                        builder.Append($@",
      {JsonConvert.ToString(variable.Name ?? string.Empty)}: {JsonConvert.ToString(variable.Value ?? string.Empty)}");
                    }

                    builder.Append($@"
    }},
    ""systemConnection"": {{
      ""data"": {{
        ""ServerId"": ""00000000-0000-0000-0000-000000000000"",
        ""ServerName"": ""127.0.0.1""
      }},
      ""name"": ""SystemVssConnection"",
      ""url"": ""https://127.0.0.1/vsts-agent-local-runner"",
      ""authorization"": {{
        ""parameters"": {{
          ""AccessToken"": ""dummy-access-token""
        }},
        ""scheme"": ""OAuth""
      }},
      ""isReady"": false
    }}
  }}
}}");
                    string message = builder.ToString();
                    try
                    {
                        jobs.Add(new JobInfo(job, message));
                    }
                    catch
                    {
                        Dump("Job message JSON", message);
                        throw;
                    }
                }
            }

            return(jobs);
        }