Skip to content

Daily public validation cadence #72

Daily public validation cadence

Daily public validation cadence #72

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