name: Pytest CI Workflow description: Run pytest with coverage and report upload to Gitea inputs: ci-branch: description: 'Target branch where test reports will be committed' required: true module-dir: description: 'Directory (folder) containing the Python module to test' required: true gitea-domain: description: 'Domain name of your Gitea instance without protocol (e.g., gitea.psi.ch)' required: true repo-path: description: 'Relative path to the root of your repository' required: true ci-token: description: ' CI authentication token (used for pushing to Gitea)' required: true enable_pixi_post_link_scripts: description: 'Set to true to enable post-link scripts for Pixi' required: false default: 'false' apt_packages: description: "List of system packages to install via apt-get" required: false default: "" custom_shell_commands: description: "Additional shell commands to run" required: false default: "" custom_shell_commands_after: description: "Additional shell commands to run" required: false default: "" test-files: description: "Comma-separated list of test files or directories to run" required: false default: "" env: description: "Environment type: pixi (default), pip, conda, or custom" required: false default: "pixi" env-setup-cmd: description: "Custom setup command(s) to prepare the environment (only used if env=custom)" required: false default: "" session-wrapper: description: "Wrapper de session (ex: dbus-run-session --)" required: false default: "" test-run-cmd: description: "Wrapper command to run pytest (e.g. 'dbus-run-session -- bash -c', 'tmux new-session ...'). If empty, pytest is run directly." required: false default: "$CMD" runs: using: "composite" steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 1 - name: Install optional apt packages if: inputs.apt_packages != '' run: | OUTPUT_DIR="outputs" LOG_FILE="${OUTPUT_DIR}/sudo-install.log" VERSIONS_FILE="${OUTPUT_DIR}/sudo-versions.log" mkdir -p "$OUTPUT_DIR" git clone -q https://gitea.psi.ch/tligui_y/test-ci.git ./temp-ci echo "⚙️ Starting APT package installation..." | tee -a "$LOG_FILE" echo " Packages: ${{ inputs.apt_packages }}" | tee -a "$LOG_FILE" # Run install, capture everything into the log sudo DEBIAN_FRONTEND=noninteractive apt-get update &>> "$LOG_FILE" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ${{ inputs.apt_packages }} &>> "$LOG_FILE" EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then echo "✅ Packages installed successfully!" | tee -a "$LOG_FILE" echo "📋 Installed APT packages (full list):" >> "$VERSIONS_FILE" dpkg -l >> "$VERSIONS_FILE" echo "Exit Code: 0" >> "$LOG_FILE" echo "Exit Code: 0" >> "$VERSIONS_FILE" else echo "❌ Package installation failed" | tee -a "$LOG_FILE" echo "Exit Code: $EXIT_CODE" >> "$LOG_FILE" exit $EXIT_CODE fi shell: bash - name: Setup environment shell: bash run: | OUTPUT_DIR="outputs" LOG_FILE="${OUTPUT_DIR}/env-setup.log" VERSIONS_FILE="${OUTPUT_DIR}/env-versions.log" case "${{ inputs.env }}" in pixi) echo "🔧 Installing Pixi..." | tee -a $LOG_FILE if [ "${{ inputs.enable_pixi_post_link_scripts }}" == "true" ]; then pixi config set --local run-post-link-scripts insecure fi curl -fsSL https://pixi.sh/install.sh | bash >> $LOG_FILE 2>&1 || { echo "❌ Pixi download failed" | tee -a $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "$HOME/.pixi/bin" >> $GITHUB_PATH export PATH="$HOME/.pixi/bin:$PATH" if [ ! -f "pixi.toml" ]; then echo "❌ Missing pixi.toml at repo root" | tee -a $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 fi echo "📦 Installing dependencies from pixi.toml..." | tee -a $LOG_FILE pixi install >> $LOG_FILE 2>&1 || { echo "❌ Pixi project dependencies failed" | tee -a $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "📋 Pixi environment details:" | tee -a $VERSIONS_FILE pixi list >> $VERSIONS_FILE 2>&1 echo "✅ Pixi environment ready" | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE echo "Exit Code: 0" >> "$VERSIONS_FILE" ;; pip) echo "📦 Installing pip dependencies..." | tee -a $LOG_FILE python -m pip install --upgrade pip >> $LOG_FILE 2>&1 if [ -f "requirements.txt" ]; then pip install -r requirements.txt >> $LOG_FILE 2>&1 || { echo "❌ Pip install failed" | tee -a $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "📋 Pip environment details:" | tee -a $VERSIONS_FILE pip freeze >> $VERSIONS_FILE 2>&1 echo "✅ Pip environment ready" | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE echo "Exit Code: 0" >> "$VERSIONS_FILE" else echo "⚠️ No requirements.txt found, skipping." | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE fi ;; conda) echo "📦 Setting up conda environment..." | tee -a $LOG_FILE if [ -f "environment.yml" ]; then conda env create -f environment.yml >> $LOG_FILE 2>&1 || { echo "❌ Conda env create failed" | tee -a $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "📋 Conda environment details:" | tee -a $VERSIONS_FILE conda list >> $VERSIONS_FILE 2>&1 echo "✅ Conda environment ready" | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE echo "Exit Code: 0" >> "$VERSIONS_FILE" else echo "⚠️ No environment.yml found, skipping." | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE fi ;; custom) echo "⚙️ Running custom environment setup..." | tee -a $LOG_FILE if [ -n "${{ inputs.env-setup-cmd }}" ]; then bash -c "${{ inputs.env-setup-cmd }}" >> $LOG_FILE 2>&1 || { echo "❌ Custom env setup failed" | tee -a $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "📋 Custom environment details (uname + python + pip list):" | tee -a $VERSIONS_FILE uname -a >> $VERSIONS_FILE python --version >> $VERSIONS_FILE 2>&1 || true pip freeze >> $VERSIONS_FILE 2>&1 || true echo "✅ Custom environment ready" | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE echo "Exit Code: 0" >> "$VERSIONS_FILE" else echo "⚠️ No custom setup command provided." | tee -a $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE fi ;; esac - name: Run custom shell commands before shell: bash run: | OUTPUT_DIR="outputs" LOG_FILE="${OUTPUT_DIR}/custom-shell-before.log" mkdir -p "$OUTPUT_DIR" if [ -z "$(echo "${{ inputs.custom_shell_commands }}" | xargs)" ]; then echo "💡 No custom shell commands found" | tee -a "$LOG_FILE" echo "Exit Code: 0" >> "$LOG_FILE" exit 0 fi echo "📜 Executing custom shell commands..." | tee -a "$LOG_FILE" echo "Writing to file" export LOG_FILE="$LOG_FILE" { echo "set -e" echo 'trap "{ EXIT_CODE=\$?; if [ \$EXIT_CODE -eq 0 ]; then echo Exit Code: 0 >> \"$LOG_FILE\"; else echo Exit Code: 1 >> \"$LOG_FILE\"; fi; } 2>/dev/null" EXIT' echo "set -x" echo "${{ inputs.custom_shell_commands }}" } > custom_commands.sh echo "chmod to file" chmod +x custom_commands.sh echo "Execute file" ./custom_commands.sh >> "$LOG_FILE" 2>&1 EXIT_CODE=$? echo "Finished file" if [ $EXIT_CODE -eq 0 ]; then echo "✅ Custom shell executed successfully" | tee -a "$LOG_FILE" echo "Exit Code: 0" >> "$LOG_FILE" else echo "❌ Custom shell execution failed" | tee -a "$LOG_FILE" exit 1 fi - name: Run tests and generate reports shell: bash run: | OUTPUT_DIR="outputs" LOG_FILE="${OUTPUT_DIR}/test-ci.log" export LOG_FILE echo "⚡ Running tests and generating reports..." | tee -a "$LOG_FILE" cp temp-ci/conftest.py ./ if [ -n "${{ inputs.test-files }}" ]; then TEST_PATH="${{ inputs.test-files }}" else TEST_PATH="." fi PYTEST_ARGS="--continue-on-collection-errors \ --cov=${{ inputs.module-dir }} \ --json-report \ --json-report-file=markdown/pytest-report.json \ --capture=no \ --log-file=outputs/pytest-output.log" CMD="pytest -s $TEST_PATH $PYTEST_ARGS" case "${{ inputs.env }}" in pixi) CMD="pixi run $CMD" ;; conda) CMD="conda run -n myenv $CMD" ;; pip|custom) ;; esac exec >>"$LOG_FILE" 2>&1 echo "⚡ Running tests and generating reports..." cat > custom_cmd.sh <<'EOS' ${{ inputs.test-run-cmd }} EOS chmod +x custom_cmd.sh export CMD ./custom_cmd.sh 2>&1 echo "▶️ END" awk '/=+ test session starts =+/{flag=1} /-+ JSON report -+/{flag=0} flag' outputs/pytest-output.log > outputs/raw-test-output.log awk '/=+ short test summary info =+/,/=+.* in .*s =+/' outputs/pytest-output.log > outputs/short-test-output.log set -e echo "" >> $LOG_FILE echo "📋 Short test summary:" >> "$LOG_FILE" if [ -s outputs/short-test-output.log ]; then cat outputs/short-test-output.log >> "$LOG_FILE" else echo "✅ All tests passed, nothing to report." >> "$LOG_FILE" fi echo "print pytest" cat outputs/pytest-output.log mkdir -p markdown echo "" >> $LOG_FILE echo "📈 Generating coverage summary..." >> $LOG_FILE # Combine all .coverage.* into one pixi run coverage combine > /dev/null 2>> outputs/coverage-error.log # Generate markdown report once pixi run coverage report --skip-covered --skip-empty --format=markdown > markdown/coverage-summary.md 2>> outputs/coverage-error.log && { echo "✅ Coverage summary generated" >> $LOG_FILE } || { echo "❌ Coverage summary generation failed" >> $LOG_FILE echo "🪵 Extracting coverage error:" >> $LOG_FILE cat outputs/coverage-error.log >> $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } if [ -f markdown/runtime_params.log ]; then echo "✅ [INFO] File 'runtime_params.log' found in project root." ls -lh markdown/runtime_params.log echo "Content of runtime_params.log:" cat runtime_params.log else echo "❌ [WARNING] File 'runtime_params.log' NOT FOUND in project root: $(pwd)" echo "Files in current directory:" ls -lh fi echo "" >> $LOG_FILE echo "📊 Generating tests markdown summary..." >> $LOG_FILE pixi run python temp-ci/json_to_md.py \ --input "markdown/pytest-report.json" \ --output "markdown/TEST-REPORT.md" \ --log_long "outputs/raw-test-output.log" \ --log_short "outputs/short-test-output.log" \ --params "markdown/runtime_params.json" \ >> "outputs/md-report.log" 2>&1 && { echo "✅ Tests markdown generated" >> $LOG_FILE } || { echo "❌ Tests markdown generation failed" >> $LOG_FILE echo "🪵 Extracting markdown generation error:" >> $LOG_FILE cat outputs/md-report.log >> $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "" >> $LOG_FILE echo "🌳 Generating JSON tree view..." >> $LOG_FILE pixi run python temp-ci/json_to_tree.py \ "markdown/pytest-report.json" \ >> "outputs/json-tree-gen.log" 2>&1 && { echo "✅ JSON tree view generated" >> $LOG_FILE } || { echo "❌ JSON tree generation failed" >> $LOG_FILE echo "🪵 Extracting json file error:" >> $LOG_FILE cat outputs/json-tree-gen.log >> $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } rm -rf ./temp-test-ci echo "Exit Code: 0" >> $LOG_FILE - name: Run custom shell commands after if: inputs.custom_shell_commands_after != '' shell: bash run: | OUTPUT_DIR="outputs" LOG_FILE="${OUTPUT_DIR}/custom-shell_after.log" mkdir -p "$OUTPUT_DIR" echo "📜 Executing custom shell commands..." echo "${{ inputs.custom_shell_commands_after }}" >> custom_commands_bis.sh chmod +x custom_commands_bis.sh # bash -e ./custom_commands_bis.sh >> $LOG_FILE 2>&1 ./custom_commands_bis.sh >> $LOG_FILE 2>&1 cat $LOG_FILE echo "✅ Custom shell executed" >> $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE - name: Commit and push reports shell: bash run: | echo "📤 Committing test reports..." git config user.name "ci-bot" git config user.email "ci-bot@example.com" git add markdown/ git commit -m "CI: update test report and coverage files" || echo "⚠️ Nothing to commit" echo "🚀 Pushing to branch '${{ inputs.ci-branch }}'..." git push https://ci-token:${{ inputs.ci-token }}@${{ inputs.gitea-domain }}/${{ inputs.repo-path }}.git HEAD:${{ inputs.ci-branch }} > push.log 2>&1 || { echo "❌ Push failed" cat push.log exit 1 } echo "✅ Reports pushed" - name: Clone Gitea Wiki shell: bash env: CI_TOKEN: ${{ inputs.ci-token }} run: | REPO_NAME="${{ github.repository }}" DOMAIN="${{ inputs.gitea-domain }}" WIKI_REPO="https://ci-token:${CI_TOKEN}@${DOMAIN}/${REPO_NAME}.wiki.git" OUTPUT_DIR="outputs" LOG_FILE="${OUTPUT_DIR}/wiki-clone.log" mkdir -p "$OUTPUT_DIR" echo "📚 Cloning wiki repo: $WIKI_REPO" >> $LOG_FILE git clone "$WIKI_REPO" wiki >> wiki_clone.log 2>&1 && { echo "✅ Wiki cloned to ./wiki" >> $LOG_FILE } || { echo "❌ Wiki clone failed" >> $LOG_FILE cat wiki_clone.log >> $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } SHORT_SHA=$(git rev-parse --short HEAD) REPO_URL="https://${{ inputs.gitea-domain }}/${{ github.repository }}" RUN_LINK="${REPO_URL}/actions/runs/${GITHUB_RUN_NUMBER}" echo "🌳 Generating wiki reports..." >> $LOG_FILE TEST_FILE="run-${GITHUB_RUN_NUMBER}-TEST-commit-${SHORT_SHA}.md" COVERAGE_FILE="run-${GITHUB_RUN_NUMBER}-COVERAGE-commit-${SHORT_SHA}.md" echo "## Test Report" > "wiki/${TEST_FILE}" echo "[View CI Run ${GITHUB_RUN_NUMBER}](${RUN_LINK}) | [Commit ${SHORT_SHA}](${REPO_URL}/commit/${SHORT_SHA})" >> "wiki/${TEST_FILE}" cat markdown/TEST-REPORT.md >> "wiki/${TEST_FILE}" echo "## Coverage Report" > "wiki/${COVERAGE_FILE}" echo "[View CI Run ${GITHUB_RUN_NUMBER}](${RUN_LINK}) | [Commit ${SHORT_SHA}](${REPO_URL}/commit/${SHORT_SHA})" >> "wiki/${COVERAGE_FILE}" cat markdown/coverage-summary.md >> "wiki/${COVERAGE_FILE}" HOME_FILE="wiki/Home.md" TEMP_CONTENT=$(mktemp) { echo "# Test Reports History" echo "" echo "## Latest Run" echo "- [run ${GITHUB_RUN_NUMBER} TEST commit ${SHORT_SHA}](${TEST_FILE})" echo "- [run ${GITHUB_RUN_NUMBER} COVERAGE commit ${SHORT_SHA}](${COVERAGE_FILE})" echo "" echo "## Previous Runs" if [[ -f "${HOME_FILE}" && -s "${HOME_FILE}" ]]; then sed -n '/## Latest Run/{n;p;n;p}' "${HOME_FILE}" sed -n '/## Previous Runs/,$p' "${HOME_FILE}" | tail -n +2 | grep -v "^#" || true fi } > "${TEMP_CONTENT}" 2> reports.log 2>&1 && { echo "✅ Wiki report preparation succeeded" >> $LOG_FILE } || { echo "❌ Wiki report preparation failed" >> $LOG_FILE cat reports.log >> $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } mv "${TEMP_CONTENT}" "${HOME_FILE}" echo "Exit Code: 0" >> $LOG_FILE - name: Commit and push wiki shell: bash env: CI_TOKEN: ${{ inputs.ci-token }} run: | OUTPUT_DIR="$(pwd)/outputs" LOG_FILE="${OUTPUT_DIR}/commit-push-wiki.log" mkdir -p "$OUTPUT_DIR" echo "🚀 Pushing wiki changes..." >> $LOG_FILE cd wiki git config user.name "ci-bot" git config user.email "ci-bot@example.com" git add . if git commit -m "Update wiki with run ${GITHUB_RUN_NUMBER}"; then REPO_NAME="${{ github.repository }}" DOMAIN="${{ inputs.gitea-domain }}" WIKI_REPO="https://ci-token:${CI_TOKEN}@${DOMAIN}/${REPO_NAME}.wiki.git" git push "$WIKI_REPO" HEAD:main >> git.log 2>&1 || { echo "❌ Push failed: Unable to push wiki changes" >> $LOG_FILE cat git.log >> $LOG_FILE echo "Exit Code: 1" >> $LOG_FILE exit 1 } echo "✅ Wiki updated successfully" >> $LOG_FILE else echo "⚠️ No changes to commit" >> $LOG_FILE fi echo "Exit Code: 0" >> $LOG_FILE - name: Log wiki report URLs shell: bash run: | OUTPUT_DIR="outputs" LOG_FILE=""$OUTPUT_DIR"/wiki-report-urls.log" mkdir -p "$OUTPUT_DIR" REPO_URL="https://${{ inputs.gitea-domain }}/${{ github.repository }}" SHORT_SHA=$(git rev-parse --short HEAD) echo "🔗 Wiki Test Report URL:" >> $LOG_FILE echo " ${REPO_URL}/wiki/run-${GITHUB_RUN_NUMBER}-TEST-commit-${SHORT_SHA}.md" >> $LOG_FILE echo "🔗 Wiki Coverage Report URL:" >> $LOG_FILE echo " ${REPO_URL}/wiki/run-${GITHUB_RUN_NUMBER}-COVERAGE-commit-${SHORT_SHA}.md" >> $LOG_FILE echo "Exit Code: 0" >> $LOG_FILE