diff --git a/.gitignore b/.gitignore index dcc5b36f..74c4390c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ /yarn-error.log .byebug_history + +/_sites/* +/_deploy/* diff --git a/Capfile b/Capfile index 13f00797..529ada80 100644 --- a/Capfile +++ b/Capfile @@ -10,6 +10,7 @@ require 'capistrano/passenger' require 'capistrano/bundler' require 'capistrano/rbenv' require 'capistrano/rails' +require 'whenever/capistrano' require 'capistrano/scm/git' install_plugin Capistrano::SCM::Git diff --git a/Gemfile b/Gemfile index 91d4789e..c8ff1251 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,7 @@ gem 'jekyll' gem 'jquery-rails' gem 'font-awesome-rails' gem 'exception_notification' +gem 'whenever', require: false group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index f0c38439..b883fffd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,6 +77,7 @@ GEM xpath (~> 2.0) childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) + chronic (0.10.2) coderay (1.1.2) colorator (1.1.0) concurrent-ruby (1.0.5) @@ -270,6 +271,8 @@ GEM websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) + whenever (0.10.0) + chronic (>= 0.6.3) xpath (2.1.0) nokogiri (~> 1.3) @@ -307,6 +310,7 @@ DEPENDENCIES turbolinks (~> 5) uglifier (>= 1.3.0) web-console (>= 3.3.0) + whenever BUNDLED WITH 1.16.0 diff --git a/_deploy/.keep b/_deploy/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index 64f8b9b3..f8e1cb86 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -14,4 +14,19 @@ class SitesController < ApplicationController redirect_to site_posts_path(site) end + + def enqueue + @site = find_site + @site.enqueue! + + redirect_to sites_path + end + + def build_log + @site = find_site + + render file: @site.build_log, + layout: false, + content_type: 'text/plain' + end end diff --git a/app/models/site.rb b/app/models/site.rb index 2a7202ff..1d1342b5 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -86,6 +86,79 @@ class Site end.flatten.uniq end + # Las usuarias que tienen acceso a este sitio se guardan en un archivo + # `.usuarias` que tiene la dirección de correo de cada una + def usuarias_file + File.join(path, '.usuarias') + end + + # Obtiene las usuarias que gestionan este sitio + def usuarias + @usuarias ||= File.read(usuarias_file).split("\n").map do |u| + Usuaria.find(u) + end + end + + def failed_file + File.join(path, '.failed') + end + + def failed? + File.exist? failed_file + end + + def defail + FileUtils.rm failed_file if failed? + end + alias :defail! :defail + + def build_log + File.join(path, 'build.log') + end + + def build_log? + File.exist? build_log + end + + def queue_file + File.join(path, '.generate') + end + + def enqueued? + File.exist? queue_file + end + alias :queued? :enqueued? + + # El sitio se genera cuando se coloca en una cola de generación, para + # que luego lo construya un cronjob + def enqueue + defail! + # TODO ya van tres métodos donde usamos esta idea, convertir en un + # helper o algo + r = File.open(queue_file, File::RDWR | File::CREAT, 0o640) do |f| + # Bloquear el archivo para que no sea accedido por otro + # proceso u otra editora + f.flock(File::LOCK_EX) + + # Empezar por el principio + f.rewind + + # Escribir la fecha de creación + f.write(Time.now.to_i.to_s) + + # Eliminar el resto + f.flush + f.truncate(f.pos) + end + end + alias :enqueue! :enqueue + + # Eliminar de la cola + def dequeue + FileUtils.rm(queue_file) if enqueued? + end + alias :dequeue! :dequeue + # El directorio donde se almacenan los sitios def self.site_path File.join(Rails.root, '_sites') diff --git a/app/views/sites/_enqueue.haml b/app/views/sites/_enqueue.haml new file mode 100644 index 00000000..fc69d339 --- /dev/null +++ b/app/views/sites/_enqueue.haml @@ -0,0 +1,13 @@ +- if site.enqueued? + %span.badge.badge-warning= t('sites.enqueued') +- else + = form_tag site_enqueue_path(site), method: :post, class: 'form-inline' do + = button_tag type: 'submit', class: 'btn btn-success' do + = fa_icon 'building' + = t('sites.enqueue') + +- if site.failed? + %span.badge.badge-alert= t('sites.failed') +- if site.build_log? + = link_to t('sites.build_log'), site_build_log_path(site), + class: 'badge badge-info' diff --git a/app/views/sites/index.haml b/app/views/sites/index.haml index 15f59d7e..a3980340 100644 --- a/app/views/sites/index.haml +++ b/app/views/sites/index.haml @@ -12,3 +12,4 @@ %tr %td= link_to site.name, site_path(site) %td= link_to t('i18n.edit'), site_i18n_edit_path(site) + %td= render 'sites/enqueue', site: site diff --git a/bin/jekyll_build_all b/bin/jekyll_build_all new file mode 100755 index 00000000..dc8971bd --- /dev/null +++ b/bin/jekyll_build_all @@ -0,0 +1,58 @@ +#!/bin/bash +# TODO convertir a ruby! +set -e + +rails_root="${PWD}" + +# Encontrar todos los sitios únicos con el archivo `.generate`. Esto +# significa que la usuaria quiso generar el sitio. +find -L ./_sites -mindepth 3 -maxdepth 3 -name .generate \ +| sed "s/\/\.generate$//" \ +| while read _path ; do + # Como seguimos todos los symlinks y los sitios pueden estar + # vinculados entre sí, volvemos a chequear si existe el archivo para + # no generarlo dos veces + test -f "${_path}/.generate" || continue + + # Obtenemos las direcciones de correo de las responsables + _mail=($(cat "${_path}/.usuarias")) + _site="$(echo "${_path}" | xargs basename)" + _deploy="${rails_root}/_deploy/${_site}" + + # Entrar al directorio del sitio + pushd "${_path}" &>/dev/null + + # Crear el sitio con lujo de detalles y guardar un log, pero a la vez + # tenerlo en la salida estándar para poder enviar al MAILTO del + # cronjob. + # + # Ya que estamos, eliminamos la ruta donde estamos paradas para no dar + # información sobre la servidora. + bundle exec \ + jekyll build --trace --destination "${_deploy}" \ + | sed -re "s,${_path},,g" \ + > "build.log" + + # Acciones posteriores + # TODO convertir en un plugin de cada sitio? + if test $? -eq 0; then + # Si funciona, enviar un mail + # TODO enviar un mail más completo y no hardcodear direcciones + mail -b "sysadmin@kefir.red" \ + -r "sutty@kefir.red" \ + -s "${_site}: :)" \ + ${_mail[@]} + else + mail -b "sysadmin@kefir.red" \ + -r "sutty@kefir.red" \ + -s "${_site}: :(" \ + ${_mail[@]} + date +%s >.failed + fi + + # Eliminar el archivo para sacar el sitio de la cola de compilación + rm -f .generate + + # Volver al principio para continuar con el siguiente sitio + popd &>/dev/null +done diff --git a/bin/whenever b/bin/whenever new file mode 100755 index 00000000..73504537 --- /dev/null +++ b/bin/whenever @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'whenever' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +bundle_binstub = File.expand_path("../bundle", __FILE__) +load(bundle_binstub) if File.file?(bundle_binstub) + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("whenever", "whenever") diff --git a/bin/wheneverize b/bin/wheneverize new file mode 100755 index 00000000..de812b68 --- /dev/null +++ b/bin/wheneverize @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'wheneverize' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +bundle_binstub = File.expand_path("../bundle", __FILE__) +load(bundle_binstub) if File.file?(bundle_binstub) + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("whenever", "wheneverize") diff --git a/config/deploy.rb b/config/deploy.rb index 0f6181d6..fec32988 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -5,5 +5,5 @@ set :repo_url, 'git@0xacab.org:itacate-kefir/sutty.git' set :bundle_flags, '--deployment' set :default_env, path: '/usr/lib/passenger/bin:$PATH' -set :linked_dirs, %w{_sites} +set :linked_dirs, %w{_sites _deploy} set :linked_files, %w{.env} diff --git a/config/locales/en.yml b/config/locales/en.yml index e930453d..a40e5e9a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -9,6 +9,10 @@ en: sites: title: 'Sites' index: 'Sites' + enqueued: 'Waiting for build' + enqueue: 'Build' + failed: 'Failed!' + build_log: 'Read log' footer: powered_by: 'is developed by' i18n: diff --git a/config/locales/es.yml b/config/locales/es.yml index e3368be8..ca14e7cc 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -9,6 +9,10 @@ es: sites: title: 'Sitios' index: 'Sitios' + enqueued: 'Esperando compilación' + enqueue: 'Compilar' + failed: '¡Falló!' + build_log: 'Ver registro' footer: powered_by: 'es desarrollada por' i18n: diff --git a/config/routes.rb b/config/routes.rb index c6defd06..329fb110 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,11 +4,14 @@ Rails.application.routes.draw do get 'login/new', to: 'login#new' post 'login', to: 'login#create' - resources :sites do + resources :sites, only: [ :index, :show ] do resources :posts get 'i18n', to: 'i18n#index' get 'i18n/edit', to: 'i18n#edit' post 'i18n', to: 'i18n#update' + + post 'enqueue', to: 'sites#enqueue' + get 'build_log', to: 'sites#build_log' end end diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 00000000..71741012 --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,6 @@ +env 'MAILTO', 'sysadmin@kefir.red' +job_type :bash, 'cd :path && ./bin/:task' + +every 3.minutes do + bash 'jekyll_build_all' +end