diff --git a/.github/actions/pre-commit/action.yml b/.github/actions/pre-commit/action.yml new file mode 100644 index 00000000..7c09f994 --- /dev/null +++ b/.github/actions/pre-commit/action.yml @@ -0,0 +1,109 @@ +name: "Pre-commit Action" +description: "Run pre-commit checks on code changes" +inputs: + config_file: + description: "Path to the pre-commit configuration file" + required: false + args: + description: "Arguments to pass to pre-commit" + required: false + default: "" + PYTHON_VERSION: + description: "Python version to use" + required: false + default: "" + ROS_WORKSPACE: + description: "Path to the ROS workspace" + required: false + default: "/opt/ros/noetic" + GITHUB_TOKEN: + description: "GitHub token to post comments on PRs" + required: true +runs: + using: "composite" + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Set up Python + if: ${{ inputs.PYTHON_VERSION != '' }} + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.PYTHON_VERSION }} + - name: Install pre-requirements + shell: bash + run: | + python3 -m pip install --upgrade pip + python3 -m pip install pre-commit + # Install apt requirements if file exists + if [[ -f ./dev-requirements.apt ]]; then + sudo apt-get update + xargs -a ./dev-requirements.apt sudo apt-get install -y + fi + + # Install pip requirements if file exists + if [[ -f ./dev-requirements.txt ]]; then + python3 -m pip install -r ./dev-requirements.txt + fi + git config --global --add safe.directory $(pwd) + - name: Get changed files + id: changed_files + shell: bash + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + # Get list of changed files in the PR + CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}) + else + # Fall back to all files if not a PR + CHANGED_FILES=$(git ls-files) + fi + echo "files<> $GITHUB_OUTPUT + echo "$CHANGED_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: Run pre-commit + shell: bash + run: | + if [[ -f ${{ inputs.ROS_WORKSPACE }}/setup.bash ]]; then + echo "Sourcing ROS workspace" + source ${{ inputs.ROS_WORKSPACE }}/setup.bash + fi + CHANGED_FILES="${{ steps.changed_files.outputs.files }}" + if [[ -z "$CHANGED_FILES" ]]; then + echo "No files changed, skipping pre-commit" + exit 0 + fi + set -o pipefail; pre-commit run ${{ inputs.args }} --files $CHANGED_FILES | tee pre-commit-report.txt + - name: Capture pre-commit output + if: ${{ github.event_name == 'pull_request' && failure()}} + id: capture_output + shell: bash + run: | + echo "report<> $GITHUB_OUTPUT + sed -n '/Failed/,$ { /Passed/d; s/\x1B\[[0-9;]*[mK]//g; p }' pre-commit-report.txt >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + - name: Publish pre-commit results to PR comment + if: ${{ failure() && github.event_name == 'pull_request' }} + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ inputs.GITHUB_TOKEN }} + repository: ${{ github.repository }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ❌ Static Analysis Failed. + + Analysis Output: + ``` + ${{ steps.capture_output.outputs.report }} + ``` + + Details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + - name: Publish pre-commit success to PR comment + if: ${{ success() && github.event_name == 'pull_request' }} + uses: peter-evans/create-or-update-comment@v5 + with: + token: ${{ inputs.GITHUB_TOKEN }} + repository: ${{ github.repository }} + issue-number: ${{ github.event.pull_request.number }} + body: | + ✅ Static Analysis Passed. Details: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/docker-workflow.yml b/.github/workflows/docker-workflow.yml index a3709f8f..adfd1bb0 100644 --- a/.github/workflows/docker-workflow.yml +++ b/.github/workflows/docker-workflow.yml @@ -22,8 +22,8 @@ on: version: required: false type: string - default: local - description: tag of image used for build step and optional push to registries + default: auto + description: version to use for the docker image, 'auto' to auto bump version on deploy deploy: required: false type: boolean @@ -98,44 +98,103 @@ on: required: false snyk_token: required: false + commit_user: + required: false + commit_mail: + required: false + commit_token: + required: false jobs: - RefreshSource: - if: ${{ inputs.deploy == 'false' }} + static-analyze: runs-on: ubuntu-22.04 steps: - - uses: docker://chinthakagodawita/autoupdate-action:v1 - continue-on-error: true + - name: Checkout + uses: actions/checkout@v5 + + - name: Hadolint ${{ inputs.docker_file }} + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ${{ inputs.docker_file }} + format: tty + failure-threshold: error + + raise-version: + runs-on: ubuntu-22.04 + outputs: + package_version: ${{ steps.get_version.outputs.version }} + branch_name: ${{ steps.branch.outputs.name }} + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Use manual version + if: ${{ inputs.version != 'auto' }} + id: manual_version + run: | + echo "version=${{ inputs.version }}" | tee -a "$GITHUB_OUTPUT" + echo "Using manual version: ${{ inputs.version }}" + + - name: Install bump-my-version + if: ${{ inputs.version == 'auto' }} + run: python3 -m pip install bump-my-version + + - name: Bump version (deploy only) + if: ${{ inputs.version == 'auto' && inputs.deploy }} + run: | + git config --global --add safe.directory "$(pwd)" + git config --global user.name "${{ secrets.commit_user }}" + git config --global user.email "${{ secrets.commit_mail }}" + bump-my-version bump patch --no-tag + + - name: Get branch name + if: ${{ inputs.version == 'auto' }} + id: branch env: - GITHUB_TOKEN: '${{ secrets.auto_commit_pwd }}' - MERGE_MSG: "Branch was auto-updated." + HEAD_REF: ${{ github.head_ref }} + REF_NAME: ${{ github.ref_name }} + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "name=${HEAD_REF}" | tee -a "$GITHUB_OUTPUT" + else + echo "name=${REF_NAME}" | tee -a "$GITHUB_OUTPUT" + fi + + - name: Get version from bump-my-version + if: ${{ inputs.version == 'auto' }} + id: auto_version + run: | + VERSION=$(bump-my-version show current_version) + echo "version=${VERSION}" | tee -a "$GITHUB_OUTPUT" + echo "Version: ${VERSION}" + + - name: Set final version + id: get_version + run: | + if [ "${{ inputs.version }}" != 'auto' ]; then + echo "version=${{ steps.manual_version.outputs.version }}" | tee -a "$GITHUB_OUTPUT" + else + echo "version=${{ steps.auto_version.outputs.version }}" | tee -a "$GITHUB_OUTPUT" + fi - build_deploy: + build: + needs: [static-analyze, raise-version] runs-on: ubuntu-22.04 + outputs: + docker_image: ${{ steps.get_image_names.outputs.GITHUB_DOCKER_IMAGE }} permissions: - contents: read # for actions/checkout to fetch code - packages: write # for gcr publishing - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + contents: read + packages: write + security-events: write + actions: read steps: - name: Checkout - uses: actions/checkout@v3 - - - name: Hadolint ${{ inputs.docker_file }} - uses: hadolint/hadolint-action@v1.6.0 - with: - dockerfile: ${{ inputs.docker_file }} - format: tty - failure-threshold: error # optional, default is info - # A space separated string of rules to ignore - # ignore: # optional - # Path to a config file - # config: # optional + uses: actions/checkout@v5 - name: Login to Private Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.registry_user }} password: ${{ secrets.registry_password }} @@ -143,7 +202,7 @@ jobs: - name: Login to Public Registry (for push permission) if: ${{ inputs.public }} - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.pub_registry_user }} password: ${{ secrets.pub_registry_password }} @@ -151,7 +210,7 @@ jobs: - name: Login to Public Github Registry (for push permission) if: ${{ inputs.public }} - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.github_registry_user }} password: ${{ secrets.github_registry_password }} @@ -160,60 +219,63 @@ jobs: - name: Compute docker image names id: get_image_names env: + VERSION: ${{ needs.raise-version.outputs.package_version }} PUBLIC_PUSH: ${{ inputs.public }} LATEST: ${{ inputs.push_latest }} run: | - PUB_DOCKER_IMAGE="" - DOCKER_IMAGES_TMP=${{ inputs.docker_registry }}/${{ inputs.docker_image }}:${{ inputs.version }} + DOCKER_IMAGES_TMP="${{ inputs.docker_registry }}/${{ inputs.docker_image }}:${VERSION}" if [ "${LATEST}" = "true" ]; then - DOCKER_IMAGES_TMP="$DOCKER_IMAGES_TMP,${{ inputs.docker_registry }}/${{ inputs.docker_image }}:latest" + DOCKER_IMAGES_TMP="${DOCKER_IMAGES_TMP},${{ inputs.docker_registry }}/${{ inputs.docker_image }}:latest" fi # If public push is needed if [ "${PUBLIC_PUSH}" = "true" ]; then - PUB_DOCKER_IMAGE_TMP="${{ inputs.public_registry }}/${{ inputs.public_image }}:${{ inputs.version }}" - GITHUB_DOCKER_IMAGE_TMP="${{ inputs.github_registry }}/${{ inputs.public_image }}:${{ inputs.version }}" - DOCKER_IMAGES_TMP="$DOCKER_IMAGES_TMP,$PUB_DOCKER_IMAGE_TMP,$GITHUB_DOCKER_IMAGE_TMP" - #echo "::set-output name=PUB_DOCKER_IMAGE::$PUB_DOCKER_IMAGE" - echo "GITHUB_DOCKER_IMAGE=$GITHUB_DOCKER_IMAGE_TMP" >> $GITHUB_OUTPUT + PUB_DOCKER_IMAGE_TMP="${{ inputs.public_registry }}/${{ inputs.public_image }}:${VERSION}" + GITHUB_DOCKER_IMAGE_TMP="${{ inputs.github_registry }}/${{ inputs.public_image }}:${VERSION}" + DOCKER_IMAGES_TMP="${DOCKER_IMAGES_TMP},${PUB_DOCKER_IMAGE_TMP},${GITHUB_DOCKER_IMAGE_TMP}" + echo "GITHUB_DOCKER_IMAGE=${GITHUB_DOCKER_IMAGE_TMP}" | tee -a "$GITHUB_OUTPUT" if [ "${LATEST}" = "true" ]; then - DOCKER_IMAGES_TMP="$DOCKER_IMAGES_TMP,${{ inputs.public_registry }}/${{ inputs.public_image }}:latest,${{ inputs.github_registry }}/${{ inputs.public_image }}:latest" + DOCKER_IMAGES_TMP="${DOCKER_IMAGES_TMP},${{ inputs.public_registry }}/${{ inputs.public_image }}:latest,${{ inputs.github_registry }}/${{ inputs.public_image }}:latest" fi fi - #echo "::set-output name=DOCKER_IMAGES::$DOCKER_IMAGES" - echo "DOCKER_IMAGES=$DOCKER_IMAGES_TMP" >> $GITHUB_OUTPUT - echo -e "Docker images to publish:\n$DOCKER_IMAGES" + echo "DOCKER_IMAGES=${DOCKER_IMAGES_TMP}" | tee -a "$GITHUB_OUTPUT" + echo -e "Docker images to publish:\n${DOCKER_IMAGES_TMP}" - name: Compute docker build arguments id: get_build_args run: | BUILD_ARGS_STR_LIST="${{ inputs.build_args }}" - if [ -n "$BUILD_ARGS_STR_LIST" ]; then - #echo "::set-output name=build_args_str_list::$BUILD_ARGS_STR_LIST" - echo "build_args_str_list=$BUILD_ARGS_STR_LIST" >> $GITHUB_OUTPUT - echo -e "Docker build arguments:\n$BUILD_ARGS_STR_LIST" + if [ -n "${BUILD_ARGS_STR_LIST}" ]; then + echo "build_args_str_list=${BUILD_ARGS_STR_LIST}" | tee -a "$GITHUB_OUTPUT" + echo -e "Docker build arguments:\n${BUILD_ARGS_STR_LIST}" fi - name: Compute docker label and outputs arguments id: get_label_and_outputs_args + env: + VERSION: ${{ needs.raise-version.outputs.package_version }} run: | LABEL_SRC="org.opencontainers.image.source=https://github.com/${{ github.repository }}" + LABEL_VERSION="org.opencontainers.image.version=${VERSION}" LABEL_DESCRIPTION="" OUTPUTS_ARGS_STR_LIST="" IFS=', ' read -r -a platforms_array <<< "${{ inputs.platforms }}" - if [ ${#platforms_array[@]} -gt 1 ]; then - OUTPUTS_ARGS_STR_LIST="type=image,name=target,annotation-index.org.opencontainers.image.description=Version ${{ inputs.version }} of repo ${{ github.repository }}" + if [ "${#platforms_array[@]}" -gt 1 ]; then + OUTPUTS_ARGS_STR_LIST="type=image,name=target,annotation-index.org.opencontainers.image.description=Version ${VERSION} of repo ${{ github.repository }}" else - LABEL_DESCRIPTION="org.opencontainers.image.description=Version ${{ inputs.version }} of repo ${{ github.repository }}" + LABEL_DESCRIPTION="org.opencontainers.image.description=Version ${VERSION} of repo ${{ github.repository }}" fi - echo "label_description=$LABEL_DESCRIPTION" >> $GITHUB_OUTPUT - echo "label_source=$LABEL_SRC" >> $GITHUB_OUTPUT - echo "outputs_args_str_list=$OUTPUTS_ARGS_STR_LIST" >> $GITHUB_OUTPUT + { + echo "label_description=${LABEL_DESCRIPTION}" + echo "label_source=${LABEL_SRC}" + echo "label_version=${LABEL_VERSION}" + echo "outputs_args_str_list=${OUTPUTS_ARGS_STR_LIST}" + } >> "$GITHUB_OUTPUT" - name: Download packages to include in build if: inputs.download_artifact @@ -224,15 +286,14 @@ jobs: - name: Set up QEMU for multi platform if: ${{ contains(inputs.platforms, 'arm') }} - uses: docker/setup-qemu-action@v2.2.0 - # https://github.com/docker/setup-buildx-action + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build with args and push:${{ inputs.deploy }} if: ${{ steps.get_build_args.outputs.build_args_str_list != '' }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: . platforms: ${{ inputs.platforms }} @@ -241,6 +302,7 @@ jobs: tags: ${{ steps.get_image_names.outputs.DOCKER_IMAGES }} labels: | ${{ steps.get_label_and_outputs_args.outputs.label_source }} + ${{ steps.get_label_and_outputs_args.outputs.label_version }} ${{ steps.get_label_and_outputs_args.outputs.label_description }} outputs: ${{ steps.get_label_and_outputs_args.outputs.outputs_args_str_list }} target: ${{ inputs.target }} @@ -249,7 +311,7 @@ jobs: - name: Build and push:${{ inputs.deploy }} if: ${{ steps.get_build_args.outputs.build_args_str_list == '' }} - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v6 with: context: . platforms: ${{ inputs.platforms }} @@ -258,13 +320,25 @@ jobs: tags: ${{ steps.get_image_names.outputs.DOCKER_IMAGES }} labels: | ${{ steps.get_label_and_outputs_args.outputs.label_source }} + ${{ steps.get_label_and_outputs_args.outputs.label_version }} ${{ steps.get_label_and_outputs_args.outputs.label_description }} outputs: ${{ steps.get_label_and_outputs_args.outputs.outputs_args_str_list }} target: ${{ inputs.target }} pull: true - - name: Snyk check - if: inputs.public && inputs.snyk_check && inputs.deploy + test: + needs: [raise-version, build] + if: ${{ inputs.public && inputs.snyk_check && inputs.deploy }} + runs-on: ubuntu-22.04 + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Snyk security scan + id: snyk uses: snyk/actions/docker@master continue-on-error: true timeout-minutes: 10 @@ -272,11 +346,49 @@ jobs: SNYK_TOKEN: ${{ secrets.snyk_token }} with: args: --severity-threshold=high --file=${{ inputs.docker_file }} - image: ${{ steps.get_image_names.outputs.GITHUB_DOCKER_IMAGE }} + image: ${{ needs.build.outputs.docker_image }} - - name: Snyk upload result to GitHub Code Scanning - if: inputs.public && inputs.snyk_check && inputs.deploy + - name: Upload Snyk results to GitHub Code Scanning uses: github/codeql-action/upload-sarif@v2 continue-on-error: true with: sarif_file: snyk.sarif + + - name: Login to registry on Snyk failure + if: ${{ steps.snyk.outcome == 'failure' }} + uses: docker/login-action@v3 + with: + username: ${{ secrets.github_registry_user }} + password: ${{ secrets.github_registry_password }} + registry: ${{ inputs.github_registry }} + + - name: Run Docker image on Snyk failure + if: ${{ steps.snyk.outcome == 'failure' }} + run: | + echo "Snyk scan failed - running Docker image for validation..." + docker run --rm ${{ needs.build.outputs.docker_image }} --version || echo "Container smoke test completed" + + publish: + needs: [raise-version, build, test] + if: ${{ inputs.version == 'auto' && inputs.deploy && always() }} + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install bump-my-version + run: python3 -m pip install bump-my-version + + - name: Bump version + run: | + git config --global --add safe.directory "$(pwd)" + git config --global user.name "${{ secrets.commit_user }}" + git config --global user.email "${{ secrets.commit_mail }}" + bump-my-version bump patch --no-tag + + - name: Push version bump + uses: CasperWA/push-protected@v2 + with: + token: ${{ secrets.commit_token }} + branch: ${{ needs.raise-version.outputs.branch_name }} + unprotect_reviews: true diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..77d93d24 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,19 @@ +name: Pre-commit checks + +on: + pull_request: + branches: + - 'v*' + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Run pre-commit checks + uses: MOV-AI/.github/.github/actions/pre-commit@DP-963_docker-image + with: + PYTHON_VERSION: '3.11' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index a42e51c2..f497a10b 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,11 @@ The images are built for the following platforms: - GitHub runner for Ubuntu 22.04 ## - name: Build Docker images -This workflow builds Docker images for CICD usage. +This workflow builds Docker images for CICD usage. The images are built and tested but not published to GitHub releases or artifacts, instead the images are stored in our private Docker registry and deployed to the CICD environment. -The images are built and tested but not published to GitHub releases or artifacts, instead the images are stored in our private Docker registry and deployed to the CICD environment. +The workflow also supports automatic version management using bump-my-version to enable multi-release line support. When the version input is omitted the workflow reads and manages versions from a .bumpversion.toml file in the target repository. + +On deployment it automatically bumps the patch version, commits the change, and pushes it back to the branch. The workflow also adds OCI-compliant docker labels including version, source, and description to all images. Manual version input can still be specified to bypass all automatic version management. ## - name: Build and pack FE packages This workflow builds and packs the frontend packages for the MOV.AI platform.