Skip to content

Commit 88e1719

Browse files
committed
feat: Add GitHub Actions workflows for xa11y integration tests
Signed-off-by: Karthik Bekal Pattathana <133984042+karthikbekalp@users.noreply.github.com>
1 parent 10b5e2f commit 88e1719

3 files changed

Lines changed: 363 additions & 0 deletions

File tree

.github/workflows/integ_macos.yml

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
name: "Integ Tests - macOS"
2+
3+
on:
4+
workflow_call:
5+
secrets:
6+
AWS_OIDC_ROLE_ARN:
7+
required: true
8+
AWS_REGION:
9+
required: true
10+
RLM_PORT:
11+
required: true
12+
INSTALLER_BUCKET:
13+
required: true
14+
INSTALLER_BUCKET_EXPECTED_OWNER:
15+
required: true
16+
LICENSE_ROLE_ARN:
17+
required: true
18+
BASTION_INSTANCE_TAG:
19+
required: true
20+
21+
jobs:
22+
IntegMacOS:
23+
if: >-
24+
github.repository == 'aws-deadline/deadline-cloud-for-cinema-4d' &&
25+
github.ref == 'refs/heads/mainline'
26+
name: Integration Tests (macOS)
27+
runs-on: macos-latest
28+
timeout-minutes: 60
29+
permissions:
30+
id-token: write
31+
contents: read
32+
env:
33+
LICENSE_ENDPOINT_DNS: "127.0.0.1"
34+
C4D_LICENSE_PORT: ${{ secrets.RLM_PORT }}
35+
redshift_LICENSE: "${{ secrets.RLM_PORT }}@127.0.0.1"
36+
g_licenseServerRLM: "127.0.0.1:${{ secrets.RLM_PORT }}"
37+
g_licenseModel: "LICENSEMODEL::RLM"
38+
C4D_VERSION: "2026"
39+
INSTALLER_BUCKET: ${{ secrets.INSTALLER_BUCKET }}
40+
INSTALLER_BUCKET_EXPECTED_OWNER: ${{ secrets.INSTALLER_BUCKET_EXPECTED_OWNER }}
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
44+
with:
45+
persist-credentials: false
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
49+
with:
50+
python-version: "3.11"
51+
52+
- name: Install hatch
53+
run: pip install --upgrade hatch
54+
55+
- name: Configure AWS credentials (OIDC)
56+
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
57+
with:
58+
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
59+
aws-region: ${{ secrets.AWS_REGION }}
60+
mask-aws-account-id: true
61+
62+
- name: Grant accessibility
63+
run: |
64+
# Create the hatch environment first so that pip's install output
65+
# (which can include marker-mismatch messages containing quotes)
66+
# doesn't pollute the captured Python path below.
67+
hatch env create integ-xa11y
68+
HATCH_PYTHON=$(hatch -e integ-xa11y run python -c "import os,sys; print(os.path.realpath(sys.executable))")
69+
SETUP_PYTHON=$(python3 -c "import os,sys; print(os.path.realpath(sys.executable))")
70+
for BIN_PATH in "$SETUP_PYTHON" "$HATCH_PYTHON"; do
71+
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
72+
"INSERT OR REPLACE INTO access(service,client,client_type,auth_value,auth_reason,auth_version,csreq,policy_id,indirect_object_identifier_type,indirect_object_identifier,indirect_object_code_identity,flags,last_modified) \
73+
VALUES('kTCCServiceAccessibility','${BIN_PATH}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,$(date +%s));"
74+
done
75+
sudo launchctl stop com.apple.tccd 2>/dev/null || true
76+
sleep 2
77+
78+
- name: Install and configure Cinema 4D
79+
run: |
80+
pip install boto3
81+
python ./pipeline/setup-runner.py --versions $C4D_VERSION
82+
83+
- name: Grant accessibility to Cinema 4D
84+
run: |
85+
C4D_BIN="/Applications/Maxon Cinema 4D 2026/Cinema 4D.app/Contents/MacOS/Cinema 4D"
86+
C4DPY_BIN="/Applications/Maxon Cinema 4D 2026/c4dpy.app/Contents/MacOS/c4dpy"
87+
for BIN_PATH in "$C4D_BIN" "$C4DPY_BIN"; do
88+
if [ -f "$BIN_PATH" ]; then
89+
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
90+
"INSERT OR REPLACE INTO access(service,client,client_type,auth_value,auth_reason,auth_version,csreq,policy_id,indirect_object_identifier_type,indirect_object_identifier,indirect_object_code_identity,flags,last_modified) \
91+
VALUES('kTCCServiceAccessibility','${BIN_PATH}',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,$(date +%s));"
92+
fi
93+
done
94+
sudo launchctl stop com.apple.tccd 2>/dev/null || true
95+
sleep 2
96+
97+
- name: Bring up license tunnel and run tests
98+
env:
99+
AWS_REGION: ${{ secrets.AWS_REGION }}
100+
RLM_PORT: ${{ secrets.RLM_PORT }}
101+
LICENSE_ROLE_ARN: ${{ secrets.LICENSE_ROLE_ARN }}
102+
BASTION_INSTANCE_TAG: ${{ secrets.BASTION_INSTANCE_TAG }}
103+
run: |
104+
rlmPort=$(echo "$RLM_PORT" | tr -d '[:space:]')
105+
region=$(echo "$AWS_REGION" | tr -d '[:space:]')
106+
107+
# Install SSM plugin
108+
curl --fail --silent --show-error --location \
109+
"https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac_arm64/session-manager-plugin.pkg" \
110+
--output "$RUNNER_TEMP/ssm-plugin.pkg"
111+
sudo installer -pkg "$RUNNER_TEMP/ssm-plugin.pkg" -target /
112+
113+
# Configure a profile that assumes the licensing role. Credentials come
114+
# from the OIDC creds already in the environment (credential_source=
115+
# Environment), so the role is assumed transparently on each
116+
# --profile license call -- nothing is printed or stored by us.
117+
aws configure set profile.license.role_arn "$LICENSE_ROLE_ARN"
118+
aws configure set profile.license.credential_source Environment
119+
aws configure set profile.license.region "$region"
120+
121+
# Resolve the bastion instance by tag at runtime (don't hardcode the ID).
122+
# The bastion listens on the RLM port and relays to the license
123+
# server itself -- CI only ever opens a session on the bastion.
124+
bastion=$(aws ec2 describe-instances \
125+
--filters "Name=tag:Name,Values=$BASTION_INSTANCE_TAG" "Name=instance-state-name,Values=running" \
126+
--query 'Reservations[0].Instances[0].InstanceId' \
127+
--profile license --region "$region" --output text)
128+
if [ -z "$bastion" ] || [ "$bastion" = "None" ]; then echo "ERROR: Bastion host not found"; exit 1; fi
129+
130+
# Start SSM port forwarding to the bastion. Log to a file so we can
131+
# capture the SessionId for deterministic teardown.
132+
aws ssm start-session \
133+
--target "$bastion" \
134+
--document-name DccInteg-PortForwardToLicenseServer \
135+
--parameters "{\"portNumber\":[\"$rlmPort\"],\"localPortNumber\":[\"$rlmPort\"]}" \
136+
--profile license --region "$region" > "$RUNNER_TEMP/ssm.log" 2>&1 &
137+
138+
# Wait for port (retry up to 30s)
139+
ready=false
140+
for i in $(seq 1 6); do
141+
sleep 5
142+
if nc -z 127.0.0.1 "$rlmPort" 2>/dev/null; then ready=true; break; fi
143+
done
144+
if [ "$ready" != "true" ]; then echo "ERROR: SSM port forward not up after 30s"; cat "$RUNNER_TEMP/ssm.log" || true; exit 1; fi
145+
echo "License tunnel up via SSM"
146+
147+
# Capture the SSM session id so the teardown step can terminate the
148+
# session on the (shared, long-lived) license host explicitly, rather
149+
# than relying on SSM's disconnect detection to reap it.
150+
ssmSessionId=$(grep -oE 'SessionId: [A-Za-z0-9._-]+' "$RUNNER_TEMP/ssm.log" | head -1 | awk '{print $2}' || true)
151+
echo "SSM_SESSION_ID=$ssmSessionId" >> "$GITHUB_ENV"
152+
echo "SSM session id: ${ssmSessionId:-<not captured>}"
153+
154+
hatch run integ-xa11y:test
155+
156+
- name: Tear down
157+
if: always()
158+
env:
159+
AWS_REGION: ${{ secrets.AWS_REGION }}
160+
run: |
161+
region=$(echo "$AWS_REGION" | tr -d '[:space:]')
162+
# Terminate the SSM session on the shared license host so the
163+
# port-forward is released immediately instead of lingering until
164+
# SSM notices the dropped connection. The license profile was
165+
# configured in the previous step (credential_source=Environment).
166+
if [ -n "${SSM_SESSION_ID:-}" ]; then
167+
aws ssm terminate-session --session-id "$SSM_SESSION_ID" \
168+
--profile license --region "$region" 2>/dev/null || true
169+
fi
170+
# Kill the local plugin as a fallback (harmless on ephemeral runners).
171+
pkill -f "session-manager-plugin" 2>/dev/null || true
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
name: "Integ Tests - Windows"
2+
3+
on:
4+
workflow_call:
5+
secrets:
6+
AWS_OIDC_ROLE_ARN:
7+
required: true
8+
AWS_REGION:
9+
required: true
10+
RLM_PORT:
11+
required: true
12+
INSTALLER_BUCKET:
13+
required: true
14+
INSTALLER_BUCKET_EXPECTED_OWNER:
15+
required: true
16+
LICENSE_ROLE_ARN:
17+
required: true
18+
BASTION_INSTANCE_TAG:
19+
required: true
20+
21+
jobs:
22+
IntegWindows:
23+
if: >-
24+
github.repository == 'aws-deadline/deadline-cloud-for-cinema-4d' &&
25+
github.ref == 'refs/heads/mainline'
26+
name: Integration Tests (Windows)
27+
runs-on: windows-latest
28+
timeout-minutes: 60
29+
permissions:
30+
id-token: write
31+
contents: read
32+
env:
33+
LICENSE_ENDPOINT_DNS: "127.0.0.1"
34+
C4D_LICENSE_PORT: ${{ secrets.RLM_PORT }}
35+
redshift_LICENSE: "${{ secrets.RLM_PORT }}@127.0.0.1"
36+
g_licenseServerRLM: "127.0.0.1:${{ secrets.RLM_PORT }}"
37+
g_licenseModel: "LICENSEMODEL::RLM"
38+
C4D_VERSION: "2026"
39+
INSTALLER_BUCKET: ${{ secrets.INSTALLER_BUCKET }}
40+
INSTALLER_BUCKET_EXPECTED_OWNER: ${{ secrets.INSTALLER_BUCKET_EXPECTED_OWNER }}
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
44+
with:
45+
persist-credentials: false
46+
47+
- name: Set up Python
48+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
49+
with:
50+
python-version: "3.11"
51+
52+
- name: Install hatch
53+
run: pip install --upgrade hatch
54+
55+
- name: Configure AWS credentials (OIDC)
56+
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4
57+
with:
58+
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
59+
aws-region: ${{ secrets.AWS_REGION }}
60+
mask-aws-account-id: true
61+
62+
- name: Install and configure Cinema 4D
63+
shell: pwsh
64+
run: |
65+
pip install boto3
66+
python ./pipeline/setup-runner.py --versions $env:C4D_VERSION
67+
68+
- name: Bring up license tunnel and run tests
69+
shell: pwsh
70+
env:
71+
AWS_REGION: ${{ secrets.AWS_REGION }}
72+
RLM_PORT: ${{ secrets.RLM_PORT }}
73+
LICENSE_ROLE_ARN: ${{ secrets.LICENSE_ROLE_ARN }}
74+
BASTION_INSTANCE_TAG: ${{ secrets.BASTION_INSTANCE_TAG }}
75+
run: |
76+
$rlmPort = $env:RLM_PORT.Trim()
77+
$region = $env:AWS_REGION.Trim()
78+
79+
# Install SSM plugin
80+
$ssmUrl = "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/windows/SessionManagerPluginSetup.exe"
81+
Invoke-WebRequest -Uri $ssmUrl -OutFile "$env:RUNNER_TEMP\ssm-plugin.exe" -UseBasicParsing
82+
Start-Process -FilePath "$env:RUNNER_TEMP\ssm-plugin.exe" -ArgumentList "/quiet" -Wait
83+
$ssmPluginDir = Join-Path $env:ProgramFiles "Amazon\SessionManagerPlugin\bin"
84+
$env:PATH = "$ssmPluginDir;$env:PATH"
85+
if (-not (Get-Command "session-manager-plugin.exe" -ErrorAction SilentlyContinue)) {
86+
Write-Error "Session Manager plugin was not found after installation"
87+
exit 1
88+
}
89+
90+
# Configure a profile that assumes the licensing role. Credentials come
91+
# from the OIDC creds already in the environment (credential_source=
92+
# Environment), so the role is assumed transparently on each
93+
# --profile license call -- nothing is printed or stored by us.
94+
aws configure set profile.license.role_arn $env:LICENSE_ROLE_ARN
95+
aws configure set profile.license.credential_source Environment
96+
aws configure set profile.license.region $region
97+
98+
# Resolve the bastion instance by tag at runtime (don't hardcode the ID).
99+
# The bastion listens on the RLM port and relays to the license
100+
# server itself -- CI only ever opens a session on the bastion.
101+
$bastion = (aws ec2 describe-instances `
102+
--filters "Name=tag:Name,Values=$($env:BASTION_INSTANCE_TAG)" "Name=instance-state-name,Values=running" `
103+
--query 'Reservations[0].Instances[0].InstanceId' `
104+
--profile license --region $region --output text).Trim()
105+
if (-not $bastion -or $bastion -eq "None") { Write-Error "Bastion host not found"; exit 1 }
106+
107+
# Start SSM port forwarding to the bastion (params via file to avoid
108+
# quoting issues). Redirect stdout to a log file so we can capture
109+
# the SessionId for deterministic teardown.
110+
$paramsFile = Join-Path $env:RUNNER_TEMP "ssm_params.json"
111+
@{portNumber=@($rlmPort);localPortNumber=@($rlmPort)} | ConvertTo-Json -Compress | Out-File -FilePath $paramsFile -Encoding ascii
112+
$ssmLog = Join-Path $env:RUNNER_TEMP "ssm.log"
113+
Start-Process -FilePath "aws" -ArgumentList "ssm start-session --target $bastion --document-name DccInteg-PortForwardToLicenseServer --parameters file://$paramsFile --profile license --region $region" -NoNewWindow -RedirectStandardOutput $ssmLog
114+
115+
# Wait for port (retry up to 30s)
116+
$ready = $false
117+
for ($i = 0; $i -lt 6; $i++) {
118+
Start-Sleep -Seconds 5
119+
$t = Test-NetConnection -ComputerName 127.0.0.1 -Port $rlmPort -WarningAction SilentlyContinue
120+
if ($t.TcpTestSucceeded) { $ready = $true; break }
121+
}
122+
if (-not $ready) { Write-Error "SSM port forward not up after 30s"; if (Test-Path $ssmLog) { Get-Content $ssmLog }; exit 1 }
123+
Write-Host "License tunnel up via SSM"
124+
125+
# Capture the SSM session id so the teardown step can terminate the
126+
# session on the (shared, long-lived) license host explicitly, rather
127+
# than relying on SSM's disconnect detection to reap it.
128+
$ssmSessionId = ""
129+
if (Test-Path $ssmLog) {
130+
$m = Select-String -Path $ssmLog -Pattern 'SessionId: ([A-Za-z0-9._-]+)' | Select-Object -First 1
131+
if ($m) { $ssmSessionId = $m.Matches[0].Groups[1].Value }
132+
}
133+
"SSM_SESSION_ID=$ssmSessionId" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
134+
if ($ssmSessionId) { Write-Host "SSM session id: $ssmSessionId" } else { Write-Host "SSM session id: <not captured>" }
135+
136+
hatch run integ-xa11y:test
137+
138+
- name: Tear down
139+
if: always()
140+
shell: pwsh
141+
env:
142+
AWS_REGION: ${{ secrets.AWS_REGION }}
143+
run: |
144+
$region = $env:AWS_REGION.Trim()
145+
# Terminate the SSM session on the shared license host so the
146+
# port-forward is released immediately instead of lingering until
147+
# SSM notices the dropped connection. The license profile was
148+
# configured in the previous step (credential_source=Environment).
149+
if ($env:SSM_SESSION_ID) {
150+
aws ssm terminate-session --session-id $env:SSM_SESSION_ID --profile license --region $region 2>$null
151+
if ($LASTEXITCODE -ne 0) {
152+
Write-Warning "Failed to terminate SSM session $env:SSM_SESSION_ID"
153+
$global:LASTEXITCODE = 0
154+
}
155+
}
156+
# Kill the local plugin as a fallback (harmless on ephemeral runners).
157+
Get-Process -Name "session-manager-plugin" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: "Cinema 4D xa11y Integration Tests"
2+
3+
on:
4+
push:
5+
branches:
6+
- mainline
7+
8+
jobs:
9+
windows:
10+
uses: ./.github/workflows/integ_windows.yml
11+
secrets:
12+
AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}
13+
AWS_REGION: ${{ secrets.AWS_REGION }}
14+
RLM_PORT: ${{ secrets.RLM_PORT }}
15+
INSTALLER_BUCKET: ${{ secrets.INSTALLER_BUCKET }}
16+
INSTALLER_BUCKET_EXPECTED_OWNER: ${{ secrets.INSTALLER_BUCKET_EXPECTED_OWNER }}
17+
LICENSE_ROLE_ARN: ${{ secrets.LICENSE_ROLE_ARN }}
18+
BASTION_INSTANCE_TAG: ${{ secrets.BASTION_INSTANCE_TAG }}
19+
permissions:
20+
id-token: write
21+
contents: read
22+
23+
macos:
24+
uses: ./.github/workflows/integ_macos.yml
25+
secrets:
26+
AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}
27+
AWS_REGION: ${{ secrets.AWS_REGION }}
28+
RLM_PORT: ${{ secrets.RLM_PORT }}
29+
INSTALLER_BUCKET: ${{ secrets.INSTALLER_BUCKET }}
30+
INSTALLER_BUCKET_EXPECTED_OWNER: ${{ secrets.INSTALLER_BUCKET_EXPECTED_OWNER }}
31+
LICENSE_ROLE_ARN: ${{ secrets.LICENSE_ROLE_ARN }}
32+
BASTION_INSTANCE_TAG: ${{ secrets.BASTION_INSTANCE_TAG }}
33+
permissions:
34+
id-token: write
35+
contents: read

0 commit comments

Comments
 (0)