├── .atomist └── hooks │ └── pre-code-build ├── .gitattributes ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── bin ├── command.ts ├── git-info.ts ├── gql-gen.ts ├── start-dev.ts └── start.ts ├── config ├── default.json └── production.json ├── docs ├── Client.md └── PathExpressions.md ├── legal └── THIRD_PARTY.md ├── lib ├── HandleCommand.ts ├── HandleEvent.ts ├── HandlerContext.ts ├── HandlerResult.ts ├── SmartParameters.ts ├── action │ ├── ActionResult.ts │ └── actionOps.ts ├── atomistWebhook.ts ├── automationClient.ts ├── configuration.ts ├── decorators.ts ├── globals.ts ├── graph │ ├── ApolloGraphClient.ts │ ├── ApolloGraphClientFactory.ts │ ├── graphQL.ts │ └── schema.json ├── internal │ ├── common │ │ ├── AbstractScriptedFlushable.ts │ │ └── Flushable.ts │ ├── env │ │ ├── NodeConfigSecretResolver.ts │ │ └── gitInfo.ts │ ├── event │ │ ├── InMemoryEventStore.ts │ │ └── NoOpEventStore.ts │ ├── graph │ │ └── graphQL.ts │ ├── invoker │ │ ├── Invoker.ts │ │ ├── Payload.ts │ │ └── disposable.ts │ ├── message │ │ ├── ConsoleMessageClient.ts │ │ └── DebugMessageClient.ts │ ├── metadata │ │ ├── MetadataStore.ts │ │ ├── decoratorSupport.ts │ │ ├── metadata.ts │ │ └── metadataReading.ts │ ├── parameterPopulation.ts │ ├── transport │ │ ├── AbstractRequestProcessor.ts │ │ ├── EventStoringAutomationEventListener.ts │ │ ├── MetricEnabledAutomationEventListener.ts │ │ ├── RequestProcessor.ts │ │ ├── cluster │ │ │ ├── ClusterMasterRequestProcessor.ts │ │ │ ├── ClusterWorkerRequestProcessor.ts │ │ │ └── messages.ts │ │ ├── express │ │ │ ├── ExpressRequestProcessor.ts │ │ │ └── ExpressServer.ts │ │ ├── showStartupMessages.ts │ │ └── websocket │ │ │ ├── DefaultWebSocketRequestProcessor.ts │ │ │ ├── WebSocketClient.ts │ │ │ ├── WebSocketLifecycle.ts │ │ │ ├── WebSocketMessageClient.ts │ │ │ ├── WebSocketRequestProcessor.ts │ │ │ └── payloads.ts │ └── util │ │ ├── Deferred.ts │ │ ├── async.ts │ │ ├── base64.ts │ │ ├── cls.ts │ │ ├── config.ts │ │ ├── health.ts │ │ ├── http.ts │ │ ├── info.ts │ │ ├── memory.ts │ │ ├── metric.ts │ │ ├── poll.ts │ │ ├── shutdown.ts │ │ └── string.ts ├── metadata │ ├── automationMetadata.ts │ └── parameterUtils.ts ├── onCommand.ts ├── onEvent.ts ├── operations │ ├── CommandDetails.ts │ ├── common │ │ ├── AbstractRemoteRepoRef.ts │ │ ├── BasicAuthCredentials.ts │ │ ├── BitBucketRepoRef.ts │ │ ├── BitBucketServerRepoRef.ts │ │ ├── GitHubRepoRef.ts │ │ ├── GitlabPrivateTokenCredentials.ts │ │ ├── GitlabRepoRef.ts │ │ ├── ProjectOperationCredentials.ts │ │ ├── RepoId.ts │ │ ├── SourceLocation.ts │ │ ├── defaultRepoLoader.ts │ │ ├── fromProjectList.ts │ │ ├── gitHubRepoLoader.ts │ │ ├── gitlabRepoLoader.ts │ │ ├── localRepoLoader.ts │ │ ├── params │ │ │ ├── AlwaysAskRepoParameters.ts │ │ │ ├── BaseEditorOrReviewerParameters.ts │ │ │ ├── Credentialed.ts │ │ │ ├── EditOneOrAllParameters.ts │ │ │ ├── FallbackParams.ts │ │ │ ├── GitHubFallbackReposParameters.ts │ │ │ ├── GitHubSourceRepoParameters.ts │ │ │ ├── GitHubTargetsParams.ts │ │ │ ├── GitlabTargetsParams.ts │ │ │ ├── MappedRepoParameters.ts │ │ │ ├── RemoteLocator.ts │ │ │ ├── SourceRepoParameters.ts │ │ │ ├── TargetsParams.ts │ │ │ └── validationPatterns.ts │ │ ├── projectAction.ts │ │ ├── repoFilter.ts │ │ ├── repoFinder.ts │ │ ├── repoLoader.ts │ │ └── repoUtils.ts │ ├── edit │ │ ├── editAll.ts │ │ ├── editModes.ts │ │ ├── editorToCommand.ts │ │ ├── projectEditor.ts │ │ └── projectEditorOps.ts │ ├── generate │ │ ├── BaseSeedDrivenGeneratorParameters.ts │ │ ├── GitHubRepoCreationParameters.ts │ │ ├── GitlabRepoCreationParameters.ts │ │ ├── NewRepoCreationParameters.ts │ │ ├── RepoCreationParameters.ts │ │ ├── SeedDrivenGeneratorParameters.ts │ │ ├── generatorUtils.ts │ │ ├── gitHubProjectPersister.ts │ │ └── remoteGitProjectPersister.ts │ ├── review │ │ ├── ReviewResult.ts │ │ ├── issueRaisingReviewRouter.ts │ │ ├── projectReviewer.ts │ │ ├── reviewAll.ts │ │ └── reviewerToCommand.ts │ ├── support │ │ ├── contextUtils.ts │ │ └── editorUtils.ts │ └── tagger │ │ ├── Tagger.ts │ │ └── taggerHandler.ts ├── project │ ├── File.ts │ ├── HasCache.ts │ ├── Project.ts │ ├── diff │ │ ├── Action.ts │ │ ├── Chain.ts │ │ ├── Changes.ts │ │ ├── Differ.ts │ │ ├── DifferenceEngine.ts │ │ └── Extractor.ts │ ├── fileGlobs.ts │ ├── fingerprint │ │ └── Fingerprint.ts │ ├── git │ │ ├── Configurable.ts │ │ ├── GitCommandGitProject.ts │ │ ├── GitProject.ts │ │ └── gitStatus.ts │ ├── local │ │ ├── LocalFile.ts │ │ ├── LocalProject.ts │ │ ├── NodeFsLocalFile.ts │ │ └── NodeFsLocalProject.ts │ ├── mem │ │ ├── InMemoryFile.ts │ │ └── InMemoryProject.ts │ ├── support │ │ ├── AbstractFile.ts │ │ └── AbstractProject.ts │ └── util │ │ ├── diagnosticUtils.ts │ │ ├── jsonUtils.ts │ │ ├── parseUtils.ts │ │ ├── projectInvariants.ts │ │ ├── projectUtils.ts │ │ └── sourceLocationUtils.ts ├── scan.ts ├── schema │ ├── schema.cortex.ts │ └── schema.ts ├── secured.ts ├── server │ ├── AbstractAutomationServer.ts │ ├── AutomationEventListener.ts │ ├── AutomationServer.ts │ └── BuildableAutomationServer.ts ├── spi │ ├── clone │ │ ├── DirectoryManager.ts │ │ ├── StableDirectoryManager.ts │ │ └── tmpDirectoryManager.ts │ ├── env │ │ ├── MetadataProcessor.ts │ │ └── SecretResolver.ts │ ├── event │ │ └── EventStore.ts │ ├── graph │ │ ├── GraphClient.ts │ │ └── GraphClientFactory.ts │ ├── http │ │ ├── axiosHttpClient.ts │ │ ├── curlHttpClient.ts │ │ ├── httpClient.ts │ │ └── wsClient.ts │ ├── message │ │ ├── MessageClient.ts │ │ └── MessageClientSupport.ts │ └── statsd │ │ ├── statsd.ts │ │ └── statsdClient.ts ├── tree │ ├── LocatedTreeNode.ts │ └── ast │ │ ├── FileHits.ts │ │ ├── FileParser.ts │ │ ├── FileParserRegistry.ts │ │ ├── astUtils.ts │ │ ├── matchTesters.ts │ │ ├── microgrammar │ │ └── MicrogrammarBasedFileParser.ts │ │ ├── regex │ │ └── RegexFileParser.ts │ │ └── typescript │ │ └── TypeScriptFileParser.ts └── util │ ├── child_process.ts │ ├── constructionUtils.ts │ ├── error.ts │ ├── gitHub.ts │ ├── http.ts │ ├── logger.ts │ ├── packageJson.ts │ ├── pool.ts │ ├── port.ts │ ├── redact.ts │ └── retry.ts ├── package-lock.json ├── package.json ├── scripts └── atomist-setup.bash ├── test ├── .atomist │ ├── client.config-production.json │ └── client.config.json ├── action │ └── actionChaining.test.ts ├── api │ ├── ApolloGraphClient.test.ts │ ├── GitProjectRemote.test.ts │ ├── apiUtils.ts │ ├── editorUtilsWithGitHubPullRequest.test.ts │ ├── generatorEndToEnd.test.ts │ └── gitHub.test.ts ├── asyncConfig.ts ├── atomist.config.ts ├── atomistWebhook.test.ts ├── benchmark │ └── generator.benchmark.ts ├── bitbucket-api │ ├── BitBucketGit.test.ts │ ├── BitBucketHelpers.ts │ └── generatorEndToEnd.test.ts ├── command │ ├── FileMessage.test.ts │ ├── HelloWorld.ts │ ├── Message.test.ts │ ├── PlainHelloWorld.ts │ ├── SearchStackOverflow.ts │ ├── SecretBaseHandler.ts │ ├── SendStartupMessage.ts │ └── onCommand.test.ts ├── configuration.test.ts ├── credentials.ts ├── empty.config.ts ├── event │ ├── GitLabPush.ts │ ├── HelloCircle.ts │ ├── HelloIssue.ts │ ├── HelloWorld.ts │ └── test.json ├── graph │ ├── GraphQL.test.ts │ ├── chatChannelFragment.graphql │ ├── onSentryAlert.graphql.custom │ ├── repoFragment.graphql │ ├── someOtherQuery.graphql │ ├── someOtherQueryWithTheSameName.graphql │ ├── someSubscription.graphql │ ├── someSubscriptionWithEnum.graphql │ ├── someSubscriptionWithEnumArray.graphql │ └── subscriptionWithFragment.graphql ├── graphql │ ├── fragment │ │ ├── chatChannelFragment.graphql │ │ └── repoFragment.graphql │ ├── ingester │ │ ├── helloWorld.graphql │ │ └── sdmGoal.graphql │ ├── mutation │ │ ├── addBotToSlackChannel.graphql │ │ ├── createSlackChannel.graphql │ │ ├── linkSlackChannelToRepo.graphql │ │ └── setChatUserPreference.graphql │ ├── query │ │ └── repos.graphql │ └── subscription │ │ ├── fooBar.graphql │ │ └── subscriptionWithFragmentInGraphql.graphql ├── internal │ ├── env │ │ └── gitInfo.test.ts │ ├── invoker │ │ ├── AbstractScriptedFlushable.test.ts │ │ ├── BuildableAutomationServer.test.ts │ │ ├── TestHandlers.ts │ │ ├── disposable.test.ts │ │ └── functionStyleCommandHandler.test.ts │ ├── metadata │ │ ├── addAtomistSpringAgent.ts │ │ ├── classStyleMetadataReading.test.ts │ │ ├── classWithExternalParametersMetadataReading.test.ts │ │ ├── eventMetadataReading.test.ts │ │ └── functionStyleMetadataReading.test.ts │ ├── transport │ │ ├── AbstractRequestProcessor.test.ts │ │ └── websocket │ │ │ ├── DefaultWebSocketTransportEventHandler.test.ts │ │ │ ├── WebSocketMessageClient.test.ts │ │ │ └── payloads.test.ts │ └── util │ │ ├── health.test.ts │ │ └── string.test.ts ├── operations │ ├── common │ │ ├── BitBucketServerRepoRef.test.ts │ │ ├── GitHubRepoRef.test.ts │ │ ├── GitlabRepoRef.test.ts │ │ ├── fromProjectList.test.ts │ │ ├── params │ │ │ ├── GitHubParams.test.ts │ │ │ ├── TargetParams.test.ts │ │ │ └── validationPatterns.test.ts │ │ └── repoUtils.test.ts │ ├── edit │ │ ├── LocalEditor.test.ts │ │ ├── VerifyEditMode.ts │ │ ├── editAll.test.ts │ │ ├── editOne.test.ts │ │ ├── editorHandler.test.ts │ │ └── projectEditorsOps.test.ts │ ├── generate │ │ └── generatorUtils.test.ts │ ├── review │ │ ├── ReviewResult.test.ts │ │ └── reviewerHandler.test.ts │ ├── support │ │ └── editorUtils.test.ts │ └── tagger │ │ └── tagger.test.ts ├── project │ ├── git │ │ ├── CachedGitClone.test.ts │ │ ├── GitCommandGitProject.test.ts │ │ ├── GitProject.test.ts │ │ └── GitStatus.test.ts │ ├── local │ │ ├── AbstractFile.test.ts │ │ ├── NodeFsLocalFile.test.ts │ │ └── NodeFsLocalProject.test.ts │ ├── mem │ │ └── InMemoryProject.test.ts │ ├── perf │ │ └── diskPerformance.test.ts │ ├── support │ │ └── AbstractProject.test.ts │ ├── util │ │ ├── diagnosticUtils.test.ts │ │ ├── jsonUtils.test.ts │ │ ├── parseUtils.test.ts │ │ ├── projectUtils.test.ts │ │ └── sourceLocationUtils.test.ts │ └── utils.ts ├── scan.test.ts ├── spi │ ├── clone │ │ └── tmpDirectoryManager.test.ts │ ├── http │ │ ├── axiosHttpClient.test.ts │ │ └── curlHttpClient.test.ts │ ├── message │ │ └── MessageClient.test.ts │ └── statsd │ │ └── statsd.test.ts ├── tree │ └── ast │ │ ├── FileParserRegistry.test.ts │ │ ├── astUtils.test.ts │ │ ├── microgrammar │ │ ├── MicrogrammarBasedFileParser.test.ts │ │ └── microgrammars.test.ts │ │ ├── regex │ │ └── RegexFileParser.test.ts │ │ └── typescript │ │ ├── TypeScriptFileParser.test.ts │ │ ├── conversionTests.ts │ │ ├── javaScriptFileParserProject.test.ts │ │ └── typeScriptFileParserProject.test.ts └── util │ ├── child_process.test.ts │ ├── pool.test.ts │ ├── redact.test.ts │ ├── safeStringify.test.ts │ └── toFactory.test.ts └── tsconfig.json /.atomist/hooks/pre-code-build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git config --global user.email "bot@atomist.com" 4 | git config --global user.name "Atomist Bot" 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | legal/THIRD_PARTY.md linguist-generated=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | *~ 5 | .#* 6 | .npmrc 7 | node_modules/ 8 | *.d.ts 9 | *.d.ts.map 10 | *.js 11 | *.js.map 12 | *.log 13 | *.txt 14 | /.nyc_output/ 15 | /build/ 16 | /coverage/ 17 | /doc/ 18 | /log/ 19 | git-info.json 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | .vscode/ 4 | *~ 5 | .#* 6 | .git* 7 | .dockerignore 8 | .npmrc* 9 | .travis.yml 10 | .atomist/ 11 | .nyc_output/ 12 | /build/ 13 | /doc/ 14 | /config/ 15 | /coverage/ 16 | /log/ 17 | /scripts/ 18 | /src/test/ 19 | /test/ 20 | /CO*.md 21 | /Dockerfile 22 | /assets/kubectl/ 23 | *.log 24 | *.txt 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @atomist/automation-client 2 | 3 | This package contains the low-level API client for the Atomist service 4 | underpinning the Atomist Software Delivery Machine (SDM) framework. 5 | Please see the [`@atomist/sdm`][sdm] package for information on how to 6 | develop SDMs. 7 | 8 | [sdm]: https://github.com/atomist/sdm 9 | 10 | ## Support 11 | 12 | General support questions should be discussed in the `#help` 13 | channel on our community Slack team 14 | at [atomist-community.slack.com][slack]. 15 | 16 | If you find a problem, please create an [issue][]. 17 | 18 | [issue]: https://github.com/atomist/automation-client-ts/issues 19 | 20 | ## Development 21 | 22 | You will need to install [node][] to build and test this project. 23 | First install the package dependencies. 24 | 25 | ``` 26 | $ npm ci 27 | ``` 28 | 29 | To run tests, define a GITHUB_TOKEN to any valid token that has repo access. The tests 30 | will create and delete repositories. 31 | 32 | Define GITHUB_VISIBILITY=public if you want these to be public; default is private. 33 | You'll get a 422 response from repo creation if you don't pay for private repos. 34 | 35 | ``` 36 | $ npm run build 37 | ``` 38 | 39 | [node]: https://nodejs.org/ (Node.js) 40 | 41 | ### Release 42 | 43 | To create a new release of the project, we push a button on the Atomist lifecycle message 44 | in the #automation-client-ts [channel](https://atomist-community.slack.com/messages/C74J6MFL0/) in Atomist Community Slack. 45 | 46 | --- 47 | 48 | Created by [Atomist][atomist]. 49 | Need Help? [Join our Slack team][slack]. 50 | 51 | [atomist]: https://atomist.com/ (Atomist - Development Automation) 52 | [slack]: https://join.atomist.com/ (Atomist Community Slack) 53 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Atomist Open Source Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the 4 | Atomist Open Source projects as found on https://github.com/atomist. 5 | 6 | * [Reporting a Vulnerability](#reporting-a-vulnerability) 7 | * [Disclosure Policy](#disclosure-policy) 8 | 9 | ## Reporting a Vulnerability 10 | 11 | The Atomist OSS team and community take all security vulnerabilities 12 | seriously. Thank you for improving the security of our open source 13 | software. We appreciate your efforts and responsible disclosure and will 14 | make every effort to acknowledge your contributions. 15 | 16 | Report security vulnerabilities by emailing the Atomist security team at: 17 | 18 | security@atomist.com 19 | 20 | The lead maintainer will acknowledge your email within 24 hours, and will 21 | send a more detailed response within 48 hours indicating the next steps in 22 | handling your report. After the initial reply to your report, the security 23 | team will endeavor to keep you informed of the progress towards a fix and 24 | full announcement, and may ask for additional information or guidance. 25 | 26 | Report security vulnerabilities in third-party modules to the person or 27 | team maintaining the module. 28 | 29 | ## Disclosure Policy 30 | 31 | When the security team receives a security bug report, they will assign it 32 | to a primary handler. This person will coordinate the fix and release 33 | process, involving the following steps: 34 | 35 | * Confirm the problem and determine the affected versions. 36 | * Audit code to find any potential similar problems. 37 | * Prepare fixes for all releases still under maintenance. These fixes 38 | will be released as fast as possible to NPM. 39 | -------------------------------------------------------------------------------- /bin/git-info.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | /* 3 | * Copyright © 2018 Atomist, Inc. 4 | * 5 | * See LICENSE file. 6 | */ 7 | 8 | import * as fs from "fs-extra"; 9 | import * as path from "path"; 10 | import { obtainGitInfo } from "../lib/internal/env/gitInfo"; 11 | 12 | /* tslint:disable:no-console */ 13 | 14 | /** 15 | * Generate git-info.json for automation client. 16 | */ 17 | async function main(): Promise { 18 | try { 19 | const cwd = process.cwd(); 20 | const gitInfoName = "git-info.json"; 21 | const gitInfoPath = path.join(cwd, gitInfoName); 22 | const gitInfo = await obtainGitInfo(cwd); 23 | await fs.writeJson(gitInfoPath, gitInfo, { spaces: 2, encoding: "utf8" }); 24 | console.info(`Successfully wrote git information to '${gitInfoPath}'`); 25 | process.exit(0); 26 | } catch (e) { 27 | console.error(`Failed to generate Git information: ${e.message}`); 28 | process.exit(1); 29 | } 30 | throw new Error("Should never get here, process.exit() called above"); 31 | } 32 | 33 | main() 34 | .catch((err: Error) => { 35 | console.error(`Unhandled exception: ${err.message}`); 36 | process.exit(101); 37 | }); 38 | -------------------------------------------------------------------------------- /bin/start.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright © 2018 Atomist, Inc. 4 | * 5 | * See LICENSE file. 6 | */ 7 | 8 | // tslint:disable-next-line:no-import-side-effect 9 | import "source-map-support/register"; 10 | import { printError } from "../lib/util/error"; 11 | 12 | async function main(): Promise { 13 | try { 14 | const logging = require("../lib/util/logger"); 15 | logging.configureLogging(logging.ClientLogging); 16 | 17 | let configuration = await require("../lib/configuration").loadConfiguration(); 18 | configuration = require("../lib/scan").enableDefaultScanning(configuration); 19 | await require("../lib/automationClient").automationClient(configuration).run(); 20 | } catch (e) { 21 | printError(e); 22 | process.exit(5); 23 | } 24 | } 25 | 26 | /* tslint:disable:no-console */ 27 | 28 | main() 29 | .catch(e => { 30 | console.error(`Unhandled exception: ${e.message}`); 31 | process.exit(10); 32 | }); 33 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "githubWebUrl": "https://api.github.com", 3 | "targetOwner": "johnsonr", 4 | "visibility": "private", 5 | "repo": "foo", 6 | 7 | "githubToken": "foo" 8 | } 9 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /lib/HandleCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandlerMetadata } from "./metadata/automationMetadata"; 2 | import { OnCommand } from "./onCommand"; 3 | 4 | /** 5 | * Interface for class-based command handlers. 6 | * These combine the parameters with the command. A fresh 7 | * instance will be created for every invocation. Prefer using the 8 | * parameters object to "this" in implementations of the handle method. 9 | * @param P parameters type 10 | */ 11 | export interface HandleCommand

{ 12 | 13 | /** 14 | * OnCommand function for this command 15 | */ 16 | handle: OnCommand

; 17 | 18 | /** 19 | * If this method is implemented, it returns a fresh parameters instance 20 | * to use for this class. Otherwise will use the class itself as its parameters. 21 | * @return {P} new parameters instance 22 | */ 23 | freshParametersInstance?(): P; 24 | 25 | } 26 | 27 | export type SelfDescribingHandleCommand

= HandleCommand

