├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── brunch-config.js ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── dockerfiles ├── faktor-elixir │ └── Dockerfile ├── faktor-git │ └── Dockerfile ├── faktor-ruby │ └── Dockerfile ├── faktor-test-image │ └── Dockerfile └── otp-18.0-ex-1.1.0 │ └── Dockerfile ├── dockertools ├── bin │ ├── print-container-info │ ├── print-elixir-info │ ├── run-elixir-deps │ ├── test-error │ ├── test-output │ └── test-produce-bad-result-json └── mix │ └── hex_faktor │ ├── .gitignore │ ├── README.md │ ├── config │ └── config.exs │ ├── lib │ ├── hex_faktor.ex │ ├── hex_faktor │ │ ├── cli.ex │ │ ├── deps.ex │ │ ├── exs_loader.ex │ │ ├── mix_exs_loader.ex │ │ ├── mix_lock_loader.ex │ │ ├── service │ │ │ └── hex_service.ex │ │ └── util │ │ │ └── json.ex │ └── mix │ │ └── tasks │ │ └── faktor.ex │ ├── mix.exs │ ├── mix.lock │ ├── setup.sh │ └── test │ ├── mix_exs_loader_test.exs │ ├── mix_lock_loader_test.exs │ └── test_helper.exs ├── lib ├── hex_faktor.ex ├── hex_faktor │ ├── app_event.ex │ ├── auth.ex │ ├── broadcast.ex │ ├── deps_object_filter.ex │ ├── email_publisher.ex │ ├── email_verifier.ex │ ├── endpoint.ex │ ├── notification_mailer.ex │ ├── notification_publisher.ex │ ├── notification_resolver.ex │ ├── project_access.ex │ ├── repo.ex │ ├── sort_helper.ex │ └── version_helper.ex ├── mix │ └── tasks │ │ ├── docker │ │ └── build.ex │ │ └── faktor │ │ ├── get.ex │ │ └── top.ex └── refaktor │ ├── builder.ex │ ├── builder │ ├── git.ex │ ├── job.ex │ ├── models │ │ ├── build.ex │ │ ├── build_job.ex │ │ ├── git_branch.ex │ │ ├── git_repo.ex │ │ └── git_revision.ex │ └── persistence │ │ ├── build.ex │ │ ├── build_job.ex │ │ ├── git_branch.ex │ │ ├── git_repo.ex │ │ ├── git_revision.ex │ │ └── job_objects.ex │ ├── docker.ex │ ├── docker │ ├── build.ex │ ├── image.ex │ └── run.ex │ ├── job │ └── elixir │ │ ├── deps.ex │ │ └── deps │ │ ├── models │ │ └── deps_object.ex │ │ └── persistence.ex │ ├── use_case │ └── git.ex │ ├── util │ └── json.ex │ └── worker │ ├── job_runner.ex │ ├── progress_callback.ex │ └── supervisor.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ ├── 20151004151127_add_git_objects.exs │ ├── 20151004151130_add_inch_objects.exs │ ├── 20151006214612_add_build_objects.exs │ ├── 20151210191401_add_deps_objects.exs │ ├── 20151222211906_create_user.exs │ ├── 20151223202938_create_project.exs │ ├── 20151223211835_create_project_user.exs │ ├── 20151227211157_create_package.exs │ ├── 20160101214426_create_notification.exs │ ├── 20160106134613_create_project_hook.exs │ ├── 20160114072856_create_project_user_settings.exs │ ├── 20160121145122_create_app_event_log.exs │ ├── 20160222194716_add_use_lock_file_to_projects.exs │ ├── 20160528081649_remove_available_versions_from_packages.exs │ ├── 20160528081729_add_project_id_and_releases_to_packages.exs │ ├── 20160611173238_create_package_user_settings.exs │ └── 20160614164904_add_metadata_to_notifications.exs │ ├── seeds.exs │ └── seeds │ ├── packages.exs │ ├── projects.csv │ └── projects.exs ├── script ├── docker.sh ├── reset_dev_setup.sh ├── run_all_elixir_projects.exs ├── send_daily_emails.exs └── send_weekly_emails.exs ├── test ├── channels │ ├── feed_channel_test.exs │ ├── project_channel_test.exs │ └── user_channel_test.exs ├── controllers │ ├── badge_controller_test.exs │ ├── component_controller_test.exs │ ├── git_hub_auth_controller_test.exs │ ├── notification_controller_test.exs │ ├── package_controller_test.exs │ ├── page_controller_test.exs │ ├── project_controller_test.exs │ ├── project_hook_controller_test.exs │ └── user_controller_test.exs ├── hex_faktor │ ├── notification_publisher_test.exs │ ├── project_access_test.exs │ └── version_helper_test.exs ├── models │ ├── app_event_log_test.exs │ ├── notification_test.exs │ ├── package_test.exs │ ├── package_user_settings_test.exs │ ├── project_hook_test.exs │ ├── project_test.exs │ ├── project_user_settings_test.exs │ ├── project_user_test.exs │ └── user_test.exs ├── persistence │ └── project_test.exs ├── refaktor │ ├── builder_test.exs │ ├── hub │ │ ├── docker │ │ │ └── run_test.exs │ │ ├── git_test.exs │ │ ├── hub_test.exs │ │ └── job_test.exs │ ├── job │ │ └── elixir │ │ │ └── deps_test.exs │ ├── persistence │ │ └── build_objects_test.exs │ └── use_case │ │ └── git_repo_test.exs ├── support │ ├── channel_case.ex │ ├── conn_case.ex │ ├── fixtures.ex │ ├── git_hub_api_mock.ex │ ├── git_hub_auth_mock.ex │ ├── model_case.ex │ ├── test_application.exs │ └── test_jobs.exs ├── test_helper.exs └── views │ ├── error_view_test.exs │ ├── layout_view_test.exs │ └── page_view_test.exs └── web ├── channels ├── feed_channel.ex ├── project_channel.ex ├── user_channel.ex └── user_socket.ex ├── controllers ├── badge_controller.ex ├── component_controller.ex ├── email_controller.ex ├── git_hub_auth_controller.ex ├── help_controller.ex ├── notification_controller.ex ├── package_controller.ex ├── package_provider.ex ├── page_controller.ex ├── project_builder.ex ├── project_controller.ex ├── project_hook_controller.ex ├── project_progress_callback.ex ├── project_provider.ex ├── project_syncer.ex └── user_controller.ex ├── gettext.ex ├── models ├── app_event_log.ex ├── notification.ex ├── package.ex ├── package_user_settings.ex ├── project.ex ├── project_hook.ex ├── project_user.ex ├── project_user_settings.ex └── user.ex ├── oauth ├── git_hub_api.ex ├── git_hub_auth.ex └── hex_sonar.ex ├── persistence ├── app_event_log.ex ├── notification.ex ├── package.ex ├── package_user_settings.ex ├── project.ex ├── project_hook.ex ├── project_user.ex ├── project_user_settings.ex └── user.ex ├── plugs ├── assign_current_user_info.ex └── logged_in.ex ├── router.ex ├── static ├── assets │ ├── favicon.ico │ ├── fonts │ │ ├── mfglabsiconset-webfont.eot │ │ ├── mfglabsiconset-webfont.svg │ │ ├── mfglabsiconset-webfont.ttf │ │ └── mfglabsiconset-webfont.woff │ ├── images │ │ ├── badge-explanation.png │ │ ├── badge.svg │ │ ├── deps.svg │ │ ├── logo-email.png │ │ └── logo.png │ └── robots.txt ├── css │ ├── _config.scss │ ├── app.css │ ├── base.forms.scss │ ├── base.layout.scss │ ├── base.typo.scss │ ├── component.alert.scss │ ├── component.btn.scss │ ├── component.bullet.scss │ ├── component.dep.scss │ ├── component.dropdown.scss │ ├── component.form-section.scss │ ├── component.label.scss │ ├── component.legend.scss │ ├── component.motd-banner.scss │ ├── component.notification-item.scss │ ├── component.notification-title.scss │ ├── component.package-list-item.scss │ ├── component.package-title.scss │ ├── component.project-actions.scss │ ├── component.project-list-item.scss │ ├── component.project-title.scss │ ├── component.sync-progress.scss │ ├── component.tabbed_navi.scss │ ├── component.todo.sidebar.scss │ ├── component.user-title.scss │ ├── generic.mfglabs_iconset.css │ ├── generic.snippets.scss │ └── keep.scss └── js │ ├── app.js │ ├── dom_init.js │ ├── hex_faktor.js │ ├── jquery.js │ ├── meta.js │ ├── project_channel.js │ ├── socket.js │ └── user_channel.js ├── templates ├── badge │ ├── all_deps--default.svg.eex │ ├── all_deps--flat-square.svg.eex │ ├── hex--default.svg.eex │ ├── prod_deps--default.svg.eex │ └── prod_deps--flat-square.svg.eex ├── component │ ├── _tab_hamburger.html.eex │ ├── alert.html.eex │ ├── dep.html.eex │ ├── notification-counter.html.eex │ ├── notification-item-package.html.eex │ ├── notification-item-project.html.eex │ ├── package-title.html.eex │ ├── project-list-item.html.eex │ ├── project-title.html.eex │ ├── tutorial-verify-email.html.eex │ └── user-title.html.eex ├── email │ ├── _footer.html.eex │ ├── notifications.html.eex │ ├── status_report.html.eex │ └── validation.html.eex ├── error │ ├── 403.html.eex │ ├── 404.html.eex │ ├── 500.html.eex │ └── 500.svg.eex ├── help │ ├── badge.html.eex │ └── index.html.eex ├── layout │ ├── _footer.html.eex │ ├── _motd-banner.html.eex │ ├── _sidebar.html.eex │ ├── app.html.eex │ ├── blank.html.eex │ └── email.html.eex ├── notification │ ├── _tab-index.html.eex │ ├── _tab_nav_index.html.eex │ └── index.html.eex ├── package │ ├── _notification_settings.html.eex │ ├── _tab-package_filter.html.eex │ ├── _tab_nav_index.html.eex │ ├── _tab_nav_show_projects.html.eex │ ├── _versions.html.eex │ ├── index.html.eex │ └── show.html.eex ├── page │ ├── about.html.eex │ └── index.html.eex ├── project │ ├── _edit.badges.html.eex │ ├── _edit.github_sync.html.eex │ ├── _edit.history.html.eex │ ├── _edit.monitoring.html.eex │ ├── _edit.notifications.html.eex │ ├── _tab-edit.html.eex │ ├── _tab-mix_env.html.eex │ ├── _tab-project_filter.html.eex │ ├── _tab_nav_edit.html.eex │ ├── _tab_nav_index.html.eex │ ├── _tab_nav_show.html.eex │ ├── add_project.html.eex │ ├── builds.html.eex │ ├── edit.html.eex │ ├── index.html.eex │ └── show.html.eex └── user │ ├── _edit.email.html.eex │ ├── _edit.github_sync.html.eex │ ├── _edit.resend_validation.html.eex │ ├── _tab-edit.html.eex │ ├── _tab_nav_edit.html.eex │ ├── edit.html.eex │ ├── resend_verify_email.html.eex │ └── verify_email.html.eex ├── views ├── badge_view.ex ├── component_view.ex ├── email_view.ex ├── error_helpers.ex ├── error_view.ex ├── help_view.ex ├── layout_view.ex ├── notification_view.ex ├── package_view.ex ├── page_view.ex ├── project_hook_view.ex ├── project_view.ex ├── user_view.ex └── view_helpers.ex └── web.ex /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /cover 4 | /db 5 | /deps 6 | /log 7 | /*.ez 8 | /tmp 9 | 10 | # Generate on crash by the VM 11 | erl_crash.dump 12 | 13 | # Static artifacts 14 | /node_modules 15 | 16 | # Since we are building assets from web/static, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # The config/prod.secret.exs file by default contains sensitive 22 | # data and you should not commit it into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets file as long as you replace its contents by environment 26 | # variables. 27 | /config/*.secret.exs 28 | /dockertools/mix/hex_faktor/config/*.secret.exs 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | 1. When reporting an issue, please strive to include as much information as possible: which device, OS and browser as well as their respective versions. If you can, please describe steps to reproduce the issue. 4 | 5 | 2. All interactions in the project follow the same [Code of Conduct as Elixir](https://github.com/elixir-lang/elixir/blob/master/CODE_OF_CONDUCT.md) 6 | 7 | Thanks for reading! 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 René Föhring 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HexFaktor discontinued 2 | 3 | This service is currently unavailable. I will have to find the time to make it GDPR compliant. 4 | 5 | In the meantime you can look at alternatives like [Dependabot for Elixir](https://dependabot.com/elixir.html). 6 | -------------------------------------------------------------------------------- /brunch-config.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // See http://brunch.io/#documentation for docs. 3 | files: { 4 | javascripts: { 5 | joinTo: "js/app.js" 6 | 7 | // To use a separate vendor.js bundle, specify two files path 8 | // https://github.com/brunch/brunch/blob/stable/docs/config.md#files 9 | // joinTo: { 10 | // "js/app.js": /^(web\/static\/js)/, 11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/ 12 | // } 13 | // 14 | // To change the order of concatenation of files, explicitly mention here 15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation 16 | // order: { 17 | // before: [ 18 | // "web/static/vendor/js/jquery-2.1.1.js", 19 | // "web/static/vendor/js/bootstrap.min.js" 20 | // ] 21 | // } 22 | }, 23 | stylesheets: { 24 | joinTo: "css/app.css" 25 | }, 26 | templates: { 27 | joinTo: "js/app.js" 28 | } 29 | }, 30 | 31 | conventions: { 32 | // This option sets where we should place non-css and non-js assets in. 33 | // By default, we set this to "/web/static/assets". Files in this directory 34 | // will be copied to `paths.public`, which is "priv/static" by default. 35 | assets: /^(web\/static\/assets)/ 36 | }, 37 | 38 | // Phoenix paths configuration 39 | paths: { 40 | // Dependencies and current project directories to watch 41 | watched: [ 42 | "web/static", 43 | "test/static" 44 | ], 45 | 46 | // Where to compile files to 47 | public: "priv/static" 48 | }, 49 | 50 | // Configure your plugins 51 | plugins: { 52 | babel: { 53 | // Do not use ES6 compiler in vendor code 54 | ignore: [/web\/static\/vendor/] 55 | } 56 | }, 57 | 58 | modules: { 59 | autoRequire: { 60 | "js/app.js": ["web/static/js/app"], 61 | "js\\app.js": ["web/static/js/app"] 62 | } 63 | }, 64 | 65 | npm: { 66 | enabled: true, 67 | whitelist: ["phoenix", "phoenix_html"] 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | use Mix.Config 7 | 8 | config :hex_faktor, tool_dir: Path.join([System.cwd!, "dockertools"]) 9 | 10 | config :hex_faktor, code_dirname: "code" 11 | config :hex_faktor, eval_dirname: "eval" 12 | config :hex_faktor, progress_callback_converter: HexFaktor.ProjectProgressCallback 13 | 14 | config :hex_faktor, git_hub_auth_module: GitHubAuth 15 | config :hex_faktor, git_hub_api_module: GitHubAPI 16 | 17 | # Configures the endpoint 18 | config :hex_faktor, HexFaktor.Endpoint, 19 | url: [host: "localhost"], 20 | root: Path.dirname(__DIR__), 21 | secret_key_base: "k9cPX7yvi0NGHhGiAqcjGeg6hkZaxuzhU/bMFsVspW/GiPI8kkz5tfdZJOzGpILw", 22 | render_errors: [accepts: ~w(html json)], 23 | pubsub: [name: HexFaktor.PubSub, 24 | adapter: Phoenix.PubSub.PG2] 25 | 26 | # Configures Elixir's Logger 27 | config :logger, :console, 28 | format: "$time $metadata[$level] $message\n", 29 | metadata: [:request_id], 30 | backends: [Rollbax.Logger] 31 | 32 | # We configure the Rollbax.Logger backend. 33 | config :logger, Rollbax.Logger, 34 | level: :error 35 | 36 | # Import environment specific config. This must remain at the bottom 37 | # of this file so it overrides the configuration defined above. 38 | import_config "#{Mix.env}.exs" 39 | 40 | # Configure phoenix generators 41 | config :phoenix, :generators, 42 | migration: true, 43 | binary_id: false 44 | 45 | config :hex_faktor, mailgun_domain: "https://api.mailgun.net/v3/mydomain.com", 46 | mailgun_key: "key-##############" 47 | 48 | config :hex_faktor, :hex_faktor_repo_url, "https://github.com/hexfaktor/hex_faktor_web" 49 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # We don't run a server during test. If one is required, 4 | # you can enable the server option below. 5 | config :hex_faktor, HexFaktor.Endpoint, 6 | http: [port: 4001], 7 | server: false 8 | 9 | # Print only warnings and errors during test 10 | config :logger, level: :warn 11 | 12 | config :hex_faktor, userid_inside_docker: 1000 13 | config :hex_faktor, work_dir: Path.join([System.cwd!, "..", "hex_faktor-workdir"]) 14 | 15 | config :hex_faktor, git_hub_auth_module: GitHubAuthMock 16 | config :hex_faktor, git_hub_api_module: GitHubAPIMock 17 | 18 | # Configure your database 19 | config :hex_faktor, HexFaktor.Repo, 20 | adapter: Ecto.Adapters.Postgres, 21 | username: "postgres", 22 | password: "postgres", 23 | database: "hex_faktor_test", 24 | hostname: "localhost", 25 | pool: Ecto.Adapters.SQL.Sandbox 26 | 27 | config :hex_faktor, :hex_server, "http://test.hex.local" 28 | 29 | config :hex_faktor, :base_url, "http://test.host" 30 | 31 | config :hex_faktor, :salt_user_socket, "salt goes here" 32 | config :hex_faktor, :salt_email_token, "salt goes here" 33 | 34 | config :hex_faktor, :worker_pool_size, 2 35 | config :hex_faktor, :worker_pool_overflow, 1 36 | 37 | config :hex_faktor, :mailgun, 38 | domain: "https://api.mailgun.net/v3/YOURDOMAIN", 39 | key: "something", 40 | mode: :test, 41 | test_file_path: "/tmp/mailgun.json" 42 | 43 | config :rollbax, 44 | access_token: "zrdz", 45 | environment: "test", 46 | enabled: :log 47 | -------------------------------------------------------------------------------- /dockerfiles/faktor-elixir/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM otp-18.0-ex-1.1.0 2 | 3 | 4 | # ADD TOOLS 5 | # ========= 6 | # refaktor/dockertools/bin will be mounted here 7 | ENV PATH /tools/bin:$PATH 8 | 9 | 10 | # ADD faktor USER 11 | # ============== 12 | RUN mkdir -p /home/faktor 13 | RUN groupadd -r faktor -g 1000 && useradd -u 1000 -r -g faktor -d /home/faktor -s /sbin/nologin -c "Docker image user" faktor && chown -R faktor:faktor /home/faktor 14 | 15 | ENV HOME /home/faktor 16 | USER faktor 17 | 18 | RUN mix local.hex --force 19 | RUN mix local.rebar --force 20 | RUN mix hex.info 21 | 22 | 23 | # Goto mounted code directory 24 | # =========================== 25 | WORKDIR /job/code 26 | -------------------------------------------------------------------------------- /dockerfiles/faktor-git/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:0.9.16 2 | 3 | RUN echo /root > /etc/container_environment/HOME 4 | 5 | # Use baseimage-docker's init system. 6 | CMD ["/sbin/my_init"] 7 | 8 | # Set the locale 9 | RUN locale-gen en_US.UTF-8 10 | ENV LANG en_US.UTF-8 11 | ENV LANGUAGE en_US:en 12 | ENV LC_ALL en_US.UTF-8 13 | 14 | WORKDIR /tmp 15 | 16 | # See : https://github.com/phusion/baseimage-docker/issues/58 17 | RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 18 | 19 | # update and install some software requirements 20 | RUN apt-get update && apt-get upgrade -y && apt-get install -y curl wget git make 21 | 22 | WORKDIR / 23 | 24 | 25 | # ADD faktor USER 26 | # ============== 27 | RUN mkdir -p /home/faktor 28 | RUN groupadd -r faktor -g 1000 && useradd -u 1000 -r -g faktor -d /home/faktor -s /sbin/nologin -c "Docker image user" faktor && chown -R faktor:faktor /home/faktor 29 | 30 | #USER faktor 31 | -------------------------------------------------------------------------------- /dockerfiles/faktor-ruby/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.2 2 | 3 | 4 | 5 | RUN gem install rubocop 6 | RUN gem install inch -v "0.7.0" 7 | 8 | 9 | 10 | # ADD TOOLS 11 | # ========= 12 | # refaktor/dockertools/bin will be mounted here 13 | ENV PATH /tools/bin:$PATH 14 | 15 | 16 | # ADD faktor USER 17 | # ============== 18 | RUN mkdir -p /home/faktor 19 | RUN groupadd -r faktor -g 1000 && useradd -u 1000 -r -g faktor -d /home/faktor -s /sbin/nologin -c "Docker image user" faktor && chown -R faktor:faktor /home/faktor 20 | 21 | USER faktor 22 | 23 | 24 | # Goto mounted code directory 25 | # =========================== 26 | WORKDIR /job/code 27 | -------------------------------------------------------------------------------- /dockerfiles/faktor-test-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:0.9.16 2 | 3 | RUN echo /root > /etc/container_environment/HOME 4 | 5 | # Use baseimage-docker's init system. 6 | CMD ["/sbin/my_init"] 7 | 8 | 9 | # ADD TOOLS 10 | # ========= 11 | # refaktor/dockertools/bin will be mounted here 12 | ENV PATH /tools/bin:$PATH 13 | 14 | 15 | # ADD faktor USER 16 | # ============== 17 | RUN mkdir -p /home/faktor 18 | RUN groupadd -r faktor -g 1000 && useradd -u 1000 -r -g faktor -d /home/faktor -s /sbin/nologin -c "Docker image user" faktor && chown -R faktor:faktor /home/faktor 19 | 20 | USER faktor 21 | -------------------------------------------------------------------------------- /dockerfiles/otp-18.0-ex-1.1.0/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:0.9.16 2 | 3 | RUN echo /root > /etc/container_environment/HOME 4 | 5 | # Use baseimage-docker's init system. 6 | CMD ["/sbin/my_init"] 7 | 8 | # Set the locale 9 | RUN locale-gen en_US.UTF-8 10 | ENV LANG en_US.UTF-8 11 | ENV LANGUAGE en_US:en 12 | ENV LC_ALL en_US.UTF-8 13 | 14 | WORKDIR /tmp 15 | 16 | # See : https://github.com/phusion/baseimage-docker/issues/58 17 | RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections 18 | 19 | # update and install some software requirements 20 | RUN apt-get update && apt-get upgrade -y && apt-get install -y curl wget git make 21 | 22 | # download and install Erlang package 23 | RUN wget http://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb \ 24 | && dpkg -i erlang-solutions_1.0_all.deb \ 25 | && apt-get update 26 | 27 | # install erlang from package 28 | RUN apt-get install -y erlang erlang-ssl erlang-inets && rm erlang-solutions_1.0_all.deb 29 | 30 | # update and install some software requirements 31 | RUN apt-get install -y unzip 32 | 33 | # Elixir 34 | WORKDIR /elixir 35 | RUN wget -q https://github.com/elixir-lang/elixir/releases/download/v1.2.6/Precompiled.zip && \ 36 | unzip Precompiled.zip && \ 37 | rm -f Precompiled.zip && \ 38 | ln -s /elixir/bin/elixirc /usr/local/bin/elixirc && \ 39 | ln -s /elixir/bin/elixir /usr/local/bin/elixir && \ 40 | ln -s /elixir/bin/mix /usr/local/bin/mix && \ 41 | ln -s /elixir/bin/iex /usr/local/bin/iex 42 | 43 | # Install local Elixir hex and rebar 44 | RUN /usr/local/bin/mix local.hex --force && \ 45 | /usr/local/bin/mix local.rebar --force 46 | 47 | WORKDIR / 48 | -------------------------------------------------------------------------------- /dockertools/bin/print-container-info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Build system for linux-docker-worker-`hostname` ..." 4 | echo "" 5 | echo "Date and time: `date`" 6 | echo "" 7 | -------------------------------------------------------------------------------- /dockertools/bin/print-elixir-info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Elixir version information" 4 | echo "================================================================================" 5 | echo "Elixir: `elixir --version`" 6 | echo "Mix: `mix --version`" 7 | 8 | mix hex > /tmp/hex_info 9 | echo "Hex: `head -n1 /tmp/hex_info`" 10 | echo "" -------------------------------------------------------------------------------- /dockertools/bin/run-elixir-deps: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | print-container-info 4 | print-elixir-info 5 | 6 | RESULT_FILE=/job/eval/result.json 7 | 8 | if [ ! -f /job/code/mix.lock ]; then 9 | echo "== File mix.lock not found! ====================================================" 10 | echo "" 11 | 12 | cd /job/code 13 | mix deps.get 14 | 15 | echo "" 16 | echo "================================================================================" 17 | echo "" 18 | fi 19 | 20 | cd /tools/mix/hex_faktor 21 | 22 | #mix faktor /job/code --json > $RESULT_FILE 23 | mix run --no-compile -e 'HexFaktor.CLI.main ["/job/code", "--json"]' > $RESULT_FILE 24 | 25 | echo "JSON written (`wc --bytes $RESULT_FILE`)" 26 | echo "" 27 | echo "Deps Analysis complete." 28 | -------------------------------------------------------------------------------- /dockertools/bin/test-error: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | exit 42 4 | -------------------------------------------------------------------------------- /dockertools/bin/test-output: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Test pipe into file" > /job/eval/stdout.log 4 | echo "{\"foo\": 42}" > /job/eval/result 5 | 6 | >&2 echo "Warning on stderr!" 7 | echo "Test complete." 8 | -------------------------------------------------------------------------------- /dockertools/bin/test-produce-bad-result-json: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Test pipe into file" > /job/eval/stdout.log 4 | echo "{\"foo\": 4" > /job/eval/result.json 5 | 6 | >&2 echo "Warning on stderr!" 7 | echo "Test complete." 8 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/README.md: -------------------------------------------------------------------------------- 1 | # HexFaktor 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: 8 | 9 | 1. Add hex_faktor to your list of dependencies in `mix.exs`: 10 | 11 | def deps do 12 | [{:hex_faktor, "~> 0.0.1"}] 13 | end 14 | 15 | 2. Ensure hex_faktor is started before your application: 16 | 17 | def application do 18 | [applications: [:hex_faktor]] 19 | end 20 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :hex_faktor, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:hex_faktor, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | import_config "config.secret.exs" 31 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/lib/hex_faktor.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor do 2 | @version Mix.Project.config[:version] 3 | 4 | def version, do: @version 5 | end 6 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/lib/hex_faktor/mix_exs_loader.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.MixExsLoader do 2 | @def_ops [:def, :defp] 3 | 4 | alias HexFaktor.ExsLoader 5 | 6 | def parse(source) do 7 | case source |> Code.string_to_quoted do 8 | {:ok, ast} -> 9 | traverse(ast, &do_traverse/2) 10 | val -> val 11 | end 12 | end 13 | 14 | defp traverse(source_ast, fun, memo \\ nil) do 15 | {_, accumulated} = Macro.prewalk(source_ast, memo, fun) 16 | accumulated 17 | end 18 | 19 | for op <- @def_ops do 20 | defp do_traverse({unquote(op), _meta, arguments} = ast, memo) do 21 | {ast, memo || handle_function_definition(arguments, memo)} 22 | end 23 | end 24 | defp do_traverse(ast, memo) do 25 | {ast, memo} 26 | end 27 | 28 | defp handle_function_definition(body, memo) do 29 | case Enum.at(body, 0) do 30 | {:deps, _meta, nil} -> parse_body(body) 31 | {:deps, _meta, []} -> parse_body(body) 32 | _ -> 33 | memo 34 | end 35 | end 36 | 37 | defp parse_body(body) do 38 | body 39 | |> do_block_for! 40 | |> ExsLoader.parse(true) 41 | end 42 | 43 | defp do_block_for!(ast) do 44 | case do_block_for(ast) do 45 | {:ok, block} -> block 46 | nil -> nil 47 | end 48 | end 49 | 50 | defp do_block_for({_atom, _meta, arguments}) when is_list(arguments) do 51 | do_block_for(arguments) 52 | end 53 | defp do_block_for([do: block]) do 54 | {:ok, block} 55 | end 56 | defp do_block_for(arguments) when is_list(arguments) do 57 | arguments 58 | |> Enum.find_value(&find_keyword(&1, :do)) 59 | end 60 | defp do_block_for(_) do 61 | nil 62 | end 63 | 64 | defp find_keyword(list, keyword) when is_list(list) do 65 | if Keyword.has_key?(list, keyword) do 66 | {:ok, list[keyword]} 67 | else 68 | nil 69 | end 70 | end 71 | defp find_keyword(_, _), do: nil 72 | end 73 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/lib/hex_faktor/mix_lock_loader.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.MixLockLoader do 2 | alias HexFaktor.ExsLoader 3 | 4 | def parse(source) do 5 | ExsLoader.parse(source, true) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/lib/hex_faktor/service/hex_service.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Service.HexService do 2 | alias HexFaktor.Util.JSON 3 | 4 | @servers Application.get_env(:hex_faktor, :hex_servers) 5 | @url ":server/api/packages/:name" 6 | 7 | def available_versions(name) do 8 | case load_json(name) do 9 | {:error, _error} -> 10 | [] 11 | list -> 12 | list 13 | |> Enum.map(fn(%{"version" => version}) -> version end) 14 | end 15 | end 16 | 17 | defp load_json(name) do 18 | case load(name, @servers) do 19 | {:ok, map} -> map["releases"] 20 | {:error, error} -> 21 | IO.puts :stderr, inspect({:error, error}) 22 | {:error, error} 23 | end 24 | end 25 | 26 | defp load(name, list, failed_servers \\ []) 27 | 28 | defp load(_name, [], _) do 29 | {:error, "All servers failed."} 30 | end 31 | defp load(name, [server|tail], failed_servers) do 32 | url = 33 | @url 34 | |> String.replace(":server", server |> to_string) 35 | |> String.replace(":name", name |> to_string) 36 | 37 | #IO.puts "Getting url: #{url}" 38 | 39 | result = 40 | case HTTPoison.get(url) do 41 | {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> 42 | {:ok, JSON.parse(body)} 43 | {:ok, %HTTPoison.Response{status_code: 404}} -> 44 | {:error, 404} 45 | {:ok, %HTTPoison.Response{status_code: 500}} -> 46 | {:error, 500} 47 | {:error, %HTTPoison.Error{reason: reason}} -> 48 | {:error, reason} 49 | end 50 | 51 | case result do 52 | {:ok, value} -> 53 | {:ok, value} 54 | {:error, _} -> 55 | :timer.sleep 1000 56 | load(name, tail, failed_servers ++ [server]) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/lib/hex_faktor/util/json.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Util.JSON do 2 | def encode(struct) do 3 | Poison.encode!(struct) 4 | end 5 | 6 | def parse(string) do 7 | case Poison.decode(string) do 8 | {:ok, map} -> map 9 | _ -> :parser_error 10 | end 11 | end 12 | 13 | def parse!(string) do 14 | Poison.decode!(string) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/lib/mix/tasks/faktor.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Faktor do 2 | use Mix.Task 3 | 4 | alias HexFaktor.CLI 5 | 6 | @shortdoc "Run deps analysis" 7 | @moduledoc @shortdoc 8 | 9 | def run(args) do 10 | CLI.main(args) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktorCLI.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hex_faktor, 7 | version: "0.1.0", 8 | elixir: "~> 1.1", 9 | escript: [main_module: HexFaktor.CLI], 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application 17 | # 18 | # Type "mix help compile.app" for more information 19 | def application do 20 | [applications: [:logger, :httpoison]] 21 | end 22 | 23 | # Dependencies can be Hex packages: 24 | # 25 | # {:mydep, "~> 0.3.0"} 26 | # 27 | # Or git/path repositories: 28 | # 29 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 30 | # 31 | # Type "mix help deps" for more examples and options 32 | defp deps do 33 | [ 34 | {:poison, "~> 1.4"}, 35 | {:httpoison, "~> 0.8.0"}, 36 | ] 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.3.0"}, 2 | "hackney": {:hex, :hackney, "1.4.6"}, 3 | "httpoison": {:hex, :httpoison, "0.8.0"}, 4 | "idna": {:hex, :idna, "1.0.2"}, 5 | "mimerl": {:hex, :mimerl, "1.0.0"}, 6 | "poison": {:hex, :poison, "1.5.0"}, 7 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}} 8 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /tools/mix/hex_faktor 4 | rm -fr _build/ 5 | mix compile --force 6 | -------------------------------------------------------------------------------- /dockertools/mix/hex_faktor/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | -------------------------------------------------------------------------------- /lib/hex_faktor.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor do 2 | use Application 3 | 4 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 5 | # for more information on OTP Applications 6 | def start(_type, _args) do 7 | import Supervisor.Spec, warn: false 8 | 9 | children = [ 10 | # Start the endpoint when the application starts 11 | supervisor(HexFaktor.Endpoint, []), 12 | # Start the Ecto repository 13 | supervisor(HexFaktor.Repo, []), 14 | # Here you could define other workers and supervisors as children 15 | # worker(HexFaktor.Worker, [arg1, arg2, arg3]), 16 | ] 17 | 18 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 19 | # for other strategies and supported options 20 | opts = [strategy: :one_for_one, name: HexFaktor.Supervisor] 21 | Supervisor.start_link(children, opts) 22 | end 23 | 24 | # Tell Phoenix to update the endpoint configuration 25 | # whenever the application is updated. 26 | def config_change(changed, _new, removed) do 27 | HexFaktor.Endpoint.config_change(changed, removed) 28 | :ok 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/hex_faktor/app_event.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.AppEvent do 2 | alias HexFaktor.Persistence.AppEventLog 3 | 4 | def log(:hex_package_update, name, projects) do 5 | AppEventLog.create(nil, "package.update_hex", %{ 6 | "name" => name, 7 | "source" => "hex", 8 | "dependent_project_ids" => projects |> Enum.map(&(&1.id)) 9 | }) 10 | end 11 | def log(:rebuild_via_hook, provider, project, branch_name) do 12 | AppEventLog.create(nil, "project.rebuild_via_hook", %{ 13 | "provider" => provider, 14 | "project_id" => project.id, 15 | "branch" => branch_name 16 | }) 17 | end 18 | def log(:sign_up, user) do 19 | AppEventLog.create(user, "user.sign_up", %{}) 20 | end 21 | def log(:sign_in, user) do 22 | AppEventLog.create(user, "user.sign_in", %{}) 23 | end 24 | def log(:sign_out, user) do 25 | AppEventLog.create(user, "user.sign_out", %{}) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/hex_faktor/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Auth do 2 | @moduledoc """ 3 | The Auth module holds functions which handle authorization. 4 | """ 5 | 6 | @admin_user_ids Application.get_env(:hex_faktor, :admin_user_ids, []) 7 | 8 | @doc "Returns true if the logged in user is an admin." 9 | def admin?(conn) do 10 | case current_user(conn) do 11 | nil -> false 12 | user -> Enum.member?(@admin_user_ids, user.id) 13 | end 14 | end 15 | 16 | @doc "Returns the GitHub access token for the current user." 17 | def access_token(conn), do: conn.assigns[:current_oauth_token] 18 | 19 | @doc "Returns the current user." 20 | def current_user(conn), do: conn.assigns[:current_user] 21 | 22 | @doc "Returns true if a user is logged in." 23 | def logged_in?(conn), do: !is_nil current_user(conn) 24 | end 25 | -------------------------------------------------------------------------------- /lib/hex_faktor/broadcast.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Broadcast do 2 | @moduledoc """ 3 | The Broadcast module is responsible for broadcasting messages to connected 4 | clients on the right channels. 5 | """ 6 | 7 | alias HexFaktor.User 8 | 9 | def call(%User{id: id}, message, payload) do 10 | broadcast!("users:#{id}", message, payload) 11 | end 12 | 13 | @doc """ 14 | Broadcasts a given `message` to the user with the given `user_id`. 15 | """ 16 | def to_user(user_id, message, payload) do 17 | broadcast!("users:#{user_id}", message, payload) 18 | end 19 | 20 | @doc """ 21 | Broadcasts a given `message` to everybody watching the project with 22 | the given `project_id`. 23 | """ 24 | def to_project(project_id, message, payload) do 25 | broadcast!("projects:#{project_id}", message, payload) 26 | end 27 | 28 | defp broadcast!(room, message, payload) do 29 | HexFaktor.Endpoint.broadcast!(room, message, payload) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/hex_faktor/deps_object_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.DepsObjectFilter do 2 | @doc "Returns all outdated deps out of a given list." 3 | def filter_outdated(deps, nil), do: filter_outdated(deps, false) 4 | def filter_outdated(deps, true) do 5 | reject_severities(deps, ["none"]) 6 | end 7 | def filter_outdated(deps, false) do 8 | reject_severities(deps, ["none", "new_release_within_req"]) 9 | end 10 | 11 | defp reject_severities(deps, rejected_severities) do 12 | deps 13 | |> Enum.filter(&(&1.toplevel && !(&1.severity in rejected_severities))) 14 | end 15 | 16 | 17 | def filter_unknown(deps) do 18 | deps 19 | |> Enum.filter(&unknown_dep?/1) 20 | end 21 | 22 | # unknown in this context means "unknown severity status" 23 | # TODO: maybe we should have :unknown as actual severity value? 24 | defp unknown_dep?(dep) do 25 | dep.available_versions |> Enum.empty? 26 | end 27 | 28 | def current_mix_env?([], "prod"), do: true 29 | def current_mix_env?([], _current), do: false 30 | def current_mix_env?(list, current) when is_list(list) do 31 | list |> Enum.any?(¤t_mix_env?(&1, current)) 32 | end 33 | def current_mix_env?(a, a), do: true 34 | def current_mix_env?(_env, nil), do: true 35 | def current_mix_env?(nil, _current), do: false 36 | def current_mix_env?(_env, _current), do: false 37 | end 38 | -------------------------------------------------------------------------------- /lib/hex_faktor/email_verifier.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.EmailVerifier do 2 | @email_token_salt Application.get_env(:hex_faktor, :salt_user_socket) 3 | 4 | @doc "Modifies a params Map for db insert/update." 5 | def update_email_params(conn, user, params) do 6 | params 7 | |> Map.put("email_token", new_email_token(conn, user)) 8 | |> Map.put("email_verified_at", nil) 9 | end 10 | 11 | defp new_email_token(conn, user) do 12 | conn 13 | |> Phoenix.Token.sign(@email_token_salt, user.id) 14 | |> String.replace(~r/[^a-zA-Z0-9]/, "") 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/hex_faktor/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hex_faktor 3 | 4 | socket "/socket", HexFaktor.UserSocket 5 | 6 | # Serve at "/" the static files from "priv/static" directory. 7 | # 8 | # You should set gzip to true if you are running phoenix.digest 9 | # when deploying your static files in production. 10 | plug Plug.Static, 11 | at: "/", from: :hex_faktor, gzip: false, 12 | only: ~w(css fonts images js favicon.ico robots.txt) 13 | 14 | # Code reloading can be explicitly enabled under the 15 | # :code_reloader configuration of your endpoint. 16 | if code_reloading? do 17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 18 | plug Phoenix.LiveReloader 19 | plug Phoenix.CodeReloader 20 | end 21 | 22 | plug Plug.RequestId 23 | plug Plug.Logger 24 | 25 | plug Plug.Parsers, 26 | parsers: [:urlencoded, :multipart, :json], 27 | pass: ["*/*"], 28 | json_decoder: Poison 29 | 30 | plug Plug.MethodOverride 31 | plug Plug.Head 32 | 33 | plug Plug.Session, 34 | store: :cookie, 35 | key: "_hex_faktor_key", 36 | signing_salt: "ddunT9U0" 37 | 38 | plug HexFaktor.Router 39 | end 40 | -------------------------------------------------------------------------------- /lib/hex_faktor/notification_resolver.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.NotificationResolver do 2 | @moduledoc """ 3 | NotificationResolver is responsible for marking no-longer-valid notifications 4 | as resolved. 5 | """ 6 | 7 | alias HexFaktor.Persistence.Notification 8 | 9 | def handle_existing_notifications(deps_objects, job_id, git_branch_id) do 10 | git_branch_id 11 | |> Notification.all_unseen_for_branch([:deps_object]) 12 | |> handle_notifications(deps_objects, job_id) 13 | end 14 | 15 | def handle_notifications(notifications, deps_objects, job_id) do 16 | notifications 17 | |> Enum.filter(&resolve_notification?(&1, deps_objects)) 18 | |> Enum.map(&(&1.id)) 19 | |> Notification.mark_as_resolved_by_build_job!(job_id) 20 | end 21 | 22 | # Returns true if the given `notification` is no longer valid because 23 | # it concerned itself with one of the now up-to-date `deps_objects` 24 | defp resolve_notification?(notification, deps_objects) do 25 | dep = notification.deps_object 26 | deps_objects 27 | |> Enum.any?(&(&1.id != dep.id && &1.name == dep.name && &1.source == dep.source)) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/hex_faktor/project_access.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectAccess do 2 | alias HexFaktor.Persistence.ProjectUser 3 | alias HexFaktor.Persistence.ProjectUserSettings 4 | 5 | def granted?(_project_id, nil) do # not logged in 6 | true 7 | end 8 | def granted?(project_id, user) when is_binary(project_id) do # not logged in 9 | {project_id, ""} = project_id |> Integer.parse 10 | granted?(project_id, user) 11 | end 12 | def granted?(project_id, user) do 13 | project_user = ProjectUser.find(project_id, user.id) 14 | !is_nil(project_user) 15 | end 16 | 17 | def grant_exclusively(projects, user) do 18 | current_project_ids = currently_granted(user) 19 | new_project_ids = projects |> Enum.map(&(&1.id)) 20 | 21 | current_project_ids 22 | |> Enum.each(fn(current_project_id) -> 23 | unless new_project_ids |> Enum.member?(current_project_id) do 24 | ProjectUser.remove(current_project_id, user.id) 25 | end 26 | end) 27 | 28 | projects 29 | |> Enum.each(&grant(&1, user)) 30 | end 31 | 32 | def grant(project, user) do 33 | ProjectUser.ensure(project.id, user.id) 34 | ProjectUserSettings.ensure(project.id, user.id, project.default_branch) 35 | end 36 | 37 | defp currently_granted(user) do 38 | ProjectUser.find_by_user_id(user.id) 39 | |> Enum.map(&(&1.project_id)) 40 | end 41 | 42 | def settings(project_id, user_id) when is_integer(project_id) and is_integer(user_id) do 43 | ProjectUserSettings.find(project_id, user_id) 44 | end 45 | def settings(project, user) do 46 | settings(project.id, user.id) 47 | end 48 | 49 | def update_settings(project, user, params) do 50 | attributes = %{ 51 | notification_branches: params["notification_branches"], 52 | email_enabled: params["email_enabled"] == "true" 53 | } 54 | project_user_settings = 55 | ProjectUserSettings.ensure(project.id, user.id, project.default_branch) 56 | ProjectUserSettings.update_attributes(project_user_settings, attributes) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/hex_faktor/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo do 2 | use Ecto.Repo, otp_app: :hex_faktor 3 | end 4 | -------------------------------------------------------------------------------- /lib/hex_faktor/sort_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.SortHelper do 2 | def ensure_order(list, list_start, list_end \\ []) do 3 | list 4 | |> to_start(list_start) 5 | |> to_end(list_end) 6 | end 7 | 8 | def to_start(list, list_start) do 9 | list_start = list_start |> Enum.filter(&(Enum.member?(list, &1))) 10 | list_start ++ (list -- list_start) 11 | end 12 | 13 | def to_end(list, list_end) do 14 | list_end = list_end |> Enum.filter(&(Enum.member?(list, &1))) 15 | (list -- list_end) ++ list_end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/hex_faktor/version_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.VersionHelper do 2 | @doc """ 3 | Returns the kind of update for the newest release. 4 | """ 5 | def kind_of_release(%{releases: releases}) do 6 | releases 7 | |> newest_version 8 | |> kind_of_release 9 | end 10 | def kind_of_release(latest_version) do 11 | case Version.parse(latest_version) do 12 | {:ok, v} -> 13 | kind_of_release(v.major, v.minor, v.patch, v.pre) 14 | _ -> 15 | nil 16 | end 17 | end 18 | def kind_of_release(_, 0, 0, []), do: :major 19 | def kind_of_release(_, _, 0, []), do: :minor 20 | def kind_of_release(_, _, _, []), do: :patch 21 | def kind_of_release(_, _, _, _), do: :pre 22 | 23 | @doc """ 24 | Returns the version number of the latest release (in terms of `updated_at`). 25 | """ 26 | def newest_version(%{releases: releases}) do 27 | newest_version(releases) 28 | end 29 | def newest_version(releases) when is_list(releases) do 30 | hash = 31 | releases 32 | |> Enum.sort_by(&(&1["updated_at"])) 33 | |> List.last 34 | hash["version"] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mix/tasks/docker/build.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Docker.Build do 2 | use Mix.Task 3 | 4 | @shortdoc "Build all Docker images" 5 | @moduledoc @shortdoc 6 | 7 | @dockerfile_dir "dockerfiles" 8 | 9 | def run(argv \\ nil) do 10 | case argv do 11 | [] -> all_image_names 12 | list -> list 13 | end 14 | |> build_images 15 | end 16 | 17 | defp all_image_names do 18 | @dockerfile_dir 19 | |> Path.join("*") 20 | |> Path.wildcard 21 | |> Enum.filter(&File.dir?/1) 22 | |> Enum.map(&Path.basename/1) 23 | end 24 | 25 | defp build_images([]), do: nil 26 | defp build_images([name|tail]) do 27 | IO.puts "Building #{name} ..." 28 | build_image(name) 29 | build_images(tail) 30 | end 31 | 32 | defp build_image(name) do 33 | Path.join(@dockerfile_dir, name) 34 | |> Refaktor.Docker.build(["-t", name]) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mix/tasks/faktor/get.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Faktor.Get do 2 | use Mix.Task 3 | 4 | @shortdoc "Get a git repo and run a docker image on it" 5 | @moduledoc @shortdoc 6 | 7 | @dockerfile_dir "dockerfiles" 8 | 9 | alias Refaktor.Builder 10 | 11 | def run(argv) when is_list(argv) do 12 | Refaktor.start 13 | 14 | count = argv |> List.wrap |> Enum.count 15 | cond do 16 | count == 1 -> run(argv |> List.first) 17 | true -> print_usage 18 | end 19 | end 20 | 21 | def run(repo_url, _image_to_run \\ "faktor-elixir") when is_binary(repo_url) do 22 | branch_name = "master" 23 | 24 | Builder.add_and_run_repo(repo_url, branch_name, jobs: []) 25 | end 26 | 27 | defp print_usage do 28 | """ 29 | Usage: mix faktor.get [IMAGE-TO-RUN] 30 | 31 | GIT-URL - local or remote Git repository that will be cloned 32 | IMAGE-TO-RUN - defaults to `faktor-elixir` 33 | """ 34 | |> IO.puts 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/mix/tasks/faktor/top.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Faktor.Top do 2 | use Mix.Task 3 | 4 | @shortdoc "Shows currently running and recent builds" 5 | @moduledoc @shortdoc 6 | 7 | @dockerfile_dir "dockerfiles" 8 | 9 | alias Refaktor.Builder.Model.Build 10 | 11 | def run(argv) when is_list(argv) do 12 | Refaktor.start 13 | 14 | Refaktor.Persistence.Build.last 15 | |> Enum.each(&print_build/1) 16 | end 17 | 18 | defp print_build(%Build{id: build_id, git_branch: git_branch, git_repo: git_repo}) do 19 | [build_id, git_repo.uid, git_branch.name] 20 | |> Enum.join(" | ") 21 | |> IO.puts 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /lib/refaktor/builder/models/build.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Builder.Model.Build do 2 | use Ecto.Model 3 | 4 | alias Refaktor.Builder.Model.GitRepo 5 | alias Refaktor.Builder.Model.GitBranch 6 | alias Refaktor.Builder.Model.BuildJob 7 | 8 | schema "builds" do 9 | field :nr, :integer 10 | field :trigger, :string 11 | 12 | timestamps 13 | 14 | belongs_to :git_repo, GitRepo 15 | belongs_to :git_branch, GitBranch 16 | has_many :build_jobs, BuildJob, on_delete: :delete_all 17 | end 18 | 19 | @required_fields ~w(nr git_repo_id) 20 | @optional_fields ~w(trigger git_branch_id) 21 | 22 | def changeset(object, params \\ :empty) do 23 | object 24 | |> cast(params, @required_fields, @optional_fields) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/refaktor/builder/models/build_job.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Builder.Model.BuildJob do 2 | use Ecto.Model 3 | 4 | alias Refaktor.Builder.Model.Build 5 | alias Refaktor.Builder.Model.GitBranch 6 | alias Refaktor.Builder.Model.GitRevision 7 | 8 | schema "build_jobs" do 9 | field :nr, :integer 10 | field :language, :string 11 | field :intent, :string 12 | field :stderr, :string 13 | field :stdout, :string 14 | 15 | field :status, :string # scheduled, running, success, failure 16 | field :debug_info, :string # json-encoded debug log 17 | field :logs, :string # output from `docker logs` 18 | field :started_at, Ecto.DateTime 19 | field :finished_at, Ecto.DateTime 20 | 21 | timestamps 22 | 23 | belongs_to :build, Build 24 | belongs_to :git_branch, GitBranch 25 | belongs_to :git_revision, GitRevision 26 | end 27 | 28 | @required_fields ~w(nr language intent build_id git_branch_id) 29 | @optional_fields ~w(started_at finished_at logs stderr stdout git_revision_id status debug_info) 30 | 31 | def changeset(object, params \\ :empty) do 32 | object 33 | |> cast(params, @required_fields, @optional_fields) 34 | |> unique_constraint(:nr, name: :build_jobs_unique_index) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/refaktor/builder/models/git_branch.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Builder.Model.GitBranch do 2 | use Ecto.Model 3 | 4 | alias Refaktor.Builder.Model.GitRepo 5 | alias Refaktor.Builder.Model.GitRevision 6 | 7 | schema "git_branches" do 8 | field :name, :string 9 | 10 | timestamps 11 | 12 | belongs_to :latest_git_revision, GitRevision 13 | belongs_to :git_repo, GitRepo 14 | end 15 | 16 | @required_fields ~w(git_repo_id name) 17 | @optional_fields ~w(latest_git_revision_id) 18 | 19 | def changeset(object, params \\ :empty) do 20 | object 21 | |> cast(params, @required_fields, @optional_fields) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/refaktor/builder/models/git_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Builder.Model.GitRepo do 2 | use Ecto.Model 3 | 4 | alias Refaktor.Builder.Model.GitBranch 5 | 6 | schema "git_repos" do 7 | field :uid, :string 8 | field :url, :string 9 | 10 | timestamps 11 | 12 | belongs_to :default_git_branch, GitBranch 13 | has_many :branches, GitBranch, on_delete: :delete_all 14 | end 15 | 16 | @required_fields ~w(uid url) 17 | @optional_fields ~w(default_git_branch_id) 18 | 19 | def changeset(object, params \\ :empty) do 20 | object 21 | |> cast(params, @required_fields, @optional_fields) 22 | |> unique_constraint(:uid) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/refaktor/builder/models/git_revision.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Builder.Model.GitRevision do 2 | use Ecto.Model 3 | 4 | alias Refaktor.Builder.Model.GitRepo 5 | 6 | schema "git_revisions" do 7 | field :sha1, :string 8 | 9 | timestamps 10 | 11 | belongs_to :git_branch, GitBranch 12 | belongs_to :git_repo, GitRepo 13 | end 14 | 15 | @required_fields ~w(git_repo_id git_branch_id sha1) 16 | @optional_fields ~w() 17 | 18 | def changeset(object, params \\ :empty) do 19 | object 20 | |> cast(params, @required_fields, @optional_fields) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/refaktor/builder/persistence/build.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Persistence.Build do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias Refaktor.Builder.Model.Build 6 | alias Refaktor.Builder.Model.BuildJob 7 | 8 | def add(git_repo_id, git_branch_id, trigger \\ nil) do 9 | %Build{} 10 | |> Build.changeset(%{ 11 | nr: git_repo_id |> next_build_nr, 12 | git_repo_id: git_repo_id, 13 | git_branch_id: git_branch_id, 14 | trigger: trigger 15 | }) 16 | |> Repo.insert! 17 | end 18 | 19 | def add_job(build_id, git_branch_id, language, intent) do 20 | %BuildJob{} 21 | |> BuildJob.changeset(%{ 22 | build_id: build_id, 23 | git_branch_id: git_branch_id, 24 | nr: build_id |> next_job_nr, 25 | language: language, 26 | intent: intent, 27 | }) 28 | |> Repo.insert! 29 | end 30 | 31 | def all(git_repo_id) do 32 | Repo.all(from r in Build, 33 | where: r.git_repo_id == ^git_repo_id, 34 | order_by: [desc: :id], 35 | preload: [build_jobs: :git_revision]) 36 | end 37 | 38 | def find_by_id(id) do 39 | Repo.one(from r in Build, where: r.id == ^id) 40 | end 41 | 42 | def last(count \\ 5) do 43 | Repo.all(from r in Build, 44 | order_by: [desc: :id], 45 | limit: ^count, 46 | preload: [:git_repo, :git_branch]) 47 | end 48 | 49 | defp next_build_nr(git_repo_id) do 50 | Repo.one(from r in Build, 51 | where: r.git_repo_id == ^git_repo_id, 52 | order_by: [desc: :nr], 53 | limit: 1, 54 | select: r.nr) 55 | |> next_nr 56 | end 57 | 58 | defp next_job_nr(build_id) do 59 | Repo.one(from r in BuildJob, 60 | where: r.build_id == ^build_id, 61 | order_by: [desc: :nr], 62 | limit: 1, 63 | select: r.nr) 64 | |> next_nr 65 | end 66 | 67 | defp next_nr(nil), do: 1 68 | defp next_nr(nr), do: nr + 1 69 | end 70 | -------------------------------------------------------------------------------- /lib/refaktor/builder/persistence/git_branch.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Persistence.GitBranch do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias Refaktor.Builder.Model.GitBranch 6 | alias Refaktor.Builder.Model.Build 7 | 8 | def add(repo_id, name) do 9 | %GitBranch{} 10 | |> GitBranch.changeset(%{git_repo_id: repo_id, name: name}) 11 | |> Repo.insert! 12 | end 13 | 14 | def find_by_id(id, preload_list \\ []) do 15 | query = from r in GitBranch, 16 | where: r.id == ^id, 17 | select: r, 18 | preload: ^preload_list 19 | Repo.one(query) 20 | end 21 | 22 | def by_name(repo_id, name) do 23 | Repo.one(from r in GitBranch, 24 | where: r.git_repo_id == ^repo_id and r.name == ^name, 25 | limit: 1) # TODO: remove me. I was added because there were two branches named "master" on the same project o_O 26 | end 27 | 28 | def latest_build(branch) do 29 | query = from r in Build, where: r.git_branch_id == ^branch.id, 30 | order_by: [desc: r.id], 31 | preload: [:build_jobs], 32 | limit: 1 33 | Repo.one(query) 34 | end 35 | 36 | def latest_builds(branch_ids) when is_list(branch_ids) do 37 | query = from r in Build, where: r.git_branch_id in ^branch_ids, 38 | order_by: [desc: r.id], 39 | preload: [:build_jobs], 40 | distinct: r.git_branch_id 41 | Repo.all(query) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/refaktor/builder/persistence/git_repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Persistence.GitRepo do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias Refaktor.Builder.Model.GitRepo 6 | 7 | def add(url, uid) do 8 | %GitRepo{} 9 | |> GitRepo.changeset(%{url: url, uid: uid}) 10 | |> Repo.insert! 11 | end 12 | 13 | def by_url(url) do 14 | Repo.one(from r in GitRepo, where: r.url == ^url) 15 | end 16 | 17 | def by_uid(uid) do 18 | Repo.one(from r in GitRepo, where: r.uid == ^uid) 19 | end 20 | 21 | def find_by_id(id) do 22 | Repo.one(from r in GitRepo, where: r.id == ^id) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/refaktor/builder/persistence/git_revision.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Persistence.GitRevision do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias Refaktor.Builder.Model.GitRevision 6 | 7 | def ensure(git_repo, git_branch, sha1) do 8 | case find(git_branch, sha1) do 9 | nil -> add(git_repo, git_branch, sha1) 10 | val -> val 11 | end 12 | end 13 | 14 | def find(git_branch, sha1) do 15 | query = from r in GitRevision, 16 | where: r.git_branch_id == ^git_branch.id and r.sha1 == ^sha1, 17 | select: r 18 | Repo.one(query) 19 | end 20 | 21 | defp add(git_repo, git_branch, sha1) do 22 | attributes = %{ 23 | "git_repo_id" => git_repo.id, 24 | "git_branch_id" => git_branch.id, 25 | "sha1" => sha1, 26 | } 27 | %GitRevision{} 28 | |> GitRevision.changeset(attributes) 29 | |> Repo.insert! 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/refaktor/builder/persistence/job_objects.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Persistence.Job do 2 | alias HexFaktor.Repo 3 | alias Refaktor.Builder.Model.BuildJob 4 | 5 | def by_id(id) do 6 | Repo.get(BuildJob, id) 7 | end 8 | 9 | def update_info(job_id, {:ok, _result}) do 10 | by_id(job_id) 11 | end 12 | def update_info(job_id, {:error, _error}) do 13 | by_id(job_id) 14 | end 15 | def update_info(job_id, {:timeout, _result}) do 16 | by_id(job_id) 17 | end 18 | def update_info(job_id, _val) do 19 | by_id(job_id) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/refaktor/docker.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Docker do 2 | alias Refaktor.Docker.Build 3 | alias Refaktor.Docker.Run 4 | 5 | def build(path, opts \\ []), do: Build.call(path, opts) 6 | 7 | def run(image, cmd, docker_run_opts \\ [], execution_opts \\ []) do 8 | Run.call(image, cmd, docker_run_opts, execution_opts) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/refaktor/docker/build.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Docker.Build do 2 | def call(image, opts \\ []) do 3 | case run(image, opts) do 4 | {output, 0} -> {:ok, output} 5 | {output, x} -> {:error, output, x} 6 | end 7 | end 8 | 9 | defp run(image, opts) do 10 | System.cmd("docker", List.flatten(["build", opts, image])) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/refaktor/docker/image.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Docker.Image do 2 | def exists?(image, opts \\ []) do 3 | case run(image, opts) do 4 | {output, 0} -> {:ok, output} 5 | {output, x} -> {:error, output, x} 6 | end 7 | end 8 | 9 | defp run(image, opts) do 10 | System.cmd("docker", List.flatten(["build", opts, image])) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/refaktor/job/elixir/deps/models/deps_object.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Job.Elixir.Deps.Model.DepsObject do 2 | use Ecto.Model 3 | 4 | schema "deps_objects" do 5 | belongs_to :build_job, BuildJob 6 | belongs_to :project, HexFaktor.Project 7 | belongs_to :git_branch, GitBranch 8 | belongs_to :git_repo, GitRepo 9 | 10 | field :language, :string, size: 10 11 | field :name, :string 12 | field :source, :string 13 | field :source_url, :string 14 | field :toplevel, :boolean 15 | field :locked_version, :string 16 | field :required_version, :string 17 | field :available_versions, {:array, :string} 18 | field :mix_envs, {:array, :string} 19 | field :severity, :string 20 | 21 | timestamps 22 | end 23 | 24 | @required_fields ~w(build_job_id language name source toplevel) 25 | @optional_fields ~w(project_id git_branch_id git_repo_id source_url locked_version required_version available_versions mix_envs severity) 26 | 27 | def changeset(object, params \\ :empty) do 28 | object 29 | |> cast(params, @required_fields, @optional_fields) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/refaktor/use_case/git.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.UseCase.Git do 2 | alias Refaktor.Persistence.GitRepo 3 | alias Refaktor.Persistence.GitBranch 4 | 5 | @doc """ 6 | Returns the GitRepo with the given +url+ (creates it if it doesn't exist). 7 | """ 8 | def get_repo(url) do 9 | case GitRepo.by_url(url) do 10 | nil -> GitRepo.add(url, url |> to_uid) 11 | val -> val 12 | end 13 | end 14 | 15 | def get_repo_and_branch(url, branch_name) do 16 | repo = get_repo(url) 17 | {repo, get_branch(repo.id, branch_name)} 18 | end 19 | 20 | def get_branch(repo_id, branch_name) do 21 | case GitBranch.by_name(repo_id, branch_name) do 22 | nil -> GitBranch.add(repo_id, branch_name) 23 | val -> val 24 | end 25 | end 26 | 27 | # this needs work! 28 | # we want to ensure that we identify two repos retrieved via different 29 | # mechanisms 30 | # 31 | # e.g. these should result in the same uid: 32 | # 33 | # "https://github.com/inch-ci/Hello-World-Elixir.git" 34 | # "git@github.com:inch-ci/Hello-World-Elixir.git" 35 | # 36 | def to_uid(repo_url) do 37 | uid = 38 | repo_url 39 | |> String.strip 40 | |> String.replace(~r/^([a-z]+\:\/\/)/, "") 41 | |> String.replace(~r/\.git$/, "") 42 | 43 | if String.contains?(uid, "git@") do 44 | uid 45 | |> String.replace(~r/^git@/, "") 46 | |> String.replace(~r/\:/, "/") 47 | else 48 | uid 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/refaktor/util/json.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Util.JSON do 2 | def encode(struct) do 3 | Poison.encode!(struct) 4 | end 5 | 6 | def parse(string) do 7 | case Poison.decode(string) do 8 | {:ok, map} -> map 9 | _ -> {:error, :json_parser, string} 10 | end 11 | end 12 | 13 | def parse!(string) do 14 | Poison.decode!(string) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/refaktor/worker/job_runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Worker.JobRunner do 2 | alias HexFaktor.Persistence.Project 3 | alias Refaktor.Job 4 | alias Refaktor.Persistence.BuildJob 5 | 6 | def run_job(job_id, job_dir, job, meta) do 7 | update_job(job_id, "running", [], :started_at) 8 | 9 | if meta["project_id"] do 10 | Project.update_latest_build_job(meta["project_id"], job_id) 11 | end 12 | 13 | try do 14 | Job.run(job_id, job_dir, job, run_opts(Mix.env, job_id), meta) 15 | rescue 16 | value -> {:error, :rescue, Exception.format(:error, value)} 17 | end 18 | |> case do 19 | {:ok, result} -> 20 | update_job(job_id, "success", {:ok, result}, :finished_at) 21 | {:ok, result} 22 | value -> 23 | update_job(job_id, "failure", value, :finished_at) 24 | value 25 | end 26 | end 27 | 28 | defp update_job(job_id, status, debug_info, timestamp_field) do 29 | BuildJob.update_status(job_id, status, debug_info, docker_logs(status, job_id)) 30 | if timestamp_field do 31 | BuildJob.update_timestamp(job_id, timestamp_field) 32 | end 33 | end 34 | 35 | defp docker_logs("running", _), do: nil 36 | defp docker_logs(_status, job_id) do 37 | {docker_logs, _} = System.cmd("sudo", ["docker", "logs", container_name(job_id)], stderr_to_stdout: true) 38 | docker_logs 39 | end 40 | 41 | defp run_opts(:test, _), do: [] 42 | defp run_opts(_, job_id), do: [container_name: container_name(job_id)] 43 | 44 | defp container_name(job_id), do: "hf-job-#{job_id}" 45 | end 46 | -------------------------------------------------------------------------------- /lib/refaktor/worker/progress_callback.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Worker.ProgressCallback do 2 | @callback_converter Application.get_env(:hex_faktor, :progress_callback_converter, __MODULE__.NoopCallback) 3 | 4 | def cast(progress_callback_data) do 5 | @callback_converter.cast(progress_callback_data) 6 | end 7 | 8 | defmodule NoopCallback do 9 | def cast(progress_callback_data) do 10 | fn(status) -> 11 | IO.inspect {status, progress_callback_data} 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/refaktor/worker/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Worker.Supervisor do 2 | alias Refaktor.Persistence.Build 3 | alias Refaktor.Persistence.GitRepo 4 | alias Refaktor.Persistence.GitBranch 5 | 6 | def enqueue_clone(build, git_repo, git_branch, jobs_to_schedule, meta) do 7 | queue = trigger_to_queue(build.trigger) 8 | params = [build.id, git_repo.id, git_branch.id, jobs_to_schedule, meta] 9 | Exq.enqueue(Exq, queue, __MODULE__, params) # calls perform below 10 | end 11 | 12 | def perform(build_id, git_repo_id, git_branch_id, jobs_to_schedule, meta) do 13 | build = Build.find_by_id(build_id) 14 | git_repo = GitRepo.find_by_id(git_repo_id) 15 | git_branch = GitBranch.find_by_id(git_branch_id) 16 | jobs_to_schedule = jobs_to_schedule |> Enum.map(&stringify_job/1) 17 | 18 | Refaktor.Builder.run_clone(build, git_repo, git_branch, jobs_to_schedule, meta) 19 | end 20 | 21 | # job was serialized into a String/binary and needs to be an Atom again 22 | defp stringify_job(%{"job" => job, "job_id" => job_id}) do 23 | %{"job" => Module.safe_concat([job]), "job_id" => job_id} 24 | end 25 | 26 | defp trigger_to_queue("manual"), do: "priority" 27 | defp trigger_to_queue(_), do: "default" 28 | end 29 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :hex_faktor, 6 | version: "0.0.1", 7 | elixir: "~> 1.1", 8 | elixirc_paths: elixirc_paths(Mix.env), 9 | compilers: [:phoenix, :gettext] ++ Mix.compilers, 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | aliases: aliases, 13 | deps: deps] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [mod: {HexFaktor, []}, 21 | applications: [:phoenix, :phoenix_html, :cowboy, :logger, :gettext, 22 | :phoenix_ecto, :postgrex, :oauth2, :poolboy, :rollbax, :exq]] 23 | end 24 | 25 | # Specifies which paths to compile per environment. 26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"] 27 | defp elixirc_paths(_), do: ["lib", "web"] 28 | 29 | # Specifies your project dependencies. 30 | # 31 | # Type `mix help deps` for examples and options. 32 | defp deps do 33 | [ 34 | {:phoenix, "~> 1.1.2"}, 35 | {:phoenix_ecto, "~> 2.0"}, 36 | {:postgrex, ">= 0.0.0"}, 37 | {:phoenix_html, "~> 2.3"}, 38 | {:phoenix_live_reload, "~> 1.0", only: :dev}, 39 | {:gettext, "~> 0.9"}, 40 | {:cowboy, "~> 1.0"}, 41 | {:poison, "~> 1.4"}, 42 | {:poolboy, "~> 1.5"}, 43 | {:oauth2, "~> 0.5.0"}, 44 | {:mailgun, "~> 0.1.1"}, 45 | {:timex, "~> 1.0"}, 46 | {:timex_ecto, "~> 0.7.0"}, 47 | {:rollbax, "~> 0.6"}, 48 | {:exq, "~> 0.6.0"} 49 | ] 50 | end 51 | 52 | # Aliases are shortcut or tasks specific to the current project. 53 | # For example, to create, migrate and run the seeds file at once: 54 | # 55 | # $ mix ecto.setup 56 | # 57 | # See the documentation for `Mix` for more info on aliases. 58 | defp aliases do 59 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 60 | "ecto.reset": ["ecto.drop", "ecto.setup"]] 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": { 3 | }, 4 | "dependencies": { 5 | "babel-brunch": "^6.0.0", 6 | "brunch": "^2.1.1", 7 | "clean-css-brunch": ">= 1.0 < 1.8", 8 | "css-brunch": ">= 1.0 < 1.8", 9 | "javascript-brunch": ">= 1.0 < 1.8", 10 | "uglify-js-brunch": ">= 1.0 < 1.8", 11 | "phoenix": "file:deps/phoenix", 12 | "phoenix_html": "file:deps/phoenix_html", 13 | 14 | "node-sass": "^3.4.2", 15 | "sass-brunch": "^1.8.10" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151004151127_add_git_objects.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.AddGitObjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:git_repos) do 6 | add :uid, :string 7 | add :url, :string 8 | 9 | timestamps 10 | 11 | add :default_git_branch_id, :integer 12 | end 13 | create index(:git_repos, ["uid"]) 14 | create unique_index(:git_repos, ["url"]) 15 | 16 | create table(:git_branches) do 17 | add :name, :string 18 | 19 | timestamps 20 | 21 | add :git_repo_id, :integer 22 | add :latest_git_revision_id, :integer 23 | end 24 | 25 | create table(:git_revisions) do 26 | add :sha1, :string 27 | 28 | timestamps 29 | 30 | add :git_repo_id, :integer 31 | add :git_branch_id, :integer 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151004151130_add_inch_objects.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.AddInchObjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:inch_object_references) do 6 | add :job_id, :integer 7 | add :inch_object_id, :integer 8 | end 9 | create index(:inch_object_references, ["job_id"]) 10 | create unique_index(:inch_object_references, 11 | ["job_id", "inch_object_id"], 12 | name: :inch_object_references_unique_index) 13 | 14 | create table(:inch_objects) do 15 | add :repo_id, :integer 16 | add :type, :string 17 | add :fullname, :text 18 | add :score, :integer 19 | add :grade, :string, size: 1 20 | add :priority, :integer 21 | add :location, :string 22 | add :digest, :string, size: 44 23 | 24 | timestamps 25 | end 26 | create index(:inch_objects, ["digest"]) 27 | 28 | create table(:inch_object_roles) do 29 | add :inch_object_id, :integer 30 | add :inch_object_role_name_id, :integer 31 | add :ref_name, :string 32 | add :priority, :integer 33 | add :score, :integer 34 | 35 | timestamps 36 | end 37 | create index(:inch_object_roles, ["inch_object_id"]) 38 | 39 | create table(:inch_object_role_names) do 40 | add :name, :string 41 | end 42 | create unique_index(:inch_object_role_names, [:name]) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151006214612_add_build_objects.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.AddBuildObjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:builds) do 6 | add :nr, :integer 7 | add :trigger, :string 8 | 9 | timestamps 10 | 11 | add :git_repo_id, :integer 12 | add :git_branch_id, :integer 13 | end 14 | create index(:builds, ["git_repo_id"]) 15 | 16 | create table(:build_jobs) do 17 | add :nr, :integer 18 | 19 | add :language, :string 20 | add :intent, :string 21 | add :stderr, :string 22 | add :stdout, :string 23 | add :logs, :text 24 | 25 | add :status, :string 26 | add :started_at, :datetime 27 | add :finished_at, :datetime 28 | add :debug_info, :text 29 | 30 | timestamps 31 | 32 | add :build_id, :integer 33 | add :git_branch_id, :integer 34 | add :git_revision_id, :integer 35 | end 36 | create index(:build_jobs, ["build_id"]) 37 | create unique_index(:build_jobs, 38 | ["nr", "build_id"], 39 | name: :build_jobs_unique_index) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151210191401_add_deps_objects.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.AddDepsObjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:deps_objects) do 6 | add :build_job_id, :integer 7 | add :project_id, :integer 8 | add :git_repo_id, :integer 9 | add :git_branch_id, :integer 10 | 11 | add :toplevel, :boolean 12 | add :language, :string 13 | add :name, :string 14 | add :source, :string 15 | add :source_url, :string 16 | add :locked_version, :string 17 | add :required_version, :string 18 | add :available_versions, {:array, :string} 19 | add :mix_envs, {:array, :string} 20 | add :severity, :string 21 | 22 | timestamps 23 | end 24 | create index(:deps_objects, ["build_job_id"]) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151222211906_create_user.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users) do 6 | add :uid, :integer 7 | add :provider, :string 8 | add :user_name, :string 9 | 10 | add :full_name, :string 11 | add :email, :string 12 | add :email_token, :string 13 | add :email_verified_at, :datetime 14 | 15 | add :last_github_sync, :datetime 16 | 17 | add :email_notification_frequency, :string 18 | add :email_newsletter, :boolean 19 | 20 | timestamps 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151223202938_create_project.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateProject do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:projects) do 6 | add :uid, :integer # unique id in the oauth provider's system 7 | add :provider, :string 8 | add :name, :string 9 | add :html_url, :string 10 | add :clone_url, :string 11 | add :default_branch, :string 12 | add :language, :string 13 | add :fork, :boolean 14 | 15 | add :active, :boolean 16 | add :git_repo_id, :integer 17 | 18 | add :last_github_sync, :datetime 19 | add :latest_build_job_id, :integer 20 | 21 | timestamps 22 | end 23 | create index(:projects, ["provider", "name"]) 24 | create unique_index(:projects, ["provider", "uid"], name: :projects_unique_index) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151223211835_create_project_user.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateProjectUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:project_users) do 6 | add :project_id, :integer 7 | add :user_id, :integer 8 | 9 | timestamps 10 | end 11 | create index(:project_users, ["project_id"]) 12 | create index(:project_users, ["user_id"]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20151227211157_create_package.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreatePackage do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:packages) do 6 | add :name, :string 7 | add :source, :string 8 | add :language, :string 9 | add :description, :string 10 | add :source_url, :string 11 | 12 | add :available_versions, {:array, :string} 13 | 14 | timestamps 15 | end 16 | create index(:packages, ["name"]) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160101214426_create_notification.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateNotification do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:notifications) do 6 | add :user_id, :integer 7 | 8 | add :project_id, :integer 9 | add :git_branch_id, :integer 10 | add :deps_object_id, :integer 11 | add :package_id, :integer 12 | 13 | add :reason, :string 14 | add :reason_hash, :string 15 | 16 | add :resolved_by_build_job_id, :integer 17 | add :seen_at, :datetime 18 | add :email_sent_at, :datetime 19 | 20 | timestamps 21 | end 22 | create index(:notifications, ["user_id", "seen_at"]) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160106134613_create_project_hook.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateProjectHook do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:project_hooks) do 6 | add :project_id, :integer 7 | add :provider, :string 8 | add :uid, :string 9 | add :active, :boolean, default: false 10 | 11 | timestamps 12 | end 13 | create index(:project_hooks, ["project_id"]) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160114072856_create_project_user_settings.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateProjectUserSettings do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:project_user_settings) do 6 | add :user_id, :integer 7 | add :project_id, :integer 8 | add :notification_branches, {:array, :string} 9 | add :email_enabled, :boolean, default: false 10 | 11 | timestamps 12 | end 13 | create index(:project_user_settings, ["project_id"]) 14 | create index(:project_user_settings, ["user_id"]) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160121145122_create_app_event_log.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreateAppEventLog do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:app_event_logs) do 6 | add :user_id, :integer 7 | add :key, :string 8 | add :value, :map 9 | 10 | timestamps 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160222194716_add_use_lock_file_to_projects.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.AddUseLockFileToProjects do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:projects) do 6 | add :use_lock_file, :boolean, default: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160528081649_remove_available_versions_from_packages.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.RemoveAvailableVersionsFromPackages do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:packages) do 6 | remove :available_versions 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160528081729_add_project_id_and_releases_to_packages.exs: -------------------------------------------------------------------------------- 1 | defmodule Elixir.HexFaktor.Repo.Migrations.AddProjectIdAndReleasesToPackages do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:packages) do 6 | add :project_id, :integer 7 | add :releases, {:array, :map} 8 | 9 | modify :description, :text 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160611173238_create_package_user_settings.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.CreatePackageUserSettings do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:package_user_settings) do 6 | add :user_id, :integer 7 | add :package_id, :integer 8 | 9 | add :notifications_for_major, :boolean, default: false 10 | add :notifications_for_minor, :boolean, default: false 11 | add :notifications_for_patch, :boolean, default: false 12 | add :notifications_for_pre, :boolean, default: false 13 | 14 | add :email_enabled, :boolean, default: false 15 | 16 | timestamps 17 | end 18 | create index(:package_user_settings, ["package_id"]) 19 | create index(:package_user_settings, ["user_id"]) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /priv/repo/migrations/20160614164904_add_metadata_to_notifications.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Repo.Migrations.AddMetadataToNotifications do 2 | use Ecto.Migration 3 | 4 | def change do 5 | alter table(:notifications) do 6 | add :metadata, :map 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # HexFaktor.Repo.insert!(%HexFaktor.SomeModel{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | Code.require_file("seeds/packages.exs", __DIR__) 14 | Code.require_file("seeds/projects.exs", __DIR__) 15 | -------------------------------------------------------------------------------- /priv/repo/seeds/packages.exs: -------------------------------------------------------------------------------- 1 | 2 | HexFaktor.Repo.insert!(%HexFaktor.Package{source: "hex", name: "credo"}) 3 | HexFaktor.Repo.insert!(%HexFaktor.Package{source: "hex", name: "bunt"}) 4 | HexFaktor.Repo.insert!(%HexFaktor.Package{source: "hex", name: "html_sanitize_ex"}) 5 | -------------------------------------------------------------------------------- /priv/repo/seeds/projects.exs: -------------------------------------------------------------------------------- 1 | require Ecto.Query 2 | import Ecto.Query 3 | 4 | filename = Path.join(__DIR__, "projects.csv") 5 | content = File.read!(filename) 6 | 7 | query = from r in HexFaktor.Project, 8 | select: count(r.id), 9 | limit: 1 10 | base_count = HexFaktor.Repo.one(query) 11 | 12 | String.split(content, "\n") 13 | #|> Enum.take(100) 14 | |> Enum.each(fn(line) -> 15 | case line |> String.split(",") do 16 | [ 17 | _package_name, 18 | github_url, 19 | name, 20 | uid, 21 | language, 22 | default_branch, 23 | fork 24 | ] -> 25 | %HexFaktor.Project{} 26 | |> HexFaktor.Project.changeset(%{ 27 | "uid" => uid, 28 | "provider" => "github", 29 | "name" => name, 30 | "clone_url" => "#{github_url}.git", 31 | "html_url" => github_url, 32 | "language" => language, 33 | "active" => false, 34 | "default_branch" => default_branch, 35 | "fork" => fork == "true" 36 | }) 37 | |> HexFaktor.Repo.insert 38 | |> case do 39 | {:ok, result} -> nil 40 | val -> IO.inspect val 41 | end 42 | _ -> 43 | nil 44 | end 45 | end) 46 | -------------------------------------------------------------------------------- /script/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | JOB_EVAL_DIR=$(pwd)/../hex_faktor-workdir/script-docker-eval 4 | JOB_CODE_DIR=$(pwd)/ 5 | 6 | mkdir -p $JOB_EVAL_DIR 7 | 8 | 9 | 10 | case "$1" in 11 | s|setup) 12 | docker run -ti -v $(pwd)/dockertools:/tools faktor-elixir /tools/mix/hex_faktor/setup.sh 13 | ;; 14 | *) 15 | docker run -ti -v $(pwd)/dockertools:/tools -v $JOB_CODE_DIR:/job/code -v $JOB_EVAL_DIR:/job/eval faktor-elixir bash 16 | ;; 17 | esac 18 | -------------------------------------------------------------------------------- /script/reset_dev_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # remove all docker containers for HexFaktor 4 | docker ps -a | grep 'hf-job-' | awk '{print $1}' | xargs --no-run-if-empty docker rm 5 | 6 | 7 | # remove all workdirs for HexFaktor 8 | rm -fr tmp/0000 9 | rm -fr ../hex_faktor-workdir/0000 10 | 11 | # reset database 12 | MIX_ENV=dev mix ecto.reset 13 | -------------------------------------------------------------------------------- /script/run_all_elixir_projects.exs: -------------------------------------------------------------------------------- 1 | HexFaktor.Persistence.Project.all_active 2 | |> Enum.filter(&(&1.language == "Elixir")) 3 | #|> Enum.take(5) 4 | |> Enum.each(fn(project) -> 5 | System.cmd("curl", ["-X", "POST", "http://localhost:4000/api/rebuild_via_hook?id=#{project.id}"]) 6 | end) 7 | -------------------------------------------------------------------------------- /script/send_daily_emails.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | 3 | Logger.info "[start] #{HexFaktor.EmailPublisher.now}" 4 | 5 | HexFaktor.EmailPublisher.send_daily_emails() 6 | 7 | Logger.info " [done] #{HexFaktor.EmailPublisher.now}" 8 | -------------------------------------------------------------------------------- /script/send_weekly_emails.exs: -------------------------------------------------------------------------------- 1 | require Logger 2 | 3 | Logger.info "[start] #{HexFaktor.EmailPublisher.now}" 4 | 5 | HexFaktor.EmailPublisher.send_weekly_emails() 6 | 7 | Logger.info " [done] #{HexFaktor.EmailPublisher.now}" 8 | -------------------------------------------------------------------------------- /test/channels/feed_channel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.FeedChannelTest do 2 | use HexFaktor.ChannelCase 3 | 4 | alias HexFaktor.FeedChannel 5 | 6 | setup do 7 | {:ok, _, socket} = 8 | socket("user_id", %{some: :assign}) 9 | |> subscribe_and_join(FeedChannel, "feeds:lobby") 10 | 11 | {:ok, socket: socket} 12 | end 13 | 14 | test "ping replies with status ok", %{socket: socket} do 15 | ref = push socket, "ping", %{"hello" => "there"} 16 | assert_reply ref, :ok, %{"hello" => "there"} 17 | end 18 | 19 | test "shout broadcasts to feeds:lobby", %{socket: socket} do 20 | push socket, "shout", %{"hello" => "all"} 21 | assert_broadcast "shout", %{"hello" => "all"} 22 | end 23 | 24 | test "broadcasts are pushed to the client", %{socket: socket} do 25 | broadcast_from! socket, "broadcast", %{"some" => "data"} 26 | assert_push "broadcast", %{"some" => "data"} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/channels/project_channel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectChannelTest do 2 | use HexFaktor.ChannelCase 3 | 4 | alias HexFaktor.ProjectChannel 5 | 6 | setup do 7 | {:ok, _, socket} = 8 | socket("user_id", %{some: :assign}) 9 | |> subscribe_and_join(ProjectChannel, "projects:lobby") 10 | 11 | {:ok, socket: socket} 12 | end 13 | 14 | test "ping replies with status ok", %{socket: socket} do 15 | ref = push socket, "ping", %{"hello" => "there"} 16 | assert_reply ref, :ok, %{"hello" => "there"} 17 | end 18 | 19 | test "shout broadcasts to projects:lobby", %{socket: socket} do 20 | push socket, "shout", %{"hello" => "all"} 21 | assert_broadcast "shout", %{"hello" => "all"} 22 | end 23 | 24 | test "broadcasts are pushed to the client", %{socket: socket} do 25 | broadcast_from! socket, "broadcast", %{"some" => "data"} 26 | assert_push "broadcast", %{"some" => "data"} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/channels/user_channel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.UserChannelTest do 2 | use HexFaktor.ChannelCase 3 | 4 | alias HexFaktor.UserChannel 5 | 6 | setup do 7 | {:ok, _, socket} = 8 | socket("user_id", %{some: :assign}) 9 | |> subscribe_and_join(UserChannel, "users:lobby") 10 | 11 | {:ok, socket: socket} 12 | end 13 | 14 | test "ping replies with status ok", %{socket: socket} do 15 | ref = push socket, "ping", %{"hello" => "there"} 16 | assert_reply ref, :ok, %{"hello" => "there"} 17 | end 18 | 19 | test "shout broadcasts to users:lobby", %{socket: socket} do 20 | push socket, "shout", %{"hello" => "all"} 21 | assert_broadcast "shout", %{"hello" => "all"} 22 | end 23 | 24 | test "broadcasts are pushed to the client", %{socket: socket} do 25 | broadcast_from! socket, "broadcast", %{"some" => "data"} 26 | assert_push "broadcast", %{"some" => "data"} 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/controllers/badge_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.BadgeControllerTest do 2 | use HexFaktor.ConnCase 3 | 4 | @existent_project_id 1 5 | @existent_project_name "rrrene/credo" 6 | 7 | # 8 | # /badge 9 | # 10 | 11 | test "GET /projects/badge when NOT logged in", %{conn: conn} do 12 | conn = get conn, "/badge/all/github/#{@existent_project_name}" 13 | assert svg_response(conn, 200) 14 | end 15 | 16 | test "GET /projects/badge", %{conn: conn} do 17 | conn = perform_login(conn) 18 | 19 | conn = get conn, "/badge/all/github/#{@existent_project_name}" 20 | assert svg_response(conn, 200) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/controllers/component_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ComponentControllerTest do 2 | use HexFaktor.ConnCase 3 | 4 | @existent_project_id 1 5 | 6 | test "GET /project-list-item when NOT logged in", %{conn: conn} do 7 | conn = get conn, "/component/project-list-item/#{@existent_project_id}" 8 | assert html_response(conn, 403) 9 | end 10 | 11 | test "GET /project-list-item when logged in", %{conn: conn} do 12 | conn = perform_login(conn) 13 | 14 | conn = get conn, "/component/project-list-item/#{@existent_project_id}" 15 | assert html_response(conn, 200) 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/controllers/git_hub_auth_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.GitHubAuthControllerTest do 2 | use HexFaktor.ConnCase 3 | 4 | test "GET /auth (logged in)" do 5 | conn = get conn(), "/auth" 6 | assert html_response(conn, 302) 7 | conn = get conn, "/auth/callback?code=1" 8 | assert html_response(conn, 302) 9 | 10 | conn = get conn, "/projects" 11 | assert html_response(conn, 200) =~ ~r/hf\:user_id/ 12 | 13 | conn = get conn, "/auth/sign_out" 14 | assert html_response(conn, 302) 15 | 16 | conn = get conn, "/" 17 | refute html_response(conn, 200) =~ ~r/hf\:user_id/ 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PageControllerTest do 2 | use HexFaktor.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get conn, "/" 6 | assert html_response(conn, 200) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/controllers/project_hook_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectHookControllerTest do 2 | use HexFaktor.ConnCase 3 | 4 | @existent_project_id 1 5 | @existent_project_name "rrrene/credo" 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/controllers/user_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.UserControllerTest do 2 | use HexFaktor.ConnCase 3 | 4 | @valid_attrs %{email_notification_frequency: "none", email_newsletter: false} 5 | @invalid_attrs %{email_notification_frequency: "something"} 6 | 7 | test "GET /settings when NOT logged in", %{conn: conn} do 8 | conn = get conn, "/settings" 9 | assert access_denied?(conn) 10 | end 11 | 12 | test "GET /settings", %{conn: conn} do 13 | conn = perform_login(conn) 14 | 15 | conn = get conn, "/settings" 16 | assert html_response(conn, 200) 17 | end 18 | 19 | test "PUT /settings", %{conn: conn} do 20 | conn = perform_login(conn) 21 | user = Repo.get!(HexFaktor.User, 1) 22 | 23 | assert "weekly" == user.email_notification_frequency 24 | assert user.email_newsletter 25 | 26 | conn = put conn, "/settings", user: @valid_attrs 27 | assert html_response(conn, 302) 28 | 29 | user = Repo.get!(HexFaktor.User, 1) 30 | assert "none" == user.email_notification_frequency 31 | refute user.email_newsletter 32 | end 33 | 34 | test "PUT /settings change email", %{conn: conn} do 35 | conn = perform_login(conn) 36 | user = Repo.get!(HexFaktor.User, 1) 37 | 38 | refute nil == user.email 39 | refute nil == user.email_verified_at 40 | assert nil == user.email_token 41 | 42 | conn = put conn, "/settings", user: %{email: "rf-changed@bamaru.de"} 43 | assert html_response(conn, 302) 44 | 45 | user = Repo.get!(HexFaktor.User, 1) 46 | refute nil == user.email 47 | assert nil == user.email_verified_at 48 | refute nil == user.email_token 49 | end 50 | 51 | test "PUT /settings with invalid attrs", %{conn: conn} do 52 | conn = perform_login(conn) 53 | user = Repo.get!(HexFaktor.User, 1) 54 | 55 | assert "weekly" == user.email_notification_frequency 56 | 57 | conn = put conn, "/settings", user: @invalid_attrs 58 | assert html_response(conn, 200) 59 | 60 | user = Repo.get!(HexFaktor.User, 1) 61 | assert "weekly" == user.email_notification_frequency 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /test/hex_faktor/notification_publisher_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.NotificationPublisherTest do 2 | use HexFaktor.ConnCase 3 | 4 | alias HexFaktor.Persistence.Package 5 | alias HexFaktor.Persistence.Notification 6 | 7 | alias HexFaktor.NotificationPublisher 8 | alias HexFaktor.Persistence.PackageUserSettings 9 | 10 | @test_package_id 1 11 | @test_user_id 1 12 | 13 | setup do 14 | package_user_settings = 15 | PackageUserSettings.ensure(@test_package_id, @test_user_id) 16 | {:ok, %{"package_user_settings" => package_user_settings}} 17 | end 18 | 19 | def notification_count do 20 | Notification.latest_for(@test_user_id, 1000) 21 | |> Enum.count 22 | end 23 | 24 | test "the truth" do 25 | assert PackageUserSettings.find(@test_package_id, @test_user_id) 26 | assert 0 == notification_count() 27 | 28 | test_package = Package.find_by_id(@test_package_id) 29 | NotificationPublisher.handle_new_package_update(test_package) 30 | assert 1 == notification_count() 31 | 32 | NotificationPublisher.handle_new_package_update(test_package) 33 | assert 1 == notification_count() 34 | 35 | new_release = 36 | %{"updated_at" => "2020-01-01T00:00:00Z", "version" => "42.0.0"} 37 | releases = [new_release] ++ test_package.releases 38 | test_package2 = 39 | %HexFaktor.Package{test_package | releases: releases} 40 | 41 | NotificationPublisher.handle_new_package_update(test_package2) 42 | assert 2 == notification_count() 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /test/hex_faktor/project_access_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectAccessTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.Persistence.Project 5 | alias HexFaktor.Persistence.User 6 | alias HexFaktor.ProjectAccess 7 | alias HexFaktor.Fixtures 8 | 9 | test "grant_exclusively" do 10 | user = User.find_by_id(1) 11 | project1 = Project.find_by_id(1) 12 | project2 = Project.find_by_id(2) 13 | 14 | assert ProjectAccess.granted?(project1.id, user) 15 | refute ProjectAccess.granted?(project2.id, user) 16 | 17 | ProjectAccess.grant_exclusively([project1, project2], user) 18 | 19 | assert ProjectAccess.granted?(project1.id, user) 20 | assert ProjectAccess.granted?(project2.id, user) 21 | 22 | ProjectAccess.grant_exclusively([project2], user) 23 | 24 | refute ProjectAccess.granted?(project1.id, user) 25 | assert ProjectAccess.granted?(project2.id, user) 26 | end 27 | 28 | test "grant & settings" do 29 | user = User.find_by_id(1) 30 | project1 = Project.find_by_id(1) 31 | project2 = Project.find_by_id(2) 32 | 33 | assert ProjectAccess.granted?(project1.id, user) 34 | refute ProjectAccess.granted?(project2.id, user) 35 | 36 | ProjectAccess.grant(project2, user) 37 | 38 | assert ProjectAccess.granted?(project1.id, user) 39 | assert ProjectAccess.granted?(project2.id, user) 40 | 41 | settings = ProjectAccess.settings(project2, user) 42 | assert ["master"] == settings.notification_branches 43 | assert settings.email_enabled 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /test/models/app_event_log_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.AppEventLogTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.AppEventLog 5 | 6 | @valid_attrs %{key: "some content", user_id: 42, value: %{}} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = AppEventLog.changeset(%AppEventLog{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = AppEventLog.changeset(%AppEventLog{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/notification_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.NotificationTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.Notification 5 | 6 | @valid_attrs %{package_id: 42, project_id: 42, git_branch_id: 42, reason: "some content", reason_hash: "some content", user_id: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Notification.changeset(%Notification{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Notification.changeset(%Notification{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/package_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PackageTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.Package 5 | 6 | @valid_attrs %{name: "some content", source: "some content", source_url: "some content"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = Package.changeset(%Package{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = Package.changeset(%Package{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/package_user_settings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PackageUserSettingsTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.PackageUserSettings 5 | 6 | @valid_attrs %{package_id: 42, user_id: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = PackageUserSettings.changeset(%PackageUserSettings{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = PackageUserSettings.changeset(%PackageUserSettings{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/project_hook_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectHookTest do 2 | use HexFaktor.ModelCase 3 | 4 | end 5 | -------------------------------------------------------------------------------- /test/models/project_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.Project 5 | alias HexFaktor.ProjectHook 6 | alias HexFaktor.ProjectUser 7 | alias HexFaktor.ProjectUserSettings 8 | alias HexFaktor.Repo 9 | 10 | alias Refaktor.Builder.Model.Build 11 | 12 | @valid_attrs %{uid: 1, provider: "github", name: "rrrene/credo", active: false, default_branch: "master", git_repo_id: 42, html_url: "some content", clone_url: "some content"} 13 | @invalid_attrs %{} 14 | 15 | test "changeset with valid attributes" do 16 | changeset = Project.changeset(%Project{}, @valid_attrs) 17 | assert changeset.valid? 18 | end 19 | 20 | test "changeset with invalid attributes" do 21 | changeset = Project.changeset(%Project{}, @invalid_attrs) 22 | refute changeset.valid? 23 | end 24 | 25 | test "deletes associations of a project when project is deleted" do 26 | old_hook_count = count(ProjectHook) 27 | old_user_count = count(ProjectUser) 28 | old_user_settings_count = count(ProjectUserSettings) 29 | old_build_count = count(Build) 30 | 31 | project = Repo.get(Project, 1) 32 | Repo.delete!(project) 33 | 34 | assert old_hook_count < count(ProjectHook) 35 | assert old_user_count < count(ProjectUser) 36 | assert old_user_settings_count < count(ProjectUserSettings) 37 | #assert old_build_count < count(Build) # TODO: add builds to fixtures 38 | end 39 | 40 | defp count(model) do 41 | from(p in model, select: count(p.id)) 42 | |> Repo.one! 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/models/project_user_settings_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectUserSettingsTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.ProjectUserSettings 5 | 6 | @valid_attrs %{active_branches: "some content", email_enabled: true, project_id: 42, user_id: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = ProjectUserSettings.changeset(%ProjectUserSettings{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = ProjectUserSettings.changeset(%ProjectUserSettings{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/project_user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectUserTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.ProjectUser 5 | 6 | @valid_attrs %{project_id: 42, user_id: 42} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = ProjectUser.changeset(%ProjectUser{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = ProjectUser.changeset(%ProjectUser{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/models/user_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.UserTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.User 5 | 6 | @valid_attrs %{uid: 1, provider: "github", full_name: "René Föhring", user_name: "rrrene", email: "rf@bamaru.de", email_notification_frequency: "none"} 7 | @invalid_attrs %{} 8 | 9 | test "changeset with valid attributes" do 10 | changeset = User.changeset(%User{}, @valid_attrs) 11 | assert changeset.valid? 12 | end 13 | 14 | test "changeset with invalid attributes" do 15 | changeset = User.changeset(%User{}, @invalid_attrs) 16 | refute changeset.valid? 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/persistence/project_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Persistence.UserTest do 2 | use HexFaktor.ModelCase 3 | 4 | alias HexFaktor.Persistence.Project 5 | 6 | @valid_github_response %{ 7 | "id" => 1296269, 8 | "full_name" => "rrrene/credo", 9 | "html_url" => "https://github.com/octocat/Hello-World", 10 | "clone_url" => "https://github.com/octocat/Hello-World.git", 11 | "default_branch" => "master", 12 | "language" => "Elixir", 13 | "fork" => false 14 | } 15 | 16 | @invalid_attrs %{} 17 | 18 | test "ensure does only create every project once" do 19 | old_count = Project.count 20 | 21 | Project.ensure(@valid_github_response) 22 | new_count = Project.count 23 | assert new_count > old_count 24 | 25 | Project.ensure(@valid_github_response) 26 | newer_count = Project.count 27 | assert new_count == newer_count 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /test/refaktor/builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.BuilderTest do 2 | @moduledoc """ 3 | NOTE: The tested jobs are defined in support/test_jobs.exs 4 | """ 5 | 6 | use ExUnit.Case 7 | 8 | @moduletag :refaktor 9 | 10 | # TODO: clone the repo, then use the local path as test URL 11 | # to avoid unnecessary network traffic 12 | @test_repo_url "https://github.com/inch-ci/Hello-World-Elixir.git" 13 | 14 | test "runs a job" do 15 | jobs = [TestOutputJob] 16 | {:ok, _build, pid} = Refaktor.Builder.add_and_run_repo(@test_repo_url, "master", jobs: jobs) 17 | 18 | receive do 19 | {:job_done, _job_id, result} -> 20 | assert {:ok, {}} == result 21 | end 22 | end 23 | 24 | test "errors a job for command exits with non-zero exit code" do 25 | jobs = [TestErrorJob] 26 | {:ok, _build, pid} = Refaktor.Builder.add_and_run_repo(@test_repo_url, "master", jobs: jobs) 27 | 28 | receive do 29 | {:job_done, job_id, result} -> 30 | build_job = get_build_job(job_id) 31 | assert "failure" == build_job.status 32 | assert {:error, {42, ""}} == result 33 | end 34 | end 35 | 36 | 37 | 38 | defp get_build_job(job_id) do 39 | Refaktor.Builder.Model.BuildJob 40 | |> HexFaktor.Repo.get!(job_id) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/refaktor/hub/docker/run_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Docker.RunTest do 2 | use ExUnit.Case 3 | 4 | @moduletag :refaktor 5 | 6 | @test_image "faktor-test-image" 7 | 8 | test "returns echoed output" do 9 | result = Refaktor.Docker.Run.call(@test_image, ["echo", "this is a mix test run"]) 10 | assert {:ok, "this is a mix test run\n"} == result 11 | end 12 | 13 | # if this test times out, it means that :kill_after does not work properly 14 | @tag timeout: 10000 15 | test "kills container after given period" do 16 | {_,_,number} = :os.timestamp 17 | name = "faktor-test-#{number}" 18 | result = Refaktor.Docker.Run.call(@test_image, ["sleep", "60"], [], [kill_after: 2000, name: name]) 19 | 20 | {:timeout, {{output_top, 0}, {output_kill, 0}}} = result 21 | assert String.length(output_top) > 0 22 | assert name == String.strip(output_kill) 23 | end 24 | 25 | test "works when command exits with != 0" do 26 | result = Refaktor.Docker.Run.call(@test_image, ["bash", "-c \"exit 5\""]) 27 | {:error, _, code} = result 28 | refute 0 == code 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/refaktor/hub/git_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.RepoTest do 2 | use ExUnit.Case 3 | 4 | @moduletag :refaktor 5 | 6 | @test_repo_url "https://github.com/inch-ci/Hello-World-Elixir.git" 7 | @fail_repo_url "https://github.com/inch-ci/Hello-World-123.git" 8 | @tmp_dir Refaktor.Builder.work_dir 9 | 10 | test "clones the git repo" do 11 | dir = Path.join(@tmp_dir, "test-repo") 12 | File.rm_rf!(dir) 13 | result = Refaktor.Builder.Git.clone(@test_repo_url, dir) 14 | {ok, repo} = result 15 | assert :ok == ok 16 | assert "master" == repo.branch 17 | assert String.length(repo.revision[:sha1]) > 0 18 | end 19 | 20 | test "clones the git repo with the right branch" do 21 | dir = Path.join(@tmp_dir, "test-repo-branched") 22 | File.rm_rf!(dir) 23 | result = Refaktor.Builder.Git.clone(@test_repo_url, dir, branch: "develop") 24 | {ok, repo} = result 25 | assert :ok == ok 26 | assert "develop" == repo.branch 27 | assert String.length(repo.revision[:sha1]) > 0 28 | end 29 | 30 | test "fails the git repo clone" do 31 | dir = Path.join(@tmp_dir, "test-repo") 32 | File.rm_rf!(dir) 33 | result = Refaktor.Builder.Git.clone(@fail_repo_url, dir) 34 | {ok, _, _} = result 35 | assert :error == ok 36 | end 37 | 38 | test "fails the git repo clone with the right branch" do 39 | dir = Path.join(@tmp_dir, "test-repo-branched") 40 | File.rm_rf!(dir) 41 | result = Refaktor.Builder.Git.clone(@test_repo_url, dir, branch: "develop123") 42 | {ok, _, _} = result 43 | assert :error == ok 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/refaktor/hub/hub_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Builder2Test do 2 | use ExUnit.Case 3 | 4 | @moduletag :refaktor 5 | 6 | @test_repo_url "https://github.com/inch-ci/Hello-World-Elixir.git" 7 | @fail_repo_url "https://github.com/inch-ci/Hello-World-123.git" 8 | 9 | test "clones the git repo" do 10 | job_id = Refaktor.Test.JobID.next 11 | result = Refaktor.Builder.clone_for_job(@test_repo_url, "master", job_id) 12 | {:ok, ^job_id, job_dir, _repo_info} = result 13 | assert String.length(job_dir) > 0 14 | end 15 | 16 | test "clones the git repo with the right branch" do 17 | job_id = Refaktor.Test.JobID.next 18 | result = Refaktor.Builder.clone_for_job(@test_repo_url, "develop", job_id) 19 | {:ok, ^job_id, job_dir, _repo_info} = result 20 | assert String.length(job_dir) > 0 21 | end 22 | 23 | test "fails the git repo clone" do 24 | job_id = Refaktor.Test.JobID.next 25 | result = Refaktor.Builder.clone_for_job(@fail_repo_url, "master", job_id) 26 | {ok, ^job_id, _job_dir, _output, _exit_code} = result 27 | assert :error == ok 28 | end 29 | 30 | test "fails the git repo clone with the right branch" do 31 | job_id = Refaktor.Test.JobID.next 32 | result = Refaktor.Builder.clone_for_job(@test_repo_url, "develop123", job_id) 33 | {ok, ^job_id, _job_dir, _output, _exit_code} = result 34 | assert :error == ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/refaktor/persistence/build_objects_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Persistence.BuildTest do 2 | use ExUnit.Case 3 | 4 | @moduletag :refaktor 5 | 6 | alias Refaktor.Persistence.Build 7 | 8 | test "adds builds" do 9 | git_repo_id = 1 10 | git_branch_id = 1 11 | result1 = Build.add(git_repo_id, git_branch_id) 12 | assert result1.id 13 | result2 = Build.add(git_repo_id, git_branch_id) 14 | assert result2.id 15 | assert 1 == result1.nr 16 | assert 2 == result2.nr 17 | end 18 | 19 | test "adds build jobs" do 20 | build_id = 101 21 | result1 = Build.add_job(build_id, "elixir", "inch") 22 | result2 = Build.add_job(build_id, "elixir", "code_analysis") 23 | assert 1 == result1.nr 24 | assert 2 == result2.nr 25 | 26 | build_id = 102 27 | result3 = Build.add_job(build_id, "elixir", "inch") 28 | assert 1 == result3.nr 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/refaktor/use_case/git_repo_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.UseCase.GitRepoTest do 2 | use ExUnit.Case 3 | 4 | alias Refaktor.UseCase.Git 5 | 6 | @moduletag :refaktor 7 | 8 | @test_repo_url "https://github.com/inch-ci/Hello-World-Elixir.git" 9 | @test_repo_url2 "git@github.com:inch-ci/Hello-World-Elixir.git" 10 | @test_branch "master" 11 | 12 | test "gets the git repo from the db" do 13 | result1 = Git.get_repo(@test_repo_url) 14 | result2 = Git.get_repo(@test_repo_url) 15 | assert result1 == result2 16 | end 17 | 18 | test "gets the git repo and branch from the db" do 19 | {repo, branch} = Git.get_repo_and_branch(@test_repo_url, @test_branch) 20 | assert @test_repo_url == repo.url 21 | assert @test_branch == branch.name 22 | end 23 | 24 | test "generates uid" do 25 | expected = "github.com/inch-ci/Hello-World-Elixir" 26 | assert expected == @test_repo_url |> Git.to_uid 27 | assert expected == @test_repo_url2 |> Git.to_uid 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | alias HexFaktor.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | 28 | # The default endpoint for testing 29 | @endpoint HexFaktor.Endpoint 30 | end 31 | end 32 | 33 | setup tags do 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.restart_test_transaction(HexFaktor.Repo, []) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | imports other functionality to make it easier 8 | to build and query models. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | 23 | alias HexFaktor.Repo 24 | import Ecto.Model 25 | import Ecto.Query, only: [from: 2] 26 | 27 | import HexFaktor.Router.Helpers 28 | 29 | # The default endpoint for testing 30 | @endpoint HexFaktor.Endpoint 31 | 32 | def perform_login(conn, user_id \\ 1) do 33 | conn = get conn, "/auth/callback?code=#{user_id}" 34 | assert html_response(conn, 302) 35 | conn 36 | end 37 | 38 | def svg_response(conn, status) do 39 | body = response(conn, status) 40 | _ = response_content_type(conn, :svg) 41 | body 42 | end 43 | 44 | def access_denied?(conn) do 45 | html_response(conn, 403) 46 | end 47 | end 48 | end 49 | 50 | setup tags do 51 | unless tags[:async] do 52 | Ecto.Adapters.SQL.restart_test_transaction(HexFaktor.Repo, []) 53 | end 54 | 55 | {:ok, conn: Phoenix.ConnTest.conn()} 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/support/git_hub_api_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule GitHubAPIMock do 2 | # we are taking a single parameter here, which is the `?code=` query 3 | # param of the call to `/auth/callback` 4 | # 5 | # TODO: use this to test what happens if the API return unexpected results 6 | def user("1") do 7 | %{ 8 | "id" => 1, 9 | "login" => "rrrene", 10 | "name" => "René Föhring", 11 | "email" => "rf@bamaru.de", 12 | } 13 | end 14 | 15 | def user("2") do 16 | %{ 17 | "id" => 2, 18 | "login" => "rrrene2", 19 | "name" => "René Föhring the Second", 20 | "email" => "rf-2@bamaru.de", 21 | } 22 | end 23 | 24 | def set_hook() do 25 | %{ 26 | "active" => true, 27 | "config" => %{ 28 | "content_type" => "json", 29 | "url" => "http://localhost:4000/rebuild" 30 | }, 31 | "created_at" => "2016-01-06T19:47:16Z", 32 | "events" => ["push"], 33 | "id" => 6860506, 34 | "fork" => false, 35 | "last_response" => %{"code" => nil, "message" => nil, "status" => "unused"}, 36 | "name" => "web", 37 | "ping_url" => "https://api.github.com/repos/inch-ci/Hello-World-Elixir/hooks/6860506/pings", 38 | "test_url" => "https://api.github.com/repos/inch-ci/Hello-World-Elixir/hooks/6860506/test", 39 | "updated_at" => "2016-01-06T19:47:16Z", 40 | "url" => "https://api.github.com/repos/inch-ci/Hello-World-Elixir/hooks/6860506" 41 | } 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/support/git_hub_auth_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule GitHubAuthMock do 2 | def authorize_url!(_params \\ []) do 3 | "/" 4 | end 5 | 6 | def user_auth(code) when is_binary(code) do 7 | code 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/support/model_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ModelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | model tests. 5 | 6 | You may define functions here to be used as helpers in 7 | your model tests. See `errors_on/2`'s definition as reference. 8 | 9 | Finally, if the test case interacts with the database, 10 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias HexFaktor.Repo 20 | import Ecto.Model 21 | import Ecto.Query, only: [from: 2] 22 | import HexFaktor.ModelCase 23 | end 24 | end 25 | 26 | setup tags do 27 | unless tags[:async] do 28 | Ecto.Adapters.SQL.restart_test_transaction(HexFaktor.Repo, []) 29 | end 30 | 31 | :ok 32 | end 33 | 34 | @doc """ 35 | Helper for returning list of errors in model when passed certain data. 36 | 37 | ## Examples 38 | 39 | Given a User model that lists `:name` as a required field and validates 40 | `:password` to be safe, it would return: 41 | 42 | iex> errors_on(%User{}, %{password: "password"}) 43 | [password: "is unsafe", name: "is blank"] 44 | 45 | You could then write your assertion like: 46 | 47 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"}) 48 | 49 | You can also create the changeset manually and retrieve the errors 50 | field directly: 51 | 52 | iex> changeset = User.changeset(%User{}, password: "password") 53 | iex> {:password, "is unsafe"} in changeset.errors 54 | true 55 | """ 56 | def errors_on(model, data) do 57 | model.__struct__.changeset(model, data).errors 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/support/test_application.exs: -------------------------------------------------------------------------------- 1 | defmodule Refaktor.Test.JobID do 2 | use GenServer 3 | 4 | @table_name __MODULE__ 5 | 6 | def start_link(opts \\ []) do 7 | {:ok, _pid} = GenServer.start_link(__MODULE__, opts, name: __MODULE__) 8 | end 9 | 10 | def next do 11 | GenServer.call(__MODULE__, {:next}) 12 | end 13 | 14 | # callbacks 15 | 16 | def init(_) do 17 | {:ok, 1000} 18 | end 19 | 20 | def handle_call({:next}, _from, current_state) do 21 | {:reply, current_state+1, current_state+1} 22 | end 23 | end 24 | 25 | defmodule Refaktor.Test.Application do 26 | use Application 27 | 28 | def start(_type, _args) do 29 | import Supervisor.Spec, warn: false 30 | 31 | children = [ 32 | worker(Refaktor.Test.JobID, []), 33 | ] 34 | 35 | opts = [strategy: :one_for_one, name: Refaktor.Test.Application.Supervisor] 36 | Supervisor.start_link(children, opts) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/support/test_jobs.exs: -------------------------------------------------------------------------------- 1 | defmodule TestOutputJob do 2 | @behaviour Refaktor.Job 3 | 4 | def language, do: "shell" 5 | def intent, do: "testing" 6 | def image_to_run, do: "faktor-test-image" 7 | def script_to_run, do: "test-output" 8 | def handle_success(_job_id, _hub_dir, _output, _meta), do: {} 9 | def handle_error(_job_id, _hub_dir, output, exit_code, _meta), do: {exit_code, output} 10 | def handle_timeout(_job_id, _hub_dir, _summary, _meta), do: {} 11 | end 12 | 13 | defmodule TestErrorJob do 14 | @behaviour Refaktor.Job 15 | 16 | def language, do: "shell" 17 | def intent, do: "testing" 18 | def image_to_run, do: "faktor-test-image" 19 | def script_to_run, do: "test-error" 20 | def handle_success(_job_id, _hub_dir, _output, _meta), do: {} 21 | def handle_error(_job_id, _hub_dir, output, exit_code, _meta), do: {exit_code, output} 22 | def handle_timeout(_job_id, _hub_dir, _summary, _meta), do: {} 23 | end 24 | 25 | defmodule TestCommandNotFoundJob do 26 | @behaviour Refaktor.Job 27 | 28 | def language, do: "shell" 29 | def intent, do: "testing" 30 | def image_to_run, do: "faktor-test-image" 31 | def script_to_run, do: "test-error-this-command-does-not-exist" 32 | def handle_success(_job_id, _hub_dir, _output, _meta), do: {} 33 | def handle_error(_job_id, _hub_dir, output, exit_code, _meta), do: {exit_code, output} 34 | def handle_timeout(_job_id, _hub_dir, _summary, _meta), do: {} 35 | end 36 | 37 | defmodule TestCommandProducesBadJSONJob do 38 | @behaviour Refaktor.Job 39 | 40 | def language, do: "shell" 41 | def intent, do: "testing" 42 | def image_to_run, do: "faktor-test-image" 43 | def script_to_run, do: "test-produce-bad-result-json" 44 | def handle_success(_job_id, hub_dir, output, _meta) do 45 | case Refaktor.Job.read_result(hub_dir) do 46 | {:error, _, _} = error_tuple -> error_tuple 47 | map -> {map, output} 48 | end 49 | end 50 | def handle_error(_job_id, _hub_dir, output, exit_code, _meta), do: {exit_code, output} 51 | def handle_timeout(_job_id, _hub_dir, _summary, _meta), do: {} 52 | end 53 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Code.require_file("support/test_application.exs", __DIR__) 2 | Code.require_file("support/test_jobs.exs", __DIR__) 3 | 4 | Refaktor.Test.Application.start([], []) 5 | 6 | ExUnit.start() 7 | 8 | # Exclude all external tests from running 9 | ExUnit.configure(exclude: [refaktor: true]) 10 | 11 | # Create the database, run migrations, and start the test transaction. 12 | Mix.Task.run "ecto.drop", ["--quiet"] 13 | Mix.Task.run "ecto.create", ["--quiet"] 14 | Mix.Task.run "ecto.migrate", ["--quiet"] 15 | 16 | HexFaktor.Fixtures.insert_basic 17 | 18 | Ecto.Adapters.SQL.begin_test_transaction(HexFaktor.Repo) 19 | 20 | # Remove the temp dir (we use to store avatars etc. during tests) so we can 21 | # check if certain files are created during testing 22 | File.rm_rf Refaktor.Builder.work_dir 23 | -------------------------------------------------------------------------------- /test/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ErrorViewTest do 2 | use HexFaktor.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | end 8 | -------------------------------------------------------------------------------- /test/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.LayoutViewTest do 2 | use HexFaktor.ConnCase, async: true 3 | end -------------------------------------------------------------------------------- /test/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PageViewTest do 2 | use HexFaktor.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /web/channels/feed_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.FeedChannel do 2 | use HexFaktor.Web, :channel 3 | 4 | def join("feeds:lobby", payload, socket) do 5 | if authorized?(payload) do 6 | {:ok, socket} 7 | else 8 | {:error, %{reason: "unauthorized"}} 9 | end 10 | end 11 | 12 | # Channels can be used in a request/response fashion 13 | # by sending replies to requests from the client 14 | def handle_in("ping", payload, socket) do 15 | {:reply, {:ok, payload}, socket} 16 | end 17 | 18 | # It is also common to receive messages from the client and 19 | # broadcast to everyone in the current topic (feeds:lobby). 20 | def handle_in("shout", payload, socket) do 21 | broadcast socket, "shout", payload 22 | {:noreply, socket} 23 | end 24 | 25 | # This is invoked every time a notification is being broadcast 26 | # to the client. The default implementation is just to push it 27 | # downstream but one could filter or change the event. 28 | def handle_out(event, payload, socket) do 29 | push socket, event, payload 30 | {:noreply, socket} 31 | end 32 | 33 | # Add authorization logic here as required. 34 | defp authorized?(_payload) do 35 | true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /web/channels/project_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectChannel do 2 | use HexFaktor.Web, :channel 3 | 4 | def join("projects:" <> _user_id, payload, socket) do 5 | if authorized?(payload) do 6 | {:ok, socket} 7 | else 8 | {:error, %{reason: "unauthorized"}} 9 | end 10 | end 11 | 12 | # Channels can be used in a request/response fashion 13 | # by sending replies to requests from the client 14 | def handle_in("ping", payload, socket) do 15 | {:reply, {:ok, payload}, socket} 16 | end 17 | 18 | # It is also common to receive messages from the client and 19 | # broadcast to everyone in the current topic (projects:lobby). 20 | def handle_in("shout", payload, socket) do 21 | broadcast socket, "shout", payload 22 | {:noreply, socket} 23 | end 24 | 25 | # This is invoked every time a notification is being broadcast 26 | # to the client. The default implementation is just to push it 27 | # downstream but one could filter or change the event. 28 | def handle_out(event, payload, socket) do 29 | push socket, event, payload 30 | {:noreply, socket} 31 | end 32 | 33 | # Add authorization logic here as required. 34 | defp authorized?(_payload) do 35 | true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /web/channels/user_channel.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.UserChannel do 2 | use HexFaktor.Web, :channel 3 | 4 | def join("users:" <> _user_id, payload, socket) do 5 | if authorized?(payload) do 6 | {:ok, socket} 7 | else 8 | {:error, %{reason: "unauthorized"}} 9 | end 10 | end 11 | 12 | # Channels can be used in a request/response fashion 13 | # by sending replies to requests from the client 14 | def handle_in("ping", payload, socket) do 15 | {:reply, {:ok, payload}, socket} 16 | end 17 | 18 | # It is also common to receive messages from the client and 19 | # broadcast to everyone in the current topic (users:lobby). 20 | def handle_in("shout", payload, socket) do 21 | broadcast socket, "shout", payload 22 | {:noreply, socket} 23 | end 24 | 25 | # This is invoked every time a notification is being broadcast 26 | # to the client. The default implementation is just to push it 27 | # downstream but one could filter or change the event. 28 | def handle_out(event, payload, socket) do 29 | push socket, event, payload 30 | {:noreply, socket} 31 | end 32 | 33 | # Add authorization logic here as required. 34 | defp authorized?(_payload) do 35 | true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | channel "feeds:lobby", HexFaktor.FeedChannel 6 | channel "users:*", HexFaktor.UserChannel 7 | channel "projects:*", HexFaktor.ProjectChannel 8 | 9 | ## Transports 10 | transport :websocket, Phoenix.Transports.WebSocket 11 | # transport :longpoll, Phoenix.Transports.LongPoll 12 | 13 | @socket_token_salt Application.get_env(:hex_faktor, :salt_user_socket) 14 | 15 | # Socket params are passed from the client and can 16 | # be used to verify and authenticate a user. After 17 | # verification, you can put default assigns into 18 | # the socket that will be set for all channels, ie 19 | # 20 | # {:ok, assign(socket, :user_id, verified_user_id)} 21 | # 22 | # To deny connection, return `:error`. 23 | # 24 | # See `Phoenix.Token` documentation for examples in 25 | # performing token verification on connect. 26 | def connect(%{"token" => token}, socket) do 27 | # max_age: 1209600 is equivalent to two weeks in seconds 28 | case Phoenix.Token.verify(socket, @socket_token_salt, token, max_age: 1209600) do 29 | {:ok, user_id} -> 30 | {:ok, assign(socket, :user_id, user_id)} 31 | {:error, _reason} -> 32 | :error 33 | end 34 | end 35 | def connect(_, socket), do: {:ok, socket} 36 | 37 | # Socket id's are topics that allow you to identify all sockets for a given user: 38 | # 39 | # def id(socket), do: "users_socket:#{socket.assigns.user_id}" 40 | # 41 | # Would allow you to broadcast a "disconnect" event and terminate 42 | # all active sockets and channels for a given user: 43 | # 44 | # HexFaktor.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{}) 45 | # 46 | # Returning `nil` makes this socket anonymous. 47 | def id(_socket), do: nil 48 | end 49 | -------------------------------------------------------------------------------- /web/controllers/email_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.EmailController do 2 | use HexFaktor.Web, :controller 3 | 4 | alias HexFaktor.Auth 5 | alias HexFaktor.NotificationMailer 6 | alias HexFaktor.Persistence.Notification 7 | alias HexFaktor.ProjectProvider 8 | alias HexFaktor.LayoutView 9 | 10 | def notifications(conn, params) do 11 | user = Auth.current_user(conn) 12 | notifications = Notification.all_unseen_for(user, [:project, :git_branch, :deps_object, :package]) 13 | {_, _, outdated_projects} = ProjectProvider.user_projects(user) 14 | 15 | assigns = NotificationMailer.notifications(user, notifications, outdated_projects) 16 | render(conn, "notifications.html", assigns ++ [layout: {LayoutView, "email.html"}]) 17 | end 18 | 19 | def status_report(conn, params) do 20 | user = Auth.current_user(conn) 21 | {_, active_projects, outdated_projects} = ProjectProvider.user_projects(user) 22 | package_notifications = Notification.all_unseen_for_packages_for(user, [:package]) 23 | 24 | assigns = NotificationMailer.status_report(user, active_projects, outdated_projects, package_notifications) 25 | render(conn, "status_report.html", assigns ++ [layout: {LayoutView, "email.html"}]) 26 | end 27 | 28 | def validation(conn, params) do 29 | user = Auth.current_user(conn) 30 | 31 | assigns = NotificationMailer.validation(user) 32 | render(conn, "validation.html", assigns ++ [layout: {LayoutView, "email.html"}]) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /web/controllers/help_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.HelpController do 2 | use HexFaktor.Web, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | 8 | def badge(conn, _params) do 9 | render(conn, "badge.html") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PageController do 2 | use HexFaktor.Web, :controller 3 | require Logger 4 | 5 | alias HexFaktor.Auth 6 | 7 | def index(conn, _params) do 8 | if Auth.current_user(conn) do 9 | redirect conn, to: project_path(conn, :index) 10 | else 11 | render conn, "index.html" 12 | end 13 | end 14 | 15 | def about(conn, _params) do 16 | render conn, "about.html" 17 | end 18 | 19 | def status_404(conn, _params) do 20 | render conn, "404.html" 21 | end 22 | 23 | def status_500(conn, _params) do 24 | 1 / 0 25 | render conn, "500.html" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /web/controllers/project_progress_callback.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectProgressCallback do 2 | require Logger 3 | 4 | # `progress_callback_data` is the map put into the `meta` map 5 | # during the build process 6 | def cast(%{"current_user_id" => current_user_id, "project_id" => project_id, "branch_name" => branch_name, "event_name" => event_name}) do 7 | fn(status) -> 8 | payload = %{ 9 | "project_id" => project_id, 10 | "branch_name" => branch_name, 11 | "status" => status, 12 | } 13 | HexFaktor.Broadcast.to_project(project_id, event_name, payload) 14 | if current_user_id do 15 | HexFaktor.Broadcast.to_user(current_user_id, event_name, payload) 16 | end 17 | end 18 | end 19 | def cast(nil) do 20 | fn(_) -> end 21 | end 22 | def cast(value) do 23 | Logger.error "Got ProjectProgressCallback.cast for value: #{inspect value}" 24 | cast(nil) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](http://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import HexFaktor.Gettext 9 | 10 | # Simple translation 11 | gettext "Here is the string to translate" 12 | 13 | # Plural translation 14 | ngettext "Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3 17 | 18 | # Domain-based translation 19 | dgettext "errors", "Here is the error message to translate" 20 | 21 | See the [Gettext Docs](http://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :hex_faktor 24 | end 25 | -------------------------------------------------------------------------------- /web/models/app_event_log.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.AppEventLog do 2 | use HexFaktor.Web, :model 3 | 4 | schema "app_event_logs" do 5 | field :user_id, :integer 6 | field :key, :string 7 | field :value, :map 8 | 9 | timestamps 10 | end 11 | 12 | @required_fields ~w(user_id key value) 13 | @optional_fields ~w() 14 | 15 | @doc """ 16 | Creates a changeset based on the `model` and `params`. 17 | 18 | If no params are provided, an invalid changeset is returned 19 | with no validation performed. 20 | """ 21 | def changeset(model, params \\ :empty) do 22 | model 23 | |> cast(params, @required_fields, @optional_fields) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /web/models/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Notification do 2 | use HexFaktor.Web, :model 3 | 4 | alias Refaktor.Job.Elixir.Deps.Model.DepsObject 5 | alias Refaktor.Builder.Model.GitBranch 6 | alias HexFaktor.Project 7 | alias HexFaktor.Package 8 | 9 | schema "notifications" do 10 | field :reason, :string 11 | field :reason_hash, :string 12 | 13 | field :resolved_by_build_job_id, :integer 14 | 15 | field :seen_at, Ecto.DateTime 16 | field :email_sent_at, Ecto.DateTime 17 | 18 | field :metadata, :map 19 | 20 | timestamps 21 | 22 | belongs_to :user, User 23 | belongs_to :project, Project 24 | belongs_to :git_branch, GitBranch 25 | belongs_to :deps_object, DepsObject 26 | belongs_to :package, Package 27 | end 28 | 29 | @required_fields ~w(user_id reason reason_hash) 30 | @optional_fields ~w(project_id git_branch_id deps_object_id package_id resolved_by_build_job_id metadata) 31 | 32 | @doc """ 33 | Creates a changeset based on the `model` and `params`. 34 | 35 | If no params are provided, an invalid changeset is returned 36 | with no validation performed. 37 | """ 38 | def changeset(model, params \\ :empty) do 39 | model 40 | |> cast(params, @required_fields, @optional_fields) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /web/models/package.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Package do 2 | use HexFaktor.Web, :model 3 | 4 | schema "packages" do 5 | field :name, :string 6 | field :source, :string 7 | field :source_url, :string 8 | 9 | field :language, :string 10 | field :description, :string 11 | 12 | field :releases, {:array, :map} 13 | 14 | # these are the current_user's project dependent on the package 15 | # set optionally in the controller if user is logged in 16 | field :dependent_projects_by_current_user, {:array, :any}, virtual: true 17 | 18 | timestamps 19 | 20 | belongs_to :project, HexFaktor.Project 21 | end 22 | 23 | @required_fields ~w(name source) 24 | @optional_fields ~w(source_url language description releases project_id) 25 | 26 | @doc """ 27 | Creates a changeset based on the `model` and `params`. 28 | 29 | If no params are provided, an invalid changeset is returned 30 | with no validation performed. 31 | """ 32 | def changeset(model, params \\ :empty) do 33 | model 34 | |> cast(params, @required_fields, @optional_fields) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /web/models/package_user_settings.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PackageUserSettings do 2 | use HexFaktor.Web, :model 3 | 4 | schema "package_user_settings" do 5 | field :user_id, :integer 6 | field :package_id, :integer 7 | 8 | field :notifications_for_major, :boolean 9 | field :notifications_for_minor, :boolean 10 | field :notifications_for_patch, :boolean 11 | field :notifications_for_pre, :boolean 12 | 13 | field :email_enabled, :boolean 14 | 15 | timestamps 16 | end 17 | 18 | @required_fields ~w(user_id package_id) 19 | @optional_fields ~w(notifications_for_major notifications_for_minor notifications_for_patch notifications_for_pre email_enabled) 20 | 21 | @doc """ 22 | Creates a changeset based on the `model` and `params`. 23 | 24 | If no params are provided, an invalid changeset is returned 25 | with no validation performed. 26 | """ 27 | def changeset(model, params \\ :empty) do 28 | model 29 | |> cast(params, @required_fields, @optional_fields) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /web/models/project.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Project do 2 | use HexFaktor.Web, :model 3 | 4 | schema "projects" do 5 | field :active, :boolean 6 | 7 | field :uid, :integer 8 | field :provider, :string 9 | 10 | field :name, :string 11 | field :html_url, :string 12 | field :clone_url, :string 13 | field :default_branch, :string 14 | field :language, :string 15 | field :fork, :boolean 16 | 17 | field :last_github_sync, Ecto.DateTime 18 | field :latest_build_job_id, :integer 19 | 20 | field :use_lock_file, :boolean 21 | 22 | field :outdated_deps, {:array, :any}, virtual: true 23 | 24 | timestamps 25 | 26 | belongs_to :git_repo, Refaktor.Builder.Model.GitRepo 27 | has_many :git_repo_branches, through: [:git_repo, :branches], on_delete: :delete_all 28 | has_many :builds, through: [:git_repo, :builds], on_delete: :delete_all 29 | has_many :project_hooks, HexFaktor.ProjectHook, on_delete: :delete_all 30 | has_many :project_users, HexFaktor.ProjectUser, on_delete: :delete_all 31 | has_many :project_user_settings, HexFaktor.ProjectUserSettings, on_delete: :delete_all 32 | end 33 | 34 | @required_fields ~w(uid provider name clone_url active default_branch) 35 | @optional_fields ~w(git_repo_id language last_github_sync html_url fork latest_build_job_id use_lock_file) 36 | 37 | @doc """ 38 | Creates a changeset based on the `model` and `params`. 39 | 40 | If no params are provided, an invalid changeset is returned 41 | with no validation performed. 42 | """ 43 | def changeset(model, params \\ :empty) do 44 | model 45 | |> cast(params, @required_fields, @optional_fields) 46 | |> unique_constraint(:uid, name: :projects_unique_index) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /web/models/project_hook.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectHook do 2 | use HexFaktor.Web, :model 3 | 4 | schema "project_hooks" do 5 | field :project_id, :integer 6 | field :provider, :string 7 | field :uid, :string 8 | field :active, :boolean, default: false 9 | 10 | timestamps 11 | end 12 | 13 | @required_fields ~w(project_id provider uid) 14 | @optional_fields ~w(active) 15 | 16 | @doc """ 17 | Creates a changeset based on the `model` and `params`. 18 | 19 | If no params are provided, an invalid changeset is returned 20 | with no validation performed. 21 | """ 22 | def changeset(model, params \\ :empty) do 23 | model 24 | |> cast(params, @required_fields, @optional_fields) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /web/models/project_user.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectUser do 2 | use HexFaktor.Web, :model 3 | 4 | alias HexFaktor.User 5 | alias HexFaktor.Project 6 | 7 | schema "project_users" do 8 | timestamps 9 | 10 | belongs_to :project, Project 11 | belongs_to :user, User 12 | end 13 | 14 | @required_fields ~w(project_id user_id) 15 | @optional_fields ~w() 16 | 17 | @doc """ 18 | Creates a changeset based on the `model` and `params`. 19 | 20 | If no params are provided, an invalid changeset is returned 21 | with no validation performed. 22 | """ 23 | def changeset(model, params \\ :empty) do 24 | model 25 | |> cast(params, @required_fields, @optional_fields) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /web/models/project_user_settings.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectUserSettings do 2 | use HexFaktor.Web, :model 3 | 4 | schema "project_user_settings" do 5 | field :notification_branches, {:array, :string} 6 | field :email_enabled, :boolean, default: true 7 | 8 | timestamps 9 | 10 | belongs_to :project, Project 11 | belongs_to :user, User 12 | end 13 | 14 | @required_fields ~w(user_id project_id notification_branches email_enabled) 15 | @optional_fields ~w() 16 | 17 | @doc """ 18 | Creates a changeset based on the `model` and `params`. 19 | 20 | If no params are provided, an invalid changeset is returned 21 | with no validation performed. 22 | """ 23 | def changeset(model, params \\ :empty) do 24 | model 25 | |> cast(params, @required_fields, @optional_fields) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /web/models/user.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.User do 2 | use HexFaktor.Web, :model 3 | 4 | schema "users" do 5 | field :uid, :integer # 6 | field :provider, :string 7 | field :user_name, :string 8 | 9 | field :full_name, :string 10 | field :email, :string 11 | field :email_token, :string 12 | field :email_verified_at, Ecto.DateTime 13 | 14 | field :last_github_sync, Ecto.DateTime 15 | 16 | field :email_notification_frequency, :string 17 | field :email_newsletter, :boolean 18 | 19 | timestamps 20 | 21 | has_many :notifications, HexFaktor.Notification, on_delete: :delete_all 22 | has_many :project_users, HexFaktor.ProjectUser, on_delete: :delete_all 23 | end 24 | 25 | @required_fields ~w(uid provider user_name email_notification_frequency) 26 | @optional_fields ~w(full_name last_github_sync email email_verified_at email_newsletter email_token) 27 | @valid_email_notification_frequency ~w(none daily weekly) 28 | 29 | @doc """ 30 | Creates a changeset based on the `model` and `params`. 31 | 32 | If no params are provided, an invalid changeset is returned 33 | with no validation performed. 34 | """ 35 | def changeset(model, params \\ :empty) do 36 | model 37 | |> cast(params, @required_fields, @optional_fields) 38 | |> validate_inclusion(:email_notification_frequency, @valid_email_notification_frequency) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /web/oauth/git_hub_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule GitHubAuth do 2 | @moduledoc """ 3 | An OAuth2 strategy for GitHub. 4 | """ 5 | use OAuth2.Strategy 6 | 7 | alias OAuth2.Strategy.AuthCode 8 | 9 | @github_access_token Application.get_env(:hex_faktor, :github_access_token) 10 | @github_client_id Application.get_env(:hex_faktor, :github_client_id) 11 | @github_client_secret Application.get_env(:hex_faktor, :github_client_secret) 12 | 13 | # Public API 14 | 15 | def access_token do 16 | OAuth2.AccessToken.new(@github_access_token, new()) 17 | |> IO.inspect 18 | end 19 | 20 | def new do 21 | OAuth2.Client.new([ 22 | strategy: __MODULE__, 23 | client_id: @github_client_id || System.get_env("CLIENT_ID"), 24 | client_secret: @github_client_secret || System.get_env("CLIENT_SECRET"), 25 | redirect_uri: System.get_env("REDIRECT_URI"), 26 | site: "https://api.github.com", 27 | authorize_url: "https://github.com/login/oauth/authorize", 28 | token_url: "https://github.com/login/oauth/access_token" 29 | ]) 30 | end 31 | 32 | def authorize_url!(params \\ []) do 33 | OAuth2.Client.authorize_url!(new(), params) 34 | end 35 | 36 | def user_auth(code) when is_binary(code) do 37 | # Exchange an auth code for an access token 38 | OAuth2.Client.get_token!(new(), code: code) 39 | end 40 | 41 | def get_token!(params \\ [], _headers \\ []) do 42 | OAuth2.Client.get_token!(new(), params) 43 | end 44 | 45 | # Strategy Callbacks 46 | 47 | def authorize_url(client, params) do 48 | AuthCode.authorize_url(client, params) 49 | end 50 | 51 | def get_token(client, params, headers) do 52 | client 53 | |> put_header("Accept", "application/json") 54 | |> put_header("Content-Type", "application/json") 55 | |> AuthCode.get_token(params, headers) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /web/oauth/hex_sonar.ex: -------------------------------------------------------------------------------- 1 | defmodule HexSonar do 2 | alias Refaktor.Util.JSON 3 | 4 | @server_and_port Application.get_env(:hex_faktor, :hex_server) 5 | @url "#{@server_and_port}/api/packages/:name" 6 | 7 | def load(name) do 8 | load_json(name) 9 | end 10 | 11 | defp load_json(name) do 12 | url = @url |> String.replace(":name", name |> to_string) 13 | case load_url(url) do 14 | {:ok, map} -> map 15 | {:error, error} -> IO.inspect error 16 | end 17 | end 18 | 19 | defp load_url(url) do 20 | # IO.puts "Getting url: #{url}" 21 | case HTTPoison.get(url) do 22 | {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> 23 | {:ok, JSON.parse(body)} 24 | {:ok, %HTTPoison.Response{status_code: 404}} -> 25 | {:error, 404} 26 | {:error, %HTTPoison.Error{reason: reason}} -> 27 | {:error, reason} 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /web/persistence/app_event_log.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Persistence.AppEventLog do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias HexFaktor.AppEventLog 6 | 7 | def all do 8 | Repo.all(AppEventLog) 9 | end 10 | 11 | def create(current_user, key, params) do 12 | user_id = current_user && current_user.id 13 | %AppEventLog{ 14 | user_id: user_id, 15 | key: key, 16 | value: params 17 | } |> Repo.insert! 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /web/persistence/project_hook.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Persistence.ProjectHook do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias HexFaktor.ProjectHook 6 | 7 | def set(project_id, github_hook_id, github_hook_active) do 8 | case find(project_id, github_hook_id) do 9 | nil -> add(project_id, github_hook_id, github_hook_active) 10 | val -> 11 | params = %{active: github_hook_active} 12 | changeset = ProjectHook.changeset val, params 13 | Repo.update!(changeset) 14 | end 15 | end 16 | 17 | def active?(project_id) do 18 | case find(project_id) do 19 | nil -> false 20 | val -> val.active 21 | end 22 | end 23 | 24 | def find(project_id) do 25 | query = from r in ProjectHook, 26 | where: r.project_id == ^project_id and 27 | r.provider == "github", 28 | select: r, 29 | limit: 1 30 | Repo.one(query) 31 | end 32 | def find(project_id, github_hook_id) do 33 | uid = to_string(github_hook_id) 34 | query = from r in ProjectHook, 35 | where: r.project_id == ^project_id and 36 | r.provider == "github" and r.uid == ^uid, 37 | select: r 38 | Repo.one(query) 39 | end 40 | 41 | def remove(project_id, github_hook_id) do 42 | case find(project_id, github_hook_id) do 43 | nil -> nil 44 | val -> Repo.delete!(val) 45 | end 46 | end 47 | 48 | defp add(project_id, github_hook_id, github_hook_active) do 49 | attributes = %{ 50 | "project_id" => project_id, 51 | "provider" => "github", 52 | "uid" => to_string(github_hook_id), 53 | "active" => github_hook_active 54 | } 55 | %ProjectHook{} 56 | |> ProjectHook.changeset(attributes) 57 | |> Repo.insert! 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /web/persistence/project_user.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Persistence.ProjectUser do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias HexFaktor.ProjectUser 6 | 7 | def ensure(project_id, user_id) do 8 | case find(project_id, user_id) do 9 | nil -> add(project_id, user_id) 10 | val -> val 11 | end 12 | end 13 | 14 | def remove(project_id, user_id) do 15 | case find(project_id, user_id) do 16 | nil -> nil 17 | val -> Repo.delete!(val) 18 | end 19 | end 20 | 21 | def find(project_id, user_id) do 22 | query = from u in ProjectUser, 23 | where: u.project_id == ^project_id and u.user_id == ^user_id, 24 | select: u 25 | Repo.one(query) 26 | end 27 | 28 | def find_by_user_id(user_id) do 29 | query = from u in ProjectUser, 30 | where: u.user_id == ^user_id, 31 | select: u 32 | Repo.all(query) 33 | end 34 | 35 | def find_user_ids_by_project_id(project_id) do 36 | query = from r in ProjectUser, 37 | select: r.user_id, 38 | where: r.project_id == ^project_id 39 | Repo.all(query) 40 | end 41 | 42 | defp add(project_id, user_id) do 43 | attributes = %{ 44 | "user_id" => user_id, 45 | "project_id" => project_id, 46 | } 47 | %ProjectUser{} 48 | |> ProjectUser.changeset(attributes) 49 | |> Repo.insert! 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /web/persistence/project_user_settings.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Persistence.ProjectUserSettings do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias HexFaktor.ProjectUserSettings 6 | 7 | def ensure(project_id, user_id, default_branch) do 8 | case find(project_id, user_id) do 9 | nil -> add(project_id, user_id, default_branch) 10 | val -> val 11 | end 12 | end 13 | 14 | def find(project_id, user_id) do 15 | query = from u in ProjectUserSettings, 16 | where: u.project_id == ^project_id and u.user_id == ^user_id, 17 | select: u 18 | Repo.one(query) 19 | end 20 | 21 | def find_by_project_id(project_id) do 22 | query = from u in ProjectUserSettings, 23 | where: u.project_id == ^project_id, 24 | select: u 25 | Repo.all(query) 26 | end 27 | 28 | def find_by_user_id(user_id) do 29 | query = from u in ProjectUserSettings, 30 | where: u.user_id == ^user_id, 31 | select: u 32 | Repo.all(query) 33 | end 34 | 35 | defp add(project_id, user_id, default_branch) do 36 | attributes = %{ 37 | "user_id" => user_id, 38 | "project_id" => project_id, 39 | "notification_branches" => [default_branch], 40 | "email_enabled" => true 41 | } 42 | %ProjectUserSettings{} 43 | |> ProjectUserSettings.changeset(attributes) 44 | |> Repo.insert! 45 | end 46 | 47 | def update_attributes(project_user_settings, attributes) do 48 | changeset = ProjectUserSettings.changeset project_user_settings, attributes 49 | if changeset.valid?, do: Repo.update!(changeset) 50 | changeset 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /web/persistence/user.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Persistence.User do 2 | import Ecto.Query, only: [from: 2] 3 | 4 | alias HexFaktor.Repo 5 | alias HexFaktor.User 6 | 7 | def create_from_auth_params(user_auth_params) do 8 | %User{ 9 | uid: user_auth_params["id"], 10 | full_name: user_auth_params["name"], 11 | user_name: user_auth_params["login"], 12 | email: user_auth_params["email"], 13 | provider: "github", 14 | email_notification_frequency: "weekly", 15 | email_newsletter: true 16 | } |> Repo.insert! 17 | end 18 | 19 | def find_by_verified_email_frequency(frequency) do 20 | query = from u in User, 21 | where: not is_nil(u.email_verified_at) and 22 | u.email_notification_frequency == ^frequency, 23 | select: u 24 | Repo.all(query) 25 | end 26 | 27 | def find_by_email_and_token(email, email_token) do 28 | query = from u in User, 29 | where: u.email == ^email and u.email_token == ^email_token, 30 | select: u 31 | Repo.one(query) 32 | end 33 | 34 | def find_by_id(user_id) do 35 | case user_id do 36 | nil -> nil 37 | _ -> Repo.get(User, user_id) 38 | end 39 | end 40 | 41 | def find_by_user_name(user_name, provider \\ "github") do 42 | query = from u in User, 43 | where: u.user_name == ^user_name and u.provider == ^provider, 44 | select: u 45 | Repo.one(query) 46 | end 47 | 48 | def update_last_github_sync(user) do 49 | attributes = %{last_github_sync: :calendar.universal_time} 50 | changeset = User.changeset user, attributes 51 | Repo.update!(changeset) 52 | end 53 | 54 | def update_attributes(user, attributes) do 55 | changeset = User.changeset user, attributes 56 | if changeset.valid?, do: Repo.update!(changeset) 57 | changeset 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /web/plugs/assign_current_user_info.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Plugs.AssignCurrentUserInfo do 2 | @moduledoc """ 3 | """ 4 | use Phoenix.Controller 5 | 6 | alias HexFaktor.Persistence.User 7 | alias HexFaktor.Persistence.Notification 8 | 9 | @socket_token_salt Application.get_env(:hex_faktor, :salt_user_socket) 10 | 11 | def init(default), do: default 12 | 13 | # Fetch the current user from the session and add it to `conn.assigns`. This 14 | # will allow you to have access to the current user in your views with 15 | # `@current_user`. 16 | def call(conn, _) do 17 | if user_id = get_session(conn, :current_user_id) do 18 | current_user = User.find_by_id(user_id) 19 | oauth_token = get_session(conn, :current_oauth_token) 20 | socket_token = 21 | if current_user do 22 | Phoenix.Token.sign(conn, @socket_token_salt, current_user.id) 23 | end 24 | notification_count = 25 | if current_user do 26 | Notification.count_unseen_for(current_user) 27 | end 28 | 29 | conn 30 | |> assign(:current_user, current_user) 31 | |> assign(:current_oauth_token, oauth_token) 32 | |> assign(:user_socket_token, socket_token) 33 | |> assign(:notification_count, notification_count) 34 | else 35 | conn 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /web/plugs/logged_in.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.Plugs.LoggedIn do 2 | @moduledoc """ 3 | """ 4 | use HexFaktor.Web, :controller 5 | alias HexFaktor.Auth 6 | 7 | def init(default), do: default 8 | 9 | def call(conn, _opts) do 10 | case Auth.current_user(conn) do 11 | nil -> redirect(conn, to: "/") |> halt 12 | _ -> conn 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /web/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/favicon.ico -------------------------------------------------------------------------------- /web/static/assets/fonts/mfglabsiconset-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/fonts/mfglabsiconset-webfont.eot -------------------------------------------------------------------------------- /web/static/assets/fonts/mfglabsiconset-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/fonts/mfglabsiconset-webfont.ttf -------------------------------------------------------------------------------- /web/static/assets/fonts/mfglabsiconset-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/fonts/mfglabsiconset-webfont.woff -------------------------------------------------------------------------------- /web/static/assets/images/badge-explanation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/images/badge-explanation.png -------------------------------------------------------------------------------- /web/static/assets/images/logo-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/images/logo-email.png -------------------------------------------------------------------------------- /web/static/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/assets/images/logo.png -------------------------------------------------------------------------------- /web/static/assets/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /web/static/css/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/static/css/app.css -------------------------------------------------------------------------------- /web/static/css/base.forms.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | label, 4 | input { 5 | font-size: 1rem; 6 | } 7 | -------------------------------------------------------------------------------- /web/static/css/base.layout.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | /* 4 | * Body resets 5 | * 6 | * Update the foundational and global aspects of the page. 7 | */ 8 | 9 | * { 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | box-sizing: border-box; 13 | } 14 | 15 | html, 16 | body { 17 | margin: 0; 18 | padding: 0; 19 | font-family: Helvetica, Arial, sans-serif; 20 | } 21 | 22 | .content__inner { 23 | position: relative; 24 | } 25 | 26 | .footer, 27 | .content { 28 | margin-left: auto; 29 | margin-right: auto; 30 | padding: 2.5rem 1rem 1rem 1rem; 31 | } 32 | .footer { 33 | font-size: 14px; 34 | color: #aaa; 35 | border-top: 2px solid #eee; 36 | text-align: center; 37 | } 38 | 39 | @include tablet_portrait_and_bigger { 40 | .content, 41 | .footer { 42 | max-width: 740px; 43 | } 44 | } 45 | 46 | @include tablet_landscape_and_bigger { 47 | .content, 48 | .footer { 49 | margin-left: 16rem; 50 | padding-left: 3rem; 51 | padding-right: 1rem; 52 | } 53 | } 54 | 55 | @include desktop_and_bigger { 56 | .content, 57 | .footer { 58 | margin-left: 20rem; 59 | max-width: 890px; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /web/static/css/component.alert.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .alert { 4 | position: relative; 5 | display:block; 6 | margin-top: 1rem; 7 | padding: 0.5rem 1rem; 8 | border-radius: 4px; 9 | } 10 | 11 | .alert--default { 12 | background: #ffa; 13 | } 14 | 15 | .alert--danger { 16 | background: #fcc; 17 | color: #c00; 18 | } 19 | 20 | .alert--info { 21 | background: #eee; 22 | color: #1A2642; 23 | } 24 | 25 | .alert--tutorial { 26 | background: #fff3d0; 27 | border-right: 2px solid #ffe7a0; 28 | border-bottom: 2px solid #ffe7a0; 29 | border-radius: 0; 30 | margin-top: 0; 31 | 32 | &:after { 33 | display: inline-block; 34 | position: absolute; 35 | z-index: 50; 36 | top: 0; 37 | right: 0; 38 | content: "HINT"; 39 | padding: 5px 10px; 40 | font-size: 12px; 41 | color: #ffaa00; 42 | background: #ffe7a0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /web/static/css/component.bullet.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .bullet { 4 | display: inline-block; 5 | width: 8px; 6 | height: 8px; 7 | margin-bottom: 2px; 8 | border-radius: 25%; 9 | } 10 | .bullet--outdated-prod { 11 | background-color: #FF911D; 12 | } 13 | .bullet--outdated-not-prod { 14 | background-color: #FFE100; 15 | } 16 | .bullet--unknown { 17 | background-color: #ccc; 18 | } 19 | -------------------------------------------------------------------------------- /web/static/css/component.dropdown.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .dropdown { 4 | position: relative; 5 | text-align: left; 6 | } 7 | .dropdown__label { 8 | cursor: pointer; 9 | } 10 | .dropdown__menu { 11 | display: none; 12 | min-width: 150px; 13 | @include clearAfter; 14 | } 15 | .dropdown__checkbox:checked ~ .dropdown__menu { 16 | display: block; 17 | } 18 | /* Hide the branch checkbox that we toggle with `.project-title__branch-toggle` */ 19 | .dropdown__checkbox { 20 | display: none; 21 | } 22 | 23 | .dropdown__menu--default { 24 | position: absolute; 25 | z-index: 50; 26 | right: 0; 27 | background: #f1f1f1; 28 | border: 1px solid #eee; 29 | border-radius: 3px; 30 | } 31 | 32 | .dropdown__item { 33 | padding: 5px 10px; 34 | } 35 | .dropdown__item--active { 36 | font-weight: bold; 37 | } 38 | 39 | .dropdown__triangle { 40 | width: 0; 41 | height: 0; 42 | border-style: solid; 43 | border-width: 0 10px 10px 10px; 44 | border-color: transparent transparent #f1f1f1 transparent; 45 | margin-top: -10px; 46 | margin-left: 50%; 47 | } 48 | 49 | .dropdown--follow { 50 | .dropdown__menu { 51 | min-width: 300px; 52 | } 53 | } -------------------------------------------------------------------------------- /web/static/css/component.form-section.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .form-section { 4 | padding-top: 1rem; 5 | margin-bottom: 1rem; 6 | } 7 | 8 | .form-section__title { 9 | text-rendering: optimizeLegibility; 10 | margin-top: 1rem; 11 | margin-bottom: 1rem; 12 | font-size: 1.25rem; 13 | line-height: 2rem; 14 | font-weight: bold; 15 | color: #313131; 16 | border-bottom: 3px solid #eee; 17 | 18 | @include clearAfter; 19 | } 20 | 21 | .form-section__input { 22 | input[type=text] { 23 | display: block; 24 | width: 100%; 25 | margin-bottom: 1rem; 26 | line-height: 2; 27 | color: #313131; 28 | border: 2px solid #eee; 29 | border-radius: 4px; 30 | background: #f5f5f5; 31 | padding: 3px; 32 | } 33 | } 34 | .form-section__input--checkbox { 35 | margin-bottom: 1rem; 36 | 37 | label { 38 | display: inline-block; 39 | } 40 | } 41 | .form-section__input--radio { 42 | margin-bottom: 1rem; 43 | 44 | label { 45 | display: inline-block; 46 | } 47 | } 48 | .form-section__label { 49 | display: block; 50 | font-weight: bold; 51 | } 52 | .form-section__label--no-frequency, 53 | .form-section__label--newsletter { 54 | font-weight: normal; 55 | margin-top: 10px; 56 | } 57 | .form-section__label--daily-frequency { 58 | } 59 | .form-section__label--weekly-frequency { 60 | margin-bottom: 10px; 61 | } 62 | 63 | @include phablet_landscape_and_bigger { 64 | .form-section__input { 65 | input[type=text] { 66 | display: inline-block; 67 | min-width: 20rem; 68 | width: auto; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /web/static/css/component.label.scss: -------------------------------------------------------------------------------- 1 | .label { 2 | background: #ddd; 3 | display: inline-block; 4 | padding: 1px 3px; 5 | border-radius: 3px; 6 | font-size: 10px; 7 | } 8 | 9 | .label--pre-version-requirement { 10 | background: #5B3F67; 11 | color: #fff; 12 | 13 | display: none; 14 | } 15 | -------------------------------------------------------------------------------- /web/static/css/component.legend.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .legend { 4 | font-size: 0.9rem; 5 | margin-top: 1rem; 6 | } 7 | .legend__headline { 8 | font-weight: bold; 9 | } 10 | .legend__item { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /web/static/css/component.motd-banner.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .motd-banner { 4 | background: #ffe; 5 | border-bottom: 1px solid #fa0; 6 | padding: 4px; 7 | padding-top: 45px; 8 | } 9 | @include tablet_landscape_and_bigger { 10 | .motd-banner { 11 | position: fixed; 12 | z-index: 50; 13 | left: 0; 14 | top: 0; 15 | width: 100%; 16 | padding: 4px; 17 | padding-left: 17rem; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/static/css/component.notification-item.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .notification-item { 4 | border: 1px solid #fff; 5 | border-radius: 3px; 6 | padding: 10px; 7 | } 8 | .notification-item:after { 9 | content: ""; 10 | display: block; 11 | clear: both; 12 | } 13 | .notification-item__icon { 14 | position: absolute; 15 | z-index: 100; 16 | } 17 | .notification-item__icon--package { 18 | a { 19 | color: #268bd2; 20 | &:hover { 21 | text-decoration: none; 22 | } 23 | } 24 | } 25 | .notification-item__icon--project { 26 | a { 27 | color: #e8bc54; 28 | &:hover { 29 | text-decoration: none; 30 | } 31 | } 32 | } 33 | .notification-item__icon--seen { 34 | color: #aaa; 35 | opacity: 0.1; 36 | } 37 | .notification-item__text, 38 | .notification-item__effect, 39 | .notification-item__time { 40 | margin-left: 3rem; 41 | } 42 | .notification-item__time { 43 | font-size: 0.75rem; 44 | color: #aaa; 45 | } 46 | .notification-item__text code { 47 | background: #FFF184; 48 | } 49 | .notification-item__env-link--prod code { 50 | background: #F9BE33; 51 | color: #fff; 52 | } 53 | -------------------------------------------------------------------------------- /web/static/css/component.notification-title.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .notification-title { 4 | text-rendering: optimizeLegibility; 5 | margin-top: 1rem; 6 | margin-bottom: 1rem; 7 | font-size: 2rem; 8 | font-weight: bold; 9 | line-height: 2rem; 10 | color: #313131; 11 | 12 | @include clearAfter; 13 | } 14 | .notification-title__branch { 15 | display: inline-block; 16 | float: right; 17 | font-size: 1rem; 18 | font-weight: normal; 19 | color: #666; 20 | } 21 | .notification-title__btn-group { 22 | float: right; 23 | } 24 | 25 | 26 | 27 | .notification-toolbar { 28 | text-rendering: optimizeLegibility; 29 | margin-top: 1rem; 30 | margin-bottom: 2rem; 31 | color: #313131; 32 | 33 | @include clearAfter; 34 | } 35 | .notification-toolbar__btn-group-left { 36 | width: 70%; 37 | float: left; 38 | } 39 | .notification-toolbar__btn-group-right { 40 | width: 40%; 41 | float: right; 42 | text-align: right; 43 | 44 | form { 45 | display: inline; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /web/static/css/component.package-title.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | $color: $headline_color; 4 | 5 | .package-title { 6 | text-rendering: optimizeLegibility; 7 | margin-top: 1rem; 8 | margin-bottom: 1rem; 9 | font-size: 1.5rem; 10 | font-weight: bold; 11 | line-height: 2rem; 12 | color: $color; 13 | 14 | @include clearAfter; 15 | 16 | @include phablet_landscape_and_bigger { 17 | font-size: 2rem; 18 | } 19 | 20 | a, 21 | a:hover { 22 | color: $color; 23 | text-decoration: none; 24 | } 25 | } 26 | .package-title__btn-group { 27 | float: right; 28 | } 29 | 30 | .package-toolbar { 31 | text-rendering: optimizeLegibility; 32 | margin-top: 1rem; 33 | margin-bottom: 2rem; 34 | color: $color; 35 | 36 | @include clearAfter; 37 | } 38 | .package-toolbar__sync-hint { 39 | display: block; 40 | } 41 | .package-toolbar__btn-group-left { 42 | } 43 | .package-toolbar__btn-group-right { 44 | } 45 | 46 | @include phablet_landscape_and_bigger { 47 | .package-toolbar__btn-group-left { 48 | width: 70%; 49 | float: left; 50 | } 51 | .package-toolbar__btn-group-right { 52 | width: 30%; 53 | float: right; 54 | text-align: right; 55 | } 56 | } -------------------------------------------------------------------------------- /web/static/css/component.project-actions.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .project-actions { 4 | @include clearAfter; 5 | 6 | border-top: 3px solid #eee; 7 | } 8 | 9 | .project-actions__link { 10 | float: left; 11 | } 12 | .project-actions__link--right { 13 | float: right; 14 | } 15 | .project-actions__link a { 16 | padding: 0.6rem 1rem; 17 | display: block; 18 | border-top: 3px solid transparent; 19 | margin-top: -3px; 20 | } 21 | 22 | .project-actions__link--label { 23 | color: #666; 24 | border-top: 3px solid #999; 25 | padding: 0.6rem 1rem 0.6rem 0; 26 | display: block; 27 | border-top: 3px solid transparent; 28 | margin-top: -3px; 29 | display: none; 30 | } 31 | 32 | .project-actions__link--search input[type=text] { 33 | border: none; 34 | padding: 0.6rem 0; 35 | line-height: 1.5; 36 | margin-top: -3px; 37 | text-align: right; 38 | &:focus { 39 | outline: none; 40 | } 41 | } 42 | .project-actions__link--search input[type=submit] { 43 | display: none; 44 | } 45 | -------------------------------------------------------------------------------- /web/static/css/component.project-title.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | $color: $headline_color; 4 | 5 | .project-title { 6 | text-rendering: optimizeLegibility; 7 | margin-top: 1rem; 8 | margin-bottom: 1rem; 9 | font-size: 1.5rem; 10 | font-weight: bold; 11 | line-height: 2rem; 12 | color: $color; 13 | 14 | @include clearAfter; 15 | 16 | @include phablet_landscape_and_bigger { 17 | font-size: 2rem; 18 | } 19 | 20 | a, 21 | a:hover { 22 | color: $color; 23 | text-decoration: none; 24 | } 25 | } 26 | .project-title__badge-container { 27 | display: inline; 28 | white-space: nowrap; 29 | } 30 | .project-title__branch { 31 | display: inline-block; 32 | float: right; 33 | font-size: 1rem; 34 | font-weight: normal; 35 | color: $text_color; 36 | } 37 | .project-title__btn-group { 38 | float: right; 39 | } 40 | 41 | 42 | .project-title__branch-toggle:active, 43 | #project-title__branch-checkbox:checked ~ .project-title__branch-toggle { 44 | opacity: 0.6; 45 | } 46 | 47 | 48 | 49 | .project-toolbar { 50 | text-rendering: optimizeLegibility; 51 | margin-top: 1rem; 52 | margin-bottom: 2rem; 53 | color: $color; 54 | 55 | @include clearAfter; 56 | } 57 | .project-toolbar__sync-hint { 58 | display: block; 59 | } 60 | .project-toolbar__btn-group-left { 61 | } 62 | .project-toolbar__btn-group-right { 63 | } 64 | 65 | @include phablet_landscape_and_bigger { 66 | .project-toolbar__btn-group-left { 67 | width: 70%; 68 | float: left; 69 | } 70 | .project-toolbar__btn-group-right { 71 | width: 30%; 72 | float: right; 73 | text-align: right; 74 | } 75 | } -------------------------------------------------------------------------------- /web/static/css/component.sync-progress.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | .sync-progress { 4 | position: relative; 5 | color: #eee; 6 | background-color: #515151; 7 | border-radius: 4px; 8 | text-shadow: 1px 1px 1px #000; 9 | 10 | a { 11 | color: #fff; 12 | text-decoration: underline; 13 | } 14 | } 15 | .sync-progress__bar { 16 | background-color: #454F56; 17 | border-radius: 4px; 18 | height: 48px; 19 | } 20 | .sync-progress__inner { 21 | position: absolute; 22 | padding: 10px; 23 | } 24 | 25 | .sync-progress--loading .sync-progress__bar { 26 | background-size: 90px 30px; 27 | background-image: linear-gradient(90deg, rgba(255, 255, 255, .15) 25%, transparent 25%, 28 | transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, 29 | transparent 75%, transparent); 30 | 31 | animation: animate-stripes-slow 2s linear infinite; 32 | } 33 | 34 | .sync-progress--building .sync-progress__bar { 35 | background-size: 90px 30px; 36 | background-image: linear-gradient(90deg, rgba(255, 255, 255, .15) 25%, transparent 25%, 37 | transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, 38 | transparent 75%, transparent); 39 | 40 | animation: animate-stripes-slow-rtl 2s linear infinite; 41 | } 42 | 43 | @keyframes animate-stripes-slow { 44 | 0% {background-position: 0 0;} 100% {background-position: 90px 0;} 45 | } 46 | @keyframes animate-stripes-slow-rtl { 47 | 0% {background-position: 90px 0;} 100% {background-position: 0 0;} 48 | } 49 | -------------------------------------------------------------------------------- /web/static/css/component.user-title.scss: -------------------------------------------------------------------------------- 1 | @import "_config"; 2 | 3 | $color: $headline_color; 4 | 5 | .user-title { 6 | text-rendering: optimizeLegibility; 7 | margin-top: 1rem; 8 | margin-bottom: 1rem; 9 | font-size: 1.5rem; 10 | font-weight: bold; 11 | line-height: 2rem; 12 | color: $color; 13 | 14 | @include clearAfter; 15 | 16 | @include phablet_landscape_and_bigger { 17 | font-size: 2rem; 18 | } 19 | 20 | a, 21 | a:hover { 22 | color: $color; 23 | text-decoration: none; 24 | } 25 | } 26 | .user-title__sync-hint { 27 | display: inline-block; 28 | float: right; 29 | font-size: 1rem; 30 | font-weight: normal; 31 | color: $text_color; 32 | } 33 | -------------------------------------------------------------------------------- /web/static/css/generic.snippets.scss: -------------------------------------------------------------------------------- 1 | 2 | .snippet-comment { 3 | color: #969696; 4 | } 5 | -------------------------------------------------------------------------------- /web/static/css/keep.scss: -------------------------------------------------------------------------------- 1 | @import "config"; 2 | 3 | .user-email--not-verified { 4 | color: #c00; 5 | } 6 | .user-email--verified { 7 | color: #5C913B; 8 | } 9 | 10 | .package-follow { 11 | background: #EEF3E7; 12 | padding: 10px; 13 | margin-top: -1.5rem; 14 | border-radius: 5px; 15 | 16 | } 17 | .package-follow code { 18 | color: #000; 19 | background: #fff; 20 | } 21 | .package-follow__triangle { 22 | width: 0; 23 | height: 0; 24 | border-style: solid; 25 | border-width: 0 10px 10px 10px; 26 | border-color: transparent transparent #f1f1f1 transparent; 27 | margin-top: -20px; 28 | margin-left: 5%; 29 | } 30 | .package-follow__inner { 31 | padding-top: 10px; 32 | } 33 | 34 | @include tablet_portrait_and_bigger { 35 | .package-follow { 36 | position: absolute; 37 | z-index: 50; 38 | right: 0; 39 | width: 60%; 40 | } 41 | .package-follow__triangle { 42 | margin-left: 95%; 43 | } 44 | } 45 | 46 | // 47 | // panel 48 | // 49 | 50 | .panel { 51 | position: fixed; 52 | z-index: 50; 53 | top: 0; 54 | height: 100%; 55 | width: 30%; 56 | background: #fff; 57 | border-left: 4px solid #f5f5f5; 58 | } 59 | .panel__inner { 60 | position: relative; 61 | padding: 20px; 62 | } 63 | .panel__close { 64 | position: absolute; 65 | z-index: 51; 66 | top: 0; 67 | right: 0; 68 | font-size: 32px; 69 | line-height: 32px; 70 | padding: 10px; 71 | color: #aaa; 72 | &:hover { 73 | color: #666; 74 | cursor: pointer; 75 | } 76 | } 77 | 78 | .panel--right { 79 | right: 0; 80 | } 81 | 82 | // 83 | // version/releases 84 | // 85 | 86 | .release__version { 87 | font-size: 1.125rem; 88 | 89 | i { 90 | opacity: 0.5; 91 | } 92 | } 93 | .release__date { 94 | color: #777; 95 | } 96 | .release--hidden { 97 | display: none; 98 | } 99 | 100 | .release--not-matching-requirement { 101 | .release__version { 102 | color: #ff911d; 103 | } 104 | } 105 | 106 | .version { 107 | color: #46b01e; 108 | } 109 | .version--pre { 110 | color: #aaa; 111 | } 112 | -------------------------------------------------------------------------------- /web/static/js/app.js: -------------------------------------------------------------------------------- 1 | // Brunch automatically concatenates all files in your 2 | // watched paths. Those paths can be configured at 3 | // config.paths.watched in "brunch-config.js". 4 | // 5 | // However, those files will only be executed if 6 | // explicitly imported. The only exception are files 7 | // in vendor, which are never wrapped in imports and 8 | // therefore are always executed. 9 | 10 | // Import dependencies 11 | // 12 | import "phoenix_html" 13 | 14 | // Import local files 15 | // 16 | // Local files can be imported directly using relative 17 | // paths "./socket" or full ones "web/static/js/socket". 18 | 19 | import socket from "./socket" 20 | import dom_init from "./dom_init" 21 | -------------------------------------------------------------------------------- /web/static/js/hex_faktor.js: -------------------------------------------------------------------------------- 1 | import jQuery from "./jquery" 2 | let $ = jQuery 3 | 4 | let faktor = { 5 | addLoadingClass: function(ele) { 6 | let $ele = $(ele); 7 | if( $ele.hasClass("btn") ) { 8 | $ele.addClass("btn--loading"); 9 | } else { 10 | $ele.addClass("loading"); 11 | } 12 | }, 13 | updateComponent: function(name, id) { 14 | var $dom_element, url; 15 | if( id ) { 16 | $dom_element = $("#"+name+"-"+id); 17 | url = "/component/"+name+"/"+id; 18 | } else { 19 | $dom_element = $("#"+name); 20 | url = "/component/"+name; 21 | } 22 | 23 | if( $dom_element.length > 0 ) { 24 | jQuery.ajax(url, 25 | { 26 | "method": "GET", 27 | "success": function(html) { 28 | $dom_element.replaceWith(html) 29 | } 30 | } 31 | ); 32 | } 33 | }, 34 | deactivateSyncRepoButton: function() { 35 | var $btn = $("a[data-syncing-label]"); 36 | if( !$btn.hasClass("btn--loading") ) { 37 | var label = $btn.data("syncing-label"); 38 | $btn 39 | .removeClass("btn--sync") 40 | .addClass("btn--sync") 41 | .addClass("btn--loading") 42 | .html(label); 43 | } 44 | }, 45 | ensureSyncProgressBar: function() { 46 | $(".sync-progress").show(); 47 | } 48 | } 49 | 50 | export default faktor 51 | -------------------------------------------------------------------------------- /web/static/js/meta.js: -------------------------------------------------------------------------------- 1 | let Meta = { 2 | get: function (name) { 3 | let tag = document.querySelector(`meta[name="${name}"]`); 4 | let value = tag && tag.content; 5 | if( value == "" || !value ) value = null; 6 | return value; 7 | } 8 | } 9 | 10 | export default Meta; 11 | -------------------------------------------------------------------------------- /web/static/js/project_channel.js: -------------------------------------------------------------------------------- 1 | import jQuery from "./jquery" 2 | let $ = jQuery 3 | import Meta from "./meta" 4 | 5 | export function init(socket) { 6 | let project_id = Meta.get("hf:project_id"); 7 | if( project_id ) { 8 | let channel = socket.channel("projects:"+project_id, {}) 9 | channel.join() 10 | .receive("ok", resp => { console.log("Joined successfully", channel.topic) }) 11 | .receive("error", resp => { console.log("Unable to join", channel.topic, resp) }) 12 | 13 | channel.on("project.build", resp => { 14 | console.log(" project.build", resp); 15 | 16 | var text = ""; 17 | if( resp.status == "scheduling" ) { 18 | text = "Scheduling worker ..."; 19 | } else if( resp.status == "cloning" ) { 20 | text = "Cloning repo ..."; 21 | } else if( resp.status == "running" ) { 22 | text = "Running deps analysis ..."; 23 | } else if( resp.status == "success" ) { 24 | if( $("[data-auto-reload]").data("auto-reload") == "on-build-end" ) { 25 | window.location.reload(); 26 | } 27 | 28 | text = 'Build successful! Reload now!'; 29 | } else if( resp.status == "error" ) { 30 | text = "Build failed! Something went wrong ..."; 31 | } 32 | var sync_html = $('
> '+text+'
'); 33 | $("#sync-progress-inject").html(sync_html); 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /web/static/js/socket.js: -------------------------------------------------------------------------------- 1 | import {Socket} from "phoenix" 2 | import Meta from "./meta" 3 | 4 | import * as ProjectChannel from "./project_channel" 5 | import * as UserChannel from "./user_channel" 6 | 7 | let socket = new Socket("/socket", {params: {token: Meta.get("hf:user_token")}}) 8 | socket.connect() 9 | 10 | ProjectChannel.init(socket) 11 | UserChannel.init(socket) 12 | 13 | export default socket 14 | -------------------------------------------------------------------------------- /web/templates/component/_tab_hamburger.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | -------------------------------------------------------------------------------- /web/templates/component/alert.html.eex: -------------------------------------------------------------------------------- 1 | <%= if @text do %> 2 |
<%= @text %>
3 | <% end %> 4 | -------------------------------------------------------------------------------- /web/templates/component/notification-counter.html.eex: -------------------------------------------------------------------------------- 1 | 2 | <%= if @notification_count == 0 do %> 3 | <%= @notification_count %> 4 | <% else %> 5 | <%= @notification_count %> 6 | <% end %> 7 | Notifications 8 | 9 | -------------------------------------------------------------------------------- /web/templates/component/notification-item-package.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
"> 3 | 4 |
5 |
6 | <%= link @item_name, to: "/packages/#{@item_name}" %> was updated to 7 | <%= @notifications |> List.first |> notification_version %>. 8 |
9 |
10 | <%= @notifications |> List.first |> notification_time %> 11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /web/templates/component/notification-item-project.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
"> 3 | 4 |
5 |
6 | <%= link @item_name, to: "/github/#{@item_name}" %> has <%= Enum.count(@notifications) %> outdated deps in 7 | <%= for mix_env <- mix_envs_for_notifications(@notifications) do %> 8 | " class="notification-item__env-link--<%= mix_env %>"><%= mix_env %> 9 | <% end %> 10 |
11 |
12 | <%= @notifications |> List.first |> notification_time %> 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /web/templates/component/package-title.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= @package.name %> 3 | <%= if @package.source_url do %> 4 | 5 | <% end %> 6 |
7 | -------------------------------------------------------------------------------- /web/templates/component/project-list-item.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%= link_project @project %> 5 |
6 |
7 | <%= if @project.project_hooks |> Enum.any?(&(&1.active)) do %> 8 |
9 | ">monitored 10 |
11 | <% else %> 12 |
13 | ">monitor this 14 |
15 | <% end %> 16 | 17 |
18 | <%= if @project.latest_build_job_id do %> 19 | "> 20 | <% end %> 21 |
22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /web/templates/component/project-title.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= if @project.provider == "github" do %> 3 | <%= @project.name %> 4 | 5 | <% end %> 6 | 7 | <%= if @project.latest_build_job_id do %> 8 |
9 | "> 10 |
11 | <% end %> 12 | 13 | <%= if @branch do %> 14 | 15 | 19 | 20 | 39 | 40 | <% end %> 41 | 42 |
43 | 44 | <%= if @branch do %> 45 | <% end %> 46 | -------------------------------------------------------------------------------- /web/templates/component/tutorial-verify-email.html.eex: -------------------------------------------------------------------------------- 1 | <%= if logged_in?(@conn) do %> 2 | <%= if !current_user(@conn).email_verified_at do %> 3 |
4 | You will be able to receive notifications about updates via email.
5 | Verify your email by clicking the link we sent you (check email settings). 6 |
7 | <% end %> 8 | <% end %> 9 | -------------------------------------------------------------------------------- /web/templates/component/user-title.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= if @user.provider == "github" do %> 3 | <%= @user.user_name %> 4 | 5 | <% end %> 6 | 7 | 8 | <%= if @include_last_sync? do %> 9 | 10 | Last GitHub sync: 11 | <%= @user.last_github_sync %> 12 | 13 | <% end %> 14 |
15 | 16 | -------------------------------------------------------------------------------- /web/templates/email/_footer.html.eex: -------------------------------------------------------------------------------- 1 | HexFaktor.org: 2 | monitor your project's deps and get notified when they are updated. 3 |
4 | <%= if assigns[:unsubcribe_url] do %> 5 | You can easily unsubscribe from these emails in the settings. 6 | <% end %> 7 | -------------------------------------------------------------------------------- /web/templates/email/validation.html.eex: -------------------------------------------------------------------------------- 1 | Greetings from HexFaktor,
2 |
3 | if you want to get email notifications about your project's Hex dependencies, please click the link below to confirm your email address:
4 |
5 | <%= raw button_link("Verify my email address", @validation_url) %>
6 |
7 | Thanks!
8 | @rrrene from HexFaktor 9 | -------------------------------------------------------------------------------- /web/templates/error/403.html.eex: -------------------------------------------------------------------------------- 1 | <%= if HexFaktor.Auth.current_user(@conn) do %> 2 | Forbidden. 3 | <% else %> 4 |

