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
26 changes: 4 additions & 22 deletions .github/workflows/build-windows-a11y-ami.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,7 @@ jobs:

- name: Install Windows Updates (repeat until converged)
run: |
for i in $(seq 1 5); do
OUTPUT=$(bash scripts/windows-a11y/ssm-run.sh "${{ steps.launch.outputs.instance_id }}" scripts/windows-a11y/install-updates.ps1 3600)
echo "${OUTPUT}"
if echo "${OUTPUT}" | grep -q "No updates found."; then
echo "No further updates."
break
fi
if echo "${OUTPUT}" | grep -q "REBOOT_REQUIRED=true"; then
echo "Rebooting instance for updates (pass ${i})..."
aws ec2 reboot-instances --instance-ids "${{ steps.launch.outputs.instance_id }}"
sleep 30
aws ec2 wait instance-status-ok --instance-ids "${{ steps.launch.outputs.instance_id }}"
fi
done
echo "WINDOWS_UPDATE_DATE=$(date -u +%Y-%m-%d)" >> "$GITHUB_ENV"
bash scripts/windows-a11y/run-windows-updates.sh "${{ steps.launch.outputs.instance_id }}"

- name: Install/update Chrome, Firefox, NVDA
id: software
Expand Down Expand Up @@ -148,13 +134,9 @@ jobs:
id: create-image
run: |
AMI_NAME="windows-a11y-${{ github.event.inputs.ami_name }}"
IMAGE_ID=$(aws ec2 create-image \
--instance-id "${{ steps.launch.outputs.instance_id }}" \
--name "${AMI_NAME}" \
--description "Windows Server 2025 A11y test environment - ${AMI_NAME}" \
--query 'ImageId' --output text)
aws ec2 wait image-available --image-ids "${IMAGE_ID}"
echo "ami_id=${IMAGE_ID}" >> "$GITHUB_OUTPUT"
bash scripts/windows-a11y/create-ami.sh \
"${{ steps.launch.outputs.instance_id }}" \
"${AMI_NAME}"

- name: Tag AMI and snapshots
run: |
Expand Down
113 changes: 101 additions & 12 deletions .github/workflows/launch-windows-a11y-ec2.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
name: Launch Windows A11y EC2
name: Manage Windows A11y EC2

on:
workflow_dispatch:
inputs:
ami_name:
description: "AMI 版本標籤(需與 build-windows-a11y-ami 的 ami_name 相同)"
action:
description: "Operation to perform"
required: true
type: string
stack_name:
description: "CloudFormation stack name"
default: "launch"
type: choice
options:
- launch
- delete
stack_suffix:
description: "Stack suffix only. Example: anson-test creates windows-a11y-anson-test"
required: true
default: "windows-a11y"
type: string
confirm_stack_name:
description: "Delete only: enter the full name, including prefix (example: windows-a11y-anson-test)"
required: false
type: string
ami_name:
description: "Launch only: AMI version label used by build-windows-a11y-ami"
required: false
type: string
instance_name:
description: "EC2 Name tag"
description: "Launch only: EC2 Name tag"
required: true
default: "windows-a11y"
type: string
instance_type:
description: "EC2 instance type"
description: "Launch only: EC2 instance type"
required: true
default: "m5.xlarge"
type: string
disk_size:
description: "Root EBS volume size (GiB)"
description: "Launch only: root EBS volume size (GiB)"
required: true
default: "100"
type: string
Expand All @@ -35,8 +46,42 @@ env:
AWS_REGION: ap-northeast-1

jobs:
validate:
name: validate stack operation
runs-on: ubuntu-latest
outputs:
stack_name: ${{ steps.stack.outputs.stack_name }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate request and resolve stack name
id: stack
env:
ACTION: ${{ inputs.action }}
AMI_NAME: ${{ inputs.ami_name }}
CONFIRM_STACK_NAME: ${{ inputs.confirm_stack_name }}
STACK_SUFFIX: ${{ inputs.stack_suffix }}
run: |
STACK_NAME=$(bash scripts/windows-a11y/validate-stack-operation.sh \
"${ACTION}" \
"${STACK_SUFFIX}" \
"${CONFIRM_STACK_NAME}" \
"${AMI_NAME}")

echo "Resolved stack name: ${STACK_NAME}"
echo "stack_name=${STACK_NAME}" >> "$GITHUB_OUTPUT"
{
echo "## Windows A11y EC2 request"
echo "- Action: \`${ACTION}\`"
echo "- Stack prefix: \`windows-a11y-\`"
echo "- Full stack name: \`${STACK_NAME}\`"
} >> "$GITHUB_STEP_SUMMARY"

launch:
name: launch ec2 from windows a11y ami
if: ${{ inputs.action == 'launch' }}
needs: validate
environment: windows-a11y
runs-on: ubuntu-latest
permissions:
Expand Down Expand Up @@ -77,7 +122,7 @@ jobs:
DISK_SIZE: ${{ inputs.disk_size }}
INSTANCE_NAME: ${{ inputs.instance_name }}
INSTANCE_TYPE: ${{ inputs.instance_type }}
STACK_NAME: ${{ inputs.stack_name }}
STACK_NAME: ${{ needs.validate.outputs.stack_name }}
run: |
aws cloudformation deploy \
--stack-name "${STACK_NAME}" \
Expand All @@ -96,7 +141,7 @@ jobs:
- name: Wait for instance and publish connection details
env:
AMI_ID: ${{ steps.ami.outputs.image_id }}
STACK_NAME: ${{ inputs.stack_name }}
STACK_NAME: ${{ needs.validate.outputs.stack_name }}
run: |
INSTANCE_ID=$(aws cloudformation describe-stacks \
--stack-name "${STACK_NAME}" \
Expand All @@ -115,3 +160,47 @@ jobs:
echo "- Public IP: \`${PUBLIC_IP}\`"
echo "- Stack: \`${STACK_NAME}\`"
} >> "$GITHUB_STEP_SUMMARY"

delete:
name: delete windows a11y stack
if: ${{ inputs.action == 'delete' }}
needs: validate
environment: windows-a11y
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_GITHUB_ACTION_ROLE }}
aws-region: ${{ env.AWS_REGION }}

