gotsha 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gotsha/config.toml +10 -0
- data/.gotsha/github_action_example.yml +29 -0
- data/.gotsha/hooks/post-commit +6 -0
- data/.gotsha/hooks/pre-push +21 -0
- data/.rspec +3 -0
- data/.rubocop.yml +19 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +19 -0
- data/Rakefile +12 -0
- data/exe/gotsha +18 -0
- data/lib/gotsha/action_dispatcher.rb +49 -0
- data/lib/gotsha/actions/commit.rb +13 -0
- data/lib/gotsha/actions/help.rb +13 -0
- data/lib/gotsha/actions/init.rb +52 -0
- data/lib/gotsha/actions/show.rb +19 -0
- data/lib/gotsha/actions/status.rb +27 -0
- data/lib/gotsha/actions/test.rb +74 -0
- data/lib/gotsha/actions/uninstall.rb +19 -0
- data/lib/gotsha/bash_command.rb +50 -0
- data/lib/gotsha/config.rb +13 -0
- data/lib/gotsha/errors.rb +8 -0
- data/lib/gotsha/templates/config.toml +39 -0
- data/lib/gotsha/templates/git_hooks/post-commit +6 -0
- data/lib/gotsha/templates/git_hooks/pre-push +21 -0
- data/lib/gotsha/templates/github_action_example.yml +44 -0
- data/lib/gotsha/user_config.rb +20 -0
- data/lib/gotsha/version.rb +5 -0
- data/lib/gotsha.rb +25 -0
- data/sig/gotsha.rbs +4 -0
- data/web/favicon.ico +0 -0
- data/web/index.html +142 -0
- data/web/logo.png +0 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2d8a54936a5f4177dc541bf77ad000ff0a047fb987206992a8380fce12bbe98a
|
|
4
|
+
data.tar.gz: cf1c7d18088b0dc33170416b6803bfcddfa47e44fed4d479eb4a84ae48ee8ef6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b23a2dd6fc8aa5365f8e8b3d24e1f44d336098333429d0b3359423d84956843f362823ab0becfe96f2970dc9ebffb618a76d7f265b14ab605c3718fb608336eb
|
|
7
|
+
data.tar.gz: 8d88f4379b75bc59b213d7c94e457d02a529e5376cd1a4243fd835b280061b6f39a4d213aa382280483585744719aa58ce19455f6fb3a73aab181fdcde803c43
|
data/.gotsha/config.toml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# If you want to create Github Action, copy this file into .gihtub folder
|
|
2
|
+
# For example to: .github/workflows/main.yml
|
|
3
|
+
name: Gotsha
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
verify:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0
|
|
15
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
16
|
+
|
|
17
|
+
- name: Fetch gotsha notes
|
|
18
|
+
run: git fetch origin 'refs/notes/gotsha:refs/notes/gotsha'
|
|
19
|
+
|
|
20
|
+
- name: Verify gotsha note
|
|
21
|
+
run: |
|
|
22
|
+
SHA="${{ github.event.pull_request.head.sha }}"
|
|
23
|
+
NOTE="$(git notes --ref=gotsha show "$SHA" 2>/dev/null || true)"
|
|
24
|
+
if [ "$NOTE" = "ok" ]; then
|
|
25
|
+
echo "✓ gotsha verified for $SHA"
|
|
26
|
+
else
|
|
27
|
+
echo "::error ::Missing gotsha note for $SHA"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
[ -f .gotsha/config.toml ] || exit 0
|
|
5
|
+
|
|
6
|
+
grep -qE 'pre_push_tests\s*=\s*true' .gotsha/config.toml && (exe/gotsha status || exe/gotsha test)
|
|
7
|
+
|
|
8
|
+
push_gotsha_notes() {
|
|
9
|
+
# if already done before, let's only push the notes and exit
|
|
10
|
+
git push --no-verify origin refs/notes/gotsha:refs/notes/gotsha && return 0
|
|
11
|
+
|
|
12
|
+
# if pushing above failed, it means we need to do some one-time Git setup
|
|
13
|
+
git fetch origin 'refs/notes/gotsha:refs/notes/gotsha-remote'
|
|
14
|
+
git notes --ref=gotsha merge -v refs/notes/gotsha-remote
|
|
15
|
+
git update-ref -d refs/notes/gotsha-remote
|
|
16
|
+
|
|
17
|
+
# ... and push again!
|
|
18
|
+
git push --no-verify origin refs/notes/gotsha:refs/notes/gotsha
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
push_gotsha_notes
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 2.6
|
|
3
|
+
NewCops: disable
|
|
4
|
+
|
|
5
|
+
Style/StringLiterals:
|
|
6
|
+
EnforcedStyle: double_quotes
|
|
7
|
+
|
|
8
|
+
Style/StringLiteralsInInterpolation:
|
|
9
|
+
EnforcedStyle: double_quotes
|
|
10
|
+
|
|
11
|
+
Metrics/MethodLength:
|
|
12
|
+
Max: 20
|
|
13
|
+
|
|
14
|
+
Style/Documentation:
|
|
15
|
+
Enabled: false
|
|
16
|
+
|
|
17
|
+
Metrics/BlockLength:
|
|
18
|
+
Exclude:
|
|
19
|
+
- 'spec/**/*'
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vitek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Gotsha — your local CI (alpha coming soon)
|
|
2
|
+
Pushing untested commits? Gotsha!
|
|
3
|
+
|
|
4
|
+
## What is it?
|
|
5
|
+
Gotsha is a tiny tool that lets you “sign off” your commit locally: it runs your tests and then stores the test results with the commit SHA (hence the gem name: got-SHA). Your pull request can then be verified against that record, so reviewers know you actually ran the checks before asking for review.
|
|
6
|
+
|
|
7
|
+
Instead of pushing everything to CI, you can run the same checks locally (faster, cheaper, works offline) and prove you did it. Gotsha will make this proof visible in your pull request.
|
|
8
|
+
|
|
9
|
+
And the best part? It all happens automatically!
|
|
10
|
+
|
|
11
|
+
<img width="664" height="523" alt="image" src="https://github.com/user-attachments/assets/6acb4a69-c405-420e-9a05-9b28df4ea1f0" />
|
|
12
|
+
|
|
13
|
+
Then, you can see the tests results in a Github action:
|
|
14
|
+
|
|
15
|
+
<img width="1022" height="858" alt="image" src="https://github.com/user-attachments/assets/cf5d6492-02a0-47ee-81ee-4e34234a7983" />
|
|
16
|
+
|
|
17
|
+
(Screenshots from a real [demo pull request](https://github.com/melounvitek/gotsha/pull/35); check it out!)
|
|
18
|
+
|
|
19
|
+
Based on your workflow and tests speed, you can configure them to auto-run on every commit, before every push, or just manually. Whenever pushing to remote repository, the Git note (which is what's used to store the test results) gets sent there as well.
|
data/Rakefile
ADDED
data/exe/gotsha
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/gotsha"
|
|
5
|
+
|
|
6
|
+
action = ARGV.first
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
message = Gotsha::ActionDispatcher.call(action)
|
|
10
|
+
puts("\n✓ Gotsha: #{message}\n\n")
|
|
11
|
+
exit 0
|
|
12
|
+
rescue Gotsha::Errors::SoftFail => e
|
|
13
|
+
puts "\n✗ Gotsha: #{e.message}\n\n"
|
|
14
|
+
exit 0
|
|
15
|
+
rescue Gotsha::Errors::HardFail => e
|
|
16
|
+
puts "\n✗ Gotsha: #{e.message}\n\n"
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
class ActionDispatcher
|
|
5
|
+
INIT_SETUP_ACTION = "init"
|
|
6
|
+
DEFAULT_ACTION = "help"
|
|
7
|
+
|
|
8
|
+
def self.call(action_name = DEFAULT_ACTION)
|
|
9
|
+
action_name ||= DEFAULT_ACTION
|
|
10
|
+
|
|
11
|
+
new.call(action_name)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(action_name)
|
|
15
|
+
@action_name = action_name
|
|
16
|
+
|
|
17
|
+
verify_configuration!
|
|
18
|
+
|
|
19
|
+
action_class.new.call
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :action_name
|
|
25
|
+
|
|
26
|
+
def verify_configuration!
|
|
27
|
+
return if action_name.to_s == INIT_SETUP_ACTION
|
|
28
|
+
|
|
29
|
+
raise(Errors::HardFail, "config files not found, please run `bundle exec gotsha init` first") if UserConfig.blank?
|
|
30
|
+
|
|
31
|
+
hooks_dir = BashCommand.run!("git config core.hooksPath").text_output
|
|
32
|
+
|
|
33
|
+
unless hooks_dir == Config::HOOKS_DIR
|
|
34
|
+
raise(Errors::HardFail, "Git hooks not configured, please run `bundle exec gotsha init`")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
return unless UserConfig.get(:autogenerated)
|
|
38
|
+
|
|
39
|
+
raise Errors::HardFail,
|
|
40
|
+
"autogenerated config detected! Please, remove `autogenerated = true` from `.gotsha/config.toml`"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def action_class
|
|
44
|
+
Kernel.const_get("Gotsha::Actions::#{action_name.capitalize}")
|
|
45
|
+
rescue NameError
|
|
46
|
+
raise Errors::HardFail, "unknown command `#{action_name}`. #{Actions::Help.new.call}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
module Actions
|
|
5
|
+
class Init
|
|
6
|
+
def call
|
|
7
|
+
puts "Creating files..."
|
|
8
|
+
|
|
9
|
+
config_files!
|
|
10
|
+
github_action!
|
|
11
|
+
hooks!
|
|
12
|
+
|
|
13
|
+
# TODO: I don't like this
|
|
14
|
+
Kernel.system("git config --local core.hooksPath .gotsha/hooks")
|
|
15
|
+
|
|
16
|
+
"done"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def config_files!
|
|
22
|
+
return if File.exist?(Config::CONFIG_FILE)
|
|
23
|
+
|
|
24
|
+
FileUtils.mkdir_p(Config::CONFIG_DIR)
|
|
25
|
+
|
|
26
|
+
File.write(Config::CONFIG_FILE, File.read(Config::CONFIG_TEMPLATE_PATH))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def github_action!
|
|
30
|
+
return if File.exist?(Config::GH_CONFIG_FILE)
|
|
31
|
+
|
|
32
|
+
FileUtils.mkdir_p(".github")
|
|
33
|
+
FileUtils.mkdir_p(".github/workflows")
|
|
34
|
+
File.write(Config::GH_CONFIG_FILE, File.read(Config::GH_CONFIG_TEMPLATE_PATH))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def hooks!
|
|
38
|
+
return if File.exist?("#{Config::HOOKS_DIR}/post-commit") && File.exist?("#{Config::HOOKS_DIR}/pre-push")
|
|
39
|
+
|
|
40
|
+
FileUtils.mkdir_p(Config::HOOKS_DIR)
|
|
41
|
+
|
|
42
|
+
%w[post-commit pre-push].each do |hook|
|
|
43
|
+
src = File.join(Config::HOOKS_TEMPLATES_DIR, "git_hooks", hook)
|
|
44
|
+
dst = File.join(Config::HOOKS_DIR, hook)
|
|
45
|
+
|
|
46
|
+
FileUtils.cp(src, dst)
|
|
47
|
+
FileUtils.chmod("+x", dst)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
module Actions
|
|
5
|
+
class Show
|
|
6
|
+
def call
|
|
7
|
+
command = BashCommand.silent_run!("git --no-pager notes --ref=gotsha show")
|
|
8
|
+
|
|
9
|
+
raise(Errors::HardFail, "not verified yet") unless command.success?
|
|
10
|
+
|
|
11
|
+
gotsha_result = command.text_output
|
|
12
|
+
|
|
13
|
+
raise(Errors::HardFail, gotsha_result) if gotsha_result.start_with?(Test::TESTS_FAILED_NOTE_PREFIX)
|
|
14
|
+
|
|
15
|
+
gotsha_result
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
module Actions
|
|
5
|
+
class Status
|
|
6
|
+
def call
|
|
7
|
+
last_commit_sha = BashCommand.run!("git --no-pager rev-parse HEAD").text_output
|
|
8
|
+
|
|
9
|
+
last_commit_note =
|
|
10
|
+
BashCommand.run!("git --no-pager notes --ref=gotsha show #{last_commit_sha}").text_output
|
|
11
|
+
|
|
12
|
+
if last_commit_note.start_with?("error: no note found") || last_commit_note.to_s.empty?
|
|
13
|
+
raise(Errors::HardFail,
|
|
14
|
+
"not verified yet")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
raise(Errors::HardFail, "tests failed") if last_commit_note.start_with?(Test::TESTS_FAILED_NOTE_PREFIX)
|
|
18
|
+
|
|
19
|
+
unless last_commit_note.start_with?(Test::TESTS_PASSED_NOTE_PREFIX)
|
|
20
|
+
raise(Errors::HardFail, "unknown note content")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
"tests passed"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
module Actions
|
|
5
|
+
class Test
|
|
6
|
+
TESTS_PASSED_NOTE_PREFIX = "Tests passed:"
|
|
7
|
+
TESTS_FAILED_NOTE_PREFIX = "Tests failed:"
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@tests_text_outputs = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
ensure_commands_defined!
|
|
15
|
+
run_commands!
|
|
16
|
+
create_git_note!(TESTS_PASSED_NOTE_PREFIX)
|
|
17
|
+
|
|
18
|
+
"commit verified"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def ensure_commands_defined!
|
|
24
|
+
return if commands.any?
|
|
25
|
+
|
|
26
|
+
raise(Errors::HardFail,
|
|
27
|
+
"please, define some test commands in `.gotsha/config.toml`")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def run_commands!
|
|
31
|
+
commands.each do |command|
|
|
32
|
+
puts "Running `#{command}`..."
|
|
33
|
+
|
|
34
|
+
command_result = BashCommand.run!(command)
|
|
35
|
+
|
|
36
|
+
@tests_text_outputs << command_result.text_output
|
|
37
|
+
|
|
38
|
+
next if command_result.success?
|
|
39
|
+
|
|
40
|
+
create_git_note!(TESTS_FAILED_NOTE_PREFIX)
|
|
41
|
+
puts command_result.text_output.split("\n").last(20).join("\n")
|
|
42
|
+
|
|
43
|
+
raise fail_exception, "tests failed (run `bundle exec gotsha show` for full output)"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_git_note!(prefix_text = "")
|
|
48
|
+
body = +""
|
|
49
|
+
body << prefix_text.to_s
|
|
50
|
+
body << "\n\n" unless prefix_text.to_s.empty?
|
|
51
|
+
body << @tests_text_outputs.join("\n\n")
|
|
52
|
+
|
|
53
|
+
b64 = [body].pack("m0") # base64 (no newlines)
|
|
54
|
+
esc = b64.gsub("'", %q('"'"')) # escape single quotes
|
|
55
|
+
|
|
56
|
+
BashCommand.silent_run!(
|
|
57
|
+
"PAGER=cat GIT_PAGER=cat sh -c 'printf %s \"#{esc}\" | base64 -d | git notes --ref=gotsha add -f -F -'"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def fail_exception
|
|
62
|
+
if UserConfig.get(:interrupt_push_on_tests_failure)
|
|
63
|
+
Errors::HardFail
|
|
64
|
+
else
|
|
65
|
+
Errors::SoftFail
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def commands
|
|
70
|
+
@commands ||= UserConfig.get(:commands) || []
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
module Actions
|
|
5
|
+
class Uninstall
|
|
6
|
+
def call
|
|
7
|
+
puts "Removing config files..."
|
|
8
|
+
|
|
9
|
+
FileUtils.rm_rf(Config::CONFIG_DIR)
|
|
10
|
+
FileUtils.rm(Config::GH_CONFIG_FILE)
|
|
11
|
+
|
|
12
|
+
puts "Unsetting Git hooks path..."
|
|
13
|
+
BashCommand.silent_run!("git config --unset core.hooksPath")
|
|
14
|
+
|
|
15
|
+
"done"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pty"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Gotsha
|
|
7
|
+
class BashCommand
|
|
8
|
+
FORCE_OUTPUT_AFTER = 5
|
|
9
|
+
|
|
10
|
+
def self.run!(command)
|
|
11
|
+
start_time = Time.now
|
|
12
|
+
UserConfig.get(:verbose) && puts(command)
|
|
13
|
+
|
|
14
|
+
stdout = +""
|
|
15
|
+
|
|
16
|
+
wrapped = %(script -qefc #{Shellwords.escape(command)} /dev/null)
|
|
17
|
+
|
|
18
|
+
io = IO.popen(wrapped, in: File::NULL, err: %i[child out])
|
|
19
|
+
begin
|
|
20
|
+
io.each do |line|
|
|
21
|
+
(UserConfig.get(:verbose) || Time.now - start_time > FORCE_OUTPUT_AFTER) && puts(line)
|
|
22
|
+
stdout << line
|
|
23
|
+
end
|
|
24
|
+
ensure
|
|
25
|
+
_, status = Process.wait2(io.pid)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
new(stdout, status)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.silent_run!(command)
|
|
32
|
+
return run!(command) if UserConfig.get(:verbose)
|
|
33
|
+
|
|
34
|
+
run!("#{command} 2>&1")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initialize(stdout, status)
|
|
38
|
+
@stdout = stdout
|
|
39
|
+
@status = status
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def success?
|
|
43
|
+
@status.success?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def text_output
|
|
47
|
+
@stdout.to_s.strip
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
module Config
|
|
5
|
+
CONFIG_DIR = ".gotsha"
|
|
6
|
+
CONFIG_FILE = File.join(CONFIG_DIR, "config.toml")
|
|
7
|
+
CONFIG_TEMPLATE_PATH = File.expand_path("templates/config.toml", __dir__)
|
|
8
|
+
GH_CONFIG_FILE = File.join(".github", "workflows", "gotsha.yml")
|
|
9
|
+
GH_CONFIG_TEMPLATE_PATH = File.expand_path("templates/github_action_example.yml", __dir__)
|
|
10
|
+
HOOKS_TEMPLATES_DIR = File.expand_path("templates", __dir__)
|
|
11
|
+
HOOKS_DIR = File.join(CONFIG_DIR, "hooks")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Gotsha
|
|
3
|
+
#
|
|
4
|
+
# Start by removing the `autogenerated` line below. While doing that,
|
|
5
|
+
# please, check the rest of this file to configure Gotsha as you need
|
|
6
|
+
#
|
|
7
|
+
autogenerated = true # Remove this line to start using Gotsha
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# Test commands
|
|
11
|
+
#
|
|
12
|
+
# This is where you define what tests to run and how.
|
|
13
|
+
# Multiple (comma-separated) commands supported, see the
|
|
14
|
+
# commented examples:
|
|
15
|
+
#
|
|
16
|
+
commands = [
|
|
17
|
+
# "bundle exec rubocop",
|
|
18
|
+
# "rspec --order rand",
|
|
19
|
+
# "docker exec -it great-app rspec"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# Git hooks config
|
|
23
|
+
#
|
|
24
|
+
# Set when Gotsha should run your tests automatically. Every
|
|
25
|
+
# test run stores the test results to the commit.
|
|
26
|
+
#
|
|
27
|
+
# If you decide to not use any hooks,
|
|
28
|
+
# you can create sign-off commit manually by this command:
|
|
29
|
+
#
|
|
30
|
+
# ```
|
|
31
|
+
# bundle exec gotsha commmit
|
|
32
|
+
# ```
|
|
33
|
+
#
|
|
34
|
+
post_commit_tests = false # run tests for every commit
|
|
35
|
+
pre_push_tests = true # run tests on every push
|
|
36
|
+
interrupt_push_on_tests_failure = false # prohibit pushing when tests fail
|
|
37
|
+
|
|
38
|
+
# Print out some debug details
|
|
39
|
+
verbose = false
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
[ -f .gotsha/config.toml ] || exit 0
|
|
5
|
+
|
|
6
|
+
grep -qE 'pre_push_tests\s*=\s*true' .gotsha/config.toml && (bundle exec gotsha status || bundle exec gotsha test)
|
|
7
|
+
|
|
8
|
+
push_gotsha_notes() {
|
|
9
|
+
# if already done before, let's only push the notes and exit
|
|
10
|
+
git push --no-verify origin refs/notes/gotsha:refs/notes/gotsha && return 0
|
|
11
|
+
|
|
12
|
+
# if pushing above failed, it means we need to do some one-time Git setup
|
|
13
|
+
git fetch origin 'refs/notes/gotsha:refs/notes/gotsha-remote'
|
|
14
|
+
git notes --ref=gotsha merge -v refs/notes/gotsha-remote
|
|
15
|
+
git update-ref -d refs/notes/gotsha-remote
|
|
16
|
+
|
|
17
|
+
# ... and push again!
|
|
18
|
+
git push --no-verify origin refs/notes/gotsha:refs/notes/gotsha
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
push_gotsha_notes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Gotsha
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
verify:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
with:
|
|
12
|
+
fetch-depth: 0
|
|
13
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
14
|
+
|
|
15
|
+
- name: Fetch Gotsha notes
|
|
16
|
+
run: git fetch origin 'refs/notes/gotsha:refs/notes/gotsha'
|
|
17
|
+
|
|
18
|
+
- name: Show tests output
|
|
19
|
+
run: |
|
|
20
|
+
SHA="${{ github.event.pull_request.head.sha }}"
|
|
21
|
+
if git --no-pager notes --ref=gotsha show "$SHA" >/dev/null 2>&1; then
|
|
22
|
+
git --no-pager notes --ref=gotsha show "$SHA"
|
|
23
|
+
else
|
|
24
|
+
echo "No gotsha note found for $SHA"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
- name: Verify
|
|
28
|
+
run: |
|
|
29
|
+
SHA="${{ github.event.pull_request.head.sha }}"
|
|
30
|
+
NOTE="$(git --no-pager notes --ref=gotsha show "$SHA" 2>/dev/null || true)"
|
|
31
|
+
|
|
32
|
+
if [ -z "$NOTE" ]; then
|
|
33
|
+
echo "::error ::Gotsha: not verified yet. Run 'gotsha commit' to verify."
|
|
34
|
+
exit 1
|
|
35
|
+
elif [[ "$NOTE" == Tests\ failed:* ]]; then
|
|
36
|
+
git --no-pager notes --ref=gotsha show "$SHA"
|
|
37
|
+
echo "::error ::Gotsha: Tests failed"
|
|
38
|
+
exit 1
|
|
39
|
+
elif [[ "$NOTE" == Tests\ passed:* ]]; then
|
|
40
|
+
echo "✓ Gotsha: tests passed"
|
|
41
|
+
else
|
|
42
|
+
echo "::error ::Unrecognized Gotsha note format for $SHA"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gotsha
|
|
4
|
+
class UserConfig
|
|
5
|
+
def self.get(key)
|
|
6
|
+
config = TomlRB.load_file(Config::CONFIG_FILE).transform_keys(&:to_sym)
|
|
7
|
+
|
|
8
|
+
ENV["GOTSHA_#{key.to_s.upcase}"] || # this allows changing config via ENV vars
|
|
9
|
+
config[key]
|
|
10
|
+
rescue Errno::ENOENT
|
|
11
|
+
nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.blank?
|
|
15
|
+
TomlRB.load_file(Config::CONFIG_FILE).empty?
|
|
16
|
+
rescue Errno::ENOENT
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/gotsha.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "toml-rb"
|
|
5
|
+
|
|
6
|
+
require_relative "gotsha/action_dispatcher"
|
|
7
|
+
require_relative "gotsha/actions/commit"
|
|
8
|
+
require_relative "gotsha/actions/help"
|
|
9
|
+
require_relative "gotsha/actions/init"
|
|
10
|
+
require_relative "gotsha/actions/show"
|
|
11
|
+
require_relative "gotsha/actions/test"
|
|
12
|
+
require_relative "gotsha/actions/uninstall"
|
|
13
|
+
require_relative "gotsha/actions/status"
|
|
14
|
+
require_relative "gotsha/bash_command"
|
|
15
|
+
require_relative "gotsha/config"
|
|
16
|
+
require_relative "gotsha/errors"
|
|
17
|
+
require_relative "gotsha/user_config"
|
|
18
|
+
require_relative "gotsha/version"
|
|
19
|
+
|
|
20
|
+
module Gotsha
|
|
21
|
+
include Config
|
|
22
|
+
include Errors
|
|
23
|
+
|
|
24
|
+
# Main entry-point: `Gotsha::ActionDispatcher.call(action_name)`
|
|
25
|
+
end
|
data/sig/gotsha.rbs
ADDED
data/web/favicon.ico
ADDED
|
Binary file
|
data/web/index.html
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<link rel="icon" href="favicon.ico" />
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>Gotsha — your local CI</title>
|
|
8
|
+
<meta name="description" content="Gotsha — sign off your commits locally by running tests & linters and proving it in your PR." />
|
|
9
|
+
<style>
|
|
10
|
+
:root{
|
|
11
|
+
--bg:#0D1117; /* GitHub dark */
|
|
12
|
+
--text:#E6EDF3; /* subtle off-white */
|
|
13
|
+
--muted:#8B949E;
|
|
14
|
+
--accent:#00FF66; /* terminal green */
|
|
15
|
+
--card:#0F1320;
|
|
16
|
+
--border:#1F2430;
|
|
17
|
+
}
|
|
18
|
+
*{box-sizing:border-box}
|
|
19
|
+
html,body{height:100%}
|
|
20
|
+
body{
|
|
21
|
+
margin:0;
|
|
22
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace;
|
|
23
|
+
background: radial-gradient(1200px 600px at 50% -10%, #101622 0%, var(--bg) 50%);
|
|
24
|
+
color:var(--text);
|
|
25
|
+
line-height:1.6;
|
|
26
|
+
}
|
|
27
|
+
.container{max-width:980px;margin:0 auto;padding:32px}
|
|
28
|
+
header{display:flex;align-items:center;gap:18px;margin-top:32px}
|
|
29
|
+
.logo{width:260px; max-width: 80vw;}
|
|
30
|
+
.title{font-size:clamp(22px, 4vw, 28px);letter-spacing:.4px}
|
|
31
|
+
main{margin-top:36px}
|
|
32
|
+
.hero{
|
|
33
|
+
display:grid;
|
|
34
|
+
grid-template-columns: 1fr;
|
|
35
|
+
gap:22px;
|
|
36
|
+
background:linear-gradient(180deg, var(--card), rgba(15,19,32,.6));
|
|
37
|
+
border:1px solid var(--border);
|
|
38
|
+
border-radius:18px;
|
|
39
|
+
padding:28px;
|
|
40
|
+
box-shadow: 0 10px 30px rgba(0,0,0,.35);
|
|
41
|
+
}
|
|
42
|
+
.hero h1{
|
|
43
|
+
margin:0;
|
|
44
|
+
font-size:clamp(28px, 6vw, 42px);
|
|
45
|
+
line-height:1.15;
|
|
46
|
+
}
|
|
47
|
+
.hero p{margin:0;color:var(--muted);font-size:clamp(14px, 2.5vw, 18px)}
|
|
48
|
+
.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:10px}
|
|
49
|
+
.btn{
|
|
50
|
+
appearance:none;border:1px solid var(--border);
|
|
51
|
+
background:#0b111b;color:var(--text);
|
|
52
|
+
padding:10px 14px;border-radius:10px;text-decoration:none;
|
|
53
|
+
font-weight:600;
|
|
54
|
+
}
|
|
55
|
+
.btn.primary{border-color:color-mix(in srgb, var(--accent) 60%, #000);
|
|
56
|
+
background:linear-gradient(180deg, color-mix(in srgb, var(--accent) 18%, #000) , #0b111b);
|
|
57
|
+
color:var(--accent);
|
|
58
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 35%, #000);
|
|
59
|
+
}
|
|
60
|
+
.grid{display:grid;grid-template-columns:1fr;gap:14px;margin-top:20px}
|
|
61
|
+
.card{
|
|
62
|
+
border:1px solid var(--border);border-radius:14px;padding:16px;background:#0b111b75
|
|
63
|
+
}
|
|
64
|
+
.card h3{margin:0 0 6px 0;font-size:18px}
|
|
65
|
+
.foot{margin-top:24px;color:var(--muted);font-size:13px}
|
|
66
|
+
/* subtle animations */
|
|
67
|
+
@keyframes glow { from { filter: drop-shadow(0 0 0 rgba(0,255,102,0)); }
|
|
68
|
+
to { filter: drop-shadow(0 0 6px rgba(0,255,102,.55)); } }
|
|
69
|
+
.logo .node{animation: glow 1.6s ease-in-out infinite alternate}
|
|
70
|
+
@media (prefers-reduced-motion: reduce){ .logo .node{animation:none} }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<div class="container">
|
|
75
|
+
<header>
|
|
76
|
+
<svg class="logo" viewBox="0 0 820 210" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Gotsha logo">
|
|
77
|
+
<defs>
|
|
78
|
+
<style>
|
|
79
|
+
.w { fill: #E6EDF3; }
|
|
80
|
+
.g { fill: #00FF66; }
|
|
81
|
+
.stroke { stroke: #00FF66; stroke-width: 20; stroke-linecap: round; stroke-linejoin: round; fill: none; }
|
|
82
|
+
</style>
|
|
83
|
+
</defs>
|
|
84
|
+
<!-- Wordmark: Got + SHA -->
|
|
85
|
+
<g transform="translate(120,148)">
|
|
86
|
+
<!-- 'Got' -->
|
|
87
|
+
<text class="w" x="0" y="0" font-size="112" font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'DejaVu Sans Mono', 'Courier New', monospace">Got</text>
|
|
88
|
+
<!-- 'SHA' -->
|
|
89
|
+
<text class="g" x="215" y="0" font-size="112" font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'DejaVu Sans Mono', 'Courier New', monospace">SHA</text>
|
|
90
|
+
</g>
|
|
91
|
+
<!-- Checkmark integrated near 'SHA' -->
|
|
92
|
+
<path class="stroke" d="M660,120 l35,35 70,-80"/>
|
|
93
|
+
</svg>
|
|
94
|
+
<div class="title">Your local CI</div>
|
|
95
|
+
</header>
|
|
96
|
+
|
|
97
|
+
<main>
|
|
98
|
+
<section class="hero" aria-labelledby="hero-title">
|
|
99
|
+
<h1 id="hero-title">Sign-off your commits locally — and let others see it.</h1>
|
|
100
|
+
<p>Run tests & linters on your machine, store results as Git notes, and let your PR show your tests output. Quickly and automatically.</p>
|
|
101
|
+
<div class="actions">
|
|
102
|
+
<a class="btn primary" href="https://github.com/melounvitek/gotsha" target="_blank">View more details on Github</a>
|
|
103
|
+
<a class="btn secondary" href="https://github.com/melounvitek/gotsha/pull/35" target="_blank">Demo pull request</a>
|
|
104
|
+
</div>
|
|
105
|
+
</section>
|
|
106
|
+
|
|
107
|
+
<section class="grid">
|
|
108
|
+
<article class="card">
|
|
109
|
+
<h3>1) Runs locally</h3>
|
|
110
|
+
<p>Gotsha runs the project test suite on your machine. This can happen automatically (for every Git commit or push), or manually; right before you ask for review. You can easily configure it whatever way suits your project best.</p>
|
|
111
|
+
</article>
|
|
112
|
+
<article class="card">
|
|
113
|
+
<h3>2) Stores test results for commit SHA</h3>
|
|
114
|
+
<p>Test results from every run are attached as <em>Git notes</em> for the recent commit SHA. (Got-SHA... you get it, right?)</p>
|
|
115
|
+
</article>
|
|
116
|
+
<article class="card">
|
|
117
|
+
<h3>3) Displays results in your GitHub PR</h3>
|
|
118
|
+
<p>Push as usual — the note follows your commit, and tiny Github Action instantly verifies it
|
|
119
|
+
and makes the tests result visible to reviewers. <a target="_blank" href="https://gotsha.org/failed_build.png">Check it out</a>; it even persist all the colors!</p>
|
|
120
|
+
</article>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<!--
|
|
124
|
+
<section class="card" id="install" aria-labelledby="install-title">
|
|
125
|
+
<h3 id="install-title">Install</h3>
|
|
126
|
+
<p>Install</p>
|
|
127
|
+
<pre><code># RubyGems
|
|
128
|
+
gem install gotsha
|
|
129
|
+
|
|
130
|
+
# Or add to your Gemfile
|
|
131
|
+
gem 'gotsha'</code></pre>
|
|
132
|
+
</section>
|
|
133
|
+
-->
|
|
134
|
+
|
|
135
|
+
<p class="foot">
|
|
136
|
+
© <span id="y"></span> <a target="_blank" href="https://x.com/melounvitek">Vítek Meloun</a> (<a target="_blank" href="mailto:vitek@meloun.info">vitek@meloun.info</a>)
|
|
137
|
+
</p>
|
|
138
|
+
</main>
|
|
139
|
+
</div>
|
|
140
|
+
<script>document.getElementById('y').textContent = new Date().getFullYear();</script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>
|
data/web/logo.png
ADDED
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gotsha
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vitek Meloun
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: toml-rb
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
26
|
+
email:
|
|
27
|
+
- vitek@meloun.info
|
|
28
|
+
executables:
|
|
29
|
+
- gotsha
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- ".gotsha/config.toml"
|
|
34
|
+
- ".gotsha/github_action_example.yml"
|
|
35
|
+
- ".gotsha/hooks/post-commit"
|
|
36
|
+
- ".gotsha/hooks/pre-push"
|
|
37
|
+
- ".rspec"
|
|
38
|
+
- ".rubocop.yml"
|
|
39
|
+
- CHANGELOG.md
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- Rakefile
|
|
43
|
+
- exe/gotsha
|
|
44
|
+
- lib/gotsha.rb
|
|
45
|
+
- lib/gotsha/action_dispatcher.rb
|
|
46
|
+
- lib/gotsha/actions/commit.rb
|
|
47
|
+
- lib/gotsha/actions/help.rb
|
|
48
|
+
- lib/gotsha/actions/init.rb
|
|
49
|
+
- lib/gotsha/actions/show.rb
|
|
50
|
+
- lib/gotsha/actions/status.rb
|
|
51
|
+
- lib/gotsha/actions/test.rb
|
|
52
|
+
- lib/gotsha/actions/uninstall.rb
|
|
53
|
+
- lib/gotsha/bash_command.rb
|
|
54
|
+
- lib/gotsha/config.rb
|
|
55
|
+
- lib/gotsha/errors.rb
|
|
56
|
+
- lib/gotsha/templates/config.toml
|
|
57
|
+
- lib/gotsha/templates/git_hooks/post-commit
|
|
58
|
+
- lib/gotsha/templates/git_hooks/pre-push
|
|
59
|
+
- lib/gotsha/templates/github_action_example.yml
|
|
60
|
+
- lib/gotsha/user_config.rb
|
|
61
|
+
- lib/gotsha/version.rb
|
|
62
|
+
- sig/gotsha.rbs
|
|
63
|
+
- web/favicon.ico
|
|
64
|
+
- web/index.html
|
|
65
|
+
- web/logo.png
|
|
66
|
+
homepage: https://www.gotsha.org/
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
homepage_uri: https://www.gotsha.org/
|
|
71
|
+
source_code_uri: https://github.com/melounvitek/gotsha
|
|
72
|
+
changelog_uri: https://github.com/melounvitek/gotsha
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '2.6'
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 3.6.9
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: 'Gotsha: your local CI'
|
|
90
|
+
test_files: []
|