Skip to content

Conversation

@olivermeyer
Copy link
Collaborator

  • Parameterizing platform_environment; defaults to staging
  • Allowing workflow dispatch triggers

@github-actions
Copy link
Contributor

github-actions bot commented Nov 7, 2025

Claude finished @olivermeyer's task —— View job


PR Review: Parameterize Platform Environment ✅

Summary

This PR adds workflow_dispatch trigger with a platform_environment parameter to the main CI/CD workflow, enabling manual workflow runs with environment selection (staging/production). The change also updates the hardcoded platform_environment: "production" to use the dynamic input with 'staging' as the default.


🟢 Review Findings: APPROVED

This is a well-structured change that improves flexibility while maintaining safety. All checks pass.

✅ Correctness Verification

1. Workflow Dispatch Configuration

workflow_dispatch:
  inputs:
    platform_environment:
      description: 'Environment to test against'
      required: false
      default: 'staging'
      type: choice
      options:
        - staging
        - production

Analysis:

  • ✅ Syntax is correct
  • type: choice limits options to valid values only
  • default: 'staging' ensures safe default for manual triggers
  • ✅ Description is clear and concise

2. Parameterization Change

Before:

platform_environment: "production"  # ❌ Hardcoded

After:

platform_environment: ${{ inputs.platform_environment || 'staging' }}  # ✅ Dynamic with safe fallback

Analysis:

  • ✅ Uses inputs.platform_environment for manual workflow_dispatch triggers
  • ✅ Falls back to 'staging' when input is not available (PR/push triggers)
  • ✅ This is a BREAKING CHANGE but in a good direction - see below

3. Impact on _test.yml

The _test.yml reusable workflow already accepts platform_environment as an input:

inputs:
  platform_environment:
    description: 'Environment to test against, that is staging or production'
    required: false
    default: 'staging'
    type: string

Analysis:

  • ✅ No changes needed in _test.yml
  • ✅ Compatible with both string values: 'staging' and 'production'
  • ✅ Has its own default of 'staging' as double safety

🎯 Behavior Changes

CRITICAL: This is a Breaking Change (But Correct)

Previous behavior:

  • All PR tests ran against production environment ❌

New behavior:

  • All PR tests run against staging environment (default fallback) ✅
  • Manual workflow_dispatch runs can choose staging or production ✅

Why This Change is CORRECT

According to .github/CLAUDE.md:

Production Environment

Use Cases:

  • Scheduled tests only (every 24 hours)
  • Release validation
  • Critical bug verification
  • NEVER use in PR CI (staging only)

The previous hardcoded "production" violated the documented best practice. This PR fixes that violation.

What Was Wrong Before

The old configuration had:

platform_environment: "production"  # ❌ WRONG - PRs should use staging

This meant every PR was testing against the production API, which is explicitly forbidden by the CI/CD documentation:

  • Production is for scheduled tests only (24h)
  • PRs should always use staging
  • Only release validation should touch production

📋 Safety Analysis

1. No Breaking Changes to Existing Workflows

Automatic triggers (push, PR, release, tag):

  • inputs.platform_environment is null
  • ✅ Fallback to 'staging' via || operator works correctly
  • ✅ All existing workflows continue to work (now correctly using staging)

Manual triggers (workflow_dispatch):

  • ✅ User must explicitly choose production
  • ✅ Defaults to staging if user doesn't change selection
  • ✅ Cannot accidentally choose invalid environment (type: choice validation)

2. Environment Secrets

Both environments have their secrets passed to _test.yml:

secrets:
  AIGNOSTICS_CLIENT_ID_DEVICE_STAGING: ${{ secrets.AIGNOSTICS_CLIENT_ID_DEVICE_STAGING }}
  AIGNOSTICS_REFRESH_TOKEN_STAGING: ${{ secrets.AIGNOSTICS_REFRESH_TOKEN_STAGING }}
  GCP_CREDENTIALS_STAGING: ${{ secrets.GCP_CREDENTIALS_STAGING }}
  AIGNOSTICS_CLIENT_ID_DEVICE_PRODUCTION: ${{ secrets.AIGNOSTICS_CLIENT_ID_DEVICE_PRODUCTION }}
  AIGNOSTICS_REFRESH_TOKEN_PRODUCTION: ${{ secrets.AIGNOSTICS_REFRESH_TOKEN_PRODUCTION }}
  GCP_CREDENTIALS_PRODUCTION: ${{ secrets.GCP_CREDENTIALS_PRODUCTION }}

