Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions .github/actions/build-mpas/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: 'Build MPAS'
description: 'Build MPAS-Atmosphere with specified compiler configuration'

inputs:
compiler:
description: 'Compiler family (gcc, nvhpc, oneapi, etc.)'
required: true
use-pio:
description: 'Use PIO library (true/false)'
required: false
default: 'false'
openacc:
description: 'Enable OpenACC (true/false)'
required: false
default: 'false'
precision:
description: 'Floating-point precision (single or double)'
required: false
default: 'single'
build-timeout:
description: 'Build timeout in minutes'
required: false
default: '20'

outputs:
executable:
description: 'Path to built executable'
value: ${{ steps.build.outputs.executable }}

runs:
using: 'composite'
steps:
- name: Build MPAS-A
id: build
shell: bash
run: |
# Source container environment if available
# This sets NETCDF, PNETCDF, PIO, and other library paths
if [ -f /container/config_env.sh ]; then
echo "Sourcing container environment from /container/config_env.sh"
source /container/config_env.sh
fi

# MPAS Makefile uses `cpp` for Registry preprocessing.
# Some containers (e.g. openSUSE Leap) don't ship it separately.
if ! command -v cpp &>/dev/null; then
echo "cpp not found, installing..."
if command -v zypper &>/dev/null; then
zypper install -y --no-recommends cpp
elif command -v dnf &>/dev/null; then
dnf install -y cpp
elif command -v apt-get &>/dev/null; then
apt-get update && apt-get install -y cpp
fi
fi

# Map compiler input to COMPILER_FAMILY if not already set
# The container should set this, but we provide a fallback
if [ -z "${COMPILER_FAMILY}" ]; then
case "${{ inputs.compiler }}" in
gcc|gfortran|gnu) export COMPILER_FAMILY="gcc" ;;
nvhpc|nvfortran) export COMPILER_FAMILY="nvhpc" ;;
oneapi|intel|ifx) export COMPILER_FAMILY="oneapi" ;;
llvm|flang|clang) export COMPILER_FAMILY="clang" ;;
*) export COMPILER_FAMILY="${{ inputs.compiler }}" ;;
esac
fi

# Set up I/O configuration
if [ "${{ inputs.use-pio }}" = "true" ]; then
export PIO_ROOT=${PIO_ROOT:-/container/pio}
export USE_PIO2=true
else
unset PIO
export USE_PIO2=false
fi

# Set up accelerator configuration
if [ "${{ inputs.openacc }}" = "true" ]; then
export OPENACC=true
fi

echo "Build configuration:"
echo " Compiler input: ${{ inputs.compiler }}"
echo " COMPILER_FAMILY: ${COMPILER_FAMILY}"
echo " NETCDF: ${NETCDF:-not set}"
echo " PNETCDF: ${PNETCDF:-not set}"
echo " USE_PIO2: ${USE_PIO2}"
echo " OPENACC: ${OPENACC:-false}"
echo " PRECISION: ${{ inputs.precision }}"

# Settings from .github/ci-config.env — edit that file to change targets/workarounds
CI_CONFIG="${GITHUB_WORKSPACE}/.github/ci-config.env"
if [ -f "${CI_CONFIG}" ]; then
source "${CI_CONFIG}"
fi

# Map COMPILER_FAMILY to make target via ci-config.env lookup
VARNAME="MAKE_TARGET_${COMPILER_FAMILY}"
MAKE_TARGET="${!VARNAME}"
if [ -z "${MAKE_TARGET}" ]; then
echo "::error::No make target for COMPILER_FAMILY=${COMPILER_FAMILY}. Add MAKE_TARGET_${COMPILER_FAMILY}= to .github/ci-config.env"
exit 1
fi

EXTRA_MAKE_FLAGS=""
if [ "${COMPILER_FAMILY}" = "nvhpc" ]; then
ARCH="${NVHPC_TARGET_ARCH:--tp=px}"
sed -i "/^nvhpc:/,/^[a-z]/ s/\"FFLAGS_OPT = /\"FFLAGS_OPT = ${ARCH} /" Makefile
sed -i "/^nvhpc:/,/^[a-z]/ s/\"CFLAGS_OPT = /\"CFLAGS_OPT = ${ARCH} /" Makefile
sed -i "/^nvhpc:/,/^[a-z]/ s/\"CXXFLAGS_OPT = /\"CXXFLAGS_OPT = ${ARCH} /" Makefile
sed -i "/^nvhpc:/,/^[a-z]/ s/\"LDFLAGS_OPT = /\"LDFLAGS_OPT = ${ARCH} /" Makefile
EXTRA_MAKE_FLAGS="${NVHPC_EXTRA_MAKE_FLAGS}"
elif [ "${COMPILER_FAMILY}" = "oneapi" ]; then
EXTRA_MAKE_FLAGS="${ONEAPI_EXTRA_MAKE_FLAGS}"
fi

