From 255c231d208ce3a8683c63b2c938cfb940556eb4 Mon Sep 17 00:00:00 2001 From: Nimish <85357445+nimish-ks@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:05:32 +0530 Subject: [PATCH] Feat gh actions workflow improvements (#169) * feat: added a callable workflow for pytest * feat: call pytest from the main workflow * fix: dependency * fix: python version reference * feat: renamed the workflow to main.yml * feat: move versioning to version.yml * feat: validate versions * feat: renamed job * test: version validator * feat: works, revert version * feat: cli build workflow * feat: use build from build.yml * fix: added version dependency * fix: remove dependency from build * feat: move linux arm64 and alpine build to build.yml * test: disable arm and apk builds * fix: dependency * chore: remove unused jobs * feat: moved docker jobs to docker.yml * test: remove trigger * feat: set version in setup.py * chore: added setuptools dev dependency * feat: refactored docker file, added entrypoint, setuptools based install * test: build * test: run docker * test: remove build dependency * test: comment * test: testbuild * feat: added version dependency * fix: tag * fix: remove tag * fix: build * feat: add arm64 and armv6 builds * fix: added rust and cargo dependency * feat: revert to old Dockerfile * feat: removed armv6 * feat: removed comments, added buildx multi arc builds * fix: testing errors * fix: version checks * fix: command * debug: version confirmation * feat: pass version as input to docker build * feat: pass version to docker build * feat: added pypi build and upload * feat: added pypi * feat: only run docker on release creation * chore: bump upload artifact version to v4 * feat: remove build dependency * feat: added comment * feat: only run pypi on release creation * feat: added a download and test version stage * feat: pass version to pypi * test: pull and test version * chore: clean up tests * chore: bump gh actions dependencies * feat: removed unneeded phase-alpine-build * feat: removed unneeded phase-alpine-build asset processing * feat: use python version from inputs * feat: remove version artifact * feat: added script to process assets * feat: use process script in process-assets * fix: .apk, .deb, .rpm hashing * chore: remove commented code * feat: moved process-assets job * feat: added attach to release * feat: added cli installation script * fix: removed GITHUB_TOKEN from main * fix: distro images * fix: permission issues during installation * fix: checksum path * fix: rpm permissions * feat: use checkout v4 * feat: move attach-to-release * feat: added jobs in main * feat: added comments * fix: sha256 sum filenames * feat: added arm64 linux install test * feat: install and test the cli from pypi on a windows, mac, linux matrix * test: matrix pypi cli install * feat: works --- .github/workflows/attach-to-release.yml | 26 ++ .github/workflows/build.yml | 156 +++++++++++ .github/workflows/build_release.yml | 336 ------------------------ .github/workflows/docker.yml | 84 ++++++ .github/workflows/main.yml | 80 ++++++ .github/workflows/process-assets.yml | 34 +++ .github/workflows/pypi.yml | 87 ++++++ .github/workflows/pytest.yml | 22 ++ .github/workflows/test-cli-install.yml | 66 +++++ .github/workflows/version.yml | 46 ++++ Dockerfile | 23 +- dev-requirements.txt | 3 +- install.sh | 147 ++++++++--- process_assets.py | 66 +++++ setup.py | 5 +- 15 files changed, 801 insertions(+), 380 deletions(-) create mode 100644 .github/workflows/attach-to-release.yml create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/build_release.yml create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/process-assets.yml create mode 100644 .github/workflows/pypi.yml create mode 100644 .github/workflows/pytest.yml create mode 100644 .github/workflows/test-cli-install.yml create mode 100644 .github/workflows/version.yml create mode 100644 process_assets.py diff --git a/.github/workflows/attach-to-release.yml b/.github/workflows/attach-to-release.yml new file mode 100644 index 00000000..55a4df4e --- /dev/null +++ b/.github/workflows/attach-to-release.yml @@ -0,0 +1,26 @@ +name: Attach Assets to Release + +on: + workflow_call: + inputs: + version: + required: true + type: string + +jobs: + attach-to-release: + name: Attach Assets to Release + runs-on: ubuntu-20.04 + steps: + - name: Download processed assets + uses: actions/download-artifact@v4 + with: + name: phase-cli-release + path: ./phase-cli-release + + - name: Attach assets to release + uses: softprops/action-gh-release@v2 + with: + files: ./phase-cli-release/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..79de676a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,156 @@ +name: Build CLI + +on: + workflow_call: + inputs: + version: + required: true + type: string + python_version: + required: true + type: string + +jobs: + build: + name: Build CLI + runs-on: ${{ matrix.os }} + strategy: + matrix: + # ubuntu-20.04 - context: https://github.com/phasehq/cli/issues/94 + # macos-13 darwin-amd64 builds (intel) + # macos-14 darwin-arm64 builds (apple silicon) + # context: https://github.com/actions/runner-images?tab=readme-ov-file#available-images + + os: [ubuntu-20.04, windows-2022, macos-13, macos-14] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + - run: pip install -r requirements.txt + - run: pip install pyinstaller + - run: pyinstaller --hidden-import _cffi_backend --paths ./phase_cli --name phase phase_cli/main.py + - name: Print GLIBC version + if: matrix.os == 'ubuntu-20.04' + run: ldd --version + + # Set LC_ALL based on the runner OS for Linux and macOS + - name: Set LC_ALL for Linux and macOS + run: export LC_ALL=C.UTF-8 + if: runner.os != 'Windows' + shell: bash + + # Set LC_ALL for Windows + - name: Set LC_ALL for Windows + run: echo "LC_ALL=C.UTF-8" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV + if: runner.os == 'Windows' + shell: pwsh + + # # Download the version file + # - uses: actions/download-artifact@v4 + # with: + # name: phase-version + + # # DEBUG + # - name: List files after downloading artifact + # shell: bash + # run: ls -al + + # # Set the version environment variable + # - name: Set VERSION + # shell: bash + # run: | + # PHASE_CLI_VERSION=$(cat PHASE_CLI_VERSION.txt) + # echo "PHASE_CLI_VERSION is: $PHASE_CLI_VERSION" + # echo "PHASE_CLI_VERSION=$PHASE_CLI_VERSION" >> $GITHUB_ENV + + # Build DEB and RPM packages for Linux + - run: | + sudo apt-get update + sudo apt-get install -y ruby-dev rubygems build-essential + sudo gem install --no-document fpm + fpm -s dir -t deb -n phase -v ${{ inputs.version }} dist/phase/=usr/bin/ + fpm -s dir -t rpm -n phase -v ${{ inputs.version }} dist/phase/=usr/bin/ + if: matrix.os == 'ubuntu-20.04' + shell: bash + + # Upload DEB and RPM packages + - uses: actions/upload-artifact@v4 + with: + name: phase-deb + path: "*.deb" + if: matrix.os == 'ubuntu-20.04' + - uses: actions/upload-artifact@v4 + with: + name: phase-rpm + path: "*.rpm" + if: matrix.os == 'ubuntu-20.04' + + - name: Set artifact name + run: | + if [[ "${{ matrix.os }}" == "macos-14" ]]; then + echo "ARTIFACT_NAME=${{ runner.os }}-arm64-binary" >> $GITHUB_ENV + elif [[ "${{ matrix.os }}" == "macos-13" ]]; then + echo "ARTIFACT_NAME=${{ runner.os }}-amd64-binary" >> $GITHUB_ENV + else + echo "ARTIFACT_NAME=${{ runner.os }}-binary" >> $GITHUB_ENV + fi + shell: bash + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACT_NAME }} + path: dist/phase* + + build_arm: + name: Build Linux ARM64 + runs-on: self-hosted + container: + image: python:3.11.0-bullseye + steps: + - uses: actions/checkout@v4 + - name: Setup Environment + run: | + python --version + pip install -r requirements.txt + pip install pyinstaller + - name: Build with PyInstaller + run: pyinstaller --hidden-import _cffi_backend --paths ./phase_cli --name phase phase_cli/main.py + - uses: actions/upload-artifact@v4 + with: + name: Linux-binary-arm64 + path: dist/phase* + + build_apk: + name: Build Alpine + runs-on: ubuntu-20.04 + container: + image: python:3.11-alpine + steps: + - uses: actions/checkout@v4 + - run: apk add --update --no-cache python3 python3-dev build-base + - run: python3 -m ensurepip + - run: pip3 install --no-cache --upgrade pip setuptools + - run: pip install -r requirements.txt + - run: pip install pyinstaller + - run: pyinstaller --hidden-import _cffi_backend --paths ./phase_cli --name phase phase_cli/main.py + - run: apk add alpine-sdk + - run: adduser -G abuild -g "Alpine Package Builder" -s /bin/ash -D builder + - run: mkdir /home/builder/package + - run: chown builder /home/builder/package + - run: apk add --no-cache sudo + - run: | + echo "builder ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers + ls ./dist/phase + cp -r ./dist/phase/* /home/builder/package/ + cp ./APKBUILD /home/builder/package + cd /home/builder/package + sudo -u builder abuild-keygen -a -i -n + sudo -u builder abuild checksum + sudo -u builder abuild -r + shell: sh + - uses: actions/upload-artifact@v4 + with: + name: phase-apk + path: /home/builder/packages/builder/x86_64/*.apk diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml deleted file mode 100644 index 781486df..00000000 --- a/.github/workflows/build_release.yml +++ /dev/null @@ -1,336 +0,0 @@ -name: Test, Build, Package the Phase CLI - -on: - pull_request: - push: - branches: - - main - -permissions: - contents: write - pull-requests: write - -jobs: - pytest: - name: Run Tests - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [3.11] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - run: pip install -r dev-requirements.txt - - run: pip install pytest - - name: Set PYTHONPATH - run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV - - run: pytest tests/*.py - - extract_version: - name: Extract Version - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Extract version - run: | - PHASE_CLI_VERSION=$(grep -oP '(?<=__version__ = ")[^"]*' phase_cli/utils/const.py) - echo "PHASE_CLI_VERSION=$PHASE_CLI_VERSION" >> $GITHUB_ENV - echo "$PHASE_CLI_VERSION" > ./PHASE_CLI_VERSION.txt - - uses: actions/upload-artifact@v4 - with: - name: phase-version - path: ./PHASE_CLI_VERSION.txt - - build: - name: Build CLI - runs-on: ${{ matrix.os }} - needs: [pytest, extract_version] - strategy: - matrix: - # ubuntu-20.04 - context: https://github.com/phasehq/cli/issues/94 - # macos-13 darwin-amd64 builds (intel) - # macos-14 darwin-arm64 builds (apple silicon) - # context: https://github.com/actions/runner-images?tab=readme-ov-file#available-images - - os: [ubuntu-20.04, windows-2022, macos-13, macos-14] - python-version: [3.11] - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - run: pip install -r requirements.txt - - run: pip install pyinstaller - - run: pyinstaller --hidden-import _cffi_backend --paths ./phase_cli --name phase phase_cli/main.py - - name: Print GLIBC version - if: matrix.os == 'ubuntu-20.04' - run: ldd --version - - # Set LC_ALL based on the runner OS for Linux and macOS - - name: Set LC_ALL for Linux and macOS - run: export LC_ALL=C.UTF-8 - if: runner.os != 'Windows' - shell: bash - - # Set LC_ALL for Windows - - name: Set LC_ALL for Windows - run: echo "LC_ALL=C.UTF-8" | Out-File -Append -Encoding utf8 $env:GITHUB_ENV - if: runner.os == 'Windows' - shell: pwsh - - # Download the version file - - uses: actions/download-artifact@v4 - with: - name: phase-version - - # DEBUG - - name: List files after downloading artifact - shell: bash - run: ls -al - - # Set the version environment variable - - name: Set VERSION - shell: bash - run: | - PHASE_CLI_VERSION=$(cat PHASE_CLI_VERSION.txt) - echo "PHASE_CLI_VERSION is: $PHASE_CLI_VERSION" - echo "PHASE_CLI_VERSION=$PHASE_CLI_VERSION" >> $GITHUB_ENV - - # Build DEB and RPM packages for Linux - - run: | - sudo apt-get update - sudo apt-get install -y ruby-dev rubygems build-essential - sudo gem install --no-document fpm - fpm -s dir -t deb -n phase -v $PHASE_CLI_VERSION dist/phase/=usr/bin/ - fpm -s dir -t rpm -n phase -v $PHASE_CLI_VERSION dist/phase/=usr/bin/ - if: matrix.os == 'ubuntu-20.04' - shell: bash - - # Upload DEB and RPM packages - - uses: actions/upload-artifact@v4 - with: - name: phase-deb - path: "*.deb" - if: matrix.os == 'ubuntu-20.04' - - uses: actions/upload-artifact@v4 - with: - name: phase-rpm - path: "*.rpm" - if: matrix.os == 'ubuntu-20.04' - - - name: Set artifact name - run: | - if [[ "${{ matrix.os }}" == "macos-14" ]]; then - echo "ARTIFACT_NAME=${{ runner.os }}-arm64-binary" >> $GITHUB_ENV - elif [[ "${{ matrix.os }}" == "macos-13" ]]; then - echo "ARTIFACT_NAME=${{ runner.os }}-amd64-binary" >> $GITHUB_ENV - else - echo "ARTIFACT_NAME=${{ runner.os }}-binary" >> $GITHUB_ENV - fi - shell: bash - - - name: Upload binary - uses: actions/upload-artifact@v4 - with: - name: ${{ env.ARTIFACT_NAME }} - path: dist/phase* - - build_arm: - name: Build CLI for ARM64 - runs-on: self-hosted - container: - image: python:3.11.0-bullseye - needs: [pytest, extract_version] - steps: - - uses: actions/checkout@v2 - - name: Setup Environment - run: | - python --version - pip install -r requirements.txt - pip install pyinstaller - - name: Build with PyInstaller - run: pyinstaller --hidden-import _cffi_backend --paths ./phase_cli --name phase phase_cli/main.py - - uses: actions/upload-artifact@v4 - with: - name: Linux-binary-arm64 - path: dist/phase* - - build_apk: - name: Build CLI (alpine-latest, 3.11) - runs-on: ubuntu-20.04 - needs: [pytest, extract_version] - container: - image: python:3.11-alpine - steps: - - uses: actions/checkout@v2 - - run: apk add --update --no-cache python3 python3-dev build-base - - run: python3 -m ensurepip - - run: pip3 install --no-cache --upgrade pip setuptools - - run: pip install -r requirements.txt - - run: pip install pyinstaller - - run: pyinstaller --hidden-import _cffi_backend --paths ./phase_cli --name phase phase_cli/main.py - - run: apk add alpine-sdk - - run: adduser -G abuild -g "Alpine Package Builder" -s /bin/ash -D builder - - run: mkdir /home/builder/package - - run: chown builder /home/builder/package - - run: apk add --no-cache sudo - - run: | - echo "builder ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers - ls ./dist/phase - cp -r ./dist/phase/* /home/builder/package/ - cp ./APKBUILD /home/builder/package - cd /home/builder/package - sudo -u builder abuild-keygen -a -i -n - sudo -u builder abuild checksum - sudo -u builder abuild -r - shell: sh - - uses: actions/upload-artifact@v4 - with: - name: phase-apk - path: /home/builder/packages/builder/x86_64/*.apk - - uses: actions/upload-artifact@v4 - with: - name: phase-alpine-build - path: ./dist/phase - - Build_and_push_docker: - name: Build & Release - Docker - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - needs: [build, build_apk] - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - - name: Download phase-version artifact - uses: actions/download-artifact@v4 - with: - name: phase-version - path: phase-version - - - name: Set VERSION from file - run: echo "VERSION=$(cat ./phase-version/PHASE_CLI_VERSION.txt | tr -d '\r')" >> $GITHUB_ENV - - name: Download phase-alpine-build artifact - uses: actions/download-artifact@v4 - with: - name: phase-alpine-build - path: dist - - - run: docker build -t phase-cli:${{ env.VERSION }} . - - - name: Push to Docker Hub - run: | - echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USERNAME }}" --password-stdin - docker tag phase-cli:${{ env.VERSION }} phasehq/cli:${{ env.VERSION }} - docker push phasehq/cli:${{ env.VERSION }} - docker tag phase-cli:${{ env.VERSION }} phasehq/cli:latest - docker push phasehq/cli:latest - - Pull_and_test_docker_image: - name: Test - CLI - Docker - needs: Build_and_push_docker - runs-on: ubuntu-20.04 - steps: - - name: Download phase-version artifact - uses: actions/download-artifact@v4 - with: - name: phase-version - path: phase-version - - - name: Set VERSION from file - run: echo "VERSION=$(cat ./phase-version/PHASE_CLI_VERSION.txt | tr -d '\r')" >> $GITHUB_ENV - - # Pull and test the versioned Docker image - - run: docker pull phasehq/cli:${{ env.VERSION }} - - run: docker run phasehq/cli:${{ env.VERSION }} phase -h - - run: docker run phasehq/cli:${{ env.VERSION }} phase -v - - # Pull and test the latest Docker image - - run: docker pull phasehq/cli:latest - - run: docker run phasehq/cli:latest phase -h - - run: docker run phasehq/cli:latest phase -v - - attach-assets: - name: Package assets - needs: [build, build_apk, build_arm] - runs-on: ubuntu-20.04 - - steps: - # Download all the artifacts from the build jobs - - name: Download all artifacts - uses: actions/download-artifact@v4 - - # # Debug: List the content of the current directory - # - name: Debug - List contents of artifacts directory - # run: find . -type f - - # Extract version from the file - - name: Set VERSION from file - run: echo "VERSION=$(cat ./phase-version/PHASE_CLI_VERSION.txt | tr -d '\r')" >> $GITHUB_ENV - - # Process assets: Rename, calculate sha256, and move them to the release directory - - name: Process assets - run: | - mkdir phase_cli_release_$VERSION - - # For DEB - mv phase-deb/*.deb phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.deb - sha256sum phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.deb > phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.deb.sha256 - - # For RPM - mv phase-rpm/*.rpm phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.rpm - sha256sum phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.rpm > phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.rpm.sha256 - - # For Linux - ZIPNAME="phase_cli_linux_amd64_$VERSION.zip" - zip -r "$ZIPNAME" Linux-binary/phase/ Linux-binary/phase/_internal - mv "$ZIPNAME" phase_cli_release_$VERSION/ - sha256sum phase_cli_release_$VERSION/"$ZIPNAME" > phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.sha256 - - # For Windows - ZIPNAME="phase_cli_windows_amd64_$VERSION.zip" - zip -r "$ZIPNAME" Windows-binary/phase/ Windows-binary/phase/_internal - mv "$ZIPNAME" phase_cli_release_$VERSION/ - sha256sum phase_cli_release_$VERSION/"$ZIPNAME" > phase_cli_release_$VERSION/phase_cli_windows_amd64_$VERSION.sha256 - - # For MacOS Intel build - ZIPNAME="phase_cli_macos_amd64_$VERSION.zip" - zip -r "$ZIPNAME" macOS-amd64-binary/phase/ macOS-binary/phase/_internal - mv "$ZIPNAME" phase_cli_release_$VERSION/ - sha256sum phase_cli_release_$VERSION/"$ZIPNAME" > phase_cli_release_$VERSION/phase_cli_macos_amd64_$VERSION.sha256 - - # For MacOS Apple silicon build - ZIPNAME="phase_cli_macos_arm64_$VERSION.zip" - zip -r "$ZIPNAME" macOS-arm64-binary/phase/ macOS-binary/phase/_internal - mv "$ZIPNAME" phase_cli_release_$VERSION/ - sha256sum phase_cli_release_$VERSION/"$ZIPNAME" > phase_cli_release_$VERSION/phase_cli_macos_arm64_$VERSION.sha256 - - # For Alpine build - ZIPNAME="phase_cli_alpine_linux_amd64_$VERSION.zip" - zip -r "$ZIPNAME" phase-alpine-build/phase phase-alpine-build/_internal - mv "$ZIPNAME" phase_cli_release_$VERSION/ - sha256sum phase_cli_release_$VERSION/"$ZIPNAME" > phase_cli_release_$VERSION/phase_cli_alpine_linux_amd64_$VERSION.sha256 - - # For Alpine package - mv phase-apk/*.apk phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.apk - sha256sum phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.apk > phase_cli_release_$VERSION/phase_cli_linux_amd64_$VERSION.apk.sha256 - # Process assets for ARM64 - - name: Process ARM64 assets - run: | - mkdir -p phase_cli_release_$VERSION/Linux-binary/phase/ - mv Linux-binary-arm64/phase/* phase_cli_release_$VERSION/Linux-binary/phase/ - - # Adjust the ZIPNAME and paths for ARM64 binary - ZIPNAME="phase_cli_linux_arm64_$VERSION.zip" - zip -r "$ZIPNAME" phase_cli_release_$VERSION/Linux-binary/phase/ phase_cli_release_$VERSION/Linux-binary/phase/_internal - mv "$ZIPNAME" phase_cli_release_$VERSION/ - sha256sum phase_cli_release_$VERSION/"$ZIPNAME" > phase_cli_release_$VERSION/phase_cli_linux_arm64_$VERSION.sha256 - # Cleanup the ARM64 binary folder - rm -rf Linux-binary - - # Upload the entire release directory as a single artifact - - uses: actions/upload-artifact@v4 - with: - name: phase-cli-release - path: ./phase_cli_release_*/ diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..50c0b194 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,84 @@ +name: Docker Build, Push, and Test + +on: + workflow_call: + inputs: + version: + required: true + type: string + secrets: + DOCKER_HUB_USERNAME: + required: true + DOCKER_HUB_PASSWORD: + required: true + +jobs: + build_push: + name: Build & Release - Docker + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + phasehq/cli:${{ inputs.version }} + phasehq/cli:latest + + pull_test: + name: Test - CLI - Docker + needs: build_push + runs-on: ubuntu-20.04 + steps: + - name: Pull versioned image + run: docker pull phasehq/cli:${{ inputs.version }} + + - name: Test versioned image version + run: | + echo "Testing versioned image: phasehq/cli:${{ inputs.version }}" + FULL_OUTPUT=$(docker run --rm phasehq/cli:${{ inputs.version }} --version) + echo "Full output: $FULL_OUTPUT" + RETURNED_VERSION=$(echo "$FULL_OUTPUT" | awk '{print $1}') + echo "Parsed version: $RETURNED_VERSION" + if [ -z "$RETURNED_VERSION" ]; then + echo "Error: Could not parse version from output" + exit 1 + fi + if [ "$RETURNED_VERSION" != "${{ inputs.version }}" ]; then + echo "Version mismatch: Expected ${{ inputs.version }}, got $RETURNED_VERSION" + exit 1 + fi + echo "Version check passed for versioned image" + + - name: Pull latest image + run: docker pull phasehq/cli:latest + + - name: Test latest image version + run: | + echo "Testing latest image: phasehq/cli:latest" + FULL_OUTPUT=$(docker run --rm phasehq/cli:latest --version) + echo "Full output: $FULL_OUTPUT" + RETURNED_VERSION=$(echo "$FULL_OUTPUT" | awk '{print $1}') + echo "Parsed version: $RETURNED_VERSION" + if [ -z "$RETURNED_VERSION" ]; then + echo "Error: Could not parse version from output" + exit 1 + fi + if [ "$RETURNED_VERSION" != "${{ inputs.version }}" ]; then + echo "Version mismatch: Expected ${{ inputs.version }}, got $RETURNED_VERSION" + exit 1 + fi + echo "Version check passed for latest image" \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..6df47742 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,80 @@ +name: Test, Build, Package the Phase CLI + +on: + pull_request: + push: + branches: + - main + release: + types: [created] + +permissions: + contents: write + pull-requests: write + +jobs: + + # Run Tests + pytest: + uses: ./.github/workflows/pytest.yml + with: + python_version: '3.11' + + # Fetch and validate version from source code + version: + uses: ./.github/workflows/version.yml + + # Build and package the CLI using PyInstaller for Windows(amd64), Mac (Intel - amd64, Apple silicon arm64), Alpine linux (amd64), Linux (amd64, arm64) .deb, .rpm, binaries + + # TODO: Add arm64 support for windows, arm64 packages (deb, rpm, apk) + build: + needs: [pytest, version] + uses: ./.github/workflows/build.yml + with: + python_version: '3.11' + version: ${{ needs.version.outputs.version }} + + # Build docker image, push it to :latest and : OS/Arch linux/amd64, linux/arm64, pull images, run the CLI and validate version + docker: + if: github.event_name == 'release' && github.event.action == 'created' + needs: [version] + uses: ./.github/workflows/docker.yml + with: + version: ${{ needs.version.outputs.version }} + secrets: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} + + # Build, package and upload to pypi, install, run the cli and validate version + pypi: + if: github.event_name == 'release' && github.event.action == 'created' + needs: [version] + uses: ./.github/workflows/pypi.yml + with: + version: ${{ needs.version.outputs.version }} + secrets: + PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + + # Download packages, builds, binaries from build stage, rename, hash and zip the assets via the script + process-assets: + needs: [build, version] + uses: ./.github/workflows/process-assets.yml + with: + version: ${{ needs.version.outputs.version }} + + # Download packaged assets zip and attach them to a release as assets + attach-to-release: + if: github.event_name == 'release' && github.event.action == 'created' + needs: [process-assets, version] + uses: ./.github/workflows/attach-to-release.yml + with: + version: ${{ needs.version.outputs.version }} + + # Install, run and validate CLI version. + test-cli-install: + needs: [version, attach-to-release] + if: github.event_name != 'release' || (github.event_name == 'release' && github.event.action == 'created') + uses: ./.github/workflows/test-cli-install.yml + with: + version: ${{ needs.version.outputs.version }} diff --git a/.github/workflows/process-assets.yml b/.github/workflows/process-assets.yml new file mode 100644 index 00000000..bddc8430 --- /dev/null +++ b/.github/workflows/process-assets.yml @@ -0,0 +1,34 @@ +name: Process and Package Assets + +on: + workflow_call: + inputs: + version: + required: true + type: string + +jobs: + process-assets: + name: Process and Package Assets + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Process assets + run: | + python process_assets.py . phase-cli-release/ --version ${{ inputs.version }} + + - name: Upload processed assets + uses: actions/upload-artifact@v4 + with: + name: phase-cli-release + path: ./phase-cli-release/ diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 00000000..4666be79 --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,87 @@ +name: Build and Deploy to PyPI + +on: + workflow_call: + inputs: + version: + required: true + type: string + secrets: + PYPI_USERNAME: + required: true + PYPI_PASSWORD: + required: true + +jobs: + build_and_deploy: + name: Build and Deploy to PyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + + # Versioning is handled in setup.py + - name: Build distribution + run: python setup.py sdist bdist_wheel + + - name: Upload to PyPI + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: twine upload dist/* + + - name: Upload distribution artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + pull_and_test: + name: Pull and Test PyPI Package + needs: build_and_deploy + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install package from PyPI + run: | + python -m pip install --upgrade pip + pip install phase-cli + + - name: Test installed package (Linux/macOS) + if: runner.os != 'Windows' + run: | + installed_version=$(phase -v) + echo "Installed version: $installed_version" + echo "Expected version: ${{ inputs.version }}" + if [ "$installed_version" != "${{ inputs.version }}" ]; then + echo "Version mismatch!" + exit 1 + fi + + - name: Test installed package (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $installed_version = phase -v + echo "Installed version: $installed_version" + echo "Expected version: ${{ inputs.version }}" + if ($installed_version -ne "${{ inputs.version }}") { + echo "Version mismatch!" + exit 1 + } \ No newline at end of file diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 00000000..c01ef420 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,22 @@ +name: Run Pytest + +on: + workflow_call: + inputs: + python_version: + required: true + type: string + +jobs: + pytest: + name: Run Tests + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + - run: pip install -r dev-requirements.txt + - name: Set PYTHONPATH + run: echo "PYTHONPATH=$PWD" >> $GITHUB_ENV + - run: pytest tests/*.py diff --git a/.github/workflows/test-cli-install.yml b/.github/workflows/test-cli-install.yml new file mode 100644 index 00000000..396f164e --- /dev/null +++ b/.github/workflows/test-cli-install.yml @@ -0,0 +1,66 @@ +name: Test CLI Installation + +on: + workflow_call: + inputs: + version: + required: true + type: string + +jobs: + test-cli-install: + strategy: + matrix: + distro: ['ubuntu-latest', 'fedora-latest', 'alpine-latest', 'archlinux-latest'] + runs-on: ubuntu-latest + container: + image: ${{ matrix.distro == 'ubuntu-latest' && 'ubuntu:latest' || matrix.distro == 'fedora-latest' && 'fedora:latest' || matrix.distro == 'alpine-latest' && 'alpine:latest' || matrix.distro == 'archlinux-latest' && 'archlinux:latest' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies (Alpine) + if: matrix.distro == 'alpine-latest' + run: apk add --no-cache bash + + - name: Install dependencies (Arch Linux) + if: matrix.distro == 'archlinux-latest' + run: pacman -Sy --noconfirm bash + + - name: Run install script + run: | + chmod +x ./install.sh + ./install.sh + + - name: Check CLI version + run: | + installed_version=$(phase -v) + expected_version=${{ inputs.version }} + if [ "$installed_version" != "$expected_version" ]; then + echo "Version mismatch: Expected $expected_version, got $installed_version" + exit 1 + fi + echo "CLI version matches: $installed_version" + + test-cli-install-arm64: + runs-on: self-hosted + container: + image: python:3.11.0-bullseye + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run install script + run: | + chmod +x ./install.sh + ./install.sh + + - name: Check CLI version + run: | + installed_version=$(phase -v) + expected_version=${{ inputs.version }} + if [ "$installed_version" != "$expected_version" ]; then + echo "Version mismatch: Expected $expected_version, got $installed_version" + exit 1 + fi + echo "CLI version matches: $installed_version" diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml new file mode 100644 index 00000000..187165ff --- /dev/null +++ b/.github/workflows/version.yml @@ -0,0 +1,46 @@ +name: Validate and set version + +on: + workflow_call: + outputs: + version: + description: "Validated Phase CLI version from const.py and APKBUILD" + value: ${{ jobs.validate_version.outputs.version }} + +jobs: + validate_version: + name: Validate and Extract Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.extract_version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Extract version from const.py + id: extract_version + run: | + PHASE_CLI_VERSION=$(grep -oP '(?<=__version__ = ")[^"]*' phase_cli/utils/const.py) + echo "version=$PHASE_CLI_VERSION" >> $GITHUB_OUTPUT + # echo "$PHASE_CLI_VERSION" > ./PHASE_CLI_VERSION.txt + + - name: Extract version from APKBUILD + id: extract_apkbuild_version + run: | + APKBUILD_VERSION=$(grep -oP '(?<=pkgver=)[^\s]*' APKBUILD) + echo "apkbuild_version=$APKBUILD_VERSION" >> $GITHUB_OUTPUT + + - name: Compare versions + run: | + if [ "${{ steps.extract_version.outputs.version }}" != "${{ steps.extract_apkbuild_version.outputs.apkbuild_version }}" ]; then + echo "Version mismatch detected!" + echo "const.py version: ${{ steps.extract_version.outputs.version }}" + echo "APKBUILD version: ${{ steps.extract_apkbuild_version.outputs.apkbuild_version }}" + exit 1 + else + echo "Versions match: ${{ steps.extract_version.outputs.version }}" + fi + + # - uses: actions/upload-artifact@v4 + # with: + # name: phase-version + # path: ./PHASE_CLI_VERSION.txt \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 71617463..ad9570fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,19 @@ -FROM alpine:latest +FROM python:3.11-alpine3.19 -# Copy the phase executable -COPY ./dist/phase /usr/local/bin/phase +# Set source directory +WORKDIR /app -# Copy the _internal directory -COPY ./dist/_internal /usr/local/bin/_internal +# Copy source +COPY phase_cli ./phase_cli +COPY setup.py requirements.txt LICENSE README.md ./ -# Give execute permission to the phase binary -RUN chmod +x /usr/local/bin/phase +# Install build dependencies and the CLI +RUN apk add --no-cache --virtual .build-deps gcc musl-dev libffi-dev openssl-dev && \ + pip install --no-cache-dir . && \ + apk del .build-deps + +# CLI Entrypoint +ENTRYPOINT ["phase"] + +# Run help by default +CMD ["--help"] \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index ddfb8b63..050d512e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -8,4 +8,5 @@ pytest-mock==3.14.0 rich==13.5.2 pyyaml==6.0.1 toml==0.10.2 -python-hcl2==4.3.2 \ No newline at end of file +python-hcl2==4.3.2 +setuptools==75.1.0 \ No newline at end of file diff --git a/install.sh b/install.sh index af45c3fe..b6ddac2c 100755 --- a/install.sh +++ b/install.sh @@ -15,24 +15,92 @@ detect_os() { fi } +has_sudo_access() { + if sudo -n true 2>/dev/null; then + return 0 + else + return 1 + fi +} + +can_install_without_sudo() { + case $OS in + ubuntu|debian) + if dpkg -l >/dev/null 2>&1; then + return 0 + fi + ;; + fedora|rhel|centos) + if rpm -q rpm >/dev/null 2>&1; then + return 0 + fi + ;; + alpine) + if apk --version >/dev/null 2>&1; then + return 0 + fi + ;; + arch) + if pacman -V >/dev/null 2>&1; then + return 0 + fi + ;; + esac + return 1 +} + +prompt_sudo() { + if [ "$EUID" -ne 0 ]; then + echo "This operation requires elevated privileges. Please enter your sudo password." + sudo -v + if [ $? -ne 0 ]; then + echo "Failed to obtain sudo privileges. Exiting." + exit 1 + fi + fi +} + +install_tool() { + local TOOL=$1 + echo "Installing $TOOL..." + if [ "$EUID" -eq 0 ] || can_install_without_sudo; then + case $OS in + ubuntu|debian) + apt-get update && apt-get install -y $TOOL + ;; + fedora|rhel|centos) + yum install -y $TOOL + ;; + alpine) + apk add $TOOL + ;; + arch) + pacman -Sy --noconfirm $TOOL + ;; + esac + else + prompt_sudo + case $OS in + ubuntu|debian) + sudo apt-get update && sudo apt-get install -y $TOOL + ;; + fedora|rhel|centos) + sudo yum install -y $TOOL + ;; + alpine) + sudo apk add $TOOL + ;; + arch) + sudo pacman -Sy --noconfirm $TOOL + ;; + esac + fi +} + check_required_tools() { - for TOOL in sudo wget curl jq sha256sum unzip; do + for TOOL in wget curl jq sha256sum unzip; do if ! command -v $TOOL > /dev/null; then - echo "Installing $TOOL..." - case $OS in - ubuntu|debian) - sudo apt-get update && sudo apt-get install -y $TOOL - ;; - fedora|rhel|centos) - sudo yum install -y $TOOL - ;; - alpine) - sudo apk add $TOOL - ;; - arch) - sudo pacman -Sy --noconfirm $TOOL - ;; - esac + install_tool $TOOL fi done } @@ -71,25 +139,17 @@ verify_checksum() { done < "$checksum_file" } -has_sudo_access() { - if sudo -n true 2>/dev/null; then - return 0 - else - return 1 - fi -} - install_from_binary() { ARCH=$(uname -m) case $ARCH in x86_64) ZIP_URL="$BASE_URL/v$VERSION/phase_cli_linux_amd64_$VERSION.zip" - CHECKSUM_URL="$BASE_URL/v$VERSION/phase_cli_linux_amd64_$VERSION.sha256" + CHECKSUM_URL="$BASE_URL/v$VERSION/phase_cli_linux_amd64_$VERSION.zip.sha256" EXTRACT_DIR="Linux-binary/phase" ;; aarch64) ZIP_URL="$BASE_URL/v$VERSION/phase_cli_linux_arm64_$VERSION.zip" - CHECKSUM_URL="$BASE_URL/v$VERSION/phase_cli_linux_arm64_$VERSION.sha256" + CHECKSUM_URL="$BASE_URL/v$VERSION/phase_cli_linux_arm64_$VERSION.zip.sha256" EXTRACT_DIR="phase_cli_release_$VERSION/Linux-binary/phase" ;; *) @@ -107,12 +167,14 @@ install_from_binary() { verify_checksum "$BINARY_PATH" "$CHECKSUM_URL" chmod +x "$BINARY_PATH" - if ! has_sudo_access; then - echo "Moving items to /usr/local/bin. Please enter your sudo password or run as root." + if [ "$EUID" -eq 0 ] || can_install_without_sudo; then + mv "$BINARY_PATH" /usr/local/bin/phase + mv "$INTERNAL_DIR_PATH" /usr/local/bin/_internal + else + prompt_sudo + sudo mv "$BINARY_PATH" /usr/local/bin/phase + sudo mv "$INTERNAL_DIR_PATH" /usr/local/bin/_internal fi - - sudo mv "$BINARY_PATH" /usr/local/bin/phase - sudo mv "$INTERNAL_DIR_PATH" /usr/local/bin/_internal } install_package() { @@ -123,26 +185,43 @@ install_package() { return fi + if [ "$EUID" -ne 0 ] && ! can_install_without_sudo; then + prompt_sudo + fi + case $OS in ubuntu|debian) PACKAGE_URL="$BASE_URL/v$VERSION/phase_cli_linux_amd64_$VERSION.deb" wget_download $PACKAGE_URL $TMPDIR/phase_cli_linux_amd64_$VERSION.deb verify_checksum "$TMPDIR/phase_cli_linux_amd64_$VERSION.deb" "$PACKAGE_URL.sha256" - sudo dpkg -i $TMPDIR/phase_cli_linux_amd64_$VERSION.deb + if [ "$EUID" -eq 0 ] || can_install_without_sudo; then + dpkg -i $TMPDIR/phase_cli_linux_amd64_$VERSION.deb + else + sudo dpkg -i $TMPDIR/phase_cli_linux_amd64_$VERSION.deb + fi ;; fedora|rhel|centos) PACKAGE_URL="$BASE_URL/v$VERSION/phase_cli_linux_amd64_$VERSION.rpm" wget_download $PACKAGE_URL $TMPDIR/phase_cli_linux_amd64_$VERSION.rpm verify_checksum "$TMPDIR/phase_cli_linux_amd64_$VERSION.rpm" "$PACKAGE_URL.sha256" - sudo rpm -U $TMPDIR/phase_cli_linux_amd64_$VERSION.rpm + if [ "$EUID" -eq 0 ]; then + rpm -Uvh $TMPDIR/phase_cli_linux_amd64_$VERSION.rpm + else + echo "Installing RPM package. This may require sudo privileges." + sudo rpm -Uvh $TMPDIR/phase_cli_linux_amd64_$VERSION.rpm + fi ;; alpine) PACKAGE_URL="$BASE_URL/v$VERSION/phase_cli_linux_amd64_$VERSION.apk" wget_download $PACKAGE_URL $TMPDIR/phase_cli_linux_amd64_$VERSION.apk verify_checksum "$TMPDIR/phase_cli_linux_amd64_$VERSION.apk" "$PACKAGE_URL.sha256" - sudo apk add --allow-untrusted $TMPDIR/phase_cli_linux_amd64_$VERSION.apk + if [ "$EUID" -eq 0 ] || can_install_without_sudo; then + apk add --allow-untrusted $TMPDIR/phase_cli_linux_amd64_$VERSION.apk + else + sudo apk add --allow-untrusted $TMPDIR/phase_cli_linux_amd64_$VERSION.apk + fi ;; *) diff --git a/process_assets.py b/process_assets.py new file mode 100644 index 00000000..15adbdee --- /dev/null +++ b/process_assets.py @@ -0,0 +1,66 @@ +import os +import argparse +import hashlib +import zipfile +from pathlib import Path + +def sha256sum(filename): + h = hashlib.sha256() + with open(filename, "rb") as f: + for block in iter(lambda: f.read(4096), b""): + h.update(block) + return h.hexdigest() + +def process_artifacts(input_dir, output_dir, version): + input_dir = Path(input_dir) + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Define expected files and their source locations here + assets = [ + ('phase_cli_linux_amd64_{version}.apk', 'phase-apk'), + ('phase_cli_linux_amd64_{version}.deb', 'phase-deb'), + ('phase_cli_linux_amd64_{version}.rpm', 'phase-rpm'), + ('phase_cli_linux_amd64_{version}.zip', 'Linux-binary'), + ('phase_cli_linux_arm64_{version}.zip', 'Linux-binary-arm64'), + ('phase_cli_macos_amd64_{version}.zip', 'macOS-amd64-binary'), + ('phase_cli_macos_arm64_{version}.zip', 'macOS-arm64-binary'), + ('phase_cli_windows_amd64_{version}.zip', 'Windows-binary'), + ] + + for output_file, source_dir in assets: + output_file = output_file.format(version=version) + source_path = input_dir / source_dir + output_path = output_dir / output_file + + if source_path.is_dir(): + # If it's a directory, zip it + with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file in source_path.rglob('*'): + if file.is_file(): + zipf.write(file, file.relative_to(source_path)) + elif source_path.is_file(): + # If it's a file, just copy it + source_path.rename(output_path) + else: + print(f"Warning: Source not found for {output_file}") + continue + + # Generate SHA256 hash + hash_file = output_path.with_name(f"{output_path.name}.sha256") + hash_value = sha256sum(output_path) + hash_file.write_text(f'{hash_value} {output_file}\n') + +def main(): + parser = argparse.ArgumentParser(description='Process Phase CLI artifacts') + parser.add_argument('input_dir', help='Input directory containing artifacts') + parser.add_argument('output_dir', help='Output directory for processed artifacts') + parser.add_argument('--version', required=True, help='Version number for file names') + + args = parser.parse_args() + + process_artifacts(args.input_dir, args.output_dir, args.version) + print(f"Artifacts processed and saved to {args.output_dir}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/setup.py b/setup.py index e19af206..deb133c4 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import os from setuptools import setup, find_packages +from phase_cli.utils.const import __version__ # Read the contents of the README.md file with open('README.md', 'r') as f: @@ -9,8 +10,8 @@ with open('requirements.txt') as f: requirements = f.read().splitlines() -# Fetch version from environment variable or set to a default -version = os.environ.get('PHASE_CLI_VERSION') +# Set version +version = __version__ setup( name='phase-cli',