├── .editorconfig ├── .gitignore ├── .travis.yml ├── ARCHITECTURE.md ├── INSTALL.md ├── LICENSE.md ├── README.md ├── brand ├── AppCenter-transparent.png ├── AppCenter.png └── AppCenter.svg ├── build ├── README.md ├── common.ts └── gulpfile.ts ├── config.example.js ├── nuxt.config.js ├── package-lock.json ├── package.json ├── src ├── README.md ├── app.ts ├── bin.js ├── bootstrap.js ├── cli │ ├── cli.ts │ ├── commands │ │ ├── build.ts │ │ ├── ci.ts │ │ ├── index.ts │ │ ├── migrate.ts │ │ ├── repo.ts │ │ ├── seed.ts │ │ └── version.ts │ └── utilities.ts ├── client │ ├── README.md │ ├── components │ │ └── the-navbar.vue │ ├── fonts │ │ └── open-sans │ │ │ ├── LICENSE.txt │ │ │ ├── bold-italic.ttf │ │ │ ├── bold.ttf │ │ │ ├── extra-bold-italic.ttf │ │ │ ├── extra-bold.ttf │ │ │ ├── italic.ttf │ │ │ ├── light-italic.ttf │ │ │ ├── light.ttf │ │ │ ├── regular.ttf │ │ │ ├── semi-bold-italic.ttf │ │ │ └── semi-bold.ttf │ ├── images │ │ └── brand │ │ │ └── elementary-logomark.svg │ ├── layouts │ │ ├── blank.vue │ │ └── default.vue │ └── pages │ │ ├── error.vue │ │ └── index.vue ├── lib │ ├── app.ts │ ├── cache │ │ ├── index.ts │ │ ├── provider.ts │ │ └── type.ts │ ├── config │ │ ├── config.ts │ │ ├── index.ts │ │ └── loader.ts │ ├── database │ │ ├── README.md │ │ ├── database.ts │ │ ├── index.ts │ │ ├── migration │ │ │ ├── 2.0.0-001-github_repositories.ts │ │ │ ├── 2.0.0-002-github_releases.ts │ │ │ ├── 2.0.0-003-users.ts │ │ │ ├── 2.0.0-004-github_users.ts │ │ │ ├── 2.0.0-005-github_repositories_github_users.ts │ │ │ ├── 2.0.0-006-stripe_accounts.ts │ │ │ ├── 2.0.0-007-projects.ts │ │ │ ├── 2.0.0-008-releases.ts │ │ │ ├── 2.0.0-009-builds.ts │ │ │ └── 2.0.0-010-build_logs.ts │ │ ├── provider.ts │ │ └── seed │ │ │ ├── 001-github_repositories.ts │ │ │ ├── 002-github_releases.ts │ │ │ ├── 003-users.ts │ │ │ ├── 004-github_users.ts │ │ │ ├── 005-github_repositories_github_users.ts │ │ │ ├── 006-stripe_accounts.ts │ │ │ ├── 007-projects.ts │ │ │ ├── 008-releases.ts │ │ │ └── 009-builds.ts │ ├── log │ │ ├── index.ts │ │ ├── level.ts │ │ ├── log.ts │ │ ├── logger.ts │ │ ├── output.ts │ │ ├── outputs │ │ │ ├── console.ts │ │ │ ├── index.ts │ │ │ └── sentry.ts │ │ └── provider.ts │ ├── queue │ │ ├── index.ts │ │ ├── provider.ts │ │ ├── providers │ │ │ └── redis │ │ │ │ ├── index.ts │ │ │ │ ├── job.ts │ │ │ │ └── queue.ts │ │ └── type.ts │ ├── service │ │ ├── aptly.ts │ │ ├── github.ts │ │ ├── index.ts │ │ ├── provider.ts │ │ └── type.ts │ └── utility │ │ ├── eventemitter.ts │ │ ├── glob.ts │ │ ├── markdown.ts │ │ ├── rdnn.ts │ │ └── template.ts ├── repo │ ├── README.md │ ├── provider.ts │ └── repo.ts └── worker │ ├── README.md │ ├── docker.ts │ ├── index.ts │ ├── log.ts │ ├── preset │ ├── build.ts │ └── release.ts │ ├── provider.ts │ ├── task │ ├── appstream │ │ ├── description.ts │ │ ├── exist.md │ │ ├── id.ts │ │ ├── index.md │ │ ├── index.ts │ │ ├── license.ts │ │ ├── name.ts │ │ ├── release.ts │ │ ├── screenshot.ts │ │ ├── stripe.ts │ │ ├── summary.ts │ │ ├── validate.md │ │ ├── validate.ts │ │ └── validate │ │ │ └── Dockerfile │ ├── build │ │ ├── deb.md │ │ ├── deb.ts │ │ └── deb │ │ │ ├── Dockerfile │ │ │ └── liftoff_0.1_amd64.deb │ ├── debian │ │ ├── changelog.ts │ │ ├── changelogTemplate.ejs │ │ ├── control.md │ │ └── control.ts │ ├── desktop │ │ ├── exec.ts │ │ ├── icon.ts │ │ ├── index.ts │ │ ├── validate.md │ │ ├── validate.ts │ │ └── validate │ │ │ └── Dockerfile │ ├── extract │ │ ├── deb.ts │ │ └── deb │ │ │ ├── Dockerfile │ │ │ └── extract-deb.sh │ ├── file │ │ ├── deb.ts │ │ └── deb │ │ │ ├── binary.md │ │ │ ├── binary.ts │ │ │ ├── nonexistent.ts │ │ │ └── nonexistentLog.md │ ├── pack │ │ ├── deb.ts │ │ └── deb │ │ │ ├── Dockerfile │ │ │ └── pack-deb.sh │ ├── task.ts │ ├── upload │ │ ├── index.ts │ │ ├── log.md │ │ ├── log.spec.ts │ │ ├── log.ts │ │ ├── package.md │ │ ├── package.spec.ts │ │ └── package.ts │ ├── workspace │ │ └── setup.ts │ └── wrapperTask.ts │ ├── type.ts │ └── worker.ts ├── test ├── bootstrap.js ├── e2e │ ├── lib │ │ ├── log │ │ │ └── outputs │ │ │ │ └── console.ts │ │ └── service │ │ │ ├── aptly.ts │ │ │ └── github.ts │ └── worker │ │ ├── docker.ts │ │ ├── task │ │ └── debian │ │ │ └── changelog.ts │ │ └── worker.ts ├── fixture │ ├── config.js │ ├── lib │ │ └── service │ │ │ ├── aptly │ │ │ └── asset.json │ │ │ └── github │ │ │ ├── asset.json │ │ │ ├── installation.json │ │ │ ├── key.pem │ │ │ ├── log.json │ │ │ └── vocal.deb │ └── worker │ │ ├── docker │ │ └── image1 │ │ │ └── Dockerfile │ │ ├── log │ │ └── test1.md │ │ └── task │ │ ├── appstream │ │ ├── blank.xml │ │ └── spice-up.xml │ │ ├── debian │ │ └── control │ │ │ ├── gold1 │ │ │ └── gold2 │ │ ├── desktop │ │ ├── blank.desktop │ │ └── spice-up.desktop │ │ └── empty ├── spec │ ├── lib │ │ ├── config │ │ │ ├── index.ts │ │ │ └── loader.ts │ │ ├── log │ │ │ ├── level.ts │ │ │ └── log.ts │ │ ├── queue │ │ │ └── provider.ts │ │ ├── service │ │ │ ├── aptly.ts │ │ │ └── github.ts │ │ └── utility │ │ │ ├── eventemitter.ts │ │ │ ├── glob.ts │ │ │ └── rdnn.ts │ └── worker │ │ ├── log.ts │ │ ├── preset │ │ └── release.ts │ │ └── task │ │ ├── appstream │ │ ├── id.ts │ │ ├── index.ts │ │ ├── release.ts │ │ ├── screenshot.ts │ │ ├── stripe.ts │ │ └── validate.ts │ │ ├── debian │ │ ├── changelog.ts │ │ └── control.ts │ │ ├── desktop │ │ ├── index.ts │ │ └── validate.ts │ │ ├── file │ │ └── deb │ │ │ └── binary.ts │ │ └── workspace │ │ └── setup.ts └── utility │ ├── app.ts │ ├── ci.ts │ ├── config.ts │ ├── database.ts │ ├── docker.ts │ ├── fs.ts │ ├── http.ts │ └── worker │ ├── context.ts │ ├── index.ts │ ├── mock.ts │ ├── repository.ts │ └── worker.ts ├── tsconfig.json ├── tsconfig.production.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # Enforce the one true coding standard 13 | [*] 14 | indent_size = 2 15 | indent_style = space 16 | 17 | # Set the max line count 18 | [*.md] 19 | max_line_length = 80 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General files 2 | *.log 3 | *.tmp 4 | 5 | # Application files 6 | .cache 7 | /config* 8 | coverage/ 9 | .nyc_output 10 | dest/ 11 | .nuxt 12 | 13 | !config.example.js 14 | 15 | # Node files 16 | node_modules 17 | 18 | # OS files 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | language: node_js 4 | 5 | sudo: required 6 | 7 | services: 8 | - docker 9 | 10 | addons: 11 | apt: 12 | sources: 13 | - ubuntu-toolchain-r-test 14 | packages: 15 | - libstdc++-4.9-dev 16 | 17 | node_js: 10 18 | 19 | cache: 20 | directories: 21 | - /tmp/liftoff 22 | 23 | script: 24 | - npm run build 25 | - npm run lint 26 | - npm run ci:test 27 | 28 | jobs: 29 | include: 30 | - stage: Test 31 | node_js: 10 32 | 33 | - node_js: 12 34 | 35 | - stage: Release 36 | script: npm run build 37 | deploy: 38 | provider: script 39 | skip_cleanup: true 40 | script: npm run ci:release 41 | on: 42 | repo: elementary/houston 43 | branch: v2 44 | -------------------------------------------------------------------------------- /ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | # Houston Architecture 2 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 | # Installing Houston 2 | 3 | ## Needed Services 4 | 5 | To setup Houston you will need a working node environment. Each operating system 6 | is different, so it's best to refer to the official 7 | [node documentation](https://nodejs.org/en/download/) on installing. 8 | 9 | **NOTE** If you are only wanting to run the worker process for `houston ci` 10 | or `houston build`, you _only_ need to install Docker. If you install Docker 11 | on the same machine as Houston, you _should_ need _no_ additional 12 | configuration. 13 | 14 | For a fully working installation you will want to ensure all of these 15 | services are setup, working, and fully accessible to Houston. 16 | 17 | You will need: 18 | 19 | - A [Knexjs supported database](http://knexjs.org/#Installation-node) 20 | - An [Aptly repository](https://www.aptly.info/) 21 | - A **local** [Docker](https://www.docker.com/) server 22 | - A [GitHub OAuth](https://github.com/organizations/elementary/settings/applications) application 23 | - A [Stripe connect](https://dashboard.stripe.com/account/applications/settings) account 24 | - A [Mandrill](https://mandrillapp.com) account 25 | 26 | ## Package 27 | 28 | Simply run `npm i -g @elementaryos/houston`. 29 | 30 | _NOTE: Depending on how you installed node, you may have to run the above command 31 | with `sudo`._ 32 | 33 | ## Source 34 | 35 | First, `git clone` this repo. 36 | 37 | Next you will need to install the needed node packages. This is done with: 38 | ```shell 39 | npm ci 40 | ``` 41 | 42 | Then build Houston with: 43 | ```shell 44 | npm run build 45 | ``` 46 | 47 | Then install it with: 48 | ```shell 49 | npm link 50 | ``` 51 | 52 | _NOTE: Depending on how you installed node, you may have to run the above command 53 | with `sudo`._ 54 | 55 | You will need to setup your configuration. Simply copy the `config.example.js` 56 | file to another location and edit it's values. This file is well documented with 57 | possible values and links to needed third party services. 58 | 59 | Lastly, you can run houston with: 60 | ```shell 61 | houston 62 | ``` 63 | 64 | For a full list of commands run: 65 | ```shell 66 | houston --help 67 | ``` 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | ### Copyright © 2018 elementary & [contributors](https://github.com/elementary/houston/graphs/contributors) 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 | ----------------------------------------------------------------------------- 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This is the old AppCenter Dashboard for elementary OS 5.1 and older. You may be looking for the [new Flatpak-based AppCenter Dashboard](https://github.com/elementary/appcenter-dashboard) or the new [AppCenter Reviews repo](https://github.com/elementary/appcenter-reviews). 2 | 3 | --- 4 | 5 |
6 | 7 |
8 | AppCenter 9 |
10 |
11 |
12 |

Houston

13 |

Backend to AppCenter

14 |
15 |
16 |
17 | 18 |

19 | 20 | travis-ci 21 | 22 | 23 | 24 | gitter 25 | 26 |

