├── .eslintignore ├── .hound.yml ├── .commit-template ├── .github ├── FUNDING.yml └── workflows │ ├── tests.yml │ ├── release.yml │ └── announce-it.yaml ├── src ├── index.ts ├── announce-it-cli.test.ts ├── announce-it-cli.ts ├── __snapshots__ │ ├── read-package-details.test.ts.snap │ ├── announce-it-cli-utils.test.ts.snap │ └── announce-it.test.ts.snap ├── read-package-details.ts ├── read-package-details.test.ts ├── announce-it-cli-utils.ts ├── announce-it-cli-utils.test.ts ├── announce-it.ts └── announce-it.test.ts ├── .env.example ├── bin └── announce-it-cli.js ├── .vscode ├── extensions.json └── settings.json ├── types ├── manakin.d.ts └── twitter-lite.d.ts ├── commitlint.config.js ├── examples └── as-a-node-module │ ├── package.json │ └── index.ts ├── jest.config.js ├── .eslintrc.js ├── .all-contributorsrc ├── LICENSE ├── .gitignore ├── package.json ├── tsconfig.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .eslintrc.js -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | tslint: 2 | config_file: tslint.json 3 | -------------------------------------------------------------------------------- /.commit-template: -------------------------------------------------------------------------------- 1 | type(scope): subject 2 | 3 | description -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - https://paypal.me/thatkookooguy?locale.x=en_US -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './announce-it'; 2 | export * from './read-package-details'; 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Twitter auth 2 | CONSUMER_KEY= 3 | CONSUMER_SECRET= 4 | ACCESS_TOKEN_KEY= 5 | ACCESS_TOKEN_SECRET= 6 | -------------------------------------------------------------------------------- /bin/announce-it-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // add console colors 3 | require('manakin').global; 4 | 5 | const cli = require('../lib/announce-it-cli'); 6 | 7 | cli.run(); 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "rbbit.typescript-hero", 4 | "orta.vscode-jest", 5 | "johnpapa.vscode-peacock", 6 | "coenraads.bracket-pair-colorizer" 7 | ] 8 | } -------------------------------------------------------------------------------- /src/announce-it-cli.test.ts: -------------------------------------------------------------------------------- 1 | describe('announce-it-cli', () => { 2 | it.todo('should get parameters from either env or command line'); 3 | 4 | it.todo('should run the cli if everything is set-up'); 5 | 6 | it.todo('should throw meaningful error if something went wrong'); 7 | }); 8 | -------------------------------------------------------------------------------- /types/manakin.d.ts: -------------------------------------------------------------------------------- 1 | export = index; 2 | declare const index: { 3 | error: Function; 4 | global: any; 5 | info: Function; 6 | local: any; 7 | log: Function; 8 | ok: Function; 9 | setBright: Function; 10 | success: Function; 11 | warn: Function; 12 | write: Function; 13 | }; 14 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ '@commitlint/config-angular' ], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', [ 7 | 'build', 8 | 'chore', 9 | 'ci', 10 | 'docs', 11 | 'feat', 12 | 'fix', 13 | 'perf', 14 | 'refactor', 15 | 'revert', 16 | 'style', 17 | 'test' 18 | ] 19 | ] 20 | } 21 | }; -------------------------------------------------------------------------------- /examples/as-a-node-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "as-a-node-module", 3 | "version": "1.0.0", 4 | "description": "this is an example for using announce it as a node module", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "k1b1b0t", 10 | "license": "ISC", 11 | "announcements": { 12 | "tweet": "Version <%= version %> of <%= package %> is live! check it out <%=homepage %>" 13 | }, 14 | "dependencies": { 15 | "@kibibit/announce-it": "file:../.." 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | coverageReporters: ["json", "lcov", "text", "clover", "html"], 5 | modulePathIgnorePatterns: ["/lib/"], 6 | collectCoverageFrom: [ 7 | "src/**/*.ts", 8 | "!src/**/index.ts", 9 | "!src/announce-it-cli.ts" 10 | ], 11 | watchPathIgnorePatterns: [ 12 | ".*test-results.*\\.js" 13 | ], 14 | reporters: ["default", "jest-stare", "jest-github-actions-reporter"], 15 | testResultsProcessor: "./node_modules/jest-stare", 16 | "testLocationInResults": true 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Run Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | pull_request: 9 | branches: 10 | - master 11 | - next 12 | 13 | jobs: 14 | build: 15 | name: Run Tests Job 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - run: npm ci 20 | - run: npm run build --if-present 21 | - run: npm test 22 | # - name: Coveralls 23 | # uses: coverallsapp/github-action@master 24 | # with: 25 | # github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /examples/as-a-node-module/index.ts: -------------------------------------------------------------------------------- 1 | import { KbAnnounceIt, PackageDetails } from '@kibibit/announce-it'; 2 | 3 | const announceIt = new KbAnnounceIt({ 4 | accessTokenKey: 'TWITTER_ACCESS_KEY', 5 | accessTokenSecret: 'TWITTER_ACCESS_SECRET', 6 | consumerKey: 'TWITTER_CONSUMER_KEY', 7 | consumerSecret: 'TWITTER_CONSUMER_SECRET' 8 | }); 9 | 10 | const myPackage: PackageDetails = require('./package'); 11 | 12 | // get generated tweet 13 | const tweet: string = announceIt.generateTweet(myPackage); 14 | 15 | console.log('going to publish this tweet!', tweet); 16 | 17 | // publish tweet to twitter 18 | announceIt.announceRelease(myPackage); 19 | -------------------------------------------------------------------------------- /src/announce-it-cli.ts: -------------------------------------------------------------------------------- 1 | import nconf from 'nconf'; 2 | 3 | import { AnnounceItCli } from './announce-it-cli-utils'; 4 | 5 | export async function run(): Promise { 6 | nconf.argv() 7 | .env(); 8 | 9 | const announceItCli = new AnnounceItCli(); 10 | 11 | const parameters = nconf.get(); 12 | const cwd = process.cwd(); 13 | 14 | return announceItCli.areVariablesDefined(parameters) 15 | .then(() => announceItCli.findRoot(cwd)) 16 | .then((root) => announceItCli.runAnnounceItCli(root, parameters)) 17 | .catch((error: Error) => { 18 | console.error('ERROR: Something went wrong'); 19 | console.error(error); 20 | 21 | throw error; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /types/twitter-lite.d.ts: -------------------------------------------------------------------------------- 1 | export = Twitter; 2 | 3 | interface TwitterOptions { 4 | subdomain: string; 5 | consumer_key: string; 6 | consumer_secret: string; 7 | access_token_key: string; 8 | access_token_secret: string; 9 | } 10 | 11 | declare class Twitter { 12 | constructor(options: TwitterOptions); 13 | 14 | authType: any; 15 | client: any; 16 | token: any; 17 | url: any; 18 | oauth: any; 19 | config: any; 20 | get(e: any, t: any): Promise; 21 | getAccessToken(e: any): any; 22 | getBearerToken(): any; 23 | getRequestToken(e: any): any; 24 | post(e: any, t: any): Promise; 25 | stream(e: any, t: any): any; 26 | } 27 | 28 | declare interface KbTwitter { 29 | 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [ "Run Tests" ] 6 | branches: [ master, next ] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 12 23 | - name: Install dependencies 24 | run: | 25 | npm ci 26 | npm install 27 | npm run build 28 | - name: Release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: npm run semantic-release -------------------------------------------------------------------------------- /src/__snapshots__/read-package-details.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`readPackageDetails should return the package details 1`] = ` 4 | Object { 5 | "announcements": Object { 6 | "tweet": "nice!", 7 | }, 8 | "author": "test@test.com", 9 | "description": "package description", 10 | "homepage": "pizza.com", 11 | "name": "package-name", 12 | "release": Object { 13 | "branches": Array [ 14 | "TEST", 15 | ], 16 | }, 17 | "repository": Object { 18 | "type": "", 19 | "url": "", 20 | }, 21 | "version": "1.0.0", 22 | } 23 | `; 24 | 25 | exports[`readPackageDetails should throw error if missing tweet from package.json 1`] = `"no tweet template found. please see the readme for more details"`; 26 | 27 | exports[`readPackageDetails should throw error if reading package.json failed 1`] = `"failed reading json file"`; 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin', 'jest'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended' 10 | ], 11 | root: true, 12 | env: { 13 | "node": true, 14 | "es2021": true, 15 | "jest/globals": true 16 | }, 17 | rules: { 18 | "@typescript-eslint/naming-convention": [ 19 | "error", 20 | { 21 | "selector": "interface", 22 | "format": ["PascalCase"], 23 | "custom": { 24 | "regex": "^I[A-Z]", 25 | "match": true 26 | } 27 | } 28 | ], 29 | '@typescript-eslint/explicit-function-return-type': 'off', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | }, 33 | }; -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "announce-it", 3 | "projectOwner": "kibibit", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "contributors": [ 12 | { 13 | "login": "ZimGil", 14 | "name": "Gil Tichon", 15 | "avatar_url": "https://avatars3.githubusercontent.com/u/39461857?v=4", 16 | "profile": "https://github.com/ZimGil", 17 | "contributions": [ 18 | "infra", 19 | "code", 20 | "projectManagement" 21 | ] 22 | }, 23 | { 24 | "login": "Thatkookooguy", 25 | "name": "Neil Kalman", 26 | "avatar_url": "https://avatars3.githubusercontent.com/u/10427304?v=4", 27 | "profile": "http://thatkookooguy.kibibit.io", 28 | "contributions": [ 29 | "infra", 30 | "ideas" 31 | ] 32 | } 33 | ], 34 | "contributorsPerLine": 7 35 | } 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "peacock.color": "#ffa63f", 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "editor.detectIndentation": false, 6 | "editor.rulers": [80], 7 | "editor.matchBrackets": "always", 8 | // Bracket Pair Colorizer 9 | "bracketPairColorizer.colorMode": "Consecutive", 10 | "bracketPairColorizer.forceUniqueOpeningColor": true, 11 | "bracketPairColorizer.showBracketsInGutter": true, 12 | "window.title": "${activeEditorShort}${separator}${rootName} [kibibit]", 13 | "typescriptHero.imports.stringQuoteStyle": "'", 14 | "typescriptHero.imports.grouping": [ 15 | "Plains", 16 | "Modules", 17 | "/^@kibibit/", 18 | "/^@kb-/", 19 | "Workspace" 20 | 21 | ], 22 | "typescriptHero.imports.organizeOnSave": true, 23 | "typescriptHero.imports.multiLineTrailingComma": false, 24 | "editor.codeActionsOnSave": { 25 | "source.fixAll.eslint": true 26 | } 27 | } -------------------------------------------------------------------------------- /src/__snapshots__/announce-it-cli-utils.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AnnounceItCli areVariablesDefined should reject if at least 1 required variable is missing 1`] = `"These Variables are required: TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN_KEY, TWITTER_ACCESS_TOKEN_SECRET, branch"`; 4 | 5 | exports[`AnnounceItCli areVariablesDefined should reject if at least 1 required variable is missing 2`] = `"These Variables are required: TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, TWITTER_ACCESS_TOKEN_KEY, TWITTER_ACCESS_TOKEN_SECRET, branch"`; 6 | 7 | exports[`AnnounceItCli areVariablesDefined should resolve if all required variables exists 1`] = `undefined`; 8 | 9 | exports[`AnnounceItCli findRoot should return the project root folder if ran inside npm project 1`] = `"project-root"`; 10 | 11 | exports[`AnnounceItCli findRoot should throw error if not inside npm project 1`] = `"couldn't find npm project root: failure (test mock)"`; 12 | -------------------------------------------------------------------------------- /src/read-package-details.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import { get, isString } from 'lodash'; 3 | import { IBranchObject } from './announce-it'; 4 | 5 | export interface IPackageDetails { 6 | name: string; 7 | version: string; 8 | description: string; 9 | author: string; 10 | homepage: string; 11 | repository: { 12 | type: string; 13 | url: string; 14 | }; 15 | announcements: { 16 | tweet: string; 17 | includeUnstable?: boolean; 18 | }; 19 | release: { 20 | branches: Array; 21 | } 22 | } 23 | 24 | export function readPackageDetails(root: string): Promise { 25 | return fs.readJson(`${ root }/package.json`) 26 | .then((packageDetails: Partial) => { 27 | if (!isString(get(packageDetails, 'announcements.tweet'))) { 28 | throw new Error('no tweet template found. please see the readme for more details'); 29 | } 30 | 31 | // here, we KNOW the package has the announcement template string 32 | return packageDetails as IPackageDetails; 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/announce-it.yaml: -------------------------------------------------------------------------------- 1 | name: Announce Release 2 | 3 | on: 4 | workflow_run: 5 | workflows: [ "Release" ] 6 | branches: [ master ] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | announce: 12 | runs-on: ubuntu-18.04 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 12 22 | - name: Install dependencies 23 | run: | 24 | npm ci 25 | npm install 26 | npm run build 27 | - name: announce-it 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | TWITTER_ACCESS_TOKEN_KEY: ${{ secrets.TWITTER_ACCESS_TOKEN_KEY }} 32 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 33 | TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }} 34 | TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }} 35 | run: npm run start:ci -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 kibibit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dist files 2 | lib/ 3 | 4 | # Environment variables 5 | .env 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | test-results 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | 67 | # next.js build output 68 | .next 69 | -------------------------------------------------------------------------------- /src/read-package-details.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra'; 2 | import { assign } from 'lodash'; 3 | 4 | import { IPackageDetails, readPackageDetails } from './read-package-details'; 5 | 6 | const packageDetails: IPackageDetails = { 7 | name: 'package-name', 8 | description: 'package description', 9 | version: '1.0.0', 10 | author: 'test@test.com', 11 | homepage: 'pizza.com', 12 | repository: { 13 | type: '', 14 | url: '' 15 | }, 16 | announcements: { 17 | tweet: 'nice!' 18 | }, 19 | release: { 20 | branches: [ 'TEST' ] 21 | } 22 | }; 23 | 24 | jest.mock('fs-extra'); 25 | const fsMocked = fs as jest.Mocked; 26 | 27 | describe('readPackageDetails', () => { 28 | it('should return the package details', () => { 29 | fsMocked.readJson.mockResolvedValue(packageDetails); 30 | return expect(readPackageDetails('rootFolder')).resolves.toMatchSnapshot(); 31 | }); 32 | 33 | it('should throw error if reading package.json failed', () => { 34 | fsMocked.readJson.mockRejectedValue(new Error('failed reading json file')); 35 | return expect(readPackageDetails('rootFolder')).rejects.toThrowErrorMatchingSnapshot(); 36 | }); 37 | 38 | it('should throw error if missing tweet from package.json', () => { 39 | const missingDetails: Partial = assign({}, packageDetails); 40 | missingDetails.announcements = undefined; 41 | fsMocked.readJson.mockResolvedValue(missingDetails); 42 | 43 | return expect(readPackageDetails('rootFolder')).rejects.toThrowErrorMatchingSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/announce-it-cli-utils.ts: -------------------------------------------------------------------------------- 1 | import findRoot from 'find-root'; 2 | import { every, isString } from 'lodash'; 3 | 4 | import { KbAnnounceIt } from './announce-it'; 5 | import { readPackageDetails } from './read-package-details'; 6 | 7 | export class AnnounceItCli { 8 | areVariablesDefined(env: NodeJS.ProcessEnv): Promise { 9 | const variables = [ 10 | 'TWITTER_CONSUMER_KEY', 11 | 'TWITTER_CONSUMER_SECRET', 12 | 'TWITTER_ACCESS_TOKEN_KEY', 13 | 'TWITTER_ACCESS_TOKEN_SECRET', 14 | 'branch' 15 | ]; 16 | 17 | const areEnvVariablesDefined = every(variables, (varName) => { 18 | return isString(env[ varName ]); 19 | }); 20 | 21 | if (!areEnvVariablesDefined) { 22 | return Promise.reject( 23 | new Error(`These Variables are required: ${ variables.join(', ') }`) 24 | ); 25 | } 26 | 27 | return Promise.resolve(); 28 | } 29 | 30 | findRoot(folder: string): Promise { 31 | return new Promise((resolve, reject) => { 32 | try { 33 | const root = findRoot(folder); 34 | 35 | resolve(root); 36 | } catch (err) { 37 | reject(new Error(`couldn't find npm project root: ${ err.message || err }`)); 38 | } 39 | }); 40 | } 41 | 42 | runAnnounceItCli(folder: string, env: NodeJS.ProcessEnv): Promise { 43 | return this.findRoot(folder) 44 | .then((root) => readPackageDetails(root)) 45 | .then((packageDetails) => { 46 | const announceIt = new KbAnnounceIt({ 47 | consumerKey: env.TWITTER_CONSUMER_KEY as string, 48 | consumerSecret: env.TWITTER_CONSUMER_SECRET as string, 49 | accessTokenKey: env.TWITTER_ACCESS_TOKEN_KEY as string, 50 | accessTokenSecret: env.TWITTER_ACCESS_TOKEN_SECRET as string, 51 | branch: env.branch as string 52 | }); 53 | 54 | return announceIt.announceRelease(packageDetails); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/announce-it-cli-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { AnnounceItCli } from './announce-it-cli-utils'; 2 | 3 | jest.mock('find-root', () => (folder: string) => { 4 | if (folder === 'test-failure') { 5 | throw new Error('failure (test mock)'); 6 | } 7 | 8 | return 'project-root'; 9 | }); 10 | 11 | describe('AnnounceItCli', () => { 12 | const announceItCli = new AnnounceItCli(); 13 | 14 | describe('areVariablesDefined', () => { 15 | it('should resolve if all required variables exists', () => { 16 | return expect(announceItCli.areVariablesDefined({ 17 | TWITTER_CONSUMER_KEY: 'TWITTER_CONSUMER_KEY', 18 | TWITTER_CONSUMER_SECRET: 'TWITTER_CONSUMER_SECRET', 19 | TWITTER_ACCESS_TOKEN_KEY: 'TWITTER_ACCESS_TOKEN_KEY', 20 | TWITTER_ACCESS_TOKEN_SECRET: 'TWITTER_ACCESS_TOKEN_SECRET', 21 | branch: 'TEST' 22 | })).resolves.toMatchSnapshot(); 23 | }); 24 | it('should reject if at least 1 required variable is missing', () => { 25 | return expect(announceItCli.areVariablesDefined({ 26 | TWITTER_CONSUMER_KEY: 'TWITTER_CONSUMER_KEY', 27 | TWITTER_CONSUMER_SECRET: 'TWITTER_CONSUMER_SECRET', 28 | TWITTER_ACCESS_TOKEN_SECRET: 'TWITTER_ACCESS_TOKEN_SECRET' 29 | })) 30 | .rejects.toThrowErrorMatchingSnapshot() 31 | .then(() => expect(announceItCli.areVariablesDefined({})) 32 | .rejects.toThrowErrorMatchingSnapshot()); 33 | }); 34 | }); 35 | 36 | describe('findRoot', () => { 37 | it('should return the project root folder if ran inside npm project', () => { 38 | return expect(announceItCli.findRoot('test-folder')).resolves.toMatchSnapshot(); 39 | }); 40 | it('should throw error if not inside npm project', () => { 41 | return expect(announceItCli.findRoot('test-failure')).rejects.toThrowErrorMatchingSnapshot(); 42 | }); 43 | }); 44 | 45 | describe('runAnnounceItCli', () => { 46 | it.todo('should run the entire flow on correct input'); 47 | it.todo('should throw error on incorrect input'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/__snapshots__/announce-it.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`KbAnnounceIt should create instance when given correct parameters 1`] = ` 4 | KbAnnounceIt { 5 | "branchName": "TEST", 6 | "client": Object { 7 | "get": [Function], 8 | "post": [Function], 9 | }, 10 | } 11 | `; 12 | 13 | exports[`KbAnnounceIt should throw error when missing options 1`] = `"ERROR: missing required options"`; 14 | 15 | exports[`KbAnnounceIt should throw error when missing options 2`] = `"ERROR: missing required options"`; 16 | 17 | exports[`KbAnnounceIt should throw error when missing options 3`] = `"ERROR: missing required options"`; 18 | 19 | exports[`KbAnnounceIt should throw error when missing options 4`] = `"ERROR: missing required options"`; 20 | 21 | exports[`KbAnnounceIt should throw error when missing options 5`] = `"ERROR: missing required options"`; 22 | 23 | exports[`kbAnnounceIt.announceRelease should post to twitter when stable release 1`] = `"test-template"`; 24 | 25 | exports[`kbAnnounceIt.announceRelease should post to twitter when unstable release and mentioned in packageDetails 1`] = `"test-template"`; 26 | 27 | exports[`kbAnnounceIt.announceRelease should post to twitter when unstable release and mentioned in packageDetails as stable 1`] = `"test-template"`; 28 | 29 | exports[`kbAnnounceIt.announceRelease should throw an error when unstable release and mentioned in packageDetails as unstable 1`] = `"Not a stable release"`; 30 | 31 | exports[`kbAnnounceIt.announceRelease should throw an error when unstable release and not mentioned in packageDetails 1`] = `"Not a stable release"`; 32 | 33 | exports[`kbAnnounceIt.announceRelease should throw error when twitter get credentials throws an error 1`] = `"Twitter Get Error"`; 34 | 35 | exports[`kbAnnounceIt.announceRelease should throw error when twitter post tweet throws an error 1`] = `"Twitter Post Error"`; 36 | 37 | exports[`kbAnnounceIt.generateTweet should throw error on missing package details input 1`] = `"The key name is missing from given object"`; 38 | 39 | exports[`kbAnnounceIt.generateTweet should throw error on non object input 1`] = `"Expecting Object"`; 40 | 41 | exports[`kbAnnounceIt.generateTweet should throw error when using missing variables in template 1`] = `"Using missing variables in tweet template"`; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kibibit/announce-it", 3 | "version": "2.0.0-next.2", 4 | "description": "Announcing your Releases To Twitter", 5 | "types": "lib/index.d.ts", 6 | "main": "lib/index.js", 7 | "files": [ 8 | "/lib", 9 | "/bin" 10 | ], 11 | "bin": { 12 | "announce-it": "bin/announce-it-cli.js" 13 | }, 14 | "scripts": { 15 | "build": "tsc", 16 | "contributors:add": "all-contributors add", 17 | "contributors:generate": "all-contributors generate", 18 | "coveralls": "cat ./coverage/lcov.info | coveralls", 19 | "lint:fix": "eslint src/**/*.ts --fix", 20 | "lint": "eslint src/**/*.ts", 21 | "semantic-release": "semantic-release", 22 | "start:dev": "ts-node ./src/announce-it-cli.ts", 23 | "start": "node ./bin/announce-it-cli.js", 24 | "test:watch": "jest --watchAll --coverage", 25 | "test": "jest --coverage", 26 | "start:ci": "npm start -- --branch master" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/Kibibit/announce-it.git" 31 | }, 32 | "author": "ZimGil ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/Kibibit/announce-it/issues" 36 | }, 37 | "homepage": "https://github.com/Kibibit/announce-it#readme", 38 | "announcements": { 39 | "tweet": "We have an announcement!\n<%= package %> <%= version %> is now live!\n\nGo check it out: <%= npmpage %>" 40 | }, 41 | "devDependencies": { 42 | "@commitlint/cli": "^11.0.0", 43 | "@commitlint/config-angular": "^11.0.0", 44 | "@commitlint/config-conventional": "^11.0.0", 45 | "@semantic-release/commit-analyzer": "^8.0.1", 46 | "@semantic-release/exec": "^5.0.0", 47 | "@semantic-release/git": "^9.0.0", 48 | "@semantic-release/github": "^7.2.0", 49 | "@semantic-release/npm": "^7.0.9", 50 | "@semantic-release/release-notes-generator": "^9.0.1", 51 | "@types/dotenv": "^6.1.1", 52 | "@types/find-root": "^1.1.2", 53 | "@types/fs-extra": "^9.0.6", 54 | "@types/jest": "^26.0.20", 55 | "@types/lodash": "^4.14.167", 56 | "@types/nconf": "^0.10.0", 57 | "@typescript-eslint/eslint-plugin": "^4.13.0", 58 | "@typescript-eslint/parser": "^4.13.0", 59 | "all-contributors-cli": "^6.19.0", 60 | "commitizen": "^4.2.2", 61 | "coveralls": "3.0.9", 62 | "cz-conventional-changelog": "^3.3.0", 63 | "eslint": "^7.17.0", 64 | "eslint-plugin-jest": "^24.1.0", 65 | "husky": "^4.3.7", 66 | "jest": "^26.6.3", 67 | "jest-github-actions-reporter": "^1.0.3", 68 | "jest-stare": "^2.2.0", 69 | "npm-check": "^5.9.2", 70 | "semantic-release": "^17.3.3", 71 | "semantic-release-cli": "^5.4.1", 72 | "ts-jest": "^26.4.4", 73 | "ts-node": "^9.1.1", 74 | "typescript": "^4.1.3" 75 | }, 76 | "dependencies": { 77 | "dotenv": "^8.2.0", 78 | "find-root": "^1.1.0", 79 | "fs-extra": "^9.0.1", 80 | "lodash": "^4.17.20", 81 | "manakin": "^0.5.2", 82 | "nconf": "^0.11.1", 83 | "twitter-lite": "^0.14.0" 84 | }, 85 | "jest-stare": { 86 | "resultDir": "test-results/", 87 | "coverageLink": "../coverage/index.html" 88 | }, 89 | "config": { 90 | "commitizen": { 91 | "path": "./node_modules/cz-conventional-changelog" 92 | } 93 | }, 94 | "husky": { 95 | "hooks": { 96 | "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true", 97 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 98 | } 99 | }, 100 | "publishConfig": { 101 | "access": "public" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/announce-it.ts: -------------------------------------------------------------------------------- 1 | import { forEach, get, isNil, isObject, template, chain, isString } from 'lodash'; 2 | import Twitter from 'twitter-lite'; 3 | 4 | import { IPackageDetails } from './read-package-details'; 5 | 6 | export interface IKbAnnounceItOptions { 7 | consumerKey: string; 8 | consumerSecret: string; 9 | accessTokenKey: string; 10 | accessTokenSecret: string; 11 | branch: string; 12 | } 13 | 14 | export interface IBranchObject { 15 | name: string; 16 | prerelease: boolean; 17 | } 18 | 19 | export interface IStabilityGroups { 20 | stable: string[]; 21 | unstable: string[]; 22 | } 23 | 24 | export class KbAnnounceIt { 25 | private client: any; 26 | private branchName: string; 27 | constructor(options: IKbAnnounceItOptions) { 28 | 29 | if (!get(options, 'consumerKey') || 30 | !get(options, 'consumerSecret') || 31 | !get(options, 'accessTokenKey') || 32 | !get(options, 'accessTokenSecret') || 33 | !get(options, 'branch')) { 34 | 35 | throw new Error('ERROR: missing required options'); 36 | } 37 | 38 | this.branchName = options.branch; 39 | this.client = new Twitter({ 40 | subdomain: 'api', 41 | consumer_key: options.consumerKey, 42 | consumer_secret: options.consumerSecret, 43 | access_token_key: options.accessTokenKey, 44 | access_token_secret: options.accessTokenSecret 45 | }); 46 | } 47 | 48 | announceRelease(packageDetails: IPackageDetails): Promise { 49 | const releaseBranches: any[] = get(packageDetails, 'release.branches'); 50 | 51 | const branchesGroupedByStability: IStabilityGroups = chain(releaseBranches) 52 | .groupBy((branch: string | IBranchObject) => { 53 | if (isString(branch)) { return 'stable'; } 54 | return branch.prerelease ? 'unstable' : 'stable'; 55 | }) 56 | .mapValues((branches: Array) => 57 | branches.map((branch) => isString(branch) ? branch : branch.name)) 58 | .defaultsDeep({ 59 | stable: [], 60 | unstable: [] 61 | }) 62 | .value() as any; 63 | 64 | const isIncludeUnstable = packageDetails.announcements.includeUnstable; 65 | const isUnstableRelease = branchesGroupedByStability 66 | .unstable.includes(this.branchName); 67 | const isStableRelease = branchesGroupedByStability 68 | .stable.includes(this.branchName); 69 | const isNotRelease = !isUnstableRelease && !isStableRelease; 70 | 71 | if ((!isIncludeUnstable && isUnstableRelease) || isNotRelease) { 72 | return Promise.reject(new Error('Not a stable release')); 73 | } 74 | 75 | const tweet = this.generateTweet(packageDetails); 76 | 77 | return this.client 78 | .get('account/verify_credentials') 79 | .then(() => this.client.post('statuses/update', { status: tweet })) 80 | .then(() => tweet); 81 | } 82 | 83 | generateTweet(packageDetails: IPackageDetails): string { 84 | this.ensureKeyAttributes(packageDetails); 85 | 86 | const tweetTemplate = template(packageDetails.announcements.tweet); 87 | let tweet: string; 88 | try { 89 | tweet = tweetTemplate({ 90 | package: packageDetails.name, 91 | version: packageDetails.version, 92 | description: packageDetails.description, 93 | author: packageDetails.author, 94 | homepage: packageDetails.homepage, 95 | npmpage: `https://www.npmjs.com/package/${ packageDetails.name }/v/${ packageDetails.version }` 96 | }); 97 | } catch (e) { 98 | throw new Error('Using missing variables in tweet template'); 99 | } 100 | 101 | return tweet; 102 | } 103 | 104 | private ensureKeyAttributes(packageDetails: Partial): void { 105 | const requiredKeys = [ 'name', 'version', 'announcements.tweet' ]; 106 | 107 | if (!isObject(packageDetails)) { 108 | throw new Error('Expecting Object'); 109 | } 110 | 111 | forEach(requiredKeys, (key) => { 112 | if (isNil(get(packageDetails, key))) { 113 | throw new Error(`The key ${ key } is missing from given object`); 114 | } 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "*": ["types/*"] 6 | }, 7 | /* Basic Options */ 8 | "target": "es5", 9 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 10 | "module": "commonjs", 11 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 12 | // "lib": [], /* Specify library files to be included in the compilation. */ 13 | // "allowJs": true, /* Allow javascript files to be compiled. */ 14 | // "checkJs": true, /* Report errors in .js files. */ 15 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 16 | "declaration": true, 17 | /* Generates corresponding '.d.ts' file. */ 18 | "declarationMap": true, 19 | /* Generates a sourcemap for each corresponding '.d.ts' file. */ 20 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 21 | // "outFile": "./", /* Concatenate and emit output to single file. */ 22 | "outDir": "./lib", 23 | /* Redirect output structure to the directory. */ 24 | "rootDir": "./src", 25 | /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 26 | // "composite": true, /* Enable project compilation */ 27 | // "incremental": true, /* Enable incremental compilation */ 28 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 29 | // "removeComments": true, /* Do not emit comments to output. */ 30 | // "noEmit": true, /* Do not emit outputs. */ 31 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 32 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 33 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 34 | 35 | /* Strict Type-Checking Options */ 36 | "strict": true, 37 | /* Enable all strict type-checking options. */ 38 | "noImplicitAny": false, 39 | /* Raise error on expressions and declarations with an implied 'any' type. */ 40 | // "strictNullChecks": true, /* Enable strict null checks. */ 41 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 42 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 43 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 44 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 45 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 46 | 47 | /* Additional Checks */ 48 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 49 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 50 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 51 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 52 | 53 | /* Module Resolution Options */ 54 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 55 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 56 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 57 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 58 | "typeRoots": ["./types", "./node_modules/@types/"], 59 | /* List of folders to include type definitions from. */ 60 | // "types": [], /* Type declaration files to be included in compilation. */ 61 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 62 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 63 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 64 | 65 | /* Source Map Options */ 66 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 69 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 70 | 71 | /* Experimental Options */ 72 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 73 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 74 | }, 75 | "include": [ 76 | "src/**/*" 77 | ], 78 | "exclude": [ 79 | "*.test.ts" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | @kibibit/announce-it 5 |