Analysis:

  • ✅ No secrets configuration changes needed
  • ✅ Both staging and production secrets already passed through
  • _test.yml selects appropriate secrets based on environment parameter

3. Alignment with Scheduled Tests

This change aligns with the existing scheduled test workflows:

  • test-scheduled-staging.yml - Uses staging environment (6h)
  • test-scheduled-production.yml - Uses production environment (24h)

Now the main CI/CD can also support both environments via manual trigger.


🔍 Additional Considerations

1. Documentation Alignment

This change perfectly aligns with .github/CLAUDE.md:

Staging Environment

Use Cases:

  • PR testing (default for all PRs) ✅ NOW CORRECT
  • E2E test execution ✅
  • Feature validation ✅
  • Claude Code development sessions ✅

Production Environment

Use Cases:

  • Scheduled tests only (every 24 hours) ✅
  • Release validation ✅
  • Critical bug verification ✅
  • NEVER use in PR CI (staging only) ✅ NOW ENFORCED

2. No CI/CD Documentation Updates Needed

The PR description states:

  • Parameterizing platform_environment; defaults to staging
  • Allowing workflow dispatch triggers

This is accurate. The .github/CLAUDE.md already documents that PR tests should use staging, so no doc updates are needed—the code is now conforming to the documented behavior.

3. Testing the Change ⚠️

Recommendation: After merge, verify:

  1. Automatic PR trigger (this will be tested by this PR itself):

    # Check that tests run against staging
    # Look for: AIGNOSTICS_PLATFORM_ENVIRONMENT=staging in logs
  2. Manual workflow_dispatch:

    # Actions → CI/CD → Run workflow
    # Select "staging" → Verify runs against staging
    # Select "production" → Verify runs against production
  3. Tag/Release trigger:

    # Create a test tag
    git tag v0.0.0-test
    git push --tags
    # Verify defaults to staging

💡 Suggestions (Non-blocking)

1. Consider Adding Workflow Dispatch to Documentation

While .github/CLAUDE.md documents the workflow extensively, you may want to add a note about manual triggering:

### Manual CI/CD Runs

You can manually trigger the CI/CD pipeline with a custom environment:

```bash
# Via GitHub UI
Actions → CI/CD → Run workflow
# Select branch and environment (staging/production)

# Via gh cli
gh workflow run ci-cd.yml -f platform_environment=staging

**Location**: `.github/CLAUDE.md` around line 1031 (after "### Manual Testing with Claude")

#### 2. **Add Validation Step in CI/CD** (Future Enhancement)

Consider adding a validation step to prevent production testing in PR context:

```yaml
- name: Validate environment for PR
  if: github.event_name == 'pull_request' && inputs.platform_environment == 'production'
  run: |
    echo "::error::Production environment is not allowed for PR testing"
    exit 1

This would provide an extra safety net, though the current default mechanism already prevents this in normal flows.


📊 Final Assessment

Category Status Notes
Syntax ✅ Pass Valid YAML, correct GitHub Actions syntax
Logic ✅ Pass Fallback chain works correctly
Safety ✅ Pass Defaults to safe environment (staging)
Documentation ✅ Pass Aligns with (and fixes) documented behavior
Breaking Changes ⚠️ Yes, but GOOD Fixes incorrect production usage in PRs
Security ✅ Pass No security concerns
Testing ✅ Pass This PR will test the new behavior

✅ Recommendation: APPROVE AND MERGE

This PR:

  1. ✅ Fixes a critical misalignment where PRs tested against production (should be staging)
  2. ✅ Adds useful flexibility for manual workflow triggers
  3. ✅ Maintains backward compatibility for automatic triggers
  4. ✅ Follows GitHub Actions best practices
  5. ✅ Aligns with documented CI/CD strategy