5 | Seems like you need to be logged in for this one. 6 |

7 | 8 | Sign in 9 | <% end %> 10 | -------------------------------------------------------------------------------- /web/templates/error/404.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= if logged_in?(@conn) do %> 11 | 12 | 13 | <% end %> 14 | 15 | HexFaktor 16 | "> 17 | 18 | <%= if logged_in?(@conn) do %> 19 | 23 | <% end %> 24 | 25 | 26 | 27 | <%= render HexFaktor.LayoutView, "_sidebar.html", conn: @conn, current_user: assigns[:current_user] %> 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |

Page not found

36 | 37 |

38 | The page you were expecting could not be found. Since HexFaktor is still in beta, this might very well be a bug. 39 |

40 |

41 | If you think it is, you would help this project by reporting it on GitHub: 42 |

43 |

44 | hexfaktor/hex_faktor_web 45 |

46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 | <%= render HexFaktor.LayoutView, "_footer.html", conn: @conn %> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /web/templates/error/500.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= if logged_in?(@conn) do %> 11 | 12 | 13 | <% end %> 14 | 15 | HexFaktor 16 | "> 17 | 18 | <%= if logged_in?(@conn) do %> 19 | 23 | <% end %> 24 | 25 | 26 | 27 | <%= render HexFaktor.LayoutView, "_sidebar.html", conn: @conn, current_user: assigns[:current_user] %> 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |

