Skip to content

Split-checkout model: scripts from main, content from PR, with approval-gated override#311

Merged
litlfred merged 10 commits intomainfrom
copilot/fix-invalid-workflow-file
Mar 11, 2026
Merged

Split-checkout model: scripts from main, content from PR, with approval-gated override#311
litlfred merged 10 commits intomainfrom
copilot/fix-invalid-workflow-file

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 11, 2026

Resolves upstream conflicts from PR #312 and implements the split-checkout model: PR feature branches carry only IG content, while pre/post-processing scripts always run from main. Trusted contributors can override the scripts source with environment approval.

Split-checkout architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Workflow YAML   │     │     Scripts       │     │    Content      │
│  (from main via  │     │  (from main via   │     │  (from PR via   │
│  pull_request_   │     │  scripts_ref      │     │  ref input)     │
│  target)         │     │  overlay/curl)    │     │                 │
└─────────────────┘     └──────────────────┘     └─────────────────┘

ghbuild.yml

  • Added scripts_ref input (default: main) to both workflow_call and workflow_dispatch
  • New "Resolve scripts source" step: sanitizes scripts_ref, sets SCRIPTS_BASE_URL env var
  • New "Overlay smart-base scripts" step: when running in smart-base, fetches input/scripts/ from scripts_ref via git checkout FETCH_HEAD --, overwriting any PR-local copies
  • Replaced all 18 hardcoded smart-base/main curl URLs with ${SCRIPTS_BASE_URL} so downstream repos also respect scripts_ref

ci.yml

  • Switched pull_requestpull_request_target — workflow YAML always from base branch
  • Passes github.event.pull_request.head.sha as ref for content checkout
  • New approve-scripts-override job: environment-gated (scripts-override) approval required when scripts_ref ≠ main; skipped for normal runs

pr-preview.yml

  • Explicitly passes scripts_ref: main
  • Updated docs to reflect split-checkout model

Security

  • scripts_ref sanitized via tr -cd 'a-zA-Z0-9._/-'
  • Zero ${{ }} expressions in run: blocks across all workflows — all context via env: blocks + Python validation scripts
  • Python scripts (find_pr.py, resolve_branch.py, stamp_deploy.py) handle pull_request_target events with strict input validation
  • Environment gates: scripts-override (script branch testing), pr-preview (deployment)
  • CodeQL: 16 untrusted-checkout alerts are the inherent pull_request_target + content-checkout pattern, mitigated by script overlay from main + passive content files + environment gates + fork exclusion
Original prompt

This section details on the original issue you should resolve

<issue_title>actions issue</issue_title>
<issue_description>
GitHub Actions
/ .github/workflows/pr-preview.yml
Invalid workflow file

(Line: 63, Col: 5): Unexpected value 'uses', (Line: 64, Col: 5): Unexpected value 'with', (Line: 51, Col: 5): Required property is missing: runs-on</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

GitHub Actions does not allow `environment` in reusable workflow call
jobs (those using `uses:` at the job level).  The `environment` key
requires `runs-on`, but reusable workflow calls define their own runner.

Split the single `deploy-preview` job into:
  1. `approve` — regular job with `runs-on`, `environment: pr-preview`
     (acts as the approval gate)
  2. `deploy-preview` — reusable workflow call (`uses: ghbuild.yml`)
     that depends on `approve`

Fixes: "Unexpected value 'uses'" (line 63), "Unexpected value 'with'"
(line 64), "Required property is missing: runs-on" (line 51).

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix invalid GitHub Actions workflow file Fix invalid workflow: split pr-preview.yml reusable workflow call from environment gate Mar 11, 2026
@litlfred
Copy link
Copy Markdown
Collaborator

@copilot calling the IG publisher is very expensive. Would splitting this out cause the publisher to run twice?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 11, 2026

@copilot calling the IG publisher is very expensive. Would splitting this out cause the publisher to run twice?

