From 268823a2575b72a1639199489447bac9251ae545 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 26 Apr 2026 07:38:02 -0400 Subject: [PATCH 1/5] Update GitHub Actions for Node 24 runtime --- .github/workflows/deploy.yml | 10 +++++----- .github/workflows/test.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6d3cd67..d239f0a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,15 +24,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.13' - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '22' @@ -59,7 +59,7 @@ jobs: cp -r viewer/dist/* docs/ - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v6 - name: Upload artifact uses: actions/upload-pages-artifact@v3 @@ -68,4 +68,4 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68e967c..8e068fc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.11' From e2ef89f6fac16499059c5708f99d02c6123fae64 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 26 Apr 2026 10:33:43 -0400 Subject: [PATCH 2/5] Clean up CI warnings --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d239f0a..831b3ba 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -62,7 +62,7 @@ jobs: uses: actions/configure-pages@v6 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: './docs' From 7c65873fcf64134468ab0aba4687c4bc4e3bae55 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 26 Apr 2026 10:44:11 -0400 Subject: [PATCH 3/5] Apply CI formatting fixes --- grants_builder/builder.py | 30 ++++++++---------- grants_builder/exporter.py | 65 +++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/grants_builder/builder.py b/grants_builder/builder.py index a057180..b68168e 100644 --- a/grants_builder/builder.py +++ b/grants_builder/builder.py @@ -51,9 +51,7 @@ def process_sections(grant_path, base_path, sections, grant_id, grant_name, foun # Validation: Check if response starts with question text question_text = section_data.get("question", "") - if question_text and response_markdown.strip().startswith( - f"# {question_text}" - ): + if question_text and response_markdown.strip().startswith(f"# {question_text}"): print( f" ⚠️ WARNING: {response_file.name} starts with question text - this will be included in the response!" ) @@ -127,9 +125,7 @@ def process_sections(grant_path, base_path, sections, grant_id, grant_name, foun "overLimit": over_limit, "needsCompletion": needs_completion, "status": ( - "needs_input" - if (over_limit or needs_completion) - else "complete" + "needs_input" if (over_limit or needs_completion) else "complete" ), } @@ -193,13 +189,13 @@ def process_grant(grant_id, grant_config): app_sections, grant_id, grant_config["name"], - grant_config["foundation"] + grant_config["foundation"], ) # Store application data separately application_data = { "metadata": app_metadata, - "responses": app_responses + "responses": app_responses, } for key, value in app_responses.items(): @@ -230,22 +226,24 @@ def process_grant(grant_id, grant_config): report_sections, grant_id, grant_config["name"], - grant_config["foundation"] + grant_config["foundation"], ) report_name = report_dir.name # Store report data separately - reports_data.append({ - "period": report_name, - "metadata": report_metadata, - "responses": report_responses - }) + reports_data.append( + { + "period": report_name, + "metadata": report_metadata, + "responses": report_responses, + } + ) for key, value in report_responses.items(): all_responses[f"report_{report_name}_{key}"] = { **value, "type": "report", - "report_period": report_name + "report_period": report_name, } responses = all_responses @@ -284,7 +282,7 @@ def process_grant(grant_id, grant_config): sections, grant_id, grant_config["name"], - grant_config["foundation"] + grant_config["foundation"], ) result = { diff --git a/grants_builder/exporter.py b/grants_builder/exporter.py index 10bb5ec..df3aa8e 100644 --- a/grants_builder/exporter.py +++ b/grants_builder/exporter.py @@ -5,7 +5,9 @@ import tempfile -def create_markdown_document(response_markdown, title, question, grant_name, foundation): +def create_markdown_document( + response_markdown, title, question, grant_name, foundation +): """Create a complete markdown document with header.""" doc = f"""# {grant_name} **{foundation}** @@ -33,21 +35,30 @@ def export_to_docx( ) # Write to temp file - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as tmp: + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as tmp: tmp.write(full_markdown) tmp_path = tmp.name try: # Use pandoc to convert to DOCX with Inter font and smaller size for tables result = subprocess.run( - ['pandoc', tmp_path, '-o', str(output_path), - '--from=markdown', '--to=docx', - '-V', 'mainfont=Inter', - '-V', 'fontsize=9pt', - '-V', 'geometry:margin=0.75in'], + [ + "pandoc", + tmp_path, + "-o", + str(output_path), + "--from=markdown", + "--to=docx", + "-V", + "mainfont=Inter", + "-V", + "fontsize=9pt", + "-V", + "geometry:margin=0.75in", + ], check=True, capture_output=True, - text=True + text=True, ) except subprocess.CalledProcessError as e: raise Exception(f"Pandoc DOCX conversion failed: {e.stderr}") @@ -60,7 +71,7 @@ def export_to_pdf( ): """Export response to PDF via DOCX conversion (better rendering than LaTeX).""" # First create DOCX - docx_temp = output_path.with_suffix('.temp.docx') + docx_temp = output_path.with_suffix(".temp.docx") export_to_docx( response_markdown, docx_temp, title, question, grant_name, foundation ) @@ -68,34 +79,52 @@ def export_to_pdf( try: # Convert DOCX to PDF using soffice (LibreOffice) result = subprocess.run( - ['soffice', '--headless', '--convert-to', 'pdf', '--outdir', - str(output_path.parent), str(docx_temp)], + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + str(output_path.parent), + str(docx_temp), + ], check=True, capture_output=True, text=True, - timeout=30 + timeout=30, ) # soffice outputs with the temp filename, rename it soffice_output = output_path.parent / f"{docx_temp.stem}.pdf" if soffice_output.exists(): soffice_output.rename(output_path) - except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e: + except ( + subprocess.CalledProcessError, + FileNotFoundError, + subprocess.TimeoutExpired, + ) as e: # Fallback to pandoc with better PDF settings full_markdown = create_markdown_document( response_markdown, title, question, grant_name, foundation ) - with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as tmp: + with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as tmp: tmp.write(full_markdown) tmp_path = tmp.name try: subprocess.run( - ['pandoc', tmp_path, '-o', str(output_path), - '--from=markdown', '--pdf-engine=xelatex', - '-V', 'geometry:margin=1in'], + [ + "pandoc", + tmp_path, + "-o", + str(output_path), + "--from=markdown", + "--pdf-engine=xelatex", + "-V", + "geometry:margin=1in", + ], check=True, capture_output=True, - text=True + text=True, ) except subprocess.CalledProcessError as e: raise Exception(f"PDF conversion failed: {e.stderr}") From bc537682d6cd249d604e00ba51a1c614a06b65f6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 26 Apr 2026 12:09:30 -0400 Subject: [PATCH 4/5] Fix CI failures --- grants_builder/builder.py | 25 +++++++++++++++++++------ grants_builder/exporter.py | 8 ++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/grants_builder/builder.py b/grants_builder/builder.py index b68168e..744faa6 100644 --- a/grants_builder/builder.py +++ b/grants_builder/builder.py @@ -32,7 +32,9 @@ def _old_strip_markdown_formatting(text): return text.strip() -def process_sections(grant_path, base_path, sections, grant_id, grant_name, foundation): +def process_sections( + grant_path, base_path, sections, grant_id, grant_name, foundation +): """Process sections from a questions file.""" responses = {} exports_dir = Path("docs/exports") @@ -51,7 +53,9 @@ def process_sections(grant_path, base_path, sections, grant_id, grant_name, foun # Validation: Check if response starts with question text question_text = section_data.get("question", "") - if question_text and response_markdown.strip().startswith(f"# {question_text}"): + if question_text and response_markdown.strip().startswith( + f"# {question_text}" + ): print( f" ⚠️ WARNING: {response_file.name} starts with question text - this will be included in the response!" ) @@ -125,7 +129,9 @@ def process_sections(grant_path, base_path, sections, grant_id, grant_name, foun "overLimit": over_limit, "needsCompletion": needs_completion, "status": ( - "needs_input" if (over_limit or needs_completion) else "complete" + "needs_input" + if (over_limit or needs_completion) + else "complete" ), } @@ -199,7 +205,10 @@ def process_grant(grant_id, grant_config): } for key, value in app_responses.items(): - all_responses[f"app_{key}"] = {**value, "type": "application"} + all_responses[f"app_{key}"] = { + **value, + "type": "application", + } # Process reports if reports_path.exists(): @@ -209,8 +218,12 @@ def process_grant(grant_id, grant_config): if report_questions_path.exists(): with open(report_questions_path) as f: report_questions_data = yaml.safe_load(f) - report_sections = report_questions_data.get("sections", {}) - report_metadata = report_questions_data.get("metadata", {}) + report_sections = report_questions_data.get( + "sections", {} + ) + report_metadata = report_questions_data.get( + "metadata", {} + ) # Handle both dict format and list format if isinstance(report_sections, list): diff --git a/grants_builder/exporter.py b/grants_builder/exporter.py index df3aa8e..2c5e170 100644 --- a/grants_builder/exporter.py +++ b/grants_builder/exporter.py @@ -35,7 +35,9 @@ def export_to_docx( ) # Write to temp file - with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as tmp: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".md", delete=False + ) as tmp: tmp.write(full_markdown) tmp_path = tmp.name @@ -106,7 +108,9 @@ def export_to_pdf( full_markdown = create_markdown_document( response_markdown, title, question, grant_name, foundation ) - with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as tmp: + with tempfile.NamedTemporaryFile( + mode="w", suffix=".md", delete=False + ) as tmp: tmp.write(full_markdown) tmp_path = tmp.name From c87932a9f02b545d13d244a0be4de3d6459c030d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 26 Apr 2026 12:20:33 -0400 Subject: [PATCH 5/5] Fix grant builder validation --- grants_builder/builder.py | 64 ++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/grants_builder/builder.py b/grants_builder/builder.py index 744faa6..c106dc3 100644 --- a/grants_builder/builder.py +++ b/grants_builder/builder.py @@ -270,33 +270,43 @@ def process_grant(grant_id, grant_config): questions_path = nsf_config_path else: # Try old location (pritzker_questions.yaml) - questions_path = grant_path / f"{grant_id}_questions.yaml" - - with open(questions_path) as f: - questions_data = yaml.safe_load(f) - - # Process responses - sections = questions_data.get("sections", {}) - - # Handle both dict format (Pritzker) and list format (PBIF) - if isinstance(sections, list): - # Convert list to dict for uniform processing - sections = { - item.get("id", f"section_{i}"): item - for i, item in enumerate(sections) - if "file" in item - } - elif not isinstance(sections, dict): - sections = {} - - responses = process_sections( - grant_path, - grant_path, - sections, - grant_id, - grant_config["name"], - grant_config["foundation"], - ) + legacy_questions_path = ( + grant_path / f"{grant_id}_questions.yaml" + ) + questions_path = ( + legacy_questions_path + if legacy_questions_path.exists() + else None + ) + + if questions_path is None: + responses = {} + else: + with open(questions_path) as f: + questions_data = yaml.safe_load(f) + + # Process responses + sections = questions_data.get("sections", {}) + + # Handle both dict format (Pritzker) and list format (PBIF) + if isinstance(sections, list): + # Convert list to dict for uniform processing + sections = { + item.get("id", f"section_{i}"): item + for i, item in enumerate(sections) + if "file" in item + } + elif not isinstance(sections, dict): + sections = {} + + responses = process_sections( + grant_path, + grant_path, + sections, + grant_id, + grant_config["name"], + grant_config["foundation"], + ) result = { "id": grant_id,