Server internal error

36 | 37 |

38 | There was an internal server error. 39 |

40 |

41 | Since Hexfaktor is still in beta, you would help this project by reporting it on GitHub: 42 |

43 |

44 | hexfaktor/hex_faktor_web 45 |

46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 | 54 | <%= render HexFaktor.LayoutView, "_footer.html", conn: @conn %> 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /web/templates/help/badge.html.eex: -------------------------------------------------------------------------------- 1 |
2 | Badges: How do they work? 3 |
4 | 5 | <%= if !logged_in?(@conn) do %> 6 |
7 | Sign in to get your own badges! 8 |
9 | <% end %> 10 | 11 |

12 | The badge colors represent your Mix envs: 13 |

14 | 15 |

16 | " class="logo" /> 17 |

18 | 19 | Green means "All is well!" 20 | 21 | 29 | 30 | Not green means "Look out!" 31 | 32 | 40 | 41 |

42 | The basis for determining whether or not something is outdated is always the version requirement set in mix.exs. 43 |

44 | -------------------------------------------------------------------------------- /web/templates/help/index.html.eex: -------------------------------------------------------------------------------- 1 |
2 | Ayyyy, need Help! 3 |
4 | 5 |

6 | This section, like this whole service, is currently being built by volunteers: 7 |