6 |

7 |

8 | 9 |

10 |

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |

23 |

24 | Announcing your application releases on social media 25 |

26 |
27 | 28 | 29 | 30 | ## How to install 31 | 32 | Install this package as a dev dependency: 33 | ```shell 34 | npm install --save-dev @kibibit/announce-it 35 | ``` 36 | 37 | ## How to use 38 | Intended to run after a new release in your continues integration 39 | 40 | ### Twitter Setup 41 | You have to create a [Developer Account on Twitter](https://developer.twitter.com/). 42 | 43 | * Create an App 44 | * From your apps list go to your app Details 45 | * Select the `Keys and tokens` tab 46 | * You'll need all 4 variables available in that page: 47 | * API key 48 | * API secret key 49 | * Access token 50 | * Access token secret 51 | 52 | 53 | ### As a command line tool 54 | - You need to pass the following parameters by either ENV parameters or 55 | calling the cli with these as cli params: 56 | 57 | * `TWITTER_CONSUMER_KEY` = API key 58 | * `TWITTER_CONSUMER_SECRET` = API secret key 59 | * `TWITTER_ACCESS_TOKEN_KEY` = Access token 60 | * `TWITTER_ACCESS_TOKEN_SECRET` = Access token secret 61 | * `branch` = the current branch 62 | 63 | As CLI params: 64 | ```bash 65 | ./node_modules/.bin/announce-it --CONSUMER_KEY --CONSUMER_SECRET --ACCESS_TOKEN_KEY --ACCESS_TOKEN_SECRET --branch 66 | ``` 67 | Both ENV parameters and the CLI arguments have the same names. If a parameter is found in both places, the CLI argument will be used. 68 | 69 | - You can add a dedicated script in your `package.json` file: 70 | ```js 71 | // ... 72 | "scripts": { 73 | // ... 74 | "announce": "announce-it" 75 | } 76 | // ... 77 | ``` 78 | - If executed with @semanic-release/exec: 79 | > because of a current issue with `@semantic-release/exec`, you need 80 | to pass the current branch directly 81 | ```js 82 | "release": { 83 | "branches": [ /* ... */ ] 84 | // ... 85 | "success": [ 86 | "@semantic-release/github", 87 | [ 88 | "@semantic-release/exec", 89 | { 90 | "successCmd": "npm start -- --branch $TRAVIS_BRANCH" 91 | } 92 | ] 93 | ], 94 | } 95 | // ... 96 | // ... 97 | ``` 98 | - If installed as a project dependency, you can run with npx: 99 | ```bash 100 | # should be ran inside your project 101 | # npx 102 | npx announce-it 103 | 104 | # directly 105 | ./node_modules/.bin/announce-it 106 | ``` 107 | - If installed globally, you can run it from within any node package with 108 | the correct setup 109 | 110 | ### As a node module 111 | 112 | ```typescript 113 | import { KbAnnounceIt, PackageDetails } from '@kibibit/announce-it'; 114 | 115 | const announceIt = new KbAnnounceIt({ 116 | accessTokenKey: 'TWITTER_ACCESS_KEY', 117 | accessTokenSecret: 'TWITTER_ACCESS_SECRET', 118 | consumerKey: 'TWITTER_CONSUMER_KEY', 119 | consumerSecret: 'TWITTER_CONSUMER_SECRET', 120 | branch: 'CURRENT_BRANCH' 121 | }); 122 | 123 | const myPackage: PackageDetails = require('./package'); 124 | 125 | // get generated tweet 126 | const tweet: string = announceIt.generateTweet(myPackage); 127 | 128 | console.log('going to tweet: ', tweet); 129 | 130 | // publish tweet to twitter 131 | announceIt.announceRelease(myPackage); 132 | 133 | ``` 134 | 135 | ### Defining your Templates 136 | Inside your `package.json` file, add an `announcements` object with `tweet` property. 137 | 138 | You can then create your own tweet message template that will be posted to twitter. 139 | 140 | ```javascript 141 | { 142 | "name": "my-package", 143 | "version": "2.4.3", 144 | // ... 145 | "announcements": { 146 | "tweet": "Version <%= version %> of <%= package %> is live! <%= npmpage %>" 147 | }, 148 | } 149 | ``` 150 | 151 | The tweet template is generated with [Lodash template](https://lodash.com/docs/4.17.11#template). 152 | 153 | You can use these variables: 154 | * Package name: `<%= package %>` 155 | * Version number: `<%= version %>` 156 | * Package description: `<%= description %>` 157 | * Package author: `<%= author %>` 158 | * Homepage link: `<%= homepage %>` 159 | * Package page on npm: `<%= npmpage %>` 160 | 161 | ## Contributing 162 | 163 | If you have suggestions for how announce-it could be improved, or want to report a bug, open an issue! We'd love all and any contributions. 164 | 165 | For more, check out the [Contributing Guide](CONTRIBUTING.md). 166 | 167 | ## Contributors 168 | 169 | Thanks goes to our contributors! ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 170 | 171 | 172 | 173 |
Gil Tichon
Gil Tichon