No changes requested. This is a quality improvement that makes the CI/CD more flexible and corrects a previous misconfiguration.


🚀 Next Steps

  1. ✅ Merge this PR
  2. ✅ Monitor this PR's CI run to confirm tests use staging
  3. ✅ Optionally test manual workflow_dispatch after merge
  4. ⚠️ Consider notifying team about the environment change (PRs now correctly use staging instead of production)

Great work, @olivermeyer! This improves both the flexibility and correctness of the CI/CD pipeline. 👏


@olivermeyer olivermeyer marked this pull request as ready for review November 7, 2025 10:56
@codecov
Copy link

codecov bot commented Nov 7, 2025

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
566 2 564 8
View the full list of 2 ❄️ flaky test(s)
tests.aignostics.application.gui_test::test_gui_run_download

Flake rate in main: 12.00% (Passed 66 times, Failed 9 times)

Stack Traces | 48s run time
user = <nicegui.testing.user.User object at 0x7f617214a060>
runner = <typer.testing.CliRunner object at 0x7f6174321220>
tmp_path = PosixPath('.../pytest-of-runner/pytest-13/test_gui_run_download1')
silent_logging = None
record_property = <function record_property.<locals>.append_property at 0x7f61742b85e0>

    @pytest.mark.e2e
    @pytest.mark.long_running
    @pytest.mark.flaky(retries=1, delay=5)
    @pytest.mark.timeout(timeout=60 * 5)
    @pytest.mark.sequential  # Helps on Linux with image analysis step otherwise timing out
    async def test_gui_run_download(  # noqa: PLR0915
        user: User, runner: CliRunner, tmp_path: Path, silent_logging: None, record_property
    ) -> None:
        """Test that the user can download a run result via the GUI."""
        record_property("tested-item-id", "SPEC-APPLICATION-SERVICE, SPEC-GUI-SERVICE")
        with patch(
            "aignostics.application._gui._page_application_run_describe.get_user_data_directory",
            return_value=tmp_path,
        ):
            # Find run
            runs = Service().application_runs(
                application_id=HETA_APPLICATION_ID,
                application_version=HETA_APPLICATION_VERSION,
                external_id=SPOT_0_GS_URL,
                has_output=True,
                limit=1,
            )
            if not runs:
                message = f"No matching runs found for application {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION}). "
                message += "This test requires the scheduled test test_application_runs_heta_version passing first."
                pytest.skip(message)
    
            run_id = runs[0].run_id
    
            # Explore run
            run = Service().application_run(run_id).details()
            print(
                f"Found existing run: {run.run_id}\n"
                f"application: {run.application_id} ({run.version_number})\n"
                f"status: {run.state}, output: {run.output}\n"
                f"submitted at: {run.submitted_at}, terminated at: {run.terminated_at}\n"
                f"statistics: {run.statistics!r}\n",
                f"custom_metadata: {run.custom_metadata!r}\n",
            )
            # Step 1: Go to latest completed run
            await user.open(f"/application/run/{run.run_id}")
            await user.should_see(f"Run {run.run_id}", retries=100)
            await user.should_see(
                f"Run of {run.application_id} ({run.version_number})",
                retries=100,
            )
    
            # Step 2: Open Result Download dialog
            await user.should_see(marker="BUTTON_DOWNLOAD_RUN", retries=100)
            user.find(marker="BUTTON_DOWNLOAD_RUN").click()
    
            # Step 3: Select Data
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert not download_run_button.enabled, "Download button should be disabled before selecting target"
            await user.should_see(marker="BUTTON_DOWNLOAD_DESTINATION_DATA", retries=100)
            user.find(marker="BUTTON_DOWNLOAD_DESTINATION_DATA").click()
    
            # Step 3: Trigger Download
            await sleep(2)  # Wait a bit for button state to update so we can click
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert download_run_button.enabled, "Download button should be enabled after selecting target"
            user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").click()
            await assert_notified(user, "Downloading ...")
    
            # Check: Download completed
            await assert_notified(user, "Download completed.", 60 * 4)
            print_directory_structure(tmp_path, "downloaded_run")
    
            # Check for directory layout as expected
            run_dir = tmp_path / run.run_id
            assert run_dir.is_dir(), f"Expected run directory {run_dir} not found"
    
            subdirs = [d for d in run_dir.iterdir() if d.is_dir()]
            assert len(subdirs) == 2, f"Expected two subdirectories in {run_dir}, but found {len(subdirs)}"
    
            input_dir = run_dir / "input"
            assert input_dir.is_dir(), f"Expected input directory {input_dir} not found"
    
            results_dir = run_dir / SPOT_0_FILENAME.replace(".tiff", "")
            assert results_dir.is_dir(), f"Expected run results directory {results_dir} not found"
    
            # Check for input file having been downloaded
            input_file = input_dir / SPOT_0_FILENAME
            assert input_file.exists(), f"Expected input file {input_file} not found"
            assert input_file.stat().st_size == SPOT_0_FILESIZE, (
                f"Expected input file size {SPOT_0_FILESIZE}, but got {input_file.stat().st_size}"
            )
    
            # Check for files in the results directory
            files_in_results_dir = list(results_dir.glob("*"))
            assert len(files_in_results_dir) == 9, (
                f"Expected 9 files in {results_dir}, but found {len(files_in_results_dir)}: "
                f"{[f.name for f in files_in_results_dir]}"
            )
    
            print(f"Found files in {results_dir}:")
            for filename, expected_size, tolerance_percent in SPOT_0_EXPECTED_RESULT_FILES:
                file_path = results_dir / filename
                if file_path.exists():
                    actual_size = file_path.stat().st_size
                    print(f"  {filename}: {actual_size} bytes (expected: {expected_size} ±{tolerance_percent}%)")
                else:
                    print(f"  {filename}: NOT FOUND")
            for filename, expected_size, tolerance_percent in SPOT_0_EXPECTED_RESULT_FILES:
                file_path = results_dir / filename
                assert file_path.exists(), f"Expected file {filename} not found"
                actual_size = file_path.stat().st_size
                min_size = expected_size * (100 - tolerance_percent) // 100
                max_size = expected_size * (100 + tolerance_percent) // 100