8 | 11 |

12 | If you have any further questions, please create an issue on GitHub so we can expand this FAQ. 13 |

14 | -------------------------------------------------------------------------------- /web/templates/layout/_footer.html.eex: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /web/templates/layout/_motd-banner.html.eex: -------------------------------------------------------------------------------- 1 |
2 | HexFaktor is in beta and likely not bug-free yet. 3 | Please report any bugs you find. 4 |
5 | -------------------------------------------------------------------------------- /web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <%= if logged_in?(@conn) do %> 11 | 12 | 13 | 14 | <% end %> 15 | <%= if assigns[:project] do %> 16 | 17 | <% end %> 18 | 19 | HexFaktor 20 | "> 21 | 22 | 23 | <%= render HexFaktor.LayoutView, "_sidebar.html", conn: @conn, current_user: assigns[:current_user] %> 24 | <%= render HexFaktor.LayoutView, "_motd-banner.html", assigns %> 25 | 26 |
27 |
28 | <%= render @view_module, @view_template, assigns %> 29 |
30 |
31 | 32 | <%= render HexFaktor.LayoutView, "_footer.html", assigns %> 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /web/templates/layout/blank.html.eex: -------------------------------------------------------------------------------- 1 | <%= render @view_module, @view_template, assigns %> 2 | -------------------------------------------------------------------------------- /web/templates/notification/_tab-index.html.eex: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /web/templates/notification/_tab_nav_index.html.eex: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |
"> 7 | Unread 8 |
9 |
"> 10 | All notifications 11 |
12 |
13 |
14 | 15 | -------------------------------------------------------------------------------- /web/templates/notification/index.html.eex: -------------------------------------------------------------------------------- 1 | <%= render HexFaktor.ComponentView, "tutorial-verify-email.html", conn: @conn %> 2 | 3 |
4 | Notifications 5 |
6 | <%= link("Mark all as read", to: "/notifications/mark_all_as_read", class: "btn btn--default", method: "POST") %> 7 | 8 |
9 |
10 | 11 | <%= render HexFaktor.NotificationView, "_tab_nav_index.html", assigns %> 12 | 13 | 14 | <%= for {{kind_of, item_name, _project_id, _git_branch_id}, notifications} <- @notification_map do %> 15 | <%= render HexFaktor.ComponentView, "notification-item-#{kind_of}.html", current_user: @current_user, item_name: item_name, notifications: notifications %> 16 | <% end %> 17 | -------------------------------------------------------------------------------- /web/templates/package/_notification_settings.html.eex: -------------------------------------------------------------------------------- 1 | <%= if @info do %> 2 |
3 |
4 |
5 | <%= if @info == :follow do %> 6 |

