private async Task <bool> IsGroupJoinableAsync(
            BuildManifestLocation location,
            string commit,
            string buildId,
            IEnumerable <string> changedSemaphorePaths,
            JoinSemaphoreGroup joinGroup)
        {
            string[] remainingSemaphores = joinGroup
                                           .ParallelSemaphorePaths
                                           .Except(changedSemaphorePaths)
                                           .ToArray();

            if (remainingSemaphores.Length == joinGroup.ParallelSemaphorePaths.Count())
            {
                // No semaphores in this group are changing: it can't be joinable by this update.
                return(false);
            }

            // TODO: Avoid redundant fetches if multiple groups share a semaphore. https://github.com/dotnet/buildtools/issues/1910
            bool[] remainingSemaphoreIsComplete = await Task.WhenAll(
                remainingSemaphores.Select(
                    async path =>
            {
                SemaphoreModel semaphore = await FetchSemaphoreAsync(
                    location.GitHubProject,
                    commit,
                    location.GitHubBasePath,
                    path);

                return(semaphore?.BuildId == buildId);
            }));

            return(remainingSemaphoreIsComplete.All(x => x));
        }
        public async Task <SemaphoreModel> FetchSemaphoreAsync(
            GitHubProject project,
            string @ref,
            string basePath,
            string semaphorePath)
        {
            string contents = await _github.GetGitHubFileContentsAsync(
                $"{basePath}/{semaphorePath}",
                project,
                @ref);

            return(SemaphoreModel.Parse(semaphorePath, contents));
        }