No, the publisher only runs once. The approve job is a lightweight gate — it just runs echo "Deployment approved" and waits for the environment approval. The actual IG build only happens in the deploy-preview job via ghbuild.yml. The split was necessary because GitHub Actions doesn't allow environment: on reusable workflow call jobs (uses: at job level).

Copilot AI and others added 3 commits March 11, 2026 16:02
…of run: blocks into env:

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
… sanitization

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title Fix invalid workflow: split pr-preview.yml reusable workflow call from environment gate Harden GitHub Actions workflows against script injection Mar 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 11, 2026

✅ Deployment Successful

Branch: copilot/fix-invalid-workflow-file
Commit: 53e5961
Deployed to: https://WorldHealthOrganization.github.io/smart-base/branches/fix-invalid-workflow-file

🌐 Open Preview
🔍 View Build Log


Deployment completed successfully! The site is now live at the URL above.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 11, 2026

✅ Deployment Successful

Branch: copilot/fix-invalid-workflow-file
Commit: df62007
Deployed to: https://WorldHealthOrganization.github.io/smart-base/branches/fix-invalid-workflow-file

🌐 Open Preview
🔍 View Build Log


Deployment completed successfully! The site is now live at the URL above.

Copilot AI and others added 2 commits March 11, 2026 17:51
… pull_request for ci.yml

Merge upstream security changes from PR #312 with our Python-based
script injection hardening approach.

Key merge decisions:
- ci.yml: Keep pull_request trigger (NOT pull_request_target).
  CI validation must build the PR code, not the base branch.
  deploy: false on PRs already prevents secrets exposure.
- pr-preview.yml: Adopt pull_request_target from upstream with our
  approve + deploy-preview split (structurally better). Pass PR head
  SHA via ref input so ghbuild.yml builds the PR code after approval.
- ghbuild.yml: Keep our Python scripts approach for sanitization.
  Move ref input from workflow_dispatch to workflow_call (needed by
  pr-preview.yml to pass the PR head SHA for checkout).
- find_pr.py: Handle both pull_request and pull_request_target events.
- resolve_branch.py: Remove INPUT_REF handling — branch name always
  comes from head_ref or ref_name, never from the checkout ref.
Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title Harden GitHub Actions workflows against script injection Harden workflows against script injection and merge upstream pull_request_target changes Mar 11, 2026
github-actions Bot and others added 3 commits March 11, 2026 18:05
Regenerated via IG Publisher and extract_translations.py.
Triggered by: Copilot
…proval gate

The split-checkout model separates content (IG input) from scripts:
- Content comes from the PR branch (ref input)
- Scripts come from smart-base main (scripts_ref input, default: main)

For smart-base PRs, the overlay step fetches input/scripts/ from main,
ensuring feature branches only need content changes.

For downstream repos, curl-based downloads now use SCRIPTS_BASE_URL
(derived from scripts_ref) instead of hardcoded main URLs.

Trusted contributors can override scripts_ref via workflow_dispatch on
ci.yml; this requires approval via the 'scripts-override' environment.

ci.yml switches from pull_request to pull_request_target so workflow
YAML always comes from the base branch (main).

Security:
- scripts_ref is sanitized (tr -cd valid chars)
- No ${{ }} expressions in run: blocks
- Environment gates: scripts-override (script testing), pr-preview (deploy)

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
…CodeQL)

Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com>
Copilot AI changed the title Harden workflows against script injection and merge upstream pull_request_target changes Split-checkout model: scripts from main, content from PR, with approval-gated override Mar 11, 2026
@litlfred litlfred marked this pull request as ready for review March 11, 2026 20:31
Copilot AI review requested due to automatic review settings March 11, 2026 20:31
@litlfred litlfred merged commit 87cf9cd into main Mar 11, 2026
4 of 5 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors GitHub Actions build/deploy workflows to a split-checkout security model (content from a specified ref, scripts from a controlled scripts_ref) and moves several previously-inline shell behaviors into dedicated Python helper scripts.