7 | You will now receive notifications for this package each time a new stable version is published. 8 |

9 |

10 | Check your e-mail settings. 11 |

12 | <% end %> 13 | <%= if @info == :unfollow do %> 14 |

15 | Got it! No notifications for this package. 16 |

17 | <% end %> 18 |
19 |
20 | <% end %> 21 | -------------------------------------------------------------------------------- /web/templates/package/_tab-package_filter.html.eex: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/templates/package/_tab_nav_show_projects.html.eex: -------------------------------------------------------------------------------- 1 | 4 | 11 | 12 | 15 |
16 | 21 | 22 | 23 | 28 |
29 | -------------------------------------------------------------------------------- /web/templates/package/_versions.html.eex: -------------------------------------------------------------------------------- 1 |
2 | <%= for %{"version" => version, "updated_at" => updated_at} <- @shown_releases do %> 3 |
4 | <%= version_badge(version) %> 5 | <%= updated_at |> to_human_date %> 6 |
7 | <% end %> 8 | 9 | <%= for %{"version" => version, "updated_at" => updated_at} <- @hidden_releases do %> 10 |
11 | <%= version_badge(version) %> 12 | <%= updated_at |> to_human_date %> 13 |
14 | <% end %> 15 |
16 | 17 | <%= if @hidden_releases |> Enum.any? do %> 18 | Show all versions 19 | <% end %> 20 | -------------------------------------------------------------------------------- /web/templates/package/index.html.eex: -------------------------------------------------------------------------------- 1 |
Hex packages
2 | 3 |
4 |
5 | Browsing packages is fun! 6 |
7 |
8 | 9 | <%= render HexFaktor.PackageView, "_tab_nav_index.html", assigns %> 10 | 11 | <%= if @packages |> Enum.empty? do %> 12 |

