Compare commits

...

3 Commits

Author SHA1 Message Date
burkkyy 3468df3782 removing unessesary files
Build and Push Docker Image / build (push) Successful in 1m40s
2026-07-14 23:22:44 -07:00
burkkyy 4a675f7da4 rewrote dev command to python, and added prod 2026-07-14 23:21:24 -07:00
burkkyy 5b31972b0f adding in production compose file 2026-07-14 23:08:03 -07:00
5 changed files with 297 additions and 277 deletions
-2
View File
@@ -1,2 +0,0 @@
nodejs 20.10.0
ruby 3.2.2
+2 -33
View File
@@ -16,44 +16,13 @@ Note that the `dev` command uses the `db` service, and so only has access to fol
## Set up `dev` command ## Set up `dev` command
The `dev` command vastly simplifies development using docker compose. It only requires `ruby`; however, `direnv` and `asdf` will make it easier to use. The `dev` command vastly simplifies development using docker compose. It only requires `python`; however, `direnv` and `asdf` will make it easier to use.
It's simply a wrapper around docker compose with the ability to quickly add custom helpers. It's simply a wrapper around docker compose with the ability to quickly add custom helpers.
All commands are just strings joined together, so it's easy to add new commmands. `dev` prints out each command that it runs, so that you can run the command manually to debug it, or just so you learn some docker compose syntax as you go. All commands are just strings joined together, so it's easy to add new commmands. `dev` prints out each command that it runs, so that you can run the command manually to debug it, or just so you learn some docker compose syntax as you go.
1. (optional) Install `asdf` as seen in <https://asdf-vm.com/guide/getting-started.html>. 1. (optional) Install [direnv](https://direnv.net/) and create an `.envrc` with
e.g. for Linux
```bash
apt install curl git
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.12.0
echo '
# asdf
. "$HOME/.asdf/asdf.sh"
. "$HOME/.asdf/completions/asdf.bash"
' >> ~/.bashrc
```
2. Install `ruby` via `asdf` as seen here <https://github.com/asdf-vm/asdf-ruby>, or using whatever custom Ruby install method works for your platform.
e.g. for Linux
```bash
asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git
# install version from .tool-versions file
asdf install ruby
asdf reshim ruby
```
You will now be able to run the `./bin/dev` command.
3. (optional) Install [direnv](https://direnv.net/) and create an `.envrc` with
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
+193 -235
View File
@@ -1,295 +1,253 @@
#!/usr/bin/env ruby #!/usr/bin/env python3
import os
import re
import subprocess
import sys
class DevHelper
# Support dashes in command names # Support dashes in command names
COMMAND_TO_METHOD = { COMMAND_TO_METHOD = {
"ts-node" => :ts_node, "ts-node": "ts_node",
"check-types" => :check_types, "check-types": "check_types",
"bash-completions" => :bash_completions, "bash-completions": "bash_completions",
"plantuml-to-png" => :plantuml_to_png, "plantuml-to-png": "plantuml_to_png",
} }
METHOD_TO_COMMAND = COMMAND_TO_METHOD.invert METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}
REPLACE_PROCESS = "replace_process"
WAIT_FOR_PROCESS = "wait_for_process" WAIT_FOR_PROCESS = "wait_for_process"
# External Interface
def self.call(*args) class DevHelper:
new.call(*args) def __init__(self):
end self.last_exit_status = 0
# Core logic # Core logic
def call(*args, **kwargs) def call(self, *args, **kwargs):
command = args[0] if not args:
method = COMMAND_TO_METHOD.fetch(command, command) return self.compose(*args, **kwargs)
if args.length.positive? && respond_to?(method)
public_send(method, *args.drop(1), **kwargs)
else
compose(*args, **kwargs)
end
end
def compose(*args, **kwargs) command, rest = args[0], args[1:]
command = compose_command(*args, **kwargs) method_name = COMMAND_TO_METHOD.get(command, command)
puts "Running: #{command}" unless kwargs[:slient] method = getattr(self, method_name, None)
if callable(method) and not method_name.startswith("_") and method_name != "call":
return method(*rest, **kwargs)
return self.compose(*args, **kwargs)
case kwargs[:execution_mode] def compose(self, *args, **kwargs):
when WAIT_FOR_PROCESS command = self._compose_command(*args, **kwargs)
wait_for_process_with_logging(command) if not kwargs.get("silent"):
else print(f"Running: {command}")
exec(command)
end if kwargs.get("execution_mode") == WAIT_FOR_PROCESS:
end self._wait_for_process_with_logging(command)
else:
os.execv("/bin/sh", ["/bin/sh", "-c", command])
# Primary command wrappers # Primary command wrappers
def build(*args, **kwargs) def build(self, *args, **kwargs):
compose(%w[build], *args, **kwargs) self.compose("build", *args, **kwargs)
end
def compile(*args, **kwargs) def compile(self, *args, **kwargs):
run(*%w[api npm run build], execution_mode: WAIT_FOR_PROCESS) self.run("api", "npm", "run", "build", execution_mode=WAIT_FOR_PROCESS)
exit($?.exitstatus) unless $?.success? if self.last_exit_status != 0:
run(*%w[web npm run build]) sys.exit(self.last_exit_status)
end self.run("web", "npm", "run", "build")
def up(*args, **kwargs) def up(self, *args, **kwargs):
compose(*%w[up --remove-orphans], *args, **kwargs) self.compose("up", "--remove-orphans", *args, **kwargs)
end
def down(*args, **kwargs) def down(self, *args, **kwargs):
compose(*%w[down --remove-orphans], *args, **kwargs) self.compose("down", "--remove-orphans", *args, **kwargs)
end
def logs(*args, **kwargs) def logs(self, *args, **kwargs):
compose(*%w[logs -f], *args, **kwargs) self.compose("logs", "-f", *args, **kwargs)
end
def run(*args, **kwargs) def run(self, *args, **kwargs):
compose(*%w[run --rm], *args, **kwargs) self.compose("run", "--rm", *args, **kwargs)
end
def ps(*args, **kwargs) def ps(self, *args, **kwargs):
compose(*%w[ps], *args, **kwargs) self.compose("ps", *args, **kwargs)
end
# Custom helpers # Custom helpers
def api(*args, **kwargs) def api(self, *args, **kwargs):
run(*%w[api], *args, **kwargs) self.run("api", *args, **kwargs)
end
def web(*args, **kwargs) def web(self, *args, **kwargs):
run(*%w[web], *args, **kwargs) self.run("web", *args, **kwargs)
end
def check_types(*args, **kwargs) def check_types(self, *args, **kwargs):
run(*%w[api npm run check-types], *args, **kwargs) self.run("api", "npm", "run", "check-types", *args, **kwargs)
end
def test(*args, **kwargs) def test(self, *args, **kwargs):
service = args[0] service = args[0] if args else None
if service == "api" if service == "api":
test_api(*args.drop(1), **kwargs) self.test_api(*args[1:], **kwargs)
elsif service == "web" elif service == "web":
test_web(*args.drop(1), **kwargs) self.test_web(*args[1:], **kwargs)
else else:
test_api(*args, **kwargs) self.test_api(*args, **kwargs)
end
end
def test_api(*args, **kwargs) def test_api(self, *args, **kwargs):
reformat_project_relative_path_filter_for_vitest!(args, "api/") args = self._reformat_project_relative_path_filter_for_vitest(list(args), "api/")
run(*%w[test_api npm run test], *args, **kwargs) self.run("test_api", "npm", "run", "test", *args, **kwargs)
end
def test_web(*args, **kwargs) def test_web(self, *args, **kwargs):
reformat_project_relative_path_filter_for_vitest!(args, "web/") args = self._reformat_project_relative_path_filter_for_vitest(list(args), "web/")
run(*%w[test_web npm run test], *args, **kwargs) self.run("test_web", "npm", "run", "test", *args, **kwargs)
end
def sqlcmd(*args, **kwargs) def sqlcmd(self, *args, **kwargs):
db_host = ENV.fetch('DB_HOST', 'localhost') db_host = os.environ.get("DB_HOST", "localhost")
db_user = ENV.fetch('DB_USER', 'sa') db_user = os.environ.get("DB_USER", "sa")
db_pass = ENV.fetch('DB_PASS', '1m5ecure!') db_pass = os.environ.get("DB_PASS", "1m5ecure!")
db_name = ENV.fetch('DB_NAME', 'YHSI') db_name = os.environ.get("DB_NAME", "YHSI")
compose( self.compose(
*%w[exec db /opt/mssql-tools/bin/sqlcmd], "exec",
*%W[-U #{db_user}], "db",
*%W[-P #{db_pass}], "/opt/mssql-tools/bin/sqlcmd",
*%W[-H #{db_host}], "-U",
*%W[-d #{db_name}], db_user,
'-I', # enable quoted identifiers, e.g. "table"."column" "-P",
db_pass,
"-H",
db_host,
"-d",
db_name,
"-I", # enable quoted identifiers, e.g. "table"."column"
*args, *args,
**kwargs **kwargs,
) )
end
def db(*args, **kwargs) def db(self, *args, **kwargs):
compose(*%w[exec db], *args, **kwargs) self.compose("exec", "db", *args, **kwargs)
end
def debug def debug(self, *args, **kwargs):
api_container_id = container_id("api") container_id = self._container_id("api")
puts "Waiting for breakpoint to trigger..." print("Waiting for breakpoint to trigger...")
puts "'ctrl-c' to exit." print("'ctrl-c' to exit.")
command = "docker attach --detach-keys ctrl-c #{api_container_id}" command = f"docker attach --detach-keys ctrl-c {container_id}"
puts "Running: #{command}" print(f"Running: {command}")
exec(command) os.execv("/bin/sh", ["/bin/sh", "-c", command])
exit 0
end
def npm(*args, **kwargs) def npm(self, *args, **kwargs):
run(*%w[api npm], *args, **kwargs) self.run("api", "npm", *args, **kwargs)
end
def ts_node(*args, **kwargs) def ts_node(self, *args, **kwargs):
run(*%w[api npm run ts-node], *args, **kwargs) self.run("api", "npm", "run", "ts-node", *args, **kwargs)
end
def knex(*args, **kwargs) def knex(self, *args, **kwargs):
if RUBY_PLATFORM =~ /linux/ if sys.platform.startswith("linux"):
run(*%w[api npm run knex], *args, execution_mode: WAIT_FOR_PROCESS, **kwargs) self.run("api", "npm", "run", "knex", *args, execution_mode=WAIT_FOR_PROCESS, **kwargs)
file_or_directory = "#{project_root}/api/src/db/migrations" file_or_directory = os.path.join(self._project_root(), "api/src/db/migrations")
exit(0) unless take_over_needed?(file_or_directory) if not self._take_over_needed(file_or_directory):
sys.exit(0)
ownit file_or_directory self.ownit(file_or_directory)
else else:
run(*%w[api npm run knex], *args, **kwargs) self.run("api", "npm", "run", "knex", *args, **kwargs)
end
end
def migrate(*args, **kwargs) def migrate(self, *args, **kwargs):
action = args[0] action = args[0] if args else None
knex("migrate:#{action}", *args.drop(1), **kwargs) self.knex(f"migrate:{action}", *args[1:], **kwargs)
end
def seed(*args, **kwargs) def seed(self, *args, **kwargs):
action = args[0] action = args[0] if args else None
knex("seed:#{action}", *args.drop(1), **kwargs) self.knex(f"seed:{action}", *args[1:], **kwargs)
end
def ownit(*args, **kwargs) def ownit(self, *args, **kwargs):
file_or_directory = args[0] file_or_directory = args[0] if args else None
raise ScriptError, "Must provide a file or directory path." if file_or_directory.nil? if file_or_directory is None:
raise ValueError("Must provide a file or directory path.")
if RUBY_PLATFORM =~ /linux/ if sys.platform.startswith("linux"):
puts "Take ownership of the file or directory? #{file_or_directory}" print(f"Take ownership of the file or directory? {file_or_directory}")
exec("sudo chown -R #{user_id}:#{group_id} #{file_or_directory}") command = f"sudo chown -R {self._user_id()}:{self._group_id()} {file_or_directory}"
else os.execv("/bin/sh", ["/bin/sh", "-c", command])
raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" else:
end raise NotImplementedError(f"Not implemented for platform {sys.platform}")
end
def plantuml_to_png(*args, **kwargs) def plantuml_to_png(self, *args, **kwargs):
file_path = args.pop args = list(args)
raise ScriptError, "Must provide a file path." if file_path.nil? if not args:
raise ValueError("Must provide a file path.")
file_path = args.pop()
png_path = file_path.gsub(/\.(wsd|pu|puml|plantuml|uml)$/, ".png") png_path = re.sub(r"\.(wsd|pu|puml|plantuml|uml)$", ".png", file_path)
command = <<~BASH command = f"curl {' '.join(args)} --data-binary @'{file_path}' http://localhost:9999/png > '{png_path}'"
curl #{args.join(" ")} \
--data-binary @'#{file_path}' \
http://localhost:9999/png > '#{png_path}'
BASH
puts "Running: #{command}" print(f"Running: {command}")
exec(command) os.execv("/bin/sh", ["/bin/sh", "-c", command])
end
def bash_completions def bash_completions(self, *args, **kwargs):
completions = completions = sorted(
public_methods(false) METHOD_TO_COMMAND.get(name, name)
.reject { |word| %i[call].include?(word) } for name in vars(DevHelper)
.map { |word| METHOD_TO_COMMAND.fetch(word, word) } if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
puts completions )
end print(" ".join(completions))
private # Private helpers
def _wait_for_process_with_logging(self, command):
process = subprocess.Popen(
["/bin/sh", "-c", f"{command} 2>&1"],
stdout=subprocess.PIPE,
text=True,
)
for line in process.stdout:
print(line, end="")
process.wait()
self.last_exit_status = process.returncode
def wait_for_process_with_logging(command) def _container_id(self, container_name, *args, **kwargs):
IO.popen("#{command} 2>&1") do |io| command = self._compose_command("ps", "-q", container_name, *args, **kwargs)
until io.eof? print(f"Running: {command}")
line = io.gets result = subprocess.run(["/bin/sh", "-c", command], stdout=subprocess.PIPE, text=True)
puts line container_id = result.stdout.strip()
end print(f"Container id is: {container_id}")
end return container_id
end
def container_id(container_name, *args, **kwargs) def _service_running(self, container_name):
command = compose_command(*%w[ps -q], container_name, *args, **kwargs) self.ps("-q", "--status=running", execution_mode=WAIT_FOR_PROCESS, silent=True)
puts "Running: #{command}" return self.last_exit_status == 0
id_of_container = `#{command}`.chomp
puts "Container id is: #{id_of_container}"
id_of_container
end
def service_running?(container_name) def _compose_command(self, *args, **kwargs):
ps(*%w[-q --status=running], execution_mode: WAIT_FOR_PROCESS, slient: true) != "" environment = kwargs.get("environment", "development")
end return f"cd {self._project_root()} && docker compose -f docker-compose.{environment}.yml {' '.join(args)}"
def compose_command(*args, **kwargs) def _project_root(self):
environment = kwargs.fetch(:environment, "development") return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
"cd #{project_root} && docker compose -f docker-compose.#{environment}.yml #{args.join(" ")}"
end
def project_root def _take_over_needed(self, file_or_directory):
@project_root ||= File.absolute_path("#{__dir__}/..") result = subprocess.run(
end ["/bin/sh", "-c", f"find {file_or_directory} -not -user {self._user_id()} -print -quit | grep -q ."],
)
return result.returncode == 0
def take_over_needed?(file_or_directory) def _user_id(self):
files_owned_by_others = if not sys.platform.startswith("linux"):
system("find #{file_or_directory} -not -user #{user_id} -print -quit | grep -q .") raise NotImplementedError(f"Not implemented for platform {sys.platform}")
files_owned_by_others return subprocess.run(["id", "-u"], stdout=subprocess.PIPE, text=True).stdout.strip()
end
def user_id def _group_id(self):
unless RUBY_PLATFORM =~ /linux/ if not sys.platform.startswith("linux"):
raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}" raise NotImplementedError(f"Not implemented for platform {sys.platform}")
end return subprocess.run(["id", "-g"], stdout=subprocess.PIPE, text=True).stdout.strip()
`id -u`.strip def _reformat_project_relative_path_filter_for_vitest(self, args, prefix):
end if args and args[0].startswith(prefix):
src_path_prefix = f"{prefix}src/"
def group_id if args[0].startswith(src_path_prefix):
unless RUBY_PLATFORM =~ /linux/
raise NotImplementedError, "Not implement for platform #{RUBY_PLATFORM}"
end
`id -g`.strip
end
def reformat_project_relative_path_filter_for_vitest!(args, prefix)
if args.length.positive? && args[0].start_with?(prefix)
src_path_prefix = "#{prefix}src/"
test_path_regex = Regexp.escape(prefix)
src_path_regex = Regexp.escape(src_path_prefix)
if args[0].start_with?(src_path_prefix)
# TODO: handle other file types # TODO: handle other file types
args[0] = args[0].gsub(/^#{src_path_regex}/, "tests/").gsub(/\.ts$/, ".test.ts") args[0] = re.sub(r"\.ts$", ".test.ts", re.sub(f"^{re.escape(src_path_prefix)}", "tests/", args[0]))
else else:
args[0] = args[0].gsub(/^#{test_path_regex}/, "") args[0] = re.sub(f"^{re.escape(prefix)}", "", args[0])
end
puts "Reformatted path filter from project relative to service relative for vitest." print("Reformatted path filter from project relative to service relative for vitest.")
end
end
end
# Only execute main function when file is executed return args
DevHelper.call(*ARGV) if $PROGRAM_NAME == __FILE__
## Dev completions
# https://iridakos.com/programming/2018/03/01/bash-programmable-completion-tutorial
# _dev_completions () {
# local dev_command_path="$(which dev)"
# local dev_function_names
# dev_function_names="$(ruby "$dev_command_path" bash_completions)"
# # COMP_WORDS: an array of all the words typed after the name of the program the compspec belongs to
# # COMP_CWORD: an index of the COMP_WORDS array pointing to the word the current cursor is at - in other words, the index of the word the cursor was when the tab key was pressed
# # COMP_LINE: the current command line
# COMPREPLY=($(compgen -W "$dev_function_names" "${COMP_WORDS[$COMP_CWORD]}"))
# }
# complete -F _dev_completions dev if __name__ == "__main__":
# complete -W "allow" direnv DevHelper().call(*sys.argv[1:])
Executable
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
# Support dashes in command names
COMMAND_TO_METHOD = {}
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}
WAIT_FOR_PROCESS = "wait_for_process"
class ProdHelper:
def __init__(self):
self.last_exit_status = 0
# Core logic
def call(self, *args, **kwargs):
if not args:
return self.compose(*args, **kwargs)
command, rest = args[0], args[1:]
method_name = COMMAND_TO_METHOD.get(command, command)
method = getattr(self, method_name, None)
if callable(method) and not method_name.startswith("_") and method_name != "call":
return method(*rest, **kwargs)
return self.compose(*args, **kwargs)
def compose(self, *args, **kwargs):
command = self._compose_command(*args, **kwargs)
if not kwargs.get("silent"):
print(f"Running: {command}")
if kwargs.get("execution_mode") == WAIT_FOR_PROCESS:
self._wait_for_process_with_logging(command)
else:
os.execv("/bin/sh", ["/bin/sh", "-c", command])
# Primary command wrappers
def up(self, *args, **kwargs):
self.compose("up", "--remove-orphans", *args, **kwargs)
def down(self, *args, **kwargs):
self.compose("down", "--remove-orphans", *args, **kwargs)
def logs(self, *args, **kwargs):
self.compose("logs", "-f", *args, **kwargs)
def ps(self, *args, **kwargs):
self.compose("ps", *args, **kwargs)
def bash_completions(self, *args, **kwargs):
completions = sorted(
METHOD_TO_COMMAND.get(name, name)
for name in vars(ProdHelper)
if not name.startswith("_") and name != "call" and callable(getattr(ProdHelper, name))
)
print(" ".join(completions))
# Private helpers
def _wait_for_process_with_logging(self, command):
process = subprocess.Popen(
["/bin/sh", "-c", f"{command} 2>&1"],
stdout=subprocess.PIPE,
text=True,
)
for line in process.stdout:
print(line, end="")
process.wait()
self.last_exit_status = process.returncode
def _compose_command(self, *args, **kwargs):
environment = kwargs.get("environment", "production")
return f"cd {self._project_root()} && docker compose -f docker-compose.{environment}.yml {' '.join(args)}"
def _project_root(self):
return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if __name__ == "__main__":
ProdHelper().call(*sys.argv[1:])
+13
View File
@@ -0,0 +1,13 @@
services:
app:
image: gitea.burke.host/burkkyy/calebburke.dev:latest
pull_policy: always
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
ports:
- "${HOST_PORT:-3000}:${HOST_PORT:-3000}"
volumes:
- ./.env:/home/node/app/.env.production