Changes:

  • Add new Python helpers to resolve branch/deploy directory, find PR numbers, and stamp deploy metadata.
  • Update reusable build workflow (ghbuild.yml) to support split-checkout (ref) and controlled script sourcing (scripts_ref), and to reduce shell interpolation of GitHub context.
  • Regenerate multiple translation POT templates (header timestamps and embedded #. Source: links).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
input/scripts/stamp_deploy.py New orchestrator for deploy metadata stamping across build phases.
input/scripts/resolve_branch.py New helper to compute and export BRANCH_DIR / BRANCH_NAME safely.
input/scripts/find_pr.py New helper to compute PR number/PR-ness via env + gh CLI.
input/pagecontent/translations/license.pot Regenerated POT header and #. Source: links.
input/pagecontent/translations/index.pot Regenerated POT header and #. Source: links.
input/pagecontent/translations/downloads.pot Regenerated POT header and #. Source: links.
input/pagecontent/translations/dak-api.pot Regenerated POT header and #. Source: links.
input/pagecontent/translations/changes.pot Regenerated POT header and #. Source: links.
input/images/translations/images.pot Regenerated POT header and #. Source: links.
.github/workflows/pr-preview.yml Add explicit approval gate job and document split-checkout model for preview deploys.
.github/workflows/ghbuild.yml Introduce ref/scripts_ref, scripts overlay/download base URL, and replace inline logic with new Python scripts.
.github/workflows/fhirbuild.yml Adjust curl payload construction to use environment variables.
.github/workflows/ci.yml Switch PR builds to pull_request_target split-checkout flow and add approval gate for non-default scripts_ref.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +179 to 180
run: python3 input/scripts/find_pr.py

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the reusable workflow context (downstream repos), input/scripts/find_pr.py may not exist in the checked-out content. Previously this logic was inline shell; now the step will fail with python3: can't open file unless the script is present. Consider adding the same on-demand download pattern used for other scripts (using ${SCRIPTS_BASE_URL}) before invoking it, or keep the logic inline for portability.

Suggested change
run: python3 input/scripts/find_pr.py
run: |
# Ensure find_pr.py is available; download from smart-base if missing
if [ ! -f "input/scripts/find_pr.py" ]; then
echo "find_pr.py not found locally, downloading from smart-base repository..."
mkdir -p input/scripts
curl -L -f -o "input/scripts/find_pr.py" "${SCRIPTS_BASE_URL}/input/scripts/find_pr.py" 2>/dev/null || echo "Failed to download find_pr.py"
fi
# Run find_pr.py if available, otherwise fall back to a non-PR default
if [ -f "input/scripts/find_pr.py" ]; then
python3 input/scripts/find_pr.py
else
echo "⚠️ find_pr.py not available, treating this run as not associated with a PR"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "IS_PR=false" >> "$GITHUB_OUTPUT"
fi
fi

Copilot uses AI. Check for mistakes.
Comment on lines +215 to 216
run: python3 input/scripts/resolve_branch.py

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step now unconditionally runs python3 input/scripts/resolve_branch.py. For downstream repos that call ghbuild.yml via workflow_call, input/scripts/resolve_branch.py will not exist unless downloaded, so the build will fail. Add an on-demand download from ${SCRIPTS_BASE_URL} (similar to other steps) or provide a fallback implementation here.

Suggested change
run: python3 input/scripts/resolve_branch.py
run: |
# Try to ensure resolve_branch.py is available, download if missing
if [ ! -f "input/scripts/resolve_branch.py" ]; then
echo "resolve_branch.py not found locally, attempting to download from smart-base repository..."
mkdir -p input/scripts
curl -L -f -o "input/scripts/resolve_branch.py" "${SCRIPTS_BASE_URL}/input/scripts/resolve_branch.py" 2>/dev/null || echo "Failed to download resolve_branch.py"
fi
if [ -f "input/scripts/resolve_branch.py" ]; then
echo "Using resolve_branch.py script to determine branch name..."
python3 input/scripts/resolve_branch.py
else
echo "⚠️ resolve_branch.py not available, falling back to shell-based branch resolution"
# Fallback: prefer GH_HEAD_REF (for PRs), otherwise use GH_REF_NAME
if [ -n "${GH_HEAD_REF}" ]; then
BRANCH_NAME="${GH_HEAD_REF}"
else
BRANCH_NAME="${GH_REF_NAME}"
fi
echo "Resolved branch name (fallback): ${BRANCH_NAME}"
echo "BRANCH_NAME=${BRANCH_NAME}" >> "$GITHUB_ENV"
fi

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +132
- name: Resolve scripts source
env:
SCRIPTS_REF_INPUT: ${{ inputs.scripts_ref || 'main' }}
run: |
# Sanitize: allow only valid Git ref characters
SCRIPTS_REF="$(printf '%s' "$SCRIPTS_REF_INPUT" | tr -cd 'a-zA-Z0-9._/-')"
if [ -z "$SCRIPTS_REF" ]; then
SCRIPTS_REF="main"
fi
SCRIPTS_BASE_URL="https://raw.githubusercontent.com/WorldHealthOrganization/smart-base/${SCRIPTS_REF}"
printf 'SCRIPTS_REF=%s\n' "$SCRIPTS_REF"
printf 'SCRIPTS_REF=%s\n' "$SCRIPTS_REF" >> "$GITHUB_ENV"
printf 'SCRIPTS_BASE_URL=%s\n' "$SCRIPTS_BASE_URL"
printf 'SCRIPTS_BASE_URL=%s\n' "$SCRIPTS_BASE_URL" >> "$GITHUB_ENV"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scripts_ref sanitization currently strips disallowed characters but still permits values like -n or .., which can change the behavior of git fetch origin "${SCRIPTS_REF}" (option injection) and produce odd ${SCRIPTS_BASE_URL} paths. Prefer rejecting unsafe refs (e.g. fail if the sanitized value differs from input, disallow leading '-' and '..') rather than silently mutating the ref.

Copilot uses AI. Check for mistakes.
Comment on lines +692 to 694
run: python3 input/scripts/stamp_deploy.py output/index.html


Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This step now runs python3 input/scripts/stamp_deploy.py, but the workflow never downloads stamp_deploy.py when it’s missing (downstream repos typically won’t have it). This will break deployments for repos relying on the reusable workflow. Add an on-demand download from ${SCRIPTS_BASE_URL} (and/or keep the previous inline logic) before executing the script.

Suggested change
run: python3 input/scripts/stamp_deploy.py output/index.html
run: |
echo "Stamping deploy metadata into output/index.html..."
# Check if the script exists locally, download if needed
if [ ! -f "input/scripts/stamp_deploy.py" ]; then
echo "stamp_deploy.py not found locally, downloading from smart-base repository..."
mkdir -p input/scripts
curl -L -f -o "input/scripts/stamp_deploy.py" \
"${SCRIPTS_BASE_URL}/input/scripts/stamp_deploy.py" \
2>/dev/null || echo "Failed to download stamp_deploy.py"
else
echo "Using local stamp_deploy.py"
fi
# Run the stamper if available
if [ -f "input/scripts/stamp_deploy.py" ]; then
python3 input/scripts/stamp_deploy.py output/index.html
echo "✅ Deploy metadata stamped into output/index.html"
else
echo "⚠️ stamp_deploy.py not available, skipping deploy metadata stamping"
fi

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +181
# Make sure file is writable
try:
os.chmod(html_file, 0o666)
except OSError:
pass
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow still runs the IG Publisher in Docker as root, so output/index.html is often root-owned. This script’s os.chmod(html_file, 0o666) will typically fail with EPERM (and is silently ignored), causing stamping to fail (also silently, since subprocess return codes aren’t propagated). Consider invoking sudo chmod a+w (via subprocess.run([...], shell=False)), or fixing ownership via sudo chown, so stamping reliably succeeds on GitHub runners.

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 22

#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/pagecontent/index.md#L7
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/copilot/fix-invalid-workflow-file/input/pagecontent/index.md#L7
#. URL: http://smart.who.int/base/index.html
#. URL: https://WorldHealthOrganization.github.io/smart-base/index.html
#: input/pagecontent/index.md:7
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The committed POT now embeds #. Source: URLs for a feature branch (blob/copilot/fix-invalid-workflow-file/...). This is likely to break links after merge and will create noisy diffs when POTs are regenerated on main. Prefer stable refs for committed templates (e.g. main) or avoid embedding the branch in source URLs.

Copilot uses AI. Check for mistakes.
Comment on lines 18 to 22

#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/images/L2_DAK_components_3x3_logoimage.svg#L46
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/copilot/fix-invalid-workflow-file/input/images/L2_DAK_components_3x3_logoimage.svg#L46
#. URL: http://smart.who.int/base/L2_DAK_components_3x3_logoimage.svg
#. URL: https://WorldHealthOrganization.github.io/smart-base/L2_DAK_components_3x3_logoimage.svg
#: input/images/L2_DAK_components_3x3_logoimage.svg:46
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This POT update appears to be a regeneration artifact: the #. Source: URL now points at a feature branch (blob/copilot/fix-invalid-workflow-file/...). That link will likely be invalid after merge. Please regenerate with a stable ref (typically main) or change source URL generation to avoid embedding ephemeral branch names in committed POTs.

Copilot uses AI. Check for mistakes.
"Plural-Forms: nplurals=6; plural=(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=10 ? 2 : n>=11 && n<=99 ? 3 : 4);\n"

#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/pagecontent/downloads.md#L3
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/copilot/fix-invalid-workflow-file/input/pagecontent/downloads.md#L3
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #. Source: URL here points at a feature branch (blob/copilot/fix-invalid-workflow-file/...). After merge this link is likely to be invalid and will cause churn when POTs are regenerated on main. Regenerate with a stable ref (typically main) or avoid embedding the branch name in committed POT source URLs.

Suggested change
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/copilot/fix-invalid-workflow-file/input/pagecontent/downloads.md#L3
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/pagecontent/downloads.md#L3

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 22
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=10 ? 2 : n>=11 && n<=99 ? 3 : 4);\n"

