diff --git a/.bandit b/.bandit new file mode 100644 index 00000000..851fcbc7 --- /dev/null +++ b/.bandit @@ -0,0 +1,4 @@ +[bandit] +exclude_dirs = /build,/dist,/.eggs,*.egg-info +# B603: We use subprocess.run with hardcoded git commands, no untrusted input +skips = B603 diff --git a/.github/tests/README.md b/.github/tests/README.md new file mode 100644 index 00000000..f527ae6e --- /dev/null +++ b/.github/tests/README.md @@ -0,0 +1,67 @@ +# Integration Tests + +This directory contains integration tests for git-cd. + +## Test Repository + +The tests use the repository at https://github.com/gitcd-io/travis-gitcd as a test repository. + +## Running Tests Locally + +To run the tests locally: + +1. Clone the test repository: + ```bash + git clone https://github.com/gitcd-io/travis-gitcd.git ~/build/gitcd-io/travis-gitcd + ``` + +2. Configure git: + ```bash + cd ~/build/gitcd-io/travis-gitcd + git config user.email "test@example.com" + git config user.name "Test User" + ``` + +3. Set environment variable: + ```bash + export GITHUB_RUN_NUMBER=test-$(date +%s) + ``` + +4. Run individual test scripts: + ```bash + bash .github/tests/git-cd-version.sh + bash .github/tests/git-cd-init.sh + # etc... + ``` + +## GitHub Actions Setup + +For the GitHub Actions workflow to work, you need to configure a secret named `GH_TEST_REPO_TOKEN` with a Personal Access Token that has write access to the `gitcd-io/travis-gitcd` repository. + +To create a PAT: +1. Go to GitHub Settings → Developer settings → Personal access tokens +2. Generate new token (classic) +3. Select scopes: `repo` (Full control of private repositories) +4. Copy the token +5. Add it as a repository secret named `GH_TEST_REPO_TOKEN` + +## Test Scripts + +- `git-cd-version.sh` - Test version command +- `git-cd-upgrade.sh` - Test upgrade functionality +- `git-cd-init.sh` - Test initialization +- `git-cd-start.sh` - Test starting a feature branch +- `git-add-build.sh` - Helper to add build commits +- `git-cd-refresh.sh` - Test refresh functionality +- `git-cd-review.sh` - Test creating pull requests +- `git-cd-finish.sh` - Test finishing a feature +- `git-cd-release.sh` - Test release process +- `git-cd-test.sh` - Test branch management +- `git-cd-compare.sh` - Test branch comparison +- `git-cd-clean.sh` - Test cleaning up branches +- `git-cd-init-release-date.sh` - Test date-based versioning init +- `git-cd-release-date.sh` - Test date-based release +- `git-cd-init-release-file.sh` - Test file-based versioning init +- `git-cd-release-file.sh` - Test file-based release +- `git-cd-subdirectory.sh` - Test running from subdirectories +- `git-reset.sh` - Helper to reset git state diff --git a/.github/tests/git-add-build.sh b/.github/tests/git-add-build.sh new file mode 100644 index 00000000..017f00e7 --- /dev/null +++ b/.github/tests/git-add-build.sh @@ -0,0 +1,11 @@ +set -e + +# change workdir to travis-gitcd +cd ~/build/gitcd-io/travis-gitcd + +echo github-$GITHUB_RUN_NUMBER-$PYTHON_VERSION >> README.rst +git commit -m "Add current build number: $GITHUB_RUN_NUMBER-$PYTHON_VERSION" . +git push + +# change back to original workdir +cd - diff --git a/.travis/tests/git-cd-clean.sh b/.github/tests/git-cd-clean.sh similarity index 63% rename from .travis/tests/git-cd-clean.sh rename to .github/tests/git-cd-clean.sh index 4e97d7fe..52a035b0 100644 --- a/.travis/tests/git-cd-clean.sh +++ b/.github/tests/git-cd-clean.sh @@ -4,13 +4,13 @@ set -e cd ~/build/gitcd-io/travis-gitcd # assert that new feature branch exists remote -git branch -a | grep "origin/feature/travis-test-$TRAVIS_JOB_NUMBER" +git branch -a | grep "origin/feature/github-test-$GITHUB_RUN_NUMBER-$PYTHON_VERSION" # delete feature branch on remote -git push origin :feature/travis-test-$TRAVIS_JOB_NUMBER +git push origin :feature/github-test-$GITHUB_RUN_NUMBER-$PYTHON_VERSION # assert that the feature branch not present on remote anymore -REMOTE_COUNT=`git branch -a | grep "origin/feature/travis-test-$TRAVIS_JOB_NUMBER" | wc -l` +REMOTE_COUNT=`git branch -a | grep "origin/feature/github-test-$GITHUB_RUN_NUMBER-$PYTHON_VERSION" | wc -l` if [ $REMOTE_COUNT != 0 ]; then echo "Feature Branch still found on remote" exit 1 @@ -25,7 +25,7 @@ expect EOD # assert the local feature branch is deleted -LOCAL_COUNT=`git branch -a | grep "feature/travis-test-$TRAVIS_JOB_NUMBER" | wc -l` +LOCAL_COUNT=`git branch -a | grep "feature/github-test-$GITHUB_RUN_NUMBER-$PYTHON_VERSION" | wc -l` if [ $LOCAL_COUNT != 0 ]; then echo "Feature Branch still found locally" exit 1 diff --git a/.travis/tests/git-cd-compare.sh b/.github/tests/git-cd-compare.sh similarity index 100% rename from .travis/tests/git-cd-compare.sh rename to .github/tests/git-cd-compare.sh diff --git a/.travis/tests/git-cd-finish.sh b/.github/tests/git-cd-finish.sh similarity index 100% rename from .travis/tests/git-cd-finish.sh rename to .github/tests/git-cd-finish.sh diff --git a/.travis/tests/git-cd-init-release-date.sh b/.github/tests/git-cd-init-release-date.sh similarity index 100% rename from .travis/tests/git-cd-init-release-date.sh rename to .github/tests/git-cd-init-release-date.sh diff --git a/.travis/tests/git-cd-init-release-file.sh b/.github/tests/git-cd-init-release-file.sh similarity index 94% rename from .travis/tests/git-cd-init-release-file.sh rename to .github/tests/git-cd-init-release-file.sh index 3ca79eff..d5344002 100644 --- a/.travis/tests/git-cd-init-release-file.sh +++ b/.github/tests/git-cd-init-release-file.sh @@ -4,7 +4,7 @@ set -e cd ~/build/gitcd-io/travis-gitcd # write version file because git-cd init checks its existence -echo $TRAVIS_JOB_NUMBER.1 > version.txt +echo $GITHUB_RUN_NUMBER-$PYTHON_VERSION.1 > version.txt # git-cd init with accepting all the defaults /usr/bin/expect </dev/null || true +git branch -D "test/github-$GITHUB_RUN_NUMBER-$PYTHON_VERSION" 2>/dev/null || true + +# Reset master to origin +git reset --hard origin/master + +# change back to original workdir +cd - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b12e0591 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,155 @@ +name: Tests + +# Note: These tests require write access to gitcd-io/travis-gitcd repository. +# You need to configure a secret named GH_TEST_REPO_TOKEN with a Personal Access Token +# that has write access to that repository. Alternatively, you can fork the test repo +# and update the repository URL in the test scripts. + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +concurrency: + group: integration-tests + cancel-in-progress: false + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12'] + max-parallel: 1 + fail-fast: false + + env: + GH_ACCESS_TOKEN: ${{ secrets.GH_TEST_REPO_TOKEN || secrets.GITHUB_TOKEN }} + PYTHON_VERSION: ${{ matrix.python-version }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install system dependencies + run: | + sudo apt-get -qq update + sudo apt-get install -y expect + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + pip install . + + - name: Run flake8 + run: | + find . -name \*.py -not -path "./build/*" -not -path "./dist/*" -not -path "./*.egg-info/*" -exec flake8 {} + + + - name: Run bandit security scan + run: | + bandit -r . -c .bandit + continue-on-error: true + + - name: Configure Git + run: | + git config --global user.email "ci@github.com" + git config --global user.name "GitHub Actions" + + - name: Clone test repository + env: + GH_TOKEN: ${{ secrets.GH_TEST_REPO_TOKEN || secrets.GITHUB_TOKEN }} + run: | + if [ -z "${{ secrets.GH_TEST_REPO_TOKEN }}" ]; then + echo "Warning: GH_TEST_REPO_TOKEN not set. Integration tests may fail." + echo "See .github/tests/README.md for setup instructions." + fi + git clone https://x-access-token:${GH_TOKEN}@github.com/gitcd-io/travis-gitcd.git ~/build/gitcd-io/travis-gitcd + cd ~/build/gitcd-io/travis-gitcd + git config user.email "ci@github.com" + git config user.name "GitHub Actions" + + - name: Cleanup previous test branches + run: | + cd ~/build/gitcd-io/travis-gitcd + # Delete any leftover branches from previous test runs + git fetch origin + # List all remote branches matching our test pattern and delete them + git branch -r | grep 'origin/feature/github-' | sed 's|origin/||' | while read branch; do + branch=$(echo "$branch" | xargs) # trim whitespace + echo "Deleting remote branch: $branch" + git push origin --delete "$branch" || true + done + git branch -r | grep 'origin/test/github-' | sed 's|origin/||' | while read branch; do + branch=$(echo "$branch" | xargs) # trim whitespace + echo "Deleting remote branch: $branch" + git push origin --delete "$branch" || true + done + git checkout master + git reset --hard origin/master + + - name: Test git-cd version + run: bash .github/tests/git-cd-version.sh + continue-on-error: false + + - name: Test git-cd upgrade + run: bash .github/tests/git-cd-upgrade.sh + continue-on-error: false + + - name: Test git-cd init + run: bash .github/tests/git-cd-init.sh + + - name: Test git-cd start + run: bash .github/tests/git-cd-start.sh + + - name: Add test build + run: bash .github/tests/git-add-build.sh + + - name: Test git-cd refresh + run: bash .github/tests/git-cd-refresh.sh + + - name: Test git-cd review + run: bash .github/tests/git-cd-review.sh + + - name: Test git-cd finish + run: bash .github/tests/git-cd-finish.sh + + - name: Test git-cd release + run: bash .github/tests/git-cd-release.sh + + - name: Test git-cd test + run: bash .github/tests/git-cd-test.sh + + - name: Test git-cd compare + run: bash .github/tests/git-cd-compare.sh + + - name: Test git-cd clean + run: bash .github/tests/git-cd-clean.sh + + - name: Test git-cd init-release-date + run: bash .github/tests/git-cd-init-release-date.sh + + - name: Test git-cd release-date + run: bash .github/tests/git-cd-release-date.sh + + - name: Reset git state + run: bash .github/tests/git-reset.sh + + - name: Test git-cd init-release-file + run: bash .github/tests/git-cd-init-release-file.sh + + - name: Test git-cd release-file + run: bash .github/tests/git-cd-release-file.sh + + - name: Reset git state (final) + run: bash .github/tests/git-reset.sh + + - name: Test git-cd subdirectory + run: bash .github/tests/git-cd-subdirectory.sh diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2be12f5b..00000000 --- a/.travis.yml +++ /dev/null @@ -1,71 +0,0 @@ -language: python -branches: - only: - - master -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" - -before_install: - - sudo apt-get -qq update - - sudo apt-get install -y expect - -# command to install dependencies -install: - - pip install -r requirements-dev.txt - - pip install . -script: - # Run flake8 on all .py files in all subfolders - - find . -name \*.py -exec flake8 {} + - - # Scan for known security issues - - bandit -r . - - # run tests - - | # default repo setup - bash .travis/setup-ssh.sh - - | - bash .travis/tests/git-cd-version.sh - - | - bash .travis/clone-test-repo.sh - - | # check git-cd upgrade - bash .travis/tests/git-cd-upgrade.sh - - | # default git-cd workflow - bash .travis/tests/git-cd-init.sh - - | - bash .travis/tests/git-cd-start.sh - - | - bash .travis/git-add-build.sh - - | - bash .travis/tests/git-cd-refresh.sh - - | - bash .travis/tests/git-cd-review.sh - - | - bash .travis/tests/git-cd-finish.sh - - | - bash .travis/tests/git-cd-release.sh - - | # git-cd test compare and clean - bash .travis/tests/git-cd-test.sh - - | - bash .travis/tests/git-cd-compare.sh - - | - bash .travis/tests/git-cd-clean.sh - - | # make a release by date format - bash .travis/tests/git-cd-init-release-date.sh - - | - bash .travis/tests/git-cd-release-date.sh - - | - bash .travis/git-reset.sh - - | # make a release by version file and extra command - bash .travis/tests/git-cd-init-release-file.sh - - | - bash .travis/tests/git-cd-release-file.sh - - | - bash .travis/git-reset.sh - - -env: - global: - - secure: "gh+8ceEarA3KYPg/Fg1vS52a8+KOdj//UwOL48ZfumOagXs2u8d3mSa61BVukroa34QKJa89UY318AtXU+LCeQk/w7SMUpL0bCqLxefdo2/DfJZqAiX74Ema5VYENi6mbRdzC9dS5rRbG04m7p3vWxahs2LPZF7J+NFZAdgZqyThbDYfb9Qa/lKunq86b60iUmlj+rfLGfRJLlm/o5Iet3SBq0ktUXflEjltWuPLNwgPPoxOITrFNZG+GPsATepKKWIweLr/jJIoSa1SpHCcxMuHrdsFPhpIFLffmCnVIORSZe4l6tbDhGntQjR4J7mhULT8uqrG92Nl6QoNBpEP4UYigI+9qLnVdQGvKznbps9M4udvGBjs4UvR91d5Kjhbr7upH+DzRuVzRxfNNCLF4j+p9Vr2uZbXoaZovGExVcraUbNJBUW/xlDHXntRzytQhomoaaBWheODrSAvBXUx0j3QlGJMcFhHdHQs5hX7EwRY2xb8Dzd83B0iqAMDizeXuQuObjIUKq0CzD1z7viaBUhDFgDyzWiWB7vKHb2L4Lxfwb0JK0cw81l9XkbsYB99DD2MaO78yR/DzYUjHXqi9EPSZslGYum4YY46gUrdoaUSqcN+pmbpdUfjIVPM/o6ap9j2mScHn3uUKDXn9qRTRecOEwleT9uNohKBRFjA0wM=" diff --git a/.travis/clone-test-repo.sh b/.travis/clone-test-repo.sh deleted file mode 100644 index 035ea8bb..00000000 --- a/.travis/clone-test-repo.sh +++ /dev/null @@ -1,3 +0,0 @@ -# setup ssh key for https://github.com/gitcd-io/travis-gitcd - -git clone git@github.com:gitcd-io/travis-gitcd.git ~/build/gitcd-io/travis-gitcd diff --git a/.travis/git-add-build.sh b/.travis/git-add-build.sh deleted file mode 100644 index 2ede9dce..00000000 --- a/.travis/git-add-build.sh +++ /dev/null @@ -1,11 +0,0 @@ -set -e - -# change workdir to travis-gitcd -cd ~/build/gitcd-io/travis-gitcd - -echo travis-$TRAVIS_JOB_NUMBER >> README.rst -git commit -m "Add current build number: $TRAVIS_JOB_NUMBER" . -git push - -# change back to original workdir -cd - diff --git a/.travis/git-reset.sh b/.travis/git-reset.sh deleted file mode 100644 index 7dafcc51..00000000 --- a/.travis/git-reset.sh +++ /dev/null @@ -1,9 +0,0 @@ -set -e - -# change workdir to travis-gitcd -cd ~/build/gitcd-io/travis-gitcd - -git reset --hard - -# change back to original workdir -cd - diff --git a/.travis/setup-ssh.sh b/.travis/setup-ssh.sh deleted file mode 100644 index 5d07e35b..00000000 --- a/.travis/setup-ssh.sh +++ /dev/null @@ -1,14 +0,0 @@ -# setup ssh key for https://github.com/gitcd-io/travis-gitcd -declare -r SSH_FILE="$(mktemp -u $HOME/.ssh/XXXXX)" - -openssl aes-256-cbc \ --K $encrypted_a8b48c8ad6aa_key \ --iv $encrypted_a8b48c8ad6aa_iv \ --in ".travis/travis_deploy_key.enc" \ --out "$SSH_FILE" -d - -chmod 600 "$SSH_FILE" \ -&& printf "%s\n" \ - "Host github.com" \ - " IdentityFile $SSH_FILE" \ - " LogLevel ERROR" >> ~/.ssh/config diff --git a/.travis/travis_deploy_key.enc b/.travis/travis_deploy_key.enc deleted file mode 100644 index e54743ed..00000000 Binary files a/.travis/travis_deploy_key.enc and /dev/null differ diff --git a/gitcd/app/upgrade.py b/gitcd/app/upgrade.py index 81fc6a08..249aef3b 100644 --- a/gitcd/app/upgrade.py +++ b/gitcd/app/upgrade.py @@ -1,6 +1,6 @@ import simpcli -from packaging import version -import pkg_resources +from packaging import version as parse_version +from importlib.metadata import version import requests from gitcd.app import App @@ -10,13 +10,13 @@ class Upgrade(App): - localVersion = 0 - pypiVersion = 0 + localVersion: str = "" + pypiVersion: str = "" packageUrl = 'https://pypi.org/pypi/gitcd/json' verboseCli = simpcli.Command(True) def getLocalVersion(self) -> str: - self.localVersion = pkg_resources.get_distribution("gitcd").version + self.localVersion = version("gitcd") return self.localVersion @@ -37,7 +37,9 @@ def getPypiVersion(self) -> str: return self.pypiVersion def isUpgradable(self) -> bool: - if version.parse(self.localVersion) < version.parse(self.pypiVersion): + local = parse_version.parse(self.localVersion) + pypi = parse_version.parse(self.pypiVersion) + if local < pypi: return True return False diff --git a/gitcd/config/__init__.py b/gitcd/config/__init__.py index 55e0e900..080c1103 100644 --- a/gitcd/config/__init__.py +++ b/gitcd/config/__init__.py @@ -5,6 +5,7 @@ from gitcd.config.defaults import GitcdPersonalDefaults from gitcd.exceptions import GitcdFileNotFoundException from gitcd.exceptions import GitcdTokenNotImplemented +from gitcd.util import getGitRoot class Parser: @@ -12,7 +13,7 @@ class Parser: yaml = {} def load(self, filename: str): - # raise exception if no .gitcd in current working dir + # raise exception if config file is not found if not os.path.isfile(filename): raise GitcdFileNotFoundException("File %s not found" % filename) @@ -37,7 +38,17 @@ class Gitcd: def __init__(self): self.config = self.defaults.load() - if os.path.isfile(self.filename): + # Find git root directory to locate .gitcd config file + git_root = getGitRoot() + if git_root: + config_path = os.path.join(git_root, self.filename) + if os.path.isfile(config_path): + config = self.parser.load(config_path) + self.config.update(config) + # Update filename to use the full path from git root + self.filename = config_path + elif os.path.isfile(self.filename): + # Fallback to current directory if not in a git repo config = self.parser.load(self.filename) self.config.update(config) diff --git a/gitcd/package.py b/gitcd/package.py index 9023b236..5b695d37 100644 --- a/gitcd/package.py +++ b/gitcd/package.py @@ -1,4 +1,4 @@ -import pkg_resources +from importlib.metadata import version import requests import pip from gitcd.exceptions import GitcdPyPiApiException @@ -12,7 +12,7 @@ def upgrade(self): pip.main(['install', '--user', '--upgrade', 'gitcd']) def getLocalVersion(self): - return pkg_resources.get_distribution("gitcd").version + return version("gitcd") def getPypiVersion(self): response = requests.get( diff --git a/gitcd/util.py b/gitcd/util.py new file mode 100644 index 00000000..5b4e5d9f --- /dev/null +++ b/gitcd/util.py @@ -0,0 +1,23 @@ +import subprocess +from typing import Optional + + +def getGitRoot() -> Optional[str]: + """ + Get the git repository root directory. + + Returns: + The absolute path to the git repository root, or None if not in a + git repository. + """ + try: + result = subprocess.run( + ['git', 'rev-parse', '--show-toplevel'], + capture_output=True, + text=True, + check=True + ) + return result.stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + # Not in a git repository or git not available + return None diff --git a/requirements-dev.txt b/requirements-dev.txt index 4898107d..10af19f0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ -flake8==3.8.4 -bandit==1.6.2 \ No newline at end of file +flake8>=6.0.0 +bandit>=1.7.5 \ No newline at end of file diff --git a/setup.py b/setup.py index 9a08e11d..0b4bfa30 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ def version(fpath): 'packaging', 'typing' ], + python_requires='>=3.9', entry_points={ 'console_scripts': [ @@ -45,10 +46,10 @@ def version(fpath): 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Utilities' ] ) diff --git a/version.txt b/version.txt index 58594069..21bb5e15 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.2.3 +2.2.5