Conversation
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 calling the IG publisher is very expensive. Would splitting this out cause the publisher to run twice? |
No, the publisher only runs once. The |
…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>
✅ Deployment SuccessfulBranch:
Deployment completed successfully! The site is now live at the URL above. |
✅ Deployment SuccessfulBranch:
Deployment completed successfully! The site is now live at the URL above. |
… 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>
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>
There was a problem hiding this comment.
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.
| run: python3 input/scripts/find_pr.py | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
| run: python3 input/scripts/resolve_branch.py | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
| - 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" |
There was a problem hiding this comment.
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.
| run: python3 input/scripts/stamp_deploy.py output/index.html | ||
|
|
||
|
|
There was a problem hiding this comment.
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.
| 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 |
| # Make sure file is writable | ||
| try: | ||
| os.chmod(html_file, 0o666) | ||
| except OSError: | ||
| pass |
There was a problem hiding this comment.
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.
|
|
||
| #. 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 |
There was a problem hiding this comment.
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.
|
|
||
| #. 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 |
There was a problem hiding this comment.
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.
| "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 |
There was a problem hiding this comment.
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.
| #. 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 |
| "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 |
There was a problem hiding this comment.
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.
| "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 |
There was a problem hiding this comment.
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.
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
ghbuild.yml
scripts_refinput (default:main) to bothworkflow_callandworkflow_dispatchscripts_ref, setsSCRIPTS_BASE_URLenv varinput/scripts/fromscripts_refviagit checkout FETCH_HEAD --, overwriting any PR-local copiessmart-base/maincurl URLs with${SCRIPTS_BASE_URL}so downstream repos also respectscripts_refci.yml
pull_request→pull_request_target— workflow YAML always from base branchgithub.event.pull_request.head.shaasreffor content checkoutapprove-scripts-overridejob: environment-gated (scripts-override) approval required whenscripts_ref ≠ main; skipped for normal runspr-preview.yml
scripts_ref: mainSecurity
scripts_refsanitized viatr -cd 'a-zA-Z0-9._/-'${{ }}expressions inrun:blocks across all workflows — all context viaenv:blocks + Python validation scriptsfind_pr.py,resolve_branch.py,stamp_deploy.py) handlepull_request_targetevents with strict input validationscripts-override(script branch testing),pr-preview(deployment)untrusted-checkoutalerts are the inherentpull_request_target+ content-checkout pattern, mitigated by script overlay from main + passive content files + environment gates + fork exclusionOriginal prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.