#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/pagecontent/dak-api.md#L3
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/copilot/fix-invalid-workflow-file/input/pagecontent/dak-api.md#L3
#. URL: http://smart.who.int/base/dak-api.html
#. URL: https://WorldHealthOrganization.github.io/smart-base/dak-api.html
#: input/pagecontent/dak-api.md:3
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The committed POT now embeds #. Source: URLs pointing at a feature branch (blob/copilot/fix-invalid-workflow-file/...). This will likely break after merge and create noisy diffs on regeneration. Prefer stable refs (e.g. main) for committed templates or avoid including the branch in source URLs.

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 22
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=6; plural=(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=10 ? 2 : n>=11 && n<=99 ? 3 : 4);\n"

#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/main/input/pagecontent/changes.md#L5
#. Source: https://github.com/WorldHealthOrganization/smart-base/blob/copilot/fix-invalid-workflow-file/input/pagecontent/changes.md#L5
#. URL: http://smart.who.int/base/changes.html
#. URL: https://WorldHealthOrganization.github.io/smart-base/changes.html
#: input/pagecontent/changes.md:5
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This template was regenerated with #. Source: URLs pointing at a feature branch (blob/copilot/fix-invalid-workflow-file/...). That link will likely 404 after merge. Please regenerate with a stable ref (typically main) or change source URL generation so committed POTs don’t depend on ephemeral branch names.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

actions issue

3 participants