Now handles correctly detection of only changed notebooks #256
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Generate Students Notebooks branch | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: ['**.ipynb'] | ||
| workflow_dispatch: | ||
| defaults: | ||
| run: | ||
| shell: bash | ||
| permissions: | ||
| contents: write | ||
| actions: write | ||
| jobs: | ||
| process: | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| CONFIG_PATH: .github/conversion.json | ||
| SUMMARY_FILE: /tmp/README.md | ||
| steps: | ||
| - name: Install Python dependencies | ||
| run: pip install chardet | ||
| - name: Initialize summary file | ||
| run: | | ||
| rm -f "$SUMMARY_FILE" | ||
| mkdir -p "$(dirname "$SUMMARY_FILE")" | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Validate JSON config | ||
| run: | | ||
| # Syntax check with jq, output to summary if error | ||
| if ! jq empty "$CONFIG_PATH" >/dev/null 2>&1; then | ||
| printf "## ❌ Error in JSON configuration file\n" >> $SUMMARY_FILE | ||
| printf "Please fix $CONFIG_PATH error:\n" >> $SUMMARY_FILE | ||
| jq empty "$CONFIG_PATH" 2>&1 | sed 's/^jq: //' >> $SUMMARY_FILE | ||
| exit 1 | ||
| fi | ||
| # Install Ajv locally (no global install) | ||
| npm install ajv@8 | ||
| # Validate with embedded schema, output errors to summary | ||
| node - <<'EOF' | ||
| const fs = require('fs'); | ||
| const Ajv = require('ajv').default; | ||
| const summaryFile = process.env.SUMMARY_FILE || '/tmp/README.md'; | ||
| const configPath = process.env.CONFIG_PATH || '.github/conversion.json'; | ||
| const schema = { | ||
| type: 'object', | ||
| required: [ | ||
| 'solution_marker', | ||
| 'placeholder', | ||
| 'generate_zip', | ||
| 'notebooks_dir', | ||
| 'student_postfix', | ||
| 'tutor_postfix' | ||
| ], | ||
| properties: { | ||
| solution_marker: { | ||
| type: 'object', | ||
| required: ['code', 'markdown'], | ||
| properties: { | ||
| code: {type: 'string'}, | ||
| markdown: {type: 'string'} | ||
| }, | ||
| additionalProperties: false | ||
| }, | ||
| placeholder: { | ||
| type: 'object', | ||
| required: ['code', 'markdown'], | ||
| properties: { | ||
| code: {type: 'string'}, | ||
| markdown: {type: 'string'} | ||
| }, | ||
| additionalProperties: false | ||
| }, | ||
| generate_zip: {type: 'boolean'}, | ||
| notebooks_dir: { | ||
| type: 'array', | ||
| items: {type: 'string'}, | ||
| minItems: 1 | ||
| }, | ||
| pre_processing: {type: 'string'}, | ||
| post_processing: {type: 'string'}, | ||
| tutor_postfix: {type: 'string'}, | ||
| student_postfix: {type: 'string'}, | ||
| rebuild_all: {type: 'boolean'} | ||
| } | ||
| }; | ||
| try { | ||
| const config = JSON.parse(fs.readFileSync(configPath)); | ||
| const ajv = new Ajv({allErrors: true, strict: true}); | ||
| const validate = ajv.compile(schema); | ||
| if (!validate(config)) { | ||
| const lines = []; | ||
| lines.push("## ❌ Error in JSON configuration file"); | ||
| lines.push("Config validation failed:"); | ||
| lines.push("```"); | ||
| validate.errors.forEach(err => { | ||
| lines.push(`- ${err.instancePath || '/'} ${err.message}`); | ||
| }); | ||
| lines.push("```"); | ||
| fs.appendFileSync(summaryFile, lines.join('\n') + '\n'); | ||
| process.exit(1); | ||
| } | ||
| } catch (error) { | ||
| const lines = []; | ||
| lines.push("## ❌ Error in JSON configuration file"); | ||
| lines.push("Parsing or validation failed:"); | ||
| lines.push("```"); | ||
| lines.push(error.message); | ||
| lines.push("```"); | ||
| fs.appendFileSync(summaryFile, lines.join('\n') + '\n'); | ||
| process.exit(1); | ||
| } | ||
| EOF | ||
| - name: Check monitored directories existence | ||
| run: | | ||
| EXIT_WITH_ERROR=false | ||
| ERROR_HEADER_CREATED=false | ||
| while IFS= read -r dir; do | ||
| if [ ! -d "$dir" ]; then | ||
| if [ "$ERROR_HEADER_CREATED" = false ]; then | ||
| printf "## ❌ Error: Missing notebook directories\n" >> $SUMMARY_FILE | ||
| printf "The following directories specified in $CONFIG_PATH don't exist:\n" >> $SUMMARY_FILE | ||
| ERROR_HEADER_CREATED=true | ||
| fi | ||
| printf -- "* \`\`\`%s\`\`\`\n" "$dir" >> $SUMMARY_FILE | ||
| EXIT_WITH_ERROR=true | ||
| fi | ||
| done < <(jq -r '.notebooks_dir[]' "$CONFIG_PATH") | ||
| if [ "$EXIT_WITH_ERROR" = true ]; then | ||
| printf "\nPlease create these directories or update the configuration file.\n" >> $SUMMARY_FILE | ||
| exit 1 | ||
| fi | ||
| - name: Store pre and post-processing command for future execution | ||
| run: | | ||
| echo "PRE_PROCESSING=$(jq -r '.pre_processing // empty' "$CONFIG_PATH")" >> $GITHUB_ENV | ||
| echo "POST_PROCESSING=$(jq -r '.post_processing // empty' "$CONFIG_PATH")" >> $GITHUB_ENV | ||
| - name: Check pre/post-processing commands syntax | ||
| run: | | ||
| # Use bash -n to check shell command syntax without execution | ||
| if [ -n "$PRE_PROCESSING" ]; then | ||
| ERROR_OUTPUT=$(echo "$PRE_PROCESSING" | bash -n 2>&1) || { | ||
| printf "## ❌ Error: Pre-processing command syntax error\n" >> $SUMMARY_FILE | ||
| printf "Please fix your pre-processing command syntax:\n" >> $SUMMARY_FILE | ||
| printf "``` | ||
| printf "%s\n" "$PRE_PROCESSING" >> $SUMMARY_FILE | ||
| printf "```\n\n" >> $SUMMARY_FILE | ||
| printf "**Error details:**\n" >> $SUMMARY_FILE | ||
| printf "``` | ||
| printf "%s\n" "$ERROR_OUTPUT" >> $SUMMARY_FILE | ||
| printf "```\n" >> $SUMMARY_FILE | ||
| exit 1 | ||
| } | ||
| fi | ||
| if [ -n "$POST_PROCESSING" ]; then | ||
| ERROR_OUTPUT=$(echo "$POST_PROCESSING" | bash -n 2>&1) || { | ||
| printf "## ❌ Error: Post-processing command syntax error\n" >> $SUMMARY_FILE | ||
| printf "Please fix your post-processing command syntax:\n" >> $SUMMARY_FILE | ||
| printf "``` | ||
| printf "%s\n" "$POST_PROCESSING" >> $SUMMARY_FILE | ||
| printf "```\n\n" >> $SUMMARY_FILE | ||
| printf "**Error details:**\n" >> $SUMMARY_FILE | ||
| printf "``` | ||
| printf "%s\n" "$ERROR_OUTPUT" >> $SUMMARY_FILE | ||
| printf "```\n" >> $SUMMARY_FILE | ||
| exit 1 | ||
| } | ||
| fi | ||
| - name: Setup Python | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.12' | ||
| - name: Setup uv | ||
| uses: astral-sh/setup-uv@v6 | ||
| with: | ||
| enable-cache: true | ||
| cache-dependency-glob: ".github/scripts/student_version.py" | ||
| - name: Execute pre-processing command | ||
| run: | | ||
| OUTPUT=$(bash -c "$PRE_PROCESSING" 2>&1) | ||
| EXIT_CODE=$? | ||
| if [ $EXIT_CODE -eq 0 ]; then | ||
| printf "## Pre-processing executed successfully ✅\n" >> $SUMMARY_FILE | ||
| if [ -n "$OUTPUT" ]; then | ||
| printf "%s\n" "$OUTPUT" >> $SUMMARY_FILE | ||
| else | ||
| printf "\n" >> $SUMMARY_FILE | ||
| fi | ||
| else | ||
| printf "## Post-processing failed ❌\n" >> $SUMMARY_FILE | ||
| printf "Exit code: %d\n" "$EXIT_CODE" >> $SUMMARY_FILE | ||
| if [ -n "$OUTPUT" ]; then | ||
| printf "\n" >> $SUMMARY_FILE | ||
| printf "Command output (including errors):\n" >> $SUMMARY_FILE | ||
| printf "``` | ||
| printf "%s\n" "$OUTPUT" >> $SUMMARY_FILE | ||
| printf "```\n" >> $SUMMARY_FILE | ||
| fi | ||
| fi | ||
| - name: Prepare Students branch | ||
| run: | | ||
| git fetch origin Students || true | ||
| # Try to checkout Students or create it from origin/Students or origin/main | ||
| git checkout Students || git checkout -b Students origin/Students || git checkout -b Students origin/main | ||
| # Merge main if necessary, favoring their changes, allow unrelated histories | ||
| git merge origin/main --strategy-option=theirs --allow-unrelated-histories || true | ||
| git push origin Students --force | ||
| # Detect if Students has just been created and is identical to main | ||
| # In such case, force processing all notebooks in the next step | ||
| if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then | ||
| echo "STUDENTS_FRESHLY_CREATED=true" >> "$GITHUB_ENV" | ||
| else | ||
| echo "STUDENTS_FRESHLY_CREATED=false" >> "$GITHUB_ENV" | ||
| fi | ||
| - name: Debug changed notebooks detection | ||
| id: debug_notebooks | ||
| shell: bash | ||
| run: | | ||
| CONFIG_PATH=".github/conversion.json" | ||
| DIRS=$(jq -r '.notebooks_dir[]' "$CONFIG_PATH") | ||
| # Print context info for debug | ||
| echo "Github SHA context" | ||
| echo "------------------" | ||
| echo "GITHUB_EVENT_BEFORE=${GITHUB_EVENT_BEFORE}" | ||
| echo "GITHUB_EVENT_AFTER=${GITHUB_EVENT_AFTER}" | ||
| echo "github.event.before=${{ github.event.before }}" | ||
| echo "github.event.after=${{ github.event.after }}" | ||
| echo "" | ||
| echo "Commits in main branch (git log):" | ||
| git log --oneline -5 | ||
| echo "" | ||
| BEFORE=${{ github.event.before }} | ||
| AFTER=${{ github.event.after }} | ||
| echo "BEFORE=$BEFORE" | ||
| echo "AFTER=$AFTER" | ||
| # Build grep pattern | ||
| PATTERNS="" | ||
| for dir in $DIRS; do | ||
| dir=$(echo "$dir" | sed 's:/*$::') | ||
| if [ -n "$PATTERNS" ]; then | ||
| PATTERNS+="|" | ||
| fi | ||
| PATTERNS+="^${dir}/.*\\.ipynb$" | ||
| done | ||
| echo "PATTERNS=$PATTERNS" | ||
| echo "" | ||
| echo "Diff files between BEFORE and AFTER:" | ||
| git diff --name-only "$BEFORE" "$AFTER" | ||
| echo "" | ||
| echo "Matched notebooks:" | ||
| git diff --name-only "$BEFORE" "$AFTER" | grep -E "$PATTERNS" || true | ||
| echo "" | ||
| # Write result to a file for step output | ||
| (git diff --name-only "$BEFORE" "$AFTER" | grep -E "$PATTERNS" || true) > notebooks_to_convert.txt | ||
| echo "notebooks<<EOF" >> $GITHUB_OUTPUT | ||
| cat notebooks_to_convert.txt >> $GITHUB_OUTPUT | ||
| echo "EOF" >> $GITHUB_OUTPUT | ||
| echo "" | ||
| echo "notebooks_to_convert.txt content:" | ||
| cat notebooks_to_convert.txt | ||
| - name: Debug notebooks_changed output variable | ||
| run: | | ||
| echo "GH_OUTPUT notebooks var BEGIN" | ||
| echo "::set-output name=notebooks::${{ steps.notebooks_changed.outputs.notebooks }}" | ||
| echo "Direct print with debug char :" | ||
| echo "${{ steps.notebooks_changed.outputs.notebooks }}" | cat -A | ||
| echo "GH_OUTPUT notebooks var END" | ||
| - name: Convert changed notebooks | ||
| run: | | ||
| echo "STEP notebooks variable =>" | ||
| echo "${{ steps.notebooks_changed.outputs.notebooks }}" | cat -A | ||
| if [ -z "${{ steps.notebooks_changed.outputs.notebooks }}" ]; then | ||
| echo "VAR EVALUATED AS EMPTY" | ||
| else | ||
| echo "VAR NOT EMPTY" | ||
| fi | ||
| echo "[DEBUG] Notebooks to convert:" | ||
| echo "${{ steps.notebooks_changed.outputs.notebooks }}" | ||
| printf "## Processing Report\n" >> $SUMMARY_FILE | ||
| printf "| Notebook | Questions | Code Blocks | ZIP |\n" >> $SUMMARY_FILE | ||
| printf "|----------|-----------|-------------|------|\n" >> $SUMMARY_FILE | ||
| echo "${{ steps.notebooks_changed.outputs.notebooks }}" | while IFS= read -r nb; do | ||
| [ -z "$nb" ] && continue | ||
| echo "[DEBUG] Converting notebook: '$nb'" | ||
| git checkout origin/main -- "$nb" | ||
| uv run .github/scripts/student_version.py "$nb" \ | ||
| --config "$CONFIG_PATH" \ | ||
| --hide-header >> $SUMMARY_FILE | ||
| done | ||
| - name: Execute post-processing command | ||
| run: | | ||
| if [ -n "$POST_PROCESSING" ]; then | ||
| OUTPUT=$(bash -c "$POST_PROCESSING" 2>&1) | ||
| EXIT_CODE=$? | ||
| if [ $EXIT_CODE -eq 0 ]; then | ||
| printf "## Post-processing executed successfully ✅\n" >> $SUMMARY_FILE | ||
| if [ -n "$OUTPUT" ]; then | ||
| printf "%s\n" "$OUTPUT" >> $SUMMARY_FILE | ||
| else | ||
| printf "\n" >> $SUMMARY_FILE | ||
| fi | ||
| else | ||
| printf "## Post-processing failed ❌\n" >> $SUMMARY_FILE | ||
| printf "Exit code: %d\n" "$EXIT_CODE" >> $SUMMARY_FILE | ||
| if [ -n "$OUTPUT" ]; then | ||
| printf "\n" >> $SUMMARY_FILE | ||
| printf "Command output (including errors):\n" >> $SUMMARY_FILE | ||
| printf "``` | ||
| printf "%s\n" "$OUTPUT" >> $SUMMARY_FILE | ||
| printf "```\n" >> $SUMMARY_FILE | ||
| fi | ||
| fi | ||
| fi | ||
| - name: manage summary | ||
| run: | | ||
| cp -a $SUMMARY_FILE . | ||
| cat README.md >> $GITHUB_STEP_SUMMARY | ||
| - name: Set up Git identity | ||
| run: | | ||
| git config --global user.name "GitHub Actions" | ||
| git config --global user.email "actions@github.com" | ||
| - name: Commit and push changes | ||
| run: | | ||
| # Only commit if there are actual changes (avoids empty commits) | ||
| git commit -m "Update student materials: $(date +'%Y-%m-%d %H:%M:%S')" || echo "No changes" | ||
| git push origin Students | ||