Implement $populate operation for SDC and update documentation
#351
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: 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 |