echo " Make target: ${MAKE_TARGET}"
echo " Extra make flags: ${EXTRA_MAKE_FLAGS:-<none>}"
echo " Parallel jobs: ${MAKE_J_PROCS:-$(nproc)}"

PRECISION_FLAG=""
if [ "${{ inputs.precision }}" = "double" ]; then
PRECISION_FLAG="PRECISION=double"
fi

timeout ${{ inputs.build-timeout }}m \
make ${MAKE_TARGET} CORE=atmosphere ${EXTRA_MAKE_FLAGS} ${PRECISION_FLAG} --jobs ${MAKE_J_PROCS:-$(nproc)}

# Set output
echo "executable=$(pwd)/atmosphere_model" >> $GITHUB_OUTPUT

- name: Verify executable
shell: bash
run: |
if [ ! -f atmosphere_model ]; then
echo "ERROR: atmosphere_model not found!"
exit 1
fi
ls -la atmosphere_model
file atmosphere_model
97 changes: 97 additions & 0 deletions .github/actions/download-testdata/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: 'Download Test Data'
description: 'Download and extract an MPAS test case archive from GitHub releases'

inputs:
resolution:
description: 'Test case resolution (e.g., 240km, 120km). Used to look up RELEASE_TESTDATA_{RES} in ci-config.env.'
required: true
dest-dir:
description: 'Destination directory name for the extracted test case'
required: false
default: ''

outputs:
case-dir:
description: 'Path to the extracted test case directory'
value: ${{ steps.extract.outputs.case-dir }}

runs:
using: composite
steps:
- name: Resolve release tag and archive
id: resolve
shell: bash
run: |
RESOLUTION="${{ inputs.resolution }}"
ARCHIVE="${RESOLUTION}.tar.gz"

CI_CONFIG="${GITHUB_WORKSPACE}/.github/ci-config.env"
if [ ! -f "${CI_CONFIG}" ]; then
echo "::error::ci-config.env not found at ${CI_CONFIG}"
exit 1
fi
source "${CI_CONFIG}"

RES_UPPER=$(echo "${RESOLUTION}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
TAG_VAR="RELEASE_TESTDATA_${RES_UPPER}"
TAG="${!TAG_VAR}"

if [ -z "${TAG}" ]; then
echo "::error::No release tag for resolution '${RESOLUTION}'. Add ${TAG_VAR}= to ci-config.env."
exit 1
fi

REPO="${DATA_REPOSITORY:-${GITHUB_REPOSITORY}}"
URL="https://github.com/${REPO}/releases/download/${TAG}/${ARCHIVE}"

echo "release-tag=${TAG}" >> $GITHUB_OUTPUT
echo "archive=${ARCHIVE}" >> $GITHUB_OUTPUT
echo "url=${URL}" >> $GITHUB_OUTPUT
echo "Resolved: ${TAG_VAR}=${TAG} → ${URL}"

- name: Cache test case archive
id: cache
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
with:
path: ${{ steps.resolve.outputs.archive }}
key: testdata-${{ steps.resolve.outputs.release-tag }}

- name: Download test case archive
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
run: |
URL="${{ steps.resolve.outputs.url }}"
ARCHIVE="${{ steps.resolve.outputs.archive }}"
echo "Downloading ${ARCHIVE} from ${URL}..."
curl -fsSL --retry 5 --retry-delay 5 "${URL}" -o "${ARCHIVE}"
echo "Downloaded $(du -h "${ARCHIVE}" | cut -f1)"

- name: Extract test case
id: extract
shell: bash
run: |
ARCHIVE="${{ steps.resolve.outputs.archive }}"
DEST="${{ inputs.dest-dir }}"

echo "Cache hit: ${{ steps.cache.outputs.cache-hit }}"
echo "Archive: ${ARCHIVE} ($(du -h "${ARCHIVE}" | cut -f1))"

tar xzf "${ARCHIVE}"

CASE_DIR=$(tar tzf "${ARCHIVE}" 2>/dev/null | head -1 | cut -d/ -f1 || true)
if [ -z "${CASE_DIR}" ]; then
CASE_DIR=$(ls -td */ 2>/dev/null | head -1 | tr -d '/')
fi

if [ -z "${CASE_DIR}" ] || [ ! -d "${CASE_DIR}" ]; then
echo "::error::Failed to extract test case from ${ARCHIVE}"
exit 1
fi

if [ -n "${DEST}" ] && [ "${DEST}" != "${CASE_DIR}" ]; then
mv "${CASE_DIR}" "${DEST}"
CASE_DIR="${DEST}"
fi

echo "case-dir=${CASE_DIR}" >> $GITHUB_OUTPUT
echo "Extracted test case to: ${CASE_DIR}"
100 changes: 100 additions & 0 deletions .github/actions/ect-summary/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: 'ECT Summary'
description: >
Generate a consolidated Ensemble Consistency Test results table from
enriched result files produced by the validate-ect action. Writes a
Markdown table to $GITHUB_STEP_SUMMARY with auto-discovered columns.

inputs:
results-path:
description: 'Directory containing downloaded ect-result-* artifact subdirectories'
required: true

runs:
using: 'composite'
steps:
- name: Generate summary table
shell: bash
run: |
RESULTS_PATH="${{ inputs.results-path }}"

# Collect all result files
RESULT_FILES=()
for f in "${RESULTS_PATH}"/ect-result-*/ect-result.txt; do
[ -f "$f" ] && RESULT_FILES+=("$f")
done

if [ ${#RESULT_FILES[@]} -eq 0 ]; then
echo "::warning::No ECT result files found in ${RESULTS_PATH}"
echo "## Ensemble Consistency Test (ECT) Results" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No results available." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi

# Discover column names from the first result file (all keys except "result")
COLUMNS=()
while IFS='=' read -r key value; do
[ -z "$key" ] && continue
if [ "$key" != "result" ]; then
COLUMNS+=("$key")
fi
done < "${RESULT_FILES[0]}"

# Build header row
HEADER="| "
SEPARATOR="| "
for col in "${COLUMNS[@]}"; do
COL_TITLE=$(echo "$col" | sed 's/.*/\u&/')
HEADER+="${COL_TITLE} | "
SEPARATOR+="--- | "
done
HEADER+="Result |"
SEPARATOR+="--- |"

# Build data rows
PASS=0 FAIL=0 ERROR=0 SKIP=0 TOTAL=0
ROWS=""
for f in "${RESULT_FILES[@]}"; do
TOTAL=$((TOTAL + 1))

# Parse key=value pairs
declare -A DATA=()
while IFS='=' read -r key value; do
[ -z "$key" ] && continue
DATA["$key"]="$value"
done < "$f"

RESULT="${DATA[result]}"
case "$RESULT" in
PASSED) ICON="PASSED"; PASS=$((PASS + 1)) ;;
FAILED) ICON="**FAILED**"; FAIL=$((FAIL + 1)) ;;
SKIPPED) ICON="SKIPPED"; SKIP=$((SKIP + 1)) ;;
*) ICON="ERROR"; ERROR=$((ERROR + 1)) ;;
esac