27 | 28 | --- 29 | 30 | > Houston is currently undergoing a rewrite to typescript with an emphasis on 31 | testability. Currently, the only part used in production for v2 is the worker 32 | process. Everything else will be found in the master branch. 33 | 34 | Houston is part of AppCenter, a multi component system for helping developers 35 | and making users' life easier. Houston includes processes for building, testing, 36 | and publishing packages, as well as the front end website. 37 | 38 | For more information about the architecture and processes that make up Houston 39 | please see the [architecture 40 | file](https://github.com/elementary/houston/blob/v2/ARCHITECTURE.md). 41 | 42 | For development information see the [various readme 43 | files](https://github.com/elementary/houston/blob/v2/src/README.md) in the 44 | `src/` directory. 45 | 46 | For building and setting up your own instance read the 47 | [install file](https://github.com/elementary/houston/blob/v2/INSTALL.md). 48 | -------------------------------------------------------------------------------- /brand/AppCenter-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/brand/AppCenter-transparent.png -------------------------------------------------------------------------------- /brand/AppCenter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/brand/AppCenter.png -------------------------------------------------------------------------------- /build/README.md: -------------------------------------------------------------------------------- 1 | # houston/build/ 2 | 3 | This folder holds everything needed to build houston. If you need to change 4 | something about the build, chances are it will be in `common.ts`. We use `gulp` 5 | to run all of the needed tasks. This includes building javascript files, 6 | and building client side assets like CSS and images. 7 | -------------------------------------------------------------------------------- /build/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/build/common.ts 3 | * Holds common build values like directories and stuff 4 | * 5 | * @exports {object} paths - A list of paths to use when building 6 | * @exports {string[]} browsers - A list of browsers to support 7 | */ 8 | 9 | import * as path from 'path' 10 | 11 | /** 12 | * paths 13 | * A list of paths to use when building 14 | * 15 | * @var {object} 16 | */ 17 | export const paths = { 18 | dest: path.resolve(__dirname, '..', 'dest'), 19 | root: path.resolve(__dirname, '..'), 20 | src: path.resolve(__dirname, '..', 'src') 21 | } 22 | 23 | /** 24 | * browsers 25 | * A list of all browsers we should support when building client side assets 26 | * 27 | * @var {string[]} 28 | */ 29 | export const browsers = ['last 2 version'] 30 | -------------------------------------------------------------------------------- /build/gulpfile.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/build/gulpfile.ts 3 | * Builds all of houston. Places everything in houston/dest directory. 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as gulp from 'gulp' 8 | import * as path from 'path' 9 | 10 | import * as sourcemap from 'gulp-sourcemaps' 11 | 12 | import * as typescript from 'gulp-typescript' 13 | 14 | import * as common from './common' 15 | 16 | const tsConfig = path.resolve(common.paths.root, 'tsconfig.production.json') 17 | const tsProject = typescript.createProject(tsConfig) 18 | 19 | /** 20 | * clean 21 | * Removes the build directory 22 | * 23 | * @async 24 | * @return {string[]} - A list of removed files 25 | */ 26 | gulp.task('clean', () => { 27 | return fs.remove(common.paths.dest) 28 | }) 29 | 30 | /** 31 | * copy 32 | * Copies over files that do not need to be built, but must be in project folder 33 | * 34 | * @return {stream} - A gulp task 35 | */ 36 | gulp.task('copy', () => { 37 | const src = path.resolve(common.paths.src) 38 | const dest = path.resolve(common.paths.dest) 39 | 40 | return gulp.src([ 41 | path.resolve(src, '**/*'), 42 | '!' + path.resolve(src, '**/*.ts') 43 | ], { base: src }) 44 | .pipe(gulp.dest(dest)) 45 | }) 46 | 47 | /** 48 | * typescript 49 | * Builds all typescript files into regular javascript files 50 | * 51 | * @return {stream} - A gulp task 52 | */ 53 | gulp.task('typescript', () => { 54 | const src = path.resolve(common.paths.src) 55 | const dest = path.resolve(common.paths.dest) 56 | 57 | return gulp.src([ 58 | path.resolve(src, '**', '*.ts'), 59 | '!' + path.resolve(src, '**', '*.e2e.ts'), 60 | '!' + path.resolve(src, '**', '*.spec.ts') 61 | ], { base: src }) 62 | .pipe(sourcemap.init()) 63 | .pipe(tsProject()) 64 | .pipe(sourcemap.write('.')) 65 | .pipe(gulp.dest(dest)) 66 | }) 67 | 68 | /** 69 | * build 70 | * Builds all houston assets 71 | * 72 | * @return {stream} - A gulp task 73 | */ 74 | gulp.task('build', gulp.series('clean', gulp.parallel('copy', 'typescript'))) 75 | 76 | /** 77 | * watch 78 | * Builds all the houston assets, but watches for changes for faster building 79 | * 80 | * @return {void} 81 | */ 82 | gulp.task('watch', () => { 83 | const src = path.resolve(common.paths.src, '**', '*.ts') 84 | 85 | return gulp.watch(src, gulp.series('typescript')) 86 | }) 87 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/nuxt.config.js 3 | * Configuration for nuxt. Duh. Has to be regular node commonjs modules 4 | */ 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | srcDir: path.resolve(__dirname, './src/client'), 10 | 11 | /** 12 | * All of the default head properties to insert. 13 | * 14 | * @see https://github.com/declandewet/vue-meta#recognized-metainfo-properties 15 | */ 16 | head: { 17 | titleTemplate: (title) => { 18 | if (title) { 19 | return `${title} - Developer ⋅ elementary` 20 | } else { 21 | return 'Developer ⋅ elementary' 22 | } 23 | }, 24 | 25 | meta: [ 26 | { charset: 'utf-8' }, 27 | { name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0' }, 28 | 29 | { name: 'description', content: 'Resources for designing, developing, and publishing apps for elementary OS' }, 30 | { name: 'author', content: 'elementary LLC' }, 31 | { name: 'theme-color', content: '#403757' }, 32 | 33 | { name: 'name', content: 'Developer ⋅ elementary' }, 34 | { name: 'description', content: 'Resources for designing, developing, and publishing apps for elementary OS' }, 35 | { name: 'image', content: 'https://elementary.io/images/developer/preview.png' }, 36 | 37 | { name: 'twitter:card', content: 'summary_large_image' }, 38 | { name: 'twitter:site', content: '@elementary' }, 39 | { name: 'twitter:creator', content: '@elementary' }, 40 | 41 | { name: 'og:title', content: 'Developer ⋅ elementary' }, 42 | { name: 'og:description', content: 'Resources for designing, developing, and publishing apps for elementary OS' }, 43 | { name: 'og:image', content: 'https://elementary.io/images/developer/preview.png' }, 44 | 45 | { name: 'apple-mobile-web-app-title', content: 'Dashboard' } 46 | // { name: 'apple-touch-icon', content: '/images/apple-touch-icon.png' } 47 | ], 48 | 49 | link: [ 50 | { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Open+Sans:300,400' } 51 | ] 52 | }, 53 | 54 | /** 55 | * Cool style for the nuxt progress bar 56 | * 57 | * @var {Object} 58 | */ 59 | loading: { 60 | color: '#3892e0', 61 | failedColor: '#da4d45' 62 | }, 63 | 64 | ErrorPage: '~/pages/error' 65 | } 66 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # houston/src/ 2 | 3 | This folder holds all of the houston code base. Each folder, with the exception 4 | of `cli` and `lib`, hold the code for a different process of houston. 5 | 6 | ## api/ 7 | 8 | This holds the code for the API server. It is strictly JSON based with no html. 9 | Anything related to the client, including pages, styles, and endpoints related 10 | to browser information should be in `client/`. 11 | 12 | ## cli/ 13 | 14 | This folder holds all of the code needed to get the configuration setup and 15 | start a process. It also holds some useful scripts like database migration 16 | and seeding. 17 | 18 | ## client/ 19 | 20 | This folder is everything the user sees. It handles controllers for the client 21 | routes, and markup for styles and pages. 22 | 23 | ## lib/ 24 | 25 | This folder holds universal code used in multiple processes. Look here if you 26 | need to change something about the database. 27 | 28 | ## repo/ 29 | 30 | This holds the code for the repository syslog server. This is used by nginx 31 | to record download counts of files in the repository. 32 | 33 | ## worker/ 34 | 35 | This is the worker process. It is responsible for building and releasing 36 | projects. 37 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/app.ts 3 | * Helpful entry points for if houston is being used as a library 4 | */ 5 | 6 | export { App } from './lib/app' 7 | 8 | export { Config } from './lib/config' 9 | 10 | export { 11 | Aptly, 12 | codeRepositoryFactory, 13 | github 14 | } from './lib/service' 15 | 16 | export { Worker } from './worker' 17 | export { Build as BuildWorker } from './worker' 18 | export { Release as ReleaseWorker } from './worker' 19 | -------------------------------------------------------------------------------- /src/bin.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | require('./bootstrap') 4 | require('./cli/cli') 5 | -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/bootstrap.ts 3 | * A bootstrap file that should be loaded once in the whole application. 4 | * Used for loading polyfills and other _hacks_ as needed. 5 | * Because this file is bootstraping the env, it must be es5 styled. Sorry. 6 | */ 7 | 8 | require('reflect-metadata') 9 | -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/cli.ts 3 | * Entry point to houston CLI 4 | */ 5 | 6 | // Command line files are allowed to have console log statements 7 | // tslint:disable no-console 8 | 9 | import * as yargs from 'yargs' 10 | 11 | import commands from './commands' 12 | 13 | yargs.version(false) 14 | yargs.help('h').alias('h', 'help') 15 | 16 | for (const c of commands) { 17 | yargs.command(c) 18 | } 19 | 20 | yargs.option('config', { alias: 'c', describe: 'Path to configuration file', type: 'string' }) 21 | 22 | yargs.recommendCommands() 23 | yargs.showHelpOnFail(true) 24 | 25 | const argv = yargs.argv 26 | 27 | if (argv._.length === 0) { 28 | yargs.showHelp() 29 | process.exit(1) 30 | } 31 | -------------------------------------------------------------------------------- /src/cli/commands/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/commands/index.ts 3 | * Exports an array of all the commands we can run. 4 | */ 5 | 6 | export default [ 7 | require('./build'), 8 | require('./ci'), 9 | require('./migrate'), 10 | require('./repo'), 11 | require('./seed'), 12 | require('./version') 13 | ] 14 | -------------------------------------------------------------------------------- /src/cli/commands/migrate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/commands/migrate.ts 3 | * Runs database migration scripts 4 | */ 5 | 6 | // Command line files are allowed to have console log statements 7 | // tslint:disable no-console 8 | 9 | import { Database } from '../../lib/database' 10 | import { setup } from '../utilities' 11 | 12 | export const command = 'migrate' 13 | export const describe = 'Changes database tables based on houston schemas' 14 | 15 | export const builder = (yargs) => { 16 | return yargs 17 | .option('direction', { describe: 'The direction to migrate', type: 'string', default: 'up' }) 18 | } 19 | 20 | export async function handler (argv) { 21 | const { app, config } = setup(argv) 22 | const database = app.get(Database) 23 | 24 | if (argv.direction === 'up') { 25 | const version = config.get('houston.version', 'latest') 26 | console.log(`Updating database tables to ${version} version`) 27 | } else if (argv.direction === 'down') { 28 | console.log(`Downgrading database tables 1 version`) 29 | } else { 30 | console.error(`Incorrect non-option arguments: got ${argv.direction}, need at be up or down`) 31 | process.exit(1) 32 | } 33 | 34 | try { 35 | if (argv.direction === 'up') { 36 | await database.knex.migrate.latest() 37 | } else if (argv.direction === 'down') { 38 | await database.knex.migrate.rollback() 39 | } 40 | } catch (e) { 41 | console.error('Error updating database tables') 42 | console.error(e.message) 43 | process.exit(1) 44 | } 45 | 46 | console.log('Updated database tables') 47 | process.exit(0) 48 | } 49 | -------------------------------------------------------------------------------- /src/cli/commands/repo.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/commands/repo.ts 3 | * Runs the repository syslogd server 4 | */ 5 | 6 | // Command line files are allowed to have console log statements 7 | // tslint:disable no-console 8 | 9 | import { Repo as Server } from '../../repo/repo' 10 | import { setup } from '../utilities' 11 | 12 | export const command = 'repo' 13 | export const describe = 'Starts the repository syslogd server' 14 | 15 | export const builder = (yargs) => { 16 | return yargs 17 | .option('port', { alias: 'p', describe: 'The port to run the server on', type: 'number', default: 0 }) 18 | } 19 | 20 | export async function handler (argv) { 21 | const { app } = setup(argv) 22 | const server = app.get(Server) 23 | 24 | await server.listen(argv.port) 25 | } 26 | -------------------------------------------------------------------------------- /src/cli/commands/seed.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/commands/seed.ts 3 | * Runs database seed scripts 4 | */ 5 | 6 | // Command line files are allowed to have console log statements 7 | // tslint:disable no-console 8 | 9 | import { Database } from '../../lib/database/database' 10 | import { setup } from '../utilities' 11 | 12 | export const command = 'seed' 13 | export const describe = 'Seeds the database tables with fake data' 14 | 15 | export async function handler (argv) { 16 | const { app } = setup(argv) 17 | const database = app.get(Database) 18 | 19 | console.log(`Seeding database tables`) 20 | 21 | try { 22 | await database.knex.seed.run() 23 | } catch (e) { 24 | console.error('Error seeding database tables') 25 | console.error(e.message) 26 | process.exit(1) 27 | } 28 | 29 | console.log('Seeded database tables') 30 | process.exit(0) 31 | } 32 | -------------------------------------------------------------------------------- /src/cli/commands/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/commands/seed.ts 3 | * Runs database seed scripts 4 | */ 5 | 6 | // Command line files are allowed to have console log statements 7 | // tslint:disable no-console 8 | 9 | import { Database } from '../../lib/database/database' 10 | import { setup } from '../utilities' 11 | 12 | export const command = 'version' 13 | export const describe = 'Displays Houston version information' 14 | 15 | export async function handler (argv) { 16 | const { config } = setup(argv) 17 | 18 | console.log(`Release: ${config.get('houston.version')}`) 19 | if (config.has('houston.commit')) { 20 | console.log(`Commit: ${config.get('houston.commit')}`) 21 | } else { 22 | console.log('Commit: Unknown') 23 | } 24 | 25 | process.exit(0) 26 | } 27 | -------------------------------------------------------------------------------- /src/cli/utilities.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/cli/utilities.ts 3 | * Some utilities for command line stuff 4 | */ 5 | 6 | // Command line files are allowed to have console log statements 7 | // tslint:disable no-console 8 | 9 | import { App } from '../lib/app' 10 | import { Config, getConfig } from '../lib/config' 11 | import { Logger } from '../lib/log' 12 | 13 | /** 14 | * Sets up some boilderplate application classes based on command line args 15 | * 16 | * @param {Object} argv 17 | * @return {Object} 18 | */ 19 | export function setup (argv): { app: App, config: Config, logger: Logger } { 20 | const config = getConfig(argv.config) 21 | const app = new App(config) 22 | const logger = app.get(Logger) 23 | 24 | process.on('unhandledRejection', (reason) => console.error(reason)) 25 | 26 | return { app, config, logger } 27 | } 28 | -------------------------------------------------------------------------------- /src/client/README.md: -------------------------------------------------------------------------------- 1 | # houston/src/client/ 2 | 3 | This is the client side houston process. It's built with `vue` using the `nuxt` 4 | framework. Currently a work in progress and might change. 5 | -------------------------------------------------------------------------------- /src/client/fonts/open-sans/bold-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/bold-italic.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/bold.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/extra-bold-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/extra-bold-italic.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/extra-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/extra-bold.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/italic.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/light-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/light-italic.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/light.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/regular.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/semi-bold-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/semi-bold-italic.ttf -------------------------------------------------------------------------------- /src/client/fonts/open-sans/semi-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/client/fonts/open-sans/semi-bold.ttf -------------------------------------------------------------------------------- /src/client/images/brand/elementary-logomark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/client/layouts/blank.vue: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/client/layouts/blank.vue 3 | * A basic no thrills layout. 4 | */ 5 | 6 | 9 | 10 | 23 | -------------------------------------------------------------------------------- /src/client/layouts/default.vue: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/client/layouts/default.vue 3 | * The default houston layout with a header, navigation, and footer. 4 | */ 5 | 6 | 13 | 14 | 26 | 27 | 36 | -------------------------------------------------------------------------------- /src/client/pages/error.vue: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/client/pages/error.vue 3 | * An awesome error page for houston 4 | */ 5 | 6 | 19 | 20 | 61 | 62 | 111 | -------------------------------------------------------------------------------- /src/client/pages/index.vue: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/client/pages/index.vue 3 | * The entry page for houston 4 | */ 5 | 6 | 11 | -------------------------------------------------------------------------------- /src/lib/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/app.ts 3 | * IOC container for houston. This is the entrypoint to anything and everything 4 | * sweat in life. 5 | */ 6 | 7 | import { Container, ContainerModule } from 'inversify' 8 | 9 | import { Config } from './config' 10 | 11 | /** 12 | * App 13 | * A houston IOC container 14 | */ 15 | export class App extends Container { 16 | 17 | /** 18 | * A list of all the providers to load in the application. 19 | * 20 | * @var {ContainerModule[]} 21 | */ 22 | protected static providers: ContainerModule[] = [ 23 | require('../repo/provider').provider, 24 | require('../worker/provider').provider, 25 | require('./cache/provider').provider, 26 | require('./database/provider').provider, 27 | require('./log/provider').provider, 28 | require('./queue/provider').provider, 29 | require('./service/provider').provider 30 | ] 31 | 32 | /** 33 | * Creates a new App 34 | * 35 | * @param {Config} config 36 | */ 37 | public constructor (config: Config) { 38 | super() 39 | 40 | this.bind(App).toConstantValue(this) 41 | this.bind(Config).toConstantValue(config) 42 | 43 | this.setupProviders() 44 | } 45 | 46 | /** 47 | * Sets up all of the providers we have throughout the application. 48 | * 49 | * @return {void} 50 | */ 51 | public setupProviders () { 52 | this.load(...App.providers) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/cache/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/cache/index.ts 3 | * Exports useful cache things 4 | */ 5 | 6 | export { 7 | Cache, 8 | ICache, 9 | ICacheFactory 10 | } from './type' 11 | -------------------------------------------------------------------------------- /src/lib/cache/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/cache/provider.ts 3 | * Registers a basic cache factory. 4 | * TODO: Build this out a bit more with redis support 5 | */ 6 | 7 | import { ContainerModule, interfaces } from 'inversify' 8 | import * as Cache from 'lru-cache' 9 | 10 | import * as type from './type' 11 | 12 | export const provider = new ContainerModule((bind) => { 13 | // This is not a great idea. I know. It's a quick and dirty fix. 14 | const instances = {} 15 | 16 | bind>(type.Cache) 17 | .toFactory((context) => (namespace, options = {}) => { 18 | if (instances[namespace]) { 19 | return instances[namespace] 20 | } else { 21 | instances[namespace] = new Cache({ 22 | maxAge: (options.maxAge || 3600) 23 | }) 24 | 25 | return instances[namespace] 26 | } 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/lib/cache/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/cache/type.d.ts 3 | * Types for the cache utility 4 | */ 5 | 6 | export const Cache = Symbol.for('Cache') // tslint:disable-line 7 | 8 | export interface ICacheOptions { 9 | tty?: number 10 | } 11 | 12 | export interface ICache { 13 | get (key: string): Promise 14 | set (key: string, value: string): Promise 15 | } 16 | 17 | export type ICacheFactory = (namespace: string, options?: ICacheOptions) => ICache 18 | -------------------------------------------------------------------------------- /src/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/config/index.ts 3 | * Exports useful config things 4 | */ 5 | 6 | export { Config } from './config' 7 | export { getConfig } from './loader' 8 | -------------------------------------------------------------------------------- /src/lib/database/README.md: -------------------------------------------------------------------------------- 1 | # houston/src/lib/database/ 2 | 3 | This folder holds all of the database logic. Everything is making use of 4 | [knex](http://knexjs.org), so we recommend you read up a bit on that. 5 | 6 | ## Tables 7 | 8 | These are all the current tables we have: 9 | 10 | - `projects` 11 | - `releases` 12 | - `builds` 13 | - `build_logs` 14 | - `build_issues` 15 | - `packages` 16 | 17 | - `github_projects` 18 | - `github_releases` 19 | - `github_build_issues` 20 | 21 | ## Database Design 22 | 23 | The main tables can be described like this: 24 | ``` 25 | projects -> 1:n -> releases -> 1:n -> builds -> 1:n -> packages 26 | ``` 27 | 28 | The `projects`, `releases`, and `build_issues` tables have a polymorphic 29 | relationship to a service table like `github_projects`. This allows us to 30 | integrate other third party services easier, and without changing existing data. 31 | 32 | ## Seeds 33 | 34 | The seed folder contains a bunch of helpful seeds designed to be used in tests 35 | and for development. Here is the lodown for how they are setup. 36 | 37 | ### Projects 38 | 39 | Keymaker 40 | - 3 Releases 41 | - 1 Build 42 | - 4 Build logs 43 | - 1 Package 44 | 45 | AppCenter 46 | - 8 Releases (2 invalid) 47 | - 3 Builds 48 | - 2 Build logs 49 | - 3 Packages 50 | 51 | Code 52 | - 1 Release (1 invalid) 53 | - 0 Builds 54 | - 0 build logs 55 | - 0 Packages 56 | 57 | Terminal 58 | - 2 Releases 59 | - 2 Builds 60 | - 3 Build logs 61 | - 1 Package 62 | 63 | ### Users 64 | 65 | The seed files include multiple users, each one representing a different 66 | permission level. As of right now you will not be able to log into these 67 | accounts. They are only used in tests. 68 | -------------------------------------------------------------------------------- /src/lib/database/database.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/database.ts 3 | * The main database class 4 | * 5 | * @exports {Class} Database - The master database connection class 6 | */ 7 | 8 | import { inject, injectable } from 'inversify' 9 | import * as Knex from 'knex' 10 | import * as path from 'path' 11 | 12 | import { Config } from '../config' 13 | import { Log } from '../log' 14 | 15 | /** 16 | * Database 17 | * The master database connection class 18 | * 19 | * @property {Knex} knex - A knex instance for queries 20 | */ 21 | @injectable() 22 | export class Database { 23 | 24 | public knex: Knex 25 | 26 | protected config: Config 27 | protected log: Log 28 | 29 | /** 30 | * Creates a Database class 31 | * 32 | * @param {Config} config - Configuration for database connection 33 | * @param {Log} [log] - The log instance to use for reporting 34 | */ 35 | constructor (@inject(Config) config: Config, @inject(Log) log: Log) { 36 | const migrationPath = path.resolve(__dirname, 'migration') 37 | const seedPath = path.resolve(__dirname, 'seed') 38 | 39 | // We assign some default file paths for migrations and seeds 40 | const databaseConfig = Object.assign({}, config.get('database'), { 41 | migrations: { 42 | directory: migrationPath, 43 | tableName: 'migrations' 44 | }, 45 | seeds: { 46 | directory: seedPath 47 | }, 48 | useNullAsDefault: false 49 | }) 50 | 51 | this.config = config 52 | this.log = log 53 | 54 | this.knex = Knex(databaseConfig) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/lib/database/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/index.ts 3 | * Export all the things we could possibly use outside of this module. 4 | */ 5 | 6 | export { Database } from './database' 7 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-001-github_repositories.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-001-github_repositories.ts 3 | * The inital houston 2.0.0 migration for GitHub repositories table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('github_repositories', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.integer('key').notNullable().unique() 23 | 24 | table.string('owner').notNullable() 25 | table.string('name').notNullable() 26 | 27 | table.boolean('is_private').notNullable() 28 | 29 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 30 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 31 | table.timestamp('deleted_at').nullable() 32 | }) 33 | } 34 | 35 | /** 36 | * down 37 | * Database information for downgrading version 2.0.0 38 | * 39 | * @param {Object} knex - An initalized Knex package 40 | * @return {Promise} - A promise of successful database migration 41 | */ 42 | export function down (knex: Knex) { 43 | return knex.schema.dropTable('github_repositories') 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-002-github_releases.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-002-github_releases.ts 3 | * The inital houston 2.0.0 migration for GitHub releases table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('github_releases', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.integer('key').notNullable().unique() 23 | 24 | table.string('tag').notNullable() 25 | 26 | table.uuid('github_repository').notNullable() 27 | table.foreign('github_repository').references('id').inTable('github_repositories') 28 | 29 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 30 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 31 | table.timestamp('deleted_at').nullable() 32 | }) 33 | } 34 | 35 | /** 36 | * down 37 | * Database information for downgrading version 2.0.0 38 | * 39 | * @param {Object} knex - An initalized Knex package 40 | * @return {Promise} - A promise of successful database migration 41 | */ 42 | export function down (knex: Knex) { 43 | return knex.schema.dropTable('github_releases') 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-003-users.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-003-users.ts 3 | * The inital houston 2.0.0 migration for users table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('users', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.string('name').notNullable() 23 | table.string('email').notNullable() 24 | 25 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 26 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 27 | table.timestamp('deleted_at').nullable() 28 | }) 29 | } 30 | 31 | /** 32 | * down 33 | * Database information for downgrading version 2.0.0 34 | * 35 | * @param {Object} knex - An initalized Knex package 36 | * @return {Promise} - A promise of successful database migration 37 | */ 38 | export function down (knex: Knex) { 39 | return knex.schema.dropTable('users') 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-004-github_users.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-004-github_users.ts 3 | * The inital houston 2.0.0 migration for github users table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('github_users', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.integer('key').notNullable().unique() 23 | table.string('login').notNullable() 24 | 25 | table.string('name').nullable() 26 | table.string('email').nullable() 27 | table.string('company').nullable() 28 | table.string('avatar').nullable() 29 | 30 | table.string('access_key').nullable() 31 | table.string('scopes').nullable() 32 | 33 | table.uuid('user_id').nullable() 34 | table.foreign('user_id').references('id').inTable('users') 35 | 36 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 37 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 38 | table.timestamp('deleted_at').nullable() 39 | }) 40 | } 41 | 42 | /** 43 | * down 44 | * Database information for downgrading version 2.0.0 45 | * 46 | * @param {Object} knex - An initalized Knex package 47 | * @return {Promise} - A promise of successful database migration 48 | */ 49 | export function down (knex: Knex) { 50 | return knex.schema.dropTable('github_users') 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-005-github_repositories_github_users.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-005-github_repositories_github_users.ts 3 | * The inital houston 2.0.0 migration for pivot table between github users and 4 | * repositories. 5 | * 6 | * @exports {Function} up - Database information for upgrading to version 2.0.0 7 | * @exports {Function} down - Database information for downgrading version 2.0.0 8 | */ 9 | 10 | import * as Knex from 'knex' 11 | 12 | /** 13 | * up 14 | * Database information for upgrading to version 2.0.0 15 | * 16 | * @param {Object} knex - An initalized Knex package 17 | * @return {Promise} - A promise of database migration 18 | */ 19 | export function up (knex: Knex) { 20 | return knex.schema.createTable('github_repositories_github_users', (table) => { 21 | table.increments() 22 | 23 | table.uuid('github_repository_id').nullable() 24 | table.foreign('github_repository_id').references('id').inTable('github_repositories') 25 | 26 | table.uuid('github_user_id').nullable() 27 | table.foreign('github_user_id').references('id').inTable('github_users') 28 | }) 29 | } 30 | 31 | /** 32 | * down 33 | * Database information for downgrading version 2.0.0 34 | * 35 | * @param {Object} knex - An initalized Knex package 36 | * @return {Promise} - A promise of successful database migration 37 | */ 38 | export function down (knex: Knex) { 39 | return knex.schema.dropTable('github_repositories_github_users') 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-006-stripe_accounts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-006-stripe_accounts.ts 3 | * The inital houston 2.0.0 migration for stripe accounts table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('stripe_accounts', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.integer('key').notNullable().unique() 23 | 24 | table.string('name').unique() 25 | table.string('color').nullable() 26 | table.string('url').nullable() 27 | 28 | table.string('public_key').nullable() 29 | table.string('secret_key').nullable() 30 | 31 | table.uuid('user_id').notNullable() 32 | table.foreign('user_id').references('id').inTable('users') 33 | 34 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 35 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 36 | table.timestamp('deleted_at').nullable() 37 | }) 38 | } 39 | 40 | /** 41 | * down 42 | * Database information for downgrading version 2.0.0 43 | * 44 | * @param {Object} knex - An initalized Knex package 45 | * @return {Promise} - A promise of successful database migration 46 | */ 47 | export function down (knex: Knex) { 48 | return knex.schema.dropTable('stripe_accounts') 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-007-projects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-007-projects.ts 3 | * The inital houston 2.0.0 migration for projects table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('projects', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.string('name_domain').unique().index() 23 | table.string('name_human').notNullable() 24 | table.string('name_developer').notNullable() 25 | 26 | table.enu('type', ['application']).defaultTo('application') 27 | 28 | table.uuid('projectable_id').notNullable() 29 | table.string('projectable_type').notNullable() 30 | 31 | table.uuid('stripe_id').nullable() 32 | table.foreign('stripe_id').references('id').inTable('stripe_accounts') 33 | 34 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 35 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 36 | table.timestamp('deleted_at').nullable() 37 | }) 38 | } 39 | 40 | /** 41 | * down 42 | * Database information for downgrading version 2.0.0 43 | * 44 | * @param {Object} knex - An initalized Knex package 45 | * @return {Promise} - A promise of successful database migration 46 | */ 47 | export function down (knex: Knex) { 48 | return knex.schema.dropTable('projects') 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-008-releases.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-008-releases.ts 3 | * The inital houston 2.0.0 migration for releases table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('releases', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.string('version').notNullable() 23 | table.integer('version_major').notNullable() 24 | table.integer('version_minor').notNullable() 25 | table.integer('version_patch').notNullable() 26 | table.integer('version_build').nullable() 27 | 28 | table.boolean('is_prerelease').defaultTo(false) 29 | 30 | table.uuid('releaseable_id').notNullable() 31 | table.string('releaseable_type').notNullable() 32 | 33 | table.uuid('project_id').notNullable() 34 | table.foreign('project_id').references('id').inTable('projects') 35 | 36 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 37 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 38 | table.timestamp('deleted_at').nullable() 39 | }) 40 | } 41 | 42 | /** 43 | * down 44 | * Database information for downgrading version 2.0.0 45 | * 46 | * @param {Object} knex - An initalized Knex package 47 | * @return {Promise} - A promise of successful database migration 48 | */ 49 | export function down (knex: Knex) { 50 | return knex.schema.dropTable('releases') 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-009-builds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-009-builds.ts 3 | * The inital houston 2.0.0 migration for builds table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('builds', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.enum('status', [ 23 | 'queue', 24 | 'build', 25 | 'test', 26 | 'review', 27 | 'publish', 28 | 'fail', 29 | 'error' 30 | ]).nullable() 31 | 32 | table.json('appcenter').nullable() 33 | table.json('appstream').nullable() 34 | 35 | table.uuid('release_id').notNullable() 36 | table.foreign('release_id').references('id').inTable('releases') 37 | 38 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 39 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 40 | table.timestamp('deleted_at').nullable() 41 | }) 42 | } 43 | 44 | /** 45 | * down 46 | * Database information for downgrading version 2.0.0 47 | * 48 | * @param {Object} knex - An initalized Knex package 49 | * @return {Promise} - A promise of successful database migration 50 | */ 51 | export function down (knex: Knex) { 52 | return knex.schema.dropTable('builds') 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/database/migration/2.0.0-010-build_logs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/migration/2.0.0-010-build_logs.ts 3 | * The inital houston 2.0.0 migration for build logs table 4 | * 5 | * @exports {Function} up - Database information for upgrading to version 2.0.0 6 | * @exports {Function} down - Database information for downgrading version 2.0.0 7 | */ 8 | 9 | import * as Knex from 'knex' 10 | 11 | /** 12 | * up 13 | * Database information for upgrading to version 2.0.0 14 | * 15 | * @param {Object} knex - An initalized Knex package 16 | * @return {Promise} - A promise of database migration 17 | */ 18 | export function up (knex: Knex) { 19 | return knex.schema.createTable('build_logs', (table) => { 20 | table.uuid('id').primary() 21 | 22 | table.string('title').notNullable() 23 | table.string('body').notNullable() 24 | 25 | table.string('test').nullable() 26 | 27 | table.json('metadata').nullable() 28 | 29 | table.uuid('build_id').nullable() 30 | table.foreign('build_id').references('id').inTable('builds') 31 | 32 | table.timestamp('created_at').notNullable().defaultTo(knex.fn.now()) 33 | table.timestamp('updated_at').notNullable().defaultTo(knex.fn.now()) 34 | table.timestamp('deleted_at').nullable() 35 | }) 36 | } 37 | 38 | /** 39 | * down 40 | * Database information for downgrading version 2.0.0 41 | * 42 | * @param {Object} knex - An initalized Knex package 43 | * @return {Promise} - A promise of successful database migration 44 | */ 45 | export function down (knex: Knex) { 46 | return knex.schema.dropTable('build_logs') 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/database/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/provider.ts 3 | * Provides the app with the needed Log classes 4 | */ 5 | 6 | import { ContainerModule } from 'inversify' 7 | 8 | import { Database } from './database' 9 | 10 | export const provider = new ContainerModule((bind) => { 11 | bind(Database).toSelf() 12 | }) 13 | -------------------------------------------------------------------------------- /src/lib/database/seed/001-github_repositories.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/001-github_repositories.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the Github repositories table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the Github repositories table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('github_repositories').del() 18 | 19 | await knex('github_repositories').insert({ 20 | created_at: new Date(), 21 | deleted_at: null, 22 | id: 'b272a75e-5263-4133-b2e1-c8894b29493c', 23 | is_private: false, 24 | key: 891357, 25 | name: 'keymaker', 26 | owner: 'btkostner', 27 | updated_at: new Date() 28 | }) 29 | 30 | await knex('github_repositories').insert({ 31 | created_at: new Date(), 32 | deleted_at: null, 33 | id: '14f2dc0d-9648-498e-b06e-a8479e0a7b26', 34 | is_private: false, 35 | key: 8234623, 36 | name: 'appcenter', 37 | owner: 'elementary', 38 | updated_at: new Date() 39 | }) 40 | 41 | await knex('github_repositories').insert({ 42 | created_at: new Date(), 43 | deleted_at: null, 44 | id: 'b353ee74-596a-4ec8-8b1c-11589bb8eb36', 45 | is_private: false, 46 | key: 48913751, 47 | name: 'code', 48 | owner: 'elementary', 49 | updated_at: new Date() 50 | }) 51 | 52 | await knex('github_repositories').insert({ 53 | created_at: new Date(), 54 | deleted_at: null, 55 | id: '274b1d3e-85bd-4ee4-88d9-5ec18f4e87c4', 56 | is_private: false, 57 | key: 49876157, 58 | name: 'terminal', 59 | owner: 'elementary', 60 | updated_at: new Date() 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/database/seed/003-users.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/003-users.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the users table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the users table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('users').del() 18 | 19 | await knex('users').insert({ 20 | created_at: new Date(), 21 | deleted_at: null, 22 | email: 'blake@elementary.io', 23 | id: '24ef2115-67e7-4ea9-8e18-ae6c44b63a71', 24 | name: 'Blake Kostner', 25 | updated_at: new Date() 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/database/seed/004-github_users.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/004-github_users.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the Github users table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the Github users table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('github_users').del() 18 | 19 | await knex('github_users').insert({ 20 | access_key: 'u9r0nuq083ru880589rnyq29nyvaw4etbaw34vtr', 21 | company: 'elementary', 22 | created_at: new Date(), 23 | email: 'blake@elementary.io', 24 | id: 'da527f7e-b865-46e1-a47e-99542d838298', 25 | key: 6423154, 26 | login: 'btkostner', 27 | name: 'Blake Kostner', 28 | scopes: 'public_repo,repo', 29 | updated_at: new Date(), 30 | user_id: '24ef2115-67e7-4ea9-8e18-ae6c44b63a71' 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/database/seed/005-github_repositories_github_users.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/005-github_repositories_github_users.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the Github repositories to Github users table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the Github repositories to Github users table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('github_repositories_github_users').del() 18 | 19 | await knex('github_repositories_github_users').insert({ 20 | github_repository_id: 'b272a75e-5263-4133-b2e1-c8894b29493c', 21 | github_user_id: 'da527f7e-b865-46e1-a47e-99542d838298' 22 | }) 23 | 24 | await knex('github_repositories_github_users').insert({ 25 | github_repository_id: '14f2dc0d-9648-498e-b06e-a8479e0a7b26', 26 | github_user_id: 'da527f7e-b865-46e1-a47e-99542d838298' 27 | }) 28 | 29 | await knex('github_repositories_github_users').insert({ 30 | github_repository_id: 'b353ee74-596a-4ec8-8b1c-11589bb8eb36', 31 | github_user_id: 'da527f7e-b865-46e1-a47e-99542d838298' 32 | }) 33 | 34 | await knex('github_repositories_github_users').insert({ 35 | github_repository_id: '274b1d3e-85bd-4ee4-88d9-5ec18f4e87c4', 36 | github_user_id: 'da527f7e-b865-46e1-a47e-99542d838298' 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/database/seed/006-stripe_accounts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/006-stripe_accounts.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the Stripe accounts table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the Stripe accounts table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('stripe_accounts').del() 18 | 19 | await knex('stripe_accounts').insert({ 20 | color: 'FFA500', 21 | created_at: new Date(), 22 | id: '326599e7-97ed-455a-9c38-122651a12be6', 23 | key: 4235823, 24 | name: 'btkostner', 25 | public_key: 'pk_test_uj0fjv0a9u9302fawfa2rasd', 26 | secret_key: 'sk_test_j89j2098vah803cnb83v298r', 27 | updated_at: new Date(), 28 | url: 'https://btkostner.io', 29 | user_id: '24ef2115-67e7-4ea9-8e18-ae6c44b63a71' 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/database/seed/007-projects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/007-projects.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the projects table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the projects table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('projects').del() 18 | 19 | await knex('projects').insert({ 20 | created_at: new Date(1), 21 | deleted_at: null, 22 | id: '24ef2115-67e7-4ea9-8e18-ae6c44b63a71', 23 | name_developer: 'Blake Kostner', 24 | name_domain: 'com.github.btkostner.keymaker', 25 | name_human: 'Keymaker', 26 | projectable_id: 'b272a75e-5263-4133-b2e1-c8894b29493c', 27 | projectable_type: 'github', 28 | stripe_id: '326599e7-97ed-455a-9c38-122651a12be6', 29 | type: 'application', 30 | updated_at: new Date() 31 | }) 32 | 33 | await knex('projects').insert({ 34 | created_at: new Date(2), 35 | deleted_at: null, 36 | id: '75fa37dc-888d-4905-97bd-73cc9e39be2a', 37 | name_developer: 'elementary LLC', 38 | name_domain: 'com.github.elementary.appcenter', 39 | name_human: 'AppCenter', 40 | projectable_id: '14f2dc0d-9648-498e-b06e-a8479e0a7b26', 41 | projectable_type: 'github', 42 | stripe_id: null, 43 | type: 'application', 44 | updated_at: new Date() 45 | }) 46 | 47 | await knex('projects').insert({ 48 | created_at: new Date(3), 49 | deleted_at: null, 50 | id: '0086b0d2-be43-45fc-8619-989104705c8a', 51 | name_developer: 'elementary LLC', 52 | name_domain: 'com.github.elementary.code', 53 | name_human: 'Code', 54 | projectable_id: 'b353ee74-596a-4ec8-8b1c-11589bb8eb36', 55 | projectable_type: 'github', 56 | stripe_id: null, 57 | type: 'application', 58 | updated_at: new Date() 59 | }) 60 | 61 | await knex('projects').insert({ 62 | created_at: new Date(4), 63 | deleted_at: null, 64 | id: '4a9e027d-c27e-483a-a0fc-b2724a19491b', 65 | name_developer: 'elementary LLC', 66 | name_domain: 'com.github.elementary.terminal', 67 | name_human: 'Terminal', 68 | projectable_id: '274b1d3e-85bd-4ee4-88d9-5ec18f4e87c4', 69 | projectable_type: 'github', 70 | stripe_id: null, 71 | type: 'application', 72 | updated_at: new Date() 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/database/seed/009-builds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/database/seed/009-builds.ts 3 | * Seeds the database 4 | * 5 | * @exports {Function} seed - Seeds the builds table 6 | */ 7 | 8 | import * as Knex from 'knex' 9 | 10 | /** 11 | * seed 12 | * Seeds the builds table 13 | * 14 | * @param {Object} knex - An initalized Knex package 15 | */ 16 | export async function seed (knex: Knex) { 17 | await knex('builds').del() 18 | 19 | // Keymaker builds 20 | await knex('builds').insert({ 21 | appcenter: '{}', 22 | appstream: '{}', 23 | created_at: new Date(), 24 | id: 'dd792007-489f-4742-9f22-bf3eea9c1794', 25 | release_id: '6f3b3345-1b6d-457a-b6ca-5b5a067c4d6c', 26 | status: 'publish', 27 | updated_at: new Date() 28 | }) 29 | 30 | // AppCenter builds 31 | await knex('builds').insert({ 32 | appcenter: '{}', 33 | appstream: '{}', 34 | created_at: new Date(), 35 | id: 'c575d74a-1402-41ed-85a3-de4ebc5f557c', 36 | release_id: '3d49def5-779a-4e2b-9e8e-2ededdbd9bbe', 37 | status: 'fail', 38 | updated_at: new Date() 39 | }) 40 | 41 | await knex('builds').insert({ 42 | appcenter: '{}', 43 | appstream: '{}', 44 | created_at: new Date(), 45 | id: '67dc9947-0519-4471-8951-a89426c63963', 46 | release_id: '393c3985-f743-4daf-b868-73ce6458f4e0', 47 | status: 'error', 48 | updated_at: new Date() 49 | }) 50 | 51 | await knex('builds').insert({ 52 | appcenter: '{}', 53 | appstream: '{}', 54 | created_at: new Date(), 55 | id: 'd63120d9-405e-48e2-91d1-9986730c7ff0', 56 | release_id: '79988df7-60c8-4356-acef-745b8108dfa4', 57 | status: 'publish', 58 | updated_at: new Date() 59 | }) 60 | 61 | // Terminal builds 62 | await knex('builds').insert({ 63 | appcenter: '{}', 64 | appstream: '{}', 65 | created_at: new Date(), 66 | id: '3dc34b70-521c-46fd-990f-3f4c9ad17e4b', 67 | release_id: '1dc97f10-9de5-4c99-808a-0364939d6a96', 68 | status: 'publish', 69 | updated_at: new Date() 70 | }) 71 | 72 | await knex('builds').insert({ 73 | appcenter: '{}', 74 | appstream: '{}', 75 | created_at: new Date(), 76 | id: 'fb2ab4e9-1596-40fd-8453-92bd4cc7965c', 77 | release_id: '877b86e8-9b96-4bf7-8243-fe1905cdd00f', 78 | status: 'review', 79 | updated_at: new Date() 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/log/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/index.ts 3 | * Export all the things we could possibly use outside of this module. 4 | */ 5 | 6 | export { Level } from './level' 7 | export { Log } from './log' 8 | export { Logger } from './logger' 9 | -------------------------------------------------------------------------------- /src/lib/log/level.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/level.ts 3 | * Some log levels 4 | */ 5 | 6 | /** 7 | * The debug level of logs 8 | * 9 | * @var {Symbol} 10 | */ 11 | export const DEBUG = Symbol() 12 | 13 | /** 14 | * The info level of logs 15 | * 16 | * @var {Symbol} 17 | */ 18 | export const INFO = Symbol() 19 | 20 | /** 21 | * The warn level of logs 22 | * 23 | * @var {Symbol} 24 | */ 25 | export const WARN = Symbol() 26 | 27 | /** 28 | * The error level of logs 29 | * 30 | * @var {Symbol} 31 | */ 32 | export const ERROR = Symbol() 33 | 34 | /** 35 | * An enum representing all of the log levels 36 | * 37 | * @var {enum} 38 | */ 39 | export enum Level { DEBUG, INFO, WARN, ERROR } 40 | 41 | /** 42 | * Parses a string value for a level symbol 43 | * 44 | * @param {string} level 45 | * @return {Level} 46 | */ 47 | export function parseLevel (level: string): Level { 48 | switch (level.toLowerCase().trim()) { 49 | case ('debug'): 50 | return Level.DEBUG 51 | case ('info'): 52 | return Level.INFO 53 | case ('warn'): 54 | return Level.WARN 55 | case ('error'): 56 | return Level.ERROR 57 | default: 58 | return Level.INFO 59 | } 60 | } 61 | 62 | /** 63 | * Returns a string given a level symbol 64 | * 65 | * @param {Level} level 66 | * @return {string} 67 | */ 68 | export function levelString (level: Level): string { 69 | switch (level) { 70 | case (Level.DEBUG): 71 | return 'debug' 72 | case (Level.INFO): 73 | return 'info' 74 | case (Level.WARN): 75 | return 'warn' 76 | case (Level.ERROR): 77 | return 'error' 78 | default: 79 | return 'info' 80 | } 81 | } 82 | 83 | /** 84 | * Returns a number index of severity for a level symbol 85 | * 86 | * @param {Level} level 87 | * @return {Number} 88 | */ 89 | export function levelIndex (level: Level): number { 90 | switch (level) { 91 | case (Level.DEBUG): 92 | return 0 93 | case (Level.INFO): 94 | return 1 95 | case (Level.WARN): 96 | return 2 97 | case (Level.ERROR): 98 | return 3 99 | default: 100 | return 1 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/lib/log/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/log.ts 3 | * A log message with super powers. 4 | * 5 | * @exports {Class} Log - A single log line. 6 | */ 7 | 8 | import { injectable } from 'inversify' 9 | 10 | import { Level } from './level' 11 | import { Logger } from './logger' 12 | 13 | /** 14 | * Log 15 | * A single log line. 16 | */ 17 | @injectable() 18 | export class Log { 19 | 20 | /** 21 | * The level of the log 22 | * 23 | * @var {Level} 24 | */ 25 | public level: Level 26 | 27 | /** 28 | * The log message 29 | * 30 | * @var {String|Null} 31 | */ 32 | public message?: string 33 | 34 | /** 35 | * Attached data to the log 36 | * 37 | * @var {Object} 38 | */ 39 | public data: object 40 | 41 | /** 42 | * An error attached to the log 43 | * 44 | * @var {Error} 45 | */ 46 | public error?: Error 47 | 48 | /** 49 | * The date the log was created 50 | * 51 | * @var {Date} 52 | */ 53 | protected date: Date 54 | 55 | /** 56 | * The current logger to use for sending the log. 57 | * 58 | * @var {Logger} 59 | */ 60 | protected logger: Logger 61 | 62 | /** 63 | * Creates a new log with default values 64 | * 65 | * @param {Logger} logger 66 | */ 67 | public constructor (logger: Logger) { 68 | this.level = Level.DEBUG 69 | this.data = {} 70 | this.date = new Date() 71 | 72 | this.logger = logger 73 | } 74 | 75 | /** 76 | * Sets the log level 77 | * 78 | * @param {Level} level 79 | * 80 | * @return {Log} 81 | */ 82 | public setLevel (level: Level): this { 83 | this.level = level 84 | 85 | return this 86 | } 87 | 88 | /** 89 | * Sets the log message 90 | * 91 | * @param {String} message 92 | * 93 | * @return {Log} 94 | */ 95 | public setMessage (message: string): this { 96 | this.message = message 97 | 98 | return this 99 | } 100 | 101 | /** 102 | * Sets data in the log 103 | * 104 | * @param {String} key 105 | * @param {*} value 106 | * 107 | * @return {Log} 108 | */ 109 | public setData (key: string, value): this { 110 | this.data[key] = value 111 | 112 | return this 113 | } 114 | 115 | /** 116 | * A shorthand for attaching an error message to a log 117 | * 118 | * @param {Error} err 119 | * 120 | * @return {Log} 121 | */ 122 | public setError (err: Error): this { 123 | this.error = err 124 | 125 | return this 126 | } 127 | 128 | /** 129 | * Gets the date this log was created. 130 | * 131 | * @return {Date} 132 | */ 133 | public getDate (): Date { 134 | return this.date 135 | } 136 | 137 | /** 138 | * Sends the log to what ever services / places it needs to be. 139 | * 140 | * @return {void} 141 | */ 142 | public send () { 143 | return this.logger.send(this) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/lib/log/output.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/output.ts 3 | * An abstract class for sending logs somewhere. 4 | */ 5 | 6 | // Disabled because of abstract and interfaces are made to be extended. 7 | // tslint:disable:no-unused-variable 8 | 9 | import { injectable } from 'inversify' 10 | 11 | import { Config } from '../config' 12 | import { Log } from './log' 13 | 14 | /** 15 | * A generic abstract class for handling logs. 16 | */ 17 | @injectable() 18 | export abstract class Output { 19 | /** 20 | * Checks if we should enable this output 21 | * 22 | * @param {Config} config 23 | * 24 | * @return {boolean} 25 | */ 26 | public static enabled (config: Config): boolean { 27 | return true 28 | } 29 | 30 | /** 31 | * Creates a new logger output 32 | * 33 | * @param {Config} config 34 | */ 35 | public constructor (config: Config) { 36 | return 37 | } 38 | 39 | /** 40 | * Does something with a debug log. 41 | * 42 | * @param {Log} log 43 | * @return {void} 44 | */ 45 | public debug (log: Log) { 46 | return 47 | } 48 | 49 | /** 50 | * Does something with a info log. 51 | * 52 | * @param {Log} log 53 | * @return {void} 54 | */ 55 | public info (log: Log) { 56 | return 57 | } 58 | 59 | /** 60 | * Does something with a warn log. 61 | * 62 | * @param {Log} log 63 | * @return {void} 64 | */ 65 | public warn (log: Log) { 66 | return 67 | } 68 | 69 | /** 70 | * Does something with a error log. 71 | * 72 | * @param {Log} log 73 | * @return {void} 74 | */ 75 | public error (log: Log) { 76 | return 77 | } 78 | } 79 | 80 | /** 81 | * An interface of the Output class as a constructor. 82 | * This is kinda pointless, but it keeps typescript happy when hinting. 83 | */ 84 | export interface OutputConstructor { 85 | new (config: Config) 86 | enabled (config: Config): boolean 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/log/outputs/console.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/services/console.ts 3 | * Outputs logs to the console 4 | */ 5 | 6 | // Disabled because this file is all about console logs 7 | // tslint:disable:no-console 8 | 9 | import { Config } from '../../config' 10 | import { Level, parseLevel } from '../level' 11 | import { Log } from '../log' 12 | import { Output } from '../output' 13 | 14 | export class Console extends Output { 15 | 16 | /** 17 | * Configuration to use for console logs. 18 | * 19 | * @var {Config} 20 | */ 21 | protected config: Config 22 | 23 | /** 24 | * Checks if this output should be enabled 25 | * 26 | * @param {Config} config 27 | * 28 | * @return {boolean} 29 | */ 30 | public static enabled (config: Config): boolean { 31 | if (config.has('log.console') === false) { 32 | return false 33 | } 34 | 35 | return (config.get('log.console') !== 'never') 36 | } 37 | 38 | /** 39 | * Creates a new Sentry output 40 | * 41 | * @param {Config} config 42 | */ 43 | public constructor (config: Config) { 44 | super(config) 45 | 46 | this.config = config 47 | } 48 | 49 | /** 50 | * Sends debug info to the console 51 | * 52 | * @param {Log} log 53 | * @return {void} 54 | */ 55 | public debug (log: Log) { 56 | console.info(log.message) 57 | } 58 | 59 | /** 60 | * Logs a message to the console 61 | * 62 | * @param {Log} log 63 | * @return {void} 64 | */ 65 | public info (log: Log) { 66 | console.info(log.message) 67 | } 68 | 69 | /** 70 | * Logs a warning log to the console 71 | * 72 | * @param {Log} log 73 | * @return {void} 74 | */ 75 | public warn (log: Log) { 76 | console.warn(log.message) 77 | } 78 | 79 | /** 80 | * Logs an error to the console 81 | * 82 | * @param {Log} log 83 | * @return {void} 84 | */ 85 | public error (log: Log) { 86 | console.error(log.message) 87 | } 88 | 89 | /** 90 | * Checks if the configuration allows a given log level. 91 | * 92 | * @param {Level} level 93 | * 94 | * @return {Boolean} 95 | */ 96 | protected allows (level: Level) { 97 | if (this.config.has('log.console') === false) { 98 | return false 99 | } 100 | 101 | const configLevel = parseLevel(this.config.get('log.console')) 102 | 103 | if (level >= configLevel) { 104 | return true 105 | } 106 | 107 | return false 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/lib/log/outputs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/outputs/index.ts 3 | * A list of outputs we can send logs to 4 | */ 5 | 6 | export { Console } from './console' 7 | export { Sentry } from './sentry' 8 | -------------------------------------------------------------------------------- /src/lib/log/outputs/sentry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/services/sentry.ts 3 | * Handles logging errors to sentry 4 | */ 5 | 6 | import { Config } from '../../config' 7 | import { Log } from '../log' 8 | import { Output } from '../output' 9 | 10 | export class Sentry extends Output { 11 | 12 | /** 13 | * The current application configuration 14 | * 15 | * @var {Config} 16 | */ 17 | protected config: Config 18 | 19 | /** 20 | * The sentry dns to use when reporting logs 21 | * 22 | * @var {String} 23 | */ 24 | protected dns: string 25 | 26 | /** 27 | * A raven instance for logging to sentry 28 | * 29 | * @var {Raven} 30 | */ 31 | protected raven 32 | 33 | /** 34 | * Checks if this output should be enabled 35 | * 36 | * @return {boolean} 37 | */ 38 | public static enabled (config: Config): boolean { 39 | if (config.has('log.sentry') === false) { 40 | return false 41 | } else if (config.has('service.sentry.secret') === false) { 42 | return false 43 | } 44 | 45 | try { 46 | require.resolve('raven') 47 | } catch (e) { 48 | return false 49 | } 50 | 51 | return (config.get('log.sentry') !== 'never') 52 | } 53 | 54 | /** 55 | * Creates a new Sentry output 56 | * 57 | * @param {Config} config 58 | */ 59 | public constructor (config: Config) { 60 | super(config) 61 | 62 | this.config = config 63 | this.dns = config.get('service.sentry.secret') 64 | 65 | this.raven = this.setup() 66 | } 67 | 68 | /** 69 | * Sends error logs to sentry 70 | * 71 | * @param {Log} log 72 | * @return {void} 73 | */ 74 | public error (log: Log) { 75 | this.raven.captureException(this.toError(log)) 76 | } 77 | 78 | /** 79 | * Transforms a log message to an error 80 | * 81 | * @param {Log} log 82 | * 83 | * @return {Error} 84 | */ 85 | public toError (log: Log): Error { 86 | const error = new Error(log.message) 87 | 88 | // Add a stack trace no including this function 89 | Error.captureStackTrace(error, this.toError) 90 | Object.assign(error, log.data, { error: log.error }) 91 | 92 | return error 93 | } 94 | 95 | /** 96 | * Sets up raven with common metadata and things. 97 | * 98 | * @return {Raven} 99 | */ 100 | protected setup () { 101 | return require('raven') 102 | .config(this.dns) 103 | .install() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/lib/log/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/log/provider.ts 3 | * Provides the app with the needed Log classes 4 | */ 5 | 6 | import { ContainerModule } from 'inversify' 7 | 8 | import { Log } from './log' 9 | import { Logger } from './logger' 10 | import { Output } from './output' 11 | import { Console } from './outputs/console' 12 | import { Sentry } from './outputs/sentry' 13 | 14 | export const provider = new ContainerModule((bind) => { 15 | bind(Output).toConstructor(Console) 16 | bind(Output).toConstructor(Sentry) 17 | 18 | bind(Log).toConstructor(Log) 19 | 20 | bind(Logger).toSelf() 21 | }) 22 | -------------------------------------------------------------------------------- /src/lib/queue/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/queue/index.ts 3 | * Exports all you need to get started with the queue system. 4 | */ 5 | 6 | export { IQueue, IQueueConstructor, Status } from './type' 7 | 8 | export const Queue = Symbol.for('Queue') // tslint:disable-line 9 | export const workerQueue = Symbol.for('workerQueue') // tslint:disable-line 10 | -------------------------------------------------------------------------------- /src/lib/queue/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/queue/provider.ts 3 | * Sets up the needed providers for the Queue system 4 | */ 5 | 6 | import { ContainerModule, interfaces } from 'inversify' 7 | 8 | import { Config } from '../config' 9 | import { Queue, workerQueue } from './index' 10 | import { Queue as RedisQueue } from './providers/redis' 11 | import { IQueue, IQueueConstructor } from './type' 12 | 13 | export const provider = new ContainerModule((bind) => { 14 | bind(Queue).toFactory((context: interfaces.Context) => { 15 | return function QueueFactory (name: string) { 16 | const config = context.container.get(Config) 17 | 18 | if (config.get('queue.client') === 'redis') { 19 | try { 20 | require.resolve('bull') 21 | } catch (e) { 22 | throw new Error('Package "bull" is not installed. Please install it.') 23 | } 24 | 25 | return new RedisQueue(config, name) 26 | } 27 | 28 | if (config.has('queue.client') === false) { 29 | throw new Error('No queue client configured') 30 | } else { 31 | throw new Error(`Unknown queue client of "${config.get('queue.client')}" configured`) 32 | } 33 | } 34 | }) 35 | 36 | bind(workerQueue).toDynamicValue((context: interfaces.Context) => { 37 | const queueConstructor = context.container.get(Queue) 38 | 39 | return queueConstructor('WorkerQueue') 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/lib/queue/providers/redis/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/queue/providers/redis/index.ts 3 | * Exports bull components 4 | */ 5 | 6 | export { Queue } from './queue' 7 | export { Job } from './job' 8 | -------------------------------------------------------------------------------- /src/lib/queue/providers/redis/job.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/queue/providers/redis/job.ts 3 | * Wraps `bull` package in types for use in our queue system. 4 | */ 5 | 6 | import * as BaseBull from 'bull' 7 | import { EventEmitter } from 'events' 8 | 9 | import * as type from '../../type' 10 | 11 | export class Job extends EventEmitter implements type.IJob { 12 | 13 | /** 14 | * The bull instance we will be proxying to 15 | * 16 | * @var {Bull} 17 | */ 18 | protected bull: BaseBull.Job 19 | 20 | /** 21 | * Creates a new queue with the given name 22 | * 23 | * @param {String} name 24 | */ 25 | constructor (job: BaseBull.Job) { 26 | super() 27 | 28 | this.bull = job 29 | } 30 | 31 | public async status (): Promise { 32 | const state = await this.bull.getState() 33 | 34 | switch (state) { 35 | case ('waiting'): 36 | return 'waiting' 37 | case ('active'): 38 | return 'active' 39 | case ('completed'): 40 | return 'completed' 41 | case ('failed'): 42 | return 'failed' 43 | case ('delayed'): 44 | return 'delayed' 45 | default: 46 | return 'failed' 47 | } 48 | } 49 | 50 | public async progress (amount) { 51 | return this.bull.progress(amount) 52 | } 53 | 54 | public async remove () { 55 | return this.bull.remove() 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/queue/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/queue/type.ts 3 | * Some typescript types for a queue system. 4 | */ 5 | 6 | import { EventEmitter } from 'events' 7 | 8 | export type Status = 'waiting' | 'active' | 'completed' | 'failed' | 'delayed' 9 | 10 | export type HandleCallback = (job: IJob) => Promise 11 | 12 | export type OnActiveCallback = (job: IJob) => void 13 | export type OnProgressCallback = (job: IJob, amount: number) => void 14 | export type OnFailedCallback = (job: IJob, error: Error) => void 15 | export type OnCompletedCallback = (job: IJob, result: object) => void 16 | 17 | export type IQueueConstructor = (name: string) => IQueue 18 | 19 | export interface IQueue { 20 | send (data: object, opts?: IJobOptions): Promise 21 | handle (fn: HandleCallback) 22 | 23 | pause (local: boolean): Promise 24 | resume (local: boolean): Promise 25 | 26 | empty (): Promise 27 | close (): Promise 28 | count (state?: Status): Promise 29 | jobs (state: Status): Promise 30 | 31 | onActive (fn: OnActiveCallback) 32 | onProgress (fn: OnProgressCallback) 33 | onFailed (fn: OnFailedCallback) 34 | onCompleted (fn: OnCompletedCallback) 35 | } 36 | 37 | export interface IJobOptions { 38 | priority?: number 39 | delay?: number 40 | attempts?: number 41 | timeout?: number 42 | } 43 | 44 | export interface IJob { 45 | status (): Promise 46 | progress (amount: number) 47 | 48 | remove (): Promise 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/service/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/service/index.ts 3 | * Entry file to all of the fun third party services we interact with 4 | */ 5 | 6 | import * as type from './type' 7 | 8 | import { GitHub } from './github' 9 | 10 | // Typescript typeguard to ensure given value is an ICodeRepository 11 | export function isCodeRepository (value): value is type.ICodeRepository { 12 | return (value != null && typeof value.clone === 'function') 13 | } 14 | 15 | // Typescript typeguard to ensure given value is an IPackageRepository 16 | export function isPackageRepository (value): value is type.IPackageRepository { 17 | return (value != null && typeof value.uploadPackage === 'function') 18 | } 19 | 20 | // Typescript typeguard to ensure given value is an ILogRepository 21 | export function isLogRepository (value): value is type.ILogRepository { 22 | return (value != null && typeof value.uploadLog === 'function') 23 | } 24 | 25 | export { 26 | ICodeRepository, 27 | ICodeRepositoryFactory, 28 | ILog, 29 | IPackage, 30 | IPackageRepository, 31 | IServiceIds 32 | } from './type' 33 | 34 | export { Aptly } from './aptly' 35 | export { github, IGitHubFactory } from './github' 36 | 37 | export const codeRepository = Symbol('codeRepository') 38 | export const packageRepository = Symbol('packageRepository') 39 | export const logRepository = Symbol('logRepository') 40 | 41 | export const codeRepositoryFactory = Symbol('codeRepositoryFactory') 42 | -------------------------------------------------------------------------------- /src/lib/service/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/service/provider.ts 3 | * Registers all the fun third party services that require the IoC container 4 | */ 5 | 6 | import { ContainerModule, interfaces } from 'inversify' 7 | 8 | import { 9 | codeRepository, 10 | codeRepositoryFactory, 11 | logRepository, 12 | packageRepository 13 | } from './index' 14 | 15 | import { Aptly } from './aptly' 16 | import { GitHub, github, IGitHubFactory } from './github' 17 | import * as type from './type' 18 | 19 | export const provider = new ContainerModule((bind) => { 20 | bind>(github) 21 | .toFactory((context) => (url) => { 22 | const instance = context.container.resolve(GitHub) 23 | instance.url = url 24 | 25 | return instance 26 | }) 27 | 28 | bind(codeRepositoryFactory).toProvider((context) => (url) => { 29 | return new Promise((resolve) => { 30 | return resolve(context.container.get(github)(url)) 31 | }) 32 | }) 33 | 34 | bind(Aptly).toSelf() 35 | 36 | bind(packageRepository).to(Aptly) 37 | }) 38 | -------------------------------------------------------------------------------- /src/lib/utility/eventemitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/utility/eventemitter.ts 3 | * An event emitter based on eventemitter2 with some nice added features 4 | */ 5 | 6 | import { EventEmitter2 } from 'eventemitter2' 7 | import * as defaultsDeep from 'lodash/defaultsDeep' 8 | 9 | const DEFAULT_OPTS = { 10 | delimiter: ':', 11 | maxListeners: 10, 12 | newListener: false, 13 | verboseMemoryLeak: true, 14 | wildcard: true 15 | } 16 | 17 | export class EventEmitter extends EventEmitter2 { 18 | /** 19 | * Creates a new event emitter 20 | * 21 | * @param {Object} [opts] 22 | */ 23 | public constructor (opts = {}) { 24 | super(defaultsDeep({}, DEFAULT_OPTS, opts)) 25 | } 26 | 27 | /** 28 | * This emites an async event, that will resolve the results by running 29 | * listeners step by step. This is great for things that can be extended and 30 | * modified by listeners. 31 | * 32 | * @param {string} event 33 | * @param {*} arg 34 | * @return {*} - Results of arg after modification from listeners 35 | */ 36 | public async emitAsyncChain (event, arg): Promise { 37 | const listeners = this.listeners(event) 38 | let value = arg 39 | 40 | for (const listener of listeners) { 41 | await Promise.resolve(listener(value)) 42 | .then((result) => (value = result)) 43 | } 44 | 45 | return value 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/utility/glob.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/utility/glob.ts 3 | * Promiseifies the glob package function 4 | */ 5 | 6 | import * as Glob from 'glob' 7 | 8 | /** 9 | * A promise-ify passthrough function for glob 10 | * 11 | * @async 12 | * 13 | * @param {String} pattern 14 | * @param {Object} [options] 15 | * 16 | * @return {String[]} 17 | */ 18 | export function glob (pattern, options = {}): Promise { 19 | return new Promise((resolve, reject) => { 20 | Glob(pattern, options, (err, res) => { 21 | if (err != null) { 22 | return reject(err) 23 | } else { 24 | return resolve(res) 25 | } 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/utility/markdown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/utility/markdown.ts 3 | * Parses markdown to an html string 4 | * 5 | * @example 6 | * ``` 7 | * import markdown from 'lib/utility/markdown' 8 | * 9 | * return markdown('# this is a string') 10 | * ``` 11 | */ 12 | 13 | import { defaultsDeep } from 'lodash' 14 | import * as Markdown from 'markdown-it' 15 | 16 | const DEFAULT_OPTS = { 17 | breaks: true, 18 | html: false, 19 | linkify: true, 20 | quotes: '“”‘’', 21 | typographer: true, 22 | xhtmlOut: true 23 | } 24 | 25 | /** 26 | * Renders a markdown string to html 27 | * 28 | * @param {String} str 29 | * @param {Object} [opts] 30 | * @return {string} 31 | */ 32 | export default function (str, opts = {}) { 33 | const markdown = new Markdown(defaultsDeep({}, DEFAULT_OPTS, opts)) 34 | 35 | return markdown.render(str) 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/utility/rdnn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/utility/rdnn.ts 3 | * Some higher level RDNN functions 4 | */ 5 | 6 | /** 7 | * Sanitizes an RDNN string for common mistakes and better unification 8 | * @see https://github.com/elementary/houston/issues/566 9 | * 10 | * @param {string} rdnn 11 | * @param {string} [normalizer] The string to use instead of spaces 12 | * @return {string} 13 | */ 14 | export function sanitize (rdnn: string, normalizer = '_'): string { 15 | return rdnn 16 | .replace(/\s/gi, normalizer) 17 | .replace(/\.([0-9])/gi, `.${normalizer}$1`) 18 | .replace(/\_|\-/gi, normalizer) 19 | .toLowerCase() 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/utility/template.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/utility/template.ts 3 | * A simple way to template strings using ejs 4 | * 5 | * @example 6 | * ``` 7 | * import template from 'lib/utility/template' 8 | * 9 | * return template('# <%= title %>', { title: 'testing' }) 10 | * ``` 11 | */ 12 | 13 | import * as ejs from 'ejs' 14 | 15 | export default ejs.render 16 | -------------------------------------------------------------------------------- /src/repo/README.md: -------------------------------------------------------------------------------- 1 | # houston/src/repo 2 | 3 | This process is responsible for receiving data from a web server via syslog. It 4 | then parses the message for a file path, and increments the download count of 5 | the package. 6 | 7 | ## Example 8 | 9 | elementary serves an aptly repository behind a nginx server. Here is a tidbit of 10 | the nginx configuration as an example: 11 | 12 | ``` 13 | # repository.conf 14 | # Configures the static repository domain with houston syslog hook 15 | 16 | log_format repo '$remote_addr|$status|$request_filename|$body_bytes_sent|$http_user_agent|$request_time'; 17 | 18 | # Standard HTTP route block 19 | server { 20 | server_name _; 21 | 22 | listen 80; 23 | listen [::]:80; 24 | 25 | root /var/repository/aptly/public; 26 | 27 | access_log syslog:server=localhost:3000 repo; 28 | } 29 | ``` 30 | 31 | Note that the package paths should be similar to: 32 | ``` 33 | /appcenter/pool/main/c/com.github.danrabbit.nimbus/com.github.danrabbit.nimbus_0.2.0_amd64.deb 34 | ``` 35 | 36 | The last path segment needs to be in the format of: 37 | ``` 38 | __.deb 39 | ``` 40 | -------------------------------------------------------------------------------- /src/repo/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/repo/provider.ts 3 | * Provides the app with the needed Repo server classes 4 | */ 5 | 6 | import { ContainerModule } from 'inversify' 7 | 8 | import { Repo } from './repo' 9 | 10 | export const provider = new ContainerModule((bind) => { 11 | bind(Repo).toSelf() 12 | }) 13 | -------------------------------------------------------------------------------- /src/worker/README.md: -------------------------------------------------------------------------------- 1 | # houston/src/worker/ 2 | 3 | This folder holds all the logic for testing, building, and fixing projects. If 4 | something goes wrong when building your project, it's probably because of code 5 | in this folder. 6 | 7 | ## worker.ts 8 | 9 | This class holds the basic information about any task we do. It holds the 10 | repository, working directory, and storage of build data. 11 | 12 | ## role/ 13 | 14 | These holds the different types of work we can do. They are mainly for putting 15 | together tasks in an easy to understand result, like building or publishing a 16 | project. 17 | 18 | ## task/ 19 | 20 | These are the most basic of tasks we can do. They only do one thing, and are 21 | very easy to test. Examples include parsing the apphub file, publishing a 22 | package to github, and building a deb package. 23 | 24 | ## The build directory 25 | 26 | Every build gets its own unique folder in the OS temporary folder. In most Linux 27 | system this ends up being `/tmp/houston`. It then has a UUID generated folder to 28 | hold the process workspace. If all tasks where ran, the workspace would end up 29 | looking similar to this 30 | ``` 31 | /tmp/houston/ad1553ea-7a27-44cd-8eb7-66540c4ad77c/ 32 | ├── clean/ # Untouched cloned repository 33 | ├── dirty/ # Folder touched during building and testing 34 | └── repository/ # Folders for each branch we used to construct the clean folder 35 | ``` 36 | -------------------------------------------------------------------------------- /src/worker/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/index.ts 3 | * Exports all the public parts of the worker process. 4 | */ 5 | 6 | export { 7 | IResult, 8 | IChange, 9 | IContext 10 | } from './type' 11 | 12 | export { Worker } from './worker' 13 | 14 | // Export presets 15 | export { Build } from './preset/build' 16 | export { Release } from './preset/release' 17 | -------------------------------------------------------------------------------- /src/worker/preset/build.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/preset/build.ts 3 | * Builds a package and edits contents for appcenter. 4 | */ 5 | 6 | import { App } from '../../lib/app' 7 | import { ICodeRepository } from '../../lib/service' 8 | import * as type from '../type' 9 | import { Worker } from '../worker' 10 | 11 | import { Appstream } from '../task/appstream' 12 | import { BuildDeb } from '../task/build/deb' 13 | import { DebianChangelog } from '../task/debian/changelog' 14 | import { DebianControl } from '../task/debian/control' 15 | import { Desktop } from '../task/desktop' 16 | import { ExtractDeb } from '../task/extract/deb' 17 | import { FileDeb } from '../task/file/deb' 18 | import { PackDeb } from '../task/pack/deb' 19 | import { WorkspaceSetup } from '../task/workspace/setup' 20 | 21 | function buildTasks (t: type.Type): type.ITaskConstructor[] { 22 | switch (t) { 23 | case 'library': 24 | case 'system-library': 25 | return [ 26 | WorkspaceSetup, 27 | DebianChangelog, 28 | DebianControl, 29 | BuildDeb 30 | ] 31 | case 'system-app': 32 | return [ 33 | WorkspaceSetup, 34 | BuildDeb 35 | ] 36 | default: 37 | return [ 38 | WorkspaceSetup, 39 | DebianChangelog, 40 | DebianControl, 41 | BuildDeb, 42 | ExtractDeb, 43 | FileDeb, 44 | Appstream, 45 | Desktop, 46 | PackDeb 47 | ] 48 | } 49 | } 50 | 51 | export function Build (app: App, repository: ICodeRepository, context: type.IContext): type.IWorker { 52 | const worker = new Worker(app, repository, context) 53 | 54 | for (const task of buildTasks(context.type)) { 55 | worker.tasks.push(task) 56 | } 57 | 58 | return worker 59 | } 60 | -------------------------------------------------------------------------------- /src/worker/preset/release.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/preset/release.ts 3 | * Releases a package to all able endpoints. 4 | */ 5 | 6 | import { App } from '../../lib/app' 7 | import { ICodeRepository } from '../../lib/service' 8 | import * as type from '../type' 9 | 10 | import { Build } from './build' 11 | 12 | import { Upload } from '../task/upload' 13 | 14 | export function Release (app: App, repository: ICodeRepository, context: type.IContext): type.IWorker { 15 | const worker = Build(app, repository, context) 16 | 17 | worker.postTasks.push(Upload) 18 | 19 | return worker 20 | } 21 | -------------------------------------------------------------------------------- /src/worker/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/provider.ts 3 | * Provides the app with the main worker classes 4 | */ 5 | 6 | import { ContainerModule } from 'inversify' 7 | 8 | import { Worker } from './index' 9 | 10 | export const provider = new ContainerModule((bind) => { 11 | bind(Worker).toConstructor(Worker) 12 | }) 13 | -------------------------------------------------------------------------------- /src/worker/task/appstream/description.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/description.ts 3 | * Checks we have a description. 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamDescription extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path) 42 | const $ = cheerio.load(raw, { xmlMode: true }) 43 | 44 | const description = $('component > description') 45 | 46 | if (description.length === 0) { 47 | throw new Log(Log.Level.ERROR, 'Missing "description" field') 48 | } 49 | 50 | const text = description.text() 51 | 52 | if (text.toLowerCase().replace(/\W/, '').indexOf('elementaryos') !== -1) { 53 | throw new Log(Log.Level.ERROR, '"description" field calls out elementary OS') 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/worker/task/appstream/exist.md: -------------------------------------------------------------------------------- 1 | # Appstream file does not exist 2 | 3 | AppCenter was unable to find your appstream file. Please ensure you have an 4 | appstream file that is placed in `/usr/share/metainfo`. It should be named 5 | `<%- name %>.appdata.xml`. 6 | 7 | ### For more information, see: 8 | - [The AppStream Quickstart Guide](https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html) 9 | - [The AppStream Metadata Specification](https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html) 10 | -------------------------------------------------------------------------------- /src/worker/task/appstream/id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/id.ts 3 | * Tests the appstream ID matches correctly 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamId extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path) 42 | const $ = cheerio.load(raw, { xmlMode: true }) 43 | 44 | const id = $('component > id') 45 | 46 | if (id.length === 0) { 47 | $('component').prepend(`${this.name}`) 48 | await fs.writeFile(this.path, $.xml()) 49 | 50 | throw new Log(Log.Level.WARN, 'Missing "id" field') 51 | } else if (id.text() !== this.name) { 52 | id.text(this.name) 53 | await fs.writeFile(this.path, $.xml()) 54 | 55 | throw new Log(Log.Level.WARN, `"id" field should be "${this.name}"`) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/worker/task/appstream/index.md: -------------------------------------------------------------------------------- 1 | # Appstream tests fail 2 | 3 | AppCenter ran a bunch of tests on your appstream file. These are the results. 4 | 5 | <%_ if ((errors || []).length > 0) { _%> 6 | ## Errors: 7 | <%_ (errors || []).forEach(function (error) { _%> 8 | - <%- (error.title || 'Unknown internal error') %> 9 | <%_ }) _%> 10 | <%_ } _%> 11 | 12 | <%_ if ((warnings || []).length > 0) { _%> 13 | ## Warnings: 14 | Most of these are fixed during the building process, but you should ensure they 15 | are fixed in your code for future releases. 16 | <%_ (warnings || []).forEach(function (warn) { _%> 17 | - <%- (warn.title || 'Unknown internal error') %> 18 | <%_ }) _%> 19 | <%_ } _%> 20 | 21 | ### For more information, see: 22 | - [The AppStream Quickstart Guide](https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html) 23 | - [The AppStream Metadata Specification](https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html) 24 | -------------------------------------------------------------------------------- /src/worker/task/appstream/license.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/license.ts 3 | * Checks that a project_license is in the appstream file 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamLicense extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path) 42 | const $ = cheerio.load(raw, { xmlMode: true }) 43 | 44 | const license = $('component > project_license') 45 | 46 | if (license.length === 0) { 47 | throw new Log(Log.Level.WARN, 'Missing "project_license" field') 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/worker/task/appstream/name.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/name.ts 3 | * Checks that a name field exists in the appstream file 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamName extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path) 42 | const $ = cheerio.load(raw, { xmlMode: true }) 43 | 44 | const name = $('component > name') 45 | 46 | if (name.length === 0) { 47 | $('component').prepend(`${this.worker.context.nameHuman}`) 48 | await fs.writeFile(this.path, $.xml()) 49 | 50 | throw new Log(Log.Level.WARN, 'Missing "name" field') 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/worker/task/appstream/screenshot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/screenshot.ts 3 | * Ensures the developer includes a screenshot 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamScreenshot extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path) 42 | const $ = cheerio.load(raw, { xmlMode: true }) 43 | 44 | const screenshots = $('component > screenshots > screenshot') 45 | 46 | if (screenshots.length < 1) { 47 | throw new Log(Log.Level.ERROR, 'Missing screenshots') 48 | } 49 | 50 | screenshots.each((i, elem) => this.checkTag($, elem)) 51 | } 52 | 53 | /** 54 | * Checks a screenshot tag in appstream file 55 | * 56 | * @param {Object} elem - Cheerio element 57 | * @return {void} 58 | */ 59 | protected checkTag ($, elem) { 60 | const screenshot = $(elem) 61 | const image = $('image', screenshot) 62 | 63 | if (image.length < 1) { 64 | this.worker.report(new Log(Log.Level.ERROR, 'Missing image tag in screenshot')) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/worker/task/appstream/stripe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/stripe.ts 3 | * Adds stripe data to appstream file 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamStripe extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | if (this.worker.context.stripe == null) { 42 | return 43 | } 44 | 45 | const raw = await fs.readFile(this.path) 46 | const $ = cheerio.load(raw, { xmlMode: true }) 47 | 48 | if ($('component > custom').length === 0) { 49 | $('component').append('') 50 | } 51 | 52 | $('component > custom').append('') 53 | 54 | const $el = $('component > custom > value:last-of-type') 55 | 56 | $el.attr('key', 'x-appcenter-stripe') 57 | $el.text(this.worker.context.stripe) 58 | 59 | await fs.writeFile(this.path, $.xml()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/worker/task/appstream/summary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/summary.ts 3 | * Checks stuff about the appstream summary field 4 | */ 5 | 6 | import * as cheerio from 'cheerio' 7 | import * as fs from 'fs-extra' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamSummary extends Task { 15 | 16 | /** 17 | * Returns the main appstream file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the appstream file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo', `${this.name}.appdata.xml`) 32 | } 33 | 34 | /** 35 | * Runs all the appstream tests 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path) 42 | const $ = cheerio.load(raw, { xmlMode: true }) 43 | 44 | const summary = $('component > summary') 45 | 46 | if (summary.length === 0) { 47 | throw new Log(Log.Level.ERROR, 'Missing "summary" field') 48 | } 49 | 50 | const text = summary.text() 51 | 52 | if (text.toLowerCase().replace(/\W/, '').indexOf('elementaryos') !== -1) { 53 | throw new Log(Log.Level.ERROR, '"summary" field calls out elementary OS') 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/worker/task/appstream/validate.md: -------------------------------------------------------------------------------- 1 | # Appstream validate fails 2 | 3 | AppCenter tried to run `appstreamcli validate` on <%= storage.nameHuman %> and 4 | received the following errors: 5 | 6 | ``` 7 | <%_ if (log != null) { _%> 8 | <%- log %> 9 | <%_ } else { _%> 10 | Unable to retrieve validate log 11 | <%_ } _%> 12 | ``` 13 | 14 | ### For more information, see: 15 | - [The AppStream Quickstart Guide](https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html) 16 | - [The AppStream Metadata Specification](https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html) 17 | -------------------------------------------------------------------------------- /src/worker/task/appstream/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/validate.ts 3 | * Runs appstreamcli to validate appstream file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as path from 'path' 8 | 9 | import { sanitize } from '../../../lib/utility/rdnn' 10 | import { Docker } from '../../docker' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class AppstreamValidate extends Task { 15 | /** 16 | * Location of the appstream cli log 17 | * 18 | * @return {string} 19 | */ 20 | protected get logPath () { 21 | return path.resolve(this.worker.workspace, 'appstream.log') 22 | } 23 | 24 | /** 25 | * Returns the main appstream file name 26 | * 27 | * @return {string} 28 | */ 29 | public get name () { 30 | return sanitize(this.worker.context.nameDomain, '-') 31 | } 32 | 33 | /** 34 | * Path to folder containing the appstream file 35 | * 36 | * @return {string} 37 | */ 38 | public get path () { 39 | return path.resolve(this.worker.workspace, 'package/usr/share/metainfo') 40 | } 41 | 42 | /** 43 | * Runs appstream validate with docker 44 | * 45 | * @async 46 | * @return {void} 47 | */ 48 | public async run () { 49 | const docker = await this.docker() 50 | 51 | const file = `${this.name}.appdata.xml` 52 | const cmd = `validate ${file} --no-color` 53 | const exit = await docker.run(cmd) 54 | 55 | if (exit !== 0) { 56 | throw await this.log() 57 | } 58 | } 59 | 60 | /** 61 | * Formats the docker log to something we can pass to the user 62 | * 63 | * @async 64 | * @return {Log} 65 | */ 66 | protected async log () { 67 | const p = path.resolve(__dirname, 'validate.md') 68 | const log = await fs.readFile(this.logPath, 'utf8') 69 | 70 | return Log.template(Log.Level.ERROR, p, { 71 | log, 72 | storage: this.worker.context 73 | }) 74 | } 75 | 76 | /** 77 | * Returns a docker instance to use for liftoff 78 | * 79 | * @async 80 | * @return {Docker} 81 | */ 82 | protected async docker (): Promise { 83 | const docker = new Docker(this.worker.config, 'appstream-validate') 84 | 85 | const exists = await docker.exists() 86 | if (exists === false) { 87 | const folder = path.resolve(__dirname, 'validate') 88 | await docker.create(folder) 89 | } 90 | 91 | docker.log = this.logPath 92 | 93 | docker.mount(this.path, '/tmp/houston') 94 | 95 | return docker 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/worker/task/appstream/validate/Dockerfile: -------------------------------------------------------------------------------- 1 | # Houston appdata docker file 2 | # Builds an ubuntu base with appstreamcli for validating appstream files 3 | # 4 | # Version: 1.0.4 5 | 6 | FROM elementary/docker:loki-stable 7 | 8 | # Install liftoff 9 | ENV DEBIAN_FRONTEND noninteractive 10 | ENV DEBIAN_PRIORITY critical 11 | ENV DEBCONF_NOWARNINGS yes 12 | 13 | # ca-certificate stuff for removing glib-net error 14 | RUN apt update && apt install -y appstream openssl ca-certificates 15 | RUN update-ca-certificates 16 | 17 | # Execution 18 | RUN mkdir -p /tmp/houston 19 | WORKDIR /tmp/houston 20 | ENTRYPOINT ["appstreamcli"] 21 | -------------------------------------------------------------------------------- /src/worker/task/build/deb.md: -------------------------------------------------------------------------------- 1 | # Failed to build <%= storage.nameHuman %> 2 | 3 | AppCenter failed to build <%= storage.nameHuman %>. 4 | 5 | <%_ if (log != null) { _%> 6 | 7 |
8 | 9 | Build Log 10 | 11 | ``` 12 | <%- log %> 13 | ``` 14 | 15 |
16 | 17 | <%_ } else { _%> 18 | 19 | ``` 20 | Unable to retrieve build log 21 | ``` 22 | 23 | <%_ } %> 24 | -------------------------------------------------------------------------------- /src/worker/task/build/deb/Dockerfile: -------------------------------------------------------------------------------- 1 | # Houston liftoff docker file 2 | # Builds an ubuntu base with liftoff for packaging debian applications 3 | # 4 | # TODO: Add liftoff to repository 5 | # 6 | # Version: 1.1.0 7 | 8 | FROM ubuntu:bionic 9 | 10 | MAINTAINER elementary 11 | 12 | # Install liftoff 13 | ENV DEBIAN_FRONTEND noninteractive 14 | ENV DEBIAN_PRIORITY critical 15 | ENV DEBCONF_NOWARNINGS yes 16 | 17 | # TODO: Update liftoff with official build or ppa 18 | COPY liftoff_0.1_amd64.deb /tmp/liftoff.deb 19 | RUN dpkg -i /tmp/liftoff.deb; exit 0 20 | 21 | RUN apt update && apt install -y -f 22 | 23 | # removes annoying log message 24 | RUN touch /root/.pbuilderrc 25 | 26 | # sudo access 27 | RUN useradd -m docker && echo "docker:docker" | chpasswd && adduser docker sudo 28 | 29 | # Execution 30 | RUN mkdir -p /tmp/houston 31 | WORKDIR /tmp/houston 32 | ENTRYPOINT ["liftoff"] 33 | -------------------------------------------------------------------------------- /src/worker/task/build/deb/liftoff_0.1_amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/src/worker/task/build/deb/liftoff_0.1_amd64.deb -------------------------------------------------------------------------------- /src/worker/task/debian/changelogTemplate.ejs: -------------------------------------------------------------------------------- 1 | <%_ changelogs.forEach(function (change, i) { _%> 2 | <%= name %> (<%= change.version %>) <%= context.distribution %>; urgency=low 3 | 4 | <% changes[i].forEach(function (c) { _%> 5 | * <%= c %> 6 | <%_ }) _%> 7 | 8 | -- <%= change.author %> <%= change.date.toUTCString().replace('GMT', '+0000') _%> 9 | 10 | 11 | <% }) %> 12 | -------------------------------------------------------------------------------- /src/worker/task/debian/control.md: -------------------------------------------------------------------------------- 1 | # Incorrect values in Debian control file 2 | 3 | AppCenter checked your Debian control files and found some errors 4 | 5 | <%_ (errors || []).forEach(function (error) { _%> 6 | - <%- error %> 7 | <%_ }) _%> 8 | -------------------------------------------------------------------------------- /src/worker/task/debian/control.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/debian/control.ts 3 | * Updates, lints, and validates the Debian control file. 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import { get, set } from 'lodash' 8 | import * as os from 'os' 9 | import * as path from 'path' 10 | 11 | import { sanitize } from '../../../lib/utility/rdnn' 12 | import { Log } from '../../log' 13 | import { Task } from '../task' 14 | 15 | export class DebianControl extends Task { 16 | 17 | /** 18 | * File location for the debian control file 19 | * 20 | * @var {string} 21 | */ 22 | public static path = 'debian/control' 23 | 24 | /** 25 | * The Debian package name 26 | * 27 | * @return {String} 28 | */ 29 | protected get name () { 30 | return sanitize(this.worker.context.nameDomain, '-') 31 | } 32 | 33 | /** 34 | * Returns the full path for the debian control file and the current test. 35 | * 36 | * @return {String} 37 | */ 38 | protected get path () { 39 | return path.resolve(this.worker.workspace, 'dirty', DebianControl.path) 40 | } 41 | 42 | /** 43 | * Checks the Debian control file for errors 44 | * 45 | * @async 46 | * @return {void} 47 | */ 48 | public async run () { 49 | const exists = fs.pathExists(this.path) 50 | if (exists === false) { 51 | throw new Log(Log.Level.ERROR, 'Missing debian control file') 52 | } 53 | 54 | const file = await fs.readFile(this.path, 'utf8') 55 | 56 | const errors = [] as string[] 57 | 58 | if (file.indexOf(`Source: ${this.name}`) === -1) { 59 | errors.push(`Source should be "${this.name}"`) 60 | } 61 | 62 | if (file.indexOf(`Package: ${this.name}`) === -1) { 63 | errors.push(`Package should be "${this.name}"`) 64 | } 65 | 66 | if (!file.match(/^Maintainer: .* <.*@.*>$/gm)) { 67 | errors.push('Maintainer should be in the form of "name 0) { 71 | const template = path.resolve(__dirname, 'control.md') 72 | 73 | throw Log.template(Log.Level.ERROR, template, { 74 | context: this.worker.context, 75 | errors 76 | }) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/worker/task/desktop/exec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/exec.ts 3 | * Checks that a exec field starts with app name in the desktop file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as ini from 'ini' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class DesktopExec extends Task { 15 | 16 | /** 17 | * Returns the desktop file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the desktop file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/applications', `${this.name}.desktop`) 32 | } 33 | 34 | /** 35 | * Checks Exec field in desktop file 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path, 'utf8') 42 | const data = ini.parse(raw) 43 | 44 | if (data['Desktop Entry'] == null) { 45 | throw new Log(Log.Level.ERROR, 'Missing application data') 46 | } 47 | 48 | const execValue = (typeof data['Desktop Entry'].Exec === 'string') 49 | ? data['Desktop Entry'].Exec 50 | : '' 51 | 52 | if (execValue.startsWith(this.name) === false) { 53 | throw new Log(Log.Level.ERROR, 'Exec field does not start with binary name') 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/worker/task/desktop/icon.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/icon.ts 3 | * Checks that a icon field is matches app name in the desktop file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as ini from 'ini' 8 | import * as path from 'path' 9 | 10 | import { sanitize } from '../../../lib/utility/rdnn' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class DesktopIcon extends Task { 15 | 16 | /** 17 | * Returns the desktop file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Path the desktop file should exist at 27 | * 28 | * @return {string} 29 | */ 30 | public get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/share/applications', `${this.name}.desktop`) 32 | } 33 | 34 | /** 35 | * Checks Icon field in desktop file 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const raw = await fs.readFile(this.path, 'utf8') 42 | const data = ini.parse(raw) 43 | 44 | if (data['Desktop Entry'] == null) { 45 | throw new Log(Log.Level.ERROR, 'Missing application data') 46 | } 47 | 48 | if (data['Desktop Entry'].Icon !== this.name) { 49 | throw new Log(Log.Level.ERROR, 'Incorrect desktop file icon value') 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/worker/task/desktop/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/desktop/index.ts 3 | * Runs a bunch of tests around the desktop file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as path from 'path' 8 | 9 | import { sanitize } from '../../../lib/utility/rdnn' 10 | import { Log } from '../../log' 11 | import { WrapperTask } from '../wrapperTask' 12 | 13 | import { DesktopExec } from './exec' 14 | import { DesktopIcon } from './icon' 15 | import { DesktopValidate } from './validate' 16 | 17 | export class Desktop extends WrapperTask { 18 | /** 19 | * All of the fun tests we should run on the desktop file 20 | * 21 | * @var {Task[]} 22 | */ 23 | public get tasks () { 24 | switch (this.worker.context.type) { 25 | // System apps are allowed system icons 26 | case 'system-app': 27 | return [ 28 | DesktopExec, 29 | DesktopValidate 30 | ] 31 | 32 | default: 33 | return [ 34 | DesktopExec, 35 | DesktopIcon, 36 | DesktopValidate 37 | ] 38 | } 39 | } 40 | 41 | /** 42 | * Returns the desktop file name 43 | * 44 | * @return {string} 45 | */ 46 | public get name () { 47 | return sanitize(this.worker.context.nameDomain, '-') 48 | } 49 | 50 | /** 51 | * Path the desktop file should exist at 52 | * 53 | * @return {string} 54 | */ 55 | public get path () { 56 | return path.resolve(this.worker.workspace, 'package/usr/share/applications', `${this.name}.desktop`) 57 | } 58 | 59 | /** 60 | * Runs all the desktop tests 61 | * 62 | * @async 63 | * @return {void} 64 | */ 65 | public async run () { 66 | const exists = await fs.exists(this.path) 67 | if (exists === false) { 68 | throw new Log(Log.Level.ERROR, 'Desktop file does not exist') 69 | } 70 | 71 | await this.runTasks() 72 | 73 | // TODO: Concat all errors that have no body to a single list log 74 | this.logs.forEach((l) => this.worker.report(l)) 75 | 76 | if (this.errorLogs.length > 0) { 77 | this.worker.stop() 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/worker/task/desktop/validate.md: -------------------------------------------------------------------------------- 1 | # Desktop validate fails 2 | 3 | AppCenter tried to run `desktop-file-validate` on <%= storage.nameHuman %> and 4 | received the following errors: 5 | 6 | <%_ Object.keys(logs).forEach(function (file) { _%> 7 | ### <%= file %> 8 | 9 | ``` 10 | <%- logs[file] %> 11 | ``` 12 | <%_ }) _%> 13 | 14 | ### For more information, see: 15 | - [The Desktop Entry Specification](https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s05.html) 16 | -------------------------------------------------------------------------------- /src/worker/task/desktop/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/validate.ts 3 | * Runs desktop files through the `desktop-file-validate` command 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as path from 'path' 8 | 9 | import { glob } from '../../../lib/utility/glob' 10 | import { Docker } from '../../docker' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class DesktopValidate extends Task { 15 | /** 16 | * Path to folder containing the desktop files 17 | * 18 | * @return {string} 19 | */ 20 | public get path () { 21 | return path.resolve(this.worker.workspace, 'package/usr/share/applications') 22 | } 23 | 24 | /** 25 | * Runs appstream validate with docker 26 | * 27 | * @async 28 | * @return {void} 29 | */ 30 | public async run () { 31 | const files = await glob(path.resolve(this.path, '*')) 32 | 33 | const logFiles = [] 34 | 35 | for (const file of files) { 36 | const fileName = path.basename(file) 37 | const docker = await this.docker(fileName) 38 | 39 | const localFile = path.relative(this.path, file) 40 | const exit = await docker.run(localFile) 41 | 42 | if (exit !== 0) { 43 | logFiles.push(fileName) 44 | } 45 | } 46 | 47 | if (logFiles.length > 0) { 48 | throw await this.log(logFiles) 49 | } 50 | } 51 | 52 | /** 53 | * Location of the desktop log file for the given test file 54 | * 55 | * @return {string} 56 | */ 57 | protected logPath (file: string) { 58 | return path.resolve(this.worker.workspace, `appstream-${file}.log`) 59 | } 60 | 61 | /** 62 | * Formats the docker log to something we can pass to the user 63 | * 64 | * @async 65 | * @param {string[]} files 66 | * @return {Log} 67 | */ 68 | protected async log (files: string[]) { 69 | const p = path.resolve(__dirname, 'validate.md') 70 | const logs = {} 71 | 72 | for (const file of files) { 73 | logs[file] = await fs.readFile(this.logPath(file), 'utf8') 74 | } 75 | 76 | return Log.template(Log.Level.ERROR, p, { 77 | logs, 78 | storage: this.worker.context 79 | }) 80 | } 81 | 82 | /** 83 | * Returns a docker instance to use for liftoff 84 | * 85 | * @async 86 | * @param {string} file 87 | * @return {Docker} 88 | */ 89 | protected async docker (file: string): Promise { 90 | const docker = new Docker(this.worker.config, 'desktop-validate') 91 | 92 | const exists = await docker.exists() 93 | if (exists === false) { 94 | const folder = path.resolve(__dirname, 'validate') 95 | await docker.create(folder) 96 | } 97 | 98 | docker.log = this.logPath(file) 99 | docker.mount(this.path, '/tmp/houston') 100 | 101 | return docker 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/worker/task/desktop/validate/Dockerfile: -------------------------------------------------------------------------------- 1 | # Houston desktop validate docker file 2 | # Builds an ubuntu base with desktop-file-utils for validating desktop files 3 | # 4 | # Version: 1.0.3 5 | 6 | FROM elementary/docker:loki-stable 7 | 8 | # Install liftoff 9 | ENV DEBIAN_FRONTEND noninteractive 10 | ENV DEBIAN_PRIORITY critical 11 | ENV DEBCONF_NOWARNINGS yes 12 | 13 | RUN apt update && apt install -y desktop-file-utils 14 | 15 | # Execution 16 | RUN mkdir -p /tmp/houston 17 | WORKDIR /tmp/houston 18 | ENTRYPOINT ["desktop-file-validate"] 19 | -------------------------------------------------------------------------------- /src/worker/task/extract/deb.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/extract/deb.ts 3 | * Extracts a deb package so we can test files 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as os from 'os' 8 | import * as path from 'path' 9 | 10 | import { Docker } from '../../docker' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class ExtractDeb extends Task { 15 | /** 16 | * The directory we will extract the deb file to 17 | * 18 | * @return {string} 19 | */ 20 | protected get path () { 21 | return path.resolve(this.worker.workspace, 'package') 22 | } 23 | 24 | /** 25 | * Runs liftoff 26 | * 27 | * @async 28 | * @return {void} 29 | */ 30 | public async run () { 31 | await this.setup() 32 | 33 | const docker = await this.docker(this.worker.workspace) 34 | 35 | // The extract script will need to chmod root files 36 | const exit = await docker.run('extract-deb', { Privileged: true }) 37 | 38 | if (exit !== 0) { 39 | throw new Log(Log.Level.ERROR, 'Unable to unpack Debian package') 40 | } 41 | } 42 | 43 | /** 44 | * Ensures the extract path is created before we run docker 45 | * 46 | * @async 47 | * @return {void} 48 | */ 49 | protected async setup () { 50 | await fs.ensureDir(this.path) 51 | } 52 | 53 | /** 54 | * Returns a docker instance to use for liftoff 55 | * 56 | * @async 57 | * @param {string} p - Folder to mount for building 58 | * @return {Docker} 59 | */ 60 | protected async docker (p: string): Promise { 61 | const docker = new Docker(this.worker.config, 'extract-deb') 62 | 63 | const exists = await docker.exists() 64 | if (exists === false) { 65 | const folder = path.resolve(__dirname, 'deb') 66 | await docker.create(folder) 67 | } 68 | 69 | docker.mount(this.worker.workspace, '/tmp/houston') 70 | 71 | return docker 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/worker/task/extract/deb/Dockerfile: -------------------------------------------------------------------------------- 1 | # Houston extract deb docker file 2 | # Extracts a debian package to editable files 3 | # 4 | # Version: 1.0.2 5 | 6 | FROM elementary/docker:loki-stable 7 | 8 | # Install liftoff 9 | ENV DEBIAN_FRONTEND noninteractive 10 | ENV DEBIAN_PRIORITY critical 11 | ENV DEBCONF_NOWARNINGS yes 12 | 13 | COPY extract-deb.sh /usr/local/bin/extract-deb 14 | RUN chmod +x /usr/local/bin/extract-deb 15 | 16 | # Execution 17 | RUN mkdir -p /tmp/houston 18 | WORKDIR /tmp/houston 19 | ENTRYPOINT ["extract-deb"] 20 | -------------------------------------------------------------------------------- /src/worker/task/extract/deb/extract-deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Extracts the `package.deb` file to `package` folder 4 | # @see: https://gist.github.com/shamil/3140558 5 | 6 | # Extracting a package 7 | dpkg-deb -x package.deb package 8 | dpkg-deb -e package.deb package/DEBIAN 9 | 10 | rm package.deb 11 | 12 | # Time to start patching the deb contents 13 | cd package 14 | 15 | # Creates a list of all the original package permissions for each file 16 | touch FILES 17 | find . -exec stat -c "0%a;%n" {} \; > FILES 18 | 19 | # Sets everything to an open permission to allow edits without root access 20 | chmod -R 777 . 21 | -------------------------------------------------------------------------------- /src/worker/task/file/deb.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/file/deb.ts 3 | * Tests debian packages for needed file paths 4 | */ 5 | 6 | import { WrapperTask } from '../wrapperTask' 7 | 8 | import { FileDebBinary } from './deb/binary' 9 | import { FileDebNonexistent } from './deb/nonexistent' 10 | 11 | export class FileDeb extends WrapperTask { 12 | /** 13 | * Tasks to run for checking file paths 14 | * 15 | * @var {Task[]} 16 | */ 17 | public get tasks () { 18 | return [ 19 | FileDebBinary, 20 | FileDebNonexistent 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/worker/task/file/deb/binary.md: -------------------------------------------------------------------------------- 1 | # Not shipping a binary 2 | 3 | AppCenter built your package but could not find a `<%= name %>` 4 | binary in `/usr/bin`. Please make sure that your build system places a binary 5 | file at `/usr/bin/<%= name %>`. 6 | 7 | For reference, the built package includes these files: 8 | 9 |
10 | 11 | Package Files 12 | 13 | ``` 14 | <% files.forEach(function (file) { _%> 15 | <%= file %> 16 | <%_ }) %> 17 | ``` 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/worker/task/file/deb/binary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/file/deb/binary.ts 3 | * Tests debian packages for needed binary file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as path from 'path' 8 | 9 | import { glob } from '../../../../lib/utility/glob' 10 | import { sanitize } from '../../../../lib/utility/rdnn' 11 | import { Log } from '../../../log' 12 | import { Task } from '../../task' 13 | 14 | export class FileDebBinary extends Task { 15 | 16 | /** 17 | * Returns the desktop file name 18 | * 19 | * @return {string} 20 | */ 21 | public get name () { 22 | return sanitize(this.worker.context.nameDomain, '-') 23 | } 24 | 25 | /** 26 | * Location of the directory to build 27 | * 28 | * @return {string} 29 | */ 30 | protected get path () { 31 | return path.resolve(this.worker.workspace, 'package/usr/bin', this.name) 32 | } 33 | 34 | /** 35 | * Runs liftoff 36 | * 37 | * @async 38 | * @return {void} 39 | */ 40 | public async run () { 41 | const exists = await fs.exists(this.path) 42 | 43 | if (exists === false) { 44 | throw Log.template(Log.Level.ERROR, path.resolve(__dirname, 'binary.md'), { 45 | context: this.worker.context, 46 | files: await this.files(), 47 | name: this.name 48 | }) 49 | } 50 | } 51 | 52 | /** 53 | * Returns a list of useful files in the package. Filters out custom files 54 | * 55 | * @async 56 | * @return {string[]} 57 | */ 58 | protected async files (): Promise { 59 | const root = path.resolve(this.worker.workspace, 'package') 60 | const files = await glob(path.resolve(root, '**/*'), { nodir: true }) 61 | 62 | return files 63 | .filter((p) => !p.startsWith(path.resolve(root, 'DEBIAN'))) 64 | .filter((p) => (p !== path.resolve(root, 'FILES'))) 65 | .map((p) => p.substring(root.length)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/worker/task/file/deb/nonexistent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/file/deb/binary.ts 3 | * Tests debian packages for needed binary file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as path from 'path' 8 | 9 | import { glob } from '../../../../lib/utility/glob' 10 | import { Log } from '../../../log' 11 | import { Task } from '../../task' 12 | 13 | export class FileDebNonexistent extends Task { 14 | /** 15 | * Folder where non-correctly installed files will end up in the Debian package 16 | * 17 | * @return {string} 18 | */ 19 | protected get path () { 20 | return path.resolve(this.worker.workspace, 'package') 21 | } 22 | 23 | /** 24 | * Glob for non-correctly installed files 25 | * 26 | * @return {string} 27 | */ 28 | protected get files () { 29 | return path.resolve(this.path, 'package/nonexistent/**/*') 30 | } 31 | 32 | /** 33 | * Checks no files are incorrectly placed in the deb package 34 | * 35 | * @async 36 | * @return {void} 37 | */ 38 | public async run () { 39 | const files = await glob(this.files) 40 | 41 | if (files.length < 1) { 42 | return 43 | } 44 | 45 | const relativePaths = files.map((file) => file.replace(`${this.path}/`, '')) 46 | 47 | const p = path.resolve(__dirname, 'nonexistentLog.md') 48 | const log = await fs.readFile(p, 'utf8') 49 | 50 | throw Log.template(Log.Level.ERROR, p, { 51 | files: relativePaths, 52 | storage: this.worker.context 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/worker/task/file/deb/nonexistentLog.md: -------------------------------------------------------------------------------- 1 | # Incorrectly installed files 2 | 3 | AppCenter noticed that some files will not be installed correctly on the 4 | file system. This is most likely a problem with your build system referencing 5 | non-existant folders. 6 | 7 | <% files.forEach(function (file) { %> 8 | - [ ] `<%= file %>` 9 | <% }) %> 10 | -------------------------------------------------------------------------------- /src/worker/task/pack/deb.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/pack/deb.ts 3 | * Packages up an extracted deb file 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as os from 'os' 8 | import * as path from 'path' 9 | 10 | import { Docker } from '../../docker' 11 | import { Log } from '../../log' 12 | import { Task } from '../task' 13 | 14 | export class PackDeb extends Task { 15 | /** 16 | * The directory we will pack to a deb file 17 | * 18 | * @return {string} 19 | */ 20 | protected get path () { 21 | return path.resolve(this.worker.workspace, 'package') 22 | } 23 | 24 | /** 25 | * Runs liftoff 26 | * 27 | * @async 28 | * @return {void} 29 | */ 30 | public async run () { 31 | const docker = await this.docker(this.worker.workspace) 32 | 33 | // The extract script will need to chmod root files 34 | const exit = await docker.run('pack-deb', { Privileged: true }) 35 | 36 | if (exit !== 0) { 37 | throw new Log(Log.Level.ERROR, 'Unable to pack Debian package') 38 | } 39 | } 40 | 41 | /** 42 | * Returns a docker instance to use for liftoff 43 | * 44 | * @async 45 | * @param {string} p - Folder to mount for building 46 | * @return {Docker} 47 | */ 48 | protected async docker (p: string): Promise { 49 | const docker = new Docker(this.worker.config, 'pack-deb') 50 | 51 | const exists = await docker.exists() 52 | if (exists === false) { 53 | const folder = path.resolve(__dirname, 'deb') 54 | await docker.create(folder) 55 | } 56 | 57 | docker.mount(this.worker.workspace, '/tmp/houston') 58 | 59 | return docker 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/worker/task/pack/deb/Dockerfile: -------------------------------------------------------------------------------- 1 | # Houston pack deb docker file 2 | # Packs a debian package from extracted files 3 | # 4 | # Version: 1.0.2 5 | 6 | FROM elementary/docker:loki-stable 7 | 8 | # Install liftoff 9 | ENV DEBIAN_FRONTEND noninteractive 10 | ENV DEBIAN_PRIORITY critical 11 | ENV DEBCONF_NOWARNINGS yes 12 | 13 | COPY pack-deb.sh /usr/local/bin/pack-deb 14 | RUN chmod +x /usr/local/bin/pack-deb 15 | 16 | # Execution 17 | RUN mkdir -p /tmp/houston 18 | WORKDIR /tmp/houston 19 | ENTRYPOINT ["pack-deb"] 20 | -------------------------------------------------------------------------------- /src/worker/task/pack/deb/pack-deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Packs `package` folder to `package.deb` 4 | # @see: https://gist.github.com/shamil/3140558 5 | 6 | # Extracting a package 7 | cd package 8 | 9 | # Restores the original package permissions 10 | while read l in; do 11 | IFS=';' read o f <<< "$l" 12 | 13 | chmod "$o" "$f" 14 | done < ./FILES 15 | 16 | # Then remove the file because it's not part of the package 17 | rm FILES 18 | 19 | 20 | # And build the package 21 | mkdir -p ./DEBIAN 22 | touch ./DEBIAN/md5sums 23 | find . -type f ! -regex '.*.hg.*' ! -regex '.*?debian-binary.*' ! -regex '.*?DEBIAN.*' -printf '%P ' | xargs md5sum > ./DEBIAN/md5sums 24 | dpkg-deb -b . ../package.deb 25 | 26 | # Remove the package directory after the deb is built 27 | cd .. 28 | rm -r package 29 | 30 | # And set this to 777 so we can remove it if needed 31 | chmod 777 ./package.deb 32 | -------------------------------------------------------------------------------- /src/worker/task/task.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/task.ts 3 | * Some worker logic. 4 | * 5 | * @exports {Class} Task 6 | */ 7 | 8 | import { Log } from '../log' 9 | import { ITask, ITaskConstructor } from '../type' 10 | import { Worker } from '../worker' 11 | 12 | export class Task implements ITask { 13 | /** 14 | * The current running worker 15 | * 16 | * @var {Worker} 17 | */ 18 | public worker: Worker 19 | 20 | /** 21 | * Creates a new Task 22 | * 23 | * @param {Worker} worker 24 | */ 25 | constructor (worker: Worker) { 26 | this.worker = worker 27 | } 28 | 29 | /** 30 | * Does logic. 31 | * 32 | * @async 33 | * @return {void} 34 | */ 35 | public async run () { 36 | this.worker.emit(`task:${this.constructor.name}:start`) 37 | // 38 | this.worker.emit(`task:${this.constructor.name}:end`) 39 | } 40 | 41 | /** 42 | * Adds a log/error to storage 43 | * 44 | * @param {Error} e 45 | * @return {Task} 46 | */ 47 | public report (e: Error) { 48 | // A real error. Not a Log 49 | if (!(e instanceof Log)) { 50 | const log = new Log(Log.Level.ERROR, 'Internal error while running') 51 | .setError(e) 52 | 53 | this.worker.report(log) 54 | this.worker.stop() 55 | } else { 56 | this.worker.report(e) 57 | 58 | if (e.level === Log.Level.ERROR) { 59 | this.worker.stop() 60 | } 61 | } 62 | 63 | return this 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/worker/task/upload/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/upload/index.ts 3 | * Responsible for uploading all the end results to third party services 4 | */ 5 | 6 | import { WrapperTask } from '../wrapperTask' 7 | 8 | import { UploadLog } from './log' 9 | import { UploadPackage } from './package' 10 | 11 | export class Upload extends WrapperTask { 12 | /** 13 | * All of the upload tasks we should run 14 | * 15 | * @var {Task[]} 16 | */ 17 | public get tasks () { 18 | return [ 19 | UploadPackage, 20 | UploadLog 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/worker/task/upload/log.md: -------------------------------------------------------------------------------- 1 | # Error uploading build logs 2 | 3 | AppCenter encountered errors while trying to upload your build logs to the 4 | following third party services: 5 | 6 | <%_ services.forEach(function (service) { _%> 7 | - <%= service %> 8 | <%_ }) _%> 9 | -------------------------------------------------------------------------------- /src/worker/task/upload/log.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/upload/log.spec.ts 3 | * Tests uploading logs to third party services 4 | */ 5 | 6 | import * as sinon from 'sinon' 7 | 8 | import { packageRepository } from '../../../lib/service' 9 | import { UploadLog } from './log' 10 | 11 | import { fixture } from '../../../../test/utility/fs' 12 | import { mock } from '../../../../test/utility/worker' 13 | 14 | test('uploads logs to codeRepository if also logRepository', async () => { 15 | const worker = await mock({ 16 | distribution: 'loki', 17 | logs: [{ title: 'test', body: 'testy test test' }], 18 | nameDomain: 'com.github.elementary.houston', 19 | package: { type: 'deb' }, 20 | references: ['refs/heads/loki'] 21 | }) 22 | 23 | worker.repository.uploadLog = sinon.fake.resolves() 24 | 25 | const setup = new UploadLog(worker) 26 | await setup.run() 27 | 28 | expect(worker.repository.uploadLog.callCount).not.toBe(0) 29 | }, 300000) 30 | -------------------------------------------------------------------------------- /src/worker/task/upload/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/upload/log.ts 3 | * Uploads end error logs to third party services 4 | */ 5 | 6 | import * as path from 'path' 7 | 8 | import { Logger } from '../../../lib/log' 9 | import { isLogRepository } from '../../../lib/service' 10 | import { Log } from '../../log' 11 | import { ILog } from '../../type' 12 | import { Task } from '../task' 13 | 14 | export class UploadLog extends Task { 15 | /** 16 | * A list of collected errors we get from third party services. 17 | * 18 | * @var {Object[]} 19 | */ 20 | protected errors: { error: Error, service: string }[] = [] 21 | 22 | /** 23 | * Uploads all of the logs to third party services 24 | * 25 | * @async 26 | * @return {void} 27 | */ 28 | public async run () { 29 | let logs = this.worker.result.logs 30 | const ref = this.worker.context.references[this.worker.context.references.length - 1] 31 | 32 | logs = await this.uploadToCodeRepository(logs, ref) 33 | 34 | this.worker.context.logs = logs 35 | 36 | if (this.errors.length !== 0) { 37 | this.reportErrors() 38 | } 39 | } 40 | 41 | /** 42 | * Uploads logs to the origin code repository if it's also a log repo 43 | * 44 | * @async 45 | * @param {ILog[]} logs 46 | * @param {string} ref 47 | * @return {ILog[]} 48 | */ 49 | protected async uploadToCodeRepository (logs, ref): Promise { 50 | if (!isLogRepository(this.worker.repository)) { 51 | return 52 | } 53 | 54 | const newLogs: ILog[] = [] 55 | 56 | try { 57 | for (const log of logs) { 58 | newLogs.push(await this.worker.repository.uploadLog(log, 'review', ref)) 59 | } 60 | } catch (error) { 61 | this.errors.push({ error, service: this.worker.repository.serviceName }) 62 | } 63 | 64 | return newLogs 65 | } 66 | 67 | /** 68 | * Concats all the errors we have and puts them to a nice markdown log. 69 | * 70 | * @throws {Log} 71 | */ 72 | protected reportErrors () { 73 | const logger = this.worker.app.get(Logger) 74 | this.errors.map((e) => logger.error('Error uploading logs').setError(e.error)) 75 | 76 | if (this.errors.length !== 0) { 77 | const logPath = path.resolve(__dirname, 'log.md') 78 | throw Log.template(Log.Level.ERROR, logPath, { 79 | services: this.errors.map((e) => e.service) 80 | }) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/worker/task/upload/package.md: -------------------------------------------------------------------------------- 1 | # Error uploading packages 2 | 3 | AppCenter encountered errors while trying to upload your package to the 4 | following services: 5 | 6 | <%_ services.forEach(function (service) { _%> 7 | - <%= service %> 8 | <%_ }) _%> 9 | -------------------------------------------------------------------------------- /src/worker/task/upload/package.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/upload/package.spec.ts 3 | * Tests uploading package assets 4 | */ 5 | 6 | import * as sinon from 'sinon' 7 | 8 | import { packageRepository } from '../../../lib/service' 9 | import { UploadPackage } from './package' 10 | 11 | import { fixture } from '../../../../test/utility/fs' 12 | import { mock } from '../../../../test/utility/worker' 13 | 14 | test('uploads package to codeRepository if also packageRepository', async () => { 15 | const worker = await mock({ 16 | distribution: 'loki', 17 | nameDomain: 'com.github.elementary.houston', 18 | package: { 19 | path: fixture('worker/docker/image1/Dockerfile'), 20 | type: 'deb' 21 | }, 22 | references: ['refs/heads/loki'] 23 | }) 24 | 25 | const setup = new UploadPackage(worker) 26 | 27 | worker.repository.uploadPackage = sinon.fake.resolves() 28 | setup.uploadToPackageRepositories = sinon.fake.resolves() 29 | 30 | await setup.run() 31 | 32 | expect(worker.repository.uploadPackage.callCount).not.toBe(0) 33 | }, 300000) 34 | 35 | test('concats error logs to something easy to read', async () => { 36 | expect.assertions(1) 37 | 38 | const worker = await mock({ 39 | distribution: 'loki', 40 | nameDomain: 'com.github.elementary.houston', 41 | package: { 42 | path: fixture('worker/docker/image1/Dockerfile'), 43 | type: 'deb' 44 | }, 45 | references: ['refs/heads/loki'] 46 | }) 47 | 48 | worker.app.unbind(packageRepository) 49 | 50 | const setup = new UploadPackage(worker) 51 | 52 | try { 53 | await setup.run() 54 | } catch (err) { 55 | expect(err.body).toMatch(/mock Repository/) 56 | } 57 | }) 58 | -------------------------------------------------------------------------------- /src/worker/task/wrapperTask.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/wrapperTask.ts 3 | * Runs a bunch of tasks in a row, collecting errors for later. 4 | */ 5 | 6 | import { Log } from '../log' 7 | import { ITaskConstructor } from '../type' 8 | import { Task } from './task' 9 | 10 | export class WrapperTask extends Task { 11 | /** 12 | * The tasks to run 13 | * 14 | * @var {ITaskConstructor[]} 15 | */ 16 | public get tasks (): ITaskConstructor[] { 17 | return [] 18 | } 19 | 20 | // BUG: We have to set a no-op setter because Jest will error if we don't 21 | public set tasks (tasks: ITaskConstructor[]) { 22 | return 23 | } 24 | 25 | /** 26 | * All of the logs that where gathered 27 | * 28 | * @var {Log[]} 29 | */ 30 | public logs: Log[] = [] 31 | 32 | /** 33 | * Returns all of the logs that are errors 34 | * 35 | * @return {Log[]} 36 | */ 37 | protected get errorLogs (): Log[] { 38 | return this.logs 39 | .filter((l) => (l.level === Log.Level.ERROR)) 40 | } 41 | 42 | /** 43 | * Does logic. 44 | * 45 | * @async 46 | * @return {void} 47 | */ 48 | public async run () { 49 | await this.runTasks() 50 | 51 | this.logs.forEach((l) => this.worker.report(l)) 52 | 53 | if (this.errorLogs.length > 0) { 54 | this.worker.stop() 55 | } 56 | } 57 | 58 | /** 59 | * Runs all the tasks. This is out of the `run` method to allow easier 60 | * custom logic for WrapperTask runners 61 | * 62 | * @async 63 | * @return {void} 64 | */ 65 | protected async runTasks () { 66 | for (const T of this.tasks) { 67 | const task = new T(this.worker) 68 | 69 | await task.run() 70 | .catch((e) => this.catchError(e)) // Binding issue 71 | } 72 | } 73 | 74 | /** 75 | * Catches an error thrown from one of the tasks 76 | * 77 | * @param {Error} e 78 | * @return {void} 79 | * @throws {Error} 80 | */ 81 | protected catchError (e: Error): void { 82 | if (e instanceof Log) { 83 | this.logs.push(e) 84 | } else { 85 | throw e 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/worker/type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/type.ts 3 | * A bunch of type definitions for the worker process 4 | */ 5 | 6 | import { App } from '../lib/app' 7 | import { Level } from '../lib/log/level' 8 | import * as service from '../lib/service' 9 | import { EventEmitter } from '../lib/utility/eventemitter' 10 | 11 | export type Type = 'app' | 'system-app' | 'library' | 'system-library' | 'debug' 12 | 13 | export { IPackage } from '../lib/service' 14 | 15 | export interface IResult { 16 | failed: boolean 17 | 18 | packages: service.IPackage[] 19 | 20 | appcenter?: object 21 | appstream?: string 22 | 23 | logs: ILog[] 24 | } 25 | 26 | export interface ILog extends service.ILog { 27 | level: Level 28 | } 29 | 30 | export interface IChange { 31 | version: string 32 | author: string 33 | changes: string 34 | date: Date 35 | } 36 | 37 | export interface IContext { 38 | type: Type 39 | 40 | nameDeveloper: string 41 | nameDomain: string 42 | nameHuman: string 43 | 44 | version: string 45 | 46 | architecture: string 47 | distribution: string 48 | 49 | references: string[] 50 | changelog: IChange[] 51 | 52 | package?: service.IPackage 53 | 54 | appcenter?: object 55 | appstream?: string // An XML formatted string 56 | 57 | stripe?: string 58 | 59 | logs: ILog[] 60 | } 61 | 62 | export interface ITaskConstructor { 63 | new (worker: IWorker): ITask 64 | } 65 | 66 | export interface ITask { 67 | run (): Promise 68 | } 69 | 70 | export interface IWorker extends EventEmitter { 71 | app: App 72 | context: IContext 73 | fails: boolean 74 | passes: boolean 75 | postTasks: ITaskConstructor[] 76 | repository: service.ICodeRepository 77 | result: IResult 78 | tasks: ITaskConstructor[] 79 | workspace: string 80 | 81 | setup () 82 | run () 83 | teardown () 84 | stop () 85 | 86 | report (err: Error) 87 | } 88 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/bootstrap.js 3 | * Runs before all tests. Sets things up. 4 | */ 5 | 6 | require('reflect-metadata') 7 | -------------------------------------------------------------------------------- /test/e2e/lib/log/outputs/console.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/e2e/lib/log/outputs/console.ts 3 | * Tests console out logging. End to End testing due to mocks of the console 4 | */ 5 | 6 | import baseTest, { Macro, TestInterface } from 'ava' 7 | import { stub } from 'sinon' 8 | 9 | import { create } from '../../../../utility/app' 10 | 11 | import { App } from '../../../../../src/app' 12 | import { Config } from '../../../../../src/lib/config' 13 | import { Level } from '../../../../../src/lib/log/level' 14 | import { Log } from '../../../../../src/lib/log/log' 15 | import { Logger } from '../../../../../src/lib/log/logger' 16 | import { Console } from '../../../../../src/lib/log/outputs/console' 17 | 18 | interface IContext { 19 | app: App, 20 | config: Config, 21 | logger: Logger, 22 | output: Console, 23 | 24 | info: stub, 25 | warn: stub, 26 | error: stub 27 | } 28 | 29 | const test = baseTest as TestInterface 30 | 31 | const testOutput: Macro<[Level, string, string], IContext> = (t, input: Level, fn: string, expected: string) => { 32 | const log = new Log(t.context.logger) 33 | .setLevel(input) 34 | 35 | t.context.output[fn](log) 36 | 37 | t.true(t.context[expected].called) 38 | } 39 | 40 | testOutput.title = (_, input: Level, fn: string, expected: string) => { 41 | return `${fn} gets outputted to console ${expected}` 42 | } 43 | 44 | test.beforeEach('setup application', async (t) => { 45 | t.context.app = await create() 46 | t.context.config = t.context.app.get(Config) 47 | 48 | t.context.config.unfreeze() 49 | t.context.config.set('log.console', 'debug') 50 | t.context.config.freeze() 51 | 52 | t.context.logger = new Logger(t.context.config) 53 | t.context.output = new Console(t.context.config) 54 | }) 55 | 56 | test.beforeEach('setup console stubs', (t) => { 57 | t.context.info = stub(console, 'info') 58 | t.context.warn = stub(console, 'warn') 59 | t.context.error = stub(console, 'error') 60 | }) 61 | 62 | test.afterEach.always((t) => { 63 | t.context.info.restore() 64 | t.context.warn.restore() 65 | t.context.error.restore() 66 | }) 67 | 68 | test.serial(testOutput, Level.DEBUG, 'debug', 'info') 69 | test.serial(testOutput, Level.INFO, 'info', 'info') 70 | test.serial(testOutput, Level.WARN, 'warn', 'warn') 71 | test.serial(testOutput, Level.ERROR, 'error', 'error') 72 | -------------------------------------------------------------------------------- /test/e2e/lib/service/aptly.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/e2e/lib/service/aptly.ts 3 | * Tests the Aptly package repository class. 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as fs from 'fs-extra' 8 | import * as os from 'os' 9 | import * as path from 'path' 10 | import * as uuid from 'uuid/v4' 11 | 12 | import { App } from '../../../../src/lib/app' 13 | import { Config } from '../../../../src/lib/config' 14 | import * as Log from '../../../../src/lib/log' 15 | import { Aptly } from '../../../../src/lib/service/aptly' 16 | import * as type from '../../../../src/lib/service/type' 17 | 18 | import { tmp } from '../../../utility/fs' 19 | import { record } from '../../../utility/http' 20 | 21 | const test = baseTest as TestInterface<{ 22 | app: App 23 | }> 24 | 25 | const DEFAULT_PKG: type.IPackage = { 26 | architecture: 'amd64', 27 | distribution: 'xenial', 28 | name: 'package', 29 | path: path.resolve(__dirname, '../../../test/fixture/lib/service/github/vocal.deb'), 30 | type: 'deb' 31 | } 32 | 33 | // To test this: `rm -rf $HOME/.aptly && aptly repo create testing && aptly api serve` 34 | // FIXME: Nock back does not like to match the upload request even when recorded 35 | test.failing('can upload a package to aptly', async (t) => { 36 | const { done } = await record('lib/service/aptly/asset.json', { ignoreBody: true }) 37 | const config = t.context.app.get(Config) 38 | 39 | config.unfreeze() 40 | config.set('service.aptly.review', 'testing') 41 | config.freeze() 42 | 43 | const aptly = t.context.app.get(Aptly) 44 | const details = await aptly.uploadPackage(DEFAULT_PKG, 'review') 45 | 46 | t.is(details.aptlyId, 'Pamd64 com.github.needle-and-thread.vocal 2.1.6 9a6a0ef178f67a1e') 47 | }) 48 | -------------------------------------------------------------------------------- /test/e2e/worker/docker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/e2e/worker/docker.ts 3 | * Tests docker usage ability 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as fs from 'fs-extra' 8 | import * as os from 'os' 9 | import * as path from 'path' 10 | import * as uuid from 'uuid/v4' 11 | 12 | import { Config } from '../../../src/lib/config' 13 | import { Docker } from '../../../src/worker/docker' 14 | 15 | import { setup as setupConfig } from '../../utility/config' 16 | import * as dockerUtility from '../../utility/docker' 17 | 18 | interface IContext { 19 | config: Config, 20 | images: string[] 21 | } 22 | 23 | const test = baseTest as TestInterface 24 | 25 | test.beforeEach(async (t) => { 26 | t.context.config = await setupConfig() 27 | t.context.images = [] 28 | }) 29 | 30 | test.afterEach(async (t) => { 31 | await Promise.all(t.context.images.map((image) => { 32 | return dockerUtility.removeImages(t.context.config, image) 33 | })) 34 | }) 35 | 36 | test('can check if image exists', async (t) => { 37 | t.context.images = [`houston-${uuid()}`] 38 | const docker = new Docker(t.context.config, t.context.images[0]) 39 | 40 | t.false(await docker.exists()) 41 | }) 42 | 43 | test('can create a docker image', async (t) => { 44 | t.context.images = [`houston-${uuid()}`] 45 | const docker = new Docker(t.context.config, t.context.images[0]) 46 | 47 | const imageDirectory = path.resolve(__dirname, '../../fixture/worker/docker/image1') 48 | await docker.create(imageDirectory) 49 | 50 | t.true(await docker.exists()) 51 | }) 52 | -------------------------------------------------------------------------------- /test/e2e/worker/task/debian/changelog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/e2e/worker/task/debian/changelog.ts 3 | * Tests that the changelog task works as needed 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { DebianChangelog } from '../../../../../src/worker/task/debian/changelog' 9 | 10 | import { mock } from '../../../../utility/worker' 11 | 12 | test('changelog renders correctly', async (t) => { 13 | const worker = await mock({ 14 | changelog: [{ 15 | author: 'Blake Kostner', 16 | changes: 'updated some fun things', 17 | date: new Date(), 18 | version: '0.0.1' 19 | }, { 20 | author: 'Blake Kostner', 21 | changes: 'resion release', 22 | date: new Date(), 23 | version: '1.0.0' 24 | }], 25 | nameDomain: 'com.github.elementary.houston' 26 | }) 27 | 28 | worker.tasks.push(DebianChangelog) 29 | 30 | await worker.setup() 31 | await worker.run() 32 | 33 | const changelog = await worker.readFile('dirty/debian/changelog') 34 | 35 | await worker.teardown() 36 | 37 | t.not(changelog.indexOf('resion release'), -1) 38 | t.not(changelog.indexOf('updated some fun things'), -1) 39 | }) 40 | -------------------------------------------------------------------------------- /test/e2e/worker/worker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/e2e/worker/worker.ts 3 | * Runs some repositories through tests for end to end testing 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as fs from 'fs-extra' 8 | import * as os from 'os' 9 | import * as path from 'path' 10 | import * as uuid from 'uuid/v4' 11 | 12 | import { App } from '../../../src/lib/app' 13 | import { Config } from '../../../src/lib/config' 14 | import { codeRepositoryFactory, ICodeRepositoryFactory } from '../../../src/lib/service' 15 | import { Build } from '../../../src/worker/preset/build' 16 | import * as type from '../../../src/worker/type' 17 | 18 | import { create } from '../../utility/app' 19 | import { tmp } from '../../utility/fs' 20 | 21 | interface IContext { 22 | app: App, 23 | config: Config, 24 | directory: string 25 | factory: ICodeRepositoryFactory 26 | } 27 | 28 | const test = baseTest as TestInterface 29 | 30 | test.beforeEach(async (t) => { 31 | t.context.app = await create() 32 | t.context.config = t.context.app.get(Config) 33 | t.context.directory = await tmp('worker') 34 | t.context.factory = t.context.app.get(codeRepositoryFactory) 35 | }) 36 | 37 | test('cassidyjames/palette passes build process', async (t) => { 38 | const repo = await t.context.factory('https://github.com/cassidyjames/palette') 39 | const context : type.IContext = { 40 | appcenter: {}, 41 | appstream: '', 42 | architecture: '', 43 | changelog: [], 44 | distribution: '', 45 | logs: [], 46 | nameDeveloper: 'Palette', 47 | nameDomain: 'com.github.cassidyjames.palette', 48 | nameHuman: 'Palette', 49 | references: ['refs/tags/2.2.0'], 50 | type: 'app', 51 | version: '2.2.0' 52 | } 53 | 54 | const proc = Build(t.context.app, repo, context) 55 | proc.workspace = path.resolve(t.context.directory, uuid()) 56 | 57 | proc.on('run:error', (e) => t.log(e)) 58 | 59 | await proc.setup() 60 | await proc.run() 61 | await proc.teardown() 62 | 63 | t.true(proc.passes) 64 | t.is(proc.result.packages.length, 1) 65 | t.is(proc.result.packages[0].name, 'com.github.cassidyjames.palette_2.2.0_amd64.deb') 66 | }) 67 | -------------------------------------------------------------------------------- /test/fixture/lib/service/github/installation.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "scope": "https://api.github.com:443", 4 | "method": "POST", 5 | "path": "/app/installations/341025/access_tokens", 6 | "body": "", 7 | "status": 201, 8 | "response": { 9 | "token": "v1.2474f2e312406721d0f20ef317948573101e6144", 10 | "expires_at": "2018-09-19T02:44:48Z" 11 | }, 12 | "rawHeaders": [ 13 | "Server", 14 | "GitHub.com", 15 | "Date", 16 | "Wed, 19 Sep 2018 01:44:48 GMT", 17 | "Content-Type", 18 | "application/json; charset=utf-8", 19 | "Content-Length", 20 | "91", 21 | "Connection", 22 | "close", 23 | "Status", 24 | "201 Created", 25 | "Cache-Control", 26 | "public, max-age=60, s-maxage=60", 27 | "Vary", 28 | "Accept", 29 | "ETag", 30 | "\"73ede67223acb029a7143bcc6a566a7c\"", 31 | "X-GitHub-Media-Type", 32 | "github.machine-man-preview; format=json", 33 | "Access-Control-Expose-Headers", 34 | "ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval", 35 | "Access-Control-Allow-Origin", 36 | "*", 37 | "Strict-Transport-Security", 38 | "max-age=31536000; includeSubdomains; preload", 39 | "X-Frame-Options", 40 | "deny", 41 | "X-Content-Type-Options", 42 | "nosniff", 43 | "X-XSS-Protection", 44 | "1; mode=block", 45 | "Referrer-Policy", 46 | "origin-when-cross-origin, strict-origin-when-cross-origin", 47 | "Content-Security-Policy", 48 | "default-src 'none'", 49 | "X-Runtime-rack", 50 | "0.030521", 51 | "X-GitHub-Request-Id", 52 | "B468:2B6B:702944:CAFFE6:5BA1AA10" 53 | ] 54 | } 55 | ] -------------------------------------------------------------------------------- /test/fixture/lib/service/github/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAqs5lt7F4qvGAyjqLEQpnj0Tzx7xRe3/0L2NYBTFhHEQrbWdY 3 | L9ZMovofmez8ipGG8Yfc6yDH5Cgq1Z5LJWNu9ZwOIikI71BTrsAVpEtOwBpO02bs 4 | 8E0HRQ8SbAZGDdigQOesuWVCZAbXeB258+CxH2XvoPh63Z3XyqzpCig7fAv3Vrlw 5 | 5haqtPlLJUGIrW5AK+bJmnu8bmX/1FLRkcS8xsf+0k1oDWj55e2QgXGk0hyN6Cl3 6 | vDFhZjHTB9gOQbJmbaFGhdk16MaDYTyoIQC/ZDZE5BvCrMTH5c9vQfHtfSL5/wxs 7 | hMUOq097Myw9RjXa3DoonQECLnBk7rx0Di/dSwIDAQABAoIBAGtktoPW7B3fFrEQ 8 | OhwOxKpKGlT9TOYvv2KEPfWV5I8XNY7mKYZ6YVkflvr7DJY7hfMbD4Yu6D/GGZiS 9 | nID3ke6PnjtQPbPqd5MqZvEUAIG8iM9Wqac1Z1q+bUDKeQV/rNLiV7GBv0qRGq0F 10 | U8PJBNTPDOnTEb2B3wopyaR/Q1dsTtYj/dg9Ap1HEgYAOi5wAAB61IeyKL0lRdva 11 | vlvLc3VgNGAOV/e+HmMsnEK+29WoYRaPx9wdhCey3Iv4TUqoDCokzariSbpNYPLE 12 | jI1NqsmYKuPbeVu6vPAC/wHjEksAmGNwhaOkYkGTrjGgSYeZymRzSX1v2p0fG/AA 13 | 1LZ1rvECgYEA4tP80JBg7MY2Dr76BIxyWz6RjbYLoIn8YWTfvNsieYymkOQNYW93 14 | l48WLqBEKXx7xSQjHX3Ce4cDHXdiS9t3SteBT7RvIPnHT9hl3GksZqMkK+flV6nH 15 | cnS4hPoC+CslsDvZUNpVX30QAk08QqfOBQ77bluDq2MPnPoe9PK4hCcCgYEAwMX3 16 | IzKKclW70oQpLBdHU3Kjvl4wnNJ+v4q+jSMFhWly0TIDfUEAJQls0Du9QI8rLGYi 17 | HuBHD31Ac35QTSwiSggChGUKYLMpbWk4NwZILOwLTEO7E4E0uNC0A+i3oIETeKF7 18 | 46OD43qzoEdMtvz3iN/vThBG2nwkpepfN1FgoD0CgYA7Q43jZEWet276TVV/iL58 19 | Lo3TC8Rf7o19WODINCz+uwvuAVeppHkHpT/zcLY+bKLd8EIoe2or4iujMEUDctTp 20 | PgMwMwFyCTDVIMIEY4pRSsCxpAYc8GQG+I4ZWEUcWBGhyRFPeawipcdgApQDClre 21 | oXp56/kr91bl+cfK0fv5swKBgQCkXKY45nDQx3SbK4AHTdnMtqQSPjDopTjYi62o 22 | nMGqXJw+7Yu4EeHTslOKwES+dNN0yagx9zvfYwW+82X4Rrb9tBKhW50bkeaymNIL 23 | aFnFo4SGhAFPwgx3v8qcwqE+Qo+dfOIq11IudIIPGHu6UbmkhHp/brVauBpvNIP2 24 | oHXoaQKBgF7hmvee6VpdtzPqXUsQQQz5Iep4glokvv7xvG3gWLZM7a5uRZqWQ9S6 25 | VAja/q68j80b21g2eiA35/gRT++zzSvx0RiOr2I6NFZx4Y/+wmfmea7DOob5C8sc 26 | ccxVMlSdhN6fh2HT8BwoTQBWLkG3kbINMekZ41/9lhMVOMnQVYfI 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/fixture/lib/service/github/vocal.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/test/fixture/lib/service/github/vocal.deb -------------------------------------------------------------------------------- /test/fixture/worker/docker/image1/Dockerfile: -------------------------------------------------------------------------------- 1 | # houston/test/fixture/worker/docker/image1/Dockerfile 2 | # A basic Dockerfile for use in testing the docker class 3 | 4 | FROM ubuntu:16.04 5 | 6 | RUN mkdir -p /tmp/justbecause 7 | 8 | ENTRYPOINT ["/bin/bash"] 9 | -------------------------------------------------------------------------------- /test/fixture/worker/log/test1.md: -------------------------------------------------------------------------------- 1 | # <%= title %> 2 | 3 | This is a basic test1 Log template. 4 | 5 | <%= body %> 6 | -------------------------------------------------------------------------------- /test/fixture/worker/task/appstream/blank.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/fixture/worker/task/debian/control/gold1: -------------------------------------------------------------------------------- 1 | Source: com.github.philip-scott.spice-up 2 | Section: editors 3 | Priority: optional 4 | Maintainer: Felipe Escoto 5 | Build-Depends: cmake, 6 | debhelper (>= 9), 7 | libgtk-3-dev, 8 | valac (>= 0.26), 9 | libgranite-dev, 10 | libjson-glib-dev, 11 | libgudev-1.0-dev, 12 | libevdev-dev 13 | 14 | Standards-Version: 3.9.6 15 | Package: com.github.philip-scott.spice-up 16 | Architecture: any 17 | Depends: ${misc:Depends}, ${shlibs:Depends} 18 | Pre-Depends: dpkg (>= 1.15.6) 19 | Description: Spice-Up 20 | Create simple and beautiful presentations 21 | -------------------------------------------------------------------------------- /test/fixture/worker/task/debian/control/gold2: -------------------------------------------------------------------------------- 1 | Source: com.github.jendrikseipp.rednotebook-elementary 2 | Section: text 3 | Priority: optional 4 | Maintainer: Jendrik Seipp 5 | Build-Depends: debhelper (>= 9) 6 | Build-Depends-Indep: python3, dh-python 7 | Standards-Version: 3.9.6 8 | Homepage: http://rednotebook.sourceforge.net/ 9 | Vcs-Git: git://github.com/jendrikseipp/rednotebook.git 10 | Vcs-Browser: https://github.com/jendrikseipp/rednotebook 11 | 12 | Package: com.github.jendrikseipp.rednotebook-elementary 13 | Architecture: all 14 | Depends: ${python3:Depends}, 15 | ${misc:Depends}, 16 | gir1.2-gdkpixbuf-2.0, 17 | gir1.2-glib-2.0, 18 | gir1.2-gtk-3.0, 19 | gir1.2-pango-1.0, 20 | gir1.2-webkit2-4.0, 21 | python3-gi, 22 | python3-yaml 23 | Recommends: python3-enchant 24 | Description: RedNotebook is a modern desktop journal. It lets you 25 | format, tag and search your entries. You can also add pictures, links 26 | and customizable templates, spell check your notes, and export to 27 | plain text, HTML, Latex or PDF. 28 | -------------------------------------------------------------------------------- /test/fixture/worker/task/desktop/blank.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Blank 3 | -------------------------------------------------------------------------------- /test/fixture/worker/task/desktop/spice-up.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Spice-Up 3 | Comment=Spice up your slides 4 | Comment[pt_BR]=Tempere os seus slides 5 | Comment[fr]=Épicer vos présentations 6 | Exec=com.github.philip-scott.spice-up %u 7 | Icon=com.github.philip-scott.spice-up 8 | Terminal=false 9 | Type=Application 10 | StartupNotify=true 11 | Categories=Office;Presentation; 12 | MimeType=application/x-spiceup; 13 | Actions=AboutDialog; 14 | 15 | [Desktop Action AboutDialog] 16 | Exec=com.github.philip-scott.spice-up --about 17 | Name=About Spice-Up 18 | Name[pt_BR]=Sobre o Spice-Up 19 | Name[fr]=À propos de Spice-Up 20 | -------------------------------------------------------------------------------- /test/fixture/worker/task/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elementary/houston/6db94aba6d01e67288efae526883980defe2fa80/test/fixture/worker/task/empty -------------------------------------------------------------------------------- /test/spec/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/config/index.ts 3 | * Tests the configuration class 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { Config } from '../../../../src/lib/config/index' 9 | 10 | test('can be created with an object', (t) => { 11 | const config = new Config({ 12 | key: 'value' 13 | }) 14 | 15 | t.is(config.get('key'), 'value') 16 | }) 17 | 18 | test('can get a value', (t) => { 19 | const config = new Config({ 20 | key: 'value' 21 | }) 22 | 23 | t.is(config.get('key'), 'value') 24 | t.is(config.get('noop'), undefined) 25 | }) 26 | 27 | test('get uses a default value', (t) => { 28 | const config = new Config({ 29 | key: 'value' 30 | }) 31 | 32 | t.is(config.get('noop', 'value'), 'value') 33 | t.is(config.get('key', 'invalid'), 'value') 34 | }) 35 | 36 | test('has returns boolean for existing value', (t) => { 37 | const config = new Config({ 38 | key: 'value' 39 | }) 40 | 41 | t.true(config.has('key')) 42 | t.false(config.has('noop')) 43 | }) 44 | 45 | test('set sets new values', (t) => { 46 | const config = new Config() 47 | 48 | config.set('key', 'value') 49 | 50 | t.is(config.get('key'), 'value') 51 | }) 52 | 53 | test('set sets nested values', (t) => { 54 | const config = new Config() 55 | 56 | config.set('key.nested', 'value') 57 | 58 | t.is(config.get('key.nested'), 'value') 59 | }) 60 | 61 | test('merge sets values', (t) => { 62 | const config = new Config({ 63 | key: 'value' 64 | }) 65 | 66 | config.merge({ 67 | key: { 68 | nested: 'value' 69 | } 70 | }) 71 | 72 | t.is(config.get('key.nested'), 'value') 73 | }) 74 | 75 | test('freeze makes the config immutable', (t) => { 76 | const config = new Config({ 77 | key: 'value' 78 | }) 79 | 80 | config.freeze() 81 | 82 | t.throws(() => config.set('key', 'bad'), /immutable/) 83 | }) 84 | 85 | test('unfreeze makes the config editable', (t) => { 86 | const config = new Config({ 87 | key: 'value' 88 | }) 89 | 90 | config.unfreeze() 91 | 92 | t.notThrows(() => config.set('key', 'good')) 93 | }) 94 | 95 | test('freeze does not mess up with a null value', (t) => { 96 | const config = new Config({ 97 | key: null 98 | }) 99 | 100 | config.freeze() 101 | 102 | t.throws(() => config.set('key', 'bad'), /immutable/) 103 | }) 104 | 105 | test('merge fails on immutable tree', (t) => { 106 | const config = new Config({ 107 | key: 'value' 108 | }) 109 | 110 | config.freeze() 111 | 112 | t.throws(() => config.merge({ more: 'value' }), /immutable/) 113 | }) 114 | -------------------------------------------------------------------------------- /test/spec/lib/config/loader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/config/loader.ts 3 | * Tests configuration loading functions. 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as path from 'path' 8 | 9 | import * as loader from '../../../../src/lib/config/loader' 10 | 11 | import { isCi } from '../../../utility/ci' 12 | 13 | const test = baseTest as TestInterface<{ 14 | testingConfigPath: string 15 | }> 16 | 17 | test.beforeEach((t) => { 18 | t.context.testingConfigPath = path.resolve(__dirname, '..', '..', '..', 'fixture', 'config.js') 19 | }) 20 | 21 | test('can convert houston environment variables to dot notation', (t) => { 22 | t.is(loader.stringToDot('HOUSTON_SERVER_PORT'), 'server.port') 23 | }) 24 | 25 | test('converts env to environment', (t) => { 26 | t.is(loader.stringToDot('HOUSTON_ENV'), 'environment') 27 | }) 28 | 29 | test('can find houston environment variables', (t) => { 30 | process.env.HOUSTON_SERVER_PORT = '3000' 31 | 32 | const config = loader.getEnvironmentConfig() 33 | 34 | t.is(config.get('server.port'), 3000) 35 | }) 36 | 37 | test('assigns environment based on NODE_ENV', (t) => { 38 | process.env.NODE_ENV = 'development' 39 | 40 | const config = loader.getEnvironmentConfig() 41 | 42 | t.is(config.get('environment'), 'development') 43 | }) 44 | 45 | test('assigns console log based on NODE_DEBUG', (t) => { 46 | process.env.NODE_DEBUG = 'true' 47 | 48 | const config = loader.getEnvironmentConfig() 49 | 50 | t.is(config.get('log.console'), 'debug') 51 | }) 52 | 53 | test('can find the package version', (t) => { 54 | const config = loader.getProgramConfig() 55 | 56 | t.true(config.has('houston.version')) 57 | t.true(config.has('houston.major')) 58 | t.true(config.has('houston.minor')) 59 | t.true(config.has('houston.patch')) 60 | }) 61 | 62 | // CI environments usually don't have the git folder. 63 | if (isCi() === false) { 64 | test('can find the git commit', (t) => { 65 | const config = loader.getProgramConfig() 66 | 67 | t.true(config.has('houston.commit')) 68 | }) 69 | 70 | test('can find the git change', (t) => { 71 | const config = loader.getProgramConfig() 72 | 73 | t.true(config.has('houston.change')) 74 | }) 75 | } 76 | 77 | test('can read configuration from file', (t) => { 78 | const { testingConfigPath } = t.context 79 | const config = loader.getFileConfig(testingConfigPath) 80 | 81 | t.is(config.get('environment'), 'testing') 82 | }) 83 | 84 | test('can read configuration from relative path', (t) => { 85 | const { testingConfigPath } = t.context 86 | const relativeConfigPath = path.relative(process.cwd(), testingConfigPath) 87 | const config = loader.getFileConfig(relativeConfigPath) 88 | 89 | t.is(config.get('environment'), 'testing') 90 | }) 91 | 92 | test('getConfig loads environment variables', (t) => { 93 | process.env.HOUSTON_KEY = 'value' 94 | 95 | const { testingConfigPath } = t.context 96 | const config = loader.getConfig(testingConfigPath) 97 | 98 | t.is(config.get('key'), 'value') 99 | }) 100 | -------------------------------------------------------------------------------- /test/spec/lib/log/level.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/log/log.ts 3 | * Tests the log class. 4 | * NOTE: Because of typescript enum values, we compare the enum index value 5 | */ 6 | 7 | import test, { Macro } from 'ava' 8 | 9 | import * as Level from '../../../../src/lib/log/level' 10 | 11 | const testParseLevel: Macro<[string, number]> = (t, input: string, expected: number) => { 12 | t.is(Level.parseLevel(input), expected) 13 | } 14 | 15 | testParseLevel.title = (_, input: string, expected: number) => { 16 | return `parseLevel ${input} = ${expected}` 17 | } 18 | 19 | const testLevelString: Macro<[number, string]> = (t, input: number, expected: string) => { 20 | t.is(Level.levelString(input), expected) 21 | } 22 | 23 | testLevelString.title = (_, input: number, expected: string) => { 24 | return `levelString ${input} = ${expected}` 25 | } 26 | 27 | test(testParseLevel, 'DEBUG', 0) 28 | test(testParseLevel, 'debug', 0) 29 | test(testParseLevel, 'info', 1) 30 | test(testParseLevel, 'warn', 2) 31 | test(testParseLevel, 'error', 3) 32 | 33 | test(testLevelString, 0, 'debug') 34 | test(testLevelString, 1, 'info') 35 | test(testLevelString, 2, 'warn') 36 | test(testLevelString, 3, 'error') 37 | 38 | test('returns a default value if no matching level found', (t) => { 39 | t.is(Level.parseLevel('noop'), 1) 40 | }) 41 | -------------------------------------------------------------------------------- /test/spec/lib/log/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/log/log.ts 3 | * Tests the log class. 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import { create } from '../../../utility/app' 8 | 9 | import { App } from '../../../../src/app' 10 | import { Level } from '../../../../src/lib/log/level' 11 | import { Log } from '../../../../src/lib/log/log' 12 | import { Logger } from '../../../../src/lib/log/logger' 13 | 14 | const test = baseTest as TestInterface<{ 15 | app: App, 16 | logger: Logger 17 | }> 18 | 19 | test.beforeEach(async (t) => { 20 | t.context.app = await create() 21 | t.context.logger = t.context.app.get(Logger) 22 | }) 23 | 24 | test('log can set level', (t) => { 25 | const log = new Log(t.context.logger) 26 | .setLevel(Level.ERROR) 27 | 28 | t.is(log.level, Level.ERROR) 29 | }) 30 | 31 | test('log can set message', (t) => { 32 | const log = new Log(t.context.logger) 33 | .setMessage('testing log message') 34 | 35 | t.is(log.message, 'testing log message') 36 | }) 37 | 38 | test('log can attach arbitrary data to error', (t) => { 39 | const log = new Log(t.context.logger) 40 | .setData('user', 'me!') 41 | 42 | t.deepEqual(log.data, { user: 'me!' }) 43 | }) 44 | 45 | test('log can attach an error', (t) => { 46 | const error = new Error('testing') 47 | const log = new Log(t.context.logger) 48 | .setError(error) 49 | 50 | t.is(log.error, error) 51 | }) 52 | 53 | test('log sets date on creation', (t) => { 54 | const log = new Log(t.context.logger) 55 | 56 | t.true(log.getDate() instanceof Date) 57 | }) 58 | -------------------------------------------------------------------------------- /test/spec/lib/queue/provider.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/queue/provider.ts 3 | * Tests out the container system for the queue. I'm sorry am a IoC noob 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | 8 | import { App } from '../../../../src/lib/app' 9 | import { Config } from '../../../../src/lib/config' 10 | import { IQueue, IQueueConstructor, Queue, workerQueue } from '../../../../src/lib/queue' 11 | 12 | import { create } from '../../../utility/app' 13 | 14 | const test = baseTest as TestInterface<{ 15 | app: App 16 | }> 17 | 18 | test.beforeEach('setup app container', async (t) => { 19 | t.context.app = await create() 20 | }) 21 | 22 | test('Queue throws error if configuration is unset', (t) => { 23 | const { app } = t.context 24 | const config = app.get(Config) 25 | const queueFactory = app.get(Queue) 26 | 27 | config.unfreeze() 28 | config.set('queue.client', null) 29 | 30 | t.throws(() => queueFactory('testing'), /config/) 31 | }) 32 | 33 | test('Queue resolves to a factory function', (t) => { 34 | const { app } = t.context 35 | const queueFactory = app.get(Queue) 36 | 37 | t.is(typeof queueFactory, 'function') 38 | }) 39 | 40 | test('workerQueue is a resolved Queue instance', (t) => { 41 | const { app } = t.context 42 | const queue = app.get(workerQueue) 43 | 44 | t.is(typeof queue.send, 'function') 45 | }) 46 | -------------------------------------------------------------------------------- /test/spec/lib/service/aptly.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/service/aptly.ts 3 | * Tests the Aptly class. 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as path from 'path' 8 | 9 | import { App } from '../../../../src/lib/app' 10 | import { Config } from '../../../../src/lib/config' 11 | import { Aptly, createUrl } from '../../../../src/lib/service/aptly' 12 | import * as type from '../../../../src/lib/service/type' 13 | 14 | import { create } from '../../../utility/app' 15 | import { record } from '../../../utility/http' 16 | 17 | const test = baseTest as TestInterface<{ 18 | app: App 19 | }> 20 | 21 | test.beforeEach(async (t) => { 22 | t.context.app = await create() 23 | }) 24 | 25 | const DEFAULT_PKG: type.IPackage = { 26 | architecture: 'amd64', 27 | distribution: 'xenial', 28 | name: 'package', 29 | path: path.resolve(__dirname, '../../../test/fixture/lib/service/github/vocal.deb'), 30 | type: 'deb' 31 | } 32 | 33 | test('createUrl removes undefined values', (t) => { 34 | t.is(createUrl('test', null, 'things', undefined, 5), 'test/things/5') 35 | }) 36 | 37 | test('resolves aptly details from a config string', (t) => { 38 | const config = t.context.app.get(Config) 39 | 40 | config.unfreeze() 41 | config.set('service.aptly.review', 'prefix') 42 | config.freeze() 43 | 44 | const aptly = t.context.app.get(Aptly) 45 | const details = aptly.getAptlyDetails(DEFAULT_PKG, 'review') 46 | 47 | t.is(details.prefix, 'prefix') 48 | }) 49 | 50 | test('resolves aptly details from a config function', (t) => { 51 | const config = t.context.app.get(Config) 52 | 53 | config.unfreeze() 54 | config.set('service.aptly.review', () => ({ 55 | architectures: ['architecture'], 56 | distribution: 'distribution', 57 | prefix: 'prefix' 58 | })) 59 | config.freeze() 60 | 61 | const aptly = t.context.app.get(Aptly) 62 | const details = aptly.getAptlyDetails(DEFAULT_PKG, 'review') 63 | 64 | t.true(details.architectures.indexOf('architecture') !== -1) 65 | t.is(details.distribution, 'distribution') 66 | t.is(details.prefix, 'prefix') 67 | }) 68 | -------------------------------------------------------------------------------- /test/spec/lib/service/github.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/service/github.ts 3 | * Tests the GitHub class. 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as path from 'path' 8 | 9 | import { github, IGitHubFactory } from '../../../../src/lib/service' 10 | import * as type from '../../../../src/lib/service/type' 11 | 12 | import { create as createApp } from '../../../utility/app' 13 | 14 | const test = baseTest as TestInterface<{ 15 | factory: IGitHubFactory 16 | }> 17 | 18 | test.beforeEach(async (t) => { 19 | const app = await createApp() 20 | 21 | t.context.factory = app.get(github) 22 | }) 23 | 24 | test('url returns correct string without authentication', (t) => { 25 | const repo = t.context.factory('https://github.com/elementary/houston') 26 | 27 | t.is(repo.url, 'https://github.com/elementary/houston.git') 28 | }) 29 | 30 | test('url returns correct string with authentication', (t) => { 31 | const repo = t.context.factory('https://x-access-token:fakeauthcode@github.com/elementary/houston') 32 | 33 | t.is(repo.url, 'https://x-access-token:fakeauthcode@github.com/elementary/houston.git') 34 | }) 35 | 36 | test('can set values based on url', (t) => { 37 | const repo = t.context.factory('https://github.com/noop/repo') 38 | 39 | repo.url = 'https://github.com/elementary/houston' 40 | 41 | t.is(repo.username, 'elementary') 42 | t.is(repo.repository, 'houston') 43 | }) 44 | 45 | test('can set values based on url with auth', (t) => { 46 | const repo = t.context.factory('https://test@github.com/test/test') 47 | 48 | repo.url = 'https://auth@github.com/elementary/houston' 49 | 50 | t.is(repo.username, 'elementary') 51 | t.is(repo.repository, 'houston') 52 | t.is(repo.authUsername, 'x-access-token') 53 | t.is(repo.authPassword, 'auth') 54 | }) 55 | 56 | test('can set values based on ssh url', (t) => { 57 | const repo = t.context.factory('https://test@github.com/test/test') 58 | 59 | repo.url = 'git@github.com:elementary/houston.git' 60 | 61 | t.is(repo.username, 'elementary') 62 | t.is(repo.repository, 'houston') 63 | }) 64 | -------------------------------------------------------------------------------- /test/spec/lib/utility/eventemitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/utility/eventemitter.ts 3 | * Tests we can do the things we need to do in fun node event like fashion 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { EventEmitter } from '../../../../src/lib/utility/eventemitter' 9 | 10 | test('it can modify a value with sync listeners', async (t) => { 11 | const em = new EventEmitter() 12 | 13 | em.on('test', (v) => (v + 2)) 14 | 15 | const value = await em.emitAsyncChain('test', 1) 16 | 17 | t.is(value, 3) 18 | }) 19 | 20 | test('it can modify a value with async listeners', async (t) => { 21 | const em = new EventEmitter() 22 | 23 | em.on('test', async (v) => (v + 1)) 24 | em.on('test', async (v) => (v + 2)) 25 | 26 | const value = await em.emitAsyncChain('test', 1) 27 | 28 | t.is(value, 4) 29 | }) 30 | 31 | test('thrown errors in listeners appear on emitter', async (t) => { 32 | const em = new EventEmitter() 33 | 34 | const err = new Error('this is an error!') 35 | 36 | em.on('test', async (v) => (v + 1)) 37 | em.on('test', async (v) => { throw err }) 38 | 39 | await t.throwsAsync(em.emitAsyncChain('test', 1), err.message) 40 | }) 41 | -------------------------------------------------------------------------------- /test/spec/lib/utility/glob.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/lib/utility/glob.spec.ts 3 | * Tests glob functions 4 | */ 5 | 6 | import * as path from 'path' 7 | 8 | import test from 'ava' 9 | 10 | import { glob } from '../../../../src/lib/utility/glob' 11 | 12 | test('it returns a promise from globbing', async (t) => { 13 | const files = await glob(path.resolve(__dirname, '**')) 14 | 15 | t.not(files.indexOf(__filename), -1) 16 | }) 17 | -------------------------------------------------------------------------------- /test/spec/lib/utility/rdnn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/lib/utility/rdnn.ts 3 | * Tests RDNN functions 4 | */ 5 | 6 | import test, { Macro } from 'ava' 7 | 8 | import * as rdnn from '../../../../src/lib/utility/rdnn' 9 | 10 | const sanitize: Macro<[string, string]> = (t, input: string, expected: string) => { 11 | t.is(rdnn.sanitize(input), expected) 12 | } 13 | 14 | sanitize.title = (t, input: string, expected: string) => { 15 | return `sanitize converts "${input}" to "${expected}"` 16 | } 17 | 18 | test(sanitize, 'com.github.btkostner.this is a repo', 'com.github.btkostner.this_is_a_repo') 19 | test(sanitize, 'com.github.btkostner.this-is-a-repo', 'com.github.btkostner.this_is_a_repo') 20 | test(sanitize, 'com.github.4u.2test', 'com.github._4u._2test') 21 | test(sanitize, 'org.7-zip.archiver', 'org._7_zip.archiver') 22 | test(sanitize, 'com.github.Username.RePoSiToRy', 'com.github.username.repository') 23 | 24 | test('sanitize uses normalizer string', (t) => { 25 | const input = 'com.github.testing.this-domain' 26 | const expected = 'com.github.testing.thisisadomain' 27 | 28 | t.is(rdnn.sanitize(input, 'isa'), expected) 29 | }) 30 | -------------------------------------------------------------------------------- /test/spec/worker/log.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/log.ts 3 | * Tests the worker Log can do log related things :shrug: 4 | */ 5 | 6 | import test from 'ava' 7 | import * as path from 'path' 8 | 9 | import { Log } from '../../../src/worker/log' 10 | 11 | test('it can render a template', (t) => { 12 | const test1 = path.resolve(__dirname, '../../fixture/worker/log/test1.md') 13 | 14 | const log = Log.template(Log.Level.ERROR, test1, { 15 | body: '## This is a subheader', 16 | title: 'testing' 17 | }) 18 | 19 | t.is(log.title, 'testing') 20 | t.is(log.body, 'This is a basic test1 Log template.\n\n## This is a subheader') 21 | }) 22 | 23 | test('setError will change the error message', (t) => { 24 | const error = new Error('this is a test') 25 | 26 | const log = new Log(Log.Level.ERROR, 'test', 'body') 27 | log.setError(error) 28 | 29 | t.is(log.message, error.message) 30 | }) 31 | 32 | test('setError will add error object to the log', (t) => { 33 | const error = new Error('this is a test') 34 | 35 | const log = new Log(Log.Level.ERROR, 'test', 'body') 36 | log.setError(error) 37 | 38 | t.is(log.error, error) 39 | }) 40 | 41 | test('toString returns a nice templated log', (t) => { 42 | const err = new Log(Log.Level.ERROR, 'title', 'body') 43 | 44 | t.is(err.toString(), '# title\n\nbody') 45 | }) 46 | -------------------------------------------------------------------------------- /test/spec/worker/preset/release.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/preset/release.ts 3 | * Tests that the release preset has all the tasks we need 4 | */ 5 | 6 | import baseTest, { TestInterface } from 'ava' 7 | import * as path from 'path' 8 | 9 | import { App } from '../../../../src/lib/app' 10 | import { Release } from '../../../../src/worker/preset/release' 11 | import { Upload } from '../../../../src/worker/task/upload' 12 | import { IContext } from '../../../../src/worker/type' 13 | 14 | import { create as createApp } from '../../../utility/app' 15 | import { context as createContext } from '../../../utility/worker' 16 | import { Repository } from '../../../utility/worker/repository' 17 | 18 | const test = baseTest as TestInterface<{ 19 | app: App, 20 | repo: Repository, 21 | context: IContext 22 | }> 23 | 24 | test.beforeEach(async (t) => { 25 | t.context.app = await createApp() 26 | t.context.repo = new Repository('https://github.com/elementary/houston') 27 | t.context.context = await createContext() 28 | }) 29 | 30 | test('includes regular tasks from Build preset', async (t) => { 31 | const worker = Release(t.context.app, t.context.repo, t.context.context) 32 | 33 | t.not(worker.tasks.length, 0) 34 | }) 35 | 36 | test('includes upload post task', async (t) => { 37 | const worker = Release(t.context.app, t.context.repo, t.context.context) 38 | 39 | t.not(worker.postTasks.indexOf(Upload), -1) 40 | }) 41 | -------------------------------------------------------------------------------- /test/spec/worker/task/appstream/id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/appstream/id.ts 3 | * Tests the appstream id test 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { AppstreamId } from '../../../../../src/worker/task/appstream/id' 9 | 10 | import { mock } from '../../../../utility/worker' 11 | 12 | test('passes with a matching ID', async (t) => { 13 | const worker = await mock({ 14 | nameDomain: 'com.github.philip-scott.spice-up' 15 | }) 16 | 17 | await worker.mock('task/appstream/spice-up.xml', 'package/usr/share/metainfo/com.github.philip-scott.spice-up.appdata.xml') 18 | 19 | worker.tasks.push(AppstreamId) 20 | 21 | await worker.setup() 22 | await worker.run() 23 | await worker.teardown() 24 | 25 | worker.context.logs.forEach((l) => t.log(l)) 26 | 27 | t.true(worker.passes) 28 | }) 29 | 30 | test('fails with an incorrect ID', async (t) => { 31 | const worker = await mock({ 32 | nameDomain: 'com.github.philip-scott.spice-up' 33 | }) 34 | 35 | await worker.mock('task/appstream/spice-up.xml', 'package/usr/share/metainfo/com.github.elementary.houston.appdata.xml') 36 | 37 | worker.tasks.push(AppstreamId) 38 | 39 | await worker.setup() 40 | await worker.run() 41 | await worker.teardown() 42 | 43 | worker.context.logs.forEach((l) => t.log(l)) 44 | 45 | t.true(worker.fails) 46 | }) 47 | -------------------------------------------------------------------------------- /test/spec/worker/task/appstream/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/appstream/index.ts 3 | * Tests that known good appstream files pass appstream testing 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { Appstream } from '../../../../../src/worker/task/appstream/index' 9 | 10 | import { mock } from '../../../../utility/worker' 11 | 12 | test('failures stop the build', async (t) => { 13 | const worker = await mock() 14 | 15 | worker.tasks.push(Appstream) 16 | 17 | await worker.setup() 18 | await worker.run() 19 | await worker.teardown() 20 | 21 | t.true(worker.fails) 22 | }) 23 | 24 | test('com.github.philip-scott.spice-up passes appstream tests', async (t) => { 25 | const worker = await mock({ 26 | nameDomain: 'com.github.philip-scott.spice-up' 27 | }) 28 | 29 | await worker.mock('task/appstream/spice-up.xml', 'package/usr/share/metainfo/com.github.philip-scott.spice-up.appdata.xml') 30 | 31 | worker.tasks.push(Appstream) 32 | 33 | await worker.setup() 34 | await worker.run() 35 | await worker.teardown() 36 | 37 | t.true(worker.passes) 38 | }) 39 | 40 | test('basic errors get concated to single log', async (t) => { 41 | const worker = await mock({ 42 | nameDomain: 'com.github.philip-scott.spice-up' 43 | }) 44 | 45 | await worker.mock('task/appstream/blank.xml', 'package/usr/share/metainfo/com.github.philip-scott.spice-up.appdata.xml') 46 | 47 | worker.tasks.push(Appstream) 48 | 49 | await worker.setup() 50 | await worker.run() 51 | await worker.teardown() 52 | 53 | worker.context.logs.forEach((l) => t.log(l)) 54 | 55 | const combinedLog = worker.context.logs 56 | .find((log) => (log.title.match(/appstream tests/i) != null)) 57 | 58 | t.true(worker.fails) 59 | 60 | t.regex(combinedLog.body, /description/) 61 | t.regex(combinedLog.body, /summary/) 62 | t.regex(combinedLog.body, /screenshots/) 63 | 64 | t.regex(combinedLog.body, /id/) 65 | t.regex(combinedLog.body, /name/) 66 | t.regex(combinedLog.body, /project_license/) 67 | }) 68 | -------------------------------------------------------------------------------- /test/spec/worker/task/appstream/screenshot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/appstream/screenshot.ts 3 | * Tests the appstream screenshot test 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { AppstreamScreenshot } from '../../../../../src/worker/task/appstream/screenshot' 9 | 10 | import { mock } from '../../../../utility/worker' 11 | 12 | test('passes with screenshots specified', async (t) => { 13 | const worker = await mock({ 14 | nameDomain: 'com.github.philip-scott.spice-up' 15 | }) 16 | 17 | await worker.mock('task/appstream/spice-up.xml', 'package/usr/share/metainfo/com.github.philip-scott.spice-up.appdata.xml') 18 | 19 | worker.tasks.push(AppstreamScreenshot) 20 | 21 | await worker.setup() 22 | await worker.run() 23 | await worker.teardown() 24 | 25 | worker.context.logs.forEach((l) => t.log(l)) 26 | 27 | t.true(worker.passes) 28 | }) 29 | -------------------------------------------------------------------------------- /test/spec/worker/task/appstream/stripe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/appstream/stripe.ts 3 | * Tests inserting stripe keys into appstream file 4 | */ 5 | 6 | import test from 'ava' 7 | import * as cheerio from 'cheerio' 8 | import * as fs from 'fs-extra' 9 | 10 | import { AppstreamStripe } from '../../../../../src/worker/task/appstream/stripe' 11 | 12 | import { mock } from '../../../../utility/worker' 13 | 14 | test('can insert a basic list of changes', async (t) => { 15 | const worker = await mock({ 16 | nameDomain: 'com.github.elementary.houston', 17 | stripe: 'testingvaluehere' 18 | }) 19 | 20 | const p = 'package/usr/share/metainfo/com.github.elementary.houston.appdata.xml' 21 | await worker.mock('task/appstream/blank.xml', p) 22 | 23 | worker.tasks.push(AppstreamStripe) 24 | 25 | await worker.setup() 26 | await worker.run() 27 | 28 | const file = await fs.readFile(worker.get(p), 'utf8') 29 | const $ = cheerio.load(file, { xmlMode: true }) 30 | 31 | worker.context.logs.forEach((l) => t.log(l)) 32 | 33 | t.true(worker.passes) 34 | t.is($('component > custom > value').text(), 'testingvaluehere') 35 | 36 | await worker.teardown() 37 | }) 38 | -------------------------------------------------------------------------------- /test/spec/worker/task/appstream/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/appstream/validate.spec.ts 3 | * Tests the validatecli docker test 4 | */ 5 | 6 | import test from 'ava' 7 | import * as fs from 'fs-extra' 8 | 9 | import { AppstreamValidate } from '../../../../../src/worker/task/appstream/validate' 10 | 11 | import { mock } from '../../../../utility/worker' 12 | 13 | test('passes with a valid appstream file', async (t) => { 14 | const worker = await mock({ 15 | nameDomain: 'com.github.philip-scott.spice-up' 16 | }) 17 | 18 | await worker.mock('task/appstream/spice-up.xml', 'package/usr/share/metainfo/com.github.philip-scott.spice-up.appdata.xml') 19 | 20 | worker.tasks.push(AppstreamValidate) 21 | 22 | await worker.setup() 23 | await worker.run() 24 | await worker.teardown() 25 | 26 | worker.context.logs.forEach((l) => t.log(l)) 27 | 28 | t.true(worker.passes) 29 | }) 30 | 31 | test('fails with a blank appstream file', async (t) => { 32 | const worker = await mock({ 33 | nameDomain: 'com.github.philip-scott.spice-up' 34 | }) 35 | 36 | await worker.mock('task/appstream/blank.xml', 'package/usr/share/metainfo/com.github.philip-scott.spice-up.appdata.xml') 37 | 38 | worker.tasks.push(AppstreamValidate) 39 | 40 | await worker.setup() 41 | await worker.run() 42 | await worker.teardown() 43 | 44 | worker.context.logs.forEach((l) => t.log(l)) 45 | 46 | t.true(worker.fails) 47 | }) 48 | -------------------------------------------------------------------------------- /test/spec/worker/task/debian/control.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/debian/control.ts 3 | * Testing the tests that test the Debian control file 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | test.todo('Debian control file source error fails build') 9 | test.todo('Debian control file package error fails build') 10 | test.todo('Debian control file maintainer error fails build') 11 | test.todo('Correct Debian control file passes build') 12 | -------------------------------------------------------------------------------- /test/spec/worker/task/desktop/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/desktop/index.ts 3 | * Tests that known good appstream files pass appstream testing 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { DesktopIcon } from '../../../../../src/worker/task/desktop/icon' 9 | import { Desktop } from '../../../../../src/worker/task/desktop/index' 10 | 11 | import { mock } from '../../../../utility/worker' 12 | 13 | test('failures stop the build', async (t) => { 14 | const worker = await mock() 15 | 16 | worker.tasks.push(Desktop) 17 | 18 | await worker.setup() 19 | await worker.run() 20 | await worker.teardown() 21 | 22 | t.true(worker.fails) 23 | }) 24 | 25 | test('com.github.philip-scott.spice-up passes desktop tests', async (t) => { 26 | const worker = await mock({ 27 | nameDomain: 'com.github.philip-scott.spice-up' 28 | }) 29 | 30 | await worker.mock('task/desktop/spice-up.desktop', 'package/usr/share/applications/com.github.philip-scott.spice-up.desktop') 31 | 32 | worker.tasks.push(Desktop) 33 | 34 | await worker.setup() 35 | await worker.run() 36 | await worker.teardown() 37 | 38 | t.true(worker.passes) 39 | }) 40 | 41 | test('system apps do not have icon validation #590', async (t) => { 42 | const worker = await mock({ 43 | nameDomain: 'io.elementary.appcenter', 44 | type: 'system-app' 45 | }) 46 | 47 | const desktop = new Desktop(worker) 48 | 49 | t.is(desktop.tasks.indexOf(DesktopIcon), -1) 50 | }) 51 | -------------------------------------------------------------------------------- /test/spec/worker/task/desktop/validate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/src/worker/task/desktop/validate.spec.ts 3 | * Tests the desktop-file-validate docker test 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { DesktopValidate } from '../../../../../src/worker/task/desktop/validate' 9 | 10 | import { mock } from '../../../../utility/worker' 11 | 12 | test('validates files besides default desktop file', async (t) => { 13 | const worker = await mock({ 14 | nameDomain: 'com.github.philip-scott.spice-up' 15 | }) 16 | 17 | await worker.mock('task/desktop/blank.desktop', 'package/usr/share/applications/blank.desktop') 18 | 19 | worker.tasks.push(DesktopValidate) 20 | 21 | await worker.setup() 22 | await worker.run() 23 | await worker.teardown() 24 | 25 | t.is(worker.context.logs.length, 1) 26 | t.regex(worker.context.logs[0].toString(), /errors/) 27 | t.regex(worker.context.logs[0].toString(), /blank\.desktop/) 28 | }) 29 | 30 | test('validate concats logs to single issue', async (t) => { 31 | const worker = await mock({ 32 | nameDomain: 'com.github.philip-scott.spice-up' 33 | }) 34 | 35 | await worker.mock('task/desktop/blank.desktop', 'package/usr/share/applications/blank.desktop') 36 | await worker.mock('task/desktop/blank.desktop', 'package/usr/share/applications/another-blank.desktop') 37 | await worker.mock('task/desktop/spice-up.desktop', 'package/usr/share/applications/com.github.philip-scott.spice-up.desktop') 38 | 39 | worker.tasks.push(DesktopValidate) 40 | 41 | await worker.setup() 42 | await worker.run() 43 | await worker.teardown() 44 | 45 | t.is(worker.context.logs.length, 1) 46 | t.regex(worker.context.logs[0].toString(), /blank\.desktop/) 47 | t.regex(worker.context.logs[0].toString(), /another-blank\.desktop/) 48 | }) 49 | -------------------------------------------------------------------------------- /test/spec/worker/task/file/deb/binary.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/file/deb/binary.ts 3 | * Tests the deb file binary test 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { FileDebBinary } from '../../../../../../src/worker/task/file/deb/binary' 9 | 10 | import { mock } from '../../../../../utility/worker' 11 | 12 | test('matches a correctly named bin file', async (t) => { 13 | const worker = await mock({ 14 | nameDomain: 'com.github.elementary.houston' 15 | }) 16 | 17 | await worker.mock('task/empty', 'package/usr/bin/com.github.elementary.houston') 18 | 19 | worker.tasks.push(FileDebBinary) 20 | 21 | await worker.setup() 22 | await worker.run() 23 | await worker.teardown() 24 | 25 | t.true(worker.passes) 26 | }) 27 | 28 | test('includes project files in error log', async (t) => { 29 | const worker = await mock({ 30 | nameDomain: 'com.github.elementary.houston' 31 | }) 32 | 33 | await worker.mock('task/empty', 'package/usr/share/docs/com.github.elementary.desktop') 34 | await worker.mock('task/empty', 'package/usr/n00p/test') 35 | 36 | worker.tasks.push(FileDebBinary) 37 | 38 | await worker.setup() 39 | await worker.run() 40 | await worker.teardown() 41 | 42 | t.true(worker.fails) 43 | t.regex(worker.context.logs[0].body, /usr\/share\/docs\/com\.github\.elementary\.desktop/) 44 | }) 45 | -------------------------------------------------------------------------------- /test/spec/worker/task/workspace/setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/spec/worker/task/workspace/setup.ts 3 | * Tests building out workspaces 4 | */ 5 | 6 | import test from 'ava' 7 | 8 | import { WorkspaceSetup } from '../../../../../src/worker/task/workspace/setup' 9 | 10 | import { mock } from '../../../../utility/worker' 11 | 12 | test('builds workspace from all matching branches', async (t) => { 13 | const worker = await mock({ 14 | distribution: 'loki', 15 | nameDomain: 'com.github.elementary.houston', 16 | references: ['refs/heads/loki'] 17 | }) 18 | 19 | worker.repository.references = async () => ([ 20 | 'refs/heads/deb-packaging-loki', 21 | 'refs/heads/deb-packaging-juno', 22 | 'refs/heads/deb-packaging', 23 | 'refs/heads/juno', 24 | 'refs/heads/loki', 25 | 'refs/heads/master' 26 | ]) 27 | 28 | const setup = new WorkspaceSetup(worker) 29 | const setups = await setup.possibleBuilds() 30 | 31 | t.deepEqual(setups, [{ 32 | architecture: 'amd64', 33 | distribution: 'loki', 34 | packageType: 'deb' 35 | }, { 36 | architecture: 'amd64', 37 | distribution: 'juno', 38 | packageType: 'deb' 39 | }]) 40 | }) 41 | 42 | test('deb-packaging adds the latest version even if builds exist', async (t) => { 43 | const worker = await mock({ 44 | distribution: 'loki', 45 | nameDomain: 'com.github.elementary.houston', 46 | references: ['refs/heads/loki'] 47 | }) 48 | 49 | worker.repository.references = async () => ([ 50 | 'refs/heads/deb-packaging-loki', 51 | 'refs/heads/deb-packaging', 52 | 'refs/heads/master' 53 | ]) 54 | 55 | const setup = new WorkspaceSetup(worker) 56 | const setups = await setup.possibleBuilds() 57 | 58 | t.deepEqual(setups, [{ 59 | architecture: 'amd64', 60 | distribution: 'loki', 61 | packageType: 'deb' 62 | }, { 63 | architecture: 'amd64', 64 | distribution: 'juno', 65 | packageType: 'deb' 66 | }]) 67 | }) 68 | 69 | test('builds workspace defaults to latest version', async (t) => { 70 | const worker = await mock({ 71 | distribution: 'loki', 72 | nameDomain: 'com.github.elementary.houston', 73 | references: ['refs/heads/loki'] 74 | }) 75 | 76 | worker.repository.references = async () => ([ 77 | 'refs/heads/deb-packaging', 78 | 'refs/heads/master' 79 | ]) 80 | 81 | const setup = new WorkspaceSetup(worker) 82 | const setups = await setup.possibleBuilds() 83 | 84 | t.deepEqual(setups, [{ 85 | architecture: 'amd64', 86 | distribution: 'juno', 87 | packageType: 'deb' 88 | }]) 89 | }) 90 | -------------------------------------------------------------------------------- /test/utility/app.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/app.ts 3 | * Utility for setting up a new app for testing. 4 | */ 5 | 6 | import { App } from '../../src/app' 7 | import { setup as setupConfig } from './config' 8 | 9 | /** 10 | * create 11 | * Creates a new testable app 12 | * 13 | * @return {App} 14 | */ 15 | export async function create (): Promise { 16 | const config = await setupConfig() 17 | 18 | return new App(config) 19 | } 20 | -------------------------------------------------------------------------------- /test/utility/ci.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/ci.ts 3 | * Utility classes for tests running on continuous integration. Most of these 4 | * apply to travis. 5 | * 6 | * @exports {Function} isCi - Checks if the current test is ran in CI 7 | */ 8 | 9 | /** 10 | * isCi 11 | * Checks if the current process is ran in CI. Useful for disabling some tests. 12 | * 13 | * @return {boolean} 14 | */ 15 | export function isCi (): boolean { 16 | if (process.env.CI === 'true' || process.env.CI === '1') { 17 | return true 18 | } 19 | 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /test/utility/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/config.ts 3 | * Sets up a configuration file used during tests 4 | * 5 | * @exports {Function} setup - Creates a new Config for testing 6 | */ 7 | 8 | import * as path from 'path' 9 | 10 | import { Config } from '../../src/lib/config' 11 | import { getFileConfig } from '../../src/lib/config/loader' 12 | 13 | /** 14 | * setup 15 | * Creates a new Config for use in tests 16 | * 17 | * @async 18 | * @return {Config} 19 | */ 20 | export async function setup (): Promise { 21 | const configPath = path.resolve(__dirname, '../fixture/config.js') 22 | return getFileConfig(configPath) 23 | } 24 | -------------------------------------------------------------------------------- /test/utility/database.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/database.ts 3 | * Usefull things for database usage in tests 4 | * 5 | * @exports {Function} migrate - Updates the database to the latest schemas 6 | * @exports {Function} seed - Adds basic data to the database for use in tests 7 | * @exports {Function} setup - Creates an in memory database for use in tests 8 | */ 9 | 10 | import { create } from './app' 11 | 12 | import { Config } from '../../src/lib/config' 13 | import { Database } from '../../src/lib/database/database' 14 | 15 | /** 16 | * migrate 17 | * Updates the database to the latest schemas 18 | * 19 | * @async 20 | * @param {Database} database - The database connection to run migrations on 21 | * @return {void} 22 | */ 23 | export async function migrate (database: Database): Promise { 24 | await database.knex.migrate.latest() 25 | } 26 | 27 | /** 28 | * seed 29 | * Adds basic data to the database for use in tests 30 | * 31 | * @async 32 | * @param {Database} database - The database connection to run migrations on 33 | * @return {void} 34 | */ 35 | export async function seed (database: Database): Promise { 36 | await database.knex.seed.run() 37 | } 38 | 39 | /** 40 | * setup 41 | * Creates an in memory database for use in tests 42 | * 43 | * @async 44 | * @param {Config} config - Configuration to use for database setup 45 | * @return {Database} - A fully setup database connection 46 | */ 47 | export async function setup (config: Config): Promise { 48 | const app = await create() 49 | const database = app.get(Database) 50 | 51 | await migrate(database) 52 | await seed(database) 53 | 54 | return database 55 | } 56 | -------------------------------------------------------------------------------- /test/utility/docker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/docker.ts 3 | * Usefull utility functions for testing with docker. 4 | * TODO: This has very similar content to the `worker/docker.ts` file. 5 | * TODO: We need to make this more specific and only remove the given images 6 | * 7 | * @exports {Function} teardown - Removes all current containers 8 | */ 9 | 10 | import * as Dockerode from 'dockerode' 11 | 12 | import { Config } from '../../src/lib/config' 13 | 14 | /** 15 | * removeImage 16 | * Removes an image from the docker server 17 | * 18 | * @async 19 | * @param {Config} config The configuration to use to connect to docker 20 | * @param {string} image The name of the image to remove 21 | * @return {void} 22 | */ 23 | export async function removeImages (config: Config, image: string): Promise { 24 | const docker = new Dockerode(config.get('docker')) 25 | 26 | const images = await docker.listImages() 27 | .then((imageInfos) => { 28 | return imageInfos.filter((imageInfo) => { 29 | return (imageInfo.RepoTags || []) 30 | .some((tag) => tag.startsWith(image)) 31 | }) 32 | }) 33 | .then((imageInfos) => { 34 | return imageInfos.map((imageInfo) => docker.getImage(imageInfo.Id)) 35 | }) 36 | 37 | await Promise.all(images.map((i) => { 38 | i.remove({ force: true }).catch((err) => { 39 | // If the image is not found, or is currently being used 40 | if (err.statusCode !== 404 && err.statusCode !== 409) { 41 | // tslint:disable-next-line no-console 42 | console.error('Unable to remove docker image', err) 43 | } 44 | }) 45 | })) 46 | } 47 | -------------------------------------------------------------------------------- /test/utility/fs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/fs.ts 3 | * Utilties for testing on the filesystem 4 | * 5 | * @exports {Function} tmp - Creates a temp directory for tests 6 | */ 7 | 8 | import * as fs from 'fs-extra' 9 | import * as os from 'os' 10 | import * as path from 'path' 11 | import * as uuid from 'uuid/v4' 12 | 13 | /** 14 | * Returns full path to a fixture file 15 | * 16 | * @param {string} file - Relative to the fixture test directory 17 | * @return {string} 18 | */ 19 | export function fixture (file = ''): string { 20 | return path.resolve(__dirname, '../fixture', file) 21 | } 22 | 23 | /** 24 | * tmp 25 | * Creates a temp directory for tests 26 | * 27 | * @async 28 | * @param {string} [dir] - Directory to create 29 | * @return {string} 30 | */ 31 | export async function tmp (dir = ''): Promise { 32 | const directory = path.resolve(os.tmpdir(), 'houston-test', dir, uuid()) 33 | 34 | await fs.ensureDir(directory) 35 | 36 | return directory 37 | } 38 | -------------------------------------------------------------------------------- /test/utility/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/http.ts 3 | * Utilities for when you are testing http related things 4 | */ 5 | 6 | import * as nock from 'nock' 7 | import * as path from 'path' 8 | 9 | nock.back.fixtures = path.resolve(__dirname, '../fixture') 10 | 11 | export interface INockOptions { 12 | ignoreBody?: boolean 13 | } 14 | 15 | /** 16 | * Sets up nock to record all API calls in the test. 17 | * 18 | * @param {String} p Path of the mock 19 | * @param {INockOptions} opts 20 | * @return {Object} 21 | * @return {Function} done - The function to run to stop mocking 22 | */ 23 | export async function record (p: string, opts?: INockOptions) { 24 | const options: any = {} // tslint:disable-line no-any 25 | 26 | if (opts != null && opts.ignoreBody === true) { 27 | options.before = (scope) => { 28 | scope.filteringRequestBody = () => '*' 29 | } 30 | } 31 | 32 | const { nockDone } = await nock.back(p, options) 33 | 34 | return { done: nockDone } 35 | } 36 | -------------------------------------------------------------------------------- /test/utility/worker/context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utililty/worker/context.ts 3 | * Helpful functions to test the worker process context 4 | */ 5 | 6 | import { defaultsDeep } from 'lodash' 7 | 8 | import { IContext } from '../../../src/worker/type' 9 | 10 | /** 11 | * Creates a new context object for testing. 12 | * 13 | * @param {Object} [override] 14 | * 15 | * @return {IContext} 16 | */ 17 | export function context (override = {}) { 18 | const def: IContext = { 19 | appcenter: {}, 20 | appstream: '', 21 | architecture: 'amd64', 22 | changelog: [], 23 | distribution: 'loki', 24 | logs: [], 25 | nameDeveloper: 'elementary', 26 | nameDomain: 'io.elementary.houston', 27 | nameHuman: 'Houston', 28 | references: ['refs/heads/master'], 29 | type: 'app', 30 | version: '0.0.1' 31 | } 32 | 33 | return defaultsDeep({}, override, def) 34 | } 35 | -------------------------------------------------------------------------------- /test/utility/worker/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utililty/worker.ts 3 | * Helpful functions to test the worker process 4 | */ 5 | 6 | export { context } from './context' 7 | export { mock } from './mock' 8 | export { TestWorker } from './worker' 9 | -------------------------------------------------------------------------------- /test/utility/worker/mock.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utililty/worker.ts 3 | * Helpful functions to test the worker process 4 | */ 5 | 6 | import { create } from '../app' 7 | import { context } from './context' 8 | import { Repository } from './repository' 9 | import { TestWorker } from './worker' 10 | 11 | export async function mock (values = {}): Promise { 12 | const app = await create() 13 | const store = context(values) 14 | const repo = new Repository('testrepo') 15 | 16 | return new TestWorker(app, repo, store) 17 | } 18 | -------------------------------------------------------------------------------- /test/utility/worker/repository.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utility/worker/repository.ts 3 | * A third party repository that implements all of our needed interfaces. 4 | * Used for testing without any side effects. 5 | */ 6 | 7 | import * as type from '../../../src/lib/service/type' 8 | import { sanitize } from '../../../src/lib/utility/rdnn' 9 | 10 | export class Repository implements type.ICodeRepository, type.IPackageRepository, type.ILogRepository { 11 | public url: string 12 | 13 | public serviceName = 'mock Repository' 14 | 15 | constructor (url: string) { 16 | this.url = url 17 | } 18 | 19 | public get rdnn () { 20 | const [host, ...paths] = this.url.split('://')[1].split('/') 21 | 22 | const h = host.split('.').reverse().join('.') 23 | const p = paths.join('.') 24 | 25 | return sanitize(`${h}${p}`) 26 | } 27 | 28 | public async clone (p: string, reference): Promise { 29 | throw new Error('Unimplimented in mock repository') 30 | } 31 | 32 | public async references (): Promise { 33 | return ['refs/origin/master'] 34 | } 35 | 36 | public async uploadPackage (pkg: type.IPackage, stage: type.IStage, reference?: string): Promise { 37 | throw new Error('Unimplimented in mock repository') 38 | } 39 | 40 | public async uploadLog (log: type.ILog, stage: type.IStage, reference?: string): Promise { 41 | throw new Error('Unimplimented in mock repository') 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/utility/worker/worker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * houston/test/utililty/worker/worker.ts 3 | * Helpful functions to test the worker process 4 | */ 5 | 6 | import * as fs from 'fs-extra' 7 | import * as os from 'os' 8 | import * as path from 'path' 9 | import * as uuid from 'uuid' 10 | 11 | import { Worker } from '../../../src/worker/worker' 12 | 13 | export class TestWorker extends Worker { 14 | /** 15 | * Creates a new worker process 16 | * 17 | * @param {Config} config - The configuration to use 18 | * @param {Repository} repository - The repository to process on 19 | * @param {IContext} context - Storage for the worker information 20 | */ 21 | constructor (config, repository, storage) { 22 | super(config, repository, storage) 23 | 24 | this.workspace = path.resolve(os.tmpdir(), 'houston-test/worker', uuid()) 25 | } 26 | 27 | /** 28 | * Returns the full path of a file in the workspace 29 | * 30 | * @param {string} p 31 | * @return {string} 32 | */ 33 | public get (p) { 34 | return path.resolve(this.workspace, p) 35 | } 36 | 37 | /** 38 | * Mocks files from the fixture worker directory to the current workspace 39 | * 40 | * @async 41 | * @param {String} from - Path relative to `houston/test/fixture/worker` 42 | * @param {String} to - Path relative to current workspace 43 | * @return {void} 44 | */ 45 | public async mock (from, to) { 46 | const fullFrom = path.resolve(__dirname, '../../fixture/worker', from) 47 | 48 | return fs.copy(fullFrom, this.get(to), { overwrite: true }) 49 | } 50 | 51 | /** 52 | * Returns the string version a file file in the worker directory 53 | * 54 | * @async 55 | * @param {string} p Path of file in worker workspace 56 | * @return {string} 57 | */ 58 | public async readFile (p) { 59 | return fs.readFile(this.get(p), 'utf8') 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.production.json", 3 | 4 | "include": [ 5 | "./src", 6 | "./test" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src" 4 | ], 5 | 6 | "compilerOptions": { 7 | "alwaysStrict": true, 8 | "baseUrl": "./src", 9 | "declaration": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "lib": ["ES6", "ES2015"], 14 | "module": "commonjs", 15 | "moduleResolution": "node", 16 | "outDir": "./dest", 17 | "pretty": true, 18 | "sourceMap": true, 19 | "target": "es5" 20 | } 21 | } 22 | --------------------------------------------------------------------------------