>               assert min_size <= actual_size <= max_size, (
                    f"File size for {filename} ({actual_size} bytes) is outside allowed range "
                    f"({min_size} to {max_size} bytes, ±{tolerance_percent}% of {expected_size})"
                )
E               AssertionError: File size for tissue_qc_geojson_polygons.json (160668 bytes) is outside allowed range (283517 to 346520 bytes, ±10% of 315019)
E               assert 283517 <= 160668

.../aignostics/application/gui_test.py:445: AssertionError
tests.aignostics.qupath.gui_test::test_gui_run_qupath_install_to_inspect

Flake rate in main: 20.59% (Passed 54 times, Failed 14 times)

Stack Traces | 68.3s run time
user = <nicegui.testing.user.User object at 0x7f61629207c0>
runner = <typer.testing.CliRunner object at 0x7f616241c850>
tmp_path = PosixPath('.../pytest-of-runner/pytest-13/test_gui_run_qupath_install_to0')
silent_logging = None, qupath_teardown = None
record_property = <function record_property.<locals>.append_property at 0x7f61624137e0>

    @pytest.mark.e2e
    @pytest.mark.long_running
    @pytest.mark.skipif(
        (platform.system() == "Linux" and platform.machine() in {"aarch64", "arm64"}) or platform.system() == "Windows",
        reason="QuPath is not supported on ARM64 Linux; Windows support is not fully tested yet",
    )
    @pytest.mark.timeout(timeout=60 * 15)
    @pytest.mark.sequential
    async def test_gui_run_qupath_install_to_inspect(  # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
        user: User, runner: CliRunner, tmp_path: Path, silent_logging: None, qupath_teardown: None, record_property
    ) -> None:
        """Test installing QuPath, downloading run results, creating QuPath project from it, and inspecting results."""
        record_property("tested-item-id", "TC-QUPATH-01, SPEC-GUI-SERVICE")
    
        # Find run
        runs = Service().application_runs(
            application_id=HETA_APPLICATION_ID,
            application_version=HETA_APPLICATION_VERSION,
            external_id=SPOT_0_GS_URL,
            has_output=True,
            limit=1,
        )
        if not runs:
            message = f"No matching runs found for application {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION}). "
            message += "This test requires the scheduled test test_application_runs_heta_version passing first."
            pytest.skip(message)
    
        run_id = runs[0].run_id
    
        # Explore run
        run = Service().application_run(run_id).details()
        print(
            f"Found existing run: {run.run_id}\n"
            f"application: {run.application_id} ({run.version_number})\n"
            f"status: {run.state}, output: {run.output}\n"
            f"submitted at: {run.submitted_at}, terminated at: {run.terminated_at}\n"
            f"statistics: {run.statistics!r}\n",
            f"custom_metadata: {run.custom_metadata!r}\n",
        )
    
        # Explore results
        results = list(Service().application_run(run_id).results())
        assert results, f"No results found for run {run_id}"
        for item in results:
            print(
                f"Found item: {item.item_id}, status: {item.state}, output: {item.output}, "
                f"external_id: {item.external_id}\n"
                f"custom_metadata: {item.custom_metadata!r}\n",
            )
    
        with patch(
            "aignostics.application._gui._page_application_run_describe.get_user_data_directory", return_value=tmp_path
        ):
            # Step 1: (Re)Install QuPath
            result = runner.invoke(cli, ["qupath", "uninstall"])
            assert result.exit_code in {0, 2}, f"Uninstall command failed with exit code {result.exit_code}"
            was_installed = not result.exit_code
    
            result = runner.invoke(cli, ["qupath", "install"])
            output = normalize_output(result.output, strip_ansi=True)
            assert f"QuPath v{QUPATH_VERSION} installed successfully" in output, (
                f"Expected 'QuPath v{QUPATH_VERSION} installed successfully' in output.\nOutput: {output}"
            )
            assert result.exit_code == 0
    
            # Step 2: Go to latest completed run via GUI
            await user.open(f"/application/run/{run.run_id}")
            await user.should_see(f"Run {run.run_id}")
            await user.should_see(f"Run of {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION})")
    
            # Step 3: Open Result Download dialog
            await user.should_see(marker="BUTTON_OPEN_QUPATH", retries=100)
            user.find(marker="BUTTON_OPEN_QUPATH").click()
    
            # Step 4: Select Data destination
            await user.should_see(marker="BUTTON_DOWNLOAD_DESTINATION_DATA")
            download_destination_data_button: ui.button = user.find(
                marker="BUTTON_DOWNLOAD_DESTINATION_DATA"
            ).elements.pop()
            assert download_destination_data_button.enabled, "Download destination button should be enabled"
            user.find(marker="BUTTON_DOWNLOAD_DESTINATION_DATA").click()
            await assert_notified(user, "Using Launchpad results directory", 30)
    
            # Step 5: Trigger Download
            await user.should_see(marker="DIALOG_BUTTON_DOWNLOAD_RUN")
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert download_run_button.enabled, "Download button should be enabled before downloading"
            user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").click()
            await assert_notified(user, "Downloading ...", 30)
    
            # Step 6: Check download completes, QuPath project created, and QuPath launched
            await assert_notified(user, "Download and QuPath project creation completed.", 60 * 5)
            print_directory_structure(tmp_path, "execute")
    
            # Check for directory layout as expected
            run_dir = tmp_path / run.run_id
            assert run_dir.is_dir(), f"Expected run directory {run_dir} not found"
    
            subdirs = [d for d in run_dir.iterdir() if d.is_dir()]
            assert len(subdirs) == 3, f"Expected three subdirectories in {run_dir}, but found {len(subdirs)}"
    
            input_dir = run_dir / "input"
            assert input_dir.is_dir(), f"Expected input directory {input_dir} not found"
    
            results_dir = run_dir / SPOT_0_FILENAME.replace(".tiff", "")
            assert results_dir.is_dir(), f"Expected run results directory {results_dir} not found"
    
            qupath_dir = run_dir / "qupath"
            assert qupath_dir.is_dir(), f"Expected QuPath directory {qupath_dir} not found"
    
            # Check for input file having been downloaded
            input_file = input_dir / SPOT_0_FILENAME
            assert input_file.exists(), f"Expected input file {input_file} not found"
            assert input_file.stat().st_size == SPOT_0_FILESIZE, (
                f"Expected input file size {SPOT_0_FILESIZE}, but got {input_file.stat().st_size}"
            )
    
            # Check for files in the results directory
            files_in_results_dir = list(results_dir.glob("*"))
            assert len(files_in_results_dir) == 9, (
                f"Expected 9 files in {results_dir}, but found {len(files_in_results_dir)}: "
                f"{[f.name for f in files_in_results_dir]}"
            )
    
            print(f"Found files in {results_dir}:")
            for filename, expected_size, tolerance_percent in SPOT_0_EXPECTED_RESULT_FILES:
                file_path = results_dir / filename
                if file_path.exists():
                    actual_size = file_path.stat().st_size
                    print(f"  {filename}: {actual_size} bytes (expected: {expected_size} ±{tolerance_percent}%)")
                else:
                    print(f"  {filename}: NOT FOUND")
            for filename, expected_size, tolerance_percent in SPOT_0_EXPECTED_RESULT_FILES:
                file_path = results_dir / filename
                assert file_path.exists(), f"Expected file {filename} not found"
                actual_size = file_path.stat().st_size
                min_size = expected_size * (100 - tolerance_percent) // 100
                max_size = expected_size * (100 + tolerance_percent) // 100