13 | No packages in this list. 14 |

15 | <% else %> 16 | <%= for package <- @packages do %> 17 |
18 |
19 |
20 | <%= link package.name, to: package_path(@conn, :show, package.name) %> 21 |
22 | <%= package.releases |> current_version() %> 23 |
24 |
25 |
26 | <%= package.source %> 27 | 28 |
29 |
30 |
31 | <% end %> 32 | <% end %> 33 | -------------------------------------------------------------------------------- /web/templates/page/about.html.eex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hexfaktor/hex_faktor_web/97690e763a30f5fbf91405043efbfe698c6c1467/web/templates/page/about.html.eex -------------------------------------------------------------------------------- /web/templates/page/index.html.eex: -------------------------------------------------------------------------------- 1 |

2 | 3 |

ElixirStatus presents: HexFaktor

4 | 5 |

6 | This is a new service that wants to provide monitoring for your Hex deps. 7 |

8 | 9 | "> 10 | 11 |

Is this ready?

12 |

13 | Yes and no. I tested this version of the software for a couple of weeks myself, but now the code needs some "real world experience".

You can join the beta by signing in via GitHub: 14 |

15 | <%= unless logged_in?(@conn) do %> 16 |

17 | Join the Beta 18 |

19 | <% end %> 20 | 21 | 22 |