🚇 💻 📆
Neil Kalman
Neil Kalman

🚇 🤔
174 | 175 | 176 | Library logo is made by 177 | Freepik 178 | from 179 | www.flaticon.com 180 | and licensed by 181 | CC 3.0 BY 182 | 183 | ## License 184 | 185 | [MIT](LICENSE) © 2019 Gil Tichon 186 | -------------------------------------------------------------------------------- /src/announce-it.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { assign, cloneDeep } from 'lodash'; 3 | 4 | import { KbAnnounceIt } from './announce-it'; 5 | import { IPackageDetails } from './read-package-details'; 6 | 7 | const TwitterMocks = () => { 8 | const defaultMock = () => Promise.resolve(); 9 | 10 | let getMock = defaultMock; 11 | let postMock = defaultMock; 12 | 13 | return { 14 | mock: { 15 | get: () => getMock(), 16 | post: () => postMock() 17 | }, 18 | reset: (userDefinedMocks: any = {}) => { 19 | getMock = userDefinedMocks.get || getMock; 20 | postMock = userDefinedMocks.post || postMock; 21 | } 22 | }; 23 | }; 24 | 25 | const twitterMocks = TwitterMocks(); 26 | 27 | jest.mock('twitter-lite', () => () => twitterMocks.mock); 28 | 29 | describe('KbAnnounceIt', () => { 30 | it('should throw error when missing options', () => { 31 | expect.assertions(5); 32 | 33 | // @ts-ignore 34 | expect(() => new KbAnnounceIt()).toThrowErrorMatchingSnapshot(); 35 | 36 | // @ts-ignore 37 | expect(() => new KbAnnounceIt({ 38 | accessTokenSecret: 'TEST', 39 | consumerKey: 'TEST', 40 | consumerSecret: 'TEST' 41 | })).toThrowErrorMatchingSnapshot(); 42 | 43 | // @ts-ignore 44 | expect(() => new KbAnnounceIt({ 45 | accessTokenKey: 'TEST', 46 | consumerKey: 'TEST', 47 | consumerSecret: 'TEST' 48 | })).toThrowErrorMatchingSnapshot(); 49 | 50 | // @ts-ignore 51 | expect(() => new KbAnnounceIt({ 52 | accessTokenKey: 'TEST', 53 | accessTokenSecret: 'TEST', 54 | consumerSecret: 'TEST' 55 | })).toThrowErrorMatchingSnapshot(); 56 | // @ts-ignore 57 | expect(() => new KbAnnounceIt({ 58 | accessTokenKey: 'TEST', 59 | accessTokenSecret: 'TEST', 60 | consumerKey: 'TEST' 61 | })).toThrowErrorMatchingSnapshot(); 62 | 63 | }); 64 | 65 | it('should create instance when given correct parameters', () => { 66 | const announceIt = new KbAnnounceIt({ 67 | accessTokenKey: 'TEST', 68 | accessTokenSecret: 'TEST', 69 | consumerKey: 'TEST', 70 | consumerSecret: 'TEST', 71 | branch: 'TEST' 72 | }); 73 | 74 | expect(announceIt).toBeDefined(); 75 | expect(announceIt).toMatchSnapshot(); 76 | }); 77 | }); 78 | 79 | describe('kbAnnounceIt.announceRelease', () => { 80 | const packageDetails: IPackageDetails = { 81 | name: 'test-repo', 82 | description: 'test-description', 83 | author: 'test-author', 84 | homepage: 'test-homepage', 85 | repository: { 86 | type: 'test-repo-type', 87 | url: 'test-repo-url' 88 | }, 89 | version: '0.0.0', 90 | announcements: { 91 | tweet: 'test-template' 92 | }, 93 | release: { 94 | branches: [ 'TEST' ] 95 | } 96 | }; 97 | 98 | let announceIt: KbAnnounceIt; 99 | 100 | beforeEach(() => { 101 | announceIt = new KbAnnounceIt({ 102 | accessTokenKey: 'TEST', 103 | accessTokenSecret: 'TEST', 104 | consumerKey: 'TEST', 105 | consumerSecret: 'TEST', 106 | branch: 'TEST' 107 | }); 108 | }); 109 | 110 | it('should post to twitter when stable release', () => { 111 | return expect(announceIt.announceRelease(packageDetails)).resolves.toMatchSnapshot(); 112 | }); 113 | 114 | it('should throw an error when unstable release and not mentioned in packageDetails', () => { 115 | const testPackageDetails = cloneDeep(packageDetails); 116 | announceIt = new KbAnnounceIt({ 117 | accessTokenKey: 'TEST', 118 | accessTokenSecret: 'TEST', 119 | consumerKey: 'TEST', 120 | consumerSecret: 'TEST', 121 | branch: 'unstable' 122 | }); 123 | 124 | return expect(announceIt.announceRelease(testPackageDetails)).rejects.toThrowErrorMatchingSnapshot(); 125 | }); 126 | 127 | it('should throw an error when unstable release and mentioned in packageDetails as unstable', () => { 128 | const testPackageDetails = cloneDeep(packageDetails); 129 | testPackageDetails.release.branches = [{ 130 | name: 'unstable', 131 | prerelease: true 132 | }]; 133 | announceIt = new KbAnnounceIt({ 134 | accessTokenKey: 'TEST', 135 | accessTokenSecret: 'TEST', 136 | consumerKey: 'TEST', 137 | consumerSecret: 'TEST', 138 | branch: 'unstable' 139 | }); 140 | 141 | 142 | return expect(announceIt.announceRelease(testPackageDetails)).rejects.toThrowErrorMatchingSnapshot(); 143 | }); 144 | 145 | it('should post to twitter when unstable release and mentioned in packageDetails as stable', () => { 146 | const testPackageDetails = cloneDeep(packageDetails); 147 | testPackageDetails.release.branches = [{ 148 | name: 'unstable', 149 | prerelease: false 150 | }]; 151 | announceIt = new KbAnnounceIt({ 152 | accessTokenKey: 'TEST', 153 | accessTokenSecret: 'TEST', 154 | consumerKey: 'TEST', 155 | consumerSecret: 'TEST', 156 | branch: 'unstable' 157 | }); 158 | 159 | 160 | return expect(announceIt.announceRelease(testPackageDetails)).resolves.toMatchSnapshot(); 161 | }); 162 | 163 | it('should post to twitter when unstable release and mentioned in packageDetails', () => { 164 | const testPackageDetails = cloneDeep(packageDetails); 165 | testPackageDetails.version = '0.0.0-next.1'; 166 | testPackageDetails.announcements.includeUnstable = true; 167 | 168 | return expect(announceIt.announceRelease(testPackageDetails)).resolves.toMatchSnapshot(); 169 | }); 170 | 171 | it('should throw error when twitter get credentials throws an error', async () => { 172 | twitterMocks.reset({ get: () => Promise.reject(new Error('Twitter Get Error')) }); 173 | 174 | // TODO: FEATURE: create a basic KbError class 175 | return expect(announceIt.announceRelease(packageDetails)).rejects 176 | .toThrowErrorMatchingSnapshot(); 177 | }); 178 | 179 | it('should throw error when twitter post tweet throws an error', async () => { 180 | expect.assertions(1); 181 | 182 | twitterMocks.reset({ 183 | post: () => Promise.reject(new Error('Twitter Post Error')), 184 | get: () => Promise.resolve() 185 | }); 186 | 187 | return expect(announceIt.announceRelease(packageDetails)).rejects 188 | .toThrowErrorMatchingSnapshot(); 189 | }); 190 | }); 191 | 192 | describe('kbAnnounceIt.generateTweet', () => { 193 | const packageDetails: IPackageDetails = { 194 | name: 'test-repo', 195 | description: 'test-description', 196 | author: 'test-author', 197 | homepage: 'test-homepage', 198 | repository: { 199 | type: 'test-repo-type', 200 | url: 'test-repo-url' 201 | }, 202 | version: '0.0.0', 203 | announcements: { 204 | tweet: 'test-template' 205 | }, 206 | release: { 207 | branches: [ 'TEST' ] 208 | } 209 | }; 210 | 211 | let announceIt: KbAnnounceIt; 212 | 213 | beforeEach(() => { 214 | announceIt = new KbAnnounceIt({ 215 | accessTokenKey: 'TEST', 216 | accessTokenSecret: 'TEST', 217 | consumerKey: 'TEST', 218 | consumerSecret: 'TEST', 219 | branch: 'TEST' 220 | }); 221 | }); 222 | 223 | it('should throw error on non object input', () => { 224 | // @ts-ignore 225 | expect(() => announceIt.generateTweet()).toThrowErrorMatchingSnapshot(); 226 | }); 227 | 228 | it('should throw error on missing package details input', () => { 229 | const testPackageDetails: any = assign({}, packageDetails); 230 | testPackageDetails.name = null; 231 | 232 | expect(() => announceIt.generateTweet(testPackageDetails)).toThrowErrorMatchingSnapshot(); 233 | }); 234 | 235 | it('should throw error when using missing variables in template', async () => { 236 | const testPackageDetails = packageDetails; 237 | testPackageDetails.announcements.tweet = 'This should <%= blow %> an error'; 238 | expect(() => announceIt.generateTweet(packageDetails)).toThrowErrorMatchingSnapshot(); 239 | 240 | }); 241 | }); 242 | --------------------------------------------------------------------------------