- name: Verify and delete CloudFormation stack
env:
STACK_NAME: ${{ needs.validate.outputs.stack_name }}
run: |
STACK_STATUS=$(aws cloudformation describe-stacks \
--stack-name "${STACK_NAME}" \
--query 'Stacks[0].StackStatus' \
--output text)

echo "Deleting ${STACK_NAME} (current status: ${STACK_STATUS})."
aws cloudformation delete-stack --stack-name "${STACK_NAME}"

if ! aws cloudformation wait stack-delete-complete --stack-name "${STACK_NAME}"; then
echo "::error::CloudFormation did not complete deletion of ${STACK_NAME}."
aws cloudformation describe-stack-events \
--stack-name "${STACK_NAME}" \
--max-items 10 \
--query 'StackEvents[].{Time:Timestamp,Status:ResourceStatus,Type:ResourceType,Reason:ResourceStatusReason}' \
--output table || true
exit 1
fi

{
echo "## Windows A11y stack deleted"
echo "- Stack: \`${STACK_NAME}\`"
echo "- Previous status: \`${STACK_STATUS}\`"
} >> "$GITHUB_STEP_SUMMARY"
2 changes: 1 addition & 1 deletion docs/windows-a11y-aws-manual-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ reliable than using the localized base image.
- Source: `Custom` → enter your office/VPN CIDR block (e.g. `203.0.113.0/24`) — do **not** use `0.0.0.0/0`.
- Description: `Office VPN RDP access`
6. **Outbound rules**: leave the default (all traffic allowed) — the instance needs outbound HTTPS for
Windows Update, Chocolatey, and the SSM agent.
Windows Update, Google, Mozilla, NV Access, and the SSM agent.
7. **Tags**: `Name` = `windows-a11y-rdp`.
8. Click **Create security group**. Copy the resulting **Security group ID** (e.g. `sg-0123456789abcdef0`).
9. Record this value — it becomes the `SECURITY_GROUP_ID` GitHub variable in step 5.
Expand Down
71 changes: 71 additions & 0 deletions scripts/windows-a11y/create-ami.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail

INSTANCE_ID="${1:?Usage: create-ami.sh <instance-id> <ami-name>}"
AMI_NAME="${2:?Usage: create-ami.sh <instance-id> <ami-name>}"
: "${GITHUB_OUTPUT:?GITHUB_OUTPUT must point to the GitHub Actions output file}"

MAX_ATTEMPTS="${AMI_MAX_ATTEMPTS:-120}"
POLL_INTERVAL_SECONDS="${AMI_POLL_INTERVAL_SECONDS:-15}"
LOG_PREFIX="[create-ami]"

IMAGE_ID=$(aws ec2 create-image \
--instance-id "${INSTANCE_ID}" \
--name "${AMI_NAME}" \
--description "Windows Server 2025 A11y test environment - ${AMI_NAME}" \
--query 'ImageId' \
--output text)

echo "${LOG_PREFIX} Created AMI ${IMAGE_ID}."
echo "ami_id=${IMAGE_ID}" >> "${GITHUB_OUTPUT}"

for attempt in $(seq 1 "${MAX_ATTEMPTS}"); do
set +e
IMAGE_JSON=$(aws ec2 describe-images --image-ids "${IMAGE_ID}" --output json 2>&1)
DESCRIBE_STATUS=$?
set -e

if (( DESCRIBE_STATUS != 0 )); then
if grep -q 'InvalidAMIID.NotFound' <<< "${IMAGE_JSON}"; then
echo "${LOG_PREFIX} AMI ${IMAGE_ID} is not visible yet (attempt ${attempt} of ${MAX_ATTEMPTS})."
if (( attempt < MAX_ATTEMPTS )); then
sleep "${POLL_INTERVAL_SECONDS}"
fi
continue
fi

echo "${IMAGE_JSON}" >&2
exit "${DESCRIBE_STATUS}"
fi

STATE=$(jq -r '.Images[0].State // "missing"' <<< "${IMAGE_JSON}")
STATE_REASON=$(jq -r '.Images[0].StateReason.Message // empty' <<< "${IMAGE_JSON}")

if [[ -n "${STATE_REASON}" ]]; then
echo "${LOG_PREFIX} AMI ${IMAGE_ID} state: ${STATE} (attempt ${attempt} of ${MAX_ATTEMPTS}); reason: ${STATE_REASON}."
else
echo "${LOG_PREFIX} AMI ${IMAGE_ID} state: ${STATE} (attempt ${attempt} of ${MAX_ATTEMPTS})."
fi

case "${STATE}" in
available)
exit 0
;;
failed)
echo "${LOG_PREFIX} AMI ${IMAGE_ID} creation failed." >&2
exit 1
;;
pending)
if (( attempt < MAX_ATTEMPTS )); then
sleep "${POLL_INTERVAL_SECONDS}"
fi
;;
*)
echo "${LOG_PREFIX} Unexpected AMI state '${STATE}' for ${IMAGE_ID}." >&2
exit 1
;;
esac
done

echo "${LOG_PREFIX} Timed out waiting for AMI ${IMAGE_ID} after ${MAX_ATTEMPTS} checks." >&2
exit 1
Loading
Loading