>               assert min_size <= actual_size <= max_size, (
                    f"File size for {filename} ({actual_size} bytes) is outside allowed range "
                    f"({min_size} to {max_size} bytes, ±{tolerance_percent}% of {expected_size})"
                )
E               AssertionError: File size for tissue_qc_geojson_polygons.json (160668 bytes) is outside allowed range (283517 to 346520 bytes, ±10% of 315019)
E               assert 283517 <= 160668

.../aignostics/qupath/gui_test.py:287: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 7, 2025

Claude finished @olivermeyer's task —— View job


PR Review: Parameterize Platform Environment

Reviewing changes to CI/CD workflow configuration...

Review Tasks

  • Read and analyze the workflow file changes
  • Review .github/CLAUDE.md for CI/CD guidelines compliance
  • Verify workflow syntax and logic correctness
  • Check for security implications
  • Validate alignment with testing strategy
  • Provide comprehensive review feedback

🎯 Summary

This PR makes a critical correction to the CI/CD workflow by:

  1. Adding workflow_dispatch trigger with configurable platform_environment parameter
  2. Changing the default test environment from hardcoded "production" to dynamic 'staging' (with fallback)

Status: ✅ EXCELLENT CHANGE - This fixes a significant violation of documented CI/CD best practices.


📋 Detailed Analysis

