Skip to content
Merged
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
207 changes: 182 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
merge_group:
branches: [main]
types: [checks_requested]
Expand All @@ -20,12 +20,15 @@ concurrency:
jobs:
changes:
name: Detect Build Scope
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
compile: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.compile }}
fast: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.fast }}
lua: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.lua }}
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
Expand All @@ -49,10 +52,43 @@ jobs:
- "CMakeLists.txt"
- "CMakePresets.json"
- ".github/workflows/ci.yml"
- ".github/workflows/reusable-build-windows.yml"
- ".github/workflows/reusable-build-linux.yml"
fast:
- "src/**"
- "tests/**"
- "modules/**"
- "mods/**"
- "data/**"
- "cmake/**"
- "vc18/**"
- "vcpkg.json"
- "vcpkg-configuration.json"
- "CMakeLists.txt"
- "CMakePresets.json"
- ".github/**"
- ".yamllint.yaml"
- "**/*.lua"
- "**/*.xml"
lua:
- "src/**"
- "tests/**"
- "modules/**"
- "mods/**"
- "data/**"
- "cmake/**"
- "vc18/**"
- "vcpkg.json"
- "vcpkg-configuration.json"
- "CMakeLists.txt"
- "CMakePresets.json"
- ".github/workflows/ci.yml"
- ".github/workflows/reusable-build-linux.yml"
- ".github/workflows/reusable-tests-lua.yml"

checks:
name: Fast Checks
needs: changes
if: needs.changes.outputs.fast == 'true'
permissions:
contents: read
checks: write
Expand All @@ -61,27 +97,117 @@ jobs:

tests-lua:
name: Lua Syntax
needs: changes
if: needs.changes.outputs.lua == 'true'
permissions:
contents: read
uses: ./.github/workflows/reusable-tests-lua.yml

build-windows:
name: Build - Windows
build-linux:
name: Build - Linux
needs: [changes, checks, tests-lua]
if: needs.changes.outputs.compile == 'true' && needs.checks.result == 'success' && needs.tests-lua.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
permissions:
contents: read
packages: read
uses: ./.github/workflows/reusable-build-windows.yml
uses: ./.github/workflows/reusable-build-linux.yml

smoke-linux:
name: Client Startup Smoke - Linux
needs: [changes, build-linux]
if: needs.changes.outputs.compile == 'true' && needs.build-linux.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
steps:
- name: Checkout repository resources
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false

- name: Download Linux release artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: linux-linux-release
path: build/linux-release/bin

- name: Install headless runtime dependencies
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y \
libgl1 \
libgl1-mesa-dri \
libglu1-mesa \
libopenal1 \
libx11-6 \
libxcursor1 \
libxi6 \
libxinerama1 \
libxrandr2 \
xauth \
xvfb

- name: Verify client artifact dependencies
shell: bash
run: |
set -euo pipefail
client="build/linux-release/bin/otclient"
test -f "${client}"
chmod +x "${client}"
if ldd "${client}" | tee "${RUNNER_TEMP}/otclient-ldd.log" | grep -q "not found"; then
echo "::error title=Missing runtime dependency::Linux client artifact has unresolved shared libraries."
exit 1
fi

- name: Launch client under virtual display
env:
ALSOFT_DRIVERS: "null"
LIBGL_ALWAYS_SOFTWARE: "1"
shell: bash
run: |
set -euo pipefail
client="./build/linux-release/bin/otclient"
user_dir="${RUNNER_TEMP}/otclient-smoke-user"
log="${RUNNER_TEMP}/otclient-smoke.log"
mkdir -p "${user_dir}"

set +e
timeout --signal=TERM --kill-after=5s 20s \
xvfb-run -a -s "-screen 0 1280x720x24" \
"${client}" --user-dir="${user_dir}" >"${log}" 2>&1
status=$?
set -e

cat "${log}"
if [[ "${status}" -ne 124 ]]; then
echo "::error title=Client startup smoke failed::Expected OTClient to stay alive until the 20s smoke timeout; exit status was ${status}."
exit 1
fi

echo "OTClient remained alive for the bounded 20s headless startup window."

- name: Upload startup smoke evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: linux-client-startup-smoke
path: |
${{ runner.temp }}/otclient-smoke.log
${{ runner.temp }}/otclient-ldd.log
if-no-files-found: warn

required:
name: CI / Required
if: always()
if: always() && (github.event_name != 'pull_request' || github.event.action != 'closed')
needs:
- changes
- checks
- tests-lua
- build-windows
- build-linux
- smoke-linux
runs-on: ubuntu-latest
permissions: {}
steps:
Expand All @@ -100,31 +226,62 @@ jobs:
jobs = json.loads(os.environ["NEEDS_JSON"])
rejected = {}

for name in ("changes", "checks", "tests-lua"):
result = jobs[name].get("result")
if result != "success":
rejected[name] = f"expected success, got {result}"
changes_result = jobs["changes"].get("result")
if changes_result != "success":
rejected["changes"] = f"expected success, got {changes_result}"