& CommandHandlerMetadata; 28 | -------------------------------------------------------------------------------- /lib/HandleEvent.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "./HandlerContext"; 2 | import { HandlerResult } from "./HandlerResult"; 3 | import { Secret } from "./internal/invoker/Payload"; 4 | import { EventHandlerMetadata } from "./metadata/automationMetadata"; 5 | import { OnEvent } from "./onEvent"; 6 | 7 | export interface EventFired { 8 | 9 | data: T; 10 | extensions: { 11 | operationName: string; 12 | }; 13 | secrets?: Secret[]; 14 | } 15 | 16 | /** 17 | * Handle the given event. Parameters will have been set on the object 18 | * @param {HandlerContext} ctx context from which GraphQL client can be obtained if it's 19 | * necessary to run further queries. 20 | * @return {Promise} result containing status and any command-specific data 21 | */ 22 | export interface HandleEvent { 23 | 24 | handle: OnEvent; 25 | 26 | } 27 | 28 | export type SelfDescribingHandleEvent = HandleEvent & EventHandlerMetadata; 29 | -------------------------------------------------------------------------------- /lib/HandlerContext.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "./configuration"; 2 | import { Contextual } from "./internal/invoker/Payload"; 3 | import { 4 | CommandIncoming, 5 | EventIncoming, 6 | } from "./internal/transport/RequestProcessor"; 7 | import { AutomationContext } from "./internal/util/cls"; 8 | import { GraphClient } from "./spi/graph/GraphClient"; 9 | import { 10 | MessageClient, 11 | SlackMessageClient, 12 | } from "./spi/message/MessageClient"; 13 | 14 | /** 15 | * Context available to all handlers 16 | */ 17 | export interface HandlerContext extends Contextual { 18 | 19 | /** 20 | * Client to use for GraphQL queries 21 | */ 22 | graphClient?: GraphClient; 23 | 24 | /** 25 | * Client to send messages 26 | */ 27 | messageClient: MessageClient & SlackMessageClient; 28 | 29 | /** 30 | * Provides access to the lifecycle of a handler and context 31 | */ 32 | lifecycle?: HandlerLifecycle; 33 | 34 | } 35 | 36 | /** 37 | * Context of the currently running automation 38 | */ 39 | export interface AutomationContextAware { 40 | 41 | context: AutomationContext; 42 | 43 | trigger: CommandIncoming | EventIncoming; 44 | } 45 | 46 | /** 47 | * Access to the currently running automation client configuration 48 | */ 49 | export interface ConfigurationAware { 50 | 51 | configuration: Configuration; 52 | } 53 | 54 | /** 55 | * Lifecycle of the handler and its context 56 | */ 57 | export interface HandlerLifecycle { 58 | 59 | /** 60 | * Register a callback that should be invoked when this context gets disposed 61 | * @param {() => Promise} callback 62 | * @param {string} description 63 | */ 64 | registerDisposable(callback: () => Promise, description?: string): void; 65 | 66 | /** 67 | * Disposes the HandlerContext. 68 | * Before disposing the context, this will invoke all registered disposables 69 | * @returns {Promise} 70 | */ 71 | dispose(): Promise; 72 | } 73 | -------------------------------------------------------------------------------- /lib/SmartParameters.ts: -------------------------------------------------------------------------------- 1 | import { Parameters } from "./decorators"; 2 | 3 | export interface ValidationError { 4 | message: string; 5 | } 6 | 7 | export type ValidationResult = void | ValidationError; 8 | 9 | export function isValidationError(vr: ValidationResult): vr is ValidationError { 10 | const maybeErr = vr as ValidationError; 11 | return !!maybeErr && !!maybeErr.message; 12 | } 13 | 14 | /** 15 | * Interface optionally implemented by parameters objects--whether HandleCommand 16 | * instances or external objects--to perform any binding logic and validate their parameters. 17 | * Allows returning a promise so that implementations can perform network calls etc 18 | * to validate. Simply return void if binding without validation. 19 | */ 20 | export interface SmartParameters { 21 | 22 | bindAndValidate(): ValidationResult | Promise; 23 | } 24 | 25 | export function isSmartParameters(a: any): a is SmartParameters { 26 | const mightBeSmart = a as SmartParameters; 27 | return !!mightBeSmart && !!mightBeSmart.bindAndValidate; 28 | } 29 | 30 | export type ParameterIndexType = string; 31 | export type ParameterType = { 32 | [key in ParameterIndexType]?: number | boolean | string | ParameterType; 33 | }; 34 | 35 | @Parameters() 36 | export class NoParameters implements ParameterType { 37 | } 38 | -------------------------------------------------------------------------------- /lib/action/ActionResult.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /** 19 | * Result of running an action, optionally on a target. Instances may 20 | * add further information beyond boolean success or failure. Useful 21 | * when promise chaining, to allow results to be included along with 22 | * the target. 23 | */ 24 | export interface ActionResult { 25 | /** Target on which we ran the action, if there is one. */ 26 | readonly target: T; 27 | 28 | /** Whether or not the action succeeded. */ 29 | readonly success: boolean; 30 | 31 | /** Error that occurred, if any */ 32 | readonly error?: Error; 33 | 34 | /** Description of step that errored, if one did. */ 35 | readonly errorStep?: string; 36 | } 37 | 38 | /** Test if an object is an ActionResult */ 39 | export function isActionResult(a: any): a is ActionResult { 40 | return a.target !== undefined && a.success !== undefined; 41 | } 42 | 43 | /** 44 | * Helper to create a successful ActionResult object. 45 | * 46 | * @param t Target for action 47 | * @return {ActionResult} with success: true 48 | */ 49 | export function successOn(t: T): ActionResult { 50 | return { 51 | success: true, 52 | target: t, 53 | }; 54 | } 55 | 56 | /** 57 | * Helper to create a failed ActionResult object. 58 | * 59 | * @param t Target for action 60 | * @param err Error that occurred. 61 | * @param f Function that failed, should have a name property. 62 | * @return {ActionResult} with success: true 63 | */ 64 | export function failureOn(t: T, err: Error, f?: any /* function */): ActionResult { 65 | return { 66 | success: false, 67 | target: t, 68 | error: err, 69 | errorStep: f && f.name ? f.name : undefined, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /lib/action/actionOps.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionResult, 3 | failureOn, 4 | isActionResult, 5 | successOn, 6 | } from "./ActionResult"; 7 | 8 | export type TOp = (p: T) => Promise; 9 | 10 | export type TAction = (t: T) => Promise>; 11 | 12 | export type Chainable = TAction | TOp; 13 | 14 | /** 15 | * Chain the actions, in the given order 16 | * @param {ProjectEditor} steps 17 | * @return {ProjectEditor} 18 | */ 19 | export function actionChain(...steps: Array>): TAction { 20 | return actionChainWithCombiner((r1, r2) => ({ 21 | ...r1, // the clojure ppl will LOVE this (I love it) 22 | ...r2, 23 | }), ...steps); 24 | } 25 | 26 | export function actionChainWithCombiner = ActionResult>( 27 | combiner: (a: R, b: R) => R, 28 | ...steps: Array>): TAction { 29 | return steps.length === 0 ? 30 | NoAction : 31 | steps.reduce((c1, c2) => { 32 | const ed1: TAction = toAction(c1); 33 | const ed2: TAction = toAction(c2); 34 | return p => ed1(p).then(r1 => { 35 | // console.log("Applied action " + c1.toString()); 36 | if (!r1.success) { return r1; } else { 37 | return ed2(r1.target).then(r2 => { 38 | // console.log("Applied action " + c2.toString()); 39 | const combinedResult: any = combiner((r1 as R), (r2 as R)); 40 | return combinedResult; 41 | }); 42 | } 43 | }); 44 | }) as TAction; // Consider adding R as a type parameter to TAction 45 | } 46 | 47 | function toAction(link: Chainable): TAction { 48 | return p => { 49 | try { 50 | const oneOrTheOther: Promise> = 51 | (link as TOp)(p); 52 | return oneOrTheOther 53 | .catch(err => failureOn(p, err, link)) 54 | .then(r => { 55 | // See what it returns 56 | return isActionResult(r) ? 57 | r : 58 | successOn(r) as ActionResult; 59 | }); 60 | } catch (error) { 61 | // console.error("Failure: " + error.message); 62 | return Promise.resolve(failureOn(p, error, link)); 63 | } 64 | }; 65 | } 66 | 67 | /** 68 | * Useful starting point for chaining 69 | */ 70 | export const NoAction: TAction = t => Promise.resolve(successOn(t)); 71 | -------------------------------------------------------------------------------- /lib/globals.ts: -------------------------------------------------------------------------------- 1 | import { AutomationClient } from "./automationClient"; 2 | import { NoOpEventStore } from "./internal/event/NoOpEventStore"; 3 | import { EventStore } from "./spi/event/EventStore"; 4 | 5 | //////////////////////////////////////////////////////// 6 | let es: EventStore; 7 | 8 | function initEventStore(): void { 9 | if (!es) { 10 | es = new NoOpEventStore(); 11 | } 12 | } 13 | 14 | /** 15 | * Globally available instance of {EventStore} to be used across the automation client. 16 | * @type {InMemoryEventStore} 17 | */ 18 | export function eventStore(): EventStore { 19 | initEventStore(); 20 | return es; 21 | } 22 | 23 | export function setEventStore(newEventStore: EventStore): void { 24 | es = newEventStore; 25 | } 26 | 27 | export function automationClientInstance(): AutomationClient { 28 | return (global as any).__runningAutomationClient; 29 | } 30 | -------------------------------------------------------------------------------- /lib/graph/ApolloGraphClientFactory.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { buildAxiosFetch } from "axios-fetch"; 3 | import * as NodeCache from "node-cache"; 4 | import { Configuration } from "../configuration"; 5 | import { configureProxy } from "../internal/util/http"; 6 | import { GraphClient } from "../spi/graph/GraphClient"; 7 | import { GraphClientFactory } from "../spi/graph/GraphClientFactory"; 8 | import { logger } from "../util/logger"; 9 | import { ApolloGraphClient } from "./ApolloGraphClient"; 10 | 11 | /** 12 | * Factory for creating GraphClient instances for incoming commands and events. 13 | * 14 | * Uses a cache to store GraphClient instances for 5 mins after which new instances will be given out. 15 | */ 16 | export class ApolloGraphClientFactory implements GraphClientFactory { 17 | 18 | private graphClients: NodeCache; 19 | 20 | public create(workspaceId: string, 21 | configuration: Configuration): GraphClient { 22 | this.init(); 23 | let graphClient = this.graphClients.get(workspaceId); 24 | if (graphClient) { 25 | return graphClient; 26 | } else { 27 | const headers = { 28 | "Authorization": `Bearer ${configuration.apiKey}`, 29 | "apollographql-client-name": `${configuration.name}/${workspaceId}`, 30 | "apollographql-client-version": configuration.version, 31 | }; 32 | graphClient = new ApolloGraphClient( 33 | `${configuration.endpoints?.graphql}/${workspaceId}`, 34 | headers, 35 | this.configure(configuration), 36 | configuration.graphql?.listeners || []); 37 | this.graphClients.set(workspaceId, graphClient); 38 | return graphClient; 39 | } 40 | logger.debug("Unable to create graph client for team '%s'", workspaceId); 41 | return null; 42 | } 43 | 44 | protected configure(configuration: Configuration): WindowOrWorkerGlobalScope["fetch"] { 45 | return buildAxiosFetch(axios.create(configureProxy({}))); 46 | } 47 | 48 | private init(): void { 49 | if (!this.graphClients) { 50 | this.graphClients = new NodeCache({ 51 | stdTTL: 1 * 60, 52 | checkperiod: 1 * 30, 53 | useClones: false, 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/internal/common/AbstractScriptedFlushable.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScriptAction, 3 | ScriptedFlushable, 4 | } from "./Flushable"; 5 | 6 | /** 7 | * Support for ScriptedFlushable operations 8 | */ 9 | export abstract class AbstractScriptedFlushable implements ScriptedFlushable { 10 | 11 | private actions: Array> = []; 12 | 13 | public recordAction(action: ScriptAction): this { 14 | this.actions.push(action); 15 | return this; 16 | } 17 | 18 | get dirty() { 19 | return this.actions.length > 0; 20 | } 21 | 22 | public flush(): Promise { 23 | // Save actions, as they may be built up again 24 | const actionsToExecute = this.actions; 25 | this.actions = []; 26 | 27 | let me: Promise = Promise.resolve(this); 28 | for (const a of actionsToExecute) { 29 | me = me.then(p => { 30 | return a(p).then(_ => p); 31 | }); 32 | } 33 | 34 | // If there were more actions built up while we went 35 | return (this.actions.length > 0) ? 36 | me.then(r => r.flush()) : 37 | me as Promise; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /lib/internal/common/Flushable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Action that can be recorded for later execution against a Flushable 3 | * in its flush() function. 4 | */ 5 | export type ScriptAction = (t: T) => Promise; 6 | 7 | /** 8 | * Interface implemented by objects that can accumulate changes 9 | * that require flushing. 10 | */ 11 | export interface Flushable { 12 | 13 | /** 14 | * Are there pending changes that need to be flushed? 15 | */ 16 | dirty: boolean; 17 | 18 | /** 19 | * Flush any pending changes. 20 | */ 21 | flush(): Promise; 22 | 23 | } 24 | 25 | /** 26 | * Interface to be implemented by Flushable objects that can accumulate a change script 27 | * and play it synchronously. 28 | */ 29 | export interface ScriptedFlushable extends Flushable { 30 | 31 | /** 32 | * Record an arbitrary action against the backing object. 33 | * @param {(p: ProjectAsync) => Promise} action 34 | */ 35 | recordAction(action: ScriptAction): this; 36 | } 37 | 38 | /** 39 | * Defer the given action until the relevant ScriptableFlushable is flushable 40 | * @param {ScriptedFlushable} flushable 41 | * @param {ScriptAction} promise 42 | */ 43 | export function defer(flushable: ScriptedFlushable, promise: Promise): void { 44 | flushable.recordAction(() => promise); 45 | } 46 | -------------------------------------------------------------------------------- /lib/internal/env/NodeConfigSecretResolver.ts: -------------------------------------------------------------------------------- 1 | import { SecretResolver } from "../../spi/env/SecretResolver"; 2 | import { logger } from "../../util/logger"; 3 | import { config } from "../util/config"; 4 | import { hideString } from "../util/string"; 5 | 6 | const AtomistPrefix = "atomist://"; 7 | 8 | /** 9 | * Local secrets: Resolve using config (resolves to /config directory). 10 | * Throw exception if not found. 11 | */ 12 | export class NodeConfigSecretResolver implements SecretResolver { 13 | 14 | public resolve(key: string): string { 15 | const resolved = key.startsWith(AtomistPrefix) ? 16 | config(key.replace(AtomistPrefix, "")) : 17 | config(key); 18 | if (!resolved) { 19 | throw new Error(`Failed to resolve '${key}'`); 20 | } else { 21 | logger.debug(`Resolved '${key}' to '${hideString(resolved)}'`); 22 | return resolved; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/internal/event/NoOpEventStore.ts: -------------------------------------------------------------------------------- 1 | import { EventStore } from "../../spi/event/EventStore"; 2 | import { 3 | CommandIncoming, 4 | EventIncoming, 5 | } from "../transport/RequestProcessor"; 6 | import { guid } from "../util/string"; 7 | 8 | export class NoOpEventStore implements EventStore { 9 | 10 | public commandSeries(): [number[], number[]] { 11 | return [[], []]; 12 | } 13 | 14 | public commands(from?: number): any[] { 15 | return []; 16 | } 17 | 18 | public eventSeries(): [number[], number[]] { 19 | return [[], []]; 20 | } 21 | 22 | public events(from?: number): any[] { 23 | return []; 24 | } 25 | 26 | public messages(from?: number): any[] { 27 | return []; 28 | } 29 | 30 | public recordCommand(command: CommandIncoming): string { 31 | return command.correlation_id ? command.correlation_id : guid(); 32 | return ""; 33 | } 34 | 35 | public recordEvent(event: EventIncoming): string { 36 | return event.extensions.correlation_id ? event.extensions.correlation_id : guid(); 37 | } 38 | 39 | public recordMessage(id: string, correlationId: string, message: any): string { 40 | return id; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/internal/invoker/Invoker.ts: -------------------------------------------------------------------------------- 1 | import { EventFired } from "../../HandleEvent"; 2 | import { HandlerContext } from "../../HandlerContext"; 3 | import { HandlerResult } from "../../HandlerResult"; 4 | import { CommandHandlerMetadata } from "../../metadata/automationMetadata"; 5 | import { CommandInvocation } from "./Payload"; 6 | 7 | export interface Invoker { 8 | 9 | /** 10 | * Validate the invocation. Does the command exist on this serve? 11 | * Are the parameters valid? 12 | * @param payload 13 | * @return metadata if the command is valid. Otherwise throw an Error 14 | */ 15 | validateCommandInvocation(payload: CommandInvocation): CommandHandlerMetadata; 16 | 17 | invokeCommand(payload: CommandInvocation, ctx: HandlerContext): Promise; 18 | 19 | onEvent(payload: EventFired, ctx: HandlerContext): Promise; 20 | } 21 | -------------------------------------------------------------------------------- /lib/internal/invoker/Payload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Context common to an event or command handler. 3 | */ 4 | import { Source } from "../transport/RequestProcessor"; 5 | 6 | export interface Contextual { 7 | 8 | /** 9 | * ID of the Atomist Workspace 10 | */ 11 | workspaceId: string; 12 | 13 | /** 14 | * Correlation Id for this invocation 15 | * Note: there can be more then one handler invocations per unique 16 | * correlation id occurring. 17 | */ 18 | correlationId: string; 19 | 20 | /** 21 | * Client internal unique Id of the current handler invocation 22 | */ 23 | invocationId?: string; 24 | 25 | /** 26 | * Source of the request, eg. Slack, Dashboard or HTTP 27 | */ 28 | source?: Source; 29 | } 30 | 31 | export interface Invocation { 32 | 33 | /** 34 | * Name of operation to invoke 35 | */ 36 | name: string; 37 | mappedParameters?: Arg[]; 38 | secrets?: Secret[]; 39 | } 40 | 41 | /** 42 | * Argument to a command handler 43 | */ 44 | export interface Arg { 45 | 46 | name: string; 47 | value: string | string[] | boolean | number; 48 | } 49 | 50 | /** 51 | * Secret to a command handler 52 | */ 53 | export interface Secret { 54 | 55 | uri: string; 56 | value: string | string[]; 57 | } 58 | 59 | /** 60 | * Command handler, editor or generator invocation 61 | */ 62 | export interface CommandInvocation extends Invocation { 63 | 64 | args: Arg[]; 65 | } 66 | -------------------------------------------------------------------------------- /lib/internal/invoker/disposable.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "../../HandlerContext"; 2 | import { logger } from "../../util/logger"; 3 | 4 | export function registerDisposable(ctx: HandlerContext): (callback: () => Promise, description?: string) => void { 5 | return (callback: () => Promise, description: string) => { 6 | if ((ctx as any).__disposables) { 7 | (ctx as any).__disposables.push({ how: callback, what: description }); 8 | } else { 9 | const disposables = [{ how: callback, what: description }]; 10 | (ctx as any).__disposables = disposables; 11 | } 12 | }; 13 | } 14 | 15 | export function dispose(ctx: HandlerContext): () => Promise { 16 | return () => { 17 | if ((ctx as any).__disposables) { 18 | function both(f1: Disposable, f2: Disposable) { 19 | return { 20 | how: () => f1.how() 21 | .then(() => f2.how() 22 | .catch(error => { 23 | logger.warn("Failed to release resource %s: %s", f2.what, error); 24 | })) 25 | , what: f1.what + " and " + f2.what, 26 | }; 27 | } 28 | 29 | const disposeEverything = (ctx as any).__disposables 30 | .reduce(both, { how: () => Promise.resolve(), what: "inconceivable" }); 31 | return disposeEverything.how() 32 | .then(result => { 33 | delete (ctx as any).__disposables; 34 | return result; 35 | }); 36 | } else { 37 | return Promise.resolve(); 38 | } 39 | }; 40 | } 41 | 42 | interface Disposable { 43 | 44 | how: () => Promise; 45 | 46 | what: string; 47 | } 48 | -------------------------------------------------------------------------------- /lib/internal/message/ConsoleMessageClient.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@atomist/slack-messages"; 2 | import { 3 | Destination, 4 | isSlackMessage, 5 | MessageClient, 6 | MessageOptions, 7 | RequiredMessageOptions, 8 | } from "../../spi/message/MessageClient"; 9 | import { 10 | DefaultSlackMessageClient, 11 | MessageClientSupport, 12 | } from "../../spi/message/MessageClientSupport"; 13 | import { logger } from "../../util/logger"; 14 | 15 | /** 16 | * Clearly display messages with channels and recipients (if DMs) on the console. 17 | */ 18 | export class ConsoleMessageClient extends MessageClientSupport implements MessageClient { 19 | 20 | public async delete(destinations: Destination | Destination[], 21 | options: RequiredMessageOptions): Promise { 22 | } 23 | 24 | protected async doSend(msg: any, 25 | destinations: Destination | Destination[], 26 | options?: MessageOptions) { 27 | let s = ""; 28 | 29 | if (isSlackMessage(msg)) { 30 | s += `@atomist: ${render(msg, true)}`; 31 | } else { 32 | s += `@atomist: ${msg}`; 33 | } 34 | 35 | logger.info(s); 36 | } 37 | } 38 | 39 | export const consoleMessageClient = new DefaultSlackMessageClient(new ConsoleMessageClient(), null); 40 | -------------------------------------------------------------------------------- /lib/internal/message/DebugMessageClient.ts: -------------------------------------------------------------------------------- 1 | import * as stringify from "json-stringify-safe"; 2 | import { 3 | Destination, 4 | MessageClient, 5 | RequiredMessageOptions, 6 | } from "../../spi/message/MessageClient"; 7 | import { MessageClientSupport } from "../../spi/message/MessageClientSupport"; 8 | import { logger } from "../../util/logger"; 9 | 10 | export class DebugMessageClient extends MessageClientSupport implements MessageClient { 11 | 12 | public async delete(destinations: Destination | Destination[], 13 | options: RequiredMessageOptions): Promise { 14 | } 15 | 16 | protected async doSend(message): Promise { 17 | logger.info(`Message\n${stringify(message, null, 2)}`); 18 | } 19 | } 20 | 21 | export const debugMessageClient = new DebugMessageClient(); 22 | -------------------------------------------------------------------------------- /lib/internal/metadata/MetadataStore.ts: -------------------------------------------------------------------------------- 1 | import { Automations } from "./metadata"; 2 | 3 | export interface MetadataStore { 4 | 5 | automations: Automations; 6 | 7 | } 8 | -------------------------------------------------------------------------------- /lib/internal/metadata/metadata.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes the automations available on an automation server. 3 | */ 4 | 5 | import { 6 | CommandHandlerMetadata, 7 | EventHandlerMetadata, 8 | } from "../../metadata/automationMetadata"; 9 | 10 | export interface Automations { 11 | 12 | name: string; 13 | version: string; 14 | policy: "ephemeral" | "durable"; 15 | team_ids: string[]; 16 | groups?: string[]; 17 | 18 | commands: CommandHandlerMetadata[]; 19 | 20 | events: EventHandlerMetadata[]; 21 | 22 | ingesters: any[]; 23 | 24 | keywords: string[]; 25 | } 26 | 27 | export function isCommandHandlerMetadata(object: any): object is CommandHandlerMetadata { 28 | return object.intent || object.mapped_parameters; 29 | } 30 | 31 | export function isEventHandlerMetadata(object: any): object is EventHandlerMetadata { 32 | return object.subscriptionName && object.subscription; 33 | } 34 | -------------------------------------------------------------------------------- /lib/internal/transport/EventStoringAutomationEventListener.ts: -------------------------------------------------------------------------------- 1 | import { eventStore } from "../../globals"; 2 | import { AutomationEventListenerSupport } from "../../server/AutomationEventListener"; 3 | import { 4 | CommandIncoming, 5 | EventIncoming, 6 | } from "./RequestProcessor"; 7 | 8 | export class EventStoringAutomationEventListener extends AutomationEventListenerSupport { 9 | 10 | public commandIncoming(payload: CommandIncoming) { 11 | eventStore().recordCommand(payload); 12 | } 13 | 14 | public eventIncoming(payload: EventIncoming) { 15 | eventStore().recordEvent(payload); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/internal/transport/MetricEnabledAutomationEventListener.ts: -------------------------------------------------------------------------------- 1 | import { EventFired } from "../../HandleEvent"; 2 | import { 3 | AutomationContextAware, 4 | HandlerContext, 5 | } from "../../HandlerContext"; 6 | import { HandlerResult } from "../../HandlerResult"; 7 | import { 8 | AutomationEventListener, 9 | AutomationEventListenerSupport, 10 | } from "../../server/AutomationEventListener"; 11 | import { CommandInvocation } from "../invoker/Payload"; 12 | import { duration } from "../util/metric"; 13 | 14 | export class MetricEnabledAutomationEventListener 15 | extends AutomationEventListenerSupport implements AutomationEventListener { 16 | 17 | public commandSuccessful(payload: CommandInvocation, ctx: HandlerContext, result: HandlerResult): Promise { 18 | const start = (ctx as any as AutomationContextAware).context.ts; 19 | duration(`command_handler.${payload.name}.success`, Date.now() - start); 20 | duration(`command_handler.global`, Date.now() - start); 21 | return Promise.resolve(); 22 | } 23 | 24 | public commandFailed(payload: CommandInvocation, ctx: HandlerContext, err: any): Promise { 25 | const start = (ctx as any as AutomationContextAware).context.ts; 26 | duration(`command_handler.${payload.name}.failure`, Date.now() - start); 27 | duration(`command_handler.global`, Date.now() - start); 28 | return Promise.resolve(); 29 | } 30 | 31 | public eventSuccessful(payload: EventFired, ctx: HandlerContext, result: HandlerResult[]): Promise { 32 | const start = (ctx as any as AutomationContextAware).context.ts; 33 | duration(`event_handler.${payload.extensions.operationName}.success`, 34 | Date.now() - start); 35 | duration(`event_handler.global`, Date.now() - start); 36 | return Promise.resolve(); 37 | } 38 | 39 | public eventFailed(payload: EventFired, ctx: HandlerContext, err: any): Promise { 40 | const start = (ctx as any as AutomationContextAware).context.ts; 41 | duration(`event_handler.${payload.extensions.operationName}.failure`, 42 | Date.now() - start); 43 | duration(`event_handler.global`, Date.now() - start); 44 | return Promise.resolve(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/internal/transport/cluster/messages.ts: -------------------------------------------------------------------------------- 1 | import * as cluster from "cluster"; 2 | import { EventFired } from "../../../HandleEvent"; 3 | import { CommandInvocation } from "../../invoker/Payload"; 4 | import { AutomationContext } from "../../util/cls"; 5 | import { RegistrationConfirmation } from "../websocket/WebSocketRequestProcessor"; 6 | 7 | export interface MasterMessage { 8 | type: "atomist:registration" | "atomist:event" | "atomist:command"; 9 | registration: RegistrationConfirmation; 10 | context: AutomationContext; 11 | data?: any; 12 | } 13 | 14 | export interface MasterManagementMessage { 15 | type: "atomist:gc" | "atomist:heapdump" | "atomist:mtrace"; 16 | } 17 | 18 | export interface WorkerMessage { 19 | type: "atomist:online" | "atomist:status" | "atomist:message" | "atomist:command_success" 20 | | "atomist:command_failure" | "atomist:event_success" | "atomist:event_failure" | "atomist:shutdown"; 21 | event?: EventFired | CommandInvocation; 22 | context: AutomationContext; 23 | data?: any; 24 | } 25 | 26 | export function broadcast(message: MasterMessage | MasterManagementMessage): void { 27 | if (cluster.isMaster) { 28 | for (const id in cluster.workers) { 29 | if (cluster.workers.hasOwnProperty(id)) { 30 | const worker = cluster.workers[id]; 31 | worker.send(message); 32 | } 33 | } 34 | } 35 | } 36 | 37 | export function workerSend(message: WorkerMessage): Promise { 38 | return Promise.resolve(process.send(message)); 39 | } 40 | -------------------------------------------------------------------------------- /lib/internal/transport/websocket/WebSocketRequestProcessor.ts: -------------------------------------------------------------------------------- 1 | import * as WebSocket from "ws"; 2 | import { RequestProcessor } from "../RequestProcessor"; 3 | 4 | export interface WebSocketRequestProcessor extends RequestProcessor { 5 | onRegistration(registration: RegistrationConfirmation); 6 | 7 | onConnect(ws: WebSocket); 8 | 9 | onDisconnect(); 10 | } 11 | 12 | export interface RegistrationConfirmation { 13 | url: string; 14 | name: string; 15 | version: string; 16 | } 17 | -------------------------------------------------------------------------------- /lib/internal/util/Deferred.ts: -------------------------------------------------------------------------------- 1 | export class Deferred { 2 | public promise: Promise; 3 | 4 | private fate: "resolved" | "unresolved"; 5 | private state: "pending" | "fulfilled" | "rejected"; 6 | 7 | // tslint:disable-next-line:ban-types 8 | private deferredResolve: Function; 9 | // tslint:disable-next-line:ban-types 10 | private deferredReject: Function; 11 | 12 | constructor() { 13 | this.state = "pending"; 14 | this.fate = "unresolved"; 15 | this.promise = new Promise((resolve, reject) => { 16 | this.deferredResolve = resolve; 17 | this.deferredReject = reject; 18 | }); 19 | this.promise.then( 20 | () => this.state = "fulfilled", 21 | () => this.state = "rejected", 22 | ); 23 | } 24 | 25 | public resolve(value?: any) { 26 | if (this.fate === "resolved") { 27 | throw new Error("Deferred cannot be resolved twice"); 28 | } 29 | this.fate = "resolved"; 30 | this.deferredResolve(value); 31 | } 32 | 33 | public reject(reason?: any) { 34 | if (this.fate === "resolved") { 35 | throw new Error("Deferred cannot be resolved twice"); 36 | } 37 | this.fate = "resolved"; 38 | this.deferredReject(reason); 39 | } 40 | 41 | public isResolved() { 42 | return this.fate === "resolved"; 43 | } 44 | 45 | public isPending() { 46 | return this.state === "pending"; 47 | } 48 | 49 | public isFulfilled() { 50 | return this.state === "fulfilled"; 51 | } 52 | 53 | public isRejected() { 54 | return this.state === "rejected"; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/internal/util/async.ts: -------------------------------------------------------------------------------- 1 | export function isPromise(a: any): a is Promise { 2 | return a && !!(a as Promise).then; 3 | } 4 | -------------------------------------------------------------------------------- /lib/internal/util/base64.ts: -------------------------------------------------------------------------------- 1 | import * as base64 from "base-64"; 2 | import * as utf8 from "utf8"; 3 | 4 | export function encode(str: string): string { 5 | const bytes = utf8.encode(str); 6 | return base64.encode(bytes); 7 | } 8 | 9 | export function decode(coded: string): string { 10 | const decoded: string | number[] = base64.decode(coded); 11 | return utf8.decode(decoded); 12 | } 13 | -------------------------------------------------------------------------------- /lib/internal/util/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Defer loading the config library until it is used. 3 | */ 4 | export function config(key: string): any | undefined { 5 | try { 6 | const c = require("config"); 7 | return c.get(key); 8 | } catch (err) { 9 | // Ignore this error 10 | } 11 | return undefined; 12 | } 13 | -------------------------------------------------------------------------------- /lib/internal/util/health.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum value expressing the state of a health check. 3 | */ 4 | export enum HealthStatus { Up = "UP", Down = "DOWN", OutOfService = "OUT_OF_SERVICE" } 5 | 6 | export interface Health

{ 7 | status: HealthStatus; 8 | detail: P; 9 | } 10 | 11 | /** 12 | * A HealthIndicator is a function that determines the Health of a sub-system or service this client 13 | * consumes. 14 | */ 15 | export type HealthIndicator = () => Health; 16 | 17 | /** 18 | * Register a HealthIndicator at startup. 19 | * @param {HealthIndicator} indicator 20 | */ 21 | export function registerHealthIndicator(indicator: HealthIndicator) { 22 | Indicators.push(indicator); 23 | } 24 | 25 | /** 26 | * Returns the combined health of the client. 27 | * @returns {Health} 28 | */ 29 | export function health(): Health { 30 | return CompositeHealthIndicator(Indicators); 31 | } 32 | 33 | // public for testing only 34 | export const Indicators: HealthIndicator[] = []; 35 | 36 | const CompositeHealthIndicator = (indicators: HealthIndicator[]) => { 37 | 38 | if (indicators.length === 1) { 39 | return indicators[0](); 40 | } else if (indicators.length === 0) { 41 | return { 42 | status: HealthStatus.Up, 43 | detail: "Service is up", 44 | }; 45 | } 46 | 47 | const status: HealthStatus = indicators.map(h => h().status).reduce((p, c) => { 48 | if (p === HealthStatus.Down) { 49 | return p; 50 | } 51 | 52 | if (p === HealthStatus.OutOfService && c === HealthStatus.Down) { 53 | return c; 54 | } 55 | 56 | if (p === HealthStatus.Up) { 57 | return c; 58 | } 59 | 60 | return p; 61 | }, HealthStatus.Up); 62 | 63 | return { 64 | status, 65 | detail: indicators.map(i => i()), 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /lib/internal/util/http.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "axios"; 2 | import * as url from "url"; 3 | 4 | export function configureProxy(config: AxiosRequestConfig): AxiosRequestConfig { 5 | if (process.env.HTTPS_PROXY || process.env.https_proxy) { 6 | const proxy = process.env.HTTPS_PROXY || process.env.https_proxy; 7 | const proxyOpts = url.parse(proxy); 8 | 9 | config.proxy = { 10 | host: proxyOpts.hostname, 11 | port: +proxyOpts.port, 12 | auth: proxyAuth(proxyOpts), 13 | }; 14 | (config.proxy as any).protocol = proxyOpts.protocol; 15 | } 16 | return config; 17 | } 18 | 19 | function proxyAuth(proxyOpts: url.UrlWithStringQuery) { 20 | if (proxyOpts.auth) { 21 | const parts = proxyOpts.auth.split(":"); 22 | if (parts.length === 2) { 23 | return { 24 | username: parts[0], 25 | password: parts[1], 26 | }; 27 | } 28 | throw new Error("Malformed Proxy authentication"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/internal/util/info.ts: -------------------------------------------------------------------------------- 1 | import * as appRoot from "app-root-path"; 2 | import * as os from "os"; 3 | import { Automations } from "../metadata/metadata"; 4 | 5 | export function info(automations: Automations): AutomationInfo { 6 | const i: AutomationInfo = { 7 | name: automations.name, 8 | version: automations.version, 9 | }; 10 | 11 | if (automations.team_ids) { 12 | i.team_ids = automations.team_ids; 13 | } 14 | if (automations.groups) { 15 | i.groups = automations.groups; 16 | } 17 | 18 | try { 19 | const pj = require(`${appRoot.path}/package.json`); 20 | i.description = pj.description; 21 | i.license = pj.license; 22 | i.author = pj.author && pj.author.name ? pj.author.name : pj.author; 23 | i.homepage = pj.homepage; 24 | } catch (err) { 25 | // Ignore missing app package.json 26 | } 27 | 28 | try { 29 | const pj = require("../../../package.json"); 30 | i.client = { 31 | name: pj.name, 32 | version: pj.version, 33 | }; 34 | } catch (err) { 35 | // Ignore the missing package.json 36 | } 37 | 38 | try { 39 | // see if we can load git-info.json from the root of the project 40 | const gi = require(`${appRoot.path}/git-info.json`); 41 | i.git = { 42 | ...gi, 43 | }; 44 | } catch (err) { 45 | // Ignore the missing git-info.json 46 | } 47 | 48 | i.system = { 49 | hostname: os.hostname(), 50 | type: os.type(), 51 | release: os.release(), 52 | platform: os.platform(), 53 | }; 54 | 55 | return i; 56 | } 57 | 58 | export interface AutomationInfo { 59 | name: string; 60 | version: string; 61 | team_ids?: string[]; 62 | groups?: string[]; 63 | description?: string; 64 | license?: string; 65 | author?: string; 66 | homepage?: string; 67 | client?: { 68 | name: string; 69 | version: string; 70 | }; 71 | git?: { 72 | sha: string; 73 | branch: string; 74 | repository: string; 75 | }; 76 | system?: { 77 | hostname: string; 78 | type: string; 79 | release: string; 80 | platform: string; 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /lib/internal/util/metric.ts: -------------------------------------------------------------------------------- 1 | import * as _metrics from "metrics"; 2 | import * as os from "os"; 3 | 4 | const report = new _metrics.Report(); 5 | 6 | export function increment(name: string) { 7 | const counter = getCounter(name); 8 | counter.inc(); 9 | report.addMetric(name, counter); 10 | } 11 | 12 | // tslint:disable-next-line:no-shadowed-variable 13 | export function duration(name: string, duration: number) { 14 | const timer = getTimer(name); 15 | timer.update(duration); 16 | report.addMetric(name, timer); 17 | } 18 | 19 | export function getCounter(name: string): _metrics.Counter { 20 | return report.getMetric(name) as _metrics.Counter || new _metrics.Counter(); 21 | } 22 | 23 | export function getTimer(name: string): _metrics.Timer { 24 | return report.getMetric(name) as _metrics.Timer || new _metrics.Timer(); 25 | } 26 | 27 | export function metrics() { 28 | const m: any = { 29 | ...report.summary(), 30 | heap: { 31 | used: process.memoryUsage().heapUsed, 32 | total: process.memoryUsage().heapTotal, 33 | rss: process.memoryUsage().rss, 34 | }, 35 | memory: { 36 | free: os.freemem(), 37 | total: os.totalmem(), 38 | }, 39 | uptime: process.uptime(), 40 | }; 41 | return m; 42 | } 43 | -------------------------------------------------------------------------------- /lib/internal/util/poll.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2019 Atomist, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /** 19 | * Run a function periodically until it returns true or until the 20 | * timeout expires. Its polling period is 1/10th the timeout. If the 21 | * timeout expires before the function returns true, the Promise will 22 | * be rejected. The function will be tried immediately and when the 23 | * total duration is reached. 24 | * 25 | * @param fn Function to call periodically 26 | * @param duration Total length of time to poll in millisends 27 | * @return Resolved Promise if function returns true within the timeout period, rejected Promise if not 28 | */ 29 | export async function poll(fn: () => boolean, duration: number = 1000): Promise { 30 | if (fn()) { 31 | return Promise.resolve(); 32 | } 33 | return new Promise((resolve, reject) => { 34 | const period = duration / 10; 35 | let interval: NodeJS.Timeout; 36 | let timeout = setTimeout(() => { 37 | if (interval) { 38 | clearInterval(interval); 39 | interval = undefined; 40 | } 41 | if (fn()) { 42 | return resolve(); 43 | } else { 44 | reject(new Error("Function did not return true in allotted time")); 45 | } 46 | }, duration); 47 | interval = setInterval(() => { 48 | if (fn()) { 49 | if (timeout) { 50 | clearTimeout(timeout); 51 | timeout = undefined; 52 | } 53 | if (interval) { 54 | clearInterval(interval); 55 | interval = undefined; 56 | } 57 | resolve(); 58 | } 59 | }, period); 60 | }); 61 | } 62 | 63 | /** 64 | * Async sleep function. 65 | * 66 | * @param ms Sleep time in milliseconds. 67 | */ 68 | export function sleep(ms: number): Promise { 69 | return new Promise(resolve => setTimeout(resolve, ms)); 70 | } 71 | -------------------------------------------------------------------------------- /lib/metadata/parameterUtils.ts: -------------------------------------------------------------------------------- 1 | import { Options } from "./automationMetadata"; 2 | 3 | export function someOf(...values: string[]): Options { 4 | return { 5 | kind: "multiple", 6 | options: values.map(value => ({ value })), 7 | }; 8 | } 9 | 10 | export function oneOf(...values: string[]): Options { 11 | return { 12 | kind: "single", 13 | options: values.map(value => ({ value })), 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /lib/operations/CommandDetails.ts: -------------------------------------------------------------------------------- 1 | import { RepoFinder } from "./common/repoFinder"; 2 | import { RepoLoader } from "./common/repoLoader"; 3 | 4 | /** 5 | * Details common to commands created via functions 6 | */ 7 | export interface CommandDetails { 8 | 9 | description: string; 10 | intent?: string | string[] | RegExp; 11 | tags?: string | string[]; 12 | autoSubmit?: boolean; 13 | 14 | repoFinder?: RepoFinder; 15 | 16 | repoLoader?: (p: PARAMS) => RepoLoader; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /lib/operations/common/BasicAuthCredentials.ts: -------------------------------------------------------------------------------- 1 | import { ProjectOperationCredentials } from "./ProjectOperationCredentials"; 2 | 3 | export interface BasicAuthCredentials extends ProjectOperationCredentials { 4 | 5 | username: string; 6 | 7 | password: string; 8 | } 9 | 10 | export function isBasicAuthCredentials(o: any): o is BasicAuthCredentials { 11 | const c = o as BasicAuthCredentials; 12 | return !!c && !!c.username && !!c.password; 13 | } 14 | -------------------------------------------------------------------------------- /lib/operations/common/GitlabPrivateTokenCredentials.ts: -------------------------------------------------------------------------------- 1 | import { ProjectOperationCredentials } from "./ProjectOperationCredentials"; 2 | 3 | /** 4 | * Credentials that uses Gitlab private tokens 5 | */ 6 | export interface GitlabPrivateTokenCredentials extends ProjectOperationCredentials { 7 | privateToken: string; 8 | } 9 | 10 | export function isGitlabPrivateTokenCredentials(poc: ProjectOperationCredentials): poc is GitlabPrivateTokenCredentials { 11 | const q = poc as GitlabPrivateTokenCredentials; 12 | return q.privateToken !== undefined; 13 | } 14 | -------------------------------------------------------------------------------- /lib/operations/common/ProjectOperationCredentials.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Tag interface for credentials for working with projects 3 | */ 4 | // tslint:disable-next-line 5 | export interface ProjectOperationCredentials { 6 | 7 | } 8 | 9 | export interface TokenCredentials extends ProjectOperationCredentials { 10 | 11 | token: string; 12 | } 13 | 14 | export function isTokenCredentials(poc: ProjectOperationCredentials): poc is TokenCredentials { 15 | const q = poc as TokenCredentials; 16 | return q.token !== undefined; 17 | } 18 | -------------------------------------------------------------------------------- /lib/operations/common/SourceLocation.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Identifies a location within a project. 4 | * Used in project reviewers 5 | */ 6 | export interface SourceLocation { 7 | 8 | readonly path: string; 9 | 10 | readonly lineFrom1?: number; 11 | 12 | readonly columnFrom1?: number; 13 | 14 | readonly offset: number; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /lib/operations/common/defaultRepoLoader.ts: -------------------------------------------------------------------------------- 1 | import { DefaultDirectoryManager } from "../../project/git/GitCommandGitProject"; 2 | import { GitProject } from "../../project/git/GitProject"; 3 | import { DirectoryManager } from "../../spi/clone/DirectoryManager"; 4 | import { gitHubRepoLoader } from "./gitHubRepoLoader"; 5 | import { LocalRepoLoader } from "./localRepoLoader"; 6 | import { ProjectOperationCredentials } from "./ProjectOperationCredentials"; 7 | import { 8 | isLocalRepoRef, 9 | RepoRef, 10 | } from "./RepoId"; 11 | import { RepoLoader } from "./repoLoader"; 12 | 13 | /** 14 | * Materialize from github 15 | * @param credentials provider token 16 | * @return function to materialize repos 17 | * @constructor 18 | */ 19 | export function defaultRepoLoader(credentials: ProjectOperationCredentials, 20 | directoryManager: DirectoryManager = DefaultDirectoryManager): RepoLoader { 21 | return (repoId: RepoRef) => { 22 | if (isLocalRepoRef(repoId)) { 23 | return LocalRepoLoader(repoId) as Promise; 24 | } else { 25 | return gitHubRepoLoader(credentials, directoryManager)(repoId); 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /lib/operations/common/fromProjectList.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "../../project/Project"; 2 | import { RepoFinder } from "./repoFinder"; 3 | import { RepoRef } from "./RepoId"; 4 | import { RepoLoader } from "./repoLoader"; 5 | 6 | export function fromListRepoFinder(projects: Project[]): RepoFinder { 7 | if (projects.some(p => !p.id)) { 8 | throw new Error("Not all projects have id"); 9 | } 10 | return () => Promise.resolve(projects.map(p => p.id)); 11 | } 12 | 13 | export function fromListRepoLoader(projects: Project[]): RepoLoader { 14 | if (projects.some(p => !p.id)) { 15 | throw new Error("Not all projects have id"); 16 | } 17 | return (id: RepoRef) => Promise.resolve(projects.find(p => p.id === id)); 18 | } 19 | -------------------------------------------------------------------------------- /lib/operations/common/gitHubRepoLoader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultDirectoryManager, 3 | GitCommandGitProject, 4 | } from "../../project/git/GitCommandGitProject"; 5 | import { GitProject } from "../../project/git/GitProject"; 6 | import { 7 | DefaultCloneOptions, 8 | DirectoryManager, 9 | } from "../../spi/clone/DirectoryManager"; 10 | import { GitHubRepoRef } from "./GitHubRepoRef"; 11 | import { ProjectOperationCredentials } from "./ProjectOperationCredentials"; 12 | import { isRemoteRepoRef } from "./RepoId"; 13 | import { RepoLoader } from "./repoLoader"; 14 | 15 | /** 16 | * Materialize from github 17 | * @param credentials provider token 18 | * @param directoryManager strategy for handling local storage 19 | * @return function to materialize repos 20 | */ 21 | export function gitHubRepoLoader(credentials: ProjectOperationCredentials, 22 | directoryManager: DirectoryManager = DefaultDirectoryManager): RepoLoader { 23 | return repoId => { 24 | // Default it if it isn't already a GitHub repo ref 25 | const gid = isRemoteRepoRef(repoId) ? repoId : new GitHubRepoRef(repoId.owner, repoId.repo, repoId.sha); 26 | return GitCommandGitProject.cloned(credentials, gid, DefaultCloneOptions, directoryManager); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /lib/operations/common/gitlabRepoLoader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DefaultDirectoryManager, 3 | GitCommandGitProject, 4 | } from "../../project/git/GitCommandGitProject"; 5 | import { GitProject } from "../../project/git/GitProject"; 6 | import { 7 | DefaultCloneOptions, 8 | DirectoryManager, 9 | } from "../../spi/clone/DirectoryManager"; 10 | import { GitlabRepoRef } from "./GitlabRepoRef"; 11 | import { ProjectOperationCredentials } from "./ProjectOperationCredentials"; 12 | import { isRemoteRepoRef } from "./RepoId"; 13 | import { RepoLoader } from "./repoLoader"; 14 | 15 | /** 16 | * Materialize from gitlab 17 | * @param credentials provider token 18 | * @param directoryManager strategy for handling local storage 19 | * @return function to materialize repos 20 | */ 21 | export function gitlabRepoLoader(credentials: ProjectOperationCredentials, 22 | directoryManager: DirectoryManager = DefaultDirectoryManager): RepoLoader { 23 | return repoId => { 24 | // Default it if it isn't already a Gitlab repo ref 25 | const gid = isRemoteRepoRef(repoId) ? repoId : GitlabRepoRef.from({ 26 | owner: repoId.owner, 27 | repo: repoId.repo, 28 | sha: repoId.sha, 29 | branch: repoId.branch, 30 | }); 31 | return GitCommandGitProject.cloned(credentials, gid, DefaultCloneOptions, directoryManager); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /lib/operations/common/localRepoLoader.ts: -------------------------------------------------------------------------------- 1 | import stringify = require("json-stringify-safe"); 2 | import { NodeFsLocalProject } from "../../project/local/NodeFsLocalProject"; 3 | import { 4 | isLocalRepoRef, 5 | RepoRef, 6 | } from "./RepoId"; 7 | import { RepoLoader } from "./repoLoader"; 8 | 9 | export const LocalRepoLoader: RepoLoader = 10 | (repoId: RepoRef) => { 11 | if (isLocalRepoRef(repoId)) { 12 | // Find it from the file system 13 | return NodeFsLocalProject.fromExistingDirectory(repoId, repoId.baseDir); 14 | } else { 15 | throw Promise.reject(`Not a local RepoId: [${stringify(repoId)}]`); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/operations/common/params/AlwaysAskRepoParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Parameter, 3 | Parameters, 4 | } from "../../../decorators"; 5 | import { GitHubTargetsParams } from "./GitHubTargetsParams"; 6 | import { 7 | GitBranchRegExp, 8 | GitHubNameRegExp, 9 | GitShaRegExp, 10 | } from "./validationPatterns"; 11 | 12 | /** 13 | * Basic editor params. Always ask for a repo. 14 | * Allow regex. 15 | */ 16 | @Parameters() 17 | export class AlwaysAskRepoParameters extends GitHubTargetsParams { 18 | 19 | @Parameter({ description: "Name of owner to edit repo in", ...GitHubNameRegExp, required: true }) 20 | public owner: string; 21 | 22 | @Parameter({ description: "Name of repo to edit or regex", pattern: /.+/, required: true }) 23 | public repo: string; 24 | 25 | @Parameter({ description: "Ref", ...GitShaRegExp, required: false }) 26 | public sha: string; 27 | 28 | @Parameter({ description: "Branch Defaults to 'master'", ...GitBranchRegExp, required: false }) 29 | public branch: string = "master"; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /lib/operations/common/params/BaseEditorOrReviewerParameters.ts: -------------------------------------------------------------------------------- 1 | import { Parameters } from "../../../decorators"; 2 | import { MappedRepoParameters } from "./MappedRepoParameters"; 3 | import { TargetsParams } from "./TargetsParams"; 4 | 5 | /** 6 | * Contract for all editor or reviewer parameters 7 | */ 8 | export interface EditorOrReviewerParameters { 9 | 10 | /** 11 | * Describe target repos 12 | */ 13 | targets: TargetsParams; 14 | } 15 | 16 | /** 17 | * Superclass for all editor or reviewer parameters 18 | */ 19 | @Parameters() 20 | export class BaseEditorOrReviewerParameters implements EditorOrReviewerParameters { 21 | 22 | constructor(public targets: TargetsParams = new MappedRepoParameters()) {} 23 | } 24 | -------------------------------------------------------------------------------- /lib/operations/common/params/Credentialed.ts: -------------------------------------------------------------------------------- 1 | import { ProjectOperationCredentials } from "../ProjectOperationCredentials"; 2 | 3 | /** 4 | * Implemented by parameters that carry ProjectOperationCredentials. 5 | * Hides whether they are tokens etc. 6 | */ 7 | export interface Credentialed { 8 | 9 | credentials: ProjectOperationCredentials; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /lib/operations/common/params/EditOneOrAllParameters.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | 3 | import { Parameters } from "../../../decorators"; 4 | import { SmartParameters } from "../../../SmartParameters"; 5 | import { BaseEditorOrReviewerParameters } from "./BaseEditorOrReviewerParameters"; 6 | import { GitHubFallbackReposParameters } from "./GitHubFallbackReposParameters"; 7 | 8 | /** 9 | * Editor parameters that apply to a single GitHub repo mapped to a Slack channel, 10 | * or otherwise use the targets.repos regex. 11 | */ 12 | @Parameters() 13 | export class EditOneOrAllParameters extends BaseEditorOrReviewerParameters implements SmartParameters { 14 | 15 | constructor() { 16 | super(new GitHubFallbackReposParameters()); 17 | } 18 | 19 | public bindAndValidate() { 20 | const targets = this.targets as GitHubFallbackReposParameters; 21 | if (!targets.repo) { 22 | assert(!!targets.repos, "Must set repos or repo"); 23 | targets.repo = targets.repos; 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /lib/operations/common/params/FallbackParams.ts: -------------------------------------------------------------------------------- 1 | import { TargetsParams } from "./TargetsParams"; 2 | 3 | /** 4 | * Resolve from a Mapped parameter or from a supplied repos regex if no repo mapping 5 | */ 6 | export type FallbackParams = TargetsParams & { repos: string }; 7 | -------------------------------------------------------------------------------- /lib/operations/common/params/GitHubFallbackReposParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | Parameter, 5 | } from "../../../decorators"; 6 | import { FallbackParams } from "./FallbackParams"; 7 | import { GitHubTargetsParams } from "./GitHubTargetsParams"; 8 | import { 9 | GitBranchRegExp, 10 | GitShaRegExp, 11 | } from "./validationPatterns"; 12 | 13 | /** 14 | * Resolve from a Mapped parameter or from a supplied repos regex if no repo mapping 15 | */ 16 | export class GitHubFallbackReposParameters extends GitHubTargetsParams implements FallbackParams { 17 | 18 | @MappedParameter(MappedParameters.GitHubOwner, false) 19 | public owner: string; 20 | 21 | @MappedParameter(MappedParameters.GitHubRepository, false) 22 | public repo: string; 23 | 24 | @Parameter({ description: "Ref", ...GitShaRegExp, required: false }) 25 | public sha: string; 26 | 27 | @Parameter({ description: "Branch Defaults to 'master'", ...GitBranchRegExp, required: false }) 28 | public branch: string = "master"; 29 | 30 | @Parameter({ description: "regex", required: false }) 31 | public repos: string = ".*"; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lib/operations/common/params/GitHubSourceRepoParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | } from "../../../decorators"; 5 | import { GitHubRepoRef } from "../GitHubRepoRef"; 6 | import { SourceRepoParameters } from "./SourceRepoParameters"; 7 | 8 | export class GitHubSourceRepoParameters extends SourceRepoParameters { 9 | 10 | @MappedParameter(MappedParameters.GitHubApiUrl, false) 11 | public apiUrl: string; 12 | 13 | get repoRef() { 14 | return (!!this.owner && !!this.repo) ? 15 | new GitHubRepoRef(this.owner, this.repo, this.sha, this.apiUrl) : 16 | undefined; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lib/operations/common/params/GitHubTargetsParams.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | Parameters, 5 | Secret, 6 | Secrets, 7 | } from "../../../decorators"; 8 | import { GitHubRepoRef } from "../GitHubRepoRef"; 9 | import { ProjectOperationCredentials } from "../ProjectOperationCredentials"; 10 | import { TargetsParams } from "./TargetsParams"; 11 | 12 | /** 13 | * Base parameters for working with GitHub repo(s). 14 | * Allows use of regex. 15 | */ 16 | @Parameters() 17 | export abstract class GitHubTargetsParams extends TargetsParams { 18 | 19 | @MappedParameter(MappedParameters.GitHubApiUrl, false) 20 | public apiUrl: string; 21 | 22 | get credentials(): ProjectOperationCredentials { 23 | if (!!this.githubToken) { 24 | return { token: this.githubToken }; 25 | } else { 26 | return undefined; 27 | } 28 | } 29 | 30 | /** 31 | * Return a single RepoRef or undefined if we're not identifying a single repo 32 | * @return {RepoRef} 33 | */ 34 | get repoRef(): GitHubRepoRef { 35 | if (!this.owner || !this.repo || this.usesRegex) { 36 | return undefined; 37 | } 38 | return GitHubRepoRef.from({ 39 | owner: this.owner, 40 | repo: this.repo, 41 | sha: this.sha, 42 | branch: this.branch, 43 | rawApiBase: this.apiUrl, 44 | }); 45 | } 46 | 47 | @Secret(Secrets.userToken(["repo", "user:email", "read:user"])) 48 | private readonly githubToken: string; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /lib/operations/common/params/MappedRepoParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | Parameter, 5 | Parameters, 6 | } from "../../../decorators"; 7 | import { GitHubTargetsParams } from "./GitHubTargetsParams"; 8 | import { 9 | GitBranchRegExp, 10 | GitShaRegExp, 11 | } from "./validationPatterns"; 12 | 13 | /** 14 | * Get target from channel mapping 15 | */ 16 | @Parameters() 17 | export class MappedRepoParameters extends GitHubTargetsParams { 18 | 19 | @MappedParameter(MappedParameters.GitHubOwner) 20 | public owner: string; 21 | 22 | @MappedParameter(MappedParameters.GitHubRepository) 23 | public repo: string; 24 | 25 | @Parameter({ description: "Ref", ...GitShaRegExp, required: false }) 26 | public sha: string; 27 | 28 | @Parameter({ description: "Branch Defaults to 'master'", ...GitBranchRegExp, required: false }) 29 | public branch: string = "master"; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /lib/operations/common/params/RemoteLocator.ts: -------------------------------------------------------------------------------- 1 | import { RemoteRepoRef } from "../RepoId"; 2 | 3 | /** 4 | * Implemented by classes that know how to identify a remote repo 5 | */ 6 | export interface RemoteLocator { 7 | 8 | /** 9 | * Return a single RepoRef or undefined if it's not possible 10 | * @return {RepoRef} 11 | */ 12 | repoRef: RemoteRepoRef; 13 | } 14 | -------------------------------------------------------------------------------- /lib/operations/common/params/SourceRepoParameters.ts: -------------------------------------------------------------------------------- 1 | import { Parameter } from "../../../decorators"; 2 | import { 3 | RemoteRepoRef, 4 | RepoRef, 5 | } from "../RepoId"; 6 | import { RemoteLocator } from "./RemoteLocator"; 7 | import { 8 | GitBranchRegExp, 9 | GitHubNameRegExp, 10 | } from "./validationPatterns"; 11 | 12 | /** 13 | * Parameters common to anything that works with a single source repo, 14 | * such as a seed driven generator 15 | */ 16 | export abstract class SourceRepoParameters implements RemoteLocator { 17 | 18 | @Parameter({ 19 | pattern: GitHubNameRegExp.pattern, 20 | displayName: "Seed Repository Owner", 21 | description: "owner, i.e., user or organization, of seed repository", 22 | validInput: GitHubNameRegExp.validInput, 23 | minLength: 1, 24 | maxLength: 100, 25 | required: false, 26 | displayable: true, 27 | }) 28 | public owner: string; 29 | 30 | @Parameter({ 31 | pattern: GitHubNameRegExp.pattern, 32 | displayName: "Seed Repository Name", 33 | description: "name of the seed repository", 34 | validInput: GitHubNameRegExp.validInput, 35 | minLength: 1, 36 | maxLength: 100, 37 | required: false, 38 | displayable: true, 39 | }) 40 | public repo: string; 41 | 42 | @Parameter({ 43 | pattern: GitBranchRegExp.pattern, 44 | displayName: "Seed Branch", 45 | description: "seed repository branch to clone for new project", 46 | validInput: GitBranchRegExp.validInput, 47 | minLength: 1, 48 | maxLength: 256, 49 | required: false, 50 | displayable: true, 51 | }) 52 | public sha: string = "master"; 53 | 54 | /** 55 | * Return a single RepoRef 56 | * This implementation returns a GitHub.com repo but it can be overridden 57 | * to return any kind of repo 58 | * @return {RepoRef} 59 | */ 60 | public abstract repoRef: RemoteRepoRef; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /lib/operations/common/params/TargetsParams.ts: -------------------------------------------------------------------------------- 1 | import { ProjectOperationCredentials } from "../ProjectOperationCredentials"; 2 | import { RepoFilter } from "../repoFilter"; 3 | import { 4 | RemoteRepoRef, 5 | RepoRef, 6 | } from "../RepoId"; 7 | import { Credentialed } from "./Credentialed"; 8 | import { RemoteLocator } from "./RemoteLocator"; 9 | import { GitHubNameRegExp } from "./validationPatterns"; 10 | 11 | /** 12 | * Base parameters for working with repo(s). 13 | * Allows use of regex. 14 | */ 15 | export abstract class TargetsParams implements Credentialed, RemoteLocator { 16 | 17 | public abstract owner: string; 18 | 19 | /** 20 | * Repo name. May be a repo name or a string containing a regular expression. 21 | */ 22 | public abstract repo: string; 23 | 24 | public abstract sha: string; 25 | 26 | public abstract branch: string; 27 | 28 | public abstract credentials: ProjectOperationCredentials; 29 | 30 | get usesRegex(): boolean { 31 | return !!this.repo && (!GitHubNameRegExp.pattern.test(this.repo) || !this.owner); 32 | } 33 | 34 | public abstract repoRef: RemoteRepoRef; 35 | 36 | /** 37 | * If we're not tied to a single repo ref, test this RepoRef 38 | * @param {RepoRef} rr 39 | * @return {boolean} 40 | */ 41 | public test: RepoFilter = rr => { 42 | if (this.repoRef) { 43 | const my = this.repoRef; 44 | return my.owner === rr.owner && my.repo === rr.repo; 45 | } 46 | if (this.usesRegex) { 47 | if (this.owner && this.owner !== rr.owner) { 48 | return false; 49 | } 50 | return new RegExp(this.repo).test(rr.repo); 51 | } 52 | return false; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /lib/operations/common/params/validationPatterns.ts: -------------------------------------------------------------------------------- 1 | export const GitHubNameRegExp = { 2 | pattern: /^[-.\w]+$/, 3 | validInput: "a valid GitHub identifier which consists of alphanumeric, ., -, and _ characters", 4 | }; 5 | 6 | export const GitBranchRegExp = { 7 | // not perfect, but pretty good 8 | pattern: /^\w(?:[./]?[-\w])*$/, 9 | validInput: "a valid Git branch name, see" + 10 | " https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html", 11 | }; 12 | 13 | export const GitShaRegExp = { 14 | pattern: /^[0-9a-f]{40}$/, 15 | validInput: "40 hex digits, lowercase", 16 | }; 17 | -------------------------------------------------------------------------------- /lib/operations/common/projectAction.ts: -------------------------------------------------------------------------------- 1 | import { ActionResult } from "../../action/ActionResult"; 2 | import { Project } from "../../project/Project"; 3 | 4 | /** 5 | * Action on a project, given parameters 6 | */ 7 | export type ProjectAction = 8 | (p: P, params: PARAMS) => Promise>; 9 | -------------------------------------------------------------------------------- /lib/operations/common/repoFilter.ts: -------------------------------------------------------------------------------- 1 | import { RepoId } from "../common/RepoId"; 2 | 3 | /** 4 | * Determine whether the given repo is eligible 5 | */ 6 | export type RepoFilter = (r: RepoId) => boolean | Promise; 7 | 8 | export const AllRepos: RepoFilter = r => true; 9 | 10 | export function andFilter(af: RepoFilter, bf: RepoFilter = () => true): RepoFilter { 11 | return r => Promise.resolve(af(r)) 12 | .then(a => Promise.resolve(bf(r)) 13 | .then(b => a && b)); 14 | } 15 | -------------------------------------------------------------------------------- /lib/operations/common/repoFinder.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "../../HandlerContext"; 2 | import { RepoRef } from "./RepoId"; 3 | 4 | /** 5 | * A function that knows how to find RepoIds from a context 6 | */ 7 | export type RepoFinder = (context: HandlerContext) => Promise; 8 | -------------------------------------------------------------------------------- /lib/operations/common/repoLoader.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "../../project/Project"; 2 | import { RepoRef } from "./RepoId"; 3 | 4 | /** 5 | * A function that knows how to materialize a repo, whether 6 | * by cloning or other means 7 | */ 8 | export type RepoLoader

= (repoId: RepoRef) => Promise

; 9 | -------------------------------------------------------------------------------- /lib/operations/common/repoUtils.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "../../HandlerContext"; 2 | import { Project } from "../../project/Project"; 3 | import { logger } from "../../util/logger"; 4 | import { executeAll } from "../../util/pool"; 5 | import { defaultRepoLoader } from "./defaultRepoLoader"; 6 | import { ProjectOperationCredentials } from "./ProjectOperationCredentials"; 7 | import { 8 | AllRepos, 9 | RepoFilter, 10 | } from "./repoFilter"; 11 | import { RepoFinder } from "./repoFinder"; 12 | import { RepoRef } from "./RepoId"; 13 | import { RepoLoader } from "./repoLoader"; 14 | 15 | /** 16 | * Perform an action against all the given repos. 17 | * Skip over repos that cannot be loaded, logging a warning. 18 | * @param {HandlerContext} ctx 19 | * @param credentials credentials for repo finding and loading 20 | * @param action action parameter 21 | * @param parameters optional parameters 22 | * @param {RepoFinder} repoFinder 23 | * @param {} repoFilter 24 | * @param {RepoLoader} repoLoader 25 | * @return {Promise} 26 | */ 27 | export async function doWithAllRepos(ctx: HandlerContext, 28 | credentials: ProjectOperationCredentials, 29 | action: (p: Project, t: P) => Promise, 30 | parameters: P, 31 | repoFinder: RepoFinder, 32 | repoFilter: RepoFilter = AllRepos, 33 | repoLoader: RepoLoader = 34 | defaultRepoLoader(credentials)): Promise { 35 | const ids = await relevantRepos(ctx, repoFinder, repoFilter); 36 | const promises = ids.map(id => 37 | () => repoLoader(id) 38 | .catch(err => { 39 | logger.debug("Unable to load repo %s/%s: %s", id.owner, id.repo, err); 40 | logger.debug(err.stack); 41 | return undefined; 42 | }) 43 | .then(p => { 44 | if (p) { 45 | return action(p, parameters); 46 | } 47 | })); 48 | 49 | return (await executeAll(promises)).filter(result => result); 50 | } 51 | 52 | export function relevantRepos(ctx: HandlerContext, 53 | repoFinder: RepoFinder, 54 | repoFilter: RepoFilter = AllRepos): Promise { 55 | return repoFinder(ctx) 56 | .then(rids => 57 | Promise.all(rids.map(rid => Promise.resolve(repoFilter(rid)) 58 | .then(relevant => relevant ? rid : undefined)))) 59 | .then(many => many.filter(s => s !== undefined)); 60 | } 61 | -------------------------------------------------------------------------------- /lib/operations/edit/projectEditor.ts: -------------------------------------------------------------------------------- 1 | import { ActionResult } from "../../action/ActionResult"; 2 | import { HandlerContext } from "../../HandlerContext"; 3 | import { 4 | isProject, 5 | Project, 6 | } from "../../project/Project"; 7 | import { EditMode } from "./editModes"; 8 | 9 | /** 10 | * Modifies the given project, returning information about the modification. 11 | * @param p project to edit 12 | * @param context context for the current command or event handler 13 | * @param params params, if available 14 | */ 15 | export type ProjectEditor

= 16 | (p: Project, context?: HandlerContext, params?: P) => Promise; 17 | 18 | export type SimpleProjectEditor

= 19 | (p: Project, context?: HandlerContext, params?: P) => Promise; 20 | 21 | export type AnyProjectEditor

= ProjectEditor

| SimpleProjectEditor

; 22 | 23 | /** 24 | * Result of editing a project. More information may be added by instances. 25 | */ 26 | export interface EditResult

extends ActionResult

{ 27 | 28 | /** 29 | * Whether or not this project was edited. 30 | * Undefined if we don't know, as not all editors keep track of their doings. 31 | */ 32 | readonly edited?: boolean; 33 | 34 | /** 35 | * Populated only if editing was successful 36 | */ 37 | readonly editMode?: EditMode; 38 | } 39 | 40 | export function toEditor

(ed: (SimpleProjectEditor

| ProjectEditor

)): ProjectEditor

{ 41 | return (proj, ctx, params) => 42 | (ed as any)(proj, ctx, params) 43 | .then(r => 44 | // See what it returns 45 | isProject(r) ? 46 | successfulEdit(r, undefined) : 47 | r as EditResult) 48 | .catch(err => failedEdit(proj, err)); 49 | } 50 | 51 | export function successfulEdit

(p: P, edited: boolean): EditResult

{ 52 | return { 53 | target: p, 54 | success: true, 55 | edited, 56 | }; 57 | } 58 | 59 | export function failedEdit

(p: P, error: Error, edited: boolean = false): EditResult

{ 60 | return { 61 | target: p, 62 | success: false, 63 | error, 64 | edited, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /lib/operations/edit/projectEditorOps.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "../../project/Project"; 2 | import { logger } from "../../util/logger"; 3 | import { 4 | AnyProjectEditor, 5 | EditResult, 6 | ProjectEditor, 7 | successfulEdit, 8 | toEditor, 9 | } from "./projectEditor"; 10 | 11 | /** 12 | * Chain the editors, in the given order 13 | * @param {ProjectEditor} projectEditors 14 | * @return {ProjectEditor} 15 | */ 16 | export function chainEditors(...projectEditors: AnyProjectEditor[]): ProjectEditor { 17 | const asProjectEditors = projectEditors.map(toEditor); 18 | return async (p, ctx, params) => { 19 | try { 20 | let cumulativeResult: EditResult = { 21 | target: p, 22 | success: true, 23 | edited: false, 24 | }; 25 | for (const pe of asProjectEditors) { 26 | const lastResult = await pe(p, ctx, params); 27 | cumulativeResult = combineEditResults(lastResult, cumulativeResult); 28 | } 29 | return cumulativeResult; 30 | } catch (error) { 31 | logger.warn("Editor failure in editorChain: %s", error); 32 | return {target: p, edited: false, success: false, error}; 33 | } 34 | }; 35 | } 36 | 37 | export function combineEditResults(r1: EditResult, r2: EditResult): EditResult { 38 | return { 39 | ...r1, 40 | ...r2, 41 | edited: (r1.edited || r2.edited) ? true : 42 | (r1.edited === false && r2.edited === false) ? false : undefined, 43 | success: r1.success && r2.success, 44 | }; 45 | } 46 | 47 | /** 48 | * Useful starting point for editor chaining 49 | * @param {Project} p 50 | * @constructor 51 | */ 52 | export const NoOpEditor: ProjectEditor = p => Promise.resolve(successfulEdit(p, false)); 53 | -------------------------------------------------------------------------------- /lib/operations/generate/BaseSeedDrivenGeneratorParameters.ts: -------------------------------------------------------------------------------- 1 | import { Parameters } from "../../decorators"; 2 | import { GitHubSourceRepoParameters } from "../common/params/GitHubSourceRepoParameters"; 3 | import { SourceRepoParameters } from "../common/params/SourceRepoParameters"; 4 | import { GitHubRepoCreationParameters } from "./GitHubRepoCreationParameters"; 5 | import { RepoCreationParameters } from "./RepoCreationParameters"; 6 | import { SeedDrivenGeneratorParameters } from "./SeedDrivenGeneratorParameters"; 7 | 8 | /** 9 | * Default parameters needed to create a new repo from a seed. 10 | * Defaults to use GitHub.com, but subclasses can override the source and target parameters. 11 | */ 12 | @Parameters() 13 | export class BaseSeedDrivenGeneratorParameters implements SeedDrivenGeneratorParameters { 14 | 15 | /** 16 | * Subclasses can override this for non GitHub target strategies. 17 | * @param {SourceRepoParameters} source 18 | * @param {NewRepoCreationParameters} target 19 | */ 20 | constructor(public source: SourceRepoParameters = new GitHubSourceRepoParameters(), 21 | public target: RepoCreationParameters = new GitHubRepoCreationParameters()) {} 22 | 23 | } 24 | -------------------------------------------------------------------------------- /lib/operations/generate/GitHubRepoCreationParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | Secret, 5 | Secrets, 6 | } from "../../decorators"; 7 | import { GitHubRepoRef } from "../common/GitHubRepoRef"; 8 | import { ProjectOperationCredentials } from "../common/ProjectOperationCredentials"; 9 | import { RemoteRepoRef } from "../common/RepoId"; 10 | import { NewRepoCreationParameters } from "./NewRepoCreationParameters"; 11 | 12 | /** 13 | * Parameters common to all generators that create new repositories 14 | */ 15 | export class GitHubRepoCreationParameters extends NewRepoCreationParameters { 16 | 17 | @Secret(Secrets.userToken(["repo", "user:email", "read:user"])) 18 | public githubToken; 19 | 20 | @MappedParameter(MappedParameters.GitHubApiUrl, false) 21 | public apiUrl: string; 22 | 23 | get credentials(): ProjectOperationCredentials { 24 | return { token: this.githubToken }; 25 | } 26 | 27 | /** 28 | * Return a single RepoRef or undefined if we're not identifying a single repo 29 | * This implementation returns a GitHub.com repo but it can be overriden 30 | * to return any kind of repo 31 | * @return {RepoRef} 32 | */ 33 | get repoRef(): RemoteRepoRef { 34 | return (!!this.owner && !!this.repo) ? 35 | new GitHubRepoRef(this.owner, this.repo, "master", this.apiUrl) : 36 | undefined; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lib/operations/generate/GitlabRepoCreationParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | } from "../../decorators"; 5 | import { GitlabPrivateTokenCredentials } from "../common/GitlabPrivateTokenCredentials"; 6 | import { 7 | GitlabRepoRef, 8 | } from "../common/GitlabRepoRef"; 9 | import { ProjectOperationCredentials } from "../common/ProjectOperationCredentials"; 10 | import { RemoteRepoRef } from "../common/RepoId"; 11 | import { NewRepoCreationParameters } from "./NewRepoCreationParameters"; 12 | 13 | /** 14 | * Parameters common to all generators that create new repositories on Gitlab 15 | */ 16 | export class GitlabRepoCreationParameters extends NewRepoCreationParameters { 17 | 18 | public token: string; 19 | 20 | @MappedParameter(MappedParameters.GitHubApiUrl) 21 | public apiUrl: string; 22 | 23 | @MappedParameter(MappedParameters.GitHubUrl) 24 | public baseRemoteUrl: string; 25 | 26 | get credentials(): ProjectOperationCredentials { 27 | return { privateToken: this.token } as GitlabPrivateTokenCredentials; 28 | } 29 | 30 | /** 31 | * Return a single RepoRef or undefined if we're not identifying a single repo 32 | * This implementation returns a Gitlab.com repo but it can be overriden 33 | * to return any kind of repo 34 | * @return {RepoRef} 35 | */ 36 | get repoRef(): RemoteRepoRef { 37 | return (!!this.owner && !!this.repo) ? 38 | GitlabRepoRef.from({ 39 | owner: this.owner, 40 | repo: this.repo, 41 | branch: "master", 42 | rawApiBase: this.apiUrl, 43 | gitlabRemoteUrl: this. baseRemoteUrl, 44 | }) : undefined; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/operations/generate/NewRepoCreationParameters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | Parameter, 5 | } from "../../decorators"; 6 | import { GitHubNameRegExp } from "../common/params/validationPatterns"; 7 | import { ProjectOperationCredentials } from "../common/ProjectOperationCredentials"; 8 | import { RemoteRepoRef } from "../common/RepoId"; 9 | import { RepoCreationParameters } from "./RepoCreationParameters"; 10 | 11 | /** 12 | * Parameters common to all generators that create new repositories 13 | */ 14 | export abstract class NewRepoCreationParameters implements RepoCreationParameters { 15 | 16 | @MappedParameter(MappedParameters.GitHubOwner) 17 | public owner: string; 18 | 19 | @Parameter({ 20 | pattern: GitHubNameRegExp.pattern, 21 | displayName: "New Repository Name", 22 | description: "name of the new repository", 23 | validInput: GitHubNameRegExp.validInput, 24 | minLength: 1, 25 | maxLength: 100, 26 | required: true, 27 | order: 1, 28 | }) 29 | public repo: string; 30 | 31 | @Parameter({ 32 | displayName: "Project Description", 33 | description: "short descriptive text describing the new project", 34 | validInput: "free text", 35 | minLength: 1, 36 | maxLength: 100, 37 | required: false, 38 | }) 39 | public description: string = "my new project"; 40 | 41 | @Parameter({ 42 | displayName: "Repository Visibility", 43 | description: "visibility of the new repository (public or private; defaults to public)", 44 | pattern: /^(public|private)$/, 45 | validInput: "public or private", 46 | minLength: 6, 47 | maxLength: 7, 48 | required: false, 49 | }) 50 | public visibility: "public" | "private" = "public"; 51 | 52 | public abstract credentials: ProjectOperationCredentials; 53 | 54 | /** 55 | * Return a single RepoRef or undefined if we're not identifying a single repo 56 | * This implementation returns a GitHub.com repo but it can be overriden 57 | * to return any kind of repo 58 | * @return {RepoRef} 59 | */ 60 | public abstract repoRef: RemoteRepoRef; 61 | 62 | } 63 | -------------------------------------------------------------------------------- /lib/operations/generate/RepoCreationParameters.ts: -------------------------------------------------------------------------------- 1 | import { Credentialed } from "../common/params/Credentialed"; 2 | import { RemoteLocator } from "../common/params/RemoteLocator"; 3 | 4 | /** 5 | * Parameters common to all generators that create new repositories 6 | */ 7 | export interface RepoCreationParameters extends Credentialed, RemoteLocator { 8 | 9 | description: string; 10 | 11 | visibility: "public" | "private"; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /lib/operations/generate/SeedDrivenGeneratorParameters.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import { RemoteLocator } from "../common/params/RemoteLocator"; 19 | import { RepoCreationParameters } from "./RepoCreationParameters"; 20 | 21 | /** 22 | * The parameters needed to create a new repo from a seed. 23 | */ 24 | export interface SeedDrivenGeneratorParameters { 25 | 26 | source: RemoteLocator; 27 | 28 | target: RepoCreationParameters; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /lib/operations/generate/gitHubProjectPersister.ts: -------------------------------------------------------------------------------- 1 | import { GitProject } from "../../project/git/GitProject"; 2 | import { ProjectPersister } from "./generatorUtils"; 3 | import { RemoteGitProjectPersister } from "./remoteGitProjectPersister"; 4 | 5 | /** 6 | * Kept only for backward compatibility: Use RemoteGitProjectPersister 7 | */ 8 | export const GitHubProjectPersister: ProjectPersister = 9 | RemoteGitProjectPersister; 10 | -------------------------------------------------------------------------------- /lib/operations/review/issueRaisingReviewRouter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | failureOn, 3 | successOn, 4 | } from "../../action/ActionResult"; 5 | import { 6 | deepLink, 7 | Issue, 8 | raiseIssue, 9 | } from "../../util/gitHub"; 10 | import { 11 | GitHubRepoRef, 12 | isGitHubRepoRef, 13 | } from "../common/GitHubRepoRef"; 14 | import { EditorOrReviewerParameters } from "../common/params/BaseEditorOrReviewerParameters"; 15 | import { TokenCredentials } from "../common/ProjectOperationCredentials"; 16 | import { ReviewRouter } from "./reviewerToCommand"; 17 | import { 18 | ProjectReview, 19 | ReviewComment, 20 | } from "./ReviewResult"; 21 | 22 | /** 23 | * Create an issue from a review, using Markdown 24 | * @param {ProjectReview} pr 25 | * @param {AllReposByDefaultParameters} params 26 | * @param {string} name 27 | * @return {any} 28 | */ 29 | export const issueRaisingReviewRouter: ReviewRouter = 30 | (pr: ProjectReview, params: EditorOrReviewerParameters, name: string) => { 31 | if (isGitHubRepoRef(pr.repoId)) { 32 | const issue = toIssue(pr, name); 33 | return raiseIssue((params.targets.credentials as TokenCredentials).token, pr.repoId, issue) 34 | .then(ap => successOn(pr.repoId)); 35 | } else { 36 | return Promise.resolve(failureOn(pr.repoId, new Error(`Not a GitHub Repo: ${JSON.stringify(pr.repoId)}`))); 37 | } 38 | }; 39 | 40 | function toIssue(pr: ProjectReview, name: string): Issue { 41 | return { 42 | title: `${pr.comments.length} problems found by ${name}`, 43 | body: "Problems:\n\n" + pr.comments.map(c => 44 | toMarkdown(pr.repoId as GitHubRepoRef, c)).join("\n"), 45 | }; 46 | } 47 | 48 | function toMarkdown(grr: GitHubRepoRef, rc: ReviewComment) { 49 | return `-\t**${rc.severity}** - ${rc.category}: [${rc.detail}](${deepLink(grr, rc.sourceLocation)})`; 50 | } 51 | -------------------------------------------------------------------------------- /lib/operations/review/projectReviewer.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "../../HandlerContext"; 2 | import { Project } from "../../project/Project"; 3 | import { ProjectReview } from "./ReviewResult"; 4 | 5 | /** 6 | * Function that can review projects. 7 | * @param p project to review 8 | * @param context context for the current command or event handler 9 | * @param params params, if available 10 | */ 11 | export type ProjectReviewer

= 12 | (p: Project, context: HandlerContext, params?: P) => Promise; 13 | -------------------------------------------------------------------------------- /lib/operations/support/contextUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HandlerResult, 3 | Success, 4 | } from "../../HandlerResult"; 5 | 6 | /** 7 | * Can throw this into the end of a handler chain to return a HandlerResult 8 | * @param whatever 9 | * @return {Promise} 10 | */ 11 | export function succeed(whatever: any): Promise { 12 | return Promise.resolve(Success); 13 | } 14 | -------------------------------------------------------------------------------- /lib/operations/tagger/Tagger.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { ActionResult } from "../../action/ActionResult"; 3 | import { HandlerContext } from "../../HandlerContext"; 4 | import { Project } from "../../project/Project"; 5 | import { EditorOrReviewerParameters } from "../common/params/BaseEditorOrReviewerParameters"; 6 | import { RepoRef } from "../common/RepoId"; 7 | 8 | export interface TaggerTags { 9 | 10 | repoId: RepoRef; 11 | 12 | tags: string[]; 13 | } 14 | 15 | export class DefaultTaggerTags implements TaggerTags { 16 | 17 | constructor(public repoId: RepoRef, public tags: string[]) { 18 | } 19 | } 20 | 21 | export type Tagger

= 22 | (p: Project, context: HandlerContext, params?: P) => Promise; 23 | 24 | export type TagRouter = 25 | (tags: TaggerTags, params: PARAMS, ctx: HandlerContext) => Promise>; 26 | 27 | /** 28 | * Combine these taggers 29 | * @param t0 first tagger 30 | * @param {Tagger} taggers 31 | * @return {Tagger} 32 | */ 33 | export function unifiedTagger(t0: Tagger, ...taggers: Tagger[]): Tagger { 34 | return (p, context, params) => { 35 | const allTags = Promise.all(([t0].concat(taggers)).map(t => t(p, context, params))); 36 | return allTags.then(tags => { 37 | return unify(tags); 38 | }); 39 | }; 40 | } 41 | 42 | function unify(tags: TaggerTags[]): TaggerTags { 43 | const uniqueTags = _.uniq(_.flatMap(tags, t => t.tags)); 44 | return new DefaultTaggerTags(tags[0].repoId, uniqueTags); 45 | } 46 | -------------------------------------------------------------------------------- /lib/project/File.ts: -------------------------------------------------------------------------------- 1 | import { ScriptedFlushable } from "../internal/common/Flushable"; 2 | import { HasCache } from "./HasCache"; 3 | 4 | /** 5 | * Operations common to all File interfaces 6 | */ 7 | export interface FileCore extends HasCache { 8 | /** 9 | * Return file name, excluding path 10 | * 11 | * @property {string} name 12 | */ 13 | readonly name: string; 14 | 15 | /** 16 | * Return file path, with forward slashes 17 | * 18 | * @property {string} path 19 | */ 20 | readonly path: string; 21 | } 22 | 23 | /** 24 | * Convenient way to defer File operations with fluent API 25 | */ 26 | export interface FileScripting extends ScriptedFlushable {} 27 | 28 | export interface FileAsync extends FileCore { 29 | setContent(content: string): Promise; 30 | 31 | rename(name: string): Promise; 32 | 33 | getContent(encoding?: string): Promise; 34 | 35 | getContentBuffer(): Promise; 36 | 37 | replace(re: RegExp, replacement: string): Promise; 38 | 39 | replaceAll(oldLiteral: string, newLiteral: string): Promise; 40 | 41 | setPath(path: string): Promise; 42 | 43 | isExecutable(): Promise; 44 | 45 | isReadable(): Promise; 46 | 47 | isBinary(): Promise; 48 | } 49 | 50 | export interface FileNonBlocking extends FileScripting, FileAsync {} 51 | 52 | /** 53 | * Sychronous file operations. Use with care as they can limit concurrency. 54 | * Following the conventions of node fs library, they use a "sync" suffix. 55 | */ 56 | export interface FileSync extends FileCore { 57 | /** 58 | * Return content. Blocks: use inputStream by preference. 59 | * 60 | * @property {string} content 61 | */ 62 | getContentSync(encoding?: string): string; 63 | 64 | setContentSync(content: string): this; 65 | } 66 | 67 | /** 68 | * Abstraction for a File. Similar to Project abstraction, 69 | * broken into three distinct styles of usage. 70 | */ 71 | export interface File extends FileScripting, FileSync, FileAsync { 72 | /** 73 | * Extension or the empty string if no extension can be determined 74 | */ 75 | extension: string; 76 | } 77 | 78 | export function isFile(a: any): a is File { 79 | const maybeF = a as File; 80 | return !!maybeF.name && !!maybeF.path && !!maybeF.getContentSync; 81 | } 82 | -------------------------------------------------------------------------------- /lib/project/HasCache.ts: -------------------------------------------------------------------------------- 1 | export interface HasCache { 2 | 3 | /** 4 | * Use to cache arbitrary content associated with this instance. 5 | * Use for smallish objects that are expensive to compute. 6 | */ 7 | readonly cache: Record; 8 | } 9 | 10 | /** 11 | * Retrieve the value if stored in the cache. Otherwise compute with the given function 12 | * and store 13 | */ 14 | export async function retrieveOrCompute(t: T, 15 | key: string, 16 | how: (t: T) => R, 17 | cache: boolean = true): Promise { 18 | if (!cache) { 19 | return how(t); 20 | } 21 | if (!t.cache[key]) { 22 | t.cache[key] = how(t); 23 | } 24 | return t.cache[key] as R; 25 | } 26 | -------------------------------------------------------------------------------- /lib/project/diff/Action.ts: -------------------------------------------------------------------------------- 1 | import { Changes } from "./Changes"; 2 | 3 | /** 4 | * Performs some side effect based on the compared fingerprints and diff 5 | * @param fingerprint format 6 | * @param diff format 7 | */ 8 | export interface Action { 9 | invoke(base: F, head: F, diff: D): void; 10 | } 11 | -------------------------------------------------------------------------------- /lib/project/diff/Chain.ts: -------------------------------------------------------------------------------- 1 | import { Action } from "./Action"; 2 | import { Changes } from "./Changes"; 3 | import { Differ } from "./Differ"; 4 | import { Extractor } from "./Extractor"; 5 | 6 | /** 7 | * Chain an Extractor, Differ, and Actions together so that this common flow can be expressed in a type safe way 8 | * @param fingerprint format 9 | * @param diff format 10 | */ 11 | export interface Chain { 12 | 13 | extractor: Extractor; 14 | 15 | differ: Differ; 16 | 17 | actions: Array>; 18 | } 19 | -------------------------------------------------------------------------------- /lib/project/diff/Changes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Behavior that is common to all diffs 3 | */ 4 | export interface Changes { 5 | /** 6 | * indication that the diff detected an actual change 7 | */ 8 | hasChanged(): boolean; 9 | } 10 | -------------------------------------------------------------------------------- /lib/project/diff/Differ.ts: -------------------------------------------------------------------------------- 1 | import { Changes } from "./Changes"; 2 | 3 | /** 4 | * Diffs two fingerprints 5 | * @param fingerprint format 6 | * @param diff format 7 | */ 8 | export interface Differ { 9 | diff(base: F, head: F): D; 10 | } 11 | -------------------------------------------------------------------------------- /lib/project/diff/DifferenceEngine.ts: -------------------------------------------------------------------------------- 1 | import { GitHubRepoRef } from "../../operations/common/GitHubRepoRef"; 2 | import { RepoRef } from "../../operations/common/RepoId"; 3 | import { GitCommandGitProject } from "../git/GitCommandGitProject"; 4 | import { GitProject } from "../git/GitProject"; 5 | import { Chain } from "./Chain"; 6 | 7 | /** 8 | * Extracts fingerprints, diffs them, and invokes actions on Github shas that are being compared 9 | */ 10 | export class DifferenceEngine { 11 | 12 | constructor(private readonly githubIssueAuth: GithubIssueAuth, private readonly chains: Array>) { 13 | } 14 | 15 | /** 16 | * Run configured diff chains for these shas 17 | * @param baseSha 18 | * @param headSha 19 | */ 20 | public async run(baseSha: string, headSha: string): Promise { 21 | const project = await this.cloneRepo(this.githubIssueAuth, baseSha); 22 | const baseFps = await Promise.all(this.chains.map(c => c.extractor.extract(project))); 23 | await project.checkout(headSha); 24 | const headFps = await Promise.all(this.chains.map(c => c.extractor.extract(project))); 25 | const diffs = this.chains.map((c, i) => c.differ.diff(baseFps[i], headFps[i])); 26 | diffs.map((d, i) => this.chains[i].actions.forEach(a => a.invoke(baseFps[i], headFps[i], d))); 27 | } 28 | 29 | private cloneRepo(githubIssueAuth: GithubIssueAuth, sha: string): Promise { 30 | return GitCommandGitProject.cloned( 31 | { token: githubIssueAuth.githubToken }, 32 | new GitHubRepoRef(githubIssueAuth.owner, githubIssueAuth.repo, githubIssueAuth.sha)); 33 | } 34 | } 35 | 36 | /** 37 | * Details that allow a GitHub issue to be referenced and modified 38 | */ 39 | export interface GithubIssueAuth extends RepoRef { 40 | githubToken: string; 41 | issueNumber: number; 42 | } 43 | -------------------------------------------------------------------------------- /lib/project/diff/Extractor.ts: -------------------------------------------------------------------------------- 1 | import { ProjectAsync } from "../Project"; 2 | 3 | /** 4 | * Extracts the fingerprint from the project 5 | * @param fingerprint format 6 | */ 7 | 8 | export interface Extractor { 9 | extract(project: ProjectAsync): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /lib/project/fileGlobs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Glob pattern to match all files in a project. Standard glob syntax. 3 | */ 4 | export const AllFiles = "**/**"; 5 | 6 | /** 7 | * Negative glob to exclude .git directory 8 | */ 9 | export const ExcludeGit = "!.git/**"; 10 | 11 | /** 12 | * Negative glob to exclude node_modules directory. We nearly always want to exclude 13 | * this when handling node projects, for performance reasons. 14 | */ 15 | export const ExcludeNodeModules = "!**/node_modules/**"; 16 | 17 | export const ExcludeTarget = "!target/**"; 18 | 19 | /** 20 | * Default exclusions (git and node modules). 21 | * Must be combined with a positive glob. 22 | */ 23 | export const DefaultExcludes = [ExcludeGit, ExcludeNodeModules, ExcludeTarget]; 24 | 25 | /** 26 | * Include all files except with default exclusions (git and node modules) 27 | */ 28 | export const DefaultFiles = [AllFiles].concat(DefaultExcludes); 29 | -------------------------------------------------------------------------------- /lib/project/fingerprint/Fingerprint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Fingerprint (aka FingerprintData) represents some property of the code in a repository 3 | * as of a particular commit. 4 | * 5 | * It might be the set of dependencies, or the count of tests, or a set of contributors. 6 | * 7 | * The data here represents a particular fingerprint value. The name, version, and abbreviation 8 | * identify the fingerprinted property; data and sha represent the value of that property. 9 | * This will be associated with a commit on a repository. 10 | */ 11 | export interface Fingerprint { 12 | 13 | /** 14 | * Name of the fingerprint. This should be a constant. 15 | */ 16 | name: string; 17 | 18 | /** 19 | * Version of the fingerprinting function. If you update your fingerprinting algorithm, 20 | * increment this constant. 21 | */ 22 | version: string; 23 | 24 | /** 25 | * A shorter name. This should be a constant. 26 | */ 27 | abbreviation: string; 28 | 29 | /** 30 | * Full data of the fingerprint: whatever text identifies the property you're representing. 31 | * 32 | * This might be a stringified map of dependency to version, for instance. 33 | */ 34 | data: string; 35 | 36 | /** 37 | * A short string that identifies the data uniquely. Used for fast comparison 38 | */ 39 | sha: string; 40 | } 41 | -------------------------------------------------------------------------------- /lib/project/git/Configurable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /** Object that allows configuring Git user name and email. */ 19 | export interface Configurable { 20 | 21 | /** 22 | * Sets the given user and email as the running git commands 23 | * @param {string} user 24 | * @param {string} email 25 | */ 26 | setUserConfig(user: string, email: string): Promise; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /lib/project/local/LocalFile.ts: -------------------------------------------------------------------------------- 1 | import { File } from "../File"; 2 | 3 | /** 4 | * Implementation of File interface backed by local file system 5 | */ 6 | export interface LocalFile extends File { 7 | 8 | /** 9 | * Real, operating system dependent, path to the file. 10 | */ 11 | realPath: string; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /lib/project/local/LocalProject.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "../Project"; 2 | 3 | /** 4 | * Implementation of LocalProject based on node file system. 5 | * Uses fs-extra vs raw fs. 6 | */ 7 | export type ReleaseFunction = () => Promise; 8 | 9 | export function isLocalProject(p: Project): p is LocalProject { 10 | return (p as any).baseDir !== undefined; 11 | } 12 | 13 | /** 14 | * Implementation of Project backed by local file system 15 | */ 16 | export interface LocalProject extends Project { 17 | 18 | readonly baseDir: string; 19 | 20 | /** 21 | * Release any locks held 22 | */ 23 | release: ReleaseFunction; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /lib/project/local/NodeFsLocalFile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra"; 2 | import { isBinaryFile } from "isbinaryfile"; 3 | import * as nodePath from "path"; 4 | import { logger } from "../../util/logger"; 5 | import { AbstractFile } from "../support/AbstractFile"; 6 | import { LocalFile } from "./LocalFile"; 7 | 8 | /** 9 | * Implementation of File interface on node file system 10 | */ 11 | export class NodeFsLocalFile extends AbstractFile implements LocalFile { 12 | 13 | constructor(public readonly baseDir: string, public path: string) { 14 | super(); 15 | if (path.startsWith(nodePath.sep)) { 16 | this.path = path.substr(1); 17 | } 18 | } 19 | 20 | get realPath(): string { 21 | return realPath(this.baseDir, this.path); 22 | } 23 | 24 | public getContentSync(encoding: string = "utf8"): string { 25 | return fs.readFileSync(this.realPath).toString(encoding); 26 | } 27 | 28 | public getContent(encoding: string = "utf8"): Promise { 29 | return this.getContentBuffer() 30 | .then(buf => buf.toString(encoding)); 31 | } 32 | 33 | public getContentBuffer(): Promise { 34 | return fs.readFile(this.realPath); 35 | } 36 | 37 | public setContent(content: string): Promise { 38 | return fs.writeFile(this.realPath, content) 39 | .then(() => this.invalidateCache()); 40 | } 41 | 42 | public setContentSync(content: string): this { 43 | fs.writeFileSync(this.realPath, content); 44 | this.invalidateCache(); 45 | return this; 46 | } 47 | 48 | public setPath(path: string): Promise { 49 | if (path !== this.path) { 50 | logger.debug(`setPath: from ${this.path} to ${path}: Unlinking ${this.realPath}`); 51 | const oldPath = this.realPath; 52 | this.path = path; 53 | return fs.move(oldPath, this.realPath).then(_ => this); 54 | } 55 | return Promise.resolve(this); 56 | } 57 | 58 | public isExecutable(): Promise { 59 | return fs.access(this.realPath, fs.constants.X_OK).then(() => true).catch(_ => false); 60 | } 61 | 62 | public isReadable(): Promise { 63 | return fs.access(this.realPath, fs.constants.R_OK).then(() => true).catch(_ => false); 64 | } 65 | 66 | public isBinary(): Promise { 67 | return isBinaryFile(this.realPath); 68 | } 69 | } 70 | 71 | function realPath(baseDir: string, path: string): string { 72 | return baseDir + (path.startsWith("/") ? "" : "/") + path; 73 | } 74 | -------------------------------------------------------------------------------- /lib/project/mem/InMemoryFile.ts: -------------------------------------------------------------------------------- 1 | import { AbstractFile } from "../support/AbstractFile"; 2 | 3 | /** 4 | * In memory File implementation. Useful in testing 5 | * and to back quasi-synchronous operations. Do not use 6 | * for very large files. 7 | */ 8 | export class InMemoryFile extends AbstractFile { 9 | 10 | private readonly initialPath: string; 11 | private readonly initialContent: string; 12 | 13 | constructor(public path: string, public content: string) { 14 | super(); 15 | this.initialPath = path; 16 | this.initialContent = content; 17 | } 18 | 19 | public getContentSync(encoding?: string): string { 20 | return this.content; 21 | } 22 | 23 | public setContentSync(content: string): this { 24 | this.content = content; 25 | this.invalidateCache(); 26 | return this; 27 | } 28 | 29 | public setContent(content: string): Promise { 30 | return Promise.resolve(this.setContentSync(content)); 31 | } 32 | 33 | public getContent(encoding?: string): Promise { 34 | return Promise.resolve(this.getContentSync(encoding)); 35 | } 36 | 37 | public getContentBuffer(): Promise { 38 | return Promise.resolve(Buffer.from(this.getContentSync(), "utf8")); 39 | } 40 | 41 | public setPath(path: string): Promise { 42 | this.path = path; 43 | return Promise.resolve(this); 44 | } 45 | 46 | get dirty(): boolean { 47 | return this.initialContent !== this.getContentSync() || this.initialPath !== this.path || super.dirty; 48 | } 49 | 50 | public isExecutable(): Promise { 51 | throw new Error("isExecutable is not implemented here"); 52 | } 53 | 54 | public isReadable(): Promise { 55 | throw new Error("isReadable is not implemented here"); 56 | } 57 | 58 | public isBinary(): Promise { 59 | // InMemoryFile does not presently support binary files 60 | return Promise.resolve(false); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/project/support/AbstractFile.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { AbstractScriptedFlushable } from "../../internal/common/AbstractScriptedFlushable"; 3 | import { File } from "../File"; 4 | 5 | /** 6 | * Convenient support for all File implementations 7 | */ 8 | export abstract class AbstractFile extends AbstractScriptedFlushable implements File { 9 | 10 | public abstract path: string; 11 | 12 | public readonly cache: Record = {}; 13 | 14 | get name(): string { 15 | return this.path.split("/").pop(); 16 | } 17 | 18 | get extension(): string { 19 | return this.name.includes(".") ? 20 | this.name.split(".").pop() : 21 | ""; 22 | } 23 | 24 | public abstract getContentSync(): string; 25 | 26 | public abstract setContentSync(content: string): this; 27 | 28 | public abstract getContent(encoding?: string): Promise; 29 | 30 | public abstract getContentBuffer(): Promise; 31 | 32 | public abstract setContent(content: string): Promise; 33 | 34 | public rename(name: string): Promise { 35 | return this.setPath(this.path.replace(new RegExp(`${this.name}$`), name)); 36 | } 37 | 38 | public abstract setPath(path: string): Promise; 39 | 40 | public replace(re: RegExp, replacement: string): Promise { 41 | return this.getContent() 42 | .then(content => 43 | this.setContent(content.replace(re, replacement)), 44 | ); 45 | } 46 | 47 | public replaceAll(oldLiteral: string, newLiteral: string): Promise { 48 | return this.getContent() 49 | .then(content => 50 | this.setContent(content.split(oldLiteral).join(newLiteral)), 51 | ); 52 | } 53 | 54 | public abstract isExecutable(): Promise; 55 | 56 | public abstract isReadable(): Promise; 57 | 58 | public abstract isBinary(): Promise; 59 | 60 | protected invalidateCache(): this { 61 | _.keys(this.cache).forEach(k => delete this.cache[k]); 62 | return this; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/project/util/diagnosticUtils.ts: -------------------------------------------------------------------------------- 1 | import { File } from "../File"; 2 | import { AllFiles } from "../fileGlobs"; 3 | 4 | import { Project } from "../Project"; 5 | import { gatherFromFiles } from "./projectUtils"; 6 | 7 | const Separator = "-------------------"; 8 | 9 | /** 10 | * Use as a diagnostic step in an editor chain. 11 | * No op that dumps file information to the console. 12 | * @param stepName identification of the step in the process we're up to 13 | * @param globPattern optional glob pattern to select files. Match all if not supplied 14 | * @param stringifier function to convert files to strings. Default uses path 15 | */ 16 | export function diagnosticDump(stepName: string, 17 | globPattern: string = AllFiles, 18 | stringifier: (f: File) => string = f => f.path): (project: Project) => Promise { 19 | return project => gatherFromFiles(project, globPattern, async f => f) 20 | .then(files => 21 | // tslint:disable-next-line:no-console 22 | console.log(`${Separator}\nProject name ${project.name}: Step=${stepName}; Files[${globPattern}]=\n` + 23 | `${files.map(f => "\t" + stringifier(f)).join("\n")}\n${Separator}`)) 24 | .then(() => project); 25 | } 26 | -------------------------------------------------------------------------------- /lib/project/util/jsonUtils.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../../util/logger"; 2 | import { ProjectAsync } from "../Project"; 3 | import { doWithFiles } from "./projectUtils"; 4 | 5 | export type JsonManipulation = (jsonObj: M) => void; 6 | 7 | /** 8 | * Manipulate the contents of the given JSON file within the project, 9 | * using its object form and writing back using the same formatting. 10 | * See the manipulate function. 11 | * @param {P} p 12 | * @param {string} jsonPath JSON file path. This function will do nothing 13 | * without error if the file is ill-formed or not found. 14 | * @param {JsonManipulation} manipulation 15 | * @return {Promise

} 16 | */ 17 | export function doWithJson( 18 | p: P, 19 | jsonPath: string, 20 | manipulation: JsonManipulation, 21 | ): Promise

{ 22 | return doWithFiles(p, jsonPath, async file => { 23 | const content = await file.getContent(); 24 | await file.setContent(manipulate(content, manipulation, jsonPath)); 25 | }); 26 | } 27 | 28 | const spacePossibilities = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, " ", " ", "\t"]; 29 | 30 | /** 31 | * Update the object form of the given JSON content and write 32 | * it back with minimal changes 33 | * @param {string} jsonIn 34 | * @param {(jsonObj: any) => Object} manipulation 35 | * @return {string} 36 | */ 37 | export function manipulate(jsonIn: string, manipulation: JsonManipulation, context: string = ""): string { 38 | if (!jsonIn) { 39 | return jsonIn; 40 | } 41 | 42 | try { 43 | const newline = jsonIn.endsWith("\n"); // does this work on Windows? 44 | const jsonToCompare = newline ? jsonIn.replace(/\n$/, "") : jsonIn; 45 | 46 | const obj = JSON.parse(jsonIn); 47 | 48 | let space: number | string = 2; 49 | for (const sp of spacePossibilities) { 50 | const maybe = JSON.stringify(obj, undefined, sp); 51 | if (jsonToCompare === maybe) { 52 | logger.debug(`Definitely inferred space as [${sp}]`); 53 | space = sp; 54 | break; 55 | } 56 | } 57 | 58 | logger.debug(`Inferred space is [${space}]`); 59 | 60 | manipulation(obj); 61 | return JSON.stringify(obj, undefined, space) + (newline ? "\n" : ""); 62 | } catch (e) { 63 | logger.warn("Syntax error parsing supposed JSON (%s). Context:[%s]. Alleged JSON:\n%s", e, context, jsonIn); 64 | return jsonIn; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/project/util/projectInvariants.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "../Project"; 2 | 3 | /** 4 | * Pass through validating something. Used to assert invariants in editors. 5 | * Reject if invariant isn't satisfied. 6 | * @param {Project} p 7 | * @param {string} path 8 | * @param assertion to satisfy invariant 9 | * @param err custom error message, if supplied 10 | * @return {Promise} 11 | */ 12 | export function assertContent(p: Project, path: string, 13 | assertion: (content: string) => boolean, err?: string): Promise { 14 | return p.findFile(path) 15 | .then(f => f.getContent() 16 | .then(content => 17 | assertion(content) ? 18 | Promise.resolve(p) : 19 | Promise.reject( 20 | err ? err : `Assertion failed about project ${p.name}: ${assertion}`), 21 | ), 22 | ); 23 | } 24 | 25 | export function assertContentIncludes(p: Project, path: string, what: string): Promise { 26 | return assertContent(p, path, content => content.includes(what), 27 | `File at [${path}] does not contain [${what}]`); 28 | } 29 | 30 | export function assertFileExists(p: Project, path: string): Promise { 31 | return assertContent(p, path, content => true, 32 | `File at [${path}] does not exist`); 33 | } 34 | -------------------------------------------------------------------------------- /lib/project/util/sourceLocationUtils.ts: -------------------------------------------------------------------------------- 1 | import { SourceLocation } from "../../operations/common/SourceLocation"; 2 | 3 | import { 4 | File, 5 | isFile, 6 | } from "../../project/File"; 7 | 8 | /** 9 | * Find the given source location within this project 10 | * @param {string} f file info: Path or File 11 | * @param {string} content 12 | * @param {number} offset 13 | * @return {SourceLocation} 14 | */ 15 | export function toSourceLocation(f: string | File, content: string, offset: number): SourceLocation { 16 | if (!content || offset < 0 || offset > content.length - 1) { 17 | return undefined; 18 | } 19 | 20 | const lines = content.substr(0, offset) 21 | .split("\n"); 22 | return { 23 | path: isFile(f) ? f.path : f, 24 | lineFrom1: lines.length, 25 | columnFrom1: lines[lines.length - 1].length + 1, 26 | offset, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /lib/schema/schema.cortex.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | // This file was automatically generated and should not be edited. 3 | /* tslint:enable */ 4 | -------------------------------------------------------------------------------- /lib/schema/schema.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | // This file was automatically generated and should not be edited. 3 | 4 | export type ReposQueryVariables = { 5 | teamId: string, 6 | offset: number, 7 | }; 8 | 9 | export type ReposQuery = { 10 | ChatTeam: Array<{ 11 | __typename: "undefined", 12 | // ChatTeam orgs Org 13 | orgs: Array<{ 14 | __typename: string, 15 | // Org repo Repo 16 | repo: Array<{ 17 | __typename: string, 18 | // owner of Repo 19 | owner: string | null, 20 | // name of Repo 21 | name: string | null, 22 | } | null> | null, 23 | } | null> | null, 24 | } | null> | null, 25 | }; 26 | /* tslint:enable */ 27 | -------------------------------------------------------------------------------- /lib/server/AutomationServer.ts: -------------------------------------------------------------------------------- 1 | import { Invoker } from "../internal/invoker/Invoker"; 2 | import { MetadataStore } from "../internal/metadata/MetadataStore"; 3 | 4 | export interface AutomationServer extends MetadataStore, Invoker { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /lib/spi/env/MetadataProcessor.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../../configuration"; 2 | import { AutomationMetadata } from "../../metadata/automationMetadata"; 3 | 4 | /** 5 | * Extension for consumers to process the handlers metadata before it gets registered 6 | * with the Atomist API. 7 | * Note: This can be useful to re-write secrets used on command and event handlers to 8 | * be sourced from local configuration values. In that case one would remove an existing 9 | * secret value from the metadata and add a new value to it. 10 | */ 11 | export interface AutomationMetadataProcessor { 12 | process(metadata: T, configuration: Configuration): T; 13 | } 14 | 15 | /** 16 | * Default AutomationMetadataProcessor that just passes through the given metadata instance. 17 | */ 18 | export class PassThroughMetadataProcessor implements AutomationMetadataProcessor { 19 | 20 | public process(metadata: T, configuration: Configuration): T { 21 | return metadata; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/spi/env/SecretResolver.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Resolve the given secret. 4 | */ 5 | export interface SecretResolver { 6 | 7 | resolve(key: string): string; 8 | } 9 | -------------------------------------------------------------------------------- /lib/spi/event/EventStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CommandIncoming, 3 | EventIncoming, 4 | } from "../../internal/transport/RequestProcessor"; 5 | 6 | /** 7 | * Implementations of {EventStore} can be used to store and retrieve automation node releated events. 8 | */ 9 | export interface EventStore { 10 | 11 | recordEvent(event: EventIncoming): string; 12 | 13 | recordCommand(command: CommandIncoming): string; 14 | 15 | recordMessage(id: string, correlationId: string, message: any): string; 16 | 17 | events(from?: number): any[]; 18 | 19 | eventSeries(): [number[], number[]]; 20 | 21 | commands(from?: number): any[]; 22 | 23 | commandSeries(): [number[], number[]]; 24 | 25 | messages(from?: number): any[]; 26 | } 27 | -------------------------------------------------------------------------------- /lib/spi/graph/GraphClientFactory.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../../configuration"; 2 | import { GraphClient } from "./GraphClient"; 3 | 4 | /** 5 | * Factory to create GraphClient instances 6 | */ 7 | export interface GraphClientFactory { 8 | 9 | /** 10 | * Create a GraphClient for the provided workspaceId 11 | * @param workspaceId 12 | * @param configuration 13 | */ 14 | create(workspaceId: string, 15 | configuration: Configuration): GraphClient; 16 | 17 | } 18 | 19 | /** 20 | * Default GraphClientFactory to use 21 | */ 22 | export const defaultGraphClientFactory = () => new LazyApolloGraphClientFactory(); 23 | 24 | /** 25 | * Lazy wrapper around the ApolloGraphClientFactory to prevent eager loading 26 | */ 27 | class LazyApolloGraphClientFactory implements GraphClientFactory { 28 | 29 | private factory: GraphClientFactory; 30 | 31 | public create(workspaceId: string, configuration: Configuration): GraphClient { 32 | if (!this.factory) { 33 | const agcf = require("../../graph/ApolloGraphClientFactory"); 34 | this.factory = new agcf.ApolloGraphClientFactory(); 35 | } 36 | return this.factory.create(workspaceId, configuration); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/spi/http/axiosHttpClient.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line:import-blacklist 2 | import axios, { AxiosRequestConfig } from "axios"; 3 | import { configureProxy } from "../../internal/util/http"; 4 | import { doWithRetry } from "../../util/retry"; 5 | import { 6 | DefaultHttpClientOptions, 7 | HttpClient, 8 | HttpClientFactory, 9 | HttpClientOptions, 10 | HttpResponse, 11 | } from "./httpClient"; 12 | 13 | /** 14 | * Axios based HttpClient implementation. 15 | */ 16 | export class AxiosHttpClient implements HttpClient { 17 | 18 | public exchange(url: string, options: HttpClientOptions = {}): Promise> { 19 | 20 | const optionsToUse: HttpClientOptions = { 21 | ...DefaultHttpClientOptions, 22 | ...options, 23 | }; 24 | 25 | const request = () => { 26 | return axios.request(this.configureOptions(configureProxy({ 27 | url, 28 | headers: optionsToUse.headers, 29 | method: optionsToUse.method.toString().toUpperCase(), 30 | data: optionsToUse.body, 31 | ...optionsToUse.options, 32 | }))) 33 | .then(result => { 34 | return { 35 | status: result.status, 36 | headers: result.headers, 37 | body: result.data, 38 | }; 39 | }); 40 | }; 41 | 42 | return doWithRetry>(request, `Requesting '${url}'`, optionsToUse.retry); 43 | } 44 | 45 | protected configureOptions(options: AxiosRequestConfig): AxiosRequestConfig { 46 | return options; 47 | } 48 | } 49 | 50 | /** 51 | * HttpClientFactory that creates HttpClient instances backed by Axios. 52 | */ 53 | export class AxiosHttpClientFactory implements HttpClientFactory { 54 | 55 | public create(url?: string): HttpClient { 56 | return new AxiosHttpClient(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/spi/http/wsClient.ts: -------------------------------------------------------------------------------- 1 | import * as HttpsProxyAgent from "https-proxy-agent"; 2 | import * as WebSocket from "ws"; 3 | import { RegistrationConfirmation } from "../../internal/transport/websocket/WebSocketRequestProcessor"; 4 | import { logger } from "../../util/logger"; 5 | 6 | /** 7 | * Factory to create a WebSocket instance. 8 | */ 9 | export interface WebSocketFactory { 10 | 11 | /** 12 | * Create a WebSocket for the provided registration 13 | * @param registration 14 | */ 15 | create(registration: RegistrationConfirmation): WebSocket; 16 | } 17 | 18 | /** 19 | * WS based WebSocketFactory implementation 20 | */ 21 | export class WSWebSocketFactory implements WebSocketFactory { 22 | 23 | public create(registration: RegistrationConfirmation): WebSocket { 24 | return new WebSocket(registration.url, this.configureOptions({})); 25 | } 26 | 27 | protected configureOptions(options: WebSocket.ClientOptions): WebSocket.ClientOptions { 28 | if (process.env.HTTPS_PROXY || process.env.https_proxy) { 29 | const proxy = process.env.HTTPS_PROXY || process.env.https_proxy; 30 | logger.debug(`WebSocket connection using proxy '${proxy}'`); 31 | options.agent = new HttpsProxyAgent(proxy); 32 | } 33 | return options; 34 | } 35 | } 36 | 37 | export const defaultWebSocketFactory = () => new WSWebSocketFactory(); 38 | -------------------------------------------------------------------------------- /lib/tree/LocatedTreeNode.ts: -------------------------------------------------------------------------------- 1 | import { TreeNode } from "@atomist/tree-path"; 2 | import { SourceLocation } from "../operations/common/SourceLocation"; 3 | 4 | /** 5 | * Extends TreeNode to include a source location within a project. 6 | */ 7 | export interface LocatedTreeNode extends TreeNode { 8 | 9 | sourceLocation: SourceLocation; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /lib/tree/ast/FileParser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PathExpression, 3 | TreeNode, 4 | } from "@atomist/tree-path"; 5 | import { File } from "../../project/File"; 6 | 7 | /** 8 | * Central interface for integration of trees and path expressions into the Atomist Project API. 9 | * Implemented by objects that can parse a file into an AST using a single grammar 10 | */ 11 | export interface FileParser { 12 | 13 | /** 14 | * Name of the top level production: name of the root TreeNode 15 | */ 16 | rootName: string; 17 | 18 | /** 19 | * Parse a file, returning an AST 20 | * @param {File} f 21 | * @return {TN} root tree node 22 | */ 23 | toAst(f: File): Promise; 24 | 25 | /** 26 | * If this method is supplied, it can help with optimization. 27 | * If we can look at the path expression and determine a match is impossible 28 | * in this file, we may be able to skip an expensive parsing operation. 29 | * @param {File} f 30 | * @param {PathExpression} pex 31 | * @return {Promise} 32 | */ 33 | couldBeMatchesInThisFile?(pex: PathExpression, f: File): Promise; 34 | 35 | /** 36 | * Can this path expression possibly be valid using this parser? 37 | * For example, if the implementation is backed by the grammar for a programming 38 | * language, the set of symbols is known in advance, as is the legality of their 39 | * combination. 40 | * If it is invalid, throw an Error. 41 | * This is useful to differentiate between nonsensical path expressions and 42 | * path expressions that didn't match anything. 43 | * If this function is not implemented, no path expressions will be rejected 44 | * @param {PathExpression} pex 45 | */ 46 | validate?(pex: PathExpression): void; 47 | } 48 | 49 | export function isFileParser(a: any): a is FileParser { 50 | return !!a && !!a.toAst; 51 | } 52 | -------------------------------------------------------------------------------- /lib/tree/ast/FileParserRegistry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isNamedNodeTest, 3 | isUnionPathExpression, 4 | PathExpression, 5 | SelfAxisSpecifier, 6 | toPathExpression, 7 | } from "@atomist/tree-path"; 8 | import { Dictionary } from "lodash"; 9 | import { FileParser } from "./FileParser"; 10 | 11 | /** 12 | * Registry of FileParsers. Allows resolution of the appropriate parser 13 | * for a path expression 14 | */ 15 | export interface FileParserRegistry { 16 | 17 | /** 18 | * Find a parser for the given path expression. 19 | * It's first location step must start with a node name. 20 | * If the FileParser supports validation, validate that it 21 | * can execute the path expression and throw an exception if not. 22 | * @param {string | PathExpression} pex 23 | * @return {FileParser} 24 | */ 25 | parserFor(pex: string | PathExpression): FileParser | undefined; 26 | } 27 | 28 | /** 29 | * Implementation of FileParserRegistry implementing fluent builder pattern 30 | */ 31 | export class DefaultFileParserRegistry implements FileParserRegistry { 32 | 33 | private readonly parserRegistry: Dictionary = {}; 34 | 35 | public addParser(pr: FileParser): this { 36 | this.parserRegistry[pr.rootName] = pr; 37 | return this; 38 | } 39 | 40 | public parserFor(pathExpression: string | PathExpression): FileParser | any { 41 | const parsed: PathExpression = toPathExpression(pathExpression); 42 | if (!isUnionPathExpression(parsed)) { 43 | const determiningStep = parsed.locationSteps.find(s => s.axis !== SelfAxisSpecifier); 44 | if (!!determiningStep && isNamedNodeTest(determiningStep.test)) { 45 | const parser = this.parserRegistry[determiningStep.test.name]; 46 | if (!!parser) { 47 | if (parser.validate) { 48 | parser.validate(parsed); 49 | } 50 | return parser; 51 | } 52 | } 53 | } 54 | return undefined; 55 | } 56 | 57 | public toString(): string { 58 | return `DefaultFileParserRegistry: parsers=[${Object.getOwnPropertyNames(this.parserRegistry)}]`; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/tree/ast/matchTesters.ts: -------------------------------------------------------------------------------- 1 | import { Grammar } from "@atomist/microgrammar"; 2 | import { PatternMatch } from "@atomist/microgrammar/lib/PatternMatch"; 3 | import { MatchTesterMaker } from "./astUtils"; 4 | 5 | /** 6 | * Exclude matches that are within a match of the given microgrammar 7 | * @param {Grammar} mg 8 | * @return {MatchTesterMaker} 9 | */ 10 | export function notWithin(mg: Grammar): MatchTesterMaker { 11 | return async file => { 12 | const content = await file.getContent(); 13 | const matches: PatternMatch[] = mg.findMatches(content); 14 | return n => !matches.some(m => { 15 | const mEndoffset = m.$offset + m.$matched.length; 16 | const nEndoffset = n.$offset + n.$value.length; 17 | return m.$offset <= n.$offset && mEndoffset >= nEndoffset; 18 | }); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /lib/util/constructionUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Interface for a no-arg function that can create an instance of the given type 4 | */ 5 | export type Factory = () => T; 6 | 7 | /** 8 | * Interface for objects with a no-arg constructor 9 | */ 10 | export type Constructor = new() => T; 11 | 12 | /** 13 | * A no-arg constructor or a no-arg function that can create 14 | * type T 15 | */ 16 | export type Maker = Factory | Constructor; 17 | 18 | /** 19 | * Convert a factory function with no arguments or a class with a no arg 20 | * constructor to a factory function 21 | * @param {Maker} fact 22 | * @return {Factory} 23 | */ 24 | export function toFactory(fact: Maker): Factory { 25 | const detyped = fact as any; 26 | try { 27 | const chf = () => new detyped(); 28 | // Try it to see if it works 29 | chf(); 30 | return chf; 31 | } catch (e) { 32 | // If we didn't succeed in using the constructor, try the other way 33 | return detyped; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/util/error.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | export function printError(e: any): void { 3 | if (e instanceof Error) { 4 | if (e.stack && e.stack.includes(e.message)) { 5 | console.error(e.stack); 6 | } else if (e.stack) { 7 | console.error(e.message); 8 | console.error(e.stack); 9 | } else { 10 | console.error(e.message); 11 | } 12 | } else { 13 | console.error(e); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/util/http.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | import { Configuration } from "../configuration"; 3 | import { automationClientInstance } from "../globals"; 4 | import { 5 | defaultHttpClientFactory, 6 | HttpClient, 7 | } from "../spi/http/httpClient"; 8 | 9 | /** 10 | * Return a HttpClient for given url 11 | * 12 | * This implementation falls back to the DefaultHttpClientFactory if no 13 | * configuration is provided and no running client instance can be found. 14 | */ 15 | export function httpClient(url: string, 16 | configuration?: Configuration): HttpClient { 17 | let cfg = configuration; 18 | if (!cfg && !!automationClientInstance()) { 19 | cfg = automationClientInstance().configuration; 20 | } 21 | return _.get(cfg, "http.client.factory", defaultHttpClientFactory()).create(url); 22 | } 23 | -------------------------------------------------------------------------------- /lib/util/packageJson.ts: -------------------------------------------------------------------------------- 1 | import * as appRoot from "app-root-path"; 2 | import * as path from "path"; 3 | 4 | /** 5 | * Load the package.json file of the consumer Node module. 6 | */ 7 | export function loadHostPackageJson(): any | undefined { 8 | try { 9 | // This works if the consumer of automation-client is not a global module 10 | return require(`${appRoot.path}/package.json`); // require is fine with / paths on windows 11 | } catch (err) { 12 | // This works if the consumer is installed globally 13 | const appDir = __dirname.split(path.join("node_modules", "@atomist", "automation-client"))[0]; 14 | try { 15 | return require(path.join(appDir, "package.json")); 16 | } catch (err) { 17 | // Intentionally left empty 18 | } 19 | } 20 | return undefined; 21 | } 22 | -------------------------------------------------------------------------------- /lib/util/pool.ts: -------------------------------------------------------------------------------- 1 | import { configurationValue } from "../configuration"; 2 | 3 | function concurrentDefault(): number { 4 | return configurationValue("pool.concurrent", 5); 5 | } 6 | 7 | /** 8 | * Execute all provided promises with a max concurrency 9 | * Results will be in the same order as the provided promises; if one promise rejects, 10 | * execution is stopped and the returned promise is rejected as well. 11 | * @param promises all promises to execute 12 | * @param concurrent the max number of concurrent promise executions 13 | */ 14 | export async function executeAll(promises: Array<() => Promise>, 15 | concurrent: number = concurrentDefault()): Promise { 16 | let index = 0; 17 | const results: any[] = []; 18 | const producer = () => { 19 | if (index < promises.length) { 20 | const promise = promises[index](); 21 | results[index] = promise; 22 | index++; 23 | return promise; 24 | } else { 25 | // tslint:disable-next-line:no-null-keyword 26 | return null; 27 | } 28 | }; 29 | 30 | const PromisePool = require("es6-promise-pool"); 31 | const pool = new PromisePool(producer, concurrent); 32 | 33 | pool.addEventListener("fulfilled", (event: any) => { 34 | results[results.indexOf(event.data.promise)] = event.data.result; 35 | }); 36 | 37 | await pool.start(); // start only returns a promise; not an [] of results 38 | 39 | return results; 40 | } 41 | -------------------------------------------------------------------------------- /lib/util/port.ts: -------------------------------------------------------------------------------- 1 | import * as portfinder from "portfinder"; 2 | 3 | /** 4 | * Scan for and return the first available port in the indicated range. 5 | * Range defaults to 2866 - 2888. 6 | * @param start 7 | * @param end 8 | */ 9 | export function scanFreePort(start: number = 2866, end: number = 2888): Promise { 10 | return portfinder.getPortPromise({ port: start, stopPort: end }); 11 | } 12 | -------------------------------------------------------------------------------- /lib/util/redact.ts: -------------------------------------------------------------------------------- 1 | import * as logform from "logform"; 2 | 3 | const redactions: Array<{ redacted: RegExp; replacement: string }> = []; 4 | 5 | /** 6 | * Prepare the logging to exclude something. 7 | * If you know you're about to, say, spawn a process that will get printed 8 | * to the log and will reveal something secret, then prepare the logger to 9 | * exclude that secret thing. 10 | * 11 | * Pass a regular expression that will match the secret thing and very little else. 12 | */ 13 | export function addRedaction(redacted: RegExp, suggestedReplacement?: string): void { 14 | const replacement = suggestedReplacement || "[REDACTED]"; 15 | redactions.push({ redacted, replacement }); 16 | } 17 | 18 | export function redact(message: string): string { 19 | let output = message; 20 | redactions.forEach(r => { 21 | output = typeof output === "string" ? output.replace(r.redacted, r.replacement) : output; 22 | }); 23 | return output; 24 | } 25 | 26 | export function redactLog(logInfo: logform.TransformableInfo): logform.TransformableInfo { 27 | let output = logInfo.message; 28 | redactions.forEach(r => { 29 | output = typeof output === "string" ? output.replace(r.redacted, r.replacement) : output; 30 | }); 31 | return { ...logInfo, message: output }; 32 | } 33 | -------------------------------------------------------------------------------- /lib/util/retry.ts: -------------------------------------------------------------------------------- 1 | import promiseRetry = require("promise-retry"); 2 | import { WrapOptions } from "retry"; 3 | 4 | import { logger } from "./logger"; 5 | 6 | /** 7 | * Default retry options for doWithRetry. 8 | */ 9 | export const DefaultRetryOptions: RetryOptions = { 10 | retries: 5, 11 | factor: 3, 12 | minTimeout: 1 * 500, 13 | maxTimeout: 5 * 1000, 14 | randomize: true, 15 | log: true, 16 | }; 17 | 18 | export interface RetryOptions extends WrapOptions { 19 | log?: boolean; 20 | } 21 | 22 | /** 23 | * Generic typed retry support 24 | * Perform the task, retrying according to the retry options 25 | * @param {() => Promise} what 26 | * @param {string} description 27 | * @param {Object} opts 28 | * @return {Promise} 29 | */ 30 | export function doWithRetry(what: () => Promise, 31 | description: string, 32 | opts: RetryOptions = {}): Promise { 33 | const retryOptions: WrapOptions = { 34 | ...DefaultRetryOptions as WrapOptions, 35 | ...opts, 36 | }; 37 | if (opts.log) { 38 | logger.log("silly", `${description} with retry options '%j'`, retryOptions); 39 | } 40 | return promiseRetry(retryOptions, retry => { 41 | return what() 42 | .catch(err => { 43 | if (opts.log) { 44 | logger.warn(`Error occurred attempting '${description}': ${err.message}`); 45 | } 46 | retry(err); 47 | }); 48 | }) as Promise; 49 | } 50 | -------------------------------------------------------------------------------- /test/.atomist/client.config-production.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaceIds": ["A998ENNA2"] 3 | } 4 | -------------------------------------------------------------------------------- /test/.atomist/client.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspaceIds": ["T7GMF5USG"], 3 | "apiKey": "6**************************************2", 4 | "endpoints": { 5 | "graphql": "https://user.graphql.ep:1313/gql/team", 6 | "api": "https://user.api.ep:4141/reg" 7 | }, 8 | "environment": "env-user", 9 | "application": "app-user", 10 | "modules": [ 11 | { 12 | "name": "@test/something-else", 13 | "apiKey": "no", 14 | "workspaceIds": ["negative"] 15 | }, 16 | { 17 | "name": "richie", 18 | "version": "<0.1.1", 19 | "apiKey": "nothing", 20 | "workspaceIds": ["nope"] 21 | }, 22 | { 23 | "name": "richie", 24 | "version": ">0.1.1", 25 | "workspaceIds": ["T7GMF5USG", "AT0M1ST01"], 26 | "environment": "env-module", 27 | "application": "app-module" 28 | }, 29 | { 30 | "name": "@atomist/automation-client", 31 | "version": "<0.0.1", 32 | "workspaceIds": ["T7GMF5USG", "AT0M1ST01", "BBD"], 33 | "environment": "env-module-bad", 34 | "application": "app-module-bad" 35 | }, 36 | { 37 | "name": "@atomist/automation-client", 38 | "version": ">0.0.1", 39 | "workspaceIds": ["AT0M1ST01"], 40 | "environment": "env-module-load", 41 | "application": "app-module-load", 42 | "policy": "durable", 43 | "http": { 44 | "host": "host-module" 45 | }, 46 | "cluster": { 47 | "enabled": true, 48 | "workers": 400 49 | }, 50 | "ws": { 51 | "termination": { 52 | "graceful": true, 53 | "gracePeriod": 900 54 | } 55 | } 56 | }, 57 | { 58 | "name": "elvis", 59 | "version": "^0.0.1", 60 | "workspaceIds": ["BLITZ"], 61 | "environment": "env-bop", 62 | "application": "app-bop" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /test/api/GitProjectRemote.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GitHubRepoRef, 3 | } from "../../lib/operations/common/GitHubRepoRef"; 4 | import { GitCommandGitProject } from "../../lib/project/git/GitCommandGitProject"; 5 | import { GitProject } from "../../lib/project/git/GitProject"; 6 | import { TestRepositoryVisibility } from "../credentials"; 7 | import { tempProject } from "../project/utils"; 8 | import { 9 | cleanAfterTest, 10 | Creds, 11 | getOwnerByToken, 12 | GitHubToken, 13 | newRepo, 14 | TestRepo, 15 | } from "./apiUtils"; 16 | 17 | describe("GitProject remote", () => { 18 | 19 | before(function(): void { 20 | if (!GitHubToken) { 21 | // tslint:disable-next-line:no-invalid-this 22 | this.skip(); 23 | } 24 | }); 25 | 26 | it.skip("add a file, init and commit, then push to new remote repo", async function(): Promise { 27 | // tslint:disable-next-line:no-invalid-this 28 | this.retries(5); 29 | 30 | const repo = `test-repo-2-${new Date().getTime()}`; 31 | const owner = await getOwnerByToken(); 32 | let gp: GitProject; 33 | try { 34 | const p = tempProject(); 35 | p.addFileSync("Thing", "1"); 36 | gp = GitCommandGitProject.fromProject(p, Creds); 37 | await gp.init(); 38 | await gp.createAndSetRemote(new GitHubRepoRef(owner, repo), "Thing1", TestRepositoryVisibility); 39 | await gp.commit("Added a Thing"); 40 | await gp.push(); 41 | await p.release(); 42 | await cleanAfterTest(gp, { owner, repo }); 43 | } catch (e) { 44 | await cleanAfterTest(gp, { owner, repo }); 45 | throw e; 46 | } 47 | }).timeout(16000); 48 | 49 | it.skip("add a file, then PR push to remote repo", async function(): Promise { 50 | // tslint:disable-next-line:no-invalid-this 51 | this.retries(3); 52 | 53 | let repo: TestRepo; 54 | let gp: GitProject; 55 | try { 56 | repo = await newRepo(); 57 | gp = await GitCommandGitProject.cloned(Creds, new GitHubRepoRef(repo.owner, repo.repo)); 58 | gp.addFileSync("Cat", "hat"); 59 | const branch = "thing2"; 60 | await gp.createBranch(branch); 61 | await gp.commit("Added a Thing"); 62 | await gp.push(); 63 | await gp.raisePullRequest("Thing2", "Adds another character"); 64 | await cleanAfterTest(gp, repo); 65 | } catch (e) { 66 | await cleanAfterTest(gp, repo); 67 | throw e; 68 | } 69 | }).timeout(40000); 70 | 71 | }); 72 | -------------------------------------------------------------------------------- /test/asyncConfig.ts: -------------------------------------------------------------------------------- 1 | export const configuration = (async () => { 2 | return { 3 | workspaceIds: ["123456"], 4 | name: "asyn-test", 5 | }; 6 | })(); 7 | -------------------------------------------------------------------------------- /test/bitbucket-api/BitBucketGit.test.ts: -------------------------------------------------------------------------------- 1 | import { BitBucketRepoRef } from "../../lib/operations/common/BitBucketRepoRef"; 2 | import { GitCommandGitProject } from "../../lib/project/git/GitCommandGitProject"; 3 | import { 4 | BitBucketCredentials, 5 | doWithNewRemote, 6 | skipBitBucketTests, 7 | } from "./BitBucketHelpers"; 8 | 9 | describe("BitBucket support", () => { 10 | 11 | before(function(): void { 12 | if (skipBitBucketTests()) { 13 | // tslint:disable-next-line:no-invalid-this 14 | this.skip(); 15 | } 16 | }); 17 | 18 | it("should clone", done => { 19 | GitCommandGitProject.cloned(BitBucketCredentials, 20 | new BitBucketRepoRef("jessitron", "poetry", "master")) 21 | .then(bp => bp.gitStatus()) 22 | .then(done, done); 23 | }).timeout(15000); 24 | 25 | it("should clone and add file in new branch", () => { 26 | return doWithNewRemote(bp => { 27 | bp.addFileSync("Thing", "1"); 28 | return bp.commit("Added Thing1") 29 | .then(() => { 30 | return bp.createBranch("thing1") 31 | .then(() => bp.push()); 32 | }); 33 | }); 34 | }).timeout(20000); 35 | 36 | it("should clone and add file in new branch then raise PR", () => { 37 | return doWithNewRemote(bp => { 38 | bp.addFileSync("Thing", "1"); 39 | return bp.commit("Added Thing1") 40 | .then(() => bp.createBranch("thing1")) 41 | .then(() => bp.push()) 42 | .then(() => bp.raisePullRequest("Add a thing", "Dr Seuss is fun")); 43 | }); 44 | }).timeout(20000); 45 | 46 | it("add a file, init and commit, then push to new remote repo", () => { 47 | return doWithNewRemote(bp => { 48 | return bp.gitStatus(); 49 | }); 50 | }).timeout(20000); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/bitbucket-api/BitBucketHelpers.ts: -------------------------------------------------------------------------------- 1 | import { BasicAuthCredentials } from "../../lib/operations/common/BasicAuthCredentials"; 2 | import { BitBucketRepoRef } from "../../lib/operations/common/BitBucketRepoRef"; 3 | import { GitCommandGitProject } from "../../lib/project/git/GitCommandGitProject"; 4 | import { GitProject } from "../../lib/project/git/GitProject"; 5 | import { TestRepositoryVisibility } from "../credentials"; 6 | import { tempProject } from "../project/utils"; 7 | 8 | export const BitBucketUser = process.env.ATLASSIAN_USER; 9 | export const BitBucketPassword = process.env.ATLASSIAN_PASSWORD; 10 | export const BitBucketCredentials: BasicAuthCredentials = { username: BitBucketUser, password: BitBucketPassword }; 11 | 12 | export function skipBitBucketTests(): boolean { 13 | if (BitBucketCredentials && BitBucketCredentials.username && BitBucketCredentials.password) { 14 | return false; 15 | } 16 | return true; 17 | } 18 | 19 | export async function doWithNewRemote(testAndVerify: (p: GitProject) => Promise) { 20 | const p = tempProject(); 21 | p.addFileSync("README.md", "Here's the readme for my new repo"); 22 | 23 | const repo = `test-${new Date().getTime()}`; 24 | 25 | const gp: GitProject = GitCommandGitProject.fromProject(p, BitBucketCredentials); 26 | const owner = BitBucketUser; 27 | 28 | const bbid = new BitBucketRepoRef(owner, repo); 29 | 30 | try { 31 | await gp.init(); 32 | await gp.createAndSetRemote(bbid, "Thing1", TestRepositoryVisibility); 33 | await gp.commit("Added a README"); 34 | await gp.push(); 35 | const clonedp = await GitCommandGitProject.cloned(BitBucketCredentials, bbid); 36 | await testAndVerify(clonedp); 37 | await bbid.deleteRemote(BitBucketCredentials); 38 | } catch (e) { 39 | await bbid.deleteRemote(BitBucketCredentials); 40 | throw e; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/bitbucket-api/generatorEndToEnd.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { BitBucketRepoRef } from "../../lib/operations/common/BitBucketRepoRef"; 3 | import { generate } from "../../lib/operations/generate/generatorUtils"; 4 | import { RemoteGitProjectPersister } from "../../lib/operations/generate/remoteGitProjectPersister"; 5 | import { GitCommandGitProject } from "../../lib/project/git/GitCommandGitProject"; 6 | import { 7 | deleteOrIgnore, 8 | tempRepoName, 9 | } from "../api/apiUtils"; 10 | import { 11 | BitBucketCredentials, 12 | BitBucketUser, 13 | skipBitBucketTests, 14 | } from "./BitBucketHelpers"; 15 | 16 | describe("BitBucket generator end to end", () => { 17 | 18 | before(function b(this: Mocha.Context): void { 19 | if (skipBitBucketTests()) { 20 | this.skip(); 21 | } 22 | }); 23 | let cleanup: () => Promise; 24 | after(async () => { 25 | if (cleanup) { 26 | await cleanup(); 27 | } 28 | }); 29 | 30 | it("should create a new BitBucket repo using generate function", async function t(this: Mocha.Context): Promise { 31 | this.retries(3); 32 | const repoName = tempRepoName(); 33 | const targetRepo = new BitBucketRepoRef(BitBucketUser, repoName); 34 | cleanup = () => deleteOrIgnore(targetRepo, BitBucketCredentials); 35 | const clonedSeed = GitCommandGitProject.cloned(BitBucketCredentials, new BitBucketRepoRef("springrod", "spring-rest-seed")); 36 | const result = await generate(clonedSeed, undefined, BitBucketCredentials, gp => Promise.resolve(gp), RemoteGitProjectPersister, targetRepo); 37 | assert(result.success); 38 | const p = await GitCommandGitProject.cloned(BitBucketCredentials, targetRepo); 39 | assert(await p.findFile("pom.xml")); 40 | }).timeout(20000); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /test/command/FileMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { CommandHandler } from "../../lib/decorators"; 2 | import { HandleCommand } from "../../lib/HandleCommand"; 3 | import { HandlerContext } from "../../lib/HandlerContext"; 4 | import { 5 | failure, 6 | HandlerResult, 7 | success, 8 | } from "../../lib/HandlerResult"; 9 | import { SlackFileMessage } from "../../lib/spi/message/MessageClient"; 10 | 11 | @CommandHandler("Handler to test different types of messages", "file_message_test") 12 | export class FileMessageTest implements HandleCommand { 13 | 14 | public handle(ctx: HandlerContext): Promise { 15 | const msg: SlackFileMessage = { 16 | content: JSON.stringify({test: "bla", bla: "test"}), 17 | fileType: "javascript", 18 | fileName: "bla.json", 19 | comment: "Some clever comment", 20 | }; 21 | 22 | return ctx.messageClient.respond(msg). 23 | then(success, failure); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/command/HelloWorld.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConfigurableCommandHandler, 3 | MappedParameter, 4 | MappedParameters, 5 | Parameter, 6 | } from "../../lib/decorators"; 7 | import { HandleCommand } from "../../lib/HandleCommand"; 8 | import { HandlerContext } from "../../lib/HandlerContext"; 9 | import { 10 | HandlerResult, 11 | Success, 12 | } from "../../lib/HandlerResult"; 13 | import { addressSlackUsersFromContext } from "../../lib/spi/message/MessageClient"; 14 | import { SecretBaseHandler } from "./SecretBaseHandler"; 15 | 16 | @ConfigurableCommandHandler("Send a hello back to the client", { intent: "hello cd", autoSubmit: true }) 17 | export class HelloWorld extends SecretBaseHandler implements HandleCommand { 18 | 19 | @Parameter({ description: "Name of person the greeting should be send to", pattern: /^.*$/, control: "textarea" }) 20 | public name: string; 21 | 22 | @MappedParameter(MappedParameters.SlackUserName) 23 | public sender: string; 24 | 25 | public async handle(ctx: HandlerContext): Promise { 26 | 27 | await ctx.messageClient.send( 28 | { text: "https://test:superpassword@google.com" }, await addressSlackUsersFromContext(ctx, "cd"), 29 | { 30 | id: "test", 31 | }); 32 | 33 | await ctx.messageClient.delete( 34 | await addressSlackUsersFromContext(ctx, "cd"), 35 | { id: "test" }); 36 | 37 | return Success; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/command/Message.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bold, 3 | SlackMessage, 4 | } from "@atomist/slack-messages"; 5 | import { 6 | CommandHandler, 7 | Parameter, 8 | } from "../../lib/decorators"; 9 | import { HandleCommand } from "../../lib/HandleCommand"; 10 | import { HandlerContext } from "../../lib/HandlerContext"; 11 | import { 12 | failure, 13 | HandlerResult, 14 | Success, 15 | } from "../../lib/HandlerResult"; 16 | import { guid } from "../../lib/internal/util/string"; 17 | import { buttonForCommand } from "../../lib/spi/message/MessageClient"; 18 | 19 | @CommandHandler("Handler to test different types of messages", "message_test") 20 | export class MessageTest implements HandleCommand { 21 | 22 | @Parameter({description: "message type"}) 23 | public type: "ttl" | "always" | "update_only"; 24 | 25 | @Parameter({description: "message type", required: false, displayable: false}) 26 | public msgId: string; 27 | 28 | public handle(ctx: HandlerContext): Promise { 29 | if (!this.msgId) { 30 | this.msgId = guid(); 31 | } 32 | const msg: SlackMessage = { 33 | text: `Selected ${bold(this.type || "none")}`, 34 | attachments: [{ 35 | fallback: "Actions", 36 | actions: [ 37 | buttonForCommand( 38 | {text: "ttl 10s"}, this, {type: "ttl", msgId: this.msgId }), 39 | buttonForCommand( 40 | {text: "always"}, this, {type: "always", msgId: this.msgId }), 41 | buttonForCommand( 42 | {text: "update"}, this, {type: "update_only", msgId: this.msgId }), 43 | ], 44 | }], 45 | }; 46 | 47 | if (this.type === "ttl") { 48 | return ctx.messageClient.respond(msg, { id: this.msgId, ttl: 1000 * 10 }) 49 | .then(() => Success, failure); 50 | } else if (this.type === "always") { 51 | return ctx.messageClient.respond(msg, { id: this.msgId, post: "always" }) 52 | .then(() => Success, failure); 53 | } else if (this.type === "update_only") { 54 | return ctx.messageClient.respond(msg, { id: this.msgId, post: "update_only" }) 55 | .then(() => Success, failure); 56 | } else { 57 | return ctx.messageClient.respond(msg, { id: this.msgId }) 58 | .then(() => Success, failure); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/command/PlainHelloWorld.ts: -------------------------------------------------------------------------------- 1 | import { Secrets } from "../../lib/decorators"; 2 | import { HandleCommand } from "../../lib/HandleCommand"; 3 | import { HandlerContext } from "../../lib/HandlerContext"; 4 | import { HandlerResult } from "../../lib/HandlerResult"; 5 | import { 6 | CommandHandlerMetadata, 7 | Parameter, 8 | SecretDeclaration, 9 | } from "../../lib/metadata/automationMetadata"; 10 | 11 | export class PlainHelloWorld implements HandleCommand, CommandHandlerMetadata { 12 | // ^ -- implementing that interface is totally optional and only 13 | // useful for getting type checking from the compiler 14 | 15 | public description: string = "Sends a hello back to the client"; 16 | public intent: string[] = ["hello world"]; 17 | public parameters: Parameter[] = [{ 18 | name: "name", 19 | display_name: "Name", pattern: "^.*$", required: true, default_value: "Jim", 20 | }]; 21 | public secrets: SecretDeclaration[] = [{ name: "userToken", uri: Secrets.UserToken }]; 22 | 23 | public name: string; 24 | 25 | public userToken: string; 26 | 27 | // Use "params" rather than "this" to get parameters and avoid scoping issues! 28 | public handle(ctx: HandlerContext, params: this): Promise { 29 | throw new Error("Not relevant"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/command/SearchStackOverflow.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Attachment, 3 | SlackMessage, 4 | } from "@atomist/slack-messages"; 5 | // tslint:disable-next-line:import-blacklist 6 | import axios from "axios"; 7 | import { 8 | CommandHandler, 9 | Parameter, 10 | Tags, 11 | } from "../../lib/decorators"; 12 | import { HandleCommand } from "../../lib/HandleCommand"; 13 | import { HandlerContext } from "../../lib/HandlerContext"; 14 | import { HandlerResult } from "../../lib/HandlerResult"; 15 | 16 | const apiSearchUrl = 17 | `http://api.stackexchange.com/2.2/search/advanced?pagesize=3&order=desc&sort=relevance&site=stackoverflow&q=`; 18 | const webSearchUrl = `http://stackoverflow.com/search?order=desc&sort=relevance&q=`; 19 | const thumbUrl = "https://slack-imgs.com/?c=1&o1=wi75.he75&url=https%3A%2F%2Fcdn.sstatic.net" + 20 | "%2FSites%2Fstackoverflow%2Fimg%2Fapple-touch-icon%402.png%3Fv%3D73d79a89bded"; 21 | 22 | @CommandHandler("Query Stack Overflow", "search so") 23 | @Tags("stack-overflow") 24 | export class SearchStackOverflow implements HandleCommand { 25 | 26 | @Parameter({ description: "your search query", pattern: /^.*$/, required: true }) 27 | public query: string; 28 | 29 | public handle(ctx: HandlerContext): Promise { 30 | return axios.get(`${apiSearchUrl}${encodeURIComponent(this.query)}`) 31 | .then(res => Promise.resolve(this.handleResult(res, this.query))) 32 | .then(msg => { 33 | return ctx.messageClient.respond(msg); 34 | }) 35 | .then(() => Promise.resolve({ code: 0 })); 36 | } 37 | 38 | private handleResult(result: any, query: string): SlackMessage { 39 | const data = result.data; 40 | const msg: SlackMessage = {}; 41 | msg.attachments = (data.items.map(i => { 42 | const attachment: Attachment = { 43 | fallback: i.title, 44 | author_name: i.owner.display_name, 45 | author_link: i.owner.link, 46 | author_icon: i.owner.profile_image, 47 | title: i.title, 48 | title_link: i.link, 49 | thumb_url: thumbUrl, 50 | footer: i.tags.join(", "), 51 | ts: i.last_activity_date, 52 | }; 53 | return attachment; 54 | })); 55 | 56 | if (data.items === null || data.items.length === 0) { 57 | msg.text = "No results found"; 58 | } else { 59 | msg.attachments.push({ 60 | fallback: "Show more...", 61 | title: "Show more...", 62 | title_link: `${webSearchUrl}${encodeURIComponent(query)}`, 63 | }); 64 | } 65 | return msg; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/command/SecretBaseHandler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | MappedParameters, 4 | Secret, 5 | Secrets, 6 | } from "../../lib/decorators"; 7 | 8 | export abstract class SecretBaseHandler { 9 | 10 | @Secret(Secrets.userToken(["repo"])) 11 | public userToken: string; 12 | 13 | @MappedParameter(MappedParameters.SlackUser) 14 | public userName: string; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/command/SendStartupMessage.ts: -------------------------------------------------------------------------------- 1 | import { SlackMessage } from "@atomist/slack-messages"; 2 | import { 3 | CommandHandler, 4 | Parameter, 5 | } from "../../lib/decorators"; 6 | import { HandleCommand } from "../../lib/HandleCommand"; 7 | import { HandlerContext } from "../../lib/HandlerContext"; 8 | import { HandlerResult } from "../../lib/HandlerResult"; 9 | import { addressSlackUsers } from "../../lib/spi/message/MessageClient"; 10 | 11 | @CommandHandler("Sends a startup message to the owner of this automation-client") 12 | export class SendStartupMessage implements HandleCommand { 13 | 14 | @Parameter({ pattern: /^.*$/ }) 15 | public owner: string; 16 | 17 | @Parameter({ pattern: /^.*$/ }) 18 | public name: string; 19 | 20 | @Parameter({ pattern: /^.*$/ }) 21 | public version: string; 22 | 23 | public handle(ctx: HandlerContext): Promise { 24 | const msg: SlackMessage = { 25 | text: `It's me, \`${this.name}/${this.version}\`! I'm now running!`, 26 | }; 27 | return ctx.messageClient.send(msg, addressSlackUsers(ctx.source.slack.team.id, this.owner)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/credentials.ts: -------------------------------------------------------------------------------- 1 | import { GitHubRepoRef } from "../lib/operations/common/GitHubRepoRef"; 2 | 3 | export const GitHubToken: string = "NOT_A_LEGIT_TOKEN"; 4 | export const Creds = { token: GitHubToken }; 5 | 6 | function visibility(): "public" | "private" { 7 | const vis = process.env.GITHUB_VISIBILITY || "public"; 8 | if (vis === "public" || vis === "private") { 9 | return vis; 10 | } 11 | throw new Error(`GITHUB_VISIBILITY must be 'public' or 'private', yours is '${vis}'`); 12 | } 13 | 14 | export const TestRepositoryVisibility = visibility(); 15 | 16 | export const ExistingRepoOwner = "atomisthqtest"; 17 | export const ExistingRepoName = "this-repository-exists"; 18 | export const ExistingRepoSha = "68ffbfaa4b6ddeff563541b4b08d3b53060a51d8"; 19 | // export const ExistingRepoSha = "c756508d31484d67e3b13805608a1be4e928900c"; 20 | export const ExistingRepoRef = new GitHubRepoRef(ExistingRepoOwner, ExistingRepoName, ExistingRepoSha); 21 | export const SeedRepoOwner = "atomist-seeds"; 22 | export const SeedRepoName = "spring-rest"; 23 | export const SeedRepoSha = "1c097a4897874b08e3b3ddb9675a1ac460ae46de"; 24 | export const SeedRepoRef = new GitHubRepoRef(SeedRepoOwner, SeedRepoName, SeedRepoSha); 25 | -------------------------------------------------------------------------------- /test/empty.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from "../lib/configuration"; 2 | 3 | export const configuration: Configuration = {}; 4 | -------------------------------------------------------------------------------- /test/event/GitLabPush.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bold, 3 | SlackMessage, 4 | url, 5 | } from "@atomist/slack-messages"; 6 | import { EventHandler } from "../../lib/decorators"; 7 | import { 8 | EventFired, 9 | HandleEvent, 10 | } from "../../lib/HandleEvent"; 11 | import { HandlerContext } from "../../lib/HandlerContext"; 12 | import { 13 | failure, 14 | HandlerResult, 15 | Success, 16 | } from "../../lib/HandlerResult"; 17 | import { addressSlackChannels } from "../../lib/spi/message/MessageClient"; 18 | 19 | @EventHandler("Notify on GitLab pushes", `subscription GitLabPush { 20 | GitLabPush { 21 | id 22 | user_username 23 | user_avatar 24 | repository { 25 | name 26 | git_http_url 27 | } 28 | commits { 29 | id 30 | url 31 | message 32 | author { 33 | name 34 | email 35 | } 36 | } 37 | } 38 | } 39 | `) 40 | export class GitLabPush implements HandleEvent { 41 | 42 | public handle(e: EventFired, ctx: HandlerContext): Promise { 43 | const push = e.data.GitLabPush[0]; 44 | const text = `${push.commits.length} new ${(push.commits.length > 1 ? "commits" : "commit")} ` + 45 | `to ${bold(url(push.repository.git_http_url, `${push.user_username}/${push.repository.name}/master`))}`; 46 | const msg: SlackMessage = { 47 | text, 48 | attachments: [{ 49 | fallback: text, 50 | author_name: `@${push.user_username}`, 51 | author_icon: push.user_avatar, 52 | text: push.commits.map(c => `\`${url(c.url, c.id.slice(0, 7))}\` ${c.message.slice(0, 49)}`).join("\n"), 53 | mrkdwn_in: ["text"], 54 | color: "#00a5ff", 55 | }, 56 | ], 57 | }; 58 | return ctx.messageClient.send(msg, addressSlackChannels("FIXME", "gitlab")) 59 | .then(() => Success, failure); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/event/HelloCircle.ts: -------------------------------------------------------------------------------- 1 | import { EventHandler } from "../../lib/decorators"; 2 | import { 3 | EventFired, 4 | HandleEvent, 5 | } from "../../lib/HandleEvent"; 6 | import { HandlerContext } from "../../lib/HandlerContext"; 7 | import { 8 | failure, 9 | HandlerResult, 10 | Success, 11 | } from "../../lib/HandlerResult"; 12 | import { addressSlackChannels } from "../../lib/spi/message/MessageClient"; 13 | 14 | @EventHandler("Notify on Circle CI events", `subscription HelloCircle 15 | { 16 | CircleCIPayload { 17 | id 18 | payload { 19 | build_num 20 | vcs_revision 21 | reponame 22 | branch 23 | } 24 | } 25 | }`) 26 | export class HelloCircle implements HandleEvent { 27 | 28 | public handle(e: EventFired, ctx: HandlerContext): Promise { 29 | const b = e.data.CircleCIPayload[0]; 30 | return ctx.messageClient.send(`*#${b.payload.build_num} ${b.payload.reponame}*`, 31 | addressSlackChannels("FIXME", "general")) 32 | .then(() => Success, failure); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/event/HelloWorld.ts: -------------------------------------------------------------------------------- 1 | import { EventHandler } from "../../lib/decorators"; 2 | import { 3 | EventFired, 4 | HandleEvent, 5 | } from "../../lib/HandleEvent"; 6 | import { HandlerContext } from "../../lib/HandlerContext"; 7 | import { 8 | HandlerResult, 9 | SuccessPromise, 10 | } from "../../lib/HandlerResult"; 11 | 12 | @EventHandler("Receive HelloWorlds via http ingestion", ` 13 | subscription HelloWorldIngester { 14 | HelloWorld { 15 | id 16 | sender { 17 | name 18 | } 19 | recipient { 20 | name 21 | } 22 | } 23 | } 24 | `) 25 | export class HelloWorldIngester implements HandleEvent { 26 | public handle(e: EventFired, ctx: HandlerContext): Promise { 27 | console.log(JSON.stringify(e.data)); 28 | return SuccessPromise; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/graph/chatChannelFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment ChannelName on ChatChannel { 2 | name 3 | } -------------------------------------------------------------------------------- /test/graph/onSentryAlert.graphql.custom: -------------------------------------------------------------------------------- 1 | subscription bla { 2 | SentryAlert { 3 | id 4 | message 5 | url 6 | event { 7 | extra { 8 | git_sha 9 | git_owner 10 | git_repo 11 | correlation_id 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /test/graph/repoFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment RepoName on Repo { 2 | name 3 | owner 4 | org { 5 | team { 6 | name 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/graph/someOtherQuery.graphql: -------------------------------------------------------------------------------- 1 | query SomeOtherQuery($teamId: ID!, $offset: Int!) { 2 | ChatTeam(id: $teamId) { 3 | orgs { 4 | repo(first: 100, offset: $offset) { 5 | owner 6 | name 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/graph/someOtherQueryWithTheSameName.graphql: -------------------------------------------------------------------------------- 1 | query ChatTeam ($teamId: ID!, $offset: Int!) { 2 | ChatTeam(id: $teamId) { 3 | orgs { 4 | repo(first: 100, offset: $offset) { 5 | owner 6 | name 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/graph/someSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription 2 | SomeSubscription( 3 | $teamId: ID!, 4 | $isPrivate: Boolean!, 5 | $offset: Int! 6 | $foo: String!, 7 | $fooBar: String!, 8 | ) { 9 | ChatTeam(id: $teamId) { 10 | orgs { 11 | repo(first: 50, offset: $offset, private: $isPrivate) { 12 | owner 13 | name 14 | bla(test: $teamId, fooBar: $fooBar, foo: $foo) { 15 | name 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/graph/someSubscriptionWithEnum.graphql: -------------------------------------------------------------------------------- 1 | subscription BuildWithStatus($status: BuildStatus!) { 2 | Build(status: $status) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /test/graph/someSubscriptionWithEnumArray.graphql: -------------------------------------------------------------------------------- 1 | subscription BuildWithStatus($statuses: [BuildStatus]!) { 2 | Build(statuss: $statuses) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /test/graph/subscriptionWithFragment.graphql: -------------------------------------------------------------------------------- 1 | subscription Test { 2 | Repo { 3 | ... RepoName 4 | channels { 5 | ... ChannelName 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /test/graphql/fragment/chatChannelFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment ChannelNameInGraphql on ChatChannel { 2 | name 3 | } -------------------------------------------------------------------------------- /test/graphql/fragment/repoFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment RepoNameInGraphql on Repo { 2 | name 3 | owner 4 | org { 5 | team { 6 | name 7 | } 8 | } 9 | channels { 10 | ... ChannelNameInGraphql 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/graphql/ingester/helloWorld.graphql: -------------------------------------------------------------------------------- 1 | type HelloWorld @rootType { 2 | sender: HelloWorldPerson! 3 | recipient(name: String!): HelloWorldPerson! 4 | urgency: Urgency! 5 | } 6 | 7 | type HelloWorldPerson { 8 | name: String! 9 | } 10 | 11 | enum Urgency { 12 | HIGH, 13 | LOW, 14 | NORMAL 15 | } 16 | -------------------------------------------------------------------------------- /test/graphql/ingester/sdmGoal.graphql: -------------------------------------------------------------------------------- 1 | type SdmGoalTest @rootType { 2 | sha: String! 3 | commit: Commit @linkTo(queryName: "commitBySha", variables: [{name: "sha", path: "$.sha"}]) 4 | } 5 | -------------------------------------------------------------------------------- /test/graphql/mutation/addBotToSlackChannel.graphql: -------------------------------------------------------------------------------- 1 | mutation AddBotToSlackChannel($channelId: String!) { 2 | addBotToSlackChannel(channelId: $channelId) { 3 | name 4 | id 5 | } 6 | } -------------------------------------------------------------------------------- /test/graphql/mutation/createSlackChannel.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateSlackChannel($name: String!) { 2 | createSlackChannel(name: $name) { 3 | name 4 | id 5 | } 6 | } -------------------------------------------------------------------------------- /test/graphql/mutation/linkSlackChannelToRepo.graphql: -------------------------------------------------------------------------------- 1 | mutation LinkSlackChannelToRepo($channelId: String!, $repo: String!, $owner: String!) { 2 | linkSlackChannelToRepo(channelId: $channelId, repo: $repo, owner: $owner) { 3 | name 4 | id 5 | } 6 | } -------------------------------------------------------------------------------- /test/graphql/mutation/setChatUserPreference.graphql: -------------------------------------------------------------------------------- 1 | mutation SetChatUserPreference( 2 | $teamId: String! 3 | $userId: String! 4 | $name: String! 5 | $value: String! 6 | ) { 7 | setChatUserPreference( 8 | chatTeamId: $teamId 9 | chatUserId: $userId 10 | name: $name 11 | value: $value 12 | ) { 13 | name 14 | value 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/graphql/query/repos.graphql: -------------------------------------------------------------------------------- 1 | query Repos($teamId: ID!, $offset: Int!) { 2 | ChatTeam(id: $teamId) { 3 | orgs(owner: "atomisthq") { 4 | repo(first: 100, offset: $offset) { 5 | owner 6 | name 7 | } 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/graphql/subscription/fooBar.graphql: -------------------------------------------------------------------------------- 1 | subscription TestFooBar { 2 | Repo { 3 | ... RepoNameInGraphql 4 | } 5 | } -------------------------------------------------------------------------------- /test/graphql/subscription/subscriptionWithFragmentInGraphql.graphql: -------------------------------------------------------------------------------- 1 | subscription Test { 2 | Repo { 3 | ... RepoNameInGraphql 4 | } 5 | } -------------------------------------------------------------------------------- /test/internal/invoker/AbstractScriptedFlushable.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | import { AbstractScriptedFlushable } from "../../../lib/internal/common/AbstractScriptedFlushable"; 4 | 5 | class TestFlushable extends AbstractScriptedFlushable { 6 | 7 | public count = 0; 8 | } 9 | 10 | describe("AbstractScriptedFlushable", () => { 11 | 12 | it("is born clean", () => { 13 | const f = new TestFlushable(); 14 | assert(!f.dirty); 15 | }); 16 | 17 | it("is dirtied by an action", () => { 18 | const f = new TestFlushable(); 19 | f.recordAction(a => Promise.resolve(a.count++)); 20 | assert(f.dirty); 21 | assert(f.count === 0); 22 | }); 23 | 24 | it("shows effect after flush", done => { 25 | const f = new TestFlushable(); 26 | f.recordAction(a => Promise.resolve(a.count++)); 27 | assert(f.count === 0); 28 | f.flush().then(_ => { 29 | assert(!f.dirty); 30 | assert(f.count === 1); 31 | done(); 32 | }); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/internal/invoker/functionStyleCommandHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "../../../lib/HandlerContext"; 2 | import { CommandInvocation } from "../../../lib/internal/invoker/Payload"; 3 | import { guid } from "../../../lib/internal/util/string"; 4 | import { BuildableAutomationServer } from "../../../lib/server/BuildableAutomationServer"; 5 | import { addAtomistSpringAgent } from "../metadata/addAtomistSpringAgent"; 6 | 7 | describe("function style command handler invocation", () => { 8 | 9 | it("should successfully invoke", done => { 10 | const ctx: HandlerContext = { 11 | workspaceId: guid(), 12 | messageClient: { 13 | respond(x) { 14 | console.log(x); 15 | return Promise.resolve(true); 16 | }, 17 | }, 18 | } as HandlerContext; 19 | const s = new BuildableAutomationServer( 20 | { name: "foobar", version: "1.0.0", workspaceIds: ["bar"], keywords: [], custom: { http: { port: 1111 } } }); 21 | s.fromCommandHandler(addAtomistSpringAgent); 22 | 23 | const payload: CommandInvocation = { 24 | name: "AddAtomistSpringAgent", 25 | mappedParameters: [{ name: "githubWebUrl", value: "the restaurant at the end of the universe" }], 26 | secrets: [{ 27 | uri: "atomist://some_secret", value: "Vogons write the best poetry", 28 | }], 29 | args: [{ 30 | name: "slackTeam", 31 | value: "T1066", 32 | }], 33 | }; 34 | s.invokeCommand(payload, ctx).then(hr => { 35 | done(); 36 | }).catch(done); 37 | }); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/internal/metadata/addAtomistSpringAgent.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappedParameter, 3 | Parameter, 4 | Parameters, 5 | Secret, 6 | Value, 7 | } from "../../../lib/decorators"; 8 | import { HandleCommand } from "../../../lib/HandleCommand"; 9 | import { commandHandlerFrom } from "../../../lib/onCommand"; 10 | import { succeed } from "../../../lib/operations/support/contextUtils"; 11 | 12 | @Parameters() 13 | export class AddAtomistSpringAgentParams { 14 | 15 | @Parameter({ 16 | displayName: "Slack Team ID", 17 | description: "team identifier for Slack team associated with this repo", 18 | pattern: /^T[0-9A-Z]+$/, 19 | validInput: "Slack team identifier of form T0123WXYZ", 20 | required: true, 21 | }) 22 | public slackTeam: string; 23 | 24 | @MappedParameter("atomist://github_url") 25 | public githubWebUrl: string; 26 | 27 | @Secret("atomist://some_secret") 28 | public someSecret: string; 29 | 30 | @Value({ path: "custom.http.port", required: true }) 31 | public port: number; 32 | } 33 | 34 | // Note we need an explicit type annotation here to avoid an error 35 | // due to exporting an un-imported type 36 | // Alternatively, if it's not exported it's fine 37 | export const addAtomistSpringAgent: HandleCommand = 38 | commandHandlerFrom((ctx, params) => 39 | ctx.messageClient.respond("I got your message: slackTeam=" + params.slackTeam) 40 | .then(succeed), 41 | AddAtomistSpringAgentParams, 42 | "AddAtomistSpringAgent", 43 | "add the Atomist Agent to a Spring Boot project", 44 | "add agent", 45 | ["atomist", "spring", "agent"], 46 | true); 47 | -------------------------------------------------------------------------------- /test/internal/metadata/eventMetadataReading.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | import { Configuration } from "../../../lib/configuration"; 4 | import { 5 | Parameters, 6 | Value, 7 | } from "../../../lib/decorators"; 8 | import { subscription } from "../../../lib/graph/graphQL"; 9 | import { Success } from "../../../lib/HandlerResult"; 10 | import { metadataFromInstance } from "../../../lib/internal/metadata/metadataReading"; 11 | import { populateValues } from "../../../lib/internal/parameterPopulation"; 12 | import { EventHandlerMetadata } from "../../../lib/metadata/automationMetadata"; 13 | import { eventHandlerFrom } from "../../../lib/onEvent"; 14 | import { HelloIssue } from "../../event/HelloIssue"; 15 | 16 | describe("class style event metadata reading", () => { 17 | 18 | it("should extract metadataFromInstance from event handler", () => { 19 | const h = new HelloIssue(); 20 | const md = metadataFromInstance(h) as EventHandlerMetadata; 21 | assert(md.subscriptionName === "HelloIssue"); 22 | 23 | const config: Configuration = { 24 | http: { 25 | port: 1111, 26 | }, 27 | }; 28 | 29 | populateValues(h, md, config); 30 | assert.equal(h.port, config.http.port); 31 | }); 32 | 33 | }); 34 | 35 | @Parameters() 36 | class ParametersWithConfig { 37 | 38 | @Value("http.port") 39 | public port: number; 40 | } 41 | 42 | describe("event style event metadata reading", () => { 43 | 44 | it("should extract metadataFromInstance from event handler", () => { 45 | const h = eventHandlerFrom( 46 | async e => { 47 | return Success; 48 | }, 49 | ParametersWithConfig, 50 | subscription("subscriptionWithFragmentInGraphql"), 51 | "TestHandler", 52 | "desc", 53 | "tag"); 54 | 55 | const md = metadataFromInstance(h) as EventHandlerMetadata; 56 | assert(md.subscriptionName === "TestHandler"); 57 | assert(md.description === "desc"); 58 | assert(md.tags[0].name === "tag"); 59 | assert(md.values[0].name === "port"); 60 | 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/internal/metadata/functionStyleMetadataReading.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | import { metadataFromInstance } from "../../../lib/internal/metadata/metadataReading"; 4 | import { CommandHandlerMetadata } from "../../../lib/metadata/automationMetadata"; 5 | import { addAtomistSpringAgent } from "./addAtomistSpringAgent"; 6 | 7 | describe("function style metadata reading", () => { 8 | 9 | it("should get correct handler name", () => { 10 | assert(metadataFromInstance(addAtomistSpringAgent).name === "AddAtomistSpringAgent"); 11 | }); 12 | 13 | it("should extract metadataFromInstance from function sourced command handler", () => { 14 | const md = metadataFromInstance(addAtomistSpringAgent) as CommandHandlerMetadata; 15 | assert(md.parameters.length === 1); 16 | assert(md.parameters[0].name === "slackTeam"); 17 | assert(md.mapped_parameters.length === 1); 18 | assert(md.mapped_parameters[0].name === "githubWebUrl"); 19 | assert(md.mapped_parameters[0].uri === "atomist://github_url"); 20 | assert(md.secrets.length === 1); 21 | assert(md.secrets[0].name === "someSecret"); 22 | assert(md.secrets[0].uri === "atomist://some_secret"); 23 | assert(md.values.length === 1); 24 | assert(md.values[0].name === "port"); 25 | assert(md.values[0].path === "custom.http.port"); 26 | assert.deepEqual(md.intent, ["add agent"]); 27 | assert.deepEqual(md.tags.map(t => t.name), ["atomist", "spring", "agent"]); 28 | assert(md.auto_submit); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/internal/util/string.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | 3 | import { obfuscateJson } from "../../../lib/internal/util/string"; 4 | 5 | describe("string", () => { 6 | 7 | describe("obfuscateJson", () => { 8 | 9 | it("should do nothing", () => { 10 | const k = "nothing"; 11 | const v = "something"; 12 | const r = obfuscateJson(k, v); 13 | assert(r === v); 14 | }); 15 | 16 | it("should remove handler values", () => { 17 | const ks = ["commands", "events", "ingesters", "listeners", "customizers", "postProcessors"]; 18 | ks.forEach(k => { 19 | const r = obfuscateJson(k, ["one", "two", "three"]); 20 | assert(r === undefined); 21 | }); 22 | }); 23 | 24 | it("should obfuscate sensitive information", () => { 25 | const ks = [ 26 | "token", 27 | "password", 28 | "jwt", 29 | "url", 30 | "secret", 31 | "authorization", 32 | "jazzToken", 33 | "my_password", 34 | "JWT", 35 | "LongURL", 36 | "dbl-secret", 37 | "authorization response", 38 | ]; 39 | ks.forEach(k => { 40 | const r = obfuscateJson(k, "doublesecret"); 41 | assert(r === "d**********t", `failing key '${k}'`); 42 | }); 43 | }); 44 | 45 | }); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /test/operations/common/fromProjectList.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | import { 4 | fromListRepoFinder, 5 | fromListRepoLoader, 6 | } from "../../../lib/operations/common/fromProjectList"; 7 | import { InMemoryProject } from "../../../lib/project/mem/InMemoryProject"; 8 | 9 | describe("fromProjectList.ts", () => { 10 | 11 | describe("fromListRepoFinder", () => { 12 | 13 | it("should object when given undefined repo id", () => { 14 | const noIdProject = InMemoryProject.of(); 15 | noIdProject.id = undefined; 16 | assert.throws(() => fromListRepoFinder([noIdProject])); 17 | }); 18 | 19 | }); 20 | 21 | describe("fromListRepoLoader", () => { 22 | 23 | it("should object when given undefined repo id", () => { 24 | const noIdProject = InMemoryProject.of(); 25 | noIdProject.id = undefined; 26 | assert.throws(() => fromListRepoLoader([noIdProject])); 27 | }); 28 | 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/operations/common/params/GitHubParams.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { GitHubRepoRef } from "../../../../lib/operations/common/GitHubRepoRef"; 3 | import { GitHubTargetsParams } from "../../../../lib/operations/common/params/GitHubTargetsParams"; 4 | import { MappedRepoParameters } from "../../../../lib/operations/common/params/MappedRepoParameters"; 5 | 6 | describe("GitHubTargetsParams", () => { 7 | 8 | it("should work with single repo", () => { 9 | const p = new MappedRepoParameters(); 10 | p.owner = "foo"; 11 | p.repo = "bar"; 12 | assert(p.repoRef.owner === p.owner); 13 | assert(p.repoRef.repo === p.repo); 14 | }); 15 | 16 | it("should default to no single repo", () => { 17 | const p = new MappedRepoParameters(); 18 | assert(!p.repoRef); 19 | }); 20 | 21 | it("should apply regex: match", () => { 22 | const p = new MappedRepoParameters(); 23 | p.repo = ".*"; 24 | assert(!p.repoRef); 25 | assert(p.test(new GitHubRepoRef("a", "b"))); 26 | }); 27 | 28 | it("should apply regex: no match", () => { 29 | const p = new MappedRepoParameters(); 30 | p.repo = "x.*"; 31 | assert(!p.repoRef); 32 | assert(!p.test(new GitHubRepoRef("a", "b"))); 33 | assert(p.test(new GitHubRepoRef("a", "xb"))); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/operations/edit/LocalEditor.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as assert from "power-assert"; 3 | import { 4 | ProjectEditor, 5 | successfulEdit, 6 | } from "../../../lib/operations/edit/projectEditor"; 7 | import { tempProject } from "../../project/utils"; 8 | 9 | describe("Local editing", () => { 10 | 11 | it("should not edit with no op editor", done => { 12 | const project = tempProject(); 13 | const editor: ProjectEditor = p => Promise.resolve(successfulEdit(p, false)); 14 | editor(project, undefined, undefined) 15 | .then(r => { 16 | assert(!r.edited); 17 | done(); 18 | }).catch(done); 19 | }); 20 | 21 | it("should edit on disk with real editor", done => { 22 | const project = tempProject(); 23 | const editor: ProjectEditor = p => { 24 | p.addFileSync("thing", "1"); 25 | return Promise.resolve(successfulEdit(p, true)); 26 | }; 27 | editor(project, undefined) 28 | .then(r => { 29 | assert(r.edited); 30 | // Reload project 31 | assert(fs.statSync(project.baseDir + "/thing").isFile()); 32 | done(); 33 | }).catch(done); 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/operations/edit/VerifyEditMode.ts: -------------------------------------------------------------------------------- 1 | import { HandlerContext } from "../../../lib/HandlerContext"; 2 | import { CustomExecutionEditMode } from "../../../lib/operations/edit/editModes"; 3 | import { 4 | EditResult, 5 | ProjectEditor, 6 | } from "../../../lib/operations/edit/projectEditor"; 7 | import { Project } from "../../../lib/project/Project"; 8 | 9 | /** 10 | * EditMode implementation that allows verification of the 11 | * resulting state of the project 12 | */ 13 | export class VerifyEditMode implements CustomExecutionEditMode { 14 | 15 | public message: string = "foo"; 16 | 17 | constructor(private readonly assertions: (p: Project) => void) { 18 | } 19 | 20 | public edit

(p: Project, action: ProjectEditor

, context: HandlerContext, parameters: P): Promise { 21 | return action(p, context, parameters) 22 | .then(er => { 23 | this.assertions(p); 24 | return er; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/operations/review/reviewerHandler.test.ts: -------------------------------------------------------------------------------- 1 | import { SlackMessage } from "@atomist/slack-messages"; 2 | import * as assert from "power-assert"; 3 | import { 4 | fromListRepoFinder, 5 | fromListRepoLoader, 6 | } from "../../../lib/operations/common/fromProjectList"; 7 | import { BaseEditorOrReviewerParameters } from "../../../lib/operations/common/params/BaseEditorOrReviewerParameters"; 8 | import { SimpleRepoId } from "../../../lib/operations/common/RepoId"; 9 | import { reviewerHandler } from "../../../lib/operations/review/reviewerToCommand"; 10 | import { 11 | DefaultReviewComment, 12 | ReviewResult, 13 | } from "../../../lib/operations/review/ReviewResult"; 14 | import { InMemoryProject } from "../../../lib/project/mem/InMemoryProject"; 15 | 16 | describe("reviewerHandler", () => { 17 | 18 | const p = InMemoryProject.from(new SimpleRepoId("a", "b"), 19 | { path: "thing", content: "1" }); 20 | 21 | const rh = reviewerHandler(() => 22 | proj => Promise.resolve({ 23 | repoId: proj.id, 24 | comments: [new DefaultReviewComment("warn", "category", "bad", undefined)], 25 | }), 26 | BaseEditorOrReviewerParameters, "test", { 27 | repoFinder: fromListRepoFinder([p]), 28 | repoLoader: () => fromListRepoLoader([p]), 29 | }); 30 | 31 | it("should return ReviewResult structure", done => { 32 | const params = new BaseEditorOrReviewerParameters(); 33 | params.targets.repo = ".*"; 34 | (rh as any).handle(MockHandlerContext, params) 35 | .then(r => { 36 | const rr = r as ReviewResult; 37 | assert(!!rr); 38 | assert(rr.projectsReviewed === 1); 39 | assert(rr.projectReviews[0].comments.length === 1); 40 | done(); 41 | }).catch(done); 42 | }); 43 | 44 | }); 45 | 46 | const MockHandlerContext = { 47 | messageClient: { 48 | respond(msg: string | SlackMessage): Promise { 49 | return Promise.resolve(); 50 | }, 51 | }, 52 | graphClient: { 53 | executeMutationFromFile(file: string, variables?: any): Promise { 54 | return Promise.resolve({ createSlackChannel: [{ id: "stts" }] }); 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /test/operations/support/editorUtils.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | 3 | import { GitHubRepoRef } from "../../../lib/operations/common/GitHubRepoRef"; 4 | import { PullRequest } from "../../../lib/operations/edit/editModes"; 5 | import { toEditor } from "../../../lib/operations/edit/projectEditor"; 6 | import { 7 | editProjectUsingBranch, 8 | editProjectUsingPullRequest, 9 | } from "../../../lib/operations/support/editorUtils"; 10 | import { GitCommandGitProject } from "../../../lib/project/git/GitCommandGitProject"; 11 | import { Creds } from "../../credentials"; 12 | 13 | describe("editorUtils", () => { 14 | 15 | const NoOpEditor = toEditor(p => { 16 | return Promise.resolve(p); 17 | }); 18 | 19 | const thisRepo = new GitHubRepoRef("atomist", "automation-client"); 20 | 21 | it("doesn't attempt to commit without changes", async () => { 22 | const p = await GitCommandGitProject.cloned(Creds, thisRepo); 23 | const er = await editProjectUsingBranch(undefined, p, NoOpEditor, 24 | { branch: "dont-create-me-or-i-barf&&&####&&& we", message: "whocares" }); 25 | assert(!er.edited); 26 | await p.release(); 27 | }).timeout(10000); 28 | 29 | it("doesn't attempt to create PR without changes", async () => { 30 | const p = await GitCommandGitProject.cloned(Creds, thisRepo); 31 | const status = await p.gitStatus(); 32 | assert(status.isClean); 33 | const er = await editProjectUsingPullRequest(undefined, p, NoOpEditor, new PullRequest("x", "y")); 34 | assert(!er.edited); 35 | await p.release(); 36 | }).timeout(10000); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/operations/tagger/tagger.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { SimpleRepoId } from "../../../lib/operations/common/RepoId"; 3 | import { 4 | DefaultTaggerTags, 5 | Tagger, 6 | unifiedTagger, 7 | } from "../../../lib/operations/tagger/Tagger"; 8 | import { InMemoryProject } from "../../../lib/project/mem/InMemoryProject"; 9 | 10 | describe("tag unification", () => { 11 | 12 | it("should unify one", done => { 13 | const rr = new SimpleRepoId("a", "b"); 14 | const springTagger: Tagger = () => Promise.resolve(new DefaultTaggerTags(rr, ["spring"])); 15 | const unified: Tagger = unifiedTagger(springTagger); 16 | unified(InMemoryProject.from(undefined), undefined, undefined) 17 | .then(tags => { 18 | assert.deepEqual(tags.repoId, rr); 19 | assert.deepEqual(tags.tags, ["spring"]); 20 | done(); 21 | }).catch(done); 22 | }); 23 | 24 | it("should unify two", done => { 25 | const rr = new SimpleRepoId("a", "b"); 26 | const springTagger: Tagger = () => Promise.resolve(new DefaultTaggerTags(rr, ["spring"])); 27 | const kotlinTagger: Tagger = () => Promise.resolve(new DefaultTaggerTags(rr, ["kotlin"])); 28 | 29 | const unified: Tagger = unifiedTagger(springTagger, kotlinTagger); 30 | unified(InMemoryProject.from(undefined), undefined, undefined) 31 | .then(tags => { 32 | assert.deepEqual(tags.repoId, rr); 33 | assert.deepEqual(tags.tags, ["spring", "kotlin"]); 34 | done(); 35 | }).catch(done); 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /test/project/local/AbstractFile.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | 4 | import { InMemoryFile } from "../../../lib/project/mem/InMemoryFile"; 5 | 6 | describe("AbstractFile", () => { 7 | 8 | describe("extension", () => { 9 | 10 | it("should work with .js in root", () => { 11 | const f = new InMemoryFile("thing.js", ""); 12 | assert(f.extension === "js"); 13 | }); 14 | 15 | it("should work with .js in directory", () => { 16 | const f = new InMemoryFile("test/thing.js", ""); 17 | assert(f.extension === "js"); 18 | }); 19 | 20 | it("should work with .tar.gz in directory", () => { 21 | const f = new InMemoryFile("test/thing.tar.gz", ""); 22 | assert(f.extension === "gz"); 23 | }); 24 | 25 | it("should work with none", () => { 26 | const f = new InMemoryFile("test/thing", ""); 27 | assert(f.extension === "", `[${f.extension}]`); 28 | }); 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/project/support/AbstractProject.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { GitHubRepoRef } from "../../../lib/operations/common/GitHubRepoRef"; 3 | import { GitCommandGitProject } from "../../../lib/project/git/GitCommandGitProject"; 4 | import { spawnPromise } from "../../../lib/util/child_process"; 5 | 6 | describe("AbstractProject", () => { 7 | 8 | describe("globMatchesWithin", () => { 9 | 10 | it.skip("should match ts pattern in nested folder", async () => { 11 | const patterns = [ 12 | "*.{d.ts,js,ts}{,.map}", 13 | "!(node_modules)/**/*.{d.ts,js,ts}{,.map}", 14 | "lib/typings/types.ts", 15 | "git-info.json", 16 | ]; 17 | 18 | const p = await GitCommandGitProject.cloned(undefined, GitHubRepoRef.from({ 19 | owner: "atomist", 20 | repo: "yaml-updater", 21 | } as any)); 22 | 23 | await spawnPromise("npm", ["ci"], { cwd: p.baseDir }); 24 | 25 | const matches = await p.getFiles(patterns); 26 | 27 | assert(matches.length > 1); 28 | assert(!matches.some(m => m.path.startsWith("node_modules"))); 29 | 30 | }).timeout(20000); 31 | 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /test/project/util/diagnosticUtils.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | import { InMemoryProject } from "../../../lib/project/mem/InMemoryProject"; 4 | import { diagnosticDump } from "../../../lib/project/util/diagnosticUtils"; 5 | import { gatherFromFiles } from "../../../lib/project/util/projectUtils"; 6 | 7 | describe("diagnosticUtils", () => { 8 | 9 | it("gatherFromFiles", done => { 10 | const p = new InMemoryProject(); 11 | p.addFileSync("Thing", "1"); 12 | Promise.resolve(p) 13 | .then(diagnosticDump("thing")) 14 | .then(p1 => { 15 | assert(p === p1); 16 | done(); 17 | }).catch(done); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /test/project/util/sourceLocationUtils.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { InMemoryFile } from "../../../lib/project/mem/InMemoryFile"; 3 | import { toSourceLocation } from "../../../lib/project/util/sourceLocationUtils"; 4 | 5 | describe("sourceLocationUtils", () => { 6 | 7 | it("should survive undefined", () => { 8 | const pos = toSourceLocation("x", undefined, 0); 9 | assert(pos === undefined); 10 | }); 11 | 12 | it("should survive null", () => { 13 | // tslint:disable-next-line:no-null-keyword 14 | const pos = toSourceLocation("x", null, 0); 15 | assert(pos === undefined); 16 | }); 17 | 18 | it("should survive negative value", () => { 19 | const pos = toSourceLocation("x", "this is valid", -474); 20 | assert(pos === undefined); 21 | }); 22 | 23 | it("should survive out of range value", () => { 24 | const pos = toSourceLocation("x", "this is valid", +474); 25 | assert(pos === undefined); 26 | }); 27 | 28 | it("should return start", () => { 29 | const path = "whatever"; 30 | const pos = toSourceLocation(path, "this is valid", 0); 31 | assert.deepEqual(pos, { lineFrom1: 1, columnFrom1: 1, offset: 0, path }); 32 | }); 33 | 34 | it("should return second line", () => { 35 | const path = "this/is/good"; 36 | const pos = toSourceLocation(path, "t\nhis is valid", 2); 37 | assert.deepEqual(pos, { lineFrom1: 2, columnFrom1: 1, offset: 2, path }); 38 | }); 39 | 40 | it("should handle blank line", () => { 41 | const f = new InMemoryFile("this/is/good", ""); 42 | const pos = toSourceLocation(f, "t\n\nhis is valid", 3); 43 | assert.deepEqual(pos, { lineFrom1: 3, columnFrom1: 1, offset: 3, path: f.path }); 44 | }); 45 | 46 | it("should handle blank lines", () => { 47 | const f = new InMemoryFile("this/is/good", ""); 48 | const pos = toSourceLocation(f, "t\n\n\nhis is valid", 5); 49 | assert.deepEqual(pos, { lineFrom1: 4, columnFrom1: 2, offset: 5, path: f.path }); 50 | }); 51 | 52 | it("should handle windows format", () => { 53 | const f = new InMemoryFile("this/is/good", ""); 54 | const pos = toSourceLocation(f, "t\r\n\r\n\r\nhis is valid", 8); 55 | assert.deepEqual(pos, { lineFrom1: 4, columnFrom1: 2, offset: 8, path: f.path }); 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /test/project/utils.ts: -------------------------------------------------------------------------------- 1 | import * as tmp from "tmp-promise"; 2 | import { ScriptedFlushable } from "../../lib/internal/common/Flushable"; 3 | import { RepoRef } from "../../lib/operations/common/RepoId"; 4 | import { LocalProject } from "../../lib/project/local/LocalProject"; 5 | import { NodeFsLocalProject } from "../../lib/project/local/NodeFsLocalProject"; 6 | 7 | tmp.setGracefulCleanup(); 8 | 9 | export function tempProject(id: RepoRef = { 10 | owner: "dummyOwner", 11 | repo: "dummyRepo", 12 | url: "", 13 | }): LocalProject & ScriptedFlushable { 14 | const dir = tmp.dirSync({ unsafeCleanup: true }); 15 | return new NodeFsLocalProject(id, dir.name, async () => dir.removeCallback()) as any as LocalProject & ScriptedFlushable; 16 | } 17 | -------------------------------------------------------------------------------- /test/scan.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { loadConfiguration } from "../lib/configuration"; 3 | 4 | describe("scan", () => { 5 | // This test only works when run from the IDE; with mocha the JS files are not visible 6 | it.skip("should find test handlers", async () => { 7 | const configuration = await loadConfiguration(); 8 | assert(configuration.commands.length === 3); 9 | assert(configuration.events.length === 1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/spi/message/MessageClient.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | import { 3 | commandName, 4 | mergeParameters, 5 | } from "../../../lib/spi/message/MessageClient"; 6 | import { HelloWorld } from "../../command/HelloWorld"; 7 | import { PlainHelloWorld } from "../../command/PlainHelloWorld"; 8 | 9 | describe("MessageClient", () => { 10 | 11 | describe("commandName", () => { 12 | 13 | it("extract commandName from string", () => { 14 | assert.equal(commandName("HelloWorld"), "HelloWorld"); 15 | }); 16 | 17 | it("extract commandName from command handler instance", () => { 18 | assert.equal(commandName(new HelloWorld()), "HelloWorld"); 19 | }); 20 | 21 | it("extract commandName from plain command handler instance", () => { 22 | assert.equal(commandName(new PlainHelloWorld()), "PlainHelloWorld"); 23 | }); 24 | 25 | it("extract commandName from command handler constructor", () => { 26 | assert.equal(commandName(HelloWorld), "HelloWorld"); 27 | }); 28 | 29 | it("extract commandName from plain command handler constructor", () => { 30 | assert.equal(commandName(PlainHelloWorld), "PlainHelloWorld"); 31 | }); 32 | }); 33 | 34 | describe("mergeParameters", () => { 35 | 36 | it("don't extract parameters from string", () => { 37 | assert.deepEqual(mergeParameters("HelloWorld", {}), {}); 38 | }); 39 | 40 | it("don't extract parameters from constuctor", () => { 41 | assert.deepEqual(mergeParameters(HelloWorld, {}), {}); 42 | }); 43 | 44 | it("extract parameters from instance", () => { 45 | const handler = new HelloWorld(); 46 | handler.name = "cd"; 47 | handler.userToken = "token_bla"; 48 | assert.deepEqual(mergeParameters(handler, {}), { name: "cd", userToken: "token_bla" }); 49 | }); 50 | 51 | it("overwrite parameters from instance with explicit parameters", () => { 52 | const handler = new HelloWorld(); 53 | handler.name = "cd"; 54 | handler.userToken = "token_bla"; 55 | assert.deepEqual(mergeParameters(handler, { name: "dd" }), 56 | { name: "dd", userToken: "token_bla" }); 57 | }); 58 | 59 | it("overwrite nested parameters from instance with explicit parameters", () => { 60 | const handler = new HelloWorld(); 61 | handler.name = "cd"; 62 | handler.userToken = "token_bla"; 63 | (handler as any).foo = { bar: "bla" }; 64 | assert.deepEqual(mergeParameters(handler, { name: "dd" }), 65 | { "name": "dd", "userToken": "token_bla", "foo.bar": "bla" }); 66 | }); 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /test/tree/ast/FileParserRegistry.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "power-assert"; 2 | 3 | import { FileParser } from "../../../lib/tree/ast/FileParser"; 4 | import { DefaultFileParserRegistry } from "../../../lib/tree/ast/FileParserRegistry"; 5 | 6 | describe("FileParserRegistry", () => { 7 | 8 | it("should find parser", () => { 9 | const fp: FileParser = { 10 | rootName: "hotdog", 11 | toAst: f => { throw new Error(); }, 12 | }; 13 | const fpr = new DefaultFileParserRegistry().addParser(fp); 14 | assert.equal(fpr.parserFor("/hotdog/this"), fp); 15 | }); 16 | 17 | it("should not find parser", () => { 18 | const fp: FileParser = { 19 | rootName: "hotdog", 20 | toAst: f => { throw new Error(); }, 21 | }; 22 | const fpr = new DefaultFileParserRegistry().addParser(fp); 23 | assert.equal(fpr.parserFor("/nothotdog/this"), undefined); 24 | }); 25 | 26 | it("should pass validation", () => { 27 | const fp: FileParser = { 28 | rootName: "hotdog", 29 | toAst: f => { throw new Error(); }, 30 | validate: pex => { /* Do nothing */ }, 31 | }; 32 | const fpr = new DefaultFileParserRegistry().addParser(fp); 33 | fpr.parserFor("/hotdog/this"); 34 | }); 35 | 36 | it("should spot invalid path expression", () => { 37 | const fp: FileParser = { 38 | rootName: "hotdog", 39 | toAst: f => { throw new Error(); }, 40 | validate: pex => { throw new Error("invalid"); }, 41 | }; 42 | const fpr = new DefaultFileParserRegistry().addParser(fp); 43 | assert.throws(() => fpr.parserFor("/hotdog/this")); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/tree/ast/typescript/javaScriptFileParserProject.test.ts: -------------------------------------------------------------------------------- 1 | import * as appRoot from "app-root-path"; 2 | import * as assert from "power-assert"; 3 | import { NodeFsLocalProject } from "../../../../lib/project/local/NodeFsLocalProject"; 4 | import { 5 | findValues, 6 | matches, 7 | } from "../../../../lib/tree/ast/astUtils"; 8 | import { 9 | TypeScriptES6FileParser, 10 | TypeScriptFileParser, 11 | } from "../../../../lib/tree/ast/typescript/TypeScriptFileParser"; 12 | 13 | /** 14 | * Parse sources in this project 15 | */ 16 | describe("TypeScriptFileParser real project parsing: JavaScript", () => { 17 | 18 | const thisProject = new NodeFsLocalProject("automation-client", appRoot.path, () => Promise.resolve()); 19 | 20 | it("should parse sources from project and use a path expression to find values", async () => { 21 | const matchResults = await matches(thisProject, { 22 | parseWith: TypeScriptES6FileParser, 23 | globPatterns: "lib/tree/ast/typescript/*.js", 24 | pathExpression: "//ClassDeclaration/Identifier", 25 | }); 26 | assert(matchResults.map(m => m.$value).includes(TypeScriptFileParser.name)); 27 | }).timeout(15000); 28 | 29 | it("should parse sources from project and use a path expression to find values using convenience method", async () => { 30 | const values = await findValues(thisProject, TypeScriptES6FileParser, "lib/tree/ast/typescript/*.js", "//ClassDeclaration/Identifier"); 31 | assert(values.includes(TypeScriptFileParser.name)); 32 | }).timeout(15000); 33 | 34 | it("should parse sources from project and find functions", async () => { 35 | const values = await findValues(thisProject, TypeScriptES6FileParser, "lib/tree/ast/**/*.js", "//FunctionDeclaration/Identifier"); 36 | assert(values.length > 2); 37 | }).timeout(15000); 38 | 39 | }); 40 | -------------------------------------------------------------------------------- /test/tree/ast/typescript/typeScriptFileParserProject.test.ts: -------------------------------------------------------------------------------- 1 | import * as appRoot from "app-root-path"; 2 | import * as assert from "power-assert"; 3 | import { NodeFsLocalProject } from "../../../../lib/project/local/NodeFsLocalProject"; 4 | import { 5 | findValues, 6 | matches, 7 | } from "../../../../lib/tree/ast/astUtils"; 8 | import { 9 | TypeScriptES6FileParser, 10 | TypeScriptFileParser, 11 | } from "../../../../lib/tree/ast/typescript/TypeScriptFileParser"; 12 | 13 | /** 14 | * Parse sources in this project 15 | */ 16 | describe("TypeScriptFileParser real project parsing: TypeScript", () => { 17 | 18 | const thisProject = new NodeFsLocalProject("automation-client", appRoot.path, () => Promise.resolve()); 19 | 20 | it("should parse sources from project and use a path expression to find values", async () => { 21 | const matchResults = await matches(thisProject, { 22 | parseWith: TypeScriptES6FileParser, 23 | globPatterns: "lib/tree/ast/typescript/*Parser.ts", 24 | pathExpression: "//ClassDeclaration/Identifier", 25 | }); 26 | assert.deepEqual(matchResults.map(m => m.$value), ["TypeScriptFileParser", "TypeScriptAstNodeTreeNode"]); 27 | }).timeout(15000); 28 | 29 | it("should parse sources from project and use a path expression to find values using convenience method", async () => { 30 | const values = await findValues(thisProject, TypeScriptES6FileParser, "lib/tree/ast/typescript/*Parser.ts", "//ClassDeclaration/Identifier"); 31 | assert.deepEqual(values, ["TypeScriptFileParser", "TypeScriptAstNodeTreeNode"]); 32 | }).timeout(15000); 33 | 34 | it("should parse sources from project and find functions", async () => { 35 | const values = await findValues(thisProject, TypeScriptES6FileParser, "lib/tree/ast/**/*.ts", "//FunctionDeclaration/Identifier"); 36 | assert(values.length > 2); 37 | }).timeout(15000); 38 | 39 | it("should parse sources from project and find exported functions", async () => { 40 | const values = await matches(thisProject, { 41 | parseWith: TypeScriptES6FileParser, 42 | globPatterns: "lib/tree/ast/**/*.ts", 43 | pathExpression: "//FunctionDeclaration[//ExportKeyword]//Identifier", 44 | }); 45 | assert(values.length > 2); 46 | }).timeout(15000); 47 | 48 | it("should find all exported functions in project", async () => { 49 | const values = await findValues(thisProject, TypeScriptES6FileParser, "lib/project/*.ts", 50 | "//FunctionDeclaration[//ExportKeyword]//Identifier"); 51 | assert(values.length > 5); 52 | }).timeout(15000); 53 | 54 | }); 55 | -------------------------------------------------------------------------------- /test/util/pool.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2018 Atomist, Inc. 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import * as assert from "power-assert"; 19 | import { executeAll } from "../../lib/util/pool"; 20 | 21 | describe("pool", () => { 22 | 23 | describe("executeAll", () => { 24 | 25 | it("should preserve order", async () => { 26 | const results = await executeAll([() => { 27 | return new Promise(resolve => { 28 | setTimeout(() => resolve("1"), 1000); 29 | }); 30 | }, () => { 31 | return new Promise(resolve => { 32 | setTimeout(() => resolve("2"), 500); 33 | }); 34 | }, () => { 35 | return new Promise(resolve => { 36 | setTimeout(() => resolve("3"), 100); 37 | }); 38 | }], 1); 39 | assert.deepStrictEqual(results, ["1", "2", "3"]); 40 | }); 41 | 42 | it("should reject with results and errors", async () => { 43 | try { 44 | await executeAll([() => { 45 | return new Promise(resolve => { 46 | setTimeout(() => resolve("1"), 1000); 47 | }); 48 | }, () => { 49 | return new Promise((resolve, reject) => { 50 | reject(new Error("2")); 51 | }); 52 | }, () => { 53 | return new Promise(resolve => { 54 | setTimeout(() => resolve("3"), 100); 55 | }); 56 | }], 1); 57 | } catch (err) { 58 | assert.strictEqual(err.message, "2"); 59 | } 60 | }); 61 | 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /test/util/safeStringify.test.ts: -------------------------------------------------------------------------------- 1 | import * as stringify from "json-stringify-safe"; 2 | 3 | import * as assert from "power-assert"; 4 | 5 | describe("Safe Stringify", () => { 6 | 7 | it("should print noncircular as usual", () => { 8 | const inputs = ["A", undefined, "", 4, 9.6, {}, { a: "yes" }, [4, 2]]; 9 | inputs.forEach(i => { 10 | assert(stringify(i) === stringify(i), "weird result on " + i); 11 | }); 12 | }); 13 | 14 | it("should space things out", () => { 15 | const nested = { a: { b: 1, c: 2 } }; 16 | 17 | const result = stringify(nested, undefined, 7); 18 | 19 | assert(stringify(nested, undefined, 7) === result, 20 | result); 21 | }); 22 | 23 | it("should not print noncircular", () => { 24 | const circle: any = {}; 25 | circle.me = circle; 26 | 27 | assert.throws(() => 28 | JSON.stringify(circle), "this test is useless if that doesn't throw"); 29 | 30 | assert.doesNotThrow(() => stringify(circle)); // don't throw 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/util/toFactory.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from "power-assert"; 3 | import { toFactory } from "../../lib/util/constructionUtils"; 4 | import { AddAtomistSpringAgent } from "../internal/invoker/TestHandlers"; 5 | 6 | describe("toFactory", () => { 7 | 8 | it("should work from factory", () => { 9 | const chm = toFactory(() => new AddAtomistSpringAgent()); 10 | assert(chm().handle); 11 | }); 12 | 13 | it("should work from constructor", () => { 14 | const chm = toFactory(AddAtomistSpringAgent); 15 | assert(chm().handle); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "newLine": "LF", 4 | "target": "ES2017", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "jsx": "React", 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "rootDir": ".", 12 | "lib": [ 13 | "DOM", 14 | "ES2017", 15 | "DOM.Iterable", 16 | "ScriptHost", 17 | "esnext.asynciterable" 18 | ], 19 | "strict": true, 20 | "noImplicitAny": false, 21 | "noImplicitThis": false, 22 | "strictNullChecks": false, 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | }, 26 | "exclude": [ 27 | "assets", 28 | "build", 29 | "doc", 30 | "legal", 31 | "log", 32 | "node_modules", 33 | ".#*" 34 | ], 35 | "compileOnSave": true, 36 | "buildOnSave": false, 37 | "atom": { 38 | "rewriteTsconfig": false 39 | } 40 | } 41 | --------------------------------------------------------------------------------