Skip to content

Implement $populate operation for SDC and update documentation #351

Implement $populate operation for SDC and update documentation

Implement $populate operation for SDC and update documentation #351

Workflow file for this run

name: Pull Request
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Fetch full history for better diff analysis
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-8.3-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Security audit
run: composer audit --abandoned=report
style:
runs-on: ubuntu-latest
env:
XDEBUG_MODE: off
COMPOSER_NO_INTERACTION: 1
steps:
- uses: actions/checkout@v4
with:
# Fetch full history for better diff analysis
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-8.3-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Validate composer files
run: composer validate --strict
- name: Check code style (Pint)
run: composer run lint -- --test
static-analysis:
runs-on: ubuntu-latest
env:
XDEBUG_MODE: off
COMPOSER_NO_INTERACTION: 1
strategy:
fail-fast: false
matrix:
component: [ bundle, codegen, fhir-path, validation, models ]
steps:
- uses: actions/checkout@v4
with:
# Fetch full history for better diff analysis
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-8.3-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run PHPStan static analysis
run: composer run phpstan:${{ matrix.component }}
tests:
runs-on: ubuntu-latest
env:
XDEBUG_MODE: coverage
COMPOSER_NO_INTERACTION: 1
strategy:
fail-fast: false
matrix:
include:
- suite: unit
- suite: integration-root
- suite: integration-serialization
- suite: integration-codegen
- suite: integration-fhir-path
- suite: integration-validation
- suite: fhirpath-spec
extra: --functional --max-batch-size=50
steps:
- uses: actions/checkout@v4
with:
# Fetch full history for better diff analysis
fetch-depth: 0
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-8.3-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run ${{ matrix.suite }} tests
run: php ./vendor/bin/paratest --testsuite=${{ matrix.suite }} ${{ matrix.extra }} --log-junit=./results/${{ matrix.suite }}-tests.xml --coverage-clover ./results/${{ matrix.suite }}-coverage.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./results
flags: ${{ matrix.suite }}
# Fan-in gate for branch protection: require this single check instead of
# individual matrix legs so the ruleset survives future matrix changes.
tests-passed:
runs-on: ubuntu-latest
needs: [ tests ]
if: ${{ always() }}
steps:
- name: Check test matrix result
run: |
if [ "${{ needs.tests.result }}" != "success" ]; then
echo "Test matrix result: ${{ needs.tests.result }}"
exit 1
fi
recipe-validation:
runs-on: ubuntu-latest
env:
XDEBUG_MODE: coverage
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Validate Symfony Flex recipe
run: composer run validate-recipe
- name: Test recipe installation
run: composer run test-recipe -- --log-junit=./results/recipe-tests.xml --coverage-clover ./results/coverage.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./results
fhir-generation-test:
runs-on: ubuntu-latest
needs: [ tests ]
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip, curl
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Install demo dependencies
run: composer install --working-dir=demo --prefer-dist --no-progress
- name: Test FHIR Model Generation
id: fhir-test
run: |
{
echo "🧪 Testing FHIR Model Generation..."
echo "================================="
echo ""
echo "📦 Regenerating R4B FHIR models (hl7.fhir.r4b.core + hl7.fhir.uv.extensions.r4b)..."
echo ""
} >> fhir_output.txt
# Regenerate the full R4B set so the git-drift check below is meaningful:
# generating only the core package would delete the extension models and
# produce a spurious diff. Must match `composer run generate-models-r4b`.
start_time=$(date +%s)
if timeout 540 php demo/bin/console fhir:generate \
--package=hl7.fhir.r4b.core \
--package=hl7.fhir.uv.extensions.r4b -v >> fhir_output.txt 2>&1; then
end_time=$(date +%s)
execution_time=$((end_time - start_time))
{
echo ""
echo "✅ **FHIR Generation Successful!**"
echo "⏱️ Execution time: ${execution_time} seconds"
echo ""
file_count=$(find src/Component/Models/src/R4B -name "*.php" | wc -l)
echo "📁 Generated ${file_count} R4B PHP model files"
} >> fhir_output.txt
echo "FHIR_SUCCESS=true" >> "$GITHUB_OUTPUT"
else
end_time=$(date +%s)
execution_time=$((end_time - start_time))
{
echo ""
echo "❌ **FHIR Generation Failed**"
echo "⏱️ Execution time: ${execution_time} seconds (timed out at 540s)"
echo ""
echo "Please check the error output above for details."
} >> fhir_output.txt
echo "FHIR_SUCCESS=false" >> "$GITHUB_OUTPUT"
fi
{
echo ""
echo "🖥️ **System Information:**"
echo "- PHP Version: $(php -v | head -n1)"
echo "- Memory Limit: $(php -r 'echo ini_get("memory_limit");')"
echo "- OS: $(uname -s) $(uname -r)"
} >> fhir_output.txt
- name: Normalise generated models with Pint
if: steps.fhir-test.outputs.FHIR_SUCCESS == 'true'
# Generated output must be run through Pint before diffing, otherwise the
# committed (already-linted) models differ from raw generation by formatting alone.
# Invoke Pint directly (not `composer run lint:models`) so we can run it silently
# and in parallel — lint:models bakes in `-v`, which conflicts with --silent.
run: ./vendor/bin/pint src/Component/Models --silent --parallel
- name: Check for uncommitted model drift
id: model-drift
if: steps.fhir-test.outputs.FHIR_SUCCESS == 'true'
run: |
drift="$(git status --porcelain -- src/Component/Models/src)"
if [ -n "$drift" ]; then
{
echo ""
echo "❌ **Generated models are out of date**"
echo ""
echo "Regenerating the R4B models produced changes that are not committed."
echo "Run \`composer run generate-models-r4b\` locally and commit the result."
echo ""
echo "<details>"
echo "<summary>📝 git status</summary>"
echo ""
echo '```'
echo "$drift"
echo '```'
echo ""
echo "</details>"
echo ""
echo "<details>"
echo "<summary>🔍 git diff (truncated to 300 lines)</summary>"
echo ""
echo '```diff'
git add -A -- src/Component/Models/src
git diff --cached -- src/Component/Models/src | head -300
echo '```'
echo ""
echo "</details>"
} >> fhir_output.txt
echo "MODELS_DRIFT=true" >> "$GITHUB_OUTPUT"
else
{
echo ""
echo "✅ **Generated models are up to date** — no git differences after regeneration."
} >> fhir_output.txt
echo "MODELS_DRIFT=false" >> "$GITHUB_OUTPUT"
fi
- name: Comment PR with FHIR Generation Results
if: ${{ !cancelled() }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Stable marker so we can update this PR's existing comment in place
// instead of posting a fresh one on every commit.
const MARKER = '<!-- fhir-model-generation-test -->';
let fhirOutput;
try {
fhirOutput = fs.readFileSync('fhir_output.txt', 'utf8');
} catch (error) {
fhirOutput = '❌ Could not read FHIR generation output file.';
}
const genSucceeded = '${{ steps.fhir-test.outputs.FHIR_SUCCESS }}' === 'true';
const drift = '${{ steps.model-drift.outputs.MODELS_DRIFT }}' === 'true';
const ok = genSucceeded && !drift;
const emoji = ok ? '✅' : '❌';
let status;
let footer;
if (!genSucceeded) {
status = 'FAILED';
footer = '⚠️ The FHIR model generation encountered issues. Please review the output above and fix any problems before merging.';
} else if (drift) {
status = 'OUT OF DATE';
footer = '⚠️ Regenerating the FHIR models produced uncommitted changes. Run `composer run generate-models-r4b` locally and commit the result before merging.';
} else {
status = 'SUCCESS';
footer = '🎉 The FHIR model generation completed successfully and the committed models are up to date.';
}
// Build with an array join to avoid leaking YAML/JS indentation into the
// rendered markdown (leading spaces would otherwise become code blocks).
const commentBody = [
MARKER,
`## ${emoji} FHIR Model Generation Test ${status}`,
'',
'This automated test regenerates the R4B FHIR models and verifies that the generation functionality works correctly with the changes in this PR, then checks that the committed models are up to date.',
'',
'<details>',
'<summary>📋 Click to view detailed output</summary>',
'',
'```',
fhirOutput,
'```',
'',
'</details>',
'',
'---',
'',
footer,
'',
'*This comment was automatically generated by the GitHub Actions workflow.*',
].join('\n');
// Find this workflow's previous comment (by marker) and update it; else create one.
const comments = await github.paginate(github.rest.issues.listComments, {
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
});
const prior = comments.find(
(c) => c.user?.type === 'Bot' && c.body?.includes(MARKER),
);
if (prior) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: prior.id,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}
- name: Fail if generation failed or models are out of date
if: steps.fhir-test.outputs.FHIR_SUCCESS != 'true' || steps.model-drift.outputs.MODELS_DRIFT == 'true'
run: |
echo "FHIR model generation check failed (success=${{ steps.fhir-test.outputs.FHIR_SUCCESS }}, drift=${{ steps.model-drift.outputs.MODELS_DRIFT }})."
exit 1
benchmarks:
runs-on: ubuntu-latest
needs: [ tests ]
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: ctype, iconv, zip, bcmath
- name: Cache Composer packages
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-8.3-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-8.3-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run benchmarks
id: bench
run: |
mkdir -p results
php ./vendor/bin/phpbench run bench/ --report=aggregate --no-interaction > results/benchmarks.txt 2>&1 || true
- name: Assert no regression against baseline
id: regression
if: hashFiles('.phpbench/**/*.xml') != ''
run: |
php ./vendor/bin/phpbench run bench/ \
--report=benchmark_compare \
--ref=baseline \
--assert="mode(variant.time.avg) < mode(baseline.time.avg) * 1.50" \
--no-interaction \
> results/regression.txt 2>&1
echo "exit_code=$?" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Comment PR with benchmark results
if: ${{ !cancelled() }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let benchOutput = '';
try {
benchOutput = fs.readFileSync('results/benchmarks.txt', 'utf8');
} catch (e) {
benchOutput = 'Could not read benchmark output.';
}
const baselineExists = '${{ hashFiles('.phpbench/**/*.xml') }}' !== '';
let regressionSection = '';
if (baselineExists) {
let regressionOutput = '';
try {
regressionOutput = fs.readFileSync('results/regression.txt', 'utf8');
} catch (e) {
regressionOutput = 'Could not read regression output.';
}
const passed = '${{ steps.regression.outputs.exit_code }}' === '0';
const icon = passed ? '✅' : '❌';
const label = passed ? 'Regression check passed' : 'Regression detected (>50% slower than baseline)';
regressionSection = `\n### ${icon} ${label}\n\n<details>\n<summary>Comparison vs baseline</summary>\n\n\`\`\`\n${regressionOutput}\n\`\`\`\n</details>`;
} else {
regressionSection = '\n> No baseline stored yet. Run `composer bench:baseline` and commit `.phpbench/` to enable regression checks.';
}
const commentBody = `## Benchmark Results\n\n<details>\n<summary>Serialization + FHIRPath benchmarks</summary>\n\n\`\`\`\n${benchOutput}\n\`\`\`\n</details>\n${regressionSection}\n\n*Run \`composer bench\` locally for full results.*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
- name: Fail if regression detected
if: steps.regression.outputs.exit_code != '0' && hashFiles('.phpbench/**/*.xml') != ''
run: exit 1