├── bootstrap.rb ├── .travis.yml ├── spec ├── helpers │ ├── nil_logger.rb │ └── github_payload_builder.rb ├── integration │ ├── config │ │ ├── tx.config │ │ └── txgh.yml │ ├── integration_spec.rb │ ├── payloads │ │ ├── github_postbody_l10n.json │ │ ├── github_postbody_release.json │ │ └── github_postbody.json │ └── cassettes │ │ ├── github_l10n_hook_endpoint.yml │ │ └── transifex_hook_endpoint.yml ├── utils_spec.rb ├── github_repo_spec.rb ├── tx_resource_spec.rb ├── tx_config_spec.rb ├── parse_config_spec.rb ├── github_request_auth_spec.rb ├── config_spec.rb ├── category_support_spec.rb ├── transifex_project_spec.rb ├── tx_branch_resource_spec.rb ├── transifex_request_auth_spec.rb ├── handlers │ ├── transifex_hook_handler_spec.rb │ └── github_hook_handler_spec.rb ├── github_api_spec.rb ├── tx_config_multi_project_spec.rb ├── app_multi_project_spec.rb ├── key_manager_multi_project_spec.rb ├── app_spec.rb ├── key_manager_spec.rb ├── spec_helper.rb └── transifex_api_spec.rb ├── lib ├── txgh │ ├── utils.rb │ ├── errors.rb │ ├── handlers.rb │ ├── tx_logger.rb │ ├── github_repo.rb │ ├── parse_config.rb │ ├── github_request_auth.rb │ ├── category_support.rb │ ├── config.rb │ ├── tx_branch_resource.rb │ ├── transifex_project.rb │ ├── tx_resource.rb │ ├── github_api.rb │ ├── tx_config.rb │ ├── key_manager.rb │ ├── transifex_request_auth.rb │ ├── handlers │ │ ├── transifex_hook_handler.rb │ │ └── github_hook_handler.rb │ ├── transifex_api.rb │ └── app.rb └── txgh.rb ├── config.ru ├── config ├── tx.config └── txgh.yml ├── Rakefile ├── .gitignore ├── Gemfile ├── Dockerfile ├── docs ├── docker.md ├── aws.md └── heroku.md ├── Gemfile.lock ├── README.md └── LICENSE /bootstrap.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File::dirname(__FILE__) 2 | $:.unshift "#{File::dirname(__FILE__)}/lib" 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | rvm: 4 | - 2.2.2 5 | script: 'bundle exec rake spec:full' 6 | -------------------------------------------------------------------------------- /spec/helpers/nil_logger.rb: -------------------------------------------------------------------------------- 1 | class NilLogger 2 | def info(*args) 3 | end 4 | 5 | def warn(*args) 6 | end 7 | 8 | def error(*args) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/txgh/utils.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | module Utils 3 | def slugify(str) 4 | str.gsub('/', '_') 5 | end 6 | end 7 | 8 | Utils.extend(Utils) 9 | end 10 | -------------------------------------------------------------------------------- /lib/txgh/errors.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | class TxghError < StandardError; end 3 | class TxghInternalError < TxghError; end 4 | 5 | class TransifexApiError < StandardError; end 6 | end 7 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require_relative 'bootstrap' 2 | require 'txgh' 3 | 4 | map '/' do 5 | use Txgh::Application 6 | run Sinatra::Base 7 | end 8 | 9 | map '/hooks' do 10 | use Txgh::Hooks 11 | run Sinatra::Base 12 | end 13 | -------------------------------------------------------------------------------- /lib/txgh/handlers.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | module Handlers 3 | autoload :GithubHookHandler, 'txgh/handlers/github_hook_handler' 4 | autoload :TransifexHookHandler, 'txgh/handlers/transifex_hook_handler' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/tx.config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = 4 | 5 | # Create one such section per resource 6 | [txgh-test-1.samplepo] 7 | file_filter = translations//sample.po 8 | source_file = sample.po 9 | source_lang = en 10 | type = PO 11 | -------------------------------------------------------------------------------- /spec/integration/config/tx.config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = 4 | 5 | # Create one such section per resource 6 | [test-project-88.samplepo] 7 | file_filter = translations//sample.po 8 | source_file = sample.po 9 | source_lang = en 10 | type = PO 11 | -------------------------------------------------------------------------------- /lib/txgh/tx_logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Txgh 4 | class TxLogger 5 | def self.logger 6 | @_logger ||= Logger.new(STDOUT).tap do |logger| 7 | logger.level = Logger::INFO 8 | logger.datetime_format = '%a %d-%m-%Y %H%M ' 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | 3 | desc 'Run specs' 4 | RSpec::Core::RakeTask.new do |t| 5 | t.pattern = './spec/**/*_spec.rb' 6 | end 7 | 8 | task default: :spec 9 | 10 | namespace :spec do 11 | desc 'Run full spec suite' 12 | task full: [:full_spec_env, :spec] 13 | 14 | task :full_spec_env do 15 | ENV['FULL_SPEC'] = 'true' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /config/secret/* 2 | *.rbc 3 | *.sassc 4 | .sass-cache 5 | capybara-*.html 6 | .rspec 7 | .rvmrc 8 | /.bundle 9 | /vendor/bundle 10 | /log/* 11 | /tmp/* 12 | /db/*.sqlite3 13 | /public/system/* 14 | /coverage/ 15 | /spec/tmp/* 16 | **.orig 17 | rerun.txt 18 | pickle-email-*.html 19 | .project 20 | config/initializers/secret_token.rb 21 | config/txgh_config.rb 22 | .env 23 | .*.swp 24 | .DS_Store -------------------------------------------------------------------------------- /spec/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe Utils do 6 | describe '.slugify' do 7 | it 'correctly slugifies a string with slashes' do 8 | expect(Utils.slugify('abc/def/ghi')).to eq('abc_def_ghi') 9 | end 10 | 11 | it 'does not replace underscores' do 12 | expect(Utils.slugify('abc_def/ghi')).to eq('abc_def_ghi') 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'faraday' 4 | gem 'faraday_middleware' 5 | gem 'json' 6 | gem 'octokit' 7 | gem 'puma' 8 | gem 'rack' 9 | gem 'rake' 10 | gem 'parseconfig' 11 | gem 'sinatra' 12 | gem 'sinatra-contrib' 13 | 14 | group :development, :test do 15 | gem 'pry-nav' 16 | end 17 | 18 | group :development do 19 | gem 'shotgun' 20 | end 21 | 22 | group :test do 23 | gem 'rack-test' 24 | gem 'rspec' 25 | gem 'vcr','~> 3.0.1' 26 | gem 'webmock','~> 1.24.6' 27 | end 28 | -------------------------------------------------------------------------------- /lib/txgh/github_repo.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | class GithubRepo 3 | attr_reader :config, :api 4 | 5 | def initialize(config, api) 6 | @config = config 7 | @api = api 8 | end 9 | 10 | def name 11 | config['name'] 12 | end 13 | 14 | def branch 15 | config['branch'] 16 | end 17 | 18 | def webhook_secret 19 | config['webhook_secret'] 20 | end 21 | 22 | def webhook_protected? 23 | !(webhook_secret || '').empty? 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.1-onbuild 2 | ENV RACK_ENV production 3 | ENV GITHUB_PUSH_SOURCE_TO Transifex Project Name 4 | ENV GITHUB_USERNAME Your github username 5 | ENV GITHUB_TOKEN Transifex API Token 6 | ENV GITHUB_WEBHOOK_SECRET Auth for Github Webhook 7 | ENV GITHUB_BRANCH master 8 | ENV TX_CONFIG_PATH config/tx.config 9 | ENV TX_USERNAME Transifex Username 10 | ENV TX_PASSWORD Transifex Password 11 | ENV TX_PUSH_TRANSLATIONS_TO Github Repo Name 12 | ENV TX_WEBHOOK_SECRET Auth for Transifex Webhook 13 | 14 | EXPOSE 9292 15 | CMD ["puma", "-p", "9292"] 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/github_repo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe GithubRepo do 6 | let(:repo_name) { 'my_org/my_repo' } 7 | let(:branch) { 'master' } 8 | let(:config) { { 'name' => repo_name, 'branch' => branch } } 9 | let(:api) { :api } 10 | let(:repo) { GithubRepo.new(config, api) } 11 | 12 | describe '#name' do 13 | it 'retrieves the repo name from the config' do 14 | expect(repo.name).to eq(repo_name) 15 | end 16 | end 17 | 18 | describe '#branch' do 19 | it 'retrieves the branch name from the config' do 20 | expect(repo.branch).to eq(branch) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/txgh/parse_config.rb: -------------------------------------------------------------------------------- 1 | require 'parseconfig' 2 | require 'tempfile' 3 | 4 | module Txgh 5 | # This class wraps the ParseConfig class from the parseconfig gem and 6 | # provides a way to load config from a string instead of just a file. 7 | class ParseConfig < ::ParseConfig 8 | class << self 9 | def load(contents) 10 | tmp = Tempfile.new('parseconfig') 11 | tmp.write(contents) 12 | tmp.flush 13 | load_file(tmp.path) 14 | ensure 15 | tmp.close if tmp 16 | end 17 | 18 | def load_file(path) 19 | # use the default file loading logic 20 | new(path) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/txgh/github_request_auth.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Txgh 4 | class GithubRequestAuth 5 | HMAC_DIGEST = OpenSSL::Digest.new('sha1') 6 | RACK_HEADER = 'HTTP_X_HUB_SIGNATURE' 7 | GITHUB_HEADER = 'X-Hub-Signature' 8 | 9 | class << self 10 | def authentic_request?(request, secret) 11 | request.body.rewind 12 | expected_signature = header_value(request.body.read, secret) 13 | actual_signature = request.env[RACK_HEADER] 14 | actual_signature == expected_signature 15 | end 16 | 17 | def header_value(content, secret) 18 | "sha1=#{digest(content, secret)}" 19 | end 20 | 21 | private 22 | 23 | def digest(content, secret) 24 | OpenSSL::HMAC.hexdigest(HMAC_DIGEST, secret, content) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /config/txgh.yml: -------------------------------------------------------------------------------- 1 | txgh: 2 | github: 3 | repos: 4 | <%= ENV['TX_PUSH_TRANSLATIONS_TO'] %>: 5 | api_username: "<%= ENV['GITHUB_USERNAME'] %>" 6 | api_token: "<%= ENV['GITHUB_TOKEN'] %>" 7 | push_source_to: "<%= ENV['GITHUB_PUSH_SOURCE_TO'] %>" 8 | branch: "<%= ENV['GITHUB_BRANCH'] %>" 9 | webhook_secret: "<%= ENV['GITHUB_WEBHOOK_SECRET'] %>" 10 | transifex: 11 | projects: 12 | <%= ENV['GITHUB_PUSH_SOURCE_TO'] %>: 13 | tx_config: "<%= ENV['TX_CONFIG_PATH'] %>" 14 | api_username: "<%= ENV['TX_USERNAME'] %>" 15 | api_password: "<%= ENV['TX_PASSWORD'] %>" 16 | push_translations_to: "<%= ENV['TX_PUSH_TRANSLATIONS_TO'] %>" 17 | push_trigger: "<%= ENV['TX_PUSH_TRIGGER_REVIEWED_OR_TRANSLATED'] %>" 18 | webhook_secret: "<%= ENV['TX_WEBHOOK_SECRET'] %>" 19 | -------------------------------------------------------------------------------- /lib/txgh/category_support.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | module CategorySupport 3 | def deserialize_categories(categories_arr) 4 | categories_arr.each_with_object({}) do |category_str, ret| 5 | category_str.split(' ').each do |category| 6 | if idx = category.index(':') 7 | ret[category[0...idx]] = category[(idx + 1)..-1] 8 | end 9 | end 10 | end 11 | end 12 | 13 | def serialize_categories(categories_hash) 14 | categories_hash.map do |key, value| 15 | "#{key}:#{value}" 16 | end 17 | end 18 | 19 | def escape_category(str) 20 | str.gsub(' ', '_') 21 | end 22 | 23 | def join_categories(arr) 24 | arr.join(' ') 25 | end 26 | end 27 | 28 | # add all the methods as class methods (they're also available as instance 29 | # methods for anyone who includes this module) 30 | CategorySupport.extend(CategorySupport) 31 | end 32 | -------------------------------------------------------------------------------- /spec/integration/config/txgh.yml: -------------------------------------------------------------------------------- 1 | txgh: 2 | github: 3 | repos: 4 | txgh-bot/txgh-test-resources: 5 | api_username: txgh-bot 6 | # github will auto-revoke a token if they notice it in one of your commits ;) 7 | api_token: <%= require 'base64'; Base64.decode64('YjViYWY3Nzk5NTdkMzVlMmI0OGZmYjk4YThlY2M1ZDY0NzAwNWRhZA==') %> 8 | push_source_to: test-project-88 9 | branch: master 10 | webhook_secret: 18d3998f576dfe933357104b87abfd61 11 | transifex: 12 | projects: 13 | test-project-88: 14 | tx_config: ./config/tx.config 15 | api_username: txgh.bot 16 | api_password: 2aqFGW99fPRKWvXBPjbrxkdiR 17 | push_translations_to: txgh-bot/txgh-test-resources 18 | push_trigger: translations 19 | webhook_secret: fce95b1748fd638c22174d34200f10cf 20 | -------------------------------------------------------------------------------- /docs/docker.md: -------------------------------------------------------------------------------- 1 | Implementing on Docker 2 | ====================== 3 | 4 | ## Using a pre-made image 5 | 6 | The quickest way to get started is to use an existing pre-made image. Simply run `docker pull mjjacko/txgh` to get started. 7 | 8 | This image is intended for developmental purposes as it sets up all of the OS and Ruby environment for you. All you need todo is complete the Txgh server configuration which includes authentication information for both Transifex and GitHub. 9 | 10 | To allow the Txgh server access to your local files, a mount point is set to '/tmp/txgh'. If this directory exists on the host, Docker will map it for you, otherwise you can always adjust it as needed. 11 | 12 | The DockerHub page can be found [here](https://hub.docker.com/r/mjjacko/txgh/) 13 | 14 | ## Building from scratch 15 | 16 | Alternatively you can build your own image. A good place to start is the [Dockerfile](https://github.com/transifex/txgh/blob/devel/Dockerfile) which is already part of the project. 17 | -------------------------------------------------------------------------------- /lib/txgh/config.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | class Config 3 | attr_reader :project_config, :repo_config, :tx_config 4 | 5 | def initialize(project_config, repo_config, tx_config) 6 | @project_config = project_config 7 | @repo_config = repo_config 8 | @tx_config = tx_config 9 | end 10 | 11 | def github_repo 12 | @github_repo ||= Txgh::GithubRepo.new(repo_config, github_api) 13 | end 14 | 15 | def transifex_project 16 | @transifex_project ||= Txgh::TransifexProject.new( 17 | project_config, tx_config, transifex_api 18 | ) 19 | end 20 | 21 | def transifex_api 22 | @transifex_api ||= Txgh::TransifexApi.create_from_credentials( 23 | project_config['api_username'], project_config['api_password'] 24 | ) 25 | end 26 | 27 | def github_api 28 | @github_api ||= Txgh::GithubApi.create_from_credentials( 29 | repo_config['api_username'], repo_config['api_token'] 30 | ) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/tx_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe TxResource do 6 | let(:resource) do 7 | TxResource.new( 8 | 'project_slug', 'resource_slug', 'type', 9 | 'source_lang', 'source_file', 'ko-KR:ko', 'translation_file' 10 | ) 11 | end 12 | 13 | describe '#L10N_resource_slug' do 14 | it 'appends L10N to the resource slug' do 15 | expect(resource.L10N_resource_slug).to eq("L10Nresource_slug") 16 | end 17 | end 18 | 19 | describe '#lang_map' do 20 | it 'converts the given language if a mapping exists for it' do 21 | expect(resource.lang_map('ko-KR')).to eq('ko') 22 | end 23 | 24 | it 'does not perform any conversion if no mapping exists for the given language' do 25 | expect(resource.lang_map('foo')).to eq('foo') 26 | end 27 | end 28 | 29 | describe '#slugs' do 30 | it 'returns an array containing the project and resource slugs' do 31 | expect(resource.slugs).to eq(%w(project_slug resource_slug)) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/txgh.rb: -------------------------------------------------------------------------------- 1 | require 'txgh/errors' 2 | 3 | module Txgh 4 | autoload :Application, 'txgh/app' 5 | autoload :CategorySupport, 'txgh/category_support' 6 | autoload :Config, 'txgh/config' 7 | autoload :GithubApi, 'txgh/github_api' 8 | autoload :GithubRepo, 'txgh/github_repo' 9 | autoload :GithubRequestAuth, 'txgh/github_request_auth' 10 | autoload :Handlers, 'txgh/handlers' 11 | autoload :Hooks, 'txgh/app' 12 | autoload :KeyManager, 'txgh/key_manager' 13 | autoload :ParseConfig, 'txgh/parse_config' 14 | autoload :TransifexApi, 'txgh/transifex_api' 15 | autoload :TransifexProject, 'txgh/transifex_project' 16 | autoload :TxBranchResource, 'txgh/tx_branch_resource' 17 | autoload :TransifexRequestAuth, 'txgh/transifex_request_auth' 18 | autoload :TxConfig, 'txgh/tx_config' 19 | autoload :TxLogger, 'txgh/tx_logger' 20 | autoload :TxResource, 'txgh/tx_resource' 21 | autoload :Utils, 'txgh/utils' 22 | end 23 | -------------------------------------------------------------------------------- /spec/tx_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe TxConfig do 6 | describe '.load' do 7 | it 'parses the config correctly' do 8 | config_str = """ 9 | [main] 10 | host = https://www.transifex.com 11 | lang_map = pt-BR:pt, ko-KR:ko 12 | 13 | [my_proj.my_resource] 14 | file_filter = translations//sample.po 15 | source_file = sample.po 16 | source_lang = en 17 | type = PO 18 | """ 19 | 20 | config = TxConfig.load(config_str) 21 | expect(config.lang_map).to eq('pt-BR' => 'pt', 'ko-KR' => 'ko') 22 | expect(config.resources.size).to eq(1) 23 | 24 | resource = config.resources.first 25 | expect(resource.project_slug).to eq('my_proj') 26 | expect(resource.resource_slug).to eq('my_resource') 27 | expect(resource.source_file).to eq('sample.po') 28 | expect(resource.source_lang).to eq('en') 29 | expect(resource.translation_file).to eq('translations//sample.po') 30 | expect(resource.type).to eq('PO') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/txgh/tx_branch_resource.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Txgh 4 | class TxBranchResource 5 | extend Forwardable 6 | 7 | def_delegators :@resource, *[ 8 | :project_slug, :resource_slug, :type, :source_lang, :source_file, 9 | :translation_file, :lang_map, :translation_path, :slugs 10 | ] 11 | 12 | attr_reader :resource, :branch 13 | 14 | class << self 15 | def find(project, resource_slug, branch) 16 | suffix = "-#{Utils.slugify(branch)}" 17 | 18 | if resource_slug.end_with?(suffix) 19 | resource_slug = resource_slug.chomp(suffix) 20 | 21 | if resource = project.resource(resource_slug) 22 | new(resource, branch) 23 | end 24 | end 25 | end 26 | end 27 | 28 | def initialize(resource, branch) 29 | @resource = resource 30 | @branch = branch 31 | end 32 | 33 | def resource_slug 34 | "#{resource.resource_slug}-#{slugified_branch}" 35 | end 36 | 37 | private 38 | 39 | def slugified_branch 40 | Utils.slugify(branch) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/txgh/transifex_project.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | class TransifexProject 3 | attr_reader :config, :tx_config, :api 4 | 5 | def initialize(config, tx_config, api) 6 | @config = config 7 | @tx_config = tx_config 8 | @api = api 9 | end 10 | 11 | def name 12 | config['name'] 13 | end 14 | 15 | def push_trigger 16 | config['push_trigger'] 17 | end 18 | 19 | def push_trigger_set? 20 | !(push_trigger || '').empty? 21 | end 22 | 23 | def webhook_secret 24 | config['webhook_secret'] 25 | end 26 | 27 | def webhook_protected? 28 | !(webhook_secret || '').empty? 29 | end 30 | 31 | def resource(slug, branch = nil) 32 | if branch 33 | TxBranchResource.find(self, slug, branch) 34 | else 35 | tx_config.resources.find do |resource| 36 | resource.resource_slug == slug 37 | end 38 | end 39 | end 40 | 41 | def resources 42 | tx_config.resources 43 | end 44 | 45 | def lang_map(tx_lang) 46 | if tx_config.lang_map.include?(tx_lang) 47 | tx_config.lang_map[tx_lang] 48 | else 49 | tx_lang 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/txgh/tx_resource.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | class TxResource 3 | attr_reader :project_slug, :resource_slug, :type, :source_lang 4 | attr_reader :source_file, :translation_file 5 | 6 | def initialize(project_slug, resource_slug, type, source_lang, source_file, 7 | lang_map, translation_file) 8 | @project_slug = project_slug 9 | @resource_slug = resource_slug 10 | @type = type 11 | @source_lang = source_lang 12 | @source_file = source_file 13 | @lang_map = {} 14 | 15 | if lang_map 16 | result = {} 17 | lang_map.split(',').each do |m| 18 | key_value = m.split(':', 2) 19 | result[key_value[0].strip] = key_value[1].strip 20 | end 21 | 22 | @lang_map = result 23 | end 24 | 25 | @translation_file = translation_file 26 | end 27 | 28 | def L10N_resource_slug 29 | "L10N#{resource_slug}" 30 | end 31 | 32 | def lang_map(tx_lang) 33 | @lang_map.fetch(tx_lang, tx_lang) 34 | end 35 | 36 | def translation_path(language) 37 | translation_file.gsub('', language) 38 | end 39 | 40 | def slugs 41 | [project_slug, resource_slug] 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/parse_config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'tempfile' 3 | 4 | include Txgh 5 | 6 | describe Txgh::ParseConfig do 7 | let(:contents) do 8 | """ 9 | [header] 10 | key = val 11 | 12 | [header2] 13 | key2 = val2 14 | """ 15 | end 16 | 17 | shared_examples 'a correct config loader' do 18 | it 'has correctly parsed the given config' do 19 | expect(config).to be_a(::ParseConfig) 20 | expect(config.groups).to eq(%w(header header2)) 21 | expect(config.params).to eq( 22 | 'header' => { 'key' => 'val' }, 23 | 'header2' => { 'key2' => 'val2' } 24 | ) 25 | end 26 | end 27 | 28 | describe '.load' do 29 | let(:config) do 30 | Txgh::ParseConfig.load(contents) 31 | end 32 | 33 | it_behaves_like 'a correct config loader' 34 | end 35 | 36 | describe '.load_file' do 37 | around(:each) do |example| 38 | Tempfile.open('parseconfig-test') do |f| 39 | f.write(contents) 40 | f.flush 41 | @file = f 42 | example.run 43 | end 44 | end 45 | 46 | let(:config) do 47 | Txgh::ParseConfig.load_file(@file.path) 48 | end 49 | 50 | it_behaves_like 'a correct config loader' 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/github_request_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rack' 3 | 4 | include Txgh 5 | 6 | describe GithubRequestAuth do 7 | let(:secret) { 'abc123' } 8 | let(:params) { '{"param1":"value1","param2":"value2","param3":123}' } 9 | let(:valid_signature) { 'ea62c3f65c8e42f155d96a25b7ba6eb5d320630e' } 10 | 11 | describe '.authentic_request?' do 12 | it 'returns true if the request is signed correctly' do 13 | request = Rack::Request.new( 14 | GithubRequestAuth::RACK_HEADER => "sha1=#{valid_signature}", 15 | 'rack.input' => StringIO.new(params) 16 | ) 17 | 18 | authentic = GithubRequestAuth.authentic_request?(request, secret) 19 | expect(authentic).to eq(true) 20 | end 21 | 22 | it 'returns false if the request is not signed correctly' do 23 | request = Rack::Request.new( 24 | GithubRequestAuth::RACK_HEADER => 'incorrect', 25 | 'rack.input' => StringIO.new(params) 26 | ) 27 | 28 | authentic = GithubRequestAuth.authentic_request?(request, secret) 29 | expect(authentic).to eq(false) 30 | end 31 | end 32 | 33 | describe '.header' do 34 | it 'calculates the signature and formats it as an http header' do 35 | value = GithubRequestAuth.header_value(params, secret) 36 | expect(value).to eq("sha1=#{valid_signature}") 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe Config do 6 | include StandardTxghSetup 7 | 8 | let(:config) do 9 | Txgh::Config.new(project_config, repo_config, tx_config) 10 | end 11 | 12 | describe '#github_repo' do 13 | it 'instantiates a github repo with the right config' do 14 | repo = config.github_repo 15 | expect(repo).to be_a(GithubRepo) 16 | expect(repo.name).to eq(repo_name) 17 | expect(repo.branch).to eq(branch) 18 | end 19 | end 20 | 21 | describe '#transifex_project' do 22 | it 'instantiates a transifex project with the right config' do 23 | project = config.transifex_project 24 | expect(project).to be_a(TransifexProject) 25 | expect(project.name).to eq(project_name) 26 | expect(project.resources.first.resource_slug).to eq(resource_slug) 27 | end 28 | end 29 | 30 | describe '#transifex_api' do 31 | it 'instantiates an API instance' do 32 | api = config.transifex_api 33 | expect(api).to be_a(TransifexApi) 34 | expect(api.connection.headers).to include('Authorization') 35 | end 36 | end 37 | 38 | describe '#github_api' do 39 | it 'instantiates an API instance' do 40 | api = config.github_api 41 | expect(api).to be_a(GithubApi) 42 | expect(api.client.login).to eq(repo_config['api_username']) 43 | expect(api.client.access_token).to eq(repo_config['api_token']) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/category_support_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe CategorySupport do 6 | describe '.deserialize_categories' do 7 | it 'converts an array of categories into a hash' do 8 | categories = %w(captain:janeway commander:chakotay) 9 | result = CategorySupport.deserialize_categories(categories) 10 | expect(result).to eq('captain' => 'janeway', 'commander' => 'chakotay') 11 | end 12 | 13 | it 'converts an array of space-separated categories' do 14 | categories = ['captain:janeway commander:chakotay'] 15 | result = CategorySupport.deserialize_categories(categories) 16 | expect(result).to eq('captain' => 'janeway', 'commander' => 'chakotay') 17 | end 18 | end 19 | 20 | describe '.serialize_categories' do 21 | it 'converts a hash of categories into an array' do 22 | categories = { 'captain' => 'janeway', 'commander' => 'chakotay' } 23 | result = CategorySupport.serialize_categories(categories) 24 | expect(result.sort).to eq(['captain:janeway', 'commander:chakotay']) 25 | end 26 | end 27 | 28 | describe '.escape_category' do 29 | it 'replaces spaces in category values' do 30 | expect(CategorySupport.escape_category('Katherine Janeway')).to( 31 | eq('Katherine_Janeway') 32 | ) 33 | end 34 | end 35 | 36 | describe '.join_categories' do 37 | it 'joins an array of categories by spaces' do 38 | expect(CategorySupport.join_categories(%w(foo:bar baz:boo))).to( 39 | eq('foo:bar baz:boo') 40 | ) 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/transifex_project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe TransifexProject do 6 | include StandardTxghSetup 7 | 8 | describe '#name' do 9 | it 'pulls the project name out of the config' do 10 | expect(transifex_project.name).to eq(project_name) 11 | end 12 | end 13 | 14 | describe '#resource' do 15 | it 'finds the resource by slug' do 16 | resource = transifex_project.resource(resource_slug) 17 | expect(resource).to be_a(TxResource) 18 | expect(resource.resource_slug).to eq(resource_slug) 19 | end 20 | 21 | it 'returns nil if there is no resource with the given slug' do 22 | resource = transifex_project.resource('foobarbaz') 23 | expect(resource).to be_nil 24 | end 25 | end 26 | 27 | describe '#resources' do 28 | it 'hands back the array of resources from the tx config' do 29 | expect(transifex_project.resources).to be_a(Array) 30 | 31 | transifex_project.resources.each_with_index do |resource, idx| 32 | expect(resource.resource_slug).to( 33 | eq(tx_config.resources[idx].resource_slug) 34 | ) 35 | end 36 | end 37 | end 38 | 39 | describe '#lang_map' do 40 | it 'converts the given language if a mapping exists for it' do 41 | expect(transifex_project.lang_map('ko-KR')).to eq('ko') 42 | expect(transifex_project.lang_map('pt-BR')).to eq('pt') 43 | end 44 | 45 | it 'does not perform any conversion if no mapping exists for the given language' do 46 | expect(transifex_project.lang_map('foo')).to eq('foo') 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/txgh/github_api.rb: -------------------------------------------------------------------------------- 1 | require 'octokit' 2 | 3 | module Txgh 4 | class GithubApi 5 | class << self 6 | def create_from_credentials(login, access_token) 7 | create_from_client( 8 | Octokit::Client.new(login: login, access_token: access_token) 9 | ) 10 | end 11 | 12 | def create_from_client(client) 13 | new(client) 14 | end 15 | end 16 | 17 | attr_reader :client 18 | 19 | def initialize(client) 20 | @client = client 21 | end 22 | 23 | def tree(repo, sha) 24 | client.tree(repo, sha, recursive: 1) 25 | end 26 | 27 | def blob(repo, sha) 28 | client.blob(repo, sha) 29 | end 30 | 31 | def create_ref(repo, branch, sha) 32 | client.create_ref(repo, branch, sha) rescue false 33 | end 34 | 35 | def commit(repo, branch, path, content) 36 | blob = client.create_blob(repo, content) 37 | master = client.ref(repo, branch) 38 | base_commit = get_commit(repo, master[:object][:sha]) 39 | 40 | tree_data = [{ path: path, mode: '100644', type: 'blob', sha: blob }] 41 | tree_options = { base_tree: base_commit[:commit][:tree][:sha] } 42 | 43 | tree = client.create_tree(repo, tree_data, tree_options) 44 | commit = client.create_commit( 45 | repo, "Updating translations for #{path}", tree[:sha], master[:object][:sha] 46 | ) 47 | 48 | # false means don't force push 49 | client.update_ref(repo, branch, commit[:sha], false) 50 | end 51 | 52 | def get_commit(repo, sha) 53 | client.commit(repo, sha) 54 | end 55 | 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/tx_branch_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe TxBranchResource do 6 | let(:resource_slug) { 'resource_slug' } 7 | let(:resource_slug_with_branch) { "#{resource_slug}-heads_my_branch" } 8 | let(:project_slug) { 'project_slug' } 9 | let(:branch) { 'heads/my_branch' } 10 | 11 | let(:api) { :api } 12 | let(:config) { { name: project_slug } } 13 | let(:resources) { [base_resource] } 14 | 15 | let(:tx_config) do 16 | TxConfig.new(resources, {}) 17 | end 18 | 19 | let(:project) do 20 | TransifexProject.new(config, tx_config, api) 21 | end 22 | 23 | let(:base_resource) do 24 | TxResource.new( 25 | project_slug, resource_slug, 'type', 'source_lang', 'source_file', 26 | 'ko-KR:ko', 'translation_file' 27 | ) 28 | end 29 | 30 | describe '.find' do 31 | it 'finds the correct resource' do 32 | resource = TxBranchResource.find(project, resource_slug_with_branch, branch) 33 | expect(resource).to be_a(TxBranchResource) 34 | expect(resource.resource).to eq(base_resource) 35 | expect(resource.branch).to eq(branch) 36 | end 37 | 38 | it 'returns nil if no resource matches' do 39 | resource = TxBranchResource.find(project, 'foobar', branch) 40 | expect(resource).to be_nil 41 | 42 | resource = TxBranchResource.find(project, resource_slug_with_branch, 'foobar') 43 | expect(resource).to be_nil 44 | end 45 | end 46 | 47 | context 'with a resource' do 48 | let(:resource) do 49 | TxBranchResource.new(base_resource, branch) 50 | end 51 | 52 | describe '#resource_slug' do 53 | it 'adds the branch name to the resource slug' do 54 | expect(resource.resource.resource_slug).to eq(resource_slug) 55 | expect(resource.resource_slug).to eq(resource_slug_with_branch) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/txgh/tx_config.rb: -------------------------------------------------------------------------------- 1 | module Txgh 2 | class TxConfig 3 | class << self 4 | def load_file(path) 5 | config = Txgh::ParseConfig.load_file(path) 6 | parse(config) 7 | end 8 | 9 | def load(contents) 10 | config = Txgh::ParseConfig.load(contents) 11 | parse(config) 12 | end 13 | 14 | def filter_by_project(config, project_name) 15 | res = config 16 | if defined? config.resources 17 | res.resources = config.resources.select { |o| o.project_slug == project_name } 18 | end 19 | res 20 | end 21 | 22 | private 23 | 24 | def parse(config) 25 | resources = [] 26 | lang_map = {} 27 | 28 | config.get_groups.each do |group| 29 | if group == 'main' 30 | main = config[group] 31 | 32 | if main['lang_map'] 33 | lang_map = parse_lang_map(main['lang_map']) 34 | end 35 | else 36 | resources.push( 37 | parse_resource(group, config[group]) 38 | ) 39 | end 40 | end 41 | 42 | new(resources, lang_map) 43 | end 44 | 45 | def parse_lang_map(lang_map) 46 | lang_map.split(',').each_with_object({}) do |m, result| 47 | key_value = m.split(':', 2) 48 | result[key_value[0].strip] = key_value[1].strip 49 | end 50 | end 51 | 52 | def parse_resource(name, resource) 53 | id = name.split('.', 2) 54 | TxResource.new( 55 | id[0].strip, id[1].strip, resource['type'], 56 | resource['source_lang'], resource['source_file'], 57 | resource['lang_map'], resource['file_filter'] 58 | ) 59 | end 60 | end 61 | 62 | attr_reader :resources, :lang_map 63 | attr_writer :resources 64 | 65 | def initialize(resources, lang_map) 66 | @resources = resources 67 | @lang_map = lang_map 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/transifex_request_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rack' 3 | 4 | include Txgh 5 | 6 | describe TransifexRequestAuth do 7 | let(:secret) { 'abc123' } 8 | let(:formdata_params) { 'param1=value1¶m2=value2¶m3=123' } 9 | let(:json_params) { '{"param1": "value1", "param2": "value2", "param3": 123}' } 10 | let(:valid_signature_v1) { 'pXucIcivBezpfNgCGTHKYeDve84=' } 11 | let(:valid_signature_v2) { '6zZG2fkHKFlNTSmckWDa+wUEyhQkPAbhaTxjMiJf23c=' } 12 | let(:date) { 'Fri, 17 Feb 2017 08:24:07 GMT' } 13 | let(:http_verb) { 'POST' } 14 | let(:url) { 'http://www.transifex.com/' } 15 | 16 | describe '.authentic_request?' do 17 | it 'returns true if the request is signed correctly' do 18 | request = Rack::Request.new( 19 | TransifexRequestAuth::RACK_HEADER => valid_signature_v1, 20 | 'rack.input' => StringIO.new(formdata_params) 21 | ) 22 | 23 | authentic = TransifexRequestAuth.authentic_request?(request, secret) 24 | expect(authentic).to eq(true) 25 | end 26 | 27 | it 'returns false if the request is not signed correctly' do 28 | request = Rack::Request.new( 29 | TransifexRequestAuth::RACK_HEADER => 'incorrect', 30 | 'rack.input' => StringIO.new(formdata_params) 31 | ) 32 | 33 | authentic = TransifexRequestAuth.authentic_request?(request, secret) 34 | expect(authentic).to eq(false) 35 | end 36 | end 37 | 38 | describe '.header' do 39 | it 'calculates the V1 signature and formats it as an http header' do 40 | params = {:param1 => 'value1', :param2 => 'value2', :param3 => 123} 41 | value = TransifexRequestAuth.header_value_v1(params, secret) 42 | expect(value).to eq(valid_signature_v1) 43 | end 44 | 45 | it 'calculates the V2 signature and formats it as an http header' do 46 | url = 'http://www.transifex.com/' 47 | value = TransifexRequestAuth.header_value_v2(http_verb, url, date, json_params, secret) 48 | expect(value).to eq(valid_signature_v2) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/handlers/transifex_hook_handler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'helpers/nil_logger' 3 | 4 | include Txgh 5 | include Txgh::Handlers 6 | 7 | describe TransifexHookHandler do 8 | include StandardTxghSetup 9 | 10 | let(:requested_resource_slug) do 11 | resource_slug 12 | end 13 | 14 | let(:handler) do 15 | TransifexHookHandler.new( 16 | project: transifex_project, 17 | repo: github_repo, 18 | resource_slug: requested_resource_slug, 19 | tx_hook_trigger: tx_hook_trigger, 20 | language: language, 21 | logger: logger 22 | ) 23 | end 24 | 25 | before(:each) do 26 | expect(transifex_api).to(receive(:download)) do |resource, language| 27 | expect(resource.project_slug).to eq(project_name) 28 | expect(resource.resource_slug).to eq(requested_resource_slug) 29 | translations 30 | end 31 | end 32 | 33 | it 'downloads translations and pushes them to the correct branch (head)' do 34 | expect(github_api).to( 35 | receive(:commit).with( 36 | repo_name, "heads/#{branch}", "translations/#{language}/sample.po", translations 37 | ) 38 | ) 39 | 40 | handler.execute 41 | end 42 | 43 | context 'when asked to process all branches' do 44 | let(:branch) { 'all' } 45 | let(:ref) { 'heads/my_branch' } 46 | 47 | let(:requested_resource_slug) do 48 | 'my_resource-heads_my_branch' 49 | end 50 | 51 | it 'pushes to the individual branch' do 52 | expect(transifex_api).to receive(:get_resource) do 53 | { 'categories' => ["branch:#{ref}"] } 54 | end 55 | 56 | expect(github_api).to( 57 | receive(:commit).with( 58 | repo_name, ref, "translations/#{language}/sample.po", translations 59 | ) 60 | ) 61 | 62 | handler.execute 63 | end 64 | end 65 | 66 | context 'with a tag instead of a branch' do 67 | let(:branch) { 'tags/my_tag' } 68 | 69 | it 'downloads translations and pushes them to the tag' do 70 | expect(github_api).to( 71 | receive(:commit).with( 72 | repo_name, "tags/my_tag", "translations/#{language}/sample.po", translations 73 | ) 74 | ) 75 | 76 | handler.execute 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/txgh/key_manager.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'etc' 3 | require 'yaml' 4 | 5 | module Txgh 6 | class KeyManager 7 | class << self 8 | def config_from_project(project_name, tx_config = nil) 9 | project_config = project_config_for(project_name) 10 | repo_config = repo_config_for(project_config['push_translations_to']) 11 | tx_config ||= Txgh::TxConfig.load_file(project_config['tx_config']) 12 | tx_config = Txgh::TxConfig.filter_by_project(tx_config,project_name) 13 | Txgh::Config.new(project_config, repo_config, tx_config) 14 | end 15 | 16 | def config_from_repo(repo_name, tx_config = nil) 17 | repo_config = repo_config_for(repo_name) 18 | project_config = project_config_for(repo_config['push_source_to']) 19 | tx_config ||= Txgh::TxConfig.load_file(project_config['tx_config']) 20 | tx_config = Txgh::TxConfig.filter_by_project(tx_config,project_config['name']) 21 | Txgh::Config.new(project_config, repo_config, tx_config) 22 | end 23 | 24 | def config_from(project_name, repo_name, tx_config = nil) 25 | project_config = project_config_for(project_name) 26 | repo_config = repo_config_for(repo_name) 27 | tx_config ||= Txgh::TxConfig.load_file(project_config['tx_config']) 28 | tx_config = Txgh::TxConfig.filter_by_project(tx_config,project_name) 29 | Txgh::Config.new(project_config, repo_config, tx_config) 30 | end 31 | 32 | private :new 33 | 34 | private 35 | 36 | def yaml 37 | path = if File.file?(File.join(Etc.getpwuid.dir, "txgh.yml")) 38 | File.join(Etc.getpwuid.dir, "txgh.yml") 39 | else 40 | File.expand_path('./config/txgh.yml') 41 | end 42 | 43 | YAML.load(ERB.new(File.read(path)).result) 44 | end 45 | 46 | def project_config_for(project_name) 47 | if config = yaml['txgh']['transifex']['projects'][project_name] 48 | config.merge('name' => project_name) 49 | end 50 | end 51 | 52 | def repo_config_for(repo_name) 53 | if config = yaml['txgh']['github']['repos'][repo_name] 54 | config.merge('name' => repo_name) 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/github_api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe GithubApi do 6 | let(:client) { double(:client) } 7 | let(:api) { GithubApi.create_from_client(client) } 8 | let(:repo) { 'my_org/my_repo' } 9 | let(:branch) { 'master' } 10 | let(:sha) { 'abc123' } 11 | 12 | describe '#tree' do 13 | it 'retrieves a git tree using the client' do 14 | expect(client).to receive(:tree).with(repo, sha, recursive: 1) 15 | api.tree(repo, sha) 16 | end 17 | end 18 | 19 | describe '#blob' do 20 | it 'retrieves a git blob using the client' do 21 | expect(client).to receive(:blob).with(repo, sha) 22 | api.blob(repo, sha) 23 | end 24 | end 25 | 26 | describe '#create_ref' do 27 | it 'creates the given ref using the client' do 28 | expect(client).to receive(:create_ref).with(repo, branch, sha) 29 | api.create_ref(repo, branch, sha) 30 | end 31 | 32 | it 'returns false on client error' do 33 | expect(client).to receive(:create_ref).and_raise(StandardError) 34 | expect(api.create_ref(repo, branch, sha)).to eq(false) 35 | end 36 | end 37 | 38 | describe '#commit' do 39 | it 'creates a new blob, tree, and commit, then updates the branch' do 40 | path = 'path/to/translations' 41 | 42 | expect(client).to receive(:create_blob).with(repo, :new_content).and_return(:blob_sha) 43 | expect(client).to receive(:ref).with(repo, branch).and_return(object: { sha: :branch_sha }) 44 | expect(client).to receive(:commit).with(repo, :branch_sha).and_return(commit: { tree: { sha: :base_tree_sha } }) 45 | expect(client).to receive(:create_tree).and_return(sha: :new_tree_sha) 46 | 47 | expect(client).to( 48 | receive(:create_commit) 49 | .with(repo, "Updating translations for #{path}", :new_tree_sha, :branch_sha) 50 | .and_return(sha: :new_commit_sha) 51 | ) 52 | 53 | expect(client).to receive(:update_ref).with(repo, branch, :new_commit_sha, false) 54 | 55 | api.commit(repo, branch, path, :new_content) 56 | end 57 | end 58 | 59 | describe '#get_commit' do 60 | it 'retrieves the given commit using the client' do 61 | expect(client).to receive(:commit).with(repo, sha) 62 | api.get_commit(repo, sha) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/tx_config_multi_project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'pry' 3 | 4 | include Txgh 5 | 6 | describe TxConfig do 7 | describe '.load' do 8 | it 'parses the config correctly' do 9 | config_str = """ 10 | [main] 11 | host = https://www.transifex.com 12 | lang_map = pt-BR:pt, ko-KR:ko 13 | 14 | [my_proj.my_resource] 15 | file_filter = translations//sample.po 16 | source_file = sample.po 17 | source_lang = en 18 | type = PO 19 | 20 | [my_proj.my_second_resource] 21 | file_filter = translations//second_sample.po 22 | source_file = second_sample.po 23 | source_lang = en 24 | type = PO 25 | 26 | [my_second_proj.my_resource] 27 | file_filter = translations/my_second_proj//sample.po 28 | source_file = sample.po 29 | source_lang = en 30 | type = PO 31 | """ 32 | 33 | config = TxConfig.load(config_str) 34 | expect(config.lang_map).to eq('pt-BR' => 'pt', 'ko-KR' => 'ko') 35 | expect(config.resources.size).to eq(3) 36 | 37 | # binding.pry 38 | resource = config.resources.first 39 | expect(resource.project_slug).to eq('my_proj') 40 | expect(resource.resource_slug).to eq('my_resource') 41 | expect(resource.source_file).to eq('sample.po') 42 | expect(resource.source_lang).to eq('en') 43 | expect(resource.translation_file).to eq('translations//sample.po') 44 | expect(resource.type).to eq('PO') 45 | 46 | resource = config.resources[1] 47 | expect(resource.project_slug).to eq('my_proj') 48 | expect(resource.resource_slug).to eq('my_second_resource') 49 | expect(resource.source_file).to eq('second_sample.po') 50 | expect(resource.source_lang).to eq('en') 51 | expect(resource.translation_file).to eq('translations//second_sample.po') 52 | expect(resource.type).to eq('PO') 53 | 54 | resource = config.resources.last 55 | expect(resource.project_slug).to eq('my_second_proj') 56 | expect(resource.resource_slug).to eq('my_resource') 57 | expect(resource.source_file).to eq('sample.po') 58 | expect(resource.source_lang).to eq('en') 59 | expect(resource.translation_file).to eq('translations/my_second_proj//sample.po') 60 | expect(resource.type).to eq('PO') 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.5.2) 5 | public_suffix (>= 2.0.2, < 4.0) 6 | backports (3.11.3) 7 | coderay (1.1.2) 8 | crack (0.4.3) 9 | safe_yaml (~> 1.0.0) 10 | diff-lcs (1.3) 11 | faraday (0.15.2) 12 | multipart-post (>= 1.2, < 3) 13 | faraday_middleware (0.12.2) 14 | faraday (>= 0.7.4, < 1.0) 15 | hashdiff (0.3.7) 16 | json (2.1.0) 17 | method_source (0.8.2) 18 | multi_json (1.13.1) 19 | multipart-post (2.0.0) 20 | octokit (4.9.0) 21 | sawyer (~> 0.8.0, >= 0.5.3) 22 | parseconfig (1.0.8) 23 | pry (0.10.4) 24 | coderay (~> 1.1.0) 25 | method_source (~> 0.8.1) 26 | slop (~> 3.4) 27 | pry-nav (0.2.4) 28 | pry (>= 0.9.10, < 0.11.0) 29 | public_suffix (3.0.2) 30 | puma (3.11.4) 31 | rack (1.6.10) 32 | rack-protection (1.5.5) 33 | rack 34 | rack-test (0.7.0) 35 | rack (>= 1.0, < 3) 36 | rake (12.3.1) 37 | rspec (3.7.0) 38 | rspec-core (~> 3.7.0) 39 | rspec-expectations (~> 3.7.0) 40 | rspec-mocks (~> 3.7.0) 41 | rspec-core (3.7.1) 42 | rspec-support (~> 3.7.0) 43 | rspec-expectations (3.7.0) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.7.0) 46 | rspec-mocks (3.7.0) 47 | diff-lcs (>= 1.2.0, < 2.0) 48 | rspec-support (~> 3.7.0) 49 | rspec-support (3.7.1) 50 | safe_yaml (1.0.4) 51 | sawyer (0.8.1) 52 | addressable (>= 2.3.5, < 2.6) 53 | faraday (~> 0.8, < 1.0) 54 | shotgun (0.9.2) 55 | rack (>= 1.0) 56 | sinatra (1.4.8) 57 | rack (~> 1.5) 58 | rack-protection (~> 1.4) 59 | tilt (>= 1.3, < 3) 60 | sinatra-contrib (1.4.7) 61 | backports (>= 2.0) 62 | multi_json 63 | rack-protection 64 | rack-test 65 | sinatra (~> 1.4.0) 66 | tilt (>= 1.3, < 3) 67 | slop (3.6.0) 68 | tilt (2.0.8) 69 | vcr (3.0.3) 70 | webmock (1.24.6) 71 | addressable (>= 2.3.6) 72 | crack (>= 0.3.2) 73 | hashdiff 74 | 75 | PLATFORMS 76 | ruby 77 | 78 | DEPENDENCIES 79 | faraday 80 | faraday_middleware 81 | json 82 | octokit 83 | parseconfig 84 | pry-nav 85 | puma 86 | rack 87 | rack-test 88 | rake 89 | rspec 90 | shotgun 91 | sinatra 92 | sinatra-contrib 93 | vcr (~> 3.0.1) 94 | webmock (~> 1.24.6) 95 | 96 | BUNDLED WITH 97 | 1.15.1 98 | -------------------------------------------------------------------------------- /spec/app_multi_project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'helpers/github_payload_builder' 3 | require 'rack/test' 4 | require 'uri' 5 | 6 | 7 | describe Txgh::Hooks do 8 | include Rack::Test::Methods 9 | include StandardTxghSetup 10 | 11 | def app 12 | # this insanity is necessary to allow the tests to stub helper methods 13 | @app ||= Class.new(Txgh::Hooks) do 14 | def call(env) 15 | # don't let sinatra dup us before calling 16 | call!(env) 17 | end 18 | end.new! # new bang gives us a raw instance (Sinatra redefines `new`) 19 | end 20 | 21 | let(:config) do 22 | Txgh::Config.new(second_project_config, second_repo_config, tx_config_multi_project) 23 | end 24 | 25 | before(:each) do 26 | allow(Txgh::KeyManager).to( 27 | receive(:config_from_project).with('my_second_awesome_project').and_return(config) 28 | ) 29 | 30 | allow(Txgh::KeyManager).to( 31 | receive(:config_from_repo).with('my_org/my_second_repo').and_return(config) 32 | ) 33 | end 34 | 35 | describe '/transifex' do 36 | let(:handler) { double(:handler) } 37 | 38 | it 'creates a handler and executes it' do 39 | expect(app).to( 40 | receive(:transifex_handler_for) do |options| 41 | expect(options[:project].name).to eq('my_second_awesome_project') 42 | expect(options[:repo].name).to eq('my_org/my_second_repo') 43 | handler 44 | end 45 | ) 46 | 47 | expect(handler).to receive(:execute) 48 | 49 | params = { 50 | 'project' => 'my_second_awesome_project', 51 | 'resource' => resource_slug, 52 | 'language' => language, 53 | 'translated' => '100' 54 | } 55 | 56 | payload = URI.encode_www_form(params.to_a) 57 | post '/transifex', payload 58 | end 59 | end 60 | 61 | describe '/github' do 62 | let(:handler) { double(:handler) } 63 | 64 | it 'creates a handler and executes it' do 65 | expect(app).to( 66 | receive(:github_handler_for) do |options| 67 | expect(options[:project].name).to eq('my_second_awesome_project') 68 | expect(options[:repo].name).to eq('my_org/my_second_repo') 69 | handler 70 | end 71 | ) 72 | 73 | expect(handler).to receive(:execute) 74 | 75 | payload = GithubPayloadBuilder.webhook_payload('my_org/my_second_repo', ref) 76 | 77 | post '/github', { 78 | 'payload' => payload.to_json 79 | } 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/txgh/transifex_request_auth.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Txgh 4 | class TransifexRequestAuth 5 | HMAC_DIGEST = OpenSSL::Digest.new('sha1') 6 | HMAC_DIGEST_256 = OpenSSL::Digest.new('sha256') 7 | RACK_HEADER = 'HTTP_X_TX_SIGNATURE' 8 | RACK_HEADER_V2 = 'HTTP_X_TX_SIGNATURE_V2' 9 | TRANSIFEX_HEADER = 'X-TX-Signature' 10 | 11 | class << self 12 | def authentic_request?(request, secret) 13 | request.body.rewind 14 | content = request.body.read 15 | http_verb = request.request_method 16 | url = request.url 17 | date = request.env['HTTP_DATE'] 18 | if request.env['CONTENT_TYPE'] == "application/json" 19 | params = JSON.parse(content) 20 | else 21 | params = URI.decode_www_form(content) 22 | end 23 | expected_signature_v1 = header_value_v1(params, secret) 24 | expected_signature_v2 = header_value_v2(http_verb, url, date, content, secret) 25 | actual_signature_v1 = request.env[RACK_HEADER] 26 | actual_signature_v2 = request.env[RACK_HEADER_V2] 27 | actual_signature_v1 == expected_signature_v1 or actual_signature_v2 == expected_signature_v2 28 | end 29 | 30 | def header_value_v1(params, secret) 31 | digest(HMAC_DIGEST, secret, transform(params)) 32 | end 33 | 34 | def header_value_v2(http_verb, url, date, content, secret) 35 | content_md5 = Digest::MD5.hexdigest content 36 | data = [http_verb, url, date, content_md5].join("\n") 37 | digest(HMAC_DIGEST_256, secret, data) 38 | end 39 | 40 | private 41 | 42 | # In order to generate a correct HMAC hash, the request body must be 43 | # parsed and made to look like a python map. If you're thinking that's 44 | # weird, you're correct, but it's apparently expected behavior. 45 | def transform(params) 46 | params = params.map do |key, val| 47 | key = "'#{key}'" 48 | val = interpret_val(val) 49 | "#{key}: #{val}" 50 | end 51 | 52 | "{#{params.join(', ')}}" 53 | end 54 | 55 | def interpret_val(val) 56 | val = "#{val}" 57 | if val =~ /\A[\d]+\z/ 58 | val 59 | else 60 | "u'#{val}'" 61 | end 62 | end 63 | 64 | def digest(hmac_digest, secret, content) 65 | Base64.encode64( 66 | OpenSSL::HMAC.digest(hmac_digest, secret, content) 67 | ).strip 68 | end 69 | 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/key_manager_multi_project_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe KeyManager do 6 | include StandardTxghSetup 7 | 8 | before(:each) do 9 | allow(KeyManager).to receive(:yaml) { yaml_config } 10 | end 11 | 12 | describe '.config_from_project' do 13 | it 'creates a config object from first project' do 14 | config = KeyManager.config_from_project(project_name, tx_config_multi_project) 15 | expect(config).to be_a(Txgh::Config) 16 | end 17 | 18 | it 'creates a config object from second project' do 19 | config = KeyManager.config_from_project('my_second_awesome_project', tx_config_multi_project) 20 | expect(config).to be_a(Txgh::Config) 21 | end 22 | 23 | it 'creates a config object that contains both project and repo configs' do 24 | config = KeyManager.config_from_project('my_second_awesome_project', tx_config_multi_project) 25 | expect(config.project_config).to eq(second_project_config) 26 | expect(config.repo_config).to eq(second_repo_config) 27 | expect(config.tx_config).to be_a(TxConfig) 28 | expect(config.tx_config.resources.first.project_slug).to eq('my_second_awesome_project') 29 | end 30 | end 31 | 32 | 33 | describe '.config_from_repo' do 34 | it 'creates a config object' do 35 | config = KeyManager.config_from_repo('my_org/my_second_repo', tx_config_multi_project) 36 | expect(config).to be_a(Txgh::Config) 37 | end 38 | 39 | it 'creates a config object that contains both project and repo configs' do 40 | config = KeyManager.config_from_repo('my_org/my_second_repo', tx_config_multi_project) 41 | expect(config.project_config).to eq(second_project_config) 42 | expect(config.repo_config).to eq(second_repo_config) 43 | expect(config.tx_config).to be_a(TxConfig) 44 | expect(config.tx_config.resources.first.project_slug).to eq('my_second_awesome_project') 45 | end 46 | end 47 | 48 | 49 | describe '.config_from' do 50 | it 'creates a config object' do 51 | config = KeyManager.config_from('my_second_awesome_project', 'my_org/my_second_repo', tx_config_multi_project) 52 | expect(config).to be_a(Txgh::Config) 53 | end 54 | 55 | it 'creates a config object that contains both project and repo configs' do 56 | config = KeyManager.config_from('my_second_awesome_project', 'my_org/my_second_repo', tx_config_multi_project) 57 | expect(config.project_config).to eq(second_project_config) 58 | expect(config.repo_config).to eq(second_repo_config) 59 | expect(config.tx_config).to be_a(TxConfig) 60 | expect(config.tx_config.resources.first.project_slug).to eq('my_second_awesome_project') 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/helpers/github_payload_builder.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class GithubPayloadBuilder 4 | class << self 5 | def webhook_payload(*args) 6 | GithubWebhookPayload.new(*args) 7 | end 8 | end 9 | end 10 | 11 | class GithubPayload 12 | def to_h 13 | # convert symbolized keys to strings 14 | JSON.parse(to_json) 15 | end 16 | 17 | def to_json 18 | @result.to_json 19 | end 20 | 21 | protected 22 | 23 | def digits 24 | @@digits ||= ('a'..'f').to_a + ('0'..'9').to_a 25 | end 26 | 27 | def generate_timestamp 28 | Time.now.strftime('%Y-%m-%dT%H:%M:%S%:z') 29 | end 30 | 31 | def generate_sha 32 | blank_commit_id.gsub(/0/) { digits.sample } 33 | end 34 | 35 | def blank_commit_id 36 | '0' * 40 37 | end 38 | end 39 | 40 | class GithubWebhookPayload < GithubPayload 41 | attr_reader :repo, :ref, :before, :after 42 | 43 | DEFAULT_USER = { 44 | name: 'Test User', 45 | email: 'test@user.com', 46 | username: 'testuser' 47 | } 48 | 49 | def initialize(repo, ref, before = nil, after = nil) 50 | @repo = repo 51 | @ref = ref 52 | @before = before || blank_commit_id 53 | @after = after || generate_sha 54 | 55 | @result = { 56 | ref: "refs/#{ref}", 57 | before: @before, 58 | after: @after, 59 | created: true, 60 | deleted: false, 61 | forced: true, 62 | base_ref: nil, 63 | compare: "https://github.com/#{@repo}/commit/#{@after[0..12]}", 64 | commits: [], 65 | repository: { 66 | name: repo.split('/').last, 67 | full_name: repo, 68 | owner: { 69 | name: repo.split('/').first 70 | } 71 | } 72 | } 73 | end 74 | 75 | def add_commit(options = {}) 76 | id = if commits.empty? && !options.include?(:id) 77 | after 78 | else 79 | options.fetch(:id) { generate_sha } 80 | end 81 | 82 | commit_data = { 83 | id: id, 84 | distinct: options.fetch(:distinct, true), 85 | message: options.fetch(:message, 'Default commit message'), 86 | timestamp: options.fetch(:timestamp) { generate_timestamp }, 87 | url: "https://github.com/#{repo}/commit/#{id}", 88 | author: options.fetch(:author, DEFAULT_USER), 89 | committer: options.fetch(:committer, DEFAULT_USER), 90 | added: options.fetch(:added, []), 91 | removed: options.fetch(:removed, []), 92 | modified: options.fetch(:modified, []) 93 | } 94 | 95 | if commit_data[:id] == after 96 | @result[:head_commit] = commit_data 97 | end 98 | 99 | commits << commit_data 100 | end 101 | 102 | def commits 103 | @result[:commits] 104 | end 105 | 106 | def head_commit 107 | @result[:head_commit] 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'helpers/github_payload_builder' 3 | require 'rack/test' 4 | require 'uri' 5 | 6 | describe Txgh::Application do 7 | include Rack::Test::Methods 8 | 9 | def app 10 | Txgh::Application 11 | end 12 | 13 | describe '/health_check' do 14 | it 'does not allow requests with no credentials' do 15 | get '/health_check' 16 | expect(last_response.status).to eq(401) 17 | end 18 | 19 | it 'does not allow invalid credentials' do 20 | authorize 'bad', 'wrong' 21 | get '/health_check' 22 | expect(last_response.status).to eq(401) 23 | end 24 | 25 | it 'indicates the server is running, returns a 200' do 26 | authorize 'foo', 'bar' 27 | get '/health_check' 28 | expect(last_response).to be_ok 29 | expect(last_response.body).to be_empty 30 | end 31 | end 32 | end 33 | 34 | describe Txgh::Hooks do 35 | include Rack::Test::Methods 36 | include StandardTxghSetup 37 | 38 | def app 39 | # this insanity is necessary to allow the tests to stub helper methods 40 | @app ||= Class.new(Txgh::Hooks) do 41 | def call(env) 42 | # don't let sinatra dup us before calling 43 | call!(env) 44 | end 45 | end.new! # new bang gives us a raw instance (Sinatra redefines `new`) 46 | end 47 | 48 | let(:config) do 49 | Txgh::Config.new(project_config, repo_config, tx_config) 50 | end 51 | 52 | before(:each) do 53 | allow(Txgh::KeyManager).to( 54 | receive(:config_from_project).with(project_name).and_return(config) 55 | ) 56 | 57 | allow(Txgh::KeyManager).to( 58 | receive(:config_from_repo).with(repo_name).and_return(config) 59 | ) 60 | end 61 | 62 | describe '/transifex' do 63 | let(:handler) { double(:handler) } 64 | 65 | it 'creates a handler and executes it' do 66 | expect(app).to( 67 | receive(:transifex_handler_for) do |options| 68 | expect(options[:project].name).to eq(project_name) 69 | expect(options[:repo].name).to eq(repo_name) 70 | handler 71 | end 72 | ) 73 | 74 | expect(handler).to receive(:execute) 75 | 76 | params = { 77 | 'project' => project_name, 78 | 'resource' => resource_slug, 79 | 'language' => language, 80 | 'translated' => '100' 81 | } 82 | 83 | payload = URI.encode_www_form(params.to_a) 84 | post '/transifex', payload 85 | end 86 | end 87 | 88 | describe '/github' do 89 | let(:handler) { double(:handler) } 90 | 91 | it 'creates a handler and executes it' do 92 | expect(app).to( 93 | receive(:github_handler_for) do |options| 94 | expect(options[:project].name).to eq(project_name) 95 | expect(options[:repo].name).to eq(repo_name) 96 | handler 97 | end 98 | ) 99 | 100 | expect(handler).to receive(:execute) 101 | 102 | payload = GithubPayloadBuilder.webhook_payload(repo_name, ref) 103 | 104 | post '/github', { 105 | 'payload' => payload.to_json 106 | } 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /spec/integration/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'json' 4 | require 'pathname' 5 | require 'rack/test' 6 | require 'uri' 7 | 8 | include Txgh 9 | 10 | describe 'integration tests', integration: true do 11 | include Rack::Test::Methods 12 | 13 | def app 14 | @app ||= Txgh::Hooks.new 15 | end 16 | 17 | around(:each) do |example| 18 | Dir.chdir('./spec/integration') do 19 | example.run 20 | end 21 | end 22 | 23 | let(:payload_path) do 24 | Pathname(File.dirname(__FILE__)).join('payloads') 25 | end 26 | 27 | let(:github_postbody) do 28 | File.read(payload_path.join('github_postbody.json')) 29 | end 30 | 31 | let(:github_postbody_release) do 32 | File.read(payload_path.join('github_postbody_release.json')) 33 | end 34 | 35 | let(:github_postbody_l10n) do 36 | File.read(payload_path.join('github_postbody_l10n.json')) 37 | end 38 | 39 | let(:project_name) { 'test-project-88' } 40 | let(:repo_name) { 'txgh-bot/txgh-test-resources' } 41 | 42 | let(:config) do 43 | Txgh::KeyManager.config_from(project_name, repo_name) 44 | end 45 | 46 | def sign_github_request(body) 47 | header( 48 | GithubRequestAuth::GITHUB_HEADER, 49 | GithubRequestAuth.header_value(body, config.github_repo.webhook_secret) 50 | ) 51 | end 52 | 53 | def sign_transifex_request(body) 54 | params = URI.decode_www_form(body) 55 | header( 56 | TransifexRequestAuth::TRANSIFEX_HEADER, 57 | TransifexRequestAuth.header_value_v1(params, config.transifex_project.webhook_secret) 58 | ) 59 | end 60 | 61 | it 'loads correct project config' do 62 | expect(config.project_config).to_not be_nil 63 | end 64 | 65 | it 'verifies the transifex hook endpoint works' do 66 | VCR.use_cassette('transifex_hook_endpoint') do 67 | params = { 68 | 'project' => 'test-project-88', 'resource' => 'samplepo', 69 | 'language' => 'el_GR', 'translated' => 100 70 | } 71 | 72 | payload = URI.encode_www_form(params.to_a) 73 | 74 | sign_transifex_request(payload) 75 | post '/transifex', payload 76 | expect(last_response).to be_ok 77 | end 78 | end 79 | 80 | it 'verifies the github hook endpoint works' do 81 | VCR.use_cassette('github_hook_endpoint') do 82 | sign_github_request(github_postbody) 83 | header 'content-type', 'application/x-www-form-urlencoded' 84 | post '/github', github_postbody 85 | expect(last_response).to be_ok 86 | end 87 | end 88 | 89 | it 'verifies the github release hook endpoint works' do 90 | VCR.use_cassette('github_release_hook_endpoint') do 91 | sign_github_request(github_postbody_release) 92 | header 'content-type', 'application/x-www-form-urlencoded' 93 | post '/github', github_postbody_release 94 | expect(last_response).to be_ok 95 | end 96 | end 97 | 98 | it 'verifies the github l10n hook endpoint works' do 99 | VCR.use_cassette('github_l10n_hook_endpoint') do 100 | sign_github_request(github_postbody_l10n) 101 | header 'content-type', 'application/x-www-form-urlencoded' 102 | post '/github', github_postbody_l10n 103 | expect(last_response).to be_ok 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/key_manager_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe KeyManager do 6 | include StandardTxghSetup 7 | 8 | before(:each) do 9 | allow(KeyManager).to receive(:yaml) { yaml_config } 10 | end 11 | 12 | describe '.config_from_project' do 13 | it 'creates a config object' do 14 | config = KeyManager.config_from_project(project_name, tx_config) 15 | expect(config).to be_a(Txgh::Config) 16 | end 17 | 18 | it 'creates a config object that contains both project and repo configs' do 19 | config = KeyManager.config_from_project(project_name, tx_config) 20 | expect(config.project_config).to eq(project_config) 21 | expect(config.repo_config).to eq(repo_config) 22 | expect(config.tx_config).to be_a(TxConfig) 23 | expect(config.tx_config.resources.first.project_slug).to eq(project_name) 24 | end 25 | 26 | it 'loads tx config from the given file if not explicitly passed in' do 27 | path = 'path/to/tx_config' 28 | project_config.merge!('tx_config' => path) 29 | expect(TxConfig).to receive(:load_file).with(path).and_return(:tx_config) 30 | config = KeyManager.config_from_project(project_name) 31 | expect(config.tx_config).to eq(:tx_config) 32 | end 33 | end 34 | 35 | describe '.config_from_repo' do 36 | it 'creates a config object' do 37 | config = KeyManager.config_from_repo(repo_name, tx_config) 38 | expect(config).to be_a(Txgh::Config) 39 | end 40 | 41 | it 'creates a config object that contains both project and repo configs' do 42 | config = KeyManager.config_from_repo(repo_name, tx_config) 43 | expect(config.project_config).to eq(project_config) 44 | expect(config.repo_config).to eq(repo_config) 45 | expect(config.tx_config).to be_a(TxConfig) 46 | expect(config.tx_config.resources.first.project_slug).to eq(project_name) 47 | end 48 | 49 | it 'loads tx config from the given file if not explicitly passed in' do 50 | path = 'path/to/tx_config' 51 | project_config.merge!('tx_config' => path) 52 | expect(TxConfig).to receive(:load_file).with(path).and_return(:tx_config) 53 | config = KeyManager.config_from_repo(repo_name) 54 | expect(config.tx_config).to eq(:tx_config) 55 | end 56 | end 57 | 58 | describe '.config_from' do 59 | it 'creates a config object' do 60 | config = KeyManager.config_from(project_name, repo_name, tx_config) 61 | expect(config).to be_a(Txgh::Config) 62 | end 63 | 64 | it 'creates a config object that contains both project and repo configs' do 65 | config = KeyManager.config_from(project_name, repo_name, tx_config) 66 | expect(config.project_config).to eq(project_config) 67 | expect(config.repo_config).to eq(repo_config) 68 | expect(config.tx_config).to be_a(TxConfig) 69 | expect(config.tx_config.resources.first.project_slug).to eq(project_name) 70 | end 71 | 72 | it 'loads tx config from the given file if not explicitly passed in' do 73 | path = 'path/to/tx_config' 74 | project_config.merge!('tx_config' => path) 75 | expect(TxConfig).to receive(:load_file).with(path).and_return(:tx_config) 76 | config = KeyManager.config_from(project_name, repo_name) 77 | expect(config.tx_config).to eq(:tx_config) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/txgh/handlers/transifex_hook_handler.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Txgh 4 | module Handlers 5 | class TransifexHookHandler 6 | include Txgh::CategorySupport 7 | 8 | attr_reader :project, :repo, :resource_slug, :language, :tx_hook_trigger, :logger 9 | 10 | def initialize(options = {}) 11 | @project = options.fetch(:project) 12 | @repo = options.fetch(:repo) 13 | @resource_slug = options.fetch(:resource_slug) 14 | @language = options.fetch(:language) 15 | @tx_hook_trigger = options.fetch(:tx_hook_trigger) 16 | @logger = options.fetch(:logger) { Logger.new(STDOUT) } 17 | end 18 | 19 | def execute 20 | logger.info(resource_slug) 21 | # Check if push trigger is set in project config 22 | if project.push_trigger_set? 23 | trigger = project.push_trigger 24 | # If not set trigger to current payload trigger value 25 | else 26 | trigger = tx_hook_trigger 27 | end 28 | # Only execute if trigger matches TX hook trigger 29 | if tx_resource && tx_hook_trigger==trigger 30 | # Do not update the source 31 | unless language == tx_resource.source_lang 32 | logger.info('request language matches resource') 33 | 34 | translations = project.api.download(tx_resource, language) 35 | 36 | translation_path = if tx_resource.lang_map(language) != language 37 | logger.info('request language is in lang_map and is not in request') 38 | tx_resource.translation_path(tx_resource.lang_map(language)) 39 | else 40 | logger.info('request language is in lang_map and is in request or is nil') 41 | tx_resource.translation_path(project.lang_map(language)) 42 | end 43 | 44 | logger.info("make github commit for branch: #{branch}") 45 | 46 | repo.api.commit( 47 | repo.name, branch, translation_path, translations 48 | ) 49 | end 50 | elsif 51 | tx_hook_trigger!=project.push_trigger 52 | logger.info("did not process changes because trigger was '#{tx_hook_trigger}' and push trigger was set to '#{project.push_trigger}'") 53 | else 54 | raise TxghError, 55 | "Could not find configuration for resource '#{resource_slug}'" 56 | end 57 | end 58 | 59 | private 60 | 61 | def branch 62 | branch_candidate = if process_all_branches? 63 | tx_resource.branch 64 | else 65 | repo.branch || 'master' 66 | end 67 | 68 | if branch_candidate.include?('tags/') 69 | branch_candidate 70 | elsif branch_candidate.include?('heads/') 71 | branch_candidate 72 | else 73 | "heads/#{branch_candidate}" 74 | end 75 | end 76 | 77 | def tx_resource 78 | @tx_resource ||= if process_all_branches? 79 | resource = project.api.get_resource(project.name, resource_slug) 80 | categories = deserialize_categories(Array(resource['categories'])) 81 | project.resource(resource_slug, categories['branch']) 82 | else 83 | project.resource(resource_slug) 84 | end 85 | end 86 | 87 | def process_all_branches? 88 | repo.branch == 'all' 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/handlers/github_hook_handler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'helpers/github_payload_builder' 3 | require 'helpers/nil_logger' 4 | 5 | include Txgh 6 | include Txgh::Handlers 7 | 8 | describe GithubHookHandler do 9 | include StandardTxghSetup 10 | 11 | let(:handler) do 12 | GithubHookHandler.new( 13 | project: transifex_project, 14 | repo: github_repo, 15 | payload: payload.to_h, 16 | logger: logger 17 | ) 18 | end 19 | 20 | let(:payload) do 21 | GithubPayloadBuilder.webhook_payload(repo_name, ref) 22 | end 23 | 24 | let(:modified_files) do 25 | file_sha = 'def456' 26 | transifex_project.resources.map do |resource| 27 | { 'path' => resource.source_file, 'sha' => file_sha.next! } 28 | end 29 | end 30 | 31 | def translations_for(path) 32 | "translations for #{path}" 33 | end 34 | 35 | before(:each) do 36 | tree_sha = 'abc123' 37 | 38 | # indicate that all the files we care about have changed 39 | payload.add_commit( 40 | modified: modified_files.map { |f| f['path'] } 41 | ) 42 | 43 | allow(github_api).to( 44 | receive(:get_commit).with(repo_name, payload.commits.first[:id]) do 45 | { 'commit' => { 'tree' => { 'sha' => tree_sha } } } 46 | end 47 | ) 48 | 49 | allow(github_api).to( 50 | receive(:tree).with(repo_name, tree_sha) do 51 | { 'tree' => modified_files } 52 | end 53 | ) 54 | 55 | modified_files.each do |file| 56 | translations = translations_for(file['path']) 57 | 58 | allow(github_api).to( 59 | receive(:blob).with(repo_name, file['sha']) do 60 | { 'content' => translations, 'encoding' => 'utf-8' } 61 | end 62 | ) 63 | end 64 | end 65 | 66 | it 'correctly uploads modified files to transifex' do 67 | modified_files.each do |file| 68 | translations = translations_for(file['path']) 69 | 70 | expect(transifex_api).to( 71 | receive(:create_or_update) do |resource, content| 72 | expect(resource.source_file).to eq(file['path']) 73 | expect(content).to eq(translations) 74 | end 75 | ) 76 | end 77 | 78 | handler.execute 79 | end 80 | 81 | context 'when asked to process all branches' do 82 | let(:branch) { 'all' } 83 | 84 | it 'uploads by branch name if asked' do 85 | allow(transifex_api).to receive(:resource_exists?).and_return(false) 86 | 87 | modified_files.each do |file| 88 | translations = translations_for(file['path']) 89 | 90 | expect(transifex_api).to( 91 | receive(:create) do |resource, content, categories| 92 | expect(resource.source_file).to eq(file['path']) 93 | expect(content).to eq(translations) 94 | expect(categories).to include("branch:#{ref}") 95 | expect(categories).to include("author:Test_User") 96 | end 97 | ) 98 | end 99 | 100 | handler.execute 101 | end 102 | end 103 | 104 | context 'with an L10N branch' do 105 | let(:ref) { 'tags/L10N_my_branch' } 106 | 107 | it 'creates an L10N tag' do 108 | modified_files.each do |file| 109 | allow(github_api).to( 110 | receive(:blob).and_return('content' => '') 111 | ) 112 | 113 | allow(transifex_api).to receive(:create_or_update) 114 | end 115 | 116 | # this is what we actually care about in this test 117 | expect(github_api).to( 118 | receive(:create_ref).with( 119 | repo_name, 'heads/L10N', payload.head_commit[:id] 120 | ) 121 | ) 122 | 123 | handler.execute 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /docs/aws.md: -------------------------------------------------------------------------------- 1 | Implementation on AWS EC2 2 | ========================= 3 | 4 | What you need: 5 | 6 | - A [Amazon EC2 instance](http://aws.amazon.com/ec2/). The basic Amazon Linux AMI should be enough. It comes with Ruby, Git and pretty much all you need. If you don't want to use EC2, you can use any kind of server with a recent version of Ruby installed with the ability to receive and send HTTP API traffic from the internet. 7 | 8 | - Administrator access to your Transifex project. 9 | 10 | - The ability to add Service Hooks to your Github repo. 11 | 12 | Once you've got your EC2 instance, [connect to it using ssh](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html). You'll do all your work from there. Take a note of its public DNS name, as you'll need it later. 13 | 14 | # Make sure you have all the build tools installed 15 | sudo yum groupinstall "Development Tools" 16 | 17 | # Install dependencies 18 | sudo yum install -y gcc-c++ patch readline readline-devel zlib \ 19 | zlib-devel libyaml-devel libffi-devel openssl-devel make bzip2 \ 20 | autoconf automake libtool bison iconv-devel 21 | 22 | # Install RVM 23 | bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer) 24 | 25 | # Install Ruby 26 | rvm install 1.9.3 27 | 28 | # Install Bundler 29 | gem install bundler --no-rdoc --no-ri 30 | 31 | # Clone txgh 32 | git clone https://github.com/jsilland/txgh.git 33 | 34 | # Edit txgh config file 35 | cd txgh 36 | vim config/txgh.yml 37 | 38 | Here is a sample yml file which you can fit to your configuration: 39 | 40 | txgh: 41 | github: 42 | repos: 43 | : 44 | api_username: 45 | api_token: 46 | push_source_to: 47 | transifex: 48 | projects: 49 | : 50 | tx_config: "/path/to/.tx/config, see below if you do not have any" 51 | api_username: 52 | api_password: 53 | push_translations_to: 54 | 55 | 56 | If your Transifex project currently uses the Transifex Command Line Client, you probably have a Transifex config file checked into your repo. Its default location is under a `.tx/` folder in the root of your git repo. If it doesn't contain one, use [this support article](http://docs.transifex.com/client/setup#installation) to create one, or use this template: 57 | 58 | 59 | [main] 60 | host = https://www.transifex.com 61 | 62 | [.] 63 | file_filter = ./Where/Translated//Files.Are 64 | source_file = ./Where/Source/Files.Are 65 | source_lang = 66 | type = 67 | 68 | Finally, start the server: 69 | 70 | # install bundled gems 71 | bundle install 72 | 73 | # start the server 74 | bundle exec rackup 75 | Puma 2.5.1 starting... 76 | * Min threads: 0, max threads: 16 77 | * Environment: development 78 | * Listening on tcp://0.0.0.0:9292 79 | 80 | Now, you can keep the server running and go configure the webhooks in Transifex and in Github: 81 | 82 | How to [configure webhooks in Github](https://help.github.com/articles/post-receive-hooks). You will want to point the new service hook you've created to: 83 | 84 | http://:9292/hooks/github 85 | 86 | To configure your webhooks in Transifex, you will need to go to your project management page and point the webhook URL to: 87 | 88 | http://:9292/hooks/transifex 89 | 90 | That's it! 91 | 92 | While this starts the server in development mode in a EC2 server, if you do any kind of larger scale development, you would probably want to run this on a more stable instance, in production mode, with appropriate monitoring. But once you've configured the webhooks, any change that makes a file be 100% translated in Transifex will trigger the server to push a new commit to Github with the updated translations files and any change in Github to the source files will trigger the server to update the source content in Transifex. 93 | 94 | -------------------------------------------------------------------------------- /lib/txgh/transifex_api.rb: -------------------------------------------------------------------------------- 1 | require 'faraday' 2 | require 'faraday_middleware' 3 | require 'json' 4 | require 'set' 5 | 6 | module Txgh 7 | class TransifexApi 8 | API_ROOT = '/api/2' 9 | 10 | class << self 11 | def create_from_credentials(username, password) 12 | connection = Faraday.new(url: 'https://www.transifex.com') do |faraday| 13 | faraday.request(:multipart) 14 | faraday.request(:json) 15 | faraday.request(:url_encoded) 16 | 17 | faraday.response(:logger) 18 | faraday.use(FaradayMiddleware::FollowRedirects) 19 | faraday.adapter(Faraday.default_adapter) 20 | end 21 | 22 | connection.basic_auth(username, password) 23 | connection.headers.update(Accept: 'application/json') 24 | create_from_connection(connection) 25 | end 26 | 27 | def create_from_connection(connection) 28 | new(connection) 29 | end 30 | end 31 | 32 | attr_reader :connection 33 | 34 | def initialize(connection) 35 | @connection = connection 36 | end 37 | 38 | def create_or_update(tx_resource, content, categories = []) 39 | if resource_exists?(tx_resource) 40 | resource = get_resource(*tx_resource.slugs) 41 | new_categories = Set.new(resource['categories']) 42 | new_categories.merge(categories) 43 | 44 | # update details first so new content is always tagged 45 | update_details(tx_resource, categories: new_categories.to_a) 46 | update_content(tx_resource, content) 47 | else 48 | create(tx_resource, content, categories) 49 | end 50 | end 51 | 52 | def create(tx_resource, content, categories = []) 53 | payload = { 54 | slug: tx_resource.resource_slug, 55 | name: tx_resource.source_file, 56 | i18n_type: tx_resource.type, 57 | categories: CategorySupport.join_categories(categories.uniq), 58 | content: get_content_io(tx_resource, content) 59 | } 60 | 61 | url = "#{API_ROOT}/project/#{tx_resource.project_slug}/resources/" 62 | response = connection.post(url, payload) 63 | raise_error!(response) 64 | end 65 | 66 | def update_content(tx_resource, content) 67 | content_io = get_content_io(tx_resource, content) 68 | payload = { content: content_io } 69 | url = "#{API_ROOT}/project/#{tx_resource.project_slug}/resource/#{tx_resource.resource_slug}/content/" 70 | response = connection.put(url, payload) 71 | raise_error!(response) 72 | end 73 | 74 | def update_details(tx_resource, details = {}) 75 | url = "#{API_ROOT}/project/#{tx_resource.project_slug}/resource/#{tx_resource.resource_slug}/" 76 | response = connection.put(url, details) 77 | raise_error!(response) 78 | end 79 | 80 | def resource_exists?(tx_resource) 81 | project = tx_resource.project_slug 82 | slug = tx_resource.resource_slug 83 | response = connection.get("#{API_ROOT}/project/#{project}/resource/#{slug}/") 84 | response.status == 200 85 | end 86 | 87 | def download(tx_resource, lang) 88 | project_slug = tx_resource.project_slug 89 | resource_slug = tx_resource.resource_slug 90 | response = connection.get( 91 | "#{API_ROOT}/project/#{project_slug}/resource/#{resource_slug}/translation/#{lang}/" 92 | ) 93 | 94 | raise_error!(response) 95 | 96 | json_data = JSON.parse(response.body) 97 | json_data['content'] 98 | end 99 | 100 | def get_resource(project_slug, resource_slug) 101 | url = "#{API_ROOT}/project/#{project_slug}/resource/#{resource_slug}/" 102 | response = connection.get(url) 103 | raise_error!(response) 104 | JSON.parse(response.body) 105 | end 106 | 107 | private 108 | 109 | def get_content_io(tx_resource, content) 110 | content_io = StringIO::new(content) 111 | content_io.set_encoding(Encoding::UTF_8.name) 112 | Faraday::UploadIO.new( 113 | content_io, 'application/octet-stream', tx_resource.source_file 114 | ) 115 | end 116 | 117 | def raise_error!(response) 118 | if (response.status / 100) != 2 119 | raise TransifexApiError, 120 | "Failed Transifex API call - returned status code: #{response.status}, body: #{response.body}" 121 | end 122 | end 123 | end 124 | end 125 | 126 | -------------------------------------------------------------------------------- /lib/txgh/app.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'json' 3 | require 'sinatra' 4 | require 'sinatra/reloader' 5 | require 'uri' 6 | 7 | module Txgh 8 | 9 | class Application < Sinatra::Base 10 | 11 | use Rack::Auth::Basic, 'Restricted Area' do |username, password| 12 | username == 'foo' && password == 'bar' 13 | end 14 | 15 | configure :development do 16 | register Sinatra::Reloader 17 | end 18 | 19 | def initialize(app = nil) 20 | super(app) 21 | end 22 | 23 | get '/health_check' do 24 | 200 25 | end 26 | 27 | end 28 | 29 | class Hooks < Sinatra::Base 30 | # Hooks are unprotected endpoints used for data integration between Github and 31 | # Transifex. They live under the /hooks namespace (see config.ru) 32 | 33 | configure :production do 34 | set :logging, nil 35 | logger = Txgh::TxLogger.logger 36 | set :logger, logger 37 | end 38 | 39 | configure :development, :test do 40 | register Sinatra::Reloader 41 | set :logging, nil 42 | logger = Txgh::TxLogger.logger 43 | set :logger, logger 44 | end 45 | 46 | def initialize(app = nil) 47 | super(app) 48 | end 49 | 50 | 51 | post '/transifex' do 52 | settings.logger.info('Processing request at /hooks/transifex') 53 | settings.logger.info(request.inspect) 54 | 55 | rbody = request.body.read 56 | payload = Hash[URI.decode_www_form(rbody)] 57 | 58 | if payload.key?('project') 59 | settings.logger.info('processing payload from form') 60 | else 61 | settings.logger.info("processing payload from request.body") 62 | payload = JSON.parse(rbody) 63 | end 64 | 65 | config = Txgh::KeyManager.config_from_project(payload['project']) 66 | 67 | if payload.key?('translated') 68 | tx_hook_trigger = 'translated' 69 | end 70 | 71 | if payload.key?('reviewed') 72 | tx_hook_trigger = 'reviewed' 73 | end 74 | 75 | if authenticated_transifex_request?(config.transifex_project, request) 76 | handler = transifex_handler_for( 77 | project: config.transifex_project, 78 | repo: config.github_repo, 79 | resource_slug: payload['resource'], 80 | language: payload['language'], 81 | tx_hook_trigger: tx_hook_trigger, 82 | logger: settings.logger 83 | ) 84 | handler.execute 85 | status 200 86 | else 87 | status 401 88 | end 89 | 90 | end 91 | 92 | post '/github' do 93 | settings.logger.info('Processing request at /hooks/github') 94 | 95 | payload = if params[:payload] 96 | settings.logger.info('processing payload from form') 97 | JSON.parse(params[:payload]) 98 | else 99 | settings.logger.info("processing payload from request.body") 100 | JSON.parse(request.body.read) 101 | end 102 | 103 | github_repo_name = "#{payload['repository']['owner']['name']}/#{payload['repository']['name']}" 104 | config = Txgh::KeyManager.config_from_repo(github_repo_name) 105 | 106 | if authenticated_github_request?(config.github_repo, request) 107 | handler = github_handler_for( 108 | project: config.transifex_project, 109 | repo: config.github_repo, 110 | payload: payload, 111 | logger: settings.logger 112 | ) 113 | 114 | handler.execute 115 | status 200 116 | else 117 | status 401 118 | end 119 | end 120 | 121 | private 122 | 123 | def authenticated_github_request?(repo, request) 124 | if repo.webhook_protected? 125 | GithubRequestAuth.authentic_request?( 126 | request, repo.webhook_secret 127 | ) 128 | else 129 | true 130 | end 131 | end 132 | 133 | def authenticated_transifex_request?(project, request) 134 | if project.webhook_protected? 135 | TransifexRequestAuth.authentic_request?( 136 | request, project.webhook_secret 137 | ) 138 | else 139 | true 140 | end 141 | end 142 | 143 | def transifex_handler_for(options) 144 | Txgh::Handlers::TransifexHookHandler.new(options) 145 | end 146 | 147 | def github_handler_for(options) 148 | Txgh::Handlers::GithubHookHandler.new(options) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require 'pry-nav' 4 | require 'rake' 5 | require 'rspec' 6 | require 'txgh' 7 | require 'vcr' 8 | require 'webmock' 9 | require 'yaml' 10 | 11 | require 'helpers/nil_logger' 12 | 13 | module StandardTxghSetup 14 | extend RSpec::SharedContext 15 | 16 | let(:logger) { NilLogger.new } 17 | let(:github_api) { double(:github_api) } 18 | let(:transifex_api) { double(:transifex_api) } 19 | 20 | let(:project_name) { 'my_awesome_project' } 21 | let(:resource_slug) { 'my_resource' } 22 | let(:repo_name) { 'my_org/my_repo' } 23 | let(:branch) { 'master' } 24 | let(:ref) { 'heads/master' } 25 | let(:language) { 'ko_KR' } 26 | let(:tx_hook_trigger) { 'translated' } 27 | let(:translations) { 'translation file contents' } 28 | 29 | let(:project_config) do 30 | { 31 | 'api_username' => 'transifex_api_username', 32 | 'api_password' => 'transifex_api_password', 33 | 'push_translations_to' => repo_name, 34 | 'push_trigger' => 'translated', 35 | 'name' => project_name 36 | } 37 | end 38 | 39 | let(:second_project_config) do 40 | { 41 | 'api_username' => 'transifex_api_username', 42 | 'api_password' => 'transifex_api_password', 43 | 'push_translations_to' => 'my_org/my_second_repo', 44 | 'name' => 'my_second_awesome_project' 45 | } 46 | end 47 | 48 | let(:repo_config) do 49 | { 50 | 'api_username' => 'github_api_username', 51 | 'api_token' => 'github_api_token', 52 | 'push_source_to' => project_name, 53 | 'branch' => branch, 54 | 'name' => repo_name 55 | } 56 | end 57 | 58 | let(:second_repo_config) do 59 | { 60 | 'api_username' => 'github_api_username', 61 | 'api_token' => 'github_api_token', 62 | 'push_source_to' => 'my_second_awesome_project', 63 | 'branch' => branch, 64 | 'name' => 'my_org/my_second_repo' 65 | } 66 | end 67 | 68 | let(:tx_config) do 69 | Txgh::TxConfig.load( 70 | """ 71 | [main] 72 | host = https://www.transifex.com 73 | lang_map = pt-BR:pt, ko-KR:ko 74 | 75 | [#{project_name}.#{resource_slug}] 76 | file_filter = translations//sample.po 77 | source_file = sample.po 78 | source_lang = en 79 | type = PO 80 | """ 81 | ) 82 | end 83 | 84 | let(:tx_config_multi_project) do 85 | Txgh::TxConfig.load( 86 | """ 87 | [main] 88 | host = https://www.transifex.com 89 | lang_map = pt-BR:pt, ko-KR:ko 90 | 91 | [my_awesome_project.my_resource] 92 | file_filter = translations//sample.po 93 | source_file = sample.po 94 | source_lang = en 95 | type = PO 96 | 97 | [my_awesome_project.my_second_resource] 98 | file_filter = translations//second_sample.po 99 | source_file = second_sample.po 100 | source_lang = en 101 | type = PO 102 | 103 | [my_second_awesome_project.my_resource] 104 | file_filter = translations/my_second_proj//sample.po 105 | source_file = sample.po 106 | source_lang = en 107 | type = PO 108 | """ 109 | ) 110 | end 111 | 112 | let(:yaml_config) do 113 | { 114 | 'txgh' => { 115 | 'github' => { 116 | 'repos' => { 117 | repo_name => repo_config, 118 | 'my_org/my_second_repo' => second_repo_config 119 | } 120 | }, 121 | 'transifex' => { 122 | 'projects' => { 123 | project_name => project_config, 124 | 'my_second_awesome_project' => second_project_config 125 | } 126 | } 127 | } 128 | } 129 | end 130 | 131 | let(:transifex_project) do 132 | TransifexProject.new(project_config, tx_config, transifex_api) 133 | end 134 | 135 | let(:github_repo) do 136 | GithubRepo.new(repo_config, github_api) 137 | end 138 | end 139 | 140 | RSpec.configure do |config| 141 | config.filter_run(focus: true) 142 | config.run_all_when_everything_filtered = true 143 | config.filter_run_excluding(integration: true) unless ENV['FULL_SPEC'] 144 | end 145 | 146 | VCR.configure do |config| 147 | config.cassette_library_dir = 'spec/integration/cassettes' 148 | config.hook_into :webmock 149 | 150 | txgh_config = Dir.chdir('./spec/integration') do 151 | Txgh::KeyManager.config_from_project('test-project-88') 152 | end 153 | 154 | config.filter_sensitive_data('') do 155 | txgh_config.repo_config['api_token'] 156 | end 157 | 158 | config.filter_sensitive_data('') do 159 | txgh_config.project_config['api_password'] 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/txgh/handlers/github_hook_handler.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'logger' 3 | 4 | module Txgh 5 | module Handlers 6 | class GithubHookHandler 7 | include Txgh::CategorySupport 8 | 9 | attr_reader :project, :repo, :payload, :logger 10 | 11 | def initialize(options = {}) 12 | @project = options.fetch(:project) 13 | @repo = options.fetch(:repo) 14 | @payload = options.fetch(:payload) 15 | @logger = options.fetch(:logger) { Logger.new(STDOUT) } 16 | end 17 | 18 | def execute 19 | # Check if the branch in the hook data is the configured branch we want 20 | logger.info("request github branch: #{branch}") 21 | logger.info("config github branch: #{github_config_branch}") 22 | 23 | if should_process_branch? 24 | logger.info('found branch in github request') 25 | 26 | tx_resources = tx_resources_for(branch) 27 | modified_resources = modified_resources_for(tx_resources) 28 | modified_resources.merge!(l10n_resources_for(tx_resources)) 29 | 30 | if github_config_branch.include?('tags/') 31 | modified_resources.merge!(tag_resources_for(tx_resources)) 32 | end 33 | 34 | # Handle DBZ 'L10N' special case 35 | if branch.include?("L10N") 36 | logger.info('processing L10N tag') 37 | 38 | # Create a new branch off tag commit 39 | if branch.include?('tags/L10N') 40 | repo.api.create_ref(repo.name, 'heads/L10N', payload['head_commit']['id']) 41 | end 42 | end 43 | 44 | update_resources(modified_resources) 45 | end 46 | end 47 | 48 | private 49 | 50 | # For each modified resource, get its content and update the content 51 | # in Transifex. 52 | def update_resources(resources) 53 | resources.each do |tx_resource, commit_sha| 54 | logger.info('process updated resource') 55 | github_api = repo.api 56 | tree_sha = github_api.get_commit(repo.name, commit_sha)['commit']['tree']['sha'] 57 | tree = github_api.tree(repo.name, tree_sha) 58 | 59 | tree['tree'].each do |file| 60 | logger.info("process each tree entry: #{file['path']}") 61 | 62 | if tx_resource.source_file == file['path'] 63 | logger.info("process resource file: #{tx_resource.source_file}") 64 | blob = github_api.blob(repo.name, file['sha']) 65 | content = blob['encoding'] == 'utf-8' ? blob['content'] : Base64.decode64(blob['content']) 66 | 67 | if upload_by_branch? 68 | upload_by_branch(tx_resource, content) 69 | else 70 | upload(tx_resource, content) 71 | end 72 | 73 | logger.info "updated tx_resource: #{tx_resource.inspect}" 74 | end 75 | end 76 | end 77 | end 78 | 79 | def upload(tx_resource, content) 80 | project.api.create_or_update(tx_resource, content) 81 | end 82 | 83 | def upload_by_branch(tx_resource, content) 84 | resource_exists = project.api.resource_exists?(tx_resource) 85 | 86 | categories = if resource_exists 87 | resource = project.api.get_resource(tx_resource.project_slug, tx_resource.resource_slug) 88 | deserialize_categories(Array(resource['categories'])) 89 | else 90 | {} 91 | end 92 | 93 | categories['branch'] ||= branch 94 | categories['author'] ||= escape_category( 95 | payload['head_commit']['committer']['name'] 96 | ) 97 | 98 | categories = serialize_categories(categories) 99 | 100 | if resource_exists 101 | project.api.update_details(tx_resource, categories: categories) 102 | project.api.update_content(tx_resource, content) 103 | else 104 | project.api.create(tx_resource, content, categories) 105 | end 106 | end 107 | 108 | def tag_resources_for(tx_resources) 109 | payload['head_commit']['modified'].each_with_object({}) do |modified, ret| 110 | # logger.info("processing modified file: #{modified}") 111 | 112 | if tx_resources.include?(modified) 113 | ret[tx_resources[modified]] = payload['head_commit']['id'] 114 | end 115 | end 116 | end 117 | 118 | def l10n_resources_for(tx_resources) 119 | payload['head_commit']['modified'].each_with_object({}) do |modified, ret| 120 | # logger.info("setting new resource: #{tx_resources[modified].L10N_resource_slug}") 121 | 122 | if tx_resources.include?(modified) 123 | ret[tx_resources[modified]] = payload['head_commit']['id'] 124 | end 125 | end 126 | end 127 | 128 | # Finds the updated resources and maps the most recent commit in which 129 | # each was modified 130 | def modified_resources_for(tx_resources) 131 | payload['commits'].each_with_object({}) do |commit, ret| 132 | logger.info('processing commit') 133 | 134 | commit['modified'].each do |modified| 135 | logger.info("processing modified file: #{modified}") 136 | 137 | if tx_resources.include?(modified) 138 | ret[tx_resources[modified]] = commit['id'] 139 | end 140 | end 141 | end 142 | end 143 | 144 | # Build an index of known Tx resources, by source file 145 | def tx_resources_for(branch) 146 | project.resources.each_with_object({}) do |resource, ret| 147 | logger.info('processing resource') 148 | 149 | # If we're processing by branch, create a branch resource. Otherwise, 150 | # use the original resource. 151 | ret[resource.source_file] = if upload_by_branch? 152 | TxBranchResource.new(resource, branch) 153 | else 154 | resource 155 | end 156 | end 157 | end 158 | 159 | def should_process_branch? 160 | process_all_branches? || ( 161 | branch.include?(github_config_branch) || branch.include?('L10N') 162 | ) 163 | end 164 | 165 | def github_config_branch 166 | @github_config_branch = begin 167 | if repo.branch == 'all' 168 | repo.branch 169 | else 170 | branch = repo.branch || 'master' 171 | branch.include?('tags/') ? branch : "heads/#{branch}" 172 | end 173 | end 174 | end 175 | 176 | def process_all_branches? 177 | github_config_branch == 'all' 178 | end 179 | 180 | alias_method :upload_by_branch?, :process_all_branches? 181 | 182 | def branch 183 | @ref ||= payload['ref'].sub(/^refs\//, '') 184 | end 185 | end 186 | end 187 | end 188 | -------------------------------------------------------------------------------- /spec/integration/payloads/github_postbody_l10n.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/tags/L10N", 3 | "before": "0000000000000000000000000000000000000000", 4 | "after": "6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed", 5 | "created": true, 6 | "deleted": false, 7 | "forced": true, 8 | "base_ref": "refs/heads/master", 9 | "compare": "https://github.com/txgh-bot/txgh-test-resources/compare/L10N", 10 | "commits": [ 11 | 12 | ], 13 | "head_commit": { 14 | "id": "6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed", 15 | "distinct": true, 16 | "message": "10", 17 | "timestamp": "2015-11-19T12:56:33-08:00", 18 | "url": "https://github.com/txgh-bot/txgh-test-resources/commit/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed", 19 | "author": { 20 | "name": "Txgh Bot", 21 | "email": "txgh.bot@gmail.com", 22 | "username": "txgh-bot" 23 | }, 24 | "committer": { 25 | "name": "Txgh Bot", 26 | "email": "txgh.bot@gmail.com", 27 | "username": "txgh-bot" 28 | }, 29 | "added": [ 30 | 31 | ], 32 | "removed": [ 33 | 34 | ], 35 | "modified": [ 36 | "sample.po" 37 | ] 38 | }, 39 | "repository": { 40 | "id": 41740726, 41 | "name": "txgh-test-resources", 42 | "full_name": "txgh-bot/txgh-test-resources", 43 | "owner": { 44 | "name": "txgh-bot", 45 | "email": "txgh.bot@gmail.com" 46 | }, 47 | "private": false, 48 | "html_url": "https://github.com/txgh-bot/txgh-test-resources", 49 | "description": "Translation test repo for txgh", 50 | "fork": false, 51 | "url": "https://github.com/txgh-bot/txgh-test-resources", 52 | "forks_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/forks", 53 | "keys_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/keys{/key_id}", 54 | "collaborators_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/collaborators{/collaborator}", 55 | "teams_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/teams", 56 | "hooks_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/hooks", 57 | "issue_events_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues/events{/number}", 58 | "events_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/events", 59 | "assignees_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/assignees{/user}", 60 | "branches_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/branches{/branch}", 61 | "tags_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/tags", 62 | "blobs_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs{/sha}", 63 | "git_tags_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/tags{/sha}", 64 | "git_refs_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs{/sha}", 65 | "trees_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees{/sha}", 66 | "statuses_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/statuses/{sha}", 67 | "languages_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/languages", 68 | "stargazers_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/stargazers", 69 | "contributors_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/contributors", 70 | "subscribers_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/subscribers", 71 | "subscription_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/subscription", 72 | "commits_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/commits{/sha}", 73 | "git_commits_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits{/sha}", 74 | "comments_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/comments{/number}", 75 | "issue_comment_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues/comments{/number}", 76 | "contents_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/contents/{+path}", 77 | "compare_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/compare/{base}...{head}", 78 | "merges_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/merges", 79 | "archive_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/{archive_format}{/ref}", 80 | "downloads_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/downloads", 81 | "issues_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues{/number}", 82 | "pulls_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/pulls{/number}", 83 | "milestones_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/milestones{/number}", 84 | "notifications_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/notifications{?since,all,participating}", 85 | "labels_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/labels{/name}", 86 | "releases_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/releases{/id}", 87 | "created_at": 1441114620, 88 | "updated_at": "2015-09-01T13:37:00Z", 89 | "pushed_at": 1447966674, 90 | "git_url": "git://github.com/txgh-bot/txgh-test-resources.git", 91 | "ssh_url": "git@github.com:txgh-bot/txgh-test-resources.git", 92 | "clone_url": "https://github.com/txgh-bot/txgh-test-resources.git", 93 | "svn_url": "https://github.com/txgh-bot/txgh-test-resources", 94 | "homepage": null, 95 | "size": 12, 96 | "stargazers_count": 0, 97 | "watchers_count": 0, 98 | "language": null, 99 | "has_issues": true, 100 | "has_downloads": true, 101 | "has_wiki": true, 102 | "has_pages": false, 103 | "forks_count": 0, 104 | "mirror_url": null, 105 | "open_issues_count": 0, 106 | "forks": 0, 107 | "open_issues": 0, 108 | "watchers": 0, 109 | "default_branch": "master", 110 | "stargazers": 0, 111 | "master_branch": "master" 112 | }, 113 | "pusher": { 114 | "name": "txgh-bot", 115 | "email": "txgh.bot@gmail.com" 116 | }, 117 | "sender": { 118 | "login": "txgh-bot", 119 | "id": 974299, 120 | "avatar_url": "https://avatars.githubusercontent.com/u/974299?v=3", 121 | "gravatar_id": "", 122 | "url": "https://api.github.com/users/txgh-bot", 123 | "html_url": "https://github.com/txgh-bot", 124 | "followers_url": "https://api.github.com/users/txgh-bot/followers", 125 | "following_url": "https://api.github.com/users/txgh-bot/following{/other_user}", 126 | "gists_url": "https://api.github.com/users/txgh-bot/gists{/gist_id}", 127 | "starred_url": "https://api.github.com/users/txgh-bot/starred{/owner}{/repo}", 128 | "subscriptions_url": "https://api.github.com/users/txgh-bot/subscriptions", 129 | "organizations_url": "https://api.github.com/users/txgh-bot/orgs", 130 | "repos_url": "https://api.github.com/users/txgh-bot/repos", 131 | "events_url": "https://api.github.com/users/txgh-bot/events{/privacy}", 132 | "received_events_url": "https://api.github.com/users/txgh-bot/received_events", 133 | "type": "User", 134 | "site_admin": false 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /spec/integration/payloads/github_postbody_release.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/tags/v3.0.0", 3 | "before": "0000000000000000000000000000000000000000", 4 | "after": "a779a46381d0d10c8e9dc390da0eaf567465cb39", 5 | "created": true, 6 | "deleted": false, 7 | "forced": true, 8 | "base_ref": "refs/heads/master", 9 | "compare": "https://github.com/txgh-bot/txgh-test-resources/compare/v3.0.0", 10 | "commits": [ 11 | 12 | ], 13 | "head_commit": { 14 | "id": "a779a46381d0d10c8e9dc390da0eaf567465cb39", 15 | "distinct": true, 16 | "message": "11", 17 | "timestamp": "2015-11-18T16:18:35-08:00", 18 | "url": "https://github.com/txgh-bot/txgh-test-resources/commit/a779a46381d0d10c8e9dc390da0eaf567465cb39", 19 | "author": { 20 | "name": "Txgh Bot", 21 | "email": "txgh.bot@gmail.com", 22 | "username": "txgh-bot" 23 | }, 24 | "committer": { 25 | "name": "Txgh Bot", 26 | "email": "txgh.bot@gmail.com", 27 | "username": "txgh-bot" 28 | }, 29 | "added": [ 30 | 31 | ], 32 | "removed": [ 33 | 34 | ], 35 | "modified": [ 36 | "sample.po" 37 | ] 38 | }, 39 | "repository": { 40 | "id": 41740726, 41 | "name": "txgh-test-resources", 42 | "full_name": "txgh-bot/txgh-test-resources", 43 | "owner": { 44 | "name": "txgh-bot", 45 | "email": "txgh.bot@gmail.com" 46 | }, 47 | "private": false, 48 | "html_url": "https://github.com/txgh-bot/txgh-test-resources", 49 | "description": "Translation test repo for txgh", 50 | "fork": false, 51 | "url": "https://github.com/txgh-bot/txgh-test-resources", 52 | "forks_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/forks", 53 | "keys_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/keys{/key_id}", 54 | "collaborators_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/collaborators{/collaborator}", 55 | "teams_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/teams", 56 | "hooks_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/hooks", 57 | "issue_events_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues/events{/number}", 58 | "events_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/events", 59 | "assignees_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/assignees{/user}", 60 | "branches_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/branches{/branch}", 61 | "tags_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/tags", 62 | "blobs_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs{/sha}", 63 | "git_tags_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/tags{/sha}", 64 | "git_refs_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs{/sha}", 65 | "trees_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees{/sha}", 66 | "statuses_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/statuses/{sha}", 67 | "languages_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/languages", 68 | "stargazers_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/stargazers", 69 | "contributors_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/contributors", 70 | "subscribers_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/subscribers", 71 | "subscription_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/subscription", 72 | "commits_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/commits{/sha}", 73 | "git_commits_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits{/sha}", 74 | "comments_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/comments{/number}", 75 | "issue_comment_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues/comments{/number}", 76 | "contents_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/contents/{+path}", 77 | "compare_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/compare/{base}...{head}", 78 | "merges_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/merges", 79 | "archive_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/{archive_format}{/ref}", 80 | "downloads_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/downloads", 81 | "issues_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues{/number}", 82 | "pulls_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/pulls{/number}", 83 | "milestones_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/milestones{/number}", 84 | "notifications_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/notifications{?since,all,participating}", 85 | "labels_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/labels{/name}", 86 | "releases_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/releases{/id}", 87 | "created_at": 1441114620, 88 | "updated_at": "2015-09-01T13:37:00Z", 89 | "pushed_at": 1447892352, 90 | "git_url": "git://github.com/txgh-bot/txgh-test-resources.git", 91 | "ssh_url": "git@github.com:txgh-bot/txgh-test-resources.git", 92 | "clone_url": "https://github.com/txgh-bot/txgh-test-resources.git", 93 | "svn_url": "https://github.com/txgh-bot/txgh-test-resources", 94 | "homepage": null, 95 | "size": 11, 96 | "stargazers_count": 0, 97 | "watchers_count": 0, 98 | "language": null, 99 | "has_issues": true, 100 | "has_downloads": true, 101 | "has_wiki": true, 102 | "has_pages": false, 103 | "forks_count": 0, 104 | "mirror_url": null, 105 | "open_issues_count": 0, 106 | "forks": 0, 107 | "open_issues": 0, 108 | "watchers": 0, 109 | "default_branch": "master", 110 | "stargazers": 0, 111 | "master_branch": "master" 112 | }, 113 | "pusher": { 114 | "name": "txgh-bot", 115 | "email": "txgh.bot@gmail.com" 116 | }, 117 | "sender": { 118 | "login": "txgh-bot", 119 | "id": 974299, 120 | "avatar_url": "https://avatars.githubusercontent.com/u/974299?v=3", 121 | "gravatar_id": "", 122 | "url": "https://api.github.com/users/txgh-bot", 123 | "html_url": "https://github.com/txgh-bot", 124 | "followers_url": "https://api.github.com/users/txgh-bot/followers", 125 | "following_url": "https://api.github.com/users/txgh-bot/following{/other_user}", 126 | "gists_url": "https://api.github.com/users/txgh-bot/gists{/gist_id}", 127 | "starred_url": "https://api.github.com/users/txgh-bot/starred{/owner}{/repo}", 128 | "subscriptions_url": "https://api.github.com/users/txgh-bot/subscriptions", 129 | "organizations_url": "https://api.github.com/users/txgh-bot/orgs", 130 | "repos_url": "https://api.github.com/users/txgh-bot/repos", 131 | "events_url": "https://api.github.com/users/txgh-bot/events{/privacy}", 132 | "received_events_url": "https://api.github.com/users/txgh-bot/received_events", 133 | "type": "User", 134 | "site_admin": false 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Transifex Txgh 2 | ==== 3 | 4 | [![Build Status](https://travis-ci.org/transifex/txgh.svg?branch=devel)](https://travis-ci.org/transifex/txgh) 5 | 6 | Description 7 | --- 8 | A lightweight web server that integrates Transifex with Github. Txgh acts as an agent for developers by automatically uploading source files to Transifex. It also acts as an agent for translators by pushing translation files to GitHub that have been 100% translated in Transifex. 9 | 10 | Installation 11 | --- 12 | To setup locally, clone this repository and install the dependencies from the Gemfile. You can place a txgh.yml file in your home directory to bootstrap configuration of the server. The quickest way to get started is to clone the repository, update your configuration, and then run the puma web server on a specific port. 13 | ```ruby 14 | puma -p 9292 15 | ``` 16 | 17 | Other platforms: 18 | 19 | - [Amazon AWS](https://github.com/transifex/txgh/blob/devel/docs/aws.md) 20 | - [Salesforce Heroku](https://github.com/transifex/txgh/blob/devel/docs/heroku.md) 21 | - [Docker Container](https://github.com/transifex/txgh/blob/devel/docs/docker.md) 22 | 23 | Directory Layout 24 | --- 25 | ``` 26 | . 27 | |-- config 28 | | |-- tx.config # sample config file 29 | | `-- txgh.yml # ERB template for flexible config 30 | |-- lib 31 | | |-- txgh 32 | | | |-- handlers # Logic specific to endpoint hooks 33 | | | | |-- ... 34 | | | | 35 | | | |-- app.rb # the main Sinatra app, includes both web service endpoints 36 | | | |-- category_support.rb 37 | | | |-- config.rb 38 | | | |-- errors.rb 39 | | | |-- github_api.rb # Wrapper for GitHub REST API 40 | | | |-- github_repo.rb # GitHub repository object 41 | | | |-- github_request_auth.rb # GitHub webhook Auth 42 | | | |-- handlers.rb 43 | | | |-- key_manager.rb # Loads configuration 44 | | | |-- parse_config.rb 45 | | | |-- transifex_api.rb # Wrapper for Tx REST API 46 | | | |-- transifex_project.rb # Tx Project Object 47 | | | |-- transifex_request_auth.rb # Tx webhook auth 48 | | | |-- tx_branch_resource.rb # Support for branches 49 | | | |-- tx_config.rb # Loads tx.config 50 | | | |-- tx_logger.rb 51 | | | |-- tx_resource.rb #Tx resource Object 52 | | | `-- utils.rb 53 | | `-- txgh.rb # includes for app dependencies 54 | |-- spec # spec unit and integration tests 55 | | |-- ... 56 | | 57 | |-- Dockerfile # DIY Docker base 58 | |-- Rakefile # rake tasks to run tests 59 | |-- bootstrap.rb # includes for application paths 60 | `-- config.ru # bootstrap for web server 61 | ``` 62 | 63 | 64 | How it works 65 | --- 66 | 67 | You configure a service hook in Github and point it to this server. The URL path to the service hook endpoint: /hooks/github 68 | You do the same for Transifex, in your project settings page, and point it to the service hook endpoint: /hooks/transifex 69 | 70 | Currently there are 4 use cases that are supported: 71 | 72 | 1) When a resource (configured in this service) in Transifex reaches 100% translated, the Txgh service will pull the translations and commit them to the target repository. 73 | 74 | 2) When a source file (configured in this service) is pushed to a specific Github branch (also configured in this service), the Txgh service will update the source resource (configured in this service) with the new file. 75 | 76 | 3) When a source file (configured in this service) is pushed to a specific Github tag (also configured in this service), the Txgh service will update the source resource (configured in this service) with the new file. 77 | 78 | 4) EXPERIMENTAL - When a source file (configured in this service) is pushed to a specific Github tag called 'L10N', Txgh will create a new branch called 'L10N' and new resources where the slug is prefixed with 'L10N'. 79 | 80 | ![Txgh Use Cases](https://www.gliffy.com/go/publish/image/9483799/L.png) 81 | 82 | 83 | Notes 84 | --- 85 | 86 | We recommend running it using Ruby 2.2.2 and installing dependencies via bundler. 87 | 88 | There are 2 important configuration files. 89 | 90 | txgh.yml - This is the base configuration for the service. To avoid needing to checkin sensitive password information, this file should pull it's settings from the Ruby ENV in production. Additionally, this file can be located in the users HOME directory to support running the server with local values. 91 | 92 | ```yaml 93 | txgh: 94 | github: 95 | repos: 96 | # This name should be org/repo 97 | MyOrg/frontend: 98 | api_username: "github_username" 99 | api_token: "github_token" 100 | # Transifex project name, as below 101 | push_source_to: "my-frontend" 102 | # The branch to watch. Set to 'all' to listen to all pushes. 103 | branch: "i18n" 104 | # Create a repo webhook. The secret is any string of your choosing, 105 | # and is input during webhook creation. TXGH uses this to validate 106 | # messages are really coming from GitHub. 107 | webhook_secret: "..." 108 | transifex: 109 | projects: 110 | # This name should match the transifex project name, without org name 111 | my-frontend: 112 | tx_config: "./config/tx.config" 113 | api_username: "transifex_user" 114 | api_password: "transifex_password" 115 | # This is the GitHub project name, as above. 116 | push_translations_to: "MyOrg/frontend" 117 | # This can be 'translated' or 'reviewed'. To catch both actions, 118 | # simply remove this key. 119 | push_trigger: "translated" 120 | # This works similarly to the GitHub webhook_secret above. 121 | webhook_secret: "..." 122 | ``` 123 | 124 | tx.config - This is a configuration which maps the source file, languages, and target translation files. It is based on this specification: http://docs.transifex.com/client/config/#txconfig 125 | 126 | --- 127 | There is a check for both V1 and V2 Transifex webhook signatures, but the V1 signature implementation is no logger maintain. Changes in the webhook response may cause V1 signature calculation to be wrong. Make sure you use the latest master that include both checks. 128 | 129 | Getting Help 130 | --- 131 | You can always get additional help via [GitHub Issues](https://github.com/transifex/txgh/issues) or [Transifex support email](support@transifex.com) 132 | 133 | License 134 | --- 135 | Txgh is primarily distributed under the terms of the Apache License (Version 2.0). 136 | 137 | See [LICENSE](https://github.com/transifex/txgh/blob/master/LICENSE) for details. 138 | 139 | Development 140 | --- 141 | In order to test and debug a local instance of TXGH you can do the following: 142 | 1. Run TXGH with: 143 | ``` 144 | puma -p 9292 145 | ``` 146 | 2. Use [https://ngrok.com](https://ngrok.com) to expose the local server 147 | ``` 148 | ./ngrok http 9292 149 | ``` 150 | 3. Take the host name ngrok generated, something like http://f61da052.ngrok.io and setup the following webhooks: 151 | * For Github: http://f61da051.ngrok.io/hooks/github 152 | * For Transifex: http://f61da051.ngrok.io/hooks/transifex 153 | 154 | Make sure the secret you have used in each case are the same as the ones that are configured in txgh.yml. 155 | 156 | -------------------------------------------------------------------------------- /spec/integration/payloads/github_postbody.json: -------------------------------------------------------------------------------- 1 | { 2 | "ref": "refs/heads/test", 3 | "before": "0000000000000000000000000000000000000000", 4 | "after": "fd2696fdb0c38d7a2bb4478b62423a701ad8f5fc", 5 | "created": true, 6 | "deleted": false, 7 | "forced": true, 8 | "base_ref": null, 9 | "compare": "https://github.com/txgh-bot/txgh-test-resources/commit/fd2696fdb0c3", 10 | "commits": [ 11 | { 12 | "id": "fd2696fdb0c38d7a2bb4478b62423a701ad8f5fc", 13 | "distinct": true, 14 | "message": "Testing branch functionality", 15 | "timestamp": "2015-10-10T22:03:14-07:00", 16 | "url": "https://github.com/txgh-bot/txgh-test-resources/commit/fd2696fdb0c38d7a2bb4478b62423a701ad8f5fc", 17 | "author": { 18 | "name": "Txgh Bot", 19 | "email": "txgh.bot@gmail.com", 20 | "username": "txgh-bot" 21 | }, 22 | "committer": { 23 | "name": "Txgh Bot", 24 | "email": "txgh.bot@gmail.com", 25 | "username": "txgh-bot" 26 | }, 27 | "added": [ 28 | 29 | ], 30 | "removed": [ 31 | 32 | ], 33 | "modified": [ 34 | "sample.po" 35 | ] 36 | } 37 | ], 38 | "head_commit": { 39 | "id": "fd2696fdb0c38d7a2bb4478b62423a701ad8f5fc", 40 | "distinct": true, 41 | "message": "Testing branch functionality", 42 | "timestamp": "2015-10-10T22:03:14-07:00", 43 | "url": "https://github.com/txgh-bot/txgh-test-resources/commit/fd2696fdb0c38d7a2bb4478b62423a701ad8f5fc", 44 | "author": { 45 | "name": "Txgh Bot", 46 | "email": "txgh.bot@gmail.com", 47 | "username": "txgh-bot" 48 | }, 49 | "committer": { 50 | "name": "Txgh Bot", 51 | "email": "txgh.bot@gmail.com", 52 | "username": "txgh-bot" 53 | }, 54 | "added": [ 55 | 56 | ], 57 | "removed": [ 58 | 59 | ], 60 | "modified": [ 61 | "sample.po" 62 | ] 63 | }, 64 | "repository": { 65 | "id": 41740726, 66 | "name": "txgh-test-resources", 67 | "full_name": "txgh-bot/txgh-test-resources", 68 | "owner": { 69 | "name": "txgh-bot", 70 | "email": "txgh.bot@gmail.com" 71 | }, 72 | "private": false, 73 | "html_url": "https://github.com/txgh-bot/txgh-test-resources", 74 | "description": "Translation test repo for txgh", 75 | "fork": false, 76 | "url": "https://github.com/txgh-bot/txgh-test-resources", 77 | "forks_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/forks", 78 | "keys_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/keys{/key_id}", 79 | "collaborators_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/collaborators{/collaborator}", 80 | "teams_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/teams", 81 | "hooks_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/hooks", 82 | "issue_events_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues/events{/number}", 83 | "events_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/events", 84 | "assignees_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/assignees{/user}", 85 | "branches_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/branches{/branch}", 86 | "tags_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/tags", 87 | "blobs_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs{/sha}", 88 | "git_tags_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/tags{/sha}", 89 | "git_refs_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs{/sha}", 90 | "trees_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees{/sha}", 91 | "statuses_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/statuses/{sha}", 92 | "languages_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/languages", 93 | "stargazers_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/stargazers", 94 | "contributors_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/contributors", 95 | "subscribers_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/subscribers", 96 | "subscription_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/subscription", 97 | "commits_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/commits{/sha}", 98 | "git_commits_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits{/sha}", 99 | "comments_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/comments{/number}", 100 | "issue_comment_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues/comments{/number}", 101 | "contents_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/contents/{+path}", 102 | "compare_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/compare/{base}...{head}", 103 | "merges_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/merges", 104 | "archive_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/{archive_format}{/ref}", 105 | "downloads_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/downloads", 106 | "issues_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/issues{/number}", 107 | "pulls_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/pulls{/number}", 108 | "milestones_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/milestones{/number}", 109 | "notifications_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/notifications{?since,all,participating}", 110 | "labels_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/labels{/name}", 111 | "releases_url": "https://api.github.com/repos/txgh-bot/txgh-test-resources/releases{/id}", 112 | "created_at": 1441114620, 113 | "updated_at": "2015-09-01T13:37:00Z", 114 | "pushed_at": 1444539817, 115 | "git_url": "git://github.com/txgh-bot/txgh-test-resources.git", 116 | "ssh_url": "git@github.com:txgh-bot/txgh-test-resources.git", 117 | "clone_url": "https://github.com/txgh-bot/txgh-test-resources.git", 118 | "svn_url": "https://github.com/txgh-bot/txgh-test-resources", 119 | "homepage": null, 120 | "size": 204, 121 | "stargazers_count": 0, 122 | "watchers_count": 0, 123 | "language": null, 124 | "has_issues": true, 125 | "has_downloads": true, 126 | "has_wiki": true, 127 | "has_pages": false, 128 | "forks_count": 0, 129 | "mirror_url": null, 130 | "open_issues_count": 0, 131 | "forks": 0, 132 | "open_issues": 0, 133 | "watchers": 0, 134 | "default_branch": "master", 135 | "stargazers": 0, 136 | "master_branch": "master" 137 | }, 138 | "pusher": { 139 | "name": "txgh-bot", 140 | "email": "txgh.bot@gmail.com" 141 | }, 142 | "sender": { 143 | "login": "txgh-bot", 144 | "id": 974299, 145 | "avatar_url": "https://avatars.githubusercontent.com/u/974299?v=3", 146 | "gravatar_id": "", 147 | "url": "https://api.github.com/users/txgh-bot", 148 | "html_url": "https://github.com/txgh-bot", 149 | "followers_url": "https://api.github.com/users/txgh-bot/followers", 150 | "following_url": "https://api.github.com/users/txgh-bot/following{/other_user}", 151 | "gists_url": "https://api.github.com/users/txgh-bot/gists{/gist_id}", 152 | "starred_url": "https://api.github.com/users/txgh-bot/starred{/owner}{/repo}", 153 | "subscriptions_url": "https://api.github.com/users/txgh-bot/subscriptions", 154 | "organizations_url": "https://api.github.com/users/txgh-bot/orgs", 155 | "repos_url": "https://api.github.com/users/txgh-bot/repos", 156 | "events_url": "https://api.github.com/users/txgh-bot/events{/privacy}", 157 | "received_events_url": "https://api.github.com/users/txgh-bot/received_events", 158 | "type": "User", 159 | "site_admin": false 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /spec/transifex_api_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Txgh 4 | 5 | describe TransifexApi do 6 | include StandardTxghSetup 7 | 8 | let(:connection) { double(:connection) } 9 | let(:api) { TransifexApi.create_from_connection(connection) } 10 | let(:resource) { transifex_project.resources.first } 11 | let(:response) { double(:response) } 12 | 13 | describe '#create_or_update' do 14 | context 'with a preexisting resource' do 15 | before(:each) do 16 | expect(api).to receive(:resource_exists?).and_return(true) 17 | end 18 | 19 | it 'updates the resource with new content' do 20 | expect(api).to receive(:update_details).with(resource, categories: []) 21 | expect(api).to receive(:update_content).with(resource, 'new_content') 22 | expect(api).to receive(:get_resource).and_return({}) 23 | 24 | api.create_or_update(resource, 'new_content') 25 | end 26 | 27 | it "additively updates the resource's categories" do 28 | expect(api).to receive(:update_details) do |rsrc, details| 29 | expect(details[:categories].sort).to eq(['branch:foobar', 'name:Jesse James']) 30 | end 31 | 32 | expect(api).to receive(:update_content).with(resource, 'new_content') 33 | expect(api).to receive(:get_resource).and_return({ 'categories' => ['name:Jesse James'] }) 34 | 35 | api.create_or_update(resource, 'new_content', ['branch:foobar']) 36 | end 37 | 38 | it 'only submits a unique set of categories' do 39 | expect(api).to receive(:update_details).with(resource, categories: ['branch:foobar']) 40 | expect(api).to receive(:update_content).with(resource, 'new_content') 41 | expect(api).to receive(:get_resource).and_return({ 'categories' => ['branch:foobar'] }) 42 | 43 | api.create_or_update(resource, 'new_content', ['branch:foobar']) 44 | end 45 | end 46 | 47 | context 'with a non-existent resource' do 48 | before(:each) do 49 | expect(api).to receive(:resource_exists?).and_return(false) 50 | end 51 | 52 | it 'makes a request with the correct parameters' do 53 | expect(connection).to receive(:post) do |url, payload| 54 | expect(url).to( 55 | end_with("project/#{project_name}/resources/") 56 | ) 57 | 58 | expect(payload[:slug]).to eq(resource_slug) 59 | expect(payload[:name]).to eq(resource.source_file) 60 | expect(payload[:i18n_type]).to eq('PO') 61 | 62 | response 63 | end 64 | 65 | allow(response).to receive(:status).and_return(200) 66 | allow(response).to receive(:body).and_return("{}") 67 | api.create_or_update(resource, 'new_content') 68 | end 69 | end 70 | end 71 | 72 | describe '#create' do 73 | it 'makes a request with the correct parameters' do 74 | expect(connection).to receive(:post) do |url, payload| 75 | expect(url).to( 76 | end_with("project/#{project_name}/resources/") 77 | ) 78 | 79 | expect(payload[:content].io.string).to eq('new_content') 80 | expect(payload[:categories]).to eq('abc def') 81 | response 82 | end 83 | 84 | expect(response).to receive(:status).and_return(200) 85 | api.create(resource, 'new_content', ['abc', 'def']) 86 | end 87 | 88 | it 'submits de-duped categories' do 89 | expect(connection).to receive(:post) do |url, payload| 90 | expect(payload[:categories]).to eq('abc') 91 | response 92 | end 93 | 94 | expect(response).to receive(:status).and_return(200) 95 | api.create(resource, 'new_content', ['abc', 'abc']) 96 | end 97 | 98 | it 'raises an exception if the api responds with an error code' do 99 | allow(connection).to receive(:post).and_return(response) 100 | allow(response).to receive(:status).and_return(404) 101 | allow(response).to receive(:body).and_return('{}') 102 | expect { api.create(resource, 'new_content') }.to raise_error(TransifexApiError) 103 | end 104 | end 105 | 106 | describe '#update_content' do 107 | it 'makes a request with the correct parameters' do 108 | expect(connection).to receive(:put) do |url, payload| 109 | expect(url).to( 110 | end_with("project/#{project_name}/resource/#{resource_slug}/content/") 111 | ) 112 | 113 | expect(payload[:content].io.string).to eq('new_content') 114 | response 115 | end 116 | 117 | expect(response).to receive(:status).and_return(200) 118 | api.update_content(resource, 'new_content') 119 | end 120 | 121 | it 'raises an exception if the api responds with an error code' do 122 | allow(connection).to receive(:put).and_return(response) 123 | allow(response).to receive(:status).and_return(404) 124 | allow(response).to receive(:body).and_return('{}') 125 | expect { api.update_content(resource, 'new_content') }.to raise_error(TransifexApiError) 126 | end 127 | end 128 | 129 | describe '#update_details' do 130 | it 'makes a request with the correct parameters' do 131 | expect(connection).to receive(:put) do |url, payload| 132 | expect(url).to( 133 | end_with("project/#{project_name}/resource/#{resource_slug}/") 134 | ) 135 | 136 | expect(payload[:i18n_type]).to eq('FOO') 137 | expect(payload[:categories]).to eq(['abc']) 138 | response 139 | end 140 | 141 | expect(response).to receive(:status).and_return(200) 142 | api.update_details(resource, i18n_type: 'FOO', categories: ['abc']) 143 | end 144 | 145 | it 'raises an exception if the api responds with an error code' do 146 | allow(connection).to receive(:put).and_return(response) 147 | allow(response).to receive(:status).and_return(404) 148 | allow(response).to receive(:body).and_return('{}') 149 | expect { api.update_details(resource, {}) }.to raise_error(TransifexApiError) 150 | end 151 | end 152 | 153 | describe '#resource_exists?' do 154 | it 'makes a request with the correct parameters' do 155 | expect(connection).to receive(:get) do |url| 156 | expect(url).to( 157 | end_with("project/#{project_name}/resource/#{resource_slug}/") 158 | ) 159 | 160 | response 161 | end 162 | 163 | expect(response).to receive(:status).and_return(200) 164 | api.resource_exists?(resource) 165 | end 166 | 167 | it 'returns true if the api responds with a 200 status code' do 168 | allow(connection).to receive(:get).and_return(response) 169 | allow(response).to receive(:status).and_return(200) 170 | expect(api.resource_exists?(resource)).to eq(true) 171 | end 172 | 173 | it 'returns false if the api does not respond with a 200 status code' do 174 | allow(connection).to receive(:get).and_return(response) 175 | allow(response).to receive(:status).and_return(404) 176 | expect(api.resource_exists?(resource)).to eq(false) 177 | end 178 | end 179 | 180 | describe '#download' do 181 | let(:language) { 'pt-BR' } 182 | 183 | it 'makes a request with the correct parameters' do 184 | expect(connection).to receive(:get) do |url| 185 | expect(url).to( 186 | end_with("project/#{project_name}/resource/#{resource_slug}/translation/#{language}/") 187 | ) 188 | 189 | response 190 | end 191 | 192 | expect(response).to receive(:status).and_return(200) 193 | allow(response).to receive(:body).and_return('{}') 194 | api.download(resource, language) 195 | end 196 | 197 | it 'parses and returns the response content' do 198 | allow(connection).to receive(:get).and_return(response) 199 | allow(response).to receive(:status).and_return(200) 200 | allow(response).to receive(:body).and_return('{"content": "foobar"}') 201 | expect(api.download(resource, language)).to eq('foobar') 202 | end 203 | 204 | it 'raises an exception if the api responds with an error code' do 205 | allow(connection).to receive(:get).and_return(response) 206 | allow(response).to receive(:status).and_return(404) 207 | allow(response).to receive(:body).and_return('{}') 208 | expect { api.download(resource, language) }.to raise_error(TransifexApiError) 209 | end 210 | end 211 | 212 | describe '#get_resource' do 213 | it 'makes a request with the correct parameters' do 214 | expect(connection).to receive(:get) do |url, payload| 215 | expect(url).to( 216 | end_with("project/#{project_name}/resource/#{resource_slug}/") 217 | ) 218 | 219 | response 220 | end 221 | 222 | expect(response).to receive(:status).and_return(200) 223 | expect(response).to receive(:body).and_return('{"foo":"bar"}') 224 | expect(api.get_resource(*resource.slugs)).to eq({ 'foo' => 'bar' }) 225 | end 226 | 227 | it 'raises an exception if the api responds with an error code' do 228 | allow(connection).to receive(:get).and_return(response) 229 | allow(response).to receive(:status).and_return(404) 230 | allow(response).to receive(:body).and_return('{}') 231 | expect { api.get_resource(*resource.slugs) }.to raise_error(TransifexApiError) 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /docs/heroku.md: -------------------------------------------------------------------------------- 1 | Implementing on Heroku 2 | ====================== 3 | 4 | Heroku provides a command-line tool for interacting with applications. When you create a new application, Heroku creates a remote Git repository (with a branch named heroku), which you can then push your code to. Change your current directory to the Txgh project’s root directory and enter the following command: 5 | ``` 6 | $ heroku create 7 | Creating nameless-eyrie-4025... done, stack is cedar-14 8 | https://nameless-eyrie-4025.herokuapp.com/ | https://git.heroku.com/nameless-eyrie-4025.git 9 | Git remote heroku added 10 | ``` 11 | By default, Heroku provides a randomly generated name, but you can supply one as a parameter. Once the new application has been created, you need to generate a Gemfile.lock and commit it to your repository. This is counter to how most projects work in git, but is a Heroku requirement: 12 | ``` 13 | $ bundle install 14 | Fetching gem metadata from https://rubygems.org/............ 15 | Fetching version metadata from https://rubygems.org/... 16 | Fetching dependency metadata from https://rubygems.org/.. 17 | Resolving dependencies... 18 | Using rake 12.0.0 19 | Using public_suffix 2.0.5 20 | Using addressable 2.5.0 21 | Using backports 3.6.8 22 | Using coderay 1.1.1 23 | Using safe_yaml 1.0.4 24 | Using crack 0.4.3 25 | Using diff-lcs 1.3 26 | Using multipart-post 2.0.0 27 | Using faraday 0.11.0 28 | Using faraday_middleware 0.11.0.1 29 | Using hashdiff 0.3.2 30 | Using json 2.0.3 31 | Using method_source 0.8.2 32 | Using multi_json 1.12.1 33 | Using sawyer 0.8.1 34 | Using octokit 4.6.2 35 | Using parseconfig 1.0.8 36 | Using slop 3.6.0 37 | Using pry 0.10.4 38 | Using pry-nav 0.2.4 39 | Using puma 3.7.0 40 | Using rack 1.6.5 41 | Using rack-protection 1.5.3 42 | Using rack-test 0.6.3 43 | Using rspec-support 3.5.0 44 | Using rspec-core 3.5.4 45 | Using rspec-expectations 3.5.0 46 | Using rspec-mocks 3.5.0 47 | Using rspec 3.5.0 48 | Using shotgun 0.9.2 49 | Using tilt 2.0.6 50 | Using sinatra 1.4.8 51 | Using sinatra-contrib 1.4.7 52 | Using vcr 3.0.3 53 | Using webmock 1.24.6 54 | Using bundler 1.10.6 55 | Bundle complete! 16 Gemfile dependencies, 37 gems now installed. 56 | Use `bundle show [gemname]` to see where a bundled gem is installed. 57 | $ git add -f Gemfile.lock 58 | $ git commit -m"Adding Gemfile.lock for Heroku's benefit" 59 | ``` 60 | Then you can deploy your app by using git: 61 | 62 | ``` 63 | $ git push heroku master 64 | Counting objects: 156, done. 65 | Delta compression using up to 2 threads. 66 | Compressing objects: 100% (71/71), done. 67 | Writing objects: 100% (156/156), 33.84 KiB | 0 bytes/s, done. 68 | Total 156 (delta 65), reused 155 (delta 65) 69 | remote: Compressing source files... done. 70 | remote: Building source: 71 | remote: 72 | remote: -----> Ruby app detected 73 | remote: -----> Compiling Ruby/Rack 74 | remote: -----> Using Ruby version: ruby-2.0.0 75 | remote: -----> Installing dependencies using bundler 1.9.7 76 | ... 77 | remote: -----> Compressing... done, 18.3MB 78 | remote: -----> Launching... done, v4 79 | remote: https://nameless-eyrie-4025.herokuapp.com/ deployed to Heroku 80 | remote: 81 | remote: Verifying deploy.... done. 82 | To https://git.heroku.com/nameless-eyrie-4025.git 83 | * [new branch] master -> master 84 | ``` 85 | 86 | You can verify the success of the deployment by opening the Heroku dashboard in your web browser and navigating to the newly created dyno. 87 | 88 | ##Updating the Configuration 89 | 90 | Before you can start pushing updates between GitHub and Transifex, you’ll need to provide the Heroku app with information on how to access each service. Txgh uses a set of environment variables to manage connections between each service. The name and description of these variables is shown in the table below: 91 | 92 | | Variable | Description | Example | 93 | | -------- | ----------- | ------- | 94 | | TX_CONFIG_PATH | Location of your Transifex project’s configuration file relative to Txgh’s root folder. | ./config/tx.config | 95 | | TX_USERNAME | Your Transifex username. | txuser | 96 | | TX_PASSWORD | Password to your Transifex account. | 1324578 | 97 | | TX_PUSH_TRANSLATIONS_TO | Name of the GitHub repository that Txgh will push updates to. | ghuser/my_repository | 98 | | TX_WEBHOOK_SECRET | Secret key given to Transifex to authenticate the webhook request (optional) | please-dont-use-this-example | 99 | | GITHUB_BRANCH | GitHub branch to update. | heads/master | 100 | | GITHUB_USERNAME | Your GitHub username. | ghuser | 101 | | GITHUB_TOKEN | A personal API token created in GitHub. | 489394e58d99095d9c6aafb49f0e2b1e | 102 | | GITHUB_PUSH_SOURCE_TO | Name of the Transifex project that Txgh will push updates to. | my_project | 103 | | GITHUB_WEBHOOK_SECRET | Secret key given to Github to authenticate the webhook request (optional) | please-dont-use-this-example | 104 | 105 | If you want to use webhook secrets, you'll need to add them to your txgh.yml as well. Add `webhook_secret: "<%= ENV['GITHUB_WEBHOOK_SECRET'] %>"` to the Github repo block in there, and `webhook_secret: "<%= ENV['TX_WEBHOOK_SECRET'] %>"` in the Transifex block. 106 | 107 | There are two ways to apply these to your Heroku app: 108 | 109 | - Add the environment variables through Heroku’s web interface. 110 | 111 | Create a local file containing your environment variables and apply it using rake. 112 | Add Environment Variables Through the Heroku Dashboard 113 | Open the Heroku dashboard in your web browser. Click on the Settings tab and scroll down to the Config Variables section. Click Reveal Config Vars, then click Edit. You’ll have access to the application’s existing variables, but more importantly you can add new variables. Add the variables listed above and click Save. 114 | 115 | - Config vars 116 | 117 | **Note** The RACK_ENV variable defaults to production, but in order for it to work with Txgh we need to set it to test. 118 | 119 | Add Environment Variables Using txgh_config.rb 120 | The txgh_config.rb file stores our environment variables inside of the Txgh folder. To create the file, copy and paste the following into a new text file. Replace the placeholder values with your actual values and save the file in the config directory as txgh_config.rb. 121 | 122 | ``` 123 | # 'test' only ENV['RACK_ENV'] 124 | config_env :test do 125 | set 'TX_CONFIG_PATH', './config/tx.config' 126 | set 'TX_USERNAME', 'txuser' 127 | set 'TX_PASSWORD', '1324578' 128 | set 'TX_PUSH_TRANSLATIONS_TO', 'ghuser/my_repository' 129 | set 'GITHUB_BRANCH', 'heads/master' 130 | set 'GITHUB_USERNAME', 'ghuser' 131 | set 'GITHUB_TOKEN', '489394e58d99095d9c6aafb49f0e2b1e' 132 | set 'GITHUB_PUSH_SOURCE_TO', 'my_project' 133 | end 134 | ``` 135 | To apply the changes to your Heroku dyno, use the rake command: 136 | 137 | ``` 138 | $ rake config_env:heroku 139 | Running echo $RACK_ENV on nameless-eyrie-4025... up, run.2376 140 | Configure Heroku according to config_env[test] 141 | 142 | === nameless-eyrie-4025 Config Vars 143 | LANG: en_US.UTF-8 144 | RACK_ENV: test 145 | GITHUB_TOKEN: 489394e58d99095d9c6aafb49f0e2b1e 146 | GITHUB_USERNAME: ghuser 147 | GITHUB_PUSH_SOURCE_TO: my_project 148 | TX_PASSWORD: 1324578 149 | TX_USERNAME: txuser 150 | TX_PUSH_TRANSLATIONS_TO: ghuser/my_repository 151 | TX_CONFIG_PATH: ./config/tx.config 152 | ``` 153 | 154 | This command updates the configuration of your Heroku app with the values specified in `txgh_config.rb` If you have any issues running the rake command, run bundle install in the Txgh project’s root directory. This compiles and installs the Ruby gems required by Txgh. Once the install completes, run the rake command again. 155 | 156 | 157 | **Note** Since this file contains sensitive information, you should avoid committing it to your Heroku repository or to your GitHub repository. 158 | 159 | Once the rake command has completed successfully, open the Heroku dashboard, navigate to the application’s settings and click Reveal Config Vars. 160 | 161 | ##Final Configuration Steps 162 | The last step is to change the value of the RACK_ENV variable. By default, Heroku sets the value of RACK_ENV to production. However, we recommend testing Txgh by setting this value to test. If you haven’t already, open your application’s environment variables in a web browser and change the value of RACK_ENV from production to test. When you’re ready to deploy, you can change this value back to production. 163 | 164 | Meanwhile, check the values of your other variables. If any values seem incorrect, you can edit them in your browser or edit and re-apply the txgh_config.rb file using rake. Once everything looks good, you can add your webhooks to Transifex and GitHub. 165 | 166 | ##Connecting Transifex and GitHub to Txgh 167 | 168 | Txgh synchronizes your Transifex and GitHub projects using webhooks, allowing Txgh to respond immediately to changes in either service. The webhook URLs follow the format https://HEROKUNAME.herokuapp.com/hooks/SOURCE, where HEROKUNAME is the name of your deployed Heroku app and SOURCE is either “transifex” or “github”. 169 | 170 | For instance, we’ll use the following URL with Transifex: 171 | 172 | https://nameless-eyrie-4025.herokuapp.com/hooks/transifex 173 | 174 | and the following URL with GitHub: 175 | 176 | https://nameless-eyrie-4025.herokuapp.com/hooks/github 177 | 178 | Open your project in Transifex. Under More Project Options, click Manage. 179 | 180 | In the Features section at the bottom of the screen is a text box titled Web Hook URL. Enter in the URL you created from your Heroku app, then click Save Project. 181 | 182 | Connecting Your GitHub Repository 183 | Connecting a GitHub repository is similar. Open your repository in a web browser and click Settings. 184 | 185 | Under Webhooks & services, click to add a webhook. You may be asked to confirm your password. Enter the Heroku app URL for the Payload URL and change the Content type to application/x-www-form-urlencoded. 186 | 187 | Click Add webhook to create your new webhook. GitHub will ping the URL to test its validity. You can check whether the ping was successful by reloading the page. 188 | 189 | Next, we’ll test out the integration by moving translations between GitHub and Transifex. 190 | 191 | ##Testing It Out 192 | 193 | To test the integration, we’ll push a new commit to GitHub, then we’ll use the new commit to update translations in Transifex. 194 | 195 | First, add a new string to the language source file in your Transifex project. Save your changes, then push the code to your GitHub repository. The push will automatically trigger the webhook. You can verify that webhook was successful by opening GitHub in a browser, navigating to the Webhooks & services, clicking on the webhook URL, and reviewing Recent Deliveries. 196 | 197 | If successful, you should see the new source strings in your Transifex project. 198 | 199 | Update the translations in Transifex. Back in your GitHub repository, review the latest commits. You should see a commit from Transifex with the latest updates to the target language. 200 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /spec/integration/cassettes/github_l10n_hook_endpoint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: post 5 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs 6 | body: 7 | encoding: UTF-8 8 | string: '{"ref":"refs/heads/L10N","sha":"6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed"}' 9 | headers: 10 | Accept: 11 | - application/vnd.github.v3+json 12 | User-Agent: 13 | - Octokit Ruby Gem 4.2.0 14 | Content-Type: 15 | - application/json 16 | Authorization: 17 | - token 18 | Accept-Encoding: 19 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 20 | response: 21 | status: 22 | code: 422 23 | message: Unprocessable Entity 24 | headers: 25 | Server: 26 | - GitHub.com 27 | Date: 28 | - Tue, 26 Jan 2016 16:58:42 GMT 29 | Content-Type: 30 | - application/json; charset=utf-8 31 | Content-Length: 32 | - '121' 33 | Status: 34 | - 422 Unprocessable Entity 35 | X-Ratelimit-Limit: 36 | - '5000' 37 | X-Ratelimit-Remaining: 38 | - '4987' 39 | X-Ratelimit-Reset: 40 | - '1453830860' 41 | X-Oauth-Scopes: 42 | - public_repo 43 | X-Accepted-Oauth-Scopes: 44 | - '' 45 | X-Github-Media-Type: 46 | - github.v3; format=json 47 | Access-Control-Allow-Credentials: 48 | - 'true' 49 | Access-Control-Expose-Headers: 50 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 51 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 52 | Access-Control-Allow-Origin: 53 | - "*" 54 | Content-Security-Policy: 55 | - default-src 'none' 56 | Strict-Transport-Security: 57 | - max-age=31536000; includeSubdomains; preload 58 | X-Content-Type-Options: 59 | - nosniff 60 | X-Frame-Options: 61 | - deny 62 | X-Xss-Protection: 63 | - 1; mode=block 64 | X-Github-Request-Id: 65 | - A6F186B5:2071:3752D2B:56A7A5C2 66 | body: 67 | encoding: UTF-8 68 | string: '{"message":"Reference already exists","documentation_url":"https://developer.github.com/v3/git/refs/#create-a-reference"}' 69 | http_version: 70 | recorded_at: Tue, 26 Jan 2016 16:58:43 GMT 71 | - request: 72 | method: get 73 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed 74 | body: 75 | encoding: US-ASCII 76 | string: '' 77 | headers: 78 | Accept: 79 | - application/vnd.github.v3+json 80 | User-Agent: 81 | - Octokit Ruby Gem 4.2.0 82 | Content-Type: 83 | - application/json 84 | Authorization: 85 | - token 86 | Accept-Encoding: 87 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 88 | response: 89 | status: 90 | code: 200 91 | message: OK 92 | headers: 93 | Server: 94 | - GitHub.com 95 | Date: 96 | - Tue, 26 Jan 2016 16:58:43 GMT 97 | Content-Type: 98 | - application/json; charset=utf-8 99 | Transfer-Encoding: 100 | - chunked 101 | Status: 102 | - 200 OK 103 | X-Ratelimit-Limit: 104 | - '5000' 105 | X-Ratelimit-Remaining: 106 | - '4986' 107 | X-Ratelimit-Reset: 108 | - '1453830860' 109 | Cache-Control: 110 | - private, max-age=60, s-maxage=60 111 | Last-Modified: 112 | - Thu, 19 Nov 2015 20:56:33 GMT 113 | Etag: 114 | - W/"7bfdb9dfafc4d11bf47411ec7fee1f31" 115 | X-Oauth-Scopes: 116 | - public_repo 117 | X-Accepted-Oauth-Scopes: 118 | - '' 119 | Vary: 120 | - Accept, Authorization, Cookie, X-GitHub-OTP 121 | - Accept-Encoding 122 | X-Github-Media-Type: 123 | - github.v3; format=json 124 | Access-Control-Allow-Credentials: 125 | - 'true' 126 | Access-Control-Expose-Headers: 127 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 128 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 129 | Access-Control-Allow-Origin: 130 | - "*" 131 | Content-Security-Policy: 132 | - default-src 'none' 133 | Strict-Transport-Security: 134 | - max-age=31536000; includeSubdomains; preload 135 | X-Content-Type-Options: 136 | - nosniff 137 | X-Frame-Options: 138 | - deny 139 | X-Xss-Protection: 140 | - 1; mode=block 141 | X-Served-By: 142 | - d594a23ec74671eba905bf91ef329026 143 | X-Github-Request-Id: 144 | - A6F186B5:2075:9DF5EBC:56A7A5C3 145 | body: 146 | encoding: UTF-8 147 | string: '{"sha":"6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed","commit":{"author":{"name":"Matthew 148 | Jackowski","email":"mattjjacko@gmail.com","date":"2015-11-19T20:56:33Z"},"committer":{"name":"Matthew 149 | Jackowski","email":"mattjjacko@gmail.com","date":"2015-11-19T20:56:33Z"},"message":"10","tree":{"sha":"572b0507c4109703788333808a0b0019c0000f50","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/572b0507c4109703788333808a0b0019c0000f50"},"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed","comment_count":0},"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed","html_url":"https://github.com/txgh-bot/txgh-test-resources/commit/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed","comments_url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed/comments","author":{"login":"matthewjackowski","id":974299,"avatar_url":"https://avatars.githubusercontent.com/u/974299?v=3","gravatar_id":"","url":"https://api.github.com/users/matthewjackowski","html_url":"https://github.com/matthewjackowski","followers_url":"https://api.github.com/users/matthewjackowski/followers","following_url":"https://api.github.com/users/matthewjackowski/following{/other_user}","gists_url":"https://api.github.com/users/matthewjackowski/gists{/gist_id}","starred_url":"https://api.github.com/users/matthewjackowski/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matthewjackowski/subscriptions","organizations_url":"https://api.github.com/users/matthewjackowski/orgs","repos_url":"https://api.github.com/users/matthewjackowski/repos","events_url":"https://api.github.com/users/matthewjackowski/events{/privacy}","received_events_url":"https://api.github.com/users/matthewjackowski/received_events","type":"User","site_admin":false},"committer":{"login":"matthewjackowski","id":974299,"avatar_url":"https://avatars.githubusercontent.com/u/974299?v=3","gravatar_id":"","url":"https://api.github.com/users/matthewjackowski","html_url":"https://github.com/matthewjackowski","followers_url":"https://api.github.com/users/matthewjackowski/followers","following_url":"https://api.github.com/users/matthewjackowski/following{/other_user}","gists_url":"https://api.github.com/users/matthewjackowski/gists{/gist_id}","starred_url":"https://api.github.com/users/matthewjackowski/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/matthewjackowski/subscriptions","organizations_url":"https://api.github.com/users/matthewjackowski/orgs","repos_url":"https://api.github.com/users/matthewjackowski/repos","events_url":"https://api.github.com/users/matthewjackowski/events{/privacy}","received_events_url":"https://api.github.com/users/matthewjackowski/received_events","type":"User","site_admin":false},"parents":[{"sha":"61e01fc7270052ac327c8938e63d1f1942a44372","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/61e01fc7270052ac327c8938e63d1f1942a44372","html_url":"https://github.com/txgh-bot/txgh-test-resources/commit/61e01fc7270052ac327c8938e63d1f1942a44372"}],"stats":{"total":2,"additions":2,"deletions":0},"files":[{"sha":"a93e31a00137e0bd23db5bc7720568f8e908b2b6","filename":"sample.po","status":"modified","additions":2,"deletions":0,"changes":2,"blob_url":"https://github.com/txgh-bot/txgh-test-resources/blob/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed/sample.po","raw_url":"https://github.com/txgh-bot/txgh-test-resources/raw/6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed/sample.po","contents_url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/contents/sample.po?ref=6eebc3eee49e8d46f6dbec16fe3360961bfdd8ed","patch":"@@ 150 | -34,3 +34,5 @@ msgid \"Eight\"\n msgstr \"Eight\"\n msgid \"Nine\"\n msgstr 151 | \"Nine\"\n+msgid \"Ten\"\n+msgstr \"Ten\""}]}' 152 | http_version: 153 | recorded_at: Tue, 26 Jan 2016 16:58:44 GMT 154 | - request: 155 | method: get 156 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/572b0507c4109703788333808a0b0019c0000f50?recursive=1 157 | body: 158 | encoding: US-ASCII 159 | string: '' 160 | headers: 161 | Accept: 162 | - application/vnd.github.v3+json 163 | User-Agent: 164 | - Octokit Ruby Gem 4.2.0 165 | Content-Type: 166 | - application/json 167 | Authorization: 168 | - token 169 | Accept-Encoding: 170 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 171 | response: 172 | status: 173 | code: 200 174 | message: OK 175 | headers: 176 | Server: 177 | - GitHub.com 178 | Date: 179 | - Tue, 26 Jan 2016 16:58:44 GMT 180 | Content-Type: 181 | - application/json; charset=utf-8 182 | Transfer-Encoding: 183 | - chunked 184 | Status: 185 | - 200 OK 186 | X-Ratelimit-Limit: 187 | - '5000' 188 | X-Ratelimit-Remaining: 189 | - '4985' 190 | X-Ratelimit-Reset: 191 | - '1453830860' 192 | Cache-Control: 193 | - private, max-age=60, s-maxage=60 194 | Last-Modified: 195 | - Mon, 11 Jan 2016 23:45:43 GMT 196 | Etag: 197 | - W/"07cd463077f05587402e4f0815c49053" 198 | X-Oauth-Scopes: 199 | - public_repo 200 | X-Accepted-Oauth-Scopes: 201 | - '' 202 | Vary: 203 | - Accept, Authorization, Cookie, X-GitHub-OTP 204 | - Accept-Encoding 205 | X-Github-Media-Type: 206 | - github.v3; format=json 207 | Access-Control-Allow-Credentials: 208 | - 'true' 209 | Access-Control-Expose-Headers: 210 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 211 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 212 | Access-Control-Allow-Origin: 213 | - "*" 214 | Content-Security-Policy: 215 | - default-src 'none' 216 | Strict-Transport-Security: 217 | - max-age=31536000; includeSubdomains; preload 218 | X-Content-Type-Options: 219 | - nosniff 220 | X-Frame-Options: 221 | - deny 222 | X-Xss-Protection: 223 | - 1; mode=block 224 | X-Served-By: 225 | - e183f7c661b1bbc2c987b3c4dc7b04e0 226 | X-Github-Request-Id: 227 | - A6F186B5:2075:9DF5FB3:56A7A5C3 228 | body: 229 | encoding: UTF-8 230 | string: '{"sha":"572b0507c4109703788333808a0b0019c0000f50","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/572b0507c4109703788333808a0b0019c0000f50","tree":[{"path":"README.md","mode":"100644","type":"blob","sha":"cee445fa5bceaeb83c8d443b28f1647c2feff565","size":53,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/cee445fa5bceaeb83c8d443b28f1647c2feff565"},{"path":"sample.po","mode":"100644","type":"blob","sha":"a93e31a00137e0bd23db5bc7720568f8e908b2b6","size":855,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/a93e31a00137e0bd23db5bc7720568f8e908b2b6"},{"path":"translations","mode":"040000","type":"tree","sha":"5b5d383f4f107ff4d53c8fa4ce1166b63d1a4458","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/5b5d383f4f107ff4d53c8fa4ce1166b63d1a4458"},{"path":"translations/el_GR","mode":"040000","type":"tree","sha":"396520f008766ce17de7b254cdfd3d7b4eda6b44","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/396520f008766ce17de7b254cdfd3d7b4eda6b44"},{"path":"translations/el_GR/sample.po","mode":"100644","type":"blob","sha":"98f5f909182137fd6416b4e672a85c013fef7a3a","size":1181,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/98f5f909182137fd6416b4e672a85c013fef7a3a"},{"path":"translations/el_GR/test.txt","mode":"100644","type":"blob","sha":"f16628a67170414c9d346279f2595e77a1c3cf0f","size":28,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/f16628a67170414c9d346279f2595e77a1c3cf0f"},{"path":"translations/test.txt","mode":"100644","type":"blob","sha":"f16628a67170414c9d346279f2595e77a1c3cf0f","size":28,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/f16628a67170414c9d346279f2595e77a1c3cf0f"}],"truncated":false}' 231 | http_version: 232 | recorded_at: Tue, 26 Jan 2016 16:58:45 GMT 233 | - request: 234 | method: get 235 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/a93e31a00137e0bd23db5bc7720568f8e908b2b6 236 | body: 237 | encoding: US-ASCII 238 | string: '' 239 | headers: 240 | Accept: 241 | - application/vnd.github.v3+json 242 | User-Agent: 243 | - Octokit Ruby Gem 4.2.0 244 | Content-Type: 245 | - application/json 246 | Authorization: 247 | - token 248 | Accept-Encoding: 249 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 250 | response: 251 | status: 252 | code: 200 253 | message: OK 254 | headers: 255 | Server: 256 | - GitHub.com 257 | Date: 258 | - Tue, 26 Jan 2016 16:58:44 GMT 259 | Content-Type: 260 | - application/json; charset=utf-8 261 | Transfer-Encoding: 262 | - chunked 263 | Status: 264 | - 200 OK 265 | X-Ratelimit-Limit: 266 | - '5000' 267 | X-Ratelimit-Remaining: 268 | - '4984' 269 | X-Ratelimit-Reset: 270 | - '1453830860' 271 | Cache-Control: 272 | - private, max-age=60, s-maxage=60 273 | Etag: 274 | - W/"dbb7c82142b789308c5d1aae70171c46" 275 | X-Oauth-Scopes: 276 | - public_repo 277 | X-Accepted-Oauth-Scopes: 278 | - '' 279 | Vary: 280 | - Accept, Authorization, Cookie, X-GitHub-OTP 281 | - Accept-Encoding 282 | X-Github-Media-Type: 283 | - github.v3; format=json 284 | Access-Control-Allow-Credentials: 285 | - 'true' 286 | Access-Control-Expose-Headers: 287 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 288 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 289 | Access-Control-Allow-Origin: 290 | - "*" 291 | Content-Security-Policy: 292 | - default-src 'none' 293 | Strict-Transport-Security: 294 | - max-age=31536000; includeSubdomains; preload 295 | X-Content-Type-Options: 296 | - nosniff 297 | X-Frame-Options: 298 | - deny 299 | X-Xss-Protection: 300 | - 1; mode=block 301 | X-Served-By: 302 | - dc1ce2bfb41810a06c705e83b388572d 303 | X-Github-Request-Id: 304 | - A6F186B5:2072:4E8154E:56A7A5C4 305 | body: 306 | encoding: UTF-8 307 | string: '{"sha":"a93e31a00137e0bd23db5bc7720568f8e908b2b6","size":855,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/a93e31a00137e0bd23db5bc7720568f8e908b2b6","content":"IyBUcmFuc2xhdGlvbiBmaWxlIGZvciBUcmFuc2lmZXguCiMgQ29weXJpZ2h0\nIChDKSAyMDA3LTIwMTAgSW5kaWZleCBMdGQuCiMgVGhpcyBmaWxlIGlzIGRp\nc3RyaWJ1dGVkIHVuZGVyIHRoZSBzYW1lIGxpY2Vuc2UgYXMgdGhlIFRyYW5z\naWZleCBwYWNrYWdlLgojCiMgVHJhbnNsYXRvcnM6Cm1zZ2lkICIiCm1zZ3N0\nciAiIgoiUHJvamVjdC1JZC1WZXJzaW9uOiBUcmFuc2lmZXhcbiIKIlJlcG9y\ndC1Nc2dpZC1CdWdzLVRvOiBcbiIKIlBPVC1DcmVhdGlvbi1EYXRlOiAyMDE1\nLTAyLTE3IDIwOjEwKzAwMDBcbiIKIlBPLVJldmlzaW9uLURhdGU6IDIwMTMt\nMTAtMjIgMTA6NTcrMDAwMFxuIgoiTGFzdC1UcmFuc2xhdG9yOiBJbGlhcy1E\naW1pdHJpb3MgVnJhY2huaXNcbiIKIkxhbmd1YWdlLVRlYW06IExBTkdVQUdF\nIDxMTEBsaS5vcmc+XG4iCiJMYW5ndWFnZTogZW5cbiIKIk1JTUUtVmVyc2lv\nbjogMS4wXG4iCiJDb250ZW50LVR5cGU6IHRleHQvcGxhaW47IGNoYXJzZXQ9\nVVRGLThcbiIKIkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IDhiaXRcbiIK\nIlBsdXJhbC1Gb3JtczogbnBsdXJhbHM9MjsgcGx1cmFsPShuICE9IDEpO1xu\nIgptc2dpZCAiT25lIgptc2dzdHIgIk9uZSIKbXNnaWQgIlR3byIKbXNnc3Ry\nICJUd28iCm1zZ2lkICJUaHJlZSIKbXNnc3RyICJUaHJlZSIKbXNnaWQgIkZv\ndXIiCm1zZ3N0ciAiRm91ciIKbXNnaWQgIkZpdmUiCm1zZ3N0ciAiRml2ZSIK\nbXNnaWQgIlNpeCIKbXNnc3RyICJTaXgiCm1zZ2lkICJTZXZlbiIKbXNnc3Ry\nICJTZXZlbiIKbXNnaWQgIkVpZ2h0Igptc2dzdHIgIkVpZ2h0Igptc2dpZCAi\nTmluZSIKbXNnc3RyICJOaW5lIgptc2dpZCAiVGVuIgptc2dzdHIgIlRlbiIK\n","encoding":"base64"}' 308 | http_version: 309 | recorded_at: Tue, 26 Jan 2016 16:58:46 GMT 310 | - request: 311 | method: get 312 | uri: https://txgh.bot:@www.transifex.com/api/2/project/test-project-88/resource/samplepo/ 313 | body: 314 | encoding: US-ASCII 315 | string: '' 316 | headers: 317 | User-Agent: 318 | - Faraday v0.9.2 319 | Accept: 320 | - application/json 321 | Accept-Encoding: 322 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 323 | response: 324 | status: 325 | code: 200 326 | message: OK 327 | headers: 328 | Server: 329 | - nginx 330 | Vary: 331 | - Accept-Encoding 332 | - Authorization, Host, Accept-Language, Cookie 333 | Cache-Control: 334 | - max-age=0 335 | Content-Type: 336 | - application/json; charset=utf-8 337 | Date: 338 | - Tue, 26 Jan 2016 16:58:45 GMT 339 | Expires: 340 | - Tue, 26 Jan 2016 16:58:45 GMT 341 | Transfer-Encoding: 342 | - chunked 343 | Content-Language: 344 | - en 345 | X-Content-Type-Options: 346 | - nosniff 347 | Connection: 348 | - keep-alive 349 | Set-Cookie: 350 | - X-Mapping-fjhppofk=B8558B7BB369B761FC4CE03884ACF0F5; path=/ 351 | Last-Modified: 352 | - Tue, 26 Jan 2016 16:58:45 GMT 353 | X-Frame-Options: 354 | - SAMEORIGIN 355 | body: 356 | encoding: UTF-8 357 | string: "{\n \"source_language_code\": \"en\", \n \"name\": \"sample.po\", 358 | \n \"i18n_type\": \"PO\", \n \"priority\": \"0\", \n \"slug\": \"samplepo\", 359 | \n \"categories\": [\n \"branch:refs/tags/L10N\", \n \"author:Txgh 360 | Bot\"\n ]\n}" 361 | http_version: 362 | recorded_at: Tue, 26 Jan 2016 16:58:46 GMT 363 | - request: 364 | method: get 365 | uri: https://txgh.bot:@www.transifex.com/api/2/project/test-project-88/resource/samplepo/ 366 | body: 367 | encoding: US-ASCII 368 | string: '' 369 | headers: 370 | User-Agent: 371 | - Faraday v0.9.2 372 | Accept: 373 | - application/json 374 | Accept-Encoding: 375 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 376 | response: 377 | status: 378 | code: 200 379 | message: OK 380 | headers: 381 | Server: 382 | - nginx 383 | Vary: 384 | - Accept-Encoding 385 | - Authorization, Host, Accept-Language, Cookie 386 | Cache-Control: 387 | - max-age=0 388 | Content-Type: 389 | - application/json; charset=utf-8 390 | Date: 391 | - Tue, 26 Jan 2016 16:58:46 GMT 392 | Expires: 393 | - Tue, 26 Jan 2016 16:58:46 GMT 394 | Transfer-Encoding: 395 | - chunked 396 | Content-Language: 397 | - en 398 | X-Content-Type-Options: 399 | - nosniff 400 | Connection: 401 | - keep-alive 402 | Set-Cookie: 403 | - X-Mapping-fjhppofk=D7F0B90A7FEFB2573FEF4C177EEBE6A8; path=/ 404 | Last-Modified: 405 | - Tue, 26 Jan 2016 16:58:46 GMT 406 | X-Frame-Options: 407 | - SAMEORIGIN 408 | body: 409 | encoding: UTF-8 410 | string: "{\n \"source_language_code\": \"en\", \n \"name\": \"sample.po\", 411 | \n \"i18n_type\": \"PO\", \n \"priority\": \"0\", \n \"slug\": \"samplepo\", 412 | \n \"categories\": [\n \"branch:refs/tags/L10N\", \n \"author:Txgh 413 | Bot\"\n ]\n}" 414 | http_version: 415 | recorded_at: Tue, 26 Jan 2016 16:58:47 GMT 416 | - request: 417 | method: put 418 | uri: https://txgh.bot:@www.transifex.com/api/2/project/test-project-88/resource/samplepo/ 419 | body: 420 | encoding: UTF-8 421 | string: '{"categories":["branch:refs/tags/L10N","author:Txgh Bot"]}' 422 | headers: 423 | User-Agent: 424 | - Faraday v0.9.2 425 | Accept: 426 | - application/json 427 | Content-Type: 428 | - application/json 429 | Accept-Encoding: 430 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 431 | response: 432 | status: 433 | code: 200 434 | message: OK 435 | headers: 436 | Server: 437 | - nginx 438 | Vary: 439 | - Accept-Encoding 440 | - Authorization, Host, Accept-Language, Cookie 441 | Cache-Control: 442 | - max-age=0 443 | Content-Type: 444 | - text/plain 445 | Date: 446 | - Tue, 26 Jan 2016 16:58:47 GMT 447 | Expires: 448 | - Tue, 26 Jan 2016 16:58:47 GMT 449 | Transfer-Encoding: 450 | - chunked 451 | Content-Language: 452 | - en 453 | X-Content-Type-Options: 454 | - nosniff 455 | Connection: 456 | - keep-alive 457 | Set-Cookie: 458 | - X-Mapping-fjhppofk=EC88B8A0F350FE88E4BF67A3F4B1C219; path=/ 459 | Last-Modified: 460 | - Tue, 26 Jan 2016 16:58:47 GMT 461 | X-Frame-Options: 462 | - SAMEORIGIN 463 | body: 464 | encoding: UTF-8 465 | string: OK 466 | http_version: 467 | recorded_at: Tue, 26 Jan 2016 16:58:48 GMT 468 | - request: 469 | method: put 470 | uri: https://txgh.bot:@www.transifex.com/api/2/project/test-project-88/resource/samplepo/content/ 471 | body: 472 | encoding: UTF-8 473 | string: "-------------RubyMultipartPost\r\nContent-Disposition: form-data; name=\"content\"; 474 | filename=\"sample.po\"\r\nContent-Length: 855\r\nContent-Type: application/octet-stream\r\nContent-Transfer-Encoding: 475 | binary\r\n\r\n# Translation file for Transifex.\n# Copyright (C) 2007-2010 476 | Indifex Ltd.\n# This file is distributed under the same license as the Transifex 477 | package.\n#\n# Translators:\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: 478 | Transifex\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2015-02-17 479 | 20:10+0000\\n\"\n\"PO-Revision-Date: 2013-10-22 10:57+0000\\n\"\n\"Last-Translator: 480 | Ilias-Dimitrios Vrachnis\\n\"\n\"Language-Team: LANGUAGE \\n\"\n\"Language: 481 | en\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 482 | 8bit\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 1);\\n\"\nmsgid \"One\"\nmsgstr 483 | \"One\"\nmsgid \"Two\"\nmsgstr \"Two\"\nmsgid \"Three\"\nmsgstr \"Three\"\nmsgid 484 | \"Four\"\nmsgstr \"Four\"\nmsgid \"Five\"\nmsgstr \"Five\"\nmsgid \"Six\"\nmsgstr 485 | \"Six\"\nmsgid \"Seven\"\nmsgstr \"Seven\"\nmsgid \"Eight\"\nmsgstr \"Eight\"\nmsgid 486 | \"Nine\"\nmsgstr \"Nine\"\nmsgid \"Ten\"\nmsgstr \"Ten\"\n\r\n-------------RubyMultipartPost--\r\n\r\n" 487 | headers: 488 | User-Agent: 489 | - Faraday v0.9.2 490 | Accept: 491 | - application/json 492 | Content-Type: 493 | - multipart/form-data; boundary=-----------RubyMultipartPost 494 | Content-Length: 495 | - '1093' 496 | Accept-Encoding: 497 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 498 | response: 499 | status: 500 | code: 200 501 | message: OK 502 | headers: 503 | Server: 504 | - nginx 505 | Vary: 506 | - Accept-Encoding 507 | - Authorization, Host, Accept-Language, Cookie 508 | Cache-Control: 509 | - max-age=0 510 | Content-Type: 511 | - application/json; charset=utf-8 512 | Date: 513 | - Tue, 26 Jan 2016 16:58:48 GMT 514 | Expires: 515 | - Tue, 26 Jan 2016 16:58:48 GMT 516 | Transfer-Encoding: 517 | - chunked 518 | Content-Language: 519 | - en 520 | X-Content-Type-Options: 521 | - nosniff 522 | Connection: 523 | - keep-alive 524 | Set-Cookie: 525 | - X-Mapping-fjhppofk=7BA9C65DD9CD0F1A96194A82BFB61D00; path=/ 526 | Last-Modified: 527 | - Tue, 26 Jan 2016 16:58:48 GMT 528 | X-Frame-Options: 529 | - SAMEORIGIN 530 | body: 531 | encoding: UTF-8 532 | string: "{\n \"strings_added\": 0, \n \"strings_updated\": 0, \n \"strings_delete\": 533 | 0, \n \"redirect\": \"/txgh-test/test-project-88/samplepo/\"\n}" 534 | http_version: 535 | recorded_at: Tue, 26 Jan 2016 16:58:49 GMT 536 | recorded_with: VCR 3.0.1 537 | -------------------------------------------------------------------------------- /spec/integration/cassettes/transifex_hook_endpoint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://txgh.bot:@www.transifex.com/api/2/project/test-project-88/resource/samplepo/translation/el_GR/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Faraday v0.9.2 12 | Accept: 13 | - application/json 14 | Accept-Encoding: 15 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 16 | response: 17 | status: 18 | code: 200 19 | message: OK 20 | headers: 21 | Server: 22 | - nginx 23 | Vary: 24 | - Accept-Encoding 25 | - Authorization, Host, Accept-Language, Cookie 26 | Cache-Control: 27 | - max-age=0 28 | Content-Type: 29 | - application/json; charset=utf-8 30 | Date: 31 | - Tue, 26 Jan 2016 16:58:37 GMT 32 | Expires: 33 | - Tue, 26 Jan 2016 16:58:37 GMT 34 | Transfer-Encoding: 35 | - chunked 36 | Content-Language: 37 | - en 38 | X-Content-Type-Options: 39 | - nosniff 40 | Connection: 41 | - keep-alive 42 | Set-Cookie: 43 | - X-Mapping-fjhppofk=EC88B8A0F350FE88E4BF67A3F4B1C219; path=/ 44 | Last-Modified: 45 | - Tue, 26 Jan 2016 16:58:37 GMT 46 | X-Frame-Options: 47 | - SAMEORIGIN 48 | body: 49 | encoding: UTF-8 50 | string: "{\n \"content\": \"# Translation file for Transifex.\\n# Copyright 51 | (C) 2007-2010 Indifex Ltd.\\n# This file is distributed under the same license 52 | as the Transifex package.\\n# \\n# Translators:\\n# Translators:\\nmsgid \\\"\\\"\\nmsgstr 53 | \\\"\\\"\\n\\\"Project-Id-Version: test-project\\\\n\\\"\\n\\\"Report-Msgid-Bugs-To: 54 | \\\\n\\\"\\n\\\"POT-Creation-Date: 2015-02-17 20:10+0000\\\\n\\\"\\n\\\"PO-Revision-Date: 55 | 2016-01-19 19:08+0000\\\\n\\\"\\n\\\"Last-Translator: Ilias-Dimitrios Vrachnis\\\\n\\\"\\n\\\"Language-Team: 56 | Greek (Greece) (http://www.transifex.com/txgh-test/test-project-88/language/el_GR/)\\\\n\\\"\\n\\\"MIME-Version: 57 | 1.0\\\\n\\\"\\n\\\"Content-Type: text/plain; charset=UTF-8\\\\n\\\"\\n\\\"Content-Transfer-Encoding: 58 | 8bit\\\\n\\\"\\n\\\"Language: el_GR\\\\n\\\"\\n\\\"Plural-Forms: nplurals=2; 59 | plural=(n != 1);\\\\n\\\"\\n\\nmsgid \\\"One\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid 60 | \\\"Two\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid \\\"Three\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid 61 | \\\"Four\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid \\\"Five\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid 62 | \\\"Six\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid \\\"Seven\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid 63 | \\\"Eight\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid \\\"Nine\\\"\\nmsgstr \\\"\\\"\\n\\nmsgid 64 | \\\"Ten\\\"\\nmsgstr \\\"\\\"\\n\", \n \"mimetype\": \"text/x-po\"\n}" 65 | http_version: 66 | recorded_at: Tue, 26 Jan 2016 16:58:39 GMT 67 | - request: 68 | method: post 69 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs 70 | body: 71 | encoding: UTF-8 72 | string: '{"content":"# Translation file for Transifex.\n# Copyright (C) 2007-2010 73 | Indifex Ltd.\n# This file is distributed under the same license as the Transifex 74 | package.\n# \n# Translators:\n# Translators:\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: 75 | test-project\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2015-02-17 76 | 20:10+0000\\n\"\n\"PO-Revision-Date: 2016-01-19 19:08+0000\\n\"\n\"Last-Translator: 77 | Ilias-Dimitrios Vrachnis\\n\"\n\"Language-Team: Greek (Greece) (http://www.transifex.com/txgh-test/test-project-88/language/el_GR/)\\n\"\n\"MIME-Version: 78 | 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 79 | 8bit\\n\"\n\"Language: el_GR\\n\"\n\"Plural-Forms: nplurals=2; plural=(n != 80 | 1);\\n\"\n\nmsgid \"One\"\nmsgstr \"\"\n\nmsgid \"Two\"\nmsgstr \"\"\n\nmsgid 81 | \"Three\"\nmsgstr \"\"\n\nmsgid \"Four\"\nmsgstr \"\"\n\nmsgid \"Five\"\nmsgstr 82 | \"\"\n\nmsgid \"Six\"\nmsgstr \"\"\n\nmsgid \"Seven\"\nmsgstr \"\"\n\nmsgid 83 | \"Eight\"\nmsgstr \"\"\n\nmsgid \"Nine\"\nmsgstr \"\"\n\nmsgid \"Ten\"\nmsgstr 84 | \"\"\n","encoding":"utf-8"}' 85 | headers: 86 | Accept: 87 | - application/vnd.github.v3+json 88 | User-Agent: 89 | - Octokit Ruby Gem 4.2.0 90 | Content-Type: 91 | - application/json 92 | Authorization: 93 | - token 94 | Accept-Encoding: 95 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 96 | response: 97 | status: 98 | code: 201 99 | message: Created 100 | headers: 101 | Server: 102 | - GitHub.com 103 | Date: 104 | - Tue, 26 Jan 2016 16:58:39 GMT 105 | Content-Type: 106 | - application/json; charset=utf-8 107 | Content-Length: 108 | - '167' 109 | Status: 110 | - 201 Created 111 | X-Ratelimit-Limit: 112 | - '5000' 113 | X-Ratelimit-Remaining: 114 | - '4993' 115 | X-Ratelimit-Reset: 116 | - '1453830860' 117 | Cache-Control: 118 | - private, max-age=60, s-maxage=60 119 | Etag: 120 | - '"ed992de3605511d4d93bfd55ab933696"' 121 | X-Oauth-Scopes: 122 | - public_repo 123 | X-Accepted-Oauth-Scopes: 124 | - '' 125 | Location: 126 | - https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/540a3d944ac19f573e756de687c8c4f3f9847b53 127 | Vary: 128 | - Accept, Authorization, Cookie, X-GitHub-OTP 129 | - Accept-Encoding 130 | X-Github-Media-Type: 131 | - github.v3; format=json 132 | Access-Control-Allow-Credentials: 133 | - 'true' 134 | Access-Control-Expose-Headers: 135 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 136 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 137 | Access-Control-Allow-Origin: 138 | - "*" 139 | Content-Security-Policy: 140 | - default-src 'none' 141 | Strict-Transport-Security: 142 | - max-age=31536000; includeSubdomains; preload 143 | X-Content-Type-Options: 144 | - nosniff 145 | X-Frame-Options: 146 | - deny 147 | X-Xss-Protection: 148 | - 1; mode=block 149 | X-Served-By: 150 | - 01d096e6cfe28f8aea352e988c332cd3 151 | X-Github-Request-Id: 152 | - A6F186B5:2075:9DF583B:56A7A5BE 153 | body: 154 | encoding: UTF-8 155 | string: '{"sha":"540a3d944ac19f573e756de687c8c4f3f9847b53","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/540a3d944ac19f573e756de687c8c4f3f9847b53"}' 156 | http_version: 157 | recorded_at: Tue, 26 Jan 2016 16:58:40 GMT 158 | - request: 159 | method: get 160 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs/heads/master 161 | body: 162 | encoding: US-ASCII 163 | string: '' 164 | headers: 165 | Accept: 166 | - application/vnd.github.v3+json 167 | User-Agent: 168 | - Octokit Ruby Gem 4.2.0 169 | Content-Type: 170 | - application/json 171 | Authorization: 172 | - token 173 | Accept-Encoding: 174 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 175 | response: 176 | status: 177 | code: 200 178 | message: OK 179 | headers: 180 | Server: 181 | - GitHub.com 182 | Date: 183 | - Tue, 26 Jan 2016 16:58:39 GMT 184 | Content-Type: 185 | - application/json; charset=utf-8 186 | Transfer-Encoding: 187 | - chunked 188 | Status: 189 | - 200 OK 190 | X-Ratelimit-Limit: 191 | - '5000' 192 | X-Ratelimit-Remaining: 193 | - '4992' 194 | X-Ratelimit-Reset: 195 | - '1453830860' 196 | Cache-Control: 197 | - private, max-age=60, s-maxage=60 198 | Last-Modified: 199 | - Mon, 11 Jan 2016 23:45:43 GMT 200 | Etag: 201 | - W/"829c010dd79e2ffb3e86de693245e41f" 202 | X-Poll-Interval: 203 | - '300' 204 | X-Oauth-Scopes: 205 | - public_repo 206 | X-Accepted-Oauth-Scopes: 207 | - '' 208 | Vary: 209 | - Accept, Authorization, Cookie, X-GitHub-OTP 210 | - Accept-Encoding 211 | X-Github-Media-Type: 212 | - github.v3; format=json 213 | Access-Control-Allow-Credentials: 214 | - 'true' 215 | Access-Control-Expose-Headers: 216 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 217 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 218 | Access-Control-Allow-Origin: 219 | - "*" 220 | Content-Security-Policy: 221 | - default-src 'none' 222 | Strict-Transport-Security: 223 | - max-age=31536000; includeSubdomains; preload 224 | X-Content-Type-Options: 225 | - nosniff 226 | X-Frame-Options: 227 | - deny 228 | X-Xss-Protection: 229 | - 1; mode=block 230 | X-Served-By: 231 | - 173530fed4bbeb1e264b2ed22e8b5c20 232 | X-Github-Request-Id: 233 | - A6F186B5:2073:6B5EDAB:56A7A5BF 234 | body: 235 | encoding: UTF-8 236 | string: '{"ref":"refs/heads/master","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs/heads/master","object":{"sha":"51886fe773848ff49f5313c8afe4e026f044ae66","type":"commit","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/51886fe773848ff49f5313c8afe4e026f044ae66"}}' 237 | http_version: 238 | recorded_at: Tue, 26 Jan 2016 16:58:40 GMT 239 | - request: 240 | method: get 241 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/51886fe773848ff49f5313c8afe4e026f044ae66 242 | body: 243 | encoding: US-ASCII 244 | string: '' 245 | headers: 246 | Accept: 247 | - application/vnd.github.v3+json 248 | User-Agent: 249 | - Octokit Ruby Gem 4.2.0 250 | Content-Type: 251 | - application/json 252 | Authorization: 253 | - token 254 | Accept-Encoding: 255 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 256 | response: 257 | status: 258 | code: 200 259 | message: OK 260 | headers: 261 | Server: 262 | - GitHub.com 263 | Date: 264 | - Tue, 26 Jan 2016 16:58:40 GMT 265 | Content-Type: 266 | - application/json; charset=utf-8 267 | Transfer-Encoding: 268 | - chunked 269 | Status: 270 | - 200 OK 271 | X-Ratelimit-Limit: 272 | - '5000' 273 | X-Ratelimit-Remaining: 274 | - '4991' 275 | X-Ratelimit-Reset: 276 | - '1453830860' 277 | Cache-Control: 278 | - private, max-age=60, s-maxage=60 279 | Last-Modified: 280 | - Mon, 25 Jan 2016 22:14:05 GMT 281 | Etag: 282 | - W/"4b78669c490ba191aff076a1e68dad2d" 283 | X-Oauth-Scopes: 284 | - public_repo 285 | X-Accepted-Oauth-Scopes: 286 | - '' 287 | Vary: 288 | - Accept, Authorization, Cookie, X-GitHub-OTP 289 | - Accept-Encoding 290 | X-Github-Media-Type: 291 | - github.v3; format=json 292 | Access-Control-Allow-Credentials: 293 | - 'true' 294 | Access-Control-Expose-Headers: 295 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 296 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 297 | Access-Control-Allow-Origin: 298 | - "*" 299 | Content-Security-Policy: 300 | - default-src 'none' 301 | Strict-Transport-Security: 302 | - max-age=31536000; includeSubdomains; preload 303 | X-Content-Type-Options: 304 | - nosniff 305 | X-Frame-Options: 306 | - deny 307 | X-Xss-Protection: 308 | - 1; mode=block 309 | X-Served-By: 310 | - a6882e5cd2513376cb9481dbcd83f3a2 311 | X-Github-Request-Id: 312 | - A6F186B5:2075:9DF5A2A:56A7A5BF 313 | body: 314 | encoding: UTF-8 315 | string: '{"sha":"51886fe773848ff49f5313c8afe4e026f044ae66","commit":{"author":{"name":"txgh-bot","email":"txgh.bot@gmail.com","date":"2016-01-25T22:14:05Z"},"committer":{"name":"txgh-bot","email":"txgh.bot@gmail.com","date":"2016-01-25T22:14:05Z"},"message":"Updating 316 | translations for translations/el_GR/sample.po","tree":{"sha":"4ee65f809366225a4d575ad47f0f8c2a05a6d460","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/4ee65f809366225a4d575ad47f0f8c2a05a6d460"},"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/51886fe773848ff49f5313c8afe4e026f044ae66","comment_count":0},"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/51886fe773848ff49f5313c8afe4e026f044ae66","html_url":"https://github.com/txgh-bot/txgh-test-resources/commit/51886fe773848ff49f5313c8afe4e026f044ae66","comments_url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/51886fe773848ff49f5313c8afe4e026f044ae66/comments","author":{"login":"txgh-bot","id":16723366,"avatar_url":"https://avatars.githubusercontent.com/u/16723366?v=3","gravatar_id":"","url":"https://api.github.com/users/txgh-bot","html_url":"https://github.com/txgh-bot","followers_url":"https://api.github.com/users/txgh-bot/followers","following_url":"https://api.github.com/users/txgh-bot/following{/other_user}","gists_url":"https://api.github.com/users/txgh-bot/gists{/gist_id}","starred_url":"https://api.github.com/users/txgh-bot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/txgh-bot/subscriptions","organizations_url":"https://api.github.com/users/txgh-bot/orgs","repos_url":"https://api.github.com/users/txgh-bot/repos","events_url":"https://api.github.com/users/txgh-bot/events{/privacy}","received_events_url":"https://api.github.com/users/txgh-bot/received_events","type":"User","site_admin":false},"committer":{"login":"txgh-bot","id":16723366,"avatar_url":"https://avatars.githubusercontent.com/u/16723366?v=3","gravatar_id":"","url":"https://api.github.com/users/txgh-bot","html_url":"https://github.com/txgh-bot","followers_url":"https://api.github.com/users/txgh-bot/followers","following_url":"https://api.github.com/users/txgh-bot/following{/other_user}","gists_url":"https://api.github.com/users/txgh-bot/gists{/gist_id}","starred_url":"https://api.github.com/users/txgh-bot/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/txgh-bot/subscriptions","organizations_url":"https://api.github.com/users/txgh-bot/orgs","repos_url":"https://api.github.com/users/txgh-bot/repos","events_url":"https://api.github.com/users/txgh-bot/events{/privacy}","received_events_url":"https://api.github.com/users/txgh-bot/received_events","type":"User","site_admin":false},"parents":[{"sha":"5a92eb64ba0f092fccb9942ea8763bdba385694f","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/commits/5a92eb64ba0f092fccb9942ea8763bdba385694f","html_url":"https://github.com/txgh-bot/txgh-test-resources/commit/5a92eb64ba0f092fccb9942ea8763bdba385694f"}],"stats":{"total":0,"additions":0,"deletions":0},"files":[]}' 317 | http_version: 318 | recorded_at: Tue, 26 Jan 2016 16:58:41 GMT 319 | - request: 320 | method: post 321 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees 322 | body: 323 | encoding: UTF-8 324 | string: '{"base_tree":"4ee65f809366225a4d575ad47f0f8c2a05a6d460","tree":[{"path":"translations/el_GR/sample.po","mode":"100644","type":"blob","sha":"540a3d944ac19f573e756de687c8c4f3f9847b53"}]}' 325 | headers: 326 | Accept: 327 | - application/vnd.github.v3+json 328 | User-Agent: 329 | - Octokit Ruby Gem 4.2.0 330 | Content-Type: 331 | - application/json 332 | Authorization: 333 | - token 334 | Accept-Encoding: 335 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 336 | response: 337 | status: 338 | code: 201 339 | message: Created 340 | headers: 341 | Server: 342 | - GitHub.com 343 | Date: 344 | - Tue, 26 Jan 2016 16:58:40 GMT 345 | Content-Type: 346 | - application/json; charset=utf-8 347 | Content-Length: 348 | - '869' 349 | Status: 350 | - 201 Created 351 | X-Ratelimit-Limit: 352 | - '5000' 353 | X-Ratelimit-Remaining: 354 | - '4990' 355 | X-Ratelimit-Reset: 356 | - '1453830860' 357 | Cache-Control: 358 | - private, max-age=60, s-maxage=60 359 | Etag: 360 | - '"285d1085d15dc469819c68a1aafc59be"' 361 | X-Oauth-Scopes: 362 | - public_repo 363 | X-Accepted-Oauth-Scopes: 364 | - '' 365 | Location: 366 | - https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/4ee65f809366225a4d575ad47f0f8c2a05a6d460 367 | Vary: 368 | - Accept, Authorization, Cookie, X-GitHub-OTP 369 | - Accept-Encoding 370 | X-Github-Media-Type: 371 | - github.v3; format=json 372 | Access-Control-Allow-Credentials: 373 | - 'true' 374 | Access-Control-Expose-Headers: 375 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 376 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 377 | Access-Control-Allow-Origin: 378 | - "*" 379 | Content-Security-Policy: 380 | - default-src 'none' 381 | Strict-Transport-Security: 382 | - max-age=31536000; includeSubdomains; preload 383 | X-Content-Type-Options: 384 | - nosniff 385 | X-Frame-Options: 386 | - deny 387 | X-Xss-Protection: 388 | - 1; mode=block 389 | X-Served-By: 390 | - 01d096e6cfe28f8aea352e988c332cd3 391 | X-Github-Request-Id: 392 | - A6F186B5:2074:8893062:56A7A5C0 393 | body: 394 | encoding: UTF-8 395 | string: '{"sha":"4ee65f809366225a4d575ad47f0f8c2a05a6d460","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/4ee65f809366225a4d575ad47f0f8c2a05a6d460","tree":[{"path":"README.md","mode":"100644","type":"blob","sha":"cee445fa5bceaeb83c8d443b28f1647c2feff565","size":53,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/cee445fa5bceaeb83c8d443b28f1647c2feff565"},{"path":"sample.po","mode":"100644","type":"blob","sha":"a93e31a00137e0bd23db5bc7720568f8e908b2b6","size":855,"url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/blobs/a93e31a00137e0bd23db5bc7720568f8e908b2b6"},{"path":"translations","mode":"040000","type":"tree","sha":"64881902c6515e26c22e022abc0bedeaa15eafdc","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/64881902c6515e26c22e022abc0bedeaa15eafdc"}],"truncated":false}' 396 | http_version: 397 | recorded_at: Tue, 26 Jan 2016 16:58:41 GMT 398 | - request: 399 | method: post 400 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits 401 | body: 402 | encoding: UTF-8 403 | string: '{"message":"Updating translations for translations/el_GR/sample.po","tree":"4ee65f809366225a4d575ad47f0f8c2a05a6d460","parents":["51886fe773848ff49f5313c8afe4e026f044ae66"]}' 404 | headers: 405 | Accept: 406 | - application/vnd.github.v3+json 407 | User-Agent: 408 | - Octokit Ruby Gem 4.2.0 409 | Content-Type: 410 | - application/json 411 | Authorization: 412 | - token 413 | Accept-Encoding: 414 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 415 | response: 416 | status: 417 | code: 201 418 | message: Created 419 | headers: 420 | Server: 421 | - GitHub.com 422 | Date: 423 | - Tue, 26 Jan 2016 16:58:41 GMT 424 | Content-Type: 425 | - application/json; charset=utf-8 426 | Content-Length: 427 | - '990' 428 | Status: 429 | - 201 Created 430 | X-Ratelimit-Limit: 431 | - '5000' 432 | X-Ratelimit-Remaining: 433 | - '4989' 434 | X-Ratelimit-Reset: 435 | - '1453830860' 436 | Cache-Control: 437 | - private, max-age=60, s-maxage=60 438 | Etag: 439 | - '"fd8f1f505d5262148b82200a46d27bd5"' 440 | X-Oauth-Scopes: 441 | - public_repo 442 | X-Accepted-Oauth-Scopes: 443 | - '' 444 | Location: 445 | - https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/f0356f2c6216af45b74a517835a6b8d11c246a33 446 | Vary: 447 | - Accept, Authorization, Cookie, X-GitHub-OTP 448 | - Accept-Encoding 449 | X-Github-Media-Type: 450 | - github.v3; format=json 451 | Access-Control-Allow-Credentials: 452 | - 'true' 453 | Access-Control-Expose-Headers: 454 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 455 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 456 | Access-Control-Allow-Origin: 457 | - "*" 458 | Content-Security-Policy: 459 | - default-src 'none' 460 | Strict-Transport-Security: 461 | - max-age=31536000; includeSubdomains; preload 462 | X-Content-Type-Options: 463 | - nosniff 464 | X-Frame-Options: 465 | - deny 466 | X-Xss-Protection: 467 | - 1; mode=block 468 | X-Served-By: 469 | - 4c8b2d4732c413f4b9aefe394bd65569 470 | X-Github-Request-Id: 471 | - A6F186B5:2074:889311A:56A7A5C1 472 | body: 473 | encoding: UTF-8 474 | string: '{"sha":"f0356f2c6216af45b74a517835a6b8d11c246a33","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/f0356f2c6216af45b74a517835a6b8d11c246a33","html_url":"https://github.com/txgh-bot/txgh-test-resources/commit/f0356f2c6216af45b74a517835a6b8d11c246a33","author":{"name":"txgh-bot","email":"txgh.bot@gmail.com","date":"2016-01-26T16:58:41Z"},"committer":{"name":"txgh-bot","email":"txgh.bot@gmail.com","date":"2016-01-26T16:58:41Z"},"tree":{"sha":"4ee65f809366225a4d575ad47f0f8c2a05a6d460","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/trees/4ee65f809366225a4d575ad47f0f8c2a05a6d460"},"message":"Updating 475 | translations for translations/el_GR/sample.po","parents":[{"sha":"51886fe773848ff49f5313c8afe4e026f044ae66","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/51886fe773848ff49f5313c8afe4e026f044ae66","html_url":"https://github.com/txgh-bot/txgh-test-resources/commit/51886fe773848ff49f5313c8afe4e026f044ae66"}]}' 476 | http_version: 477 | recorded_at: Tue, 26 Jan 2016 16:58:42 GMT 478 | - request: 479 | method: patch 480 | uri: https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs/heads/master 481 | body: 482 | encoding: UTF-8 483 | string: '{"sha":"f0356f2c6216af45b74a517835a6b8d11c246a33","force":true}' 484 | headers: 485 | Accept: 486 | - application/vnd.github.v3+json 487 | User-Agent: 488 | - Octokit Ruby Gem 4.2.0 489 | Content-Type: 490 | - application/json 491 | Authorization: 492 | - token 493 | Accept-Encoding: 494 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 495 | response: 496 | status: 497 | code: 200 498 | message: OK 499 | headers: 500 | Server: 501 | - GitHub.com 502 | Date: 503 | - Tue, 26 Jan 2016 16:58:42 GMT 504 | Content-Type: 505 | - application/json; charset=utf-8 506 | Transfer-Encoding: 507 | - chunked 508 | Status: 509 | - 200 OK 510 | X-Ratelimit-Limit: 511 | - '5000' 512 | X-Ratelimit-Remaining: 513 | - '4988' 514 | X-Ratelimit-Reset: 515 | - '1453830860' 516 | Cache-Control: 517 | - private, max-age=60, s-maxage=60 518 | Etag: 519 | - W/"a660aabe64518dee2922fd0ed8b92530" 520 | X-Oauth-Scopes: 521 | - public_repo 522 | X-Accepted-Oauth-Scopes: 523 | - '' 524 | Vary: 525 | - Accept, Authorization, Cookie, X-GitHub-OTP 526 | - Accept-Encoding 527 | X-Github-Media-Type: 528 | - github.v3; format=json 529 | Access-Control-Allow-Credentials: 530 | - 'true' 531 | Access-Control-Expose-Headers: 532 | - ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 533 | X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval 534 | Access-Control-Allow-Origin: 535 | - "*" 536 | Content-Security-Policy: 537 | - default-src 'none' 538 | Strict-Transport-Security: 539 | - max-age=31536000; includeSubdomains; preload 540 | X-Content-Type-Options: 541 | - nosniff 542 | X-Frame-Options: 543 | - deny 544 | X-Xss-Protection: 545 | - 1; mode=block 546 | X-Served-By: 547 | - a30e6f9aa7cf5731b87dfb3b9992202d 548 | X-Github-Request-Id: 549 | - A6F186B5:2075:9DF5CDD:56A7A5C1 550 | body: 551 | encoding: UTF-8 552 | string: '{"ref":"refs/heads/master","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/refs/heads/master","object":{"sha":"f0356f2c6216af45b74a517835a6b8d11c246a33","type":"commit","url":"https://api.github.com/repos/txgh-bot/txgh-test-resources/git/commits/f0356f2c6216af45b74a517835a6b8d11c246a33"}}' 553 | http_version: 554 | recorded_at: Tue, 26 Jan 2016 16:58:43 GMT 555 | recorded_with: VCR 3.0.1 556 | --------------------------------------------------------------------------------