Thanks!

23 |

24 | This, like my other Elixir projects, would not have been possible without consistent support by our fantastic community. Thank you so much! 25 |

26 | 27 |
28 |


29 |

Wait! Who are you?

30 |

31 | I'm @rrrene. I've been around Elixir for a while and created Credo and ElixirStatus. 32 |

33 | 34 | 35 |

Embargo?

36 |

37 | There is no reason why you should not share your thoughts, excitement and/or critique about this project on Twitter, in a blog post or in the issues section of the GitHub repo. 38 |

39 |

40 | The project will, however, remain under the beta.* domain for a couple of weeks so we can ensure everything is working properly. 41 |

42 | 43 | 44 |

Open Source?

45 |

46 | Yes! Everything can be found here: hexfaktor/hex_faktor_web 47 |

48 | 49 |

Thanks again!

-------------------------------------------------------------------------------- /web/templates/project/_edit.badges.html.eex: -------------------------------------------------------------------------------- 1 |

2 | HexFaktor provides 2 badges to display your deps status: 3 | (how do the colors work?) 4 |

5 |

6 | To display the badge, just copy-n-paste one of these snippets into your project's README: 7 |

8 | 9 | 10 |

Deps badge

11 | 12 |
13 | 
14 | 
15 | # Markdown:
16 | 
17 | [![Deps Status](<%= badge_url(@project, :all) %>)](<%= project_url(@project) %>)
18 | 
19 | # Raw Image URL:
20 | 
21 | <%= badge_url(@project, :all) %>
22 | 
23 | 24 |

