diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000000..50ce32c1a23 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,355 @@ +name: Compile SOFA and run Tests +on: + workflow_call: + inputs: + sofa-branch-name: + type: string + required: true + sofa-commit-sha: + type: string + required: true + pr-owner-url: + type: string + required: false + pr-branch-name: + type: string + required: false + pr-commit-sha: + type: string + required: false + preset: + type: string + required: true + python-version: + type: string + required: true + ci-depends-on: + type: string + required: false + with-all-tests: + type: boolean + required: false + default: false + force-full-build: + type: boolean + required: false + default: false + generate-binaries: + type: boolean + required: false + default: false + external-plugins: + type: string + required: false + additionnal-cmake-flags: + type: string + required: false + builder-os: + type: string + required: true + default: '["sh-ubuntu_gcc_release"]' + dash-info: + type: string + required: false + default: 'NONE' + description: 'Json scructure with three parameters {"COMMIT_HASH":"fullhsashofthecommit", }' + +jobs: + display-inputs: + runs-on: ubuntu-latest + steps: + - name: Display + shell: bash + run: | + echo "Build and test launched with following parameters:" + echo "sofa-branch-name : ${{ inputs.sofa-branch-name }}" + echo "sofa-commit-sha : ${{ inputs.sofa-commit-sha }}" + echo "pr-owner-url : ${{ inputs.pr-owner-url }}" + echo "pr-branch-name : ${{ inputs.pr-branch-name }}" + echo "pr-commit-sha : ${{ inputs.pr-commit-sha }}" + echo "preset : ${{ inputs.preset }}" + echo "python-version : ${{ inputs.python-version }}" + echo "ci-depends-on : ${{ inputs.ci-depends-on }}" + echo "with-all-tests : ${{ inputs.with-all-tests }}" + echo "force-full-build : ${{ inputs.force-full-build }}" + echo "generate-binaries : ${{ inputs.generate-binaries }}" + echo "external-plugins : ${{ inputs.external-plugins }}" + echo "additionnal-cmake-flags : ${{ inputs.additionnal-cmake-flags }}" + echo "builder-os : ${{ inputs.builder-os }}" + + build: + strategy: + matrix: + os: ${{ fromJson(inputs.builder-os) }} + runs-on: ${{ matrix.os }} + steps: + - name: Configure builder + shell: bash + run: | + ## Go to workspace given by the runner (this file is generated by the pre-build script) + WORKSPACE=$(cat $GITHUB_WORKFLOW_SHA)/${{ matrix.os }} + echo "WORKSPACE=$WORKSPACE" >> $GITHUB_ENV + + if [ ! -d $WORKSPACE ]; then + mkdir -p $WORKSPACE + fi + cd $WORKSPACE + + echo "Workspace folder for this build will be $WORKSPACE" + + ## Setup github + # Git config (needed by CMake ExternalProject) + if ! git config --get user.name; then + git config --system user.name 'SOFA Bot' > /dev/null 2>&1 || + git config --global user.name 'SOFA Bot' > /dev/null 2>&1 || + git config user.name 'SOFA Bot' > /dev/null 2>&1 || + echo "WARNING: cannot setup git config" + fi + if ! git config --get user.email; then + git config --system user.email '<>' > /dev/null 2>&1 || + git config --global user.email '<>' > /dev/null 2>&1 || + git config user.email '<>' > /dev/null 2>&1 || + echo "WARNING: cannot setup git config" + fi + + ##TODO (if required) fix path too long for windows (see main.sh:201) + + - name: Clone SOFA and CI + shell: bash + run: | + cd $WORKSPACE + + ## Clone sofa and merge origin master + echo "Cloning SOFA at commit $GITHUB_WORKFLOW_SHA" + if [ -d $WORKSPACE/sofa ]; then + rm -rf $WORKSPACE/sofa + fi + SRC_DIR=$(pwd)/sofa + echo "SRC_DIR=$SRC_DIR" >> $GITHUB_ENV + + if [ ! -z "${{ inputs.pr-owner-url }}" ]; then + echo "This is a PR, merging branch ${{ inputs.pr-branch-name }} from remote ${{ inputs.pr-owner-url }} into origin branch ${{ inputs.sofa-branch-name }}" + git clone -b ${{ inputs.sofa-branch-name }} --single-branch https://www.github.com/sofa-framework/sofa + cd sofa + git remote add pr ${{ inputs.pr-owner-url }} + git fetch pr + + if [ "${{ inputs.pr-commit-sha }}" == "HEAD" ]; then + git merge ${{ inputs.pr-branch-name }} + else + git merge ${{ inputs.pr-commit-sha }} + fi + else + echo "This is not a PR: checking out sha ${{ inputs.sofa-commit-sha }} from branch ${{ inputs.sofa-branch-name }}" + git clone -b ${{ inputs.sofa-branch-name }} --single-branch https://www.github.com/bakpaul/sofa ## TODO_before_PR_in_SOFA : REMOVE THIS WITH SOFA_FRAMEWORK @@@@@@@@@@@@@ + cd sofa + git checkout ${{ inputs.sofa-commit-sha }} + fi + + cd $WORKSPACE + + ## Clone CI and use ci-depends-on structure + ### TODO_before_PR_in_SOFA : UNCOMMENT THIS WHEN PUSHIN TO REAL SOFA + # ci_branch=${{ inputs.sofa-branch-name }} + # ci_repo_url="https://www.github.com/sofa-framework/ci" + ### AND REMOVE THOSE + ci_branch=jenkins_gha_migration + ci_repo_url="https://www.github.com/bakpaul/ci" + + + # check if ci has a ci-depends-on + ci_ci_depends_on=$(echo "${{ inputs.ci-depends-on }}" | jq .ci) + if [ -n "${{ inputs.ci-depends-on }}" ] && [ "$ci_ci_depends_on" != "null" ]; then + echo "ci-depends-on for ci repository detected." + ci_repo_url=$(echo "${{ inputs.ci-depends-on }}" | jq .ci.repo_url) + ci_branch=$(echo "${{ inputs.ci-depends-on }}" | jq .ci.branch_name) + fi + echo "CI_BRANCH=$ci_branch" >> $GITHUB_ENV + + + echo "Cloning CI from remote ${ci_repo_url}, selecting branch ${ci_branch}" + + if [ -d $WORKSPACE/ci ]; then + rm -rf $WORKSPACE/ci + fi + CI_DIR=$WORKSPACE/ci + echo "CI_DIR=$CI_DIR" >> $GITHUB_ENV + + git clone -b ${ci_branch//\"} --single-branch ${ci_repo_url//\"} + + cd $WORKSPACE + + ## Setup build folder + if [ "${{ inputs.force-full-build }}" == "true" ] && [ -d $WORKSPACE/build ]; then + rm -rf $WORKSPACE/build + fi + if [ ! -d $WORKSPACE/build ]; then + mkdir $WORKSPACE/build + fi + + BUILD_DIR=$WORKSPACE/build + echo "BUILD_DIR=$BUILD_DIR" >> $GITHUB_ENV + + if [ -n "${{ inputs.external-plugins }}" ]; then + echo "Setting up external plugins." + for plugin in ${{ inputs.external-plugins }}; do + plugin_base=${plugin%@*} + plugin_branch=${plugin##*@} + plugin_repo=$(basename "$plugin_base") + + echo "Adding line 'sofa_add_external(plugin $plugin_repo GIT_REF $plugin_branch GIT_REPOSITORY $plugin_base ON)' to file ${SRC_DIR}/applications/CMakeLists.txt" + echo "sofa_add_external(plugin $plugin_repo GIT_REF $plugin_branch GIT_REPOSITORY $plugin_base ON)" >> "${SRC_DIR}/applications/CMakeLists.txt" + done + fi + + + - name: Notify dashboard + shell: bash + run: | + . ${CI_DIR}/scripts/utils.sh + . ${CI_DIR}/scripts/dashboard.sh + + #Really necessary ? + #TODO + #Need more information. Might be good to + + - name: Build + id: build-step + continue-on-error: true + shell: bash + run: | + ## Configure the build: setup cmake variables + # retrive build type and compiler + BUILD_TYPE=$(echo ${{ matrix.os }} | awk -F '_' '{print $3}') + CONFIG=$(echo ${{ matrix.os }} | awk -F '_' '{print $1"_"$2}') + NODE_NAME="${{ runner.name }}" # Needed by configure-and-build --> TODO: be able to get hostname multiplatform + echo "NODE_NAME=$NODE_NAME" >> $GITHUB_ENV + + ## Deal with ci-depends-on + # Loop over each key-value pair in the JSON avoiding ci repository if inside + CMAKE_OPTIONS="${{inputs.additionnal-cmake-flags}}" + for key in $(echo "${{ inputs.ci-depends-on }}" | jq -r 'keys[]' | grep -v '^ci$'); do + + repo_url=$(echo "${{ inputs.ci-depends-on }}" | jq -r ".\"$key\".repo_url") + branch_name=$(echo "${{ inputs.ci-depends-on }}" | jq -r ".\"$key\".branch_name") + + # Format the CMake flags for this key and append to the result + fixed_name=$(echo "$key" | awk '{gsub(/\./, "_"); print toupper($0)}') + flag_repository="-D${fixed_name}_GIT_REPOSITORY=\"$repo_url\")" + flag_tag="-D${fixed_name}_GIT_TAG=\"$branch_name\")" + + # Append both flags to the result string with a space + CMAKE_OPTIONS="$CMAKE_OPTIONS $flag_repository $flag_tag " + done + + if [[ ! -n "$CMAKE_OPTIONS" ]]; then + CMAKE_OPTIONS="no-additionnal-cmake-flags" + echo "No ci-depends-on detected." + else + echo "Adding the following cmake variable : $CMAKE_OPTIONS" + fi + + echo "" + echo "------ Before build (and docker) ------" + echo "Main folders:" + echo " - WORKSPACE = $WORKSPACE" + echo " - SRC_DIR = $SRC_DIR" + echo " - CI_DIR = $CI_DIR" + echo " - BUILD_DIR = $BUILD_DIR" + echo "" + echo "Configuration:" + echo " - NODE_NAME = $NODE_NAME" + echo " - BUILD_TYPE = $BUILD_TYPE" + echo " - CONFIG = $CONFIG" + echo " - PYTHON_VERSION = ${{ inputs.python-version }}" + echo " - PRESET = ${{ inputs.preset }}" + echo " - GENERATE_BINARIES = ${{ inputs.generate-binaries }}" + echo " - ADDITIONNAL_CMAKE_OPTIONS = $CMAKE_OPTIONS" + echo "---------------------------------------" + echo "" + + #If os has got docker then use it + if [[ "${{ matrix.os }}" == *"ubuntu"* ]] || [[ "${{ matrix.os }}" == *"fedora"* ]]; then + builder_type=$(echo "${{ matrix.os }}" | awk -F '_' '{print $1}' | awk -F '-' '{print $2}') + if [[ "${{ inputs.sofa-branch-name }}" == "master" ]] || [[ "${{ inputs.sofa-branch-name }}" =~ ^v[0-9]{2}\.[0-9]{2}$ ]]; then + default_tag=${{ inputs.sofa-branch-name }} + else + default_tag=master + fi + + echo "dckr_pat_ohBds9gXTs9Iy91QPKJZXGdVS5s" | docker login -u sofaframework --password-stdin + + echo "Pulling Docker image sofaframework/sofabuilder_${builder_type}:${CI_BRANCH} ..." + docker pull --quiet sofaframework/sofabuilder_${builder_type}:${CI_BRANCH} || true + DOCKER_IMAGE="sofaframework/sofabuilder_${builder_type}:${CI_BRANCH}" + + if [[ "$(docker image list --format "table {{.Repository}}:{{.Tag}}")" != *"sofaframework/sofabuilder_${builder_type}:${CI_BRANCH}"* ]]; then + echo "Docker image sofaframework/sofabuilder_${builder_type}:${CI_BRANCH} doesn't exist, pulling default tag ${default_tag} " + docker pull --quiet sofaframework/sofabuilder_${builder_type}:${default_tag} + + DOCKER_IMAGE="sofaframework/sofabuilder_${builder_type}:${default_tag}" + fi + + echo "DOCKER_IMAGE=$DOCKER_IMAGE" >> $GITHUB_ENV + + + echo "Launching configuration and build through docker using image ${DOCKER_IMAGE}. Running ./ci/scripts/configure-and-build.sh /workspace/build/ /workspace/sofa/ /workspace/ci/scripts/ \"${NODE_NAME}\" \"${BUILD_TYPE}\" \"${CONFIG}\" \"${{ inputs.python-version }}\" \"${{ inputs.preset }}\" \"${{ inputs.generate-binaries }}\" \"${CMAKE_OPTIONS}\"" + docker run --rm \ + --user $(id -u):$(id -g) --network=host -v $WORKSPACE:/workspace \ + ${DOCKER_IMAGE} \ + /bin/bash -c "env ; cd /workspace; /bin/bash ./ci/scripts/configure-and-build.sh /workspace/build/ /workspace/sofa/ /workspace/ci/scripts/ \"${NODE_NAME}\" \"${BUILD_TYPE}\" \"${CONFIG}\" \"${{ inputs.python-version }}\" \"${{ inputs.preset }}\" \"${{ inputs.generate-binaries }}\" \"${CMAKE_OPTIONS}\"" + else + echo "Launching configuration and build" + + bash ${CI_DIR}/scripts/configure-and-build.sh "${BUILD_DIR}" "${SRC_DIR}" "${CI_DIR}/scripts/" "${NODE_NAME}" "${BUILD_TYPE}" "${CONFIG}" "${{ inputs.python-version }}" "${{ inputs.preset }}" "${{ inputs.generate-binaries }}" "${CMAKE_OPTIONS}" + fi + + + - name: Notify dashboard + if: always() + shell: bash + run: | + #TODO + + - name: Launch tests + continue-on-error: true + if: steps.build-step.outcome == 'success' + shell: bash + run: | + # If os has got docker then use it + # Here no need to fetch it because it has already been taken care of in the build step + if [[ "${{ matrix.os }}" == *"ubuntu"* ]] || [[ "${{ matrix.os }}" == *"fedora"* ]]; then + + echo "Launching test suite through docker using image ${DOCKER_IMAGE}. Running ./ci/scripts/test.sh /workspace/build/ /workspace/sofa/ /workspace/ci/ ${NODE_NAME} ${{ inputs.python-version }}" + docker run --rm \ + --user $(id -u):$(id -g) --network=host -v $WORKSPACE:/workspace \ + ${DOCKER_IMAGE} \ + /bin/bash -c "env ; cd /workspace; /bin/bash ./ci/scripts/test.sh /workspace/build/ /workspace/sofa/ /workspace/ci/scripts/ ${NODE_NAME} ${{ inputs.python-version }} ${{ inputs.with-all-tests }}" + else + echo "Launching test suite" + + bash ${CI_DIR}/scripts/test.sh "${BUILD_DIR}" "${SRC_DIR}" "${CI_DIR}/scripts/" "${NODE_NAME}" "${{ inputs.python-version }}" "${{ inputs.with-all-tests }}" + fi + + + - name: Publish artifacts + if: steps.build-step.outcome == 'success' && inputs.generate-binaries == true + shell: bash + run: | + #TODO + + - name: Publish build logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: build-logs + path: | + ${{ env.BUILD_DIR }}/make-output.txt + + - name: Publish tests logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: tests-logs + path: | + ${{ env.BUILD_DIR }}/tests_results/ diff --git a/.github/workflows/trigger-build-and-tests.yml b/.github/workflows/trigger-build-and-tests.yml new file mode 100644 index 00000000000..595a0261c1f --- /dev/null +++ b/.github/workflows/trigger-build-and-tests.yml @@ -0,0 +1,367 @@ +name: Trigger build and tests + +# =============================================================== +# =============================================================== + + +on: + # On-demand binary generation + workflow_dispatch: + inputs: + branch: + description: 'Specify the stable branch to use to generate the new binaries' + required: true + type: string + python_version: + description: 'Version of Python used' + required: true + default: '3.12' + type: string + external-plugins: + description: 'List of github repository and branch name to add to build tree, separated by a space. Syntax: https://www.github.com/me/myrepo@mybranch' + required: false + type: string + additionnal-cmake-flags: + description: 'CMake flags to add during the CMake call. This is to be used along external-plugins. The syntax is identical to a CMake call' + required: false + type: string + preset: + type: choice + description: Which preset to use for compilation + options: + - standard + - supported-plugins + - full + - standard-dev + - supported-plugins-dev + - full-dev + default: 'full' + builder-os: + type: choice + description: On which OS run the binaries generation + options: + - '["sh-ubuntu_gcc_release"]' + - '["sh-windows_vs2022_release"]' + - '["sh-macos_clang_release"]' + - '["sh-ubuntu_gcc_release","sh-windows_vs2022_release","sh-macos_clang_release"]' + default: '["sh-ubuntu_gcc_release","sh-windows_vs2022_release","sh-macos_clang_release"]' + + + + # Nightly build + schedule: + - cron: '0 2 * * *' # Evevery night + + # PR-related build (open, labels, push) + pull_request: + types: [opened, synchronize] + + # Comment on PR + issue_comment: + types: [created, edited] + + # CI for dashboard master + push: + branches: + - 'master' + +# =============================================================== +# =============================================================== + + +jobs: + # Filter build handling : push in master, commits in PR, comments in PR and dispatch + filter_build: + if: ${{ github.event_name != 'schedule'}} #TODO_before_PR_in_SOFA - remove + ### if: ${{ github.repository_owner == 'sofa-framework' && github.event_name != 'schedule' }} #TODO_before_PR_in_SOFA - uncomment + runs-on: ubuntu-latest + outputs: + SOFA_BRANCH_NAME: ${{ steps.export-vars.outputs.SOFA_BRANCH_NAME }} + SOFA_COMMIT_SHA: ${{ steps.export-vars.outputs.SOFA_COMMIT_SHA }} + PRESET: ${{ steps.export-vars.outputs.PRESET }} + PYTHON_VERSION: ${{ steps.export-vars.outputs.PYTHON_VERSION }} + CI_DEPENDS_ON: ${{ steps.export-vars.outputs.CI_DEPENDS_ON }} + WITH_ALL_TESTS: ${{ steps.export-vars.outputs.WITH_ALL_TESTS }} + FORCE_FULL_BUILD: ${{ steps.export-vars.outputs.FORCE_FULL_BUILD }} + EXTERNAL_PLUGINS: ${{ steps.export-vars.outputs.EXTERNAL_PLUGINS }} + ADDITIONNAL_CMAKE_FLAGS: ${{ steps.export-vars.outputs.ADDITIONNAL_CMAKE_FLAGS }} + GENERATE_BINARIES: ${{ steps.export-vars.outputs.GENERATE_BINARIES }} + PR_OWNER_URL: ${{ steps.export-vars.outputs.PR_OWNER_URL }} + PR_BRANCH_NAME: ${{ steps.export-vars.outputs.PR_BRANCH_NAME }} + PR_COMMIT_SHA: ${{ steps.export-vars.outputs.PR_COMMIT_SHA }} + BUILDER_OS: ${{ steps.export-vars.outputs.BUILDER_OS }} + + steps: + - name: Default values of environment variables + run: | + echo "SOFA_BRANCH_NAME=master" >> $GITHUB_ENV # SOFA_BRANCH_NAME: "master" + echo "SOFA_COMMIT_SHA=HEAD" >> $GITHUB_ENV # SOFA_COMMIT_SHA: "HEAD" + echo "PRESET=full" >> $GITHUB_ENV # PRESET: "full" + echo "PYTHON_VERSION=3.12" >> $GITHUB_ENV # PYTHON_VERSION: "3.12" + echo "CI_DEPENDS_ON=" >> $GITHUB_ENV # CI_DEPENDS_ON: "" + echo "WITH_ALL_TESTS=false" >> $GITHUB_ENV # WITH_ALL_TESTS: false + echo "FORCE_FULL_BUILD=false" >> $GITHUB_ENV # FORCE_FULL_BUILD: false + echo "EXTERNAL_PLUGINS=" >> $GITHUB_ENV # EXTERNAL_PLUGINS: "" + echo "ADDITIONNAL_CMAKE_FLAGS=" >> $GITHUB_ENV # ADDITIONNAL_CMAKE_FLAGS: "" + echo "GENERATE_BINARIES=false" >> $GITHUB_ENV # GENERATE_BINARIES: false + echo "PR_OWNER_URL=" >> $GITHUB_ENV # PR_OWNER_URL: "" + echo "PR_BRANCH_NAME=" >> $GITHUB_ENV # PR_BRANCH_NAME: "" + echo "PR_COMMIT_SHA=HEAD" >> $GITHUB_ENV # PR_COMMIT_SHA: "HEAD" + echo 'BUILDER_OS=["sh-ubuntu_gcc_release"]' >> $GITHUB_ENV # BUILDER_OS: ["sh-ubuntu_gcc_release"] + + - name: Run on dispatch + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo "This step runs only for binary generation." + + BRANCH=${{ github.event.inputs.branch }} + PYTHON=${{ github.event.inputs.python_version }} + + # Validate branch format (e.g., v25.06) + if [ [ ! "$BRANCH" =~ ^v[0-9]{2}\.[0-9]{2}$ ] && [ "$BRANCH" != "master" ] ]; then + echo "Error: Invalid branch name format: $BRANCH." + echo "Branch name should be either master or any release branch (e.g., v25.06)" + exit 1 + fi + echo "Branch name $BRANCH is valid." + + # Validate Python version format (e.g., 3.12) + if [[ ! "$PYTHON" =~ ^[0-9]{1}\.[0-9]{2}$ ]]; then + if [[ ! "$PYTHON" =~ ^3\.(9|1[0-8])$ ]]; then + echo "Error: Invalid Python version format: $PYTHON" + exit 1 + fi + fi + echo "Python version $PYTHON is valid." + + # Save all information in environment variables + echo "SOFA_BRANCH_NAME=${{ github.event.inputs.branch }}" >> $GITHUB_ENV + echo "SOFA_COMMIT_SHA=HEAD" >> $GITHUB_ENV + echo "PRESET=${{ github.event.inputs.preset }}" >> $GITHUB_ENV + echo "PYTHON_VERSION=${{ github.event.inputs.python_version }}" >> $GITHUB_ENV + echo "GENERATE_BINARIES=true" >> $GITHUB_ENV + echo 'BUILDER_OS=${{ github.event.inputs.builder-os }}' >> $GITHUB_ENV + echo "FORCE_FULL_BUILD=true" >> $GITHUB_ENV + echo "EXTERNAL_PLUGINS=${{ inputs.external-plugins }}" >> $GITHUB_ENV + echo "ADDITIONNAL_CMAKE_FLAGS=${{ inputs.additionnal-cmake-flags }}" >> $GITHUB_ENV + + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install pip packages + run: | + pip install python-graphql-client + pip install requests + + - name: Check out code + uses: actions/checkout@v2 + + - name: Check push on master case (e.g. merge) + if: ${{ github.event_name == 'push'}} + run: | + echo "This step runs only for push on the master branch." + echo "SOFA_COMMIT_SHA=${{ github.sha }}">> $GITHUB_ENV + echo "WITH_ALL_TESTS=true" >> $GITHUB_ENV + echo "FORCE_FULL_BUILD=true" >> $GITHUB_ENV + echo 'BUILDER_OS=["sh-ubuntu_gcc_release","sh-ubuntu_clang_release","sh-ubuntu_clang_debug","sh-fedora_clang_release","sh-windows_vs2022_release","sh-macos_clang_release"]' >> $GITHUB_ENV + + - name: Run when PR is opened or a commit is pushed in a PR + if: ${{ github.event_name == 'pull_request' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + OWNER_NAME: ${{ github.event.pull_request.head.repo.owner.login }} + PR_COMMIT_SHA: ${{ github.event.pull_request.head.sha }} + run: | + echo "This step runs only when a PR is opened or synchronized." + + # Trigger the Build action + python scripts/github_CI/checkPRInfoBeforeBuild.py + + - name: Run when PR comment is created or edited > set up environment variables + id: pr-comment-setup + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + echo "This step runs only when a PR comment is created or edited." + COMMENT_BODY=$(jq -r '.comment.body' < $GITHUB_EVENT_PATH) + echo "Comment: $COMMENT_BODY" + + # Trigger the Build action if [ci-build] is in the comment + if [[ "$COMMENT_BODY" == *"[ci-build]"* ]]; then + echo "Fetching PR #$PR_NUMBER information" + + pr_data=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/bakpaul/sofa/pulls/$PR_NUMBER") #TODO_before_PR_in_SOFA - replace with sofa-framework + + # Extract data + OWNER_NAME=$(echo "$pr_data" | jq -r '.user.login') + PR_COMMIT_SHA=$(echo "$pr_data" | jq -r '.head.sha') + + echo "- PR number : $PR_NUMBER" + echo "- PR owner name : $OWNER_NAME" + echo "- PR commit SHA : $PR_COMMIT_SHA" + + # Save to environment variables for future steps + echo "OWNER_NAME=$OWNER_NAME" >> $GITHUB_ENV + echo "PR_COMMIT_SHA=$PR_COMMIT_SHA" >> $GITHUB_ENV + + echo "Env: OWNER_NAME = $OWNER_NAME" + echo "Env: PR_COMMIT_SHA = $PR_COMMIT_SHA" + fi + + - name: Run when PR comment is created or edited > trigger python script + if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && steps.pr-comment-setup.outcome == 'success' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.issue.number }} + run: | + COMMENT_BODY=$(jq -r '.comment.body' < $GITHUB_EVENT_PATH) + echo "Comment: $COMMENT_BODY" + + # Trigger the Build action if [ci-build] is in the comment + if [[ "$COMMENT_BODY" == *"[ci-build]"* ]]; then + python scripts/github_CI/checkPRInfoBeforeBuild.py + fi + + - name: Export environment variables as outputs + id: export-vars + run: | + # Validate branch format (e.g., v25.06) + if [ [ ! "$SOFA_BRANCH_NAME" =~ ^v[0-9]{2}\.[0-9]{2}$ ] && [ "$SOFA_BRANCH_NAME" != "master" ] ]; then + echo "Error: Invalid branch name format: $SOFA_BRANCH_NAME." + echo "Branch name should be either master or any release branch (e.g., v25.06)" + exit 1 + fi + + echo "SOFA_BRANCH_NAME=${SOFA_BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "SOFA_COMMIT_SHA=${SOFA_COMMIT_SHA}" >> $GITHUB_OUTPUT + echo "PRESET=${PRESET}" >> $GITHUB_OUTPUT + echo "PYTHON_VERSION=${PYTHON_VERSION}" >> $GITHUB_OUTPUT + echo "CI_DEPENDS_ON=${CI_DEPENDS_ON}" >> $GITHUB_OUTPUT + echo "WITH_ALL_TESTS=${WITH_ALL_TESTS}" >> $GITHUB_OUTPUT + echo "FORCE_FULL_BUILD=${FORCE_FULL_BUILD}" >> $GITHUB_OUTPUT + echo "EXTERNAL_PLUGINS=${EXTERNAL_PLUGINS}" >> $GITHUB_OUTPUT + echo "ADDITIONNAL_CMAKE_FLAGS=${ADDITIONNAL_CMAKE_FLAGS}" >> $GITHUB_OUTPUT + echo "GENERATE_BINARIES=${GENERATE_BINARIES}" >> $GITHUB_OUTPUT + echo "PR_OWNER_URL=${PR_OWNER_URL}" >> $GITHUB_OUTPUT + echo "PR_BRANCH_NAME=${PR_BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "PR_COMMIT_SHA=${PR_COMMIT_SHA}" >> $GITHUB_OUTPUT + echo "BUILDER_OS=${BUILDER_OS}" >> $GITHUB_OUTPUT + + + + # Nightly build triggered once a day + nightly_build: + runs-on: ubuntu-latest + if: github.event.schedule == '0 2 * * *' #TODO_before_PR_in_SOFA - remove + ### if: ${{ github.repository_owner == 'sofa-framework' && github.event.schedule == '0 2 * * *' }} #TODO_before_PR_in_SOFA - uncomment + strategy: + matrix: + sofa_version: [master, v24.12] + outputs: + SOFA_BRANCH_NAME: ${{ steps.export-vars.outputs.SOFA_BRANCH_NAME }} + SOFA_COMMIT_SHA: ${{ steps.export-vars.outputs.SOFA_COMMIT_SHA }} + PRESET: ${{ steps.export-vars.outputs.PRESET }} + PYTHON_VERSION: ${{ steps.export-vars.outputs.PYTHON_VERSION }} + CI_DEPENDS_ON: ${{ steps.export-vars.outputs.CI_DEPENDS_ON }} + WITH_ALL_TESTS: ${{ steps.export-vars.outputs.WITH_ALL_TESTS }} + FORCE_FULL_BUILD: ${{ steps.export-vars.outputs.FORCE_FULL_BUILD }} + EXTERNAL_PLUGINS: ${{ steps.export-vars.outputs.EXTERNAL_PLUGINS }} + ADDITIONNAL_CMAKE_FLAGS: ${{ steps.export-vars.outputs.ADDITIONNAL_CMAKE_FLAGS }} + GENERATE_BINARIES: ${{ steps.export-vars.outputs.GENERATE_BINARIES }} + PR_OWNER_URL: ${{ steps.export-vars.outputs.PR_OWNER_URL }} + PR_BRANCH_NAME: ${{ steps.export-vars.outputs.PR_BRANCH_NAME }} + PR_COMMIT_SHA: ${{ steps.export-vars.outputs.PR_COMMIT_SHA }} + BUILDER_OS: ${{ steps.export-vars.outputs.BUILDER_OS }} + + steps: + - name: Default values of environment variables + run: | + echo "SOFA_BRANCH_NAME=master" >> $GITHUB_ENV # SOFA_BRANCH_NAME: "master" + echo "SOFA_COMMIT_SHA=HEAD" >> $GITHUB_ENV # SOFA_COMMIT_SHA: "HEAD" + echo "PRESET=full" >> $GITHUB_ENV # PRESET: "full" + echo "PYTHON_VERSION=3.12" >> $GITHUB_ENV # PYTHON_VERSION: "3.12" + echo "CI_DEPENDS_ON=" >> $GITHUB_ENV # CI_DEPENDS_ON: "" + echo "WITH_ALL_TESTS=false" >> $GITHUB_ENV # WITH_ALL_TESTS: false + echo "FORCE_FULL_BUILD=false" >> $GITHUB_ENV # FORCE_FULL_BUILD: false + echo "EXTERNAL_PLUGINS=" >> $GITHUB_ENV # EXTERNAL_PLUGINS: "" + echo "ADDITIONNAL_CMAKE_FLAGS=" >> $GITHUB_ENV # ADDITIONNAL_CMAKE_FLAGS: "" + echo "GENERATE_BINARIES=false" >> $GITHUB_ENV # GENERATE_BINARIES: false + echo "PR_OWNER_URL=" >> $GITHUB_ENV # PR_OWNER_URL: "" + echo "PR_BRANCH_NAME=" >> $GITHUB_ENV # PR_BRANCH_NAME: "" + echo "PR_COMMIT_SHA=HEAD" >> $GITHUB_ENV # PR_COMMIT_SHA: "HEAD" + echo 'BUILDER_OS=["sh-ubuntu_gcc_release"]' >> $GITHUB_ENV # BUILDER_OS: ["sh-ubuntu_gcc_release"] + + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install pip packages + run: | + pip install python-graphql-client + pip install requests + + - name: Check out code + uses: actions/checkout@v2 + + - name: Run when nightly + run: | + echo "This step runs only for nightly builds." + echo "SOFA_BRANCH_NAME=${{ matrix.sofa_version }}" >> $GITHUB_ENV + echo "PRESET=standard-dev" >> $GITHUB_ENV + echo "WITH_ALL_TESTS=true" >> $GITHUB_ENV + echo "GENERATE_BINARIES=true" >> $GITHUB_ENV + echo 'BUILDER_OS=["sh-ubuntu_gcc_release","sh-windows_vs2022_release","sh-macos_clang_release"]' >> $GITHUB_ENV + + - name: Export environment variables as outputs + id: export-vars + run: | + echo "SOFA_BRANCH_NAME=${SOFA_BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "SOFA_COMMIT_SHA=${SOFA_COMMIT_SHA}" >> $GITHUB_OUTPUT + echo "PRESET=${PRESET}" >> $GITHUB_OUTPUT + echo "PYTHON_VERSION=${PYTHON_VERSION}" >> $GITHUB_OUTPUT + echo "CI_DEPENDS_ON=${CI_DEPENDS_ON}" >> $GITHUB_OUTPUT + echo "WITH_ALL_TESTS=${WITH_ALL_TESTS}" >> $GITHUB_OUTPUT + echo "FORCE_FULL_BUILD=${FORCE_FULL_BUILD}" >> $GITHUB_OUTPUT + echo "EXTERNAL_PLUGINS=${EXTERNAL_PLUGINS}" >> $GITHUB_OUTPUT + echo "ADDITIONNAL_CMAKE_FLAGS=${ADDITIONNAL_CMAKE_FLAGS}" >> $GITHUB_OUTPUT + echo "GENERATE_BINARIES=${GENERATE_BINARIES}" >> $GITHUB_OUTPUT + echo "PR_OWNER_URL=${PR_OWNER_URL}" >> $GITHUB_OUTPUT + echo "PR_BRANCH_NAME=${PR_BRANCH_NAME}" >> $GITHUB_OUTPUT + echo "PR_COMMIT_SHA=${PR_COMMIT_SHA}" >> $GITHUB_OUTPUT + echo "BUILDER_OS=${BUILDER_OS}" >> $GITHUB_OUTPUT + + + + + # =============================================================== + # =============================================================== + + # Trigger the build and sharing all parameters from filter_build > outputs + call-workflow-passing-data: + needs: filter_build + ### if: ${{ github.repository_owner == 'sofa-framework' }} #TODO_before_PR_in_SOFA - uncomment + uses: bakpaul/sofa/.github/workflows/build-and-test.yml@master #TODO_before_PR_in_SOFA - replace with sofa-framework + with: + sofa-branch-name: ${{ needs.filter_build.outputs.SOFA_BRANCH_NAME }} + sofa-commit-sha: ${{ needs.filter_build.outputs.SOFA_COMMIT_SHA }} + preset: ${{ needs.filter_build.outputs.PRESET }} + python-version: ${{ needs.filter_build.outputs.PYTHON_VERSION }} + ci-depends-on: ${{ needs.filter_build.outputs.CI_DEPENDS_ON }} + with-all-tests: ${{ needs.filter_build.outputs.WITH_ALL_TESTS == 'true'}} + force-full-build: ${{ needs.filter_build.outputs.FORCE_FULL_BUILD == 'true'}} + external-plugins: ${{ needs.filter_build.outputs.EXTERNAL_PLUGINS }} + additionnal-cmake-flags: ${{ needs.filter_build.outputs.ADDITIONNAL_CMAKE_FLAGS }} + generate-binaries: ${{ needs.filter_build.outputs.GENERATE_BINARIES == 'true'}} + pr-owner-url: ${{ needs.filter_build.outputs.PR_OWNER_URL }} + pr-branch-name: ${{ needs.filter_build.outputs.PR_BRANCH_NAME }} + pr-commit-sha: ${{ needs.filter_build.outputs.PR_COMMIT_SHA }} + builder-os: ${{ needs.filter_build.outputs.BUILDER_OS }} diff --git a/scripts/github_CI/checkPRInfoBeforeBuild.py b/scripts/github_CI/checkPRInfoBeforeBuild.py new file mode 100644 index 00000000000..3305cb42def --- /dev/null +++ b/scripts/github_CI/checkPRInfoBeforeBuild.py @@ -0,0 +1,225 @@ +#!python + +import os, re, requests + +GITHUB_TOKEN = os.getenv('GITHUB_TOKEN') +PR_NUMBER = os.getenv('PR_NUMBER') +OWNER_NAME = os.getenv('OWNER_NAME') +PR_COMMIT_SHA = os.getenv('PR_COMMIT_SHA') + + +if (not GITHUB_TOKEN) or (not PR_NUMBER) or (not OWNER_NAME) or (not PR_COMMIT_SHA): + print("Error: Missing required environment variables.") + if (not GITHUB_TOKEN): + print(" - Missing GITHUB_TOKEN") + if (not PR_NUMBER): + print(" - Missing PR_NUMBER") + if (not OWNER_NAME): + print(" - Missing OWNER_NAME") + if (not PR_COMMIT_SHA): + print(" - Missing PR_COMMIT_SHA") + exit(1) + + +# GitHub API base URL +API_URL = f"https://api.github.com/repos/bakpaul/sofa" #TODO_before_PR_in_SOFA - replace with sofa-framework + + +# Headers for authentication +HEADERS = { + "Authorization": f"Bearer {GITHUB_TOKEN}", + "Accept": "application/vnd.github.v3+json" +} + + +# Flags to determine actions +to_review_label_found = False +is_draft_pr = False +with_all_tests_found = False +force_full_build_found = False +dependency_dict = {} + +# ======================================================================== + +# Check PR labels +def check_labels(): + global to_review_label_found + labels_url = f"{API_URL}/issues/{PR_NUMBER}/labels" + response = requests.get(labels_url, headers=HEADERS) + + if response.status_code != 200: + print(f"Failed to fetch labels: {response.status_code}") + exit(1) + + labels = [label['name'].lower() for label in response.json()] + print(f"Labels found: {labels}.") + + if "pr: status to review" in labels: + to_review_label_found = True + print("PR is marked as 'to review'.") + else: + print(f"Flag to review has not been found. CI will stop.") + exit(1) + + +# ======================================================================== + +# Check the PR draft status +def check_if_draft(): + global is_draft_pr + pr_url = f"{API_URL}/pulls/{PR_NUMBER}" + response = requests.get(pr_url, headers=HEADERS) + + if response.status_code != 200: + print(f"Failed to fetch pull request details: {response.status_code}") + exit(1) + + pr_data = response.json() + is_draft_pr = pr_data.get('draft', False) + + if is_draft_pr: + print("The pull request is a draft. The Bash script will not run.") + + +# ======================================================================== + +# Check PR comments for "[with-all-tests]" and "[force-full-build]" +def check_comments(): + global with_all_tests_found + global force_full_build_found + comments_url = f"{API_URL}/issues/{PR_NUMBER}/comments" + response = requests.get(comments_url, headers=HEADERS) + + if response.status_code != 200: + print(f"Failed to fetch comments: {response.status_code}") + exit(1) + + comments = [comment['body'].lower() for comment in response.json()] + + if any("[with-all-tests]" in comment for comment in comments): + with_all_tests_found = True + print("Found a comment containing 'with-all-tests'.") + if any("[force-full-build]" in comment for comment in comments): + force_full_build_found = True + print("Found a comment containing 'with-all-tests'.") + + +# ======================================================================== + +# Export all needed PR information +def export_pr_info(): + pr_url = f"{API_URL}/pulls/{PR_NUMBER}" + response = requests.get(pr_url, headers=HEADERS) + + if response.status_code != 200: + print(f"Failed to fetch pull request details: {response.status_code}") + exit(1) + + pr_data = response.json() + + pr_url = str(pr_data['user']['html_url']) + "/" + str(pr_data['base']['repo']['name']) + pr_branch_name = pr_data['head']['ref'] + pr_commit_sha = pr_data['head']['sha'] + + print("PR comes from the repository: "+str(pr_url)) + print("PR branch name is: "+str(pr_branch_name)) + print("PR commit sha is: "+str(pr_commit_sha)) + + with open(os.environ["GITHUB_ENV"], "a") as env_file: + env_file.write(f"PR_OWNER_URL={pr_url}\n") + env_file.write(f"PR_BRANCH_NAME={pr_branch_name}\n") + env_file.write(f"PR_COMMIT_SHA={pr_commit_sha}\n") + + +# ======================================================================== + +# Extract repositories from ci-depends-on +def extract_ci_depends_on(): + global dependency_dict + + pr_url = f"{API_URL}/pulls/{PR_NUMBER}" + response = requests.get(pr_url, headers=HEADERS) + + if response.status_code != 200: + print(f"Failed to fetch pull request details: {response.status_code}") + exit(1) + + pr_data = response.json() + + # Extract the PR description and look for [ci-depends-on ...] patterns + pr_body = pr_data.get("body", "") + ci_depends_on = [] + + # Search in each line for the pattern "[ci-depends-on ...]" + for line in pr_body.splitlines(): + match = re.search(r'\[ci-depends-on (.+?)\]', line) + if match: + dependency = match.group(1).strip() + ci_depends_on.append(dependency) + print(f"Found ci-depends-on dependency: {dependency}") + + # Ensure the URL is in the expected dependency format, e.g. https://github.com/sofa-framework/Sofa.Qt/pull/6 + parts = dependency.split('/') + if len(parts) != 7 or parts[0] != 'https:' or parts[1] != '' or parts[2] != 'github.com': + raise ValueError("") + print(f"Invalid URL ci-depends-on format: {dependency}") + exit(1) + + owner = parts[3] + repo = parts[4] + pull_number = parts[6] + dependency_request_url = f"https://api.github.com/repos/{owner}/{repo}/pulls/{pull_number}" + + response = requests.get(dependency_request_url, headers=HEADERS) + + if response.status_code != 200: + print(f"Failed to fetch pull request details: {response.status_code}") + exit(1) + + dependency_pr_data = response.json() + + key = dependency_pr_data['base']['repo']['name'] #Sofa.Qt + repo_url = dependency_pr_data['head']['repo']['html_url'] #https://github.com/{remote from which pr comes}/Sofa.Qt + branch_name = dependency_pr_data['head']['ref'] #my_feature_branch + + dependency_dict[key] = { + "repo_url": repo_url, + "branch_name": branch_name + } + + match = re.search(r'\[with-all-tests\]', line) + if match: + with_all_tests_found = True + + +# ======================================================================== +# Script core +# ======================================================================== + + +# Execute the checks +check_labels() +check_if_draft() + +# Trigger the build if conditions are met +if to_review_label_found and not is_draft_pr: + # Export PR information (url, name, sha) + export_pr_info() + + # Check compilation options in PR comments + check_comments() + + # Extract dependency repositories + extract_ci_depends_on() + + # Export all environment variables specific to pull-requests + with open(os.environ["GITHUB_ENV"], "a") as env_file: + env_file.write(f"WITH_ALL_TESTS={with_all_tests_found}\n") + env_file.write(f"FORCE_FULL_BUILD={force_full_build_found}\n") + + ci_depends_on_str = f"{dependency_dict}".replace("'", "\\\"") + env_file.write(f"CI_DEPENDS_ON={ci_depends_on_str}\n") + env_file.write(f'BUILDER_OS=["sh-ubuntu_gcc_release","sh-fedora_clang_release","sh-windows_vs2022_release","sh-macos_clang_release"]') + + +# ========================================================================