outputs = jobs["changes"].get("outputs", {})
scopes = {
"compile": outputs.get("compile"),
"fast": outputs.get("fast"),
"lua": outputs.get("lua"),
}
for scope, value in scopes.items():
if value not in {"true", "false"}:
rejected[f"scope:{scope}"] = (
f"expected true or false, got {value!r}"
)

for job_name, scope_name in (("checks", "fast"), ("tests-lua", "lua")):
result = jobs[job_name].get("result")
scope_value = scopes[scope_name]
if scope_value == "true" and result != "success":
rejected[job_name] = (
f"scope {scope_name}=true requires success, got {result}"
)
elif scope_value == "false" and result != "skipped":
rejected[job_name] = (
f"scope {scope_name}=false requires skipped, got {result}"
)

compile_scope = jobs["changes"].get("outputs", {}).get("compile")
if compile_scope not in {"true", "false"}:
rejected["scope:compile"] = (
f"expected true or false, got {compile_scope!r}"
)
compile_scope = scopes["compile"]
if compile_scope == "true":
for required_scope in ("fast", "lua"):
if scopes[required_scope] != "true":
rejected[f"scope:{required_scope}"] = (
f"compile=true requires {required_scope}=true, got {scopes[required_scope]!r}"
)

is_draft = os.environ["IS_DRAFT"] == "true"
windows_result = jobs["build-windows"].get("result")
if windows_result not in {"success", "skipped"}:
rejected["build-windows"] = (
f"unexpected conclusion {windows_result}"
)
elif not is_draft and compile_scope == "true" and windows_result != "success":
rejected["build-windows"] = (
f"scope compile=true requires success, got {windows_result}"
)
for job_name in ("build-linux", "smoke-linux"):
result = jobs[job_name].get("result")
if result not in {"success", "skipped"}:
rejected[job_name] = f"unexpected conclusion {result}"
elif not is_draft and compile_scope == "true" and result != "success":
rejected[job_name] = (
f"scope compile=true requires success, got {result}"
)
elif (is_draft or compile_scope == "false") and result != "skipped":
rejected[job_name] = (
f"draft or compile=false requires skipped, got {result}"
)

print("Required job results:")
for name in sorted(jobs):
print(f"- {name}: {jobs[name].get('result')}")
print("Detected scopes:")
for name in sorted(scopes):
print(f"- {name}: {scopes[name]}")

if rejected:
for name, reason in sorted(rejected.items()):
Expand Down
30 changes: 6 additions & 24 deletions .github/workflows/infrastructure-retry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ permissions: {}
jobs:
retry-once:
name: Retry infrastructure failure once
# GitHub aggregates an exact job-level startup_failure as workflow failure;
# the script below proves that classification before issuing a retry.
# Superseded runs are intentionally cancelled by CI concurrency and must not
# create more runner demand. Only a real timeout or a proven startup failure
# is eligible for one automatic retry.
if: >-
github.event.workflow_run.run_attempt == 1 &&
(github.event.workflow_run.conclusion == 'cancelled' ||
github.event.workflow_run.conclusion == 'timed_out' ||
(github.event.workflow_run.conclusion == 'timed_out' ||
github.event.workflow_run.conclusion == 'failure')
runs-on: ubuntu-latest
timeout-minutes: 5
Expand All @@ -28,37 +28,19 @@ jobs:
env:
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
GH_TOKEN: ${{ github.token }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_NUMBER: ${{ github.event.workflow_run.run_number }}
WORKFLOW_EVENT: ${{ github.event.workflow_run.event }}
WORKFLOW_ID: ${{ github.event.workflow_run.workflow_id }}
shell: bash
run: |
set -euo pipefail

if [[ ! "${RUN_ID}" =~ ^[0-9]+$ || ! "${RUN_NUMBER}" =~ ^[0-9]+$ || ! "${WORKFLOW_ID}" =~ ^[0-9]+$ ]]; then
if [[ ! "${RUN_ID}" =~ ^[0-9]+$ ]]; then
echo "::error title=Invalid workflow run metadata::Refusing to issue a retry request."
exit 1
fi

retry_reason=""
if [[ "${CONCLUSION}" == "cancelled" ]]; then
newer_runs="$(
gh api --method GET \
"repos/${REPOSITORY}/actions/workflows/${WORKFLOW_ID}/runs" \
-f branch="${HEAD_BRANCH}" \
-f event="${WORKFLOW_EVENT}" \
-F per_page=100 \
--jq ".workflow_runs | map(select(.run_number > ${RUN_NUMBER})) | length"
)"
if (( newer_runs > 0 )); then
echo "::notice title=Stale cancellation is not retried::A newer CI run already exists for this branch and event."
exit 0
fi
retry_reason="workflow was cancelled and has not been superseded"
elif [[ "${CONCLUSION}" == "timed_out" ]]; then
if [[ "${CONCLUSION}" == "timed_out" ]]; then
# Repository policy intentionally permits one retry for the exact timed_out conclusion.
retry_reason="workflow timed out"
elif [[ "${CONCLUSION}" == "failure" ]]; then
Expand Down
Loading
Loading