ROW="| "
for col in "${COLUMNS[@]}"; do
ROW+="${DATA[$col]:-—} | "
done
ROW+="${ICON} |"
ROWS+="${ROW}"$'\n'

unset DATA
done

SORTED_ROWS=$(echo "$ROWS" | sort)

{
echo "## Ensemble Consistency Test (ECT) Results"
echo ""
echo "$HEADER"
echo "$SEPARATOR"
echo "$SORTED_ROWS"
echo ""
echo "**Total: ${TOTAL}** — ${PASS} passed, ${FAIL} failed, ${ERROR} error, ${SKIP} skipped"

if [ ${FAIL} -gt 0 ] || [ ${ERROR} -gt 0 ]; then
echo ""
echo "> One or more ECT validations failed. Check individual ECT Validate job logs for PyCECT details."
fi
} >> "$GITHUB_STEP_SUMMARY"
47 changes: 47 additions & 0 deletions .github/actions/mpas-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: 'Get MPAS Version'
description: >
Read the MPAS version string from src/core_atmosphere/Registry.xml.
Strict — fails the workflow if the file is missing or the version
attribute cannot be parsed (no silent "unknown" fallback).

inputs:
registry-path:
description: 'Path to Registry.xml relative to GITHUB_WORKSPACE'
required: false
default: 'src/core_atmosphere/Registry.xml'

outputs:
version:
description: 'MPAS version string (e.g., 8.4.0)'
value: ${{ steps.extract.outputs.version }}

runs:
using: 'composite'
steps:
- id: extract
shell: bash
run: |
python3 - "${{ inputs.registry-path }}" <<'PYEOF'
import os
import sys
import xml.etree.ElementTree as ET

path = sys.argv[1]
if not os.path.isfile(path):
print(f"::error::MPAS Registry not found at {path}")
sys.exit(1)
try:
root = ET.parse(path).getroot()
except ET.ParseError as e:
print(f"::error::Could not parse {path}: {e}")
sys.exit(1)

version = root.attrib.get('version')
if not version:
print(f"::error::No version attribute on <registry> in {path}")
sys.exit(1)

with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f"version={version}\n")
print(f"MPAS version: {version}")
PYEOF
Loading