1. What Changed

File: .github/workflows/ci-cd.yml

Addition (lines 14-23):

workflow_dispatch:
  inputs:
    platform_environment:
      description: 'Environment to test against'
      required: false
      default: 'staging'
      type: choice
      options:
        - staging
        - production

Change (line 97):

# Before:
platform_environment: "production"

# After:
platform_environment: ${{ inputs.platform_environment || 'staging' }}

2. Correctness Verification ✅

Workflow Dispatch Configuration:

  • ✅ Syntax is valid GitHub Actions YAML
  • type: choice restricts values to safe options only
  • required: false allows automatic triggers to work
  • default: 'staging' ensures safe default for manual runs
  • ✅ Description is clear and user-friendly

Parameterization Logic:

  • ✅ Expression ${{ inputs.platform_environment || 'staging' }} is correct
  • ✅ When inputs.platform_environment is null (PR/push/tag triggers), falls back to 'staging'
  • ✅ When user manually triggers with selection, uses selected value
  • ✅ Compatible with _test.yml input definition

Reusable Workflow Compatibility:

The _test.yml workflow already accepts this input:

inputs:
  platform_environment:
    description: 'Environment to test against, that is staging or production'
    required: false
    default: 'staging'
    type: string
  • ✅ Type compatibility: choice (in caller) → string (in callee) ✅
  • ✅ Double safety: Both workflows default to 'staging'

3. Alignment with Documentation 🎯

Per .github/CLAUDE.md lines 770-794:

Staging Environment

Use Cases:

  • PR testing (default for all PRs)
  • E2E test execution ✅
  • Feature validation ✅

Production Environment

Use Cases:

  • Scheduled tests only (every 24 hours) ✅
  • Release validation ✅
  • NEVER use in PR CI (staging only)

Before this PR: ❌ Violated policy - PRs tested against production
After this PR: ✅ Compliant - PRs test against staging by default

This PR fixes a critical documentation/implementation mismatch.

4. Behavior Impact Analysis

Previous Behavior (WRONG):

platform_environment: "production"
  • ❌ Every PR tested against production API
  • ❌ Violated documented best practice
  • ❌ Risk of production impact from experimental code
  • ❌ Inconsistent with scheduled test strategy

New Behavior (CORRECT):

platform_environment: ${{ inputs.platform_environment || 'staging' }}
Trigger Type inputs.platform_environment Resolved Value Correct?
PR (opened/sync) null 'staging' ✅ YES
Push to main null 'staging' ✅ YES
Tag v*.*.* null 'staging' ✅ YES
Release created null 'staging' ✅ YES
Manual (default) 'staging' 'staging' ✅ YES
Manual (select prod) 'production' 'production' ✅ YES (intentional)

Key Insight: This is a BREAKING CHANGE but in the RIGHT DIRECTION - it corrects previously incorrect behavior.

5. Security & Safety ✅

Credential Handling:

  • ✅ Both staging and production secrets already passed to _test.yml (lines 105-110)
  • _test.yml selects appropriate credentials based on environment parameter
  • ✅ No new secret exposure

Environment Selection:

  • type: choice prevents arbitrary environment strings
  • ✅ Cannot accidentally use invalid environment
  • ✅ Manual production access requires explicit selection
  • ✅ Default to staging is safest option

Risk Assessment:

  • ⚠️ Medium Risk: Changes test environment for all existing triggers
  • Mitigated: New environment (staging) is safer than old (production)
  • Intentional: Aligns with documented strategy
  • Reversible: Can be quickly fixed if issues arise

6. Conventional Commits Compliance ✅

Commit Messages:

9286acb: chore: parameterize platform_environment in CI/CD workflow
f59390f: chore(gha): Target staging with CI/CD workflow
  • ✅ Uses valid type: chore
  • ✅ Optional scope: (gha) for "GitHub Actions"
  • ✅ Descriptive commit messages
  • ✅ Follows repository convention

7. Testing Strategy Alignment ✅

No test marker issues: ✅ (No test code changed)

Test execution:

  • This PR's tests will validate the new behavior
  • Current PR CI run is using the new configuration
  • Verify in logs: AIGNOSTICS_PLATFORM_ENVIRONMENT=staging

Coverage: ✅ No source code changed, coverage requirements N/A

8. CI/CD Impact Assessment

Workflows Affected:

  • ci-cd.yml - Modified (this PR)
  • _test.yml - No changes needed (already compatible)
  • test-scheduled-staging.yml - No changes needed (explicitly passes 'staging')
  • test-scheduled-production.yml - No changes needed (explicitly passes 'production')

Behavior Matrix:

Workflow Before After Impact
PR CI Production ❌ Staging ✅ FIXED
Push to main Production ❌ Staging ✅ FIXED
Tag release Production ❌ Staging ✅ IMPROVED
Manual trigger N/A Staging (default) ✅ NEW FEATURE
Scheduled staging Staging ✅ Staging ✅ NO CHANGE
Scheduled production Production ✅ Production ✅ NO CHANGE

BetterStack Monitoring: ✅ No impact (scheduled tests unchanged)


💡 Observations & Recommendations

Strengths 👏

  1. Fixes Critical Issue: Corrects production usage in PRs (violated policy)
  2. Adds Flexibility: Manual trigger capability for production testing when needed
  3. Safe Defaults: Double fallback to staging (caller + callee)
  4. Type Safety: type: choice prevents invalid values
  5. Backward Compatible: Existing scheduled workflows unaffected
  6. Well Documented: PR description accurately describes changes

