Repository navigation
Daily public validation cadence #72
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: Daily public validation cadence | |
| on: | |
| schedule: | |
| - cron: '0 7 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: validation-pilot-main | |
| cancel-in-progress: true | |
| jobs: | |
| discover: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| build_readiness_matrix: ${{ steps.discovery.outputs.build_readiness_matrix }} | |
| live_service_matrix: ${{ steps.discovery.outputs.live_service_matrix }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: .github/scripts/requirements.txt | |
| - name: Install validation script dependencies | |
| run: python -m pip install -r .github/scripts/requirements.txt | |
| - name: Discover full sample inventory | |
| id: discovery | |
| run: | | |
| set -euo pipefail | |
| python .github/scripts/discover-validation-samples.py \ | |
| --manifest "$RUNNER_TEMP/validation-manifest.json" \ | |
| --matrix "$RUNNER_TEMP/validation-matrix.json" \ | |
| --build-readiness-matrix "$RUNNER_TEMP/validation-build-readiness-matrix.json" \ | |
| --live-service-matrix "$RUNNER_TEMP/validation-live-service-matrix.json" > "$RUNNER_TEMP/discovery.json" | |
| printf 'build_readiness_matrix=%s\n' "$(cat "$RUNNER_TEMP/validation-build-readiness-matrix.json")" >> "$GITHUB_OUTPUT" | |
| printf 'live_service_matrix=%s\n' "$(cat "$RUNNER_TEMP/validation-live-service-matrix.json")" >> "$GITHUB_OUTPUT" | |
| cat "$RUNNER_TEMP/discovery.json" | |
| - name: Persist discovered manifest | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: validation-pilot-manifest | |
| path: ${{ runner.temp }}/validation-manifest.json | |
| if-no-files-found: error | |
| retention-days: 90 | |
| build-readiness: | |
| needs: discover | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 32 | |
| matrix: ${{ fromJSON(needs.discover.outputs.build_readiness_matrix) }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 | |
| if: matrix.validator_language == 'csharp' | |
| with: | |
| dotnet-version: 8.0.x | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | |
| if: matrix.validator_language == 'python' | |
| with: | |
| python-version: '3.12' | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| if: matrix.validator_language == 'typescript' | |
| with: | |
| node-version: '20' | |
| - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4.9.1 | |
| if: matrix.validator_language == 'java' | |
| with: | |
| distribution: temurin | |
| java-version: '17' | |
| - name: Install yq | |
| if: matrix.eligible | |
| run: | | |
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 | |
| sudo chmod +x /usr/local/bin/yq | |
| - name: Run normalized build-readiness validation | |
| if: always() | |
| run: | | |
| set -uo pipefail | |
| mkdir -p "$RUNNER_TEMP/pilot-result" | |
| args=( | |
| --sample-id "${{ matrix.id }}" | |
| --language "${{ matrix.language }}" | |
| --validator-language "${{ matrix.validator_language }}" | |
| --shape "${{ matrix.shape }}" | |
| --sample-path "${{ matrix.path }}" | |
| --output "$RUNNER_TEMP/pilot-result/sample-result.json" | |
| --diagnostic "$RUNNER_TEMP/pilot-result/diagnostics.log" | |
| ) | |
| if [ "${{ matrix.eligible }}" != "true" ]; then | |
| args+=(--skip-reason "${{ matrix.skip_reason }}") | |
| fi | |
| python .github/scripts/run-validation-pilot.py "${args[@]}" | |
| - name: Persist sample result and diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: validation-pilot-${{ matrix.id }} | |
| path: ${{ runner.temp }}/pilot-result | |
| if-no-files-found: error | |
| retention-days: 30 | |
| live-service: | |
| needs: discover | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| # Legacy external identifier: renaming it also requires an Entra OIDC subject migration. | |
| environment: L4-validation | |
| permissions: | |
| contents: read | |
| id-token: write | |
| env: | |
| # P4.1 never provisions. P4.2 owns any future cold-provisioning caller. | |
| SKIP_PROVISION: "true" | |
| AZURE_AI_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }} | |
| FOUNDRY_PROJECT_ENDPOINT: ${{ vars.AZURE_AI_PROJECT_ENDPOINT }} | |
| MODEL_DEPLOYMENT: ${{ vars.MODEL_DEPLOYMENT }} | |
| FOUNDRY_MODEL_DEPLOYMENT: ${{ vars.MODEL_DEPLOYMENT }} | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 4 | |
| matrix: ${{ fromJSON(needs.discover.outputs.live_service_matrix) }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 | |
| if: matrix.validator_language == 'csharp' | |
| with: | |
| dotnet-version: 8.0.x | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | |
| if: matrix.validator_language == 'python' | |
| with: | |
| python-version: '3.12' | |
| - name: Install yq | |
| run: | | |
| sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64 | |
| sudo chmod +x /usr/local/bin/yq | |
| - name: Install declared live-service Python runtime dependencies | |
| if: matrix.validator_language == 'python' | |
| run: python -m pip install -r "${{ matrix.path }}/requirements.txt" | |
| - name: Azure login | |
| uses: azure/login@7184910d9eb2b1c5e48f7073824a90609bb9b6d6 # v2.3.1 | |
| with: | |
| client-id: ${{ vars.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ vars.AZURE_TENANT_ID }} | |
| subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} | |
| - name: Run normalized live-service validation | |
| if: always() | |
| run: | | |
| set -uo pipefail | |
| mkdir -p "$RUNNER_TEMP/pilot-result" | |
| python .github/scripts/run-validation-pilot.py \ | |
| --sample-id "${{ matrix.id }}" \ | |
| --language "${{ matrix.language }}" \ | |
| --validator-language "${{ matrix.validator_language }}" \ | |
| --shape "${{ matrix.shape }}" \ | |
| --sample-path "${{ matrix.path }}" \ | |
| --output "$RUNNER_TEMP/pilot-result/sample-result.json" \ | |
| --diagnostic "$RUNNER_TEMP/pilot-result/diagnostics.log" \ | |
| --run-live-service | |
| - name: Persist sample result and diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: validation-pilot-${{ matrix.id }} | |
| path: ${{ runner.temp }}/pilot-result | |
| if-no-files-found: error | |
| retention-days: 30 | |
| completeness: | |
| if: always() | |
| needs: [discover, build-readiness, live-service] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| with: | |
| pattern: validation-pilot-* | |
| path: ${{ runner.temp }}/pilot-artifacts | |
| - name: Validate complete normalized result set | |
| run: | | |
| set -uo pipefail | |
| manifest="$RUNNER_TEMP/pilot-artifacts/validation-pilot-manifest/validation-manifest.json" | |
| if python .github/scripts/validate-validation-pilot-results.py \ | |
| --manifest "$manifest" \ | |
| --artifacts "$RUNNER_TEMP/pilot-artifacts"; then | |
| completeness_rc=0 | |
| else | |
| completeness_rc=$? | |
| fi | |
| cp "$manifest" "$RUNNER_TEMP/pilot-artifacts/manifest.json" | |
| exit "$completeness_rc" | |
| - name: Persist run metadata and normalized summary | |
| if: always() | |
| run: | | |
| python -c 'import json,os; from pathlib import Path; p=Path(os.environ["RUNNER_TEMP"])/"pilot-artifacts"/"run-metadata.json"; p.write_text(json.dumps({"schema_version":2,"repository":os.environ["GITHUB_REPOSITORY"],"workflow":os.environ["GITHUB_WORKFLOW"],"run_id":os.environ["GITHUB_RUN_ID"],"run_attempt":os.environ["GITHUB_RUN_ATTEMPT"],"sha":os.environ["GITHUB_SHA"],"ref":os.environ["GITHUB_REF"],"completed_at":__import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat().replace("+00:00","Z")},indent=2)+"\n")' | |
| continue-on-error: true | |
| - name: Upload normalized run | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: validation-pilot-run-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/pilot-artifacts | |
| if-no-files-found: error | |
| retention-days: 90 | |
| report: | |
| needs: completeness | |
| if: ${{ always() && !cancelled() }} | |
| uses: ./.github/workflows/validation-report.yml | |
| with: | |
| results-artifact: validation-pilot-run-${{ github.run_id }}-${{ github.run_attempt }} | |
| manifest-path: manifest.json | |
| # Publishes a durable "current status of every sample" dashboard to GitHub | |
| # Pages. Unlike `report` (a run-scoped summary that only lives on this run's | |
| # job summary page), this is the one stable URL for "what is passing right | |
| # now". It intentionally still publishes on an incomplete completeness | |
| # result (with a visible banner) rather than skip -- see | |
| # render-validation-dashboard.py -- so a broken run is never silently masked | |
| # by the previous day's stale deployment. | |
| # | |
| # Scoped to the canonical repository's default branch so a fork enabling its | |
| # own Pages site can't be confused with this workflow's publication, and | |
| # given its own `pages` concurrency group (queued, not cancel-in-progress) | |
| # so a manual dispatch can never race a scheduled run's deployment. | |
| publish-dashboard: | |
| needs: completeness | |
| if: >- | |
| always() && !cancelled() && | |
| github.repository == 'microsoft-foundry/foundry-samples' && | |
| github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| concurrency: | |
| group: pages | |
| cancel-in-progress: false | |
| environment: | |
| name: github-pages | |
| url: ${{ steps.deployment.outputs.page_url }} | |
| permissions: | |
| contents: read | |
| actions: read | |
| pages: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Download normalized run | |
| id: download | |
| continue-on-error: true | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| with: | |
| name: validation-pilot-run-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/pilot-artifacts | |
| - name: Collect per-sample job links | |
| if: always() && steps.download.outcome == 'success' | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs" \ | |
| --paginate -q '.jobs[] | {name, html_url}' \ | |
| > "$RUNNER_TEMP/jobs.ndjson" | |
| python .github/scripts/collect-job-links.py \ | |
| --jobs-file "$RUNNER_TEMP/jobs.ndjson" \ | |
| --output "$RUNNER_TEMP/pilot-artifacts/job-links.json" | |
| - name: Collect per-sample artifact links | |
| if: always() && steps.download.outcome == 'success' | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/artifacts" \ | |
| --paginate -q '.artifacts[] | {name, id}' \ | |
| > "$RUNNER_TEMP/artifacts.ndjson" | |
| python .github/scripts/collect-artifact-links.py \ | |
| --artifacts-file "$RUNNER_TEMP/artifacts.ndjson" \ | |
| --repository "$GITHUB_REPOSITORY" \ | |
| --run-id "$GITHUB_RUN_ID" \ | |
| --run-attempt "$GITHUB_RUN_ATTEMPT" \ | |
| --output "$RUNNER_TEMP/pilot-artifacts/artifact-links.json" | |
| - name: Render dashboard | |
| # Always renders *something* deployable, even when the run artifact | |
| # above couldn't be downloaded (e.g. discovery or completeness | |
| # failed before uploading it). Without this fallback, this step -- | |
| # and therefore the deploy-pages step below -- would simply never | |
| # run, leaving whatever was deployed by a previous run looking like | |
| # the current status. A visible "couldn't retrieve results" page is | |
| # always preferable to a silently stale one. | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$RUNNER_TEMP/site" | |
| if [ "${{ steps.download.outcome }}" = "success" ] && \ | |
| python .github/scripts/render-validation-dashboard.py \ | |
| --results-dir "$RUNNER_TEMP/pilot-artifacts" \ | |
| --expected-samples "$RUNNER_TEMP/pilot-artifacts/manifest.json" \ | |
| --output "$RUNNER_TEMP/site/index.html" \ | |
| --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ | |
| --job-links "$RUNNER_TEMP/pilot-artifacts/job-links.json" \ | |
| --artifact-links "$RUNNER_TEMP/pilot-artifacts/artifact-links.json" \ | |
| --codeowners .github/CODEOWNERS; then | |
| : | |
| else | |
| python .github/scripts/render-fallback-dashboard.py \ | |
| --output "$RUNNER_TEMP/site/index.html" \ | |
| --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" | |
| fi | |
| - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 | |
| - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 | |
| with: | |
| path: ${{ runner.temp }}/site | |
| - id: deployment | |
| uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 |