Production dependencies only badge

25 | 26 |
27 | 
28 | 
29 | # Markdown:
30 | 
31 | [![Deps Status](<%= badge_url(@project, :prod) %>)](<%= project_url(@project) %>)
32 | 
33 | # Raw Image URL:
34 | 
35 | <%= badge_url(@project, :prod) %>
36 | 
37 | -------------------------------------------------------------------------------- /web/templates/project/_edit.github_sync.html.eex: -------------------------------------------------------------------------------- 1 |

2 | You can sync this project's info and branches with GitHub at any time. 3 |

4 | 5 | 6 | <%= link("Sync branches & info", to: "/projects/sync_github/#{@project.name}", class: "btn btn--default", method: "POST") %> 7 | -------------------------------------------------------------------------------- /web/templates/project/_edit.history.html.eex: -------------------------------------------------------------------------------- 1 |

2 | Here you can see the build history of your project. 3 |

4 |

5 | HexFaktor will generate a notification whenever your deps are updated. 6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | <%= for build <- @builds do %> 19 | <% build_job = build.build_jobs |> List.first %> 20 | <%= if build_job do %> 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | <% end %> 33 | <% end %> 34 |
NumberRevisionStatusStartedDuration
#<%= build.nr %><%= github_revision_link(@project, build_job.git_revision) %><%= build_job.status %> 26 | 27 | <%= time_ago_in_words build_job.inserted_at %> 28 | 29 | <%= build_job_duration build_job %>
35 | -------------------------------------------------------------------------------- /web/templates/project/_tab-edit.html.eex: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /web/templates/project/_tab-mix_env.html.eex: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /web/templates/project/_tab-project_filter.html.eex: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /web/templates/project/_tab_nav_edit.html.eex: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 | <%= for {section, label} <- @sections do %> 7 | <%= render HexFaktor.ProjectView, "_tab-edit.html", [class: "tab-nav__tab", active?: section == @current_section, section: section, label: label] %> 8 | <% end %> 9 |
10 |
11 | 12 | 15 |
16 |
17 | <%= for {section, label} <- @sections do %> 18 | <%= if section == @current_section do %> 19 | <%= render HexFaktor.ProjectView, "_tab-edit.html", [class: "tab-nav__tab", active?: true, section: @current_section, label: label] %> 20 | <% end %> 21 | <% end %> 22 | <%= render HexFaktor.ComponentView, "_tab_hamburger.html" %> 23 |
24 | 25 | 26 |
27 | <%= for {section, label} <- @sections do %> 28 | <%= render HexFaktor.ProjectView, "_tab-edit.html", [class: "tab-nav__dropdown__tab", active?: section == @current_section, section: section, label: label] %> 29 | <% end %> 30 |
31 |
32 | -------------------------------------------------------------------------------- /web/templates/project/add_project.html.eex: -------------------------------------------------------------------------------- 1 | <%= render HexFaktor.ComponentView, "user-title.html", user: @current_user, include_last_sync?: true %> 2 | 3 |
4 |
5 | Add a project by clicking "monitor this": 6 |
7 | 8 |
9 | Sync GitHub 10 | 11 |
12 |
13 | 14 | <%= if @fresh_user? do %> 15 |
16 | <% else %> 17 | <%= render HexFaktor.ProjectView, "_tab_nav_index.html", assigns %> 18 | 19 | <%= if @projects |> Enum.empty? do %> 20 |

21 | No projects in this list. 22 |

23 | <% else %> 24 | <%= for project <- @projects do %> 25 | <%= render HexFaktor.ComponentView, "project-list-item.html", project: project, outdated_project?: @outdated_project_ids |> Enum.member?(project.id) %> 26 | <% end %> 27 | 28 |
29 | 30 | Already monitored projects are omitted from this list. 31 |
32 | <% end %> 33 | <% end %> 34 | -------------------------------------------------------------------------------- /web/templates/project/builds.html.eex: -------------------------------------------------------------------------------- 1 | <%= link "Back to projects", to: "/projects" %> 2 | 3 |

<%= @project.name %> <%= link "GitHub", to: @project.html_url, target: "_blank" %>

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <%= for build <- @builds do %> 15 | <% build_job = build.build_jobs |> List.first %> 16 | 17 | 18 | 19 | 20 | 21 | <% end %> 22 |
NumberRevisionStatus
#<%= build.nr %><%= build_job.git_revision_id %><%= build_job.status %>
23 | -------------------------------------------------------------------------------- /web/templates/project/edit.html.eex: -------------------------------------------------------------------------------- 1 | <%= render HexFaktor.ComponentView, "project-title.html", conn: @conn, project: @project, branch: @project.git_repo_branches |> Enum.find(&(&1.name == @project.default_branch)) %> 2 | 3 |
4 | 7 |
8 | 9 | <%= render HexFaktor.ProjectView, "_tab_nav_edit.html", assigns %> 10 | 11 | <%= render HexFaktor.ComponentView, "alert.html", text: get_flash(@conn, :info) %> 12 | 13 | <%= render HexFaktor.ProjectView, "_edit.#{@current_section}.html", assigns %> 14 | -------------------------------------------------------------------------------- /web/templates/user/_edit.github_sync.html.eex: -------------------------------------------------------------------------------- 1 |

2 | You can sync your account with GitHub at any time. 3 |

4 | 5 | <%= link("Sync projects", to: "/projects/sync_github", class: "btn btn--default", method: "POST") %> 6 | -------------------------------------------------------------------------------- /web/templates/user/_edit.resend_validation.html.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | If you have not received the confirmation email sent to <%= @current_user.email %>, you can resend it. 5 |

6 | <%= if is_nil(@user.email_verified_at) do %> 7 | <%= link "Resend validation e-mail.", to: user_path(@conn, :resend_verify_email), method: "POST", class: "btn btn--default" %> 8 | <% end %> 9 |
10 |
11 | -------------------------------------------------------------------------------- /web/templates/user/_tab-edit.html.eex: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /web/templates/user/_tab_nav_edit.html.eex: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 | <%= for {section, label} <- @sections do %> 7 | <%= render HexFaktor.UserView, "_tab-edit.html", [class: "tab-nav__tab", active?: section == @current_section, section: section, label: label] %> 8 | <% end %> 9 |
10 |
11 | 12 | 15 |
16 |
17 | <%= if @current_section do %> 18 | <%= render HexFaktor.UserView, "_tab-edit.html", [class: "tab-nav__tab", active?: true, section: @current_section] %> 19 | <% end %> 20 | <%= render HexFaktor.ComponentView, "_tab_hamburger.html" %> 21 |
22 | 23 | 24 |
25 | <%= for {section, label} <- @sections do %> 26 | <%= render HexFaktor.UserView, "_tab-edit.html", [class: "tab-nav__dropdown__tab", active?: section == @current_section, section: section, label: label] %> 27 | <% end %> 28 |
29 |
-------------------------------------------------------------------------------- /web/templates/user/edit.html.eex: -------------------------------------------------------------------------------- 1 | <%= render HexFaktor.ComponentView, "user-title.html", user: @current_user, include_last_sync?: true %> 2 | 3 | <%= render HexFaktor.UserView, "_tab_nav_edit.html", assigns %> 4 | 5 | <%= render HexFaktor.ComponentView, "alert.html", text: get_flash(@conn, :info) %> 6 | 7 | <%= render HexFaktor.UserView, "_edit.#{@current_section}.html", assigns %> 8 | -------------------------------------------------------------------------------- /web/templates/user/resend_verify_email.html.eex: -------------------------------------------------------------------------------- 1 |

Here we go!

2 | 3 |

4 | The email was sent again. 5 |

6 | -------------------------------------------------------------------------------- /web/templates/user/verify_email.html.eex: -------------------------------------------------------------------------------- 1 | <%#= render HexFaktor.ComponentView, "user-title.html", user: @current_user %> 2 | 3 | <%= if @success do %> 4 |

Yay, success!

5 | 6 | Congrats, you verified your e-mail! 7 | <% else %> 8 |

It did not work!

9 |

10 | Could not verify your e-mail. Did you click on an old verification mail? 11 |

12 | <% end %> 13 | 14 | <%= if !HexFaktor.Auth.current_user(@conn) do %> 15 | Sign in now! 16 | <% end %> 17 | -------------------------------------------------------------------------------- /web/views/badge_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.BadgeView do 2 | use HexFaktor.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | if error = form.errors[field] do 13 | content_tag :span, translate_error(error), class: "help-block" 14 | end 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # Because error messages were defined within Ecto, we must 22 | # call the Gettext module passing our Gettext backend. We 23 | # also use the "errors" domain as translations are placed 24 | # in the errors.po file. On your own code and templates, 25 | # this could be written simply as: 26 | # 27 | # dngettext "errors", "1 file", "%{count} files", count 28 | # 29 | Gettext.dngettext(HexFaktor.Gettext, "errors", msg, msg, opts[:count], opts) 30 | end 31 | 32 | def translate_error(msg) do 33 | Gettext.dgettext(HexFaktor.Gettext, "errors", msg) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ErrorView do 2 | use HexFaktor.Web, :view 3 | 4 | # In case no render clause matches or no 5 | # template is found, let's render it as 500 6 | def template_not_found(_template, assigns) do 7 | render "500.html", assigns 8 | end 9 | 10 | def render("404.json", _), do: %{error: true, reason: 404} 11 | def render("500.json", _), do: %{error: true, reason: 500} 12 | end 13 | -------------------------------------------------------------------------------- /web/views/help_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.HelpView do 2 | use HexFaktor.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.LayoutView do 2 | use HexFaktor.Web, :view 3 | 4 | end 5 | -------------------------------------------------------------------------------- /web/views/notification_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.NotificationView do 2 | use HexFaktor.Web, :view 3 | end 4 | -------------------------------------------------------------------------------- /web/views/package_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PackageView do 2 | use HexFaktor.Web, :view 3 | 4 | def current_version([%{"version" => version}|tail]), do: version 5 | def current_version(_), do: nil 6 | 7 | defp following?(nil), do: false 8 | defp following?(package_user_settings) do 9 | package_user_settings.notifications_for_major || 10 | package_user_settings.notifications_for_minor || 11 | package_user_settings.notifications_for_patch || 12 | package_user_settings.notifications_for_pre 13 | end 14 | 15 | def render("index.json", %{packages: packages}) do 16 | packages |> Enum.map(&package_to_json/1) 17 | end 18 | 19 | def render("show.json", %{package: package}) do 20 | package |> package_to_json() 21 | end 22 | 23 | defp package_to_json(package) do 24 | versions = 25 | package.releases 26 | |> Enum.map(&release_to_json/1) 27 | %{ 28 | name: package.name, 29 | description: package.description, 30 | source_url: package.source_url, 31 | releases: versions, 32 | cached_at: package.updated_at, 33 | } 34 | end 35 | 36 | defp release_to_json(release) do 37 | %{ 38 | version: release["version"], 39 | updated_at: release["updated_at"] 40 | } 41 | end 42 | 43 | def render("ok.json", _), do: %{ok: true} 44 | def render("error.json", _), do: %{error: true} 45 | 46 | def render("404.html", _), do: "Not found" 47 | def render("404.json", _), do: %{error: true} 48 | 49 | @month_names [nil, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] 50 | #[nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 51 | def to_human_date(date) do 52 | month_name = @month_names |> Enum.at(date.month) 53 | "#{month_name} #{date.day}, #{date.year}" 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.PageView do 2 | use HexFaktor.Web, :view 3 | 4 | def render("ok.json", _) do 5 | %{ok: true} 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /web/views/project_hook_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectHookView do 2 | use HexFaktor.Web, :view 3 | 4 | def render("ok.json", _), do: %{ok: true} 5 | def render("error.json", _), do: %{error: true} 6 | end 7 | -------------------------------------------------------------------------------- /web/views/project_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.ProjectView do 2 | use HexFaktor.Web, :view 3 | 4 | alias HexFaktor.Project 5 | 6 | def render("ok.json", _), do: %{ok: true} 7 | def render("error.json", _), do: %{error: true} 8 | 9 | def github_revision_link(_project, nil), do: nil 10 | def github_revision_link(%Project{provider: "github"} = project, git_revision) do 11 | sha1 = git_revision.sha1 12 | text = sha1 |> String.slice(0..7) 13 | url = "https://github.com/#{project.name}/commit/#{sha1}" 14 | link text, to: url, target: "_blank" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /web/views/user_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HexFaktor.UserView do 2 | use HexFaktor.Web, :view 3 | end 4 | --------------------------------------------------------------------------------