Potential Considerations (Non-Blocking)

1. Documentation Update (Optional)

Consider adding a note to .github/CLAUDE.md about the manual trigger capability.

Suggested Addition (around line 1031, after "### Manual Testing with Claude"):

### Manual CI/CD Workflow Runs

You can manually trigger the main CI/CD pipeline with a custom environment:

**Via GitHub UI**:
1. Actions → CI/CD → Run workflow
2. Select branch
3. Choose environment: `staging` (default) or `production`

**Via GitHub CLI**:
```bash
# Run with staging (default)
gh workflow run ci-cd.yml --ref my-branch

# Run with production
gh workflow run ci-cd.yml --ref my-branch -f platform_environment=production

Use Cases:

  • Testing specific branch against production (use with caution)
  • Validating fix against production environment
  • Release candidate validation

**Decision**: Not required for this PR - can be added in follow-up.

##### 2. Add Safety Check for PR + Production (Future Enhancement)

Consider adding a validation step to prevent manual production testing in PR context:

```yaml
- name: Validate environment for PR
  if: github.event_name == 'pull_request' && inputs.platform_environment == 'production'
  run: |
    echo "::error::Production environment is not allowed for PR testing"
    exit 1

Decision: Not necessary now - the default mechanism already prevents this in normal flows. Only manual override can select production, which requires intentional action.

3. Team Communication (Important)

Recommendation: After merge, consider notifying the team about this change:

Heads up: PR #248 changes CI/CD to test against staging (was previously production). This aligns with our documented policy. If you notice any test behavior changes, this is expected.

Why: Developers may have unknowingly relied on production testing in PRs. Setting expectations prevents confusion.


🚦 Final Assessment

Category Status Notes
Syntax ✅ Pass Valid GitHub Actions YAML
Logic ✅ Pass Fallback chain works correctly
Safety ✅ Pass Defaults to safer environment
Documentation Alignment ✅ Pass Fixes documented policy violation
Breaking Changes ⚠️ Yes But GOOD - fixes incorrect behavior
Security ✅ Pass No new security concerns
Testing ✅ Pass This PR validates the change
Commit Convention ✅ Pass Follows conventional commits
CI/CD Best Practices ✅ Pass Follows GitHub Actions patterns

✅ Recommendation: APPROVED

This PR:

  1. ✅ Fixes a critical misalignment where PRs incorrectly tested against production
  2. ✅ Adds useful manual trigger capability
  3. ✅ Maintains backward compatibility for scheduled tests
  4. ✅ Follows GitHub Actions best practices
  5. ✅ Aligns with documented CI/CD strategy
  6. ✅ Uses safe defaults and type validation

No blocking issues found. This is a quality improvement that corrects a previous misconfiguration.


🚀 Next Steps

After Merge:

  1. ✅ Monitor this PR's CI run to confirm staging usage (logs should show AIGNOSTICS_PLATFORM_ENVIRONMENT=staging)
  2. ✅ Optionally test manual workflow_dispatch with both environments
  3. ⚠️ Consider team notification about environment change
  4. 📝 Optional: Add manual trigger documentation to .github/CLAUDE.md (future PR)

Verification Commands (after merge):

# Verify staging is used in CI logs
gh run view <run-id> --log | grep AIGNOSTICS_PLATFORM_ENVIRONMENT

# Test manual trigger with staging (default)
gh workflow run ci-cd.yml --ref main

# Test manual trigger with production (careful!)
gh workflow run ci-cd.yml --ref main -f platform_environment=production

Great work, @olivermeyer! This PR improves both flexibility and correctness of the CI/CD pipeline. 👏

The previous bot review was thorough and accurate - I concur with its assessment. This change is exactly what's needed to align implementation with documented best practices for medical device software CI/CD.


@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 7, 2025

@olivermeyer olivermeyer merged commit fe80fef into main Nov 7, 2025
22 of 27 checks passed
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.

3 participants