Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
7ffd897
automated ci fixes
Feb 18, 2026
ac3db78
testing
Feb 18, 2026
d88e8f6
pyc
Feb 18, 2026
b9c9e59
move fix mode selection to interactive Python menu, remove hardcoded …
Feb 18, 2026
dc4d5ce
move pip to python
Feb 18, 2026
3c36283
auto re-fetch and re-pip
Feb 18, 2026
607f3dc
ask for repo/branch interactively when no args provided
Feb 18, 2026
75b30f6
add session management, CI URL auto-extraction, and improved compare …
Feb 18, 2026
9fd56fe
fix removesuffix for Python 3.8 compatibility
Feb 18, 2026
01c919f
add interactive Claude session, setup script, and CI context for ci…
Feb 18, 2026
3cdb530
interactive help
Feb 18, 2026
79ce05b
add session resume, progress display, and incremental container setup
Feb 18, 2026
5733935
testing
Feb 18, 2026
e3abea0
error
Feb 18, 2026
d31a242
handle ctrl+c during reproduce to continue to claude phase
Feb 18, 2026
de88bbb
pass container name to reproduce instead of rename after
Feb 18, 2026
c9a2441
use --container-name from args for python-side container checks
Feb 18, 2026
82308c8
set IS_SANDBOX=1 to allow claude as root in container
Feb 18, 2026
ad61f30
add --verbose flag required for stream-json with -p
Feb 18, 2026
cdff64d
add ci_tool rearchitect design doc
Feb 18, 2026
f3e6b47
add ci_tool rearchitect implementation plan
Feb 18, 2026
6fc9b9b
remove unused rename_container from containers.py
Feb 18, 2026
08831fa
rewrite ci_reproduce.py with Python Docker orchestration
Feb 18, 2026
75f0112
fix spec review issues in ci_reproduce.py
Feb 18, 2026
5f26949
address code quality review feedback in ci_reproduce.py
Feb 18, 2026
0953553
refactor ci_fix.py: consolidated prompting and linear flow
Feb 18, 2026
a1128c2
adapt cli.py reproduce handler for new reproduce_ci interface
Feb 18, 2026
55d23ca
setup.sh: install ci_tool and hand off after setup
Feb 18, 2026
209cad6
lint: fix all pylint warnings across ci_tool modules
Feb 18, 2026
40d0c93
fix critical review issues: Python 3.8 compat, scripts branch, error …
Feb 18, 2026
2672141
scripts branch: env var override and fix default to existing branch
Feb 18, 2026
a805a9a
pass THIS_SCRIPT_BRANCH to ci_tool as CI_TOOL_SCRIPTS_BRANCH
Feb 18, 2026
37e798b
show final container name after session name prompt
Feb 18, 2026
334b42f
show er_ci_ prefix in session name prompt message
Feb 18, 2026
1b0e681
fix display_progress: force_terminal and remove Live context
Feb 18, 2026
d3de9e5
add tty flag to docker_exec, use for Claude streaming
Feb 18, 2026
e6a70b5
fix scripts branch: hardcode correct internal branch, remove bash pas…
Feb 18, 2026
96ee588
hide raw docker command, add event debug logging
Feb 18, 2026
97fd909
fix display to match Claude Code stream-json format
Feb 18, 2026
9e56f67
fix resume_claude: add IS_SANDBOX=1 for root containers
Feb 18, 2026
7216386
add session handoff doc and TODO.md for ci_tool
Feb 18, 2026
76f4c7c
lots of silent failures
Feb 18, 2026
177a366
gh auth
Feb 18, 2026
3c33375
reduce token usage: grep logs instead of reading whole files
Feb 18, 2026
637b6b4
add persistent learnings file across ci_tool sessions
Feb 18, 2026
21c9761
reduce test output verbosity for colcon test results
Feb 18, 2026
66f8ace
render claude output as rich markdown (tables, headers, code)
Feb 18, 2026
871a0e6
sandbox
Feb 18, 2026
1b8dcbd
fixes
Feb 18, 2026
40d5bfb
permissions issue in claude settigns
Feb 18, 2026
a5ea82d
Merge branch 'ERD-1633_reproduce_ci_locally' of https://github.com/Ex…
Feb 18, 2026
95244a6
Compact tool output with spinner in display_progress
Feb 18, 2026
4b26870
docs grammar
Feb 19, 2026
03ea8c2
add -h | --help, and update readme
Feb 19, 2026
94e671a
Merge branch 'ERD-1633_reproduce_ci_locally_tool' of https://github.c…
Feb 19, 2026
1a261ed
attempt to reduce token usage, untested
Feb 19, 2026
48cbc32
Selective verbose test output via .verbose_tests marker
tomqext Feb 25, 2026
6fd2462
Add TODOs for parallel CI analysis and branch extraction from URL
tomqext Feb 25, 2026
62ac833
Add TODO to simplify ci_tool main menu
tomqext Feb 25, 2026
6b0c4d9
Add design doc for CI analyse mode
tomqext Feb 25, 2026
05ea6d7
Update design and plan for CI analyse mode
tomqext Feb 25, 2026
3a766b9
Add TODO for Analyse CI mode, supersede parallel analysis item
tomqext Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.pyc
117 changes: 109 additions & 8 deletions .helper_bash_functions
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,37 @@ for d in sys.argv[1:]:
print(f" ... ({len(lines) - 20} more lines)")
PYEOF
}
_find_verbose_test_packages() {
local requested_packages="$1"
local verbose_packages=""
while IFS=$'\t' read -r name path _; do
if echo " ${requested_packages} " | grep -q " ${name} "; then
if [ -f "${path}/.verbose_tests" ]; then
verbose_packages="${verbose_packages} ${name}"
fi
fi
done < <(colcon list 2>/dev/null)
echo "${verbose_packages## }"
}
colcon_test_these_packages() { THIS_DIR=$(pwd)
cd ${CATKIN_WS_PATH}
colcon_build_no_deps $1
source install/setup.bash && \
colcon test --packages-select $1
source install/setup.bash
local verbose_packages
verbose_packages=$(_find_verbose_test_packages "$1")
if [ -n "${verbose_packages}" ]; then
echo -e "${Yellow}Packages with verbose tests: ${verbose_packages}${Color_Off}"
colcon test --packages-select $1 --packages-skip ${verbose_packages}
colcon test --packages-select ${verbose_packages} --event-handlers console_direct+
else
colcon test --packages-select $1
fi
for pkg in $1; do
if ! colcon test-result --test-result-base "build/$pkg/test_results"; then
_show_test_failures "build/$pkg/test_results"
fi
done
cd $THIS_DIR

}
colcon_test_this_package() { colcon_test_these_packages "$@"; } # Alias for backwards compatability
find_cmake_project_names_from_dir() { if [[ -z $1 ]]; then DIR_TO_SEARCH="."; else DIR_TO_SEARCH=$1; fi; wget -qO- https://raw.githubusercontent.com/Extend-Robotics/er_build_tools/refs/heads/main/bin/find_cmake_project_names.py | python3 - $DIR_TO_SEARCH; }
Expand Down Expand Up @@ -136,11 +155,35 @@ ps_aux() { ps aux | cgrep $1 | grep -v grep ; }
kill_any_process() { ps_aux_command $1; conf="$(confirm "kill these processes? [Y/n]")"; if [[ $conf == "y" ]]; then echo "killing..."; sudo kill -9 $(ps_aux $1 | awk {'print $2}'); sleep 1; echo "remaining: "; ps_aux_command $1 else echo "not killing"; fi ; }
docker_exec () { if [[ $(docker container ls -q | wc -l) -eq 1 ]]; then docker exec -it $(docker container ls -q) bash; else echo "wrong number of containers running"; fi; }
awk_line_length() { if [[ -z $2 ]]; then MAX_LINE_LENGTH=200; else MAX_LINE_LENGTH=$2; fi; cat $1 | awk 'length($0) < '"$MAX_LINE_LENGTH"''; }
update_helper_bash_functions() { if [ ! -f ~/.helper_bash_functions ]; then
echo -e "${Red}ERROR: Tried to replace this file but couldn't find it, something has gone wrong!${Color_Off}\n"
return
fi
wget -O ~/.helper_bash_functions ${THIS_SCRIPT_URL}
MERGE_VARS_URL="https://raw.githubusercontent.com/Extend-Robotics/er_build_tools/refs/heads/${THIS_SCRIPT_BRANCH}/bin/merge_helper_vars.py"

update_helper_bash_functions() {
if [ ! -f ~/.helper_bash_functions ]; then
echo -e "${Red}ERROR: Tried to replace this file but couldn't find it, something has gone wrong!${Color_Off}\n"
return 1
fi

local tmp_new
tmp_new=$(mktemp)
if ! wget -q -O "${tmp_new}" "${THIS_SCRIPT_URL}"; then
echo -e "${Red}Error: Failed to download updated helper_bash_functions${Color_Off}"
rm -f "${tmp_new}"
return 1
fi

local merge_script
merge_script=$(curl -fSL "${MERGE_VARS_URL}") || {
echo -e "${Red}Error: Failed to fetch merge_helper_vars.py${Color_Off}"
rm -f "${tmp_new}"
return 1
}

python3 <(echo "${merge_script}") ~/.helper_bash_functions "${tmp_new}"
cp "${tmp_new}" ~/.helper_bash_functions
rm -f "${tmp_new}"

echo ""
echo -e "${Green}helper_bash_functions updated. Run 'source ~/.helper_bash_functions' to reload.${Color_Off}"
}

# python linters
Expand Down Expand Up @@ -203,6 +246,64 @@ remove_ci_container() {
echo -e "${Green}Container '${container_name}' removed${Color_Off}"
}

# CI tool (Python CLI) - interactive CI reproduction + Claude-powered fix
CI_TOOL_CACHE_DIR="${HOME}/.ci_tool"
CI_TOOL_RAW_URL="https://raw.githubusercontent.com/Extend-Robotics/er_build_tools/refs/heads/${THIS_SCRIPT_BRANCH}/bin/ci_tool"
CI_TOOL_FILES="__init__.py __main__.py cli.py preflight.py containers.py ci_reproduce.py claude_setup.py ci_fix.py claude_session.py display_progress.py requirements.txt"
CI_TOOL_DATA_FILES="ci_context/CLAUDE.md"

install_ci_tool() {
echo -e "${Yellow}Installing ci_tool from er_build_tools (branch: ${THIS_SCRIPT_BRANCH})...${Color_Off}"
mkdir -p "${CI_TOOL_CACHE_DIR}/ci_tool"
local failed="false"
for file in ${CI_TOOL_FILES}; do
echo " Fetching ${file}..."
if ! curl -fSL "${CI_TOOL_RAW_URL}/${file}" -o "${CI_TOOL_CACHE_DIR}/ci_tool/${file}"; then
echo -e "${Red} Failed to fetch ${file}${Color_Off}"
failed="true"
fi
done
for file in ${CI_TOOL_DATA_FILES}; do
echo " Fetching ${file}..."
mkdir -p "${CI_TOOL_CACHE_DIR}/ci_tool/$(dirname "${file}")"
if ! curl -fSL "${CI_TOOL_RAW_URL}/${file}" -o "${CI_TOOL_CACHE_DIR}/ci_tool/${file}"; then
echo -e "${Red} Failed to fetch ${file}${Color_Off}"
failed="true"
fi
done
if [ "${failed}" = "true" ]; then
echo -e "${Red}Error: Failed to install ci_tool. Check network and branch '${THIS_SCRIPT_BRANCH}'.${Color_Off}"
return 1
fi
echo -e "${Green}ci_tool installed to ${CI_TOOL_CACHE_DIR}${Color_Off}"
}

update_ci_tool() { install_ci_tool; }

ci_tool() {
mkdir -p "${CI_TOOL_CACHE_DIR}/ci_tool"
local fetch_failed="false"
for file in ${CI_TOOL_FILES}; do
if ! curl -fsSL "${CI_TOOL_RAW_URL}/${file}" -o "${CI_TOOL_CACHE_DIR}/ci_tool/${file}" 2>/dev/null; then
fetch_failed="true"
fi
done
for file in ${CI_TOOL_DATA_FILES}; do
mkdir -p "${CI_TOOL_CACHE_DIR}/ci_tool/$(dirname "${file}")" 2>/dev/null
if ! curl -fsSL "${CI_TOOL_RAW_URL}/${file}" -o "${CI_TOOL_CACHE_DIR}/ci_tool/${file}" 2>/dev/null; then
fetch_failed="true"
fi
done
if [ "${fetch_failed}" = "true" ]; then
if [ ! -f "${CI_TOOL_CACHE_DIR}/ci_tool/__main__.py" ]; then
echo -e "${Red}Error: Failed to fetch ci_tool and no cached version available${Color_Off}"
return 1
fi
echo -e "${Yellow}Warning: Failed to fetch latest ci_tool, using cached version${Color_Off}"
fi
python3 "${CI_TOOL_CACHE_DIR}/ci_tool" "$@"
}
ci_fix() { ci_tool fix "$@"; }


er_python_linters_here() { local ret=0
Expand Down
71 changes: 71 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# er_build_tools

Public build tools and CI utilities for Extend Robotics ROS1 repositories. The main component is `ci_tool`, an interactive CLI that reproduces CI failures locally in Docker and uses Claude Code to fix them.

## Project Structure

```
bin/
ci_tool/ # Main Python package
__main__.py # Entry point (auto-installs missing deps)
cli.py # Menu dispatch router
ci_fix.py # Core workflow: reproduce -> Claude analysis -> fix -> shell
ci_reproduce.py # Docker container setup for CI reproduction
claude_setup.py # Install Claude + copy credentials/config into container
claude_session.py # Interactive Claude session launcher
containers.py # Docker lifecycle (create, exec, cp, remove)
preflight.py # Auth/setup validation (fail-fast)
display_progress.py # Stream-json output processor for Claude
ci_context/
CLAUDE.md # CI-specific instructions for Claude inside containers
setup.sh # User-facing setup script
reproduce_ci.sh # Public wrapper for CI reproduction
.helper_bash_functions # Sourced by users; provides colcon/rosdep helpers + ci_tool alias
pylintrc # Pylint config (strict: fail-under=10.0, max-line-length=140)
```

## Code Style

- Python 3.6+, `from __future__ import annotations` in all modules
- snake_case everywhere; PascalCase for classes only
- 4-space indentation, max 140 char line length
- Pylint must pass at 10.0 (`pylintrc` at repo root)
- Use `# pylint: disable=...` pragmas only when essential, with justification
- Interactive prompts via `InquirerPy`; terminal UI via `rich`

## Conventions

- **Fail fast**: `PreflightError` for expected failures, `RuntimeError` for unexpected. No silent defaults, no fallback behaviour.
- **Minimal diffs**: Only change what's requested. No cosmetic cleanups, no "while I'm here" changes.
- **Self-documenting code**: Verbose variable names. Comments only for maths or external doc links.
- **Subprocess calls**: Use `docker_exec()` / `run_command()` from `containers.py`. Pass `check=False` when non-zero is expected; `quiet=True` to suppress echo.
- **State files**: `/ros_ws/.ci_fix_state.json` inside containers (session_id, phase, attempt_count)
- **Learnings persistence**: `~/.ci_tool/learnings/{org}_{repo}.md` on host, `/ros_ws/.ci_learnings.md` in container

## Environment Variables

- `GH_TOKEN` or `ER_SETUP_TOKEN` — GitHub token (checked in preflight)
- `CI_TOOL_SCRIPTS_BRANCH` — branch of er_build_tools_internal to fetch scripts from
- `IS_SANDBOX=1` — injected into all docker exec calls for Claude

## Running

```bash
# From host
source ~/.helper_bash_functions
ci_tool # interactive menu
ci_fix # shortcut for ci_tool fix

# Lint
pylint --rcfile=pylintrc bin/ci_tool/
```

## Testing

No unit tests yet. Test manually by running `ci_tool` workflows end-to-end.

## Common Pitfalls

- Container Claude settings must use valid `defaultMode` values: `"acceptEdits"`, `"bypassPermissions"`, `"default"`, `"dontAsk"`, `"plan"`. The old `"dangerouslySkipPermissions"` is invalid.
- `docker_cp_to_container` requires the container to be running.
- Claude inside containers runs with `--dangerously-skip-permissions` flag (separate from the settings.json mode).
94 changes: 89 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,95 @@

Public build tools and utilities for Extend Robotics repositories.

## reproduce_ci.sh — Reproduce CI Locally
## Quick Setup

Install helper bash functions, set your GitHub token, and authenticate Claude:

```bash
bash <(curl -fsSL https://raw.githubusercontent.com/Extend-Robotics/er_build_tools/refs/heads/main/bin/setup.sh)
```

This installs `~/.helper_bash_functions` which provides build helpers, git aliases, and `ci_tool`.

## ci_tool: Fix CI Failures with Claude

`ci_tool` is an interactive CLI that reproduces CI failures locally in Docker and uses Claude Code to fix them.

### Prerequisites

- **Docker** installed and running
- **Claude Code** installed and authenticated: `npm install -g @anthropic-ai/claude-code && claude`
- **GitHub token** with `repo` scope: [create one](https://github.com/settings/tokens)

### Usage

```bash
source ~/.helper_bash_functions
ci_tool
```

This opens an interactive menu. You can also run subcommands directly — see `ci_tool --help`:

```
$ ci_tool --help
ci_tool — Fix CI failures with Claude

Usage: ci_tool [command]

Commands:
fix Fix CI failures with Claude
reproduce Reproduce CI environment in Docker
claude Interactive Claude session in container
shell Shell into an existing CI container
retest Re-run tests in a CI container
clean Remove CI containers

Shortcuts:
ci_fix Alias for 'ci_tool fix'

Run without arguments for interactive menu.
```

### Fix Workflow

1. Run `ci_fix`
2. Create a new session or reuse an existing container
3. Optionally paste a GitHub Actions URL to target a specific failure
4. ci_tool reproduces the CI environment in Docker
5. Claude analyses the test output and applies fixes
6. You're dropped into a shell to review changes, commit, and push

### Manual Setup

If you prefer not to use the setup script:

1. Download helper functions:
```bash
curl -fsSL https://raw.githubusercontent.com/Extend-Robotics/er_build_tools/refs/heads/main/.helper_bash_functions \
-o ~/.helper_bash_functions
```
2. Add your GitHub token to the top of `~/.helper_bash_functions`:
```bash
export GH_TOKEN="ghp_your_token_here"
```
3. Source in your shell:
```bash
echo 'source ~/.helper_bash_functions' >> ~/.bashrc
source ~/.helper_bash_functions
```
4. Install and authenticate Claude Code:
```bash
npm install -g @anthropic-ai/claude-code
claude
```

---

## reproduce_ci.sh: Reproduce CI Locally

When CI fails, debugging requires pushing commits and waiting for results. This script reproduces the exact CI environment locally in a persistent Docker container, so you can debug interactively.

It creates a Docker container using the same image as CI, clones your repo and its dependencies, builds everything, and optionally runs tests mirroring the steps in `setup_and_build_ros_ws.yml`.
It creates a Docker container using the same image as CI, clones your repo and its dependencies, builds everything, and optionally runs tests, mirroring the steps in `setup_and_build_ros_ws.yml`.

### Quick Start

Expand Down Expand Up @@ -98,8 +182,8 @@ docker rm -f er_ci_reproduced_testing_env

### Troubleshooting

**Container already exists** Remove it first: `docker rm -f er_ci_reproduced_testing_env`
**Container already exists**: Remove it first: `docker rm -f er_ci_reproduced_testing_env`

**404 when fetching scripts** Check that your `--gh-token` has access to `er_build_tools_internal`, and that the `--scripts-branch` exists.
**404 when fetching scripts**: Check that your `--gh-token` has access to `er_build_tools_internal`, and that the `--scripts-branch` exists.

**`DISPLAY` error with graphical forwarding** Either set `DISPLAY` (e.g. via X11 forwarding) or pass `--graphical false`.
**`DISPLAY` error with graphical forwarding**: Either set `DISPLAY` (e.g. via X11 forwarding) or pass `--graphical false`.
35 changes: 35 additions & 0 deletions bin/ci_tool/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# ci_tool TODO

## Features

- [ ] **Analyse CI mode**: Implement the plan in `docs/plans/2026-02-25-ci-analyse-mode-plan.md`. New "Analyse CI" menu item with two sub-modes: "Remote only (fast)" fetches GH Actions logs, filters with regex, diagnoses with Claude haiku on host — no Docker. "Remote + local reproduction" runs both in parallel with a Rich Live split-panel display, then offers to transition into fix mode. New files: `ci_analyse.py`, `ci_analyse_display.py`, `ci_log_filter.py`.
- [x] ~~**Parallel CI analysis during local reproduction**~~: Superseded by the Analyse CI mode above.

## UX

- [ ] **Simplify the main menu**: Too many top-level options (reproduce, fix, claude, shell, retest, clean, exit). Several overlap — e.g. "Reproduce CI" is already a step within "Fix CI with Claude", and "Claude session" / "Shell into container" / "Re-run tests" are all post-reproduce actions on an existing container. Consolidate into fewer choices and push the rest into sub-menus or contextual prompts.

## Bug Fixes

- [ ] If branch name is empty/blank, default to the repo's default branch instead of requiring input
- [ ] In "Reproduce CI (create container)" mode, extract the branch name from the GitHub Actions URL (like `extract_info_from_ci_url` already does in "Fix CI with Claude" mode) instead of requiring the user to enter it manually

## Done
- [x] ~~Render markdown in terminal~~ — display_progress.py now buffers text between tool calls and renders via `rich.markdown.Markdown` (tables, headers, code blocks, bold/italic)
- [x] ~~Empty workspace after reproduce~~ — `_docker_exec_workspace_setup()` distinguishes setup failures from test failures; `wstool scrape` fixed in internal repo
- [x] ~~Silent failures~~ — 21 issues audited and fixed across all modules
- [x] ~~resume_claude auth~~ — `IS_SANDBOX=1` passed via `docker exec -e` on all calls (`.bashrc` not sourced by non-interactive shells)
- [x] ~~gh CLI auth warning~~ — removed redundant `gh auth login` (GH_TOKEN env var handles auth)
- [x] ~~Token efficiency~~ — prompts updated to use grep instead of reading full logs
- [x] ~~Persistent learnings~~ — `~/.ci_tool/learnings/{org}_{repo}.md` persists between sessions

## Testing

- [ ] Add unit tests for each module, leveraging the clean separation of concerns:
- **ci_reproduce.py**: `_parse_repo_url` (edge cases: trailing slashes, `.git` suffix, invalid URLs, non-GitHub URLs), `_fetch_github_raw_file` (HTTP errors, timeouts, bad tokens), `prompt_for_reproduce_args` / `prompt_for_repo_and_branch` (input validation)
- **ci_fix.py**: `extract_run_id_from_url` (valid/invalid/malformed URLs), `extract_info_from_ci_url` (API errors, missing fields, bad URLs), `gather_session_info` (all input combinations: with/without CI URL, new/resume, empty fields)
- **containers.py**: `sanitize_container_name`, `container_exists`/`container_is_running` (mock docker calls)
- **preflight.py**: each check in isolation (mock docker/gh/claude)
- **cli.py**: `dispatch_subcommand` routing, `_handle_container_collision` (all three choices)
- [ ] Input validation / boundary tests: verify weird input combinations at module boundaries (e.g. gather_session_info output dict is always valid input for reproduce_ci, prompt outputs satisfy reproduce_ci preconditions)
- [ ] Integration-style tests: mock Docker/GitHub and run full flows end-to-end (new session, resume session, reproduce-only)
Empty file added bin/ci_tool/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions bin/ci_tool/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Entry point for: python3 -m ci_tool or python3 /path/to/ci_tool"""
import os
import subprocess
import sys

ci_tool_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(ci_tool_dir))

try:
from ci_tool.cli import main
except ImportError:
requirements_file = os.path.join(ci_tool_dir, "requirements.txt")
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "--user", "--quiet",
"-r", requirements_file]
)
from ci_tool.cli import main

if __name__ == "__main__":
main()
Loading
Loading