From a4a31f21dd455f78971f1c5e3557326594aa0e6f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:30:33 +0000 Subject: [PATCH 1/4] Add CI/CD workflows, dependabot config and runner image --- .github/dependabot.yml | 31 +++ .github/scripts/testng_summary.py | 75 ++++++ .github/workflows/cd.yml | 195 ++++++++++++++ .github/workflows/ci.yml | 239 ++++++++++++++++++ Dockerfile | 35 +++ README.md | 85 +++++++ pom.xml | 4 +- .../java/example/example/tests/BaseTest.java | 86 ++++++- 8 files changed, 741 insertions(+), 9 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/scripts/testng_summary.py create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml create mode 100644 Dockerfile diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4d637fa --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,31 @@ +version: 2 +updates: + - package-ecosystem: maven + directory: / + schedule: + interval: weekly + day: monday + time: '06:00' + open-pull-requests-limit: 5 + labels: [dependencies, java] + groups: + selenium: + patterns: ['org.seleniumhq.selenium:*', 'io.github.bonigarcia:*'] + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: '06:00' + open-pull-requests-limit: 5 + labels: [dependencies, github-actions] + + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + day: monday + time: '06:00' + open-pull-requests-limit: 3 + labels: [dependencies, docker] diff --git a/.github/scripts/testng_summary.py b/.github/scripts/testng_summary.py new file mode 100644 index 0000000..543bb00 --- /dev/null +++ b/.github/scripts/testng_summary.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Render a TestNG/Surefire result summary into the GitHub Actions job summary.""" + +import glob +import os +import xml.etree.ElementTree as ET + +BROWSER = os.environ.get("BROWSER", "chrome") +SUMMARY_PATH = os.environ.get("GITHUB_STEP_SUMMARY") + + +def collect(): + totals = {"tests": 0, "failures": 0, "errors": 0, "skipped": 0, "time": 0.0} + failed = [] + for path in sorted(glob.glob("target/surefire-reports/TEST-*.xml")): + root = ET.parse(path).getroot() + totals["tests"] += int(root.get("tests", 0)) + totals["failures"] += int(root.get("failures", 0)) + totals["errors"] += int(root.get("errors", 0)) + totals["skipped"] += int(root.get("skipped", 0)) + totals["time"] += float(root.get("time", 0) or 0) + for case in root.iter("testcase"): + for outcome in ("failure", "error"): + node = case.find(outcome) + if node is not None: + failed.append( + ( + "{}.{}".format(case.get("classname", "?"), case.get("name", "?")), + (node.get("message") or node.get("type") or "").strip().splitlines()[:1], + ) + ) + return totals, failed + + +def main(): + totals, failed = collect() + passed = totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"] + lines = [ + "## TestNG results ({})".format(BROWSER), + "", + "| Total | Passed | Failed | Errors | Skipped | Time (s) |", + "| ----: | -----: | -----: | -----: | ------: | -------: |", + "| {tests} | {passed} | {failures} | {errors} | {skipped} | {time:.1f} |".format( + passed=passed, **totals + ), + "", + ] + + if not totals["tests"]: + lines.append( + "No Surefire result files were produced - the suite did not start " + "(check the run log for browser or network failures)." + ) + elif failed: + lines.append("### Failed tests") + lines.append("") + for name, message in failed: + lines.append("- `{}`{}".format(name, ": " + message[0] if message else "")) + lines.append("") + lines.append( + "> The demo suite drives public websites and `FaceBookLoginTest` " + "asserts false by design, so failures here do not fail the workflow." + ) + else: + lines.append("All tests passed.") + + report = "\n".join(lines) + "\n" + print(report) + if SUMMARY_PATH: + with open(SUMMARY_PATH, "a", encoding="utf-8") as handle: + handle.write(report) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..3e32210 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,195 @@ +name: CD (mocked AWS delivery) + +# DEMO PIPELINE. Every AWS identifier below is a placeholder: the account ID, +# role ARN, ECR registry, ECS cluster/service and S3 bucket do not exist. The +# pipeline runs in DRY_RUN mode unless BOTH the `dry_run` input is set to false +# AND the repository variable ENABLE_REAL_AWS_DEPLOY is exactly 'true', so a +# normal run never authenticates to or mutates real infrastructure. + +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Mock every AWS call (leave true for the demo)' + type: boolean + default: true + suite: + description: 'TestNG suite the deployed runner executes' + type: string + default: './src/test/resources/suites/testng.xml' + browser: + description: 'Browser the deployed runner uses' + type: choice + options: [chrome, firefox] + default: chrome + +concurrency: + group: cd-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + # --- MOCKED PLACEHOLDERS (not real AWS resources) --- + AWS_REGION: us-east-1 + AWS_ACCOUNT_ID: '123456789012' + ECR_REGISTRY: 123456789012.dkr.ecr.us-east-1.amazonaws.com + ECR_REPOSITORY: demo-selenium-testng + ECS_CLUSTER: demo-selenium-testng + ECS_TASK_DEFINITION: demo-selenium-testng-runner + REPORTS_BUCKET: demo-selenium-testng-reports + OIDC_ROLE_ARN: arn:aws:iam::123456789012:role/demo-selenium-testng-github-oidc + # ---------------------------------------------------- + +jobs: + guard: + name: Resolve execution mode + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + dry_run: ${{ steps.mode.outputs.dry_run }} + steps: + - name: Decide dry run + id: mode + env: + REQUESTED_DRY_RUN: ${{ inputs.dry_run }} + ENABLE_REAL_AWS_DEPLOY: ${{ vars.ENABLE_REAL_AWS_DEPLOY }} + run: | + if [ "$REQUESTED_DRY_RUN" = "false" ] && [ "$ENABLE_REAL_AWS_DEPLOY" = "true" ]; then + dry_run=false + else + dry_run=true + fi + echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" + echo "DRY_RUN=$dry_run (real AWS calls require dry_run=false and repository variable ENABLE_REAL_AWS_DEPLOY=true)" >> "$GITHUB_STEP_SUMMARY" + + build-image: + name: Build runner image and push to ECR (mocked) + needs: guard + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + id-token: write # required for AWS OIDC in a real deployment + outputs: + image: ${{ steps.tag.outputs.image }} + steps: + - uses: actions/checkout@v4 + + - name: Compute image tag + id: tag + run: echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" + + - uses: docker/setup-buildx-action@v3 + + - name: Build runner image + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: ${{ steps.tag.outputs.image }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Configure AWS credentials via OIDC (real run only) + if: needs.guard.outputs.dry_run == 'false' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.OIDC_ROLE_ARN }} # MOCKED placeholder role ARN + aws-region: ${{ env.AWS_REGION }} + + - name: Push to Amazon ECR (real run only) + if: needs.guard.outputs.dry_run == 'false' + run: | + aws ecr get-login-password --region "$AWS_REGION" \ + | docker login --username AWS --password-stdin "$ECR_REGISTRY" + docker push "${{ steps.tag.outputs.image }}" + + - name: Mock ECR push + if: needs.guard.outputs.dry_run == 'true' + run: | + { + echo "## Mocked ECR push" + echo '' + echo 'Image built locally, no registry contacted:' + echo '```' + echo "aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY" + echo "docker push ${{ steps.tag.outputs.image }}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + docker image inspect "${{ steps.tag.outputs.image }}" --format 'built {{.Id}} ({{.Size}} bytes)' + + run-suite: + name: Run suite on ECS (${{ matrix.environment }}, mocked) + needs: [guard, build-image] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + id-token: write + strategy: + max-parallel: 1 + fail-fast: true + matrix: + environment: [staging, production] # production requires reviewer approval + environment: + name: ${{ matrix.environment }} + steps: + - name: Configure AWS credentials via OIDC (real run only) + if: needs.guard.outputs.dry_run == 'false' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.OIDC_ROLE_ARN }} # MOCKED placeholder role ARN + aws-region: ${{ env.AWS_REGION }} + + - name: Deploy runner and execute suite (real run only) + if: needs.guard.outputs.dry_run == 'false' + env: + IMAGE: ${{ needs.build-image.outputs.image }} + TARGET_ENV: ${{ matrix.environment }} + run: | + aws ecs run-task \ + --cluster "$ECS_CLUSTER-$TARGET_ENV" \ + --task-definition "$ECS_TASK_DEFINITION" \ + --launch-type FARGATE \ + --overrides "{\"containerOverrides\":[{\"name\":\"runner\",\"image\":\"$IMAGE\",\"environment\":[{\"name\":\"SUITE_XML_FILE\",\"value\":\"${{ inputs.suite }}\"},{\"name\":\"BROWSER\",\"value\":\"${{ inputs.browser }}\"}]}]}" + + - name: Mock ECS deployment and suite execution + if: needs.guard.outputs.dry_run == 'true' + env: + IMAGE: ${{ needs.build-image.outputs.image }} + TARGET_ENV: ${{ matrix.environment }} + run: | + { + echo "## Mocked ECS run - $TARGET_ENV" + echo '' + echo "Cluster: \`$ECS_CLUSTER-$TARGET_ENV\` (placeholder)" + echo "Task definition: \`$ECS_TASK_DEFINITION\` (placeholder)" + echo "Image: \`$IMAGE\` (placeholder registry)" + echo "Suite: \`${{ inputs.suite }}\` on \`${{ inputs.browser }}\`" + echo '' + echo '```' + echo "aws ecs run-task --cluster $ECS_CLUSTER-$TARGET_ENV --task-definition $ECS_TASK_DEFINITION --launch-type FARGATE" + echo "aws ecs wait tasks-stopped --cluster $ECS_CLUSTER-$TARGET_ENV --tasks " + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish ExtentReports to S3 (mocked) + env: + TARGET_ENV: ${{ matrix.environment }} + run: | + destination="s3://$REPORTS_BUCKET/$TARGET_ENV/${GITHUB_RUN_ID}/" + if [ "${{ needs.guard.outputs.dry_run }}" = "false" ]; then + aws s3 cp ExtentReports/ "$destination" --recursive + else + { + echo "## Mocked report publication - $TARGET_ENV" + echo '' + echo '```' + echo "aws s3 cp ExtentReports/ $destination --recursive" + echo '```' + echo "Bucket \`$REPORTS_BUCKET\` is a placeholder and is never contacted." + } >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..27c8775 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,239 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + # Nightly full-suite run at 02:00 UTC + - cron: '0 2 * * *' + workflow_dispatch: + inputs: + suite: + description: 'TestNG suite file to execute' + type: string + default: './src/test/resources/suites/testng.xml' + browser: + description: 'Browser to run the suite against' + type: choice + options: [chrome, firefox, both] + default: chrome + thread_count: + description: 'TestNG thread-count for parallel class execution' + type: string + default: '5' + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +env: + JAVA_VERSION: '17' + +jobs: + compile: + name: Compile check + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + + # The pom targets Java 1.8 bytecode, but Selenium 4.25 ships Java 11 class + # files, so the toolchain itself must be 11+. Temurin 17 is used for the JDK + # while maven.compiler.source/target stay at 1.8. + - name: Compile main and test sources + run: mvn -ntp -B clean test-compile + + matrix-setup: + name: Resolve browser matrix + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + browsers: ${{ steps.resolve.outputs.browsers }} + steps: + - name: Resolve browsers + id: resolve + env: + REQUESTED: ${{ inputs.browser }} + run: | + case "${REQUESTED:-}" in + chrome) browsers='["chrome"]' ;; + firefox) browsers='["firefox"]' ;; + *) browsers='["chrome","firefox"]' ;; + esac + echo "browsers=$browsers" >> "$GITHUB_STEP_SUMMARY" + echo "browsers=$browsers" >> "$GITHUB_OUTPUT" + + test: + name: UI suite (${{ matrix.browser }}) + needs: [compile, matrix-setup] + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + browser: ${{ fromJSON(needs.matrix-setup.outputs.browsers) }} + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + + - name: Set up Chrome + if: matrix.browser == 'chrome' + uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + install-chromedriver: true + + - name: Set up Firefox + if: matrix.browser == 'firefox' + uses: browser-actions/setup-firefox@v1 + + - name: Set up geckodriver + if: matrix.browser == 'firefox' + uses: browser-actions/setup-geckodriver@latest + with: + token: ${{ secrets.GITHUB_TOKEN }} + + # BaseTest prefers a driver binary already on the runner (webdriver.*.driver + # system property or the CHROMEWEBDRIVER/GECKOWEBDRIVER directories) and only + # falls back to WebDriverManager downloads, so the run works without network + # access to the driver mirrors. + - name: Locate driver binaries + id: drivers + run: | + for candidate in chromedriver geckodriver; do + path="$(command -v "$candidate" || true)" + [ -n "$path" ] && echo "$candidate=$path" >> "$GITHUB_OUTPUT" + done + + - name: Prepare suite (thread-count = ${{ inputs.thread_count || 5 }}) + id: suite + env: + SUITE: ${{ inputs.suite || './src/test/resources/suites/testng.xml' }} + THREADS: ${{ inputs.thread_count || '5' }} + run: | + mkdir -p target/suites + out="target/suites/$(basename "$SUITE")" + sed -E "s/thread-count=\"[0-9]+\"/thread-count=\"$THREADS\"/g" "$SUITE" > "$out" + echo "file=$out" >> "$GITHUB_OUTPUT" + + # The bundled demo suite drives public websites (google.co.in, facebook.com) + # and FaceBookLoginTest asserts false on purpose, so test failures must not + # fail the workflow. Results are surfaced in the job summary and artifacts. + - name: Run TestNG suite (${{ matrix.browser }}, headless) + continue-on-error: true + run: | + mvn -ntp -B test \ + -DsuiteXmlFile="${{ steps.suite.outputs.file }}" \ + -Dbrowser=${{ matrix.browser }} \ + -Dheadless=true \ + -Dmaven.test.failure.ignore=true \ + ${{ steps.drivers.outputs.chromedriver && format('-Dwebdriver.chrome.driver={0}', steps.drivers.outputs.chromedriver) || '' }} \ + ${{ steps.drivers.outputs.geckodriver && format('-Dwebdriver.gecko.driver={0}', steps.drivers.outputs.geckodriver) || '' }} + + - name: Test summary + if: always() + env: + BROWSER: ${{ matrix.browser }} + run: python3 .github/scripts/testng_summary.py + + - name: Upload ExtentReports HTML + if: always() + uses: actions/upload-artifact@v4 + with: + name: extent-report-${{ matrix.browser }} + path: | + ExtentReports/ + logfile.log + if-no-files-found: warn + retention-days: 14 + + - name: Upload TestNG and Surefire results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.browser }} + path: | + target/surefire-reports/ + test-output/ + if-no-files-found: warn + retention-days: 14 + + dependency-scan: + name: Dependency vulnerability scan + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + cache: maven + + - name: Resolve dependency tree + run: mvn -ntp -B dependency:tree -DoutputFile=target/dependency-tree.txt + + - name: Scan dependencies with Trivy + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln + severity: CRITICAL,HIGH + format: sarif + output: trivy-results.sarif + exit-code: '0' + + - name: Upload Trivy SARIF + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-results.sarif + continue-on-error: true + + - name: Upload dependency tree + if: always() + uses: actions/upload-artifact@v4 + with: + name: dependency-tree + path: target/dependency-tree.txt + if-no-files-found: warn + retention-days: 14 + + dependency-review: + name: Dependency review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: critical + comment-summary-in-pr: on-failure diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7d9a100 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Runner image for the Selenium/TestNG suite. The container is the delivered +# artifact: CD pushes it to (a mocked) Amazon ECR and runs it as an ECS task. +FROM maven:3.9-eclipse-temurin-17 + +ENV DEBIAN_FRONTEND=noninteractive + +# Google Chrome plus the matching Chrome for Testing chromedriver, so the suite +# never has to download a driver at runtime. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates fonts-liberation unzip wget \ + && wget -q -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \ + && apt-get install -y --no-install-recommends /tmp/chrome.deb \ + && chrome_version="$(google-chrome --version | awk '{print $3}')" \ + && wget -q -O /tmp/chromedriver.zip "https://storage.googleapis.com/chrome-for-testing-public/${chrome_version}/linux64/chromedriver-linux64.zip" \ + && unzip -j /tmp/chromedriver.zip chromedriver-linux64/chromedriver -d /usr/local/bin \ + && chmod +x /usr/local/bin/chromedriver \ + && rm -f /tmp/chrome.deb /tmp/chromedriver.zip \ + && rm -rf /var/lib/apt/lists/* + +# BaseTest picks the driver up from CHROMEWEBDRIVER instead of downloading one. +ENV CHROMEWEBDRIVER=/usr/local/bin + +WORKDIR /automation + +COPY pom.xml ./ +RUN mvn -ntp -B dependency:go-offline + +COPY src ./src + +RUN mvn -ntp -B test-compile + +ENV SUITE_XML_FILE=./src/test/resources/suites/testng.xml \ + BROWSER=chrome + +ENTRYPOINT ["/bin/sh", "-c", "mvn -ntp -B test -DsuiteXmlFile=$SUITE_XML_FILE -Dbrowser=$BROWSER -Dheadless=true"] diff --git a/README.md b/README.md index 33ab4ca..2ae2cd7 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,91 @@ public class GoogleSearchTest extends BaseTest { ``` 4.Execute the test cases by maven command `mvn clean test` +Runtime options +--- + +| Option | Default | Description | +| --- | --- | --- | +| `-DsuiteXmlFile=` | `./src/test/resources/suites/testng.xml` | TestNG suite executed by Surefire. | +| `-Dbrowser=` | `chrome` | Browser started by `BaseTest`. | +| `-Dheadless=` | `true` | Chrome uses `--headless=new`, Firefox uses `-headless`. | + +`BaseTest` uses a driver binary already present on the machine when +`webdriver.chrome.driver` / `webdriver.gecko.driver` is set or when the +`CHROMEWEBDRIVER` / `GECKOWEBDRIVER` directories exist (as on GitHub-hosted +runners and in the provided `Dockerfile`), and only falls back to a +WebDriverManager download otherwise. This keeps CI runs working without network +access to the driver mirrors. + +The JDK must be 11 or newer because Selenium 4.25 ships Java 11 class files; +the produced bytecode still targets Java 8 (`maven.compiler.source/target`). + +CI/CD +--- + +### `.github/workflows/ci.yml` + +Triggers: push to `main`/`master`, pull requests, a nightly cron (02:00 UTC, +full suite on both browsers) and `workflow_dispatch`. + +Manual-run inputs: `suite` (testng.xml path), `browser` (`chrome`, `firefox` or +`both`) and `thread_count` (rewritten into a copy of the suite file before the +run). + +Jobs: + +1. **Compile check** - `mvn -ntp -B clean test-compile` on Temurin 17 with the Maven cache. +2. **UI suite** - runs the TestNG suite headlessly on a `chrome`/`firefox` matrix, publishes the ExtentReports HTML, `logfile.log`, Surefire/TestNG results (screenshots are embedded in the Extent report as base64) and writes a pass/fail table into the job summary. +3. **Dependency vulnerability scan** - Trivy filesystem scan uploaded as SARIF, plus the resolved dependency tree as an artifact. +4. **Dependency review** - `actions/dependency-review-action` on pull requests. + +The suite job is deliberately non-blocking (`continue-on-error` plus +`-Dmaven.test.failure.ignore=true`): the demo tests drive public websites +(`google.co.in`, `facebook.com`) and `FaceBookLoginTest` asserts `false` on +purpose, so an outage or the intentional failure must not break CI. Point +`suite` at your own suite and remove those flags to make the suite gating. + +### `.github/workflows/cd.yml` + +A **mocked** AWS delivery pipeline for demos: it builds the runner container +(`Dockerfile`), "pushes" it to Amazon ECR and "runs" the suite as an ECS task in +`staging` and then `production`, finally "publishing" the Extent report to S3. + +Every AWS identifier in the workflow is a placeholder and is commented as such: + +| Value | Placeholder | +| --- | --- | +| Account / registry | `123456789012.dkr.ecr.us-east-1.amazonaws.com` | +| ECR repository, ECS cluster/namespace | `demo-selenium-testng` | +| ECS task definition | `demo-selenium-testng-runner` | +| Reports bucket | `s3://demo-selenium-testng-reports` | +| OIDC role | `arn:aws:iam::123456789012:role/demo-selenium-testng-github-oidc` | + +Safety gate: the workflow is `workflow_dispatch`-only and runs in DRY_RUN mode +unless the `dry_run` input is `false` **and** the repository variable +`ENABLE_REAL_AWS_DEPLOY` is exactly `true`. In DRY_RUN the image is built but +nothing authenticates to AWS - the `aws` commands are printed into the job +summary instead. + +### Required secrets and variables + +Nothing has to be configured for the demo. To point the pipelines at real +infrastructure: + +| Name | Kind | Purpose | +| --- | --- | --- | +| `ENABLE_REAL_AWS_DEPLOY` | repository variable | Must be `true` (with `dry_run=false`) before any AWS call is made. | +| `OIDC_ROLE_ARN`, `AWS_ACCOUNT_ID`, `AWS_REGION`, `ECR_REGISTRY`, `ECR_REPOSITORY`, `ECS_CLUSTER`, `ECS_TASK_DEFINITION`, `REPORTS_BUCKET` | workflow `env` (placeholders today) | Replace with real values; the OIDC role must trust this repository. | +| `GITHUB_TOKEN` | provided automatically | Used by `setup-geckodriver` and SARIF upload. | + +GitHub environments `staging` and `production` must exist; add required +reviewers to `production` so the last stage waits for approval. + +### `.github/dependabot.yml` + +Weekly updates for Maven dependencies (Selenium/WebDriverManager grouped), +GitHub Actions and the Dockerfile base image. + --- Reproting diff --git a/pom.xml b/pom.xml index 4eecc16..2ecab2e 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,8 @@ UTF-8 1.8 1.8 + + ./src/test/resources/suites/testng.xml

x

@@ -65,7 +67,7 @@ 2.19.1 - ./src/test/resources/suites/testng.xml + ${suiteXmlFile} diff --git a/src/test/java/example/example/tests/BaseTest.java b/src/test/java/example/example/tests/BaseTest.java index b962ab1..e7c65b6 100644 --- a/src/test/java/example/example/tests/BaseTest.java +++ b/src/test/java/example/example/tests/BaseTest.java @@ -1,10 +1,13 @@ package example.example.tests; +import java.io.File; import java.util.concurrent.TimeUnit; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; import org.testng.ITestContext; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterSuite; @@ -65,17 +68,83 @@ public void wrapAllUp(ITestContext context) { */ @BeforeClass protected void setup() { -// System.setProperty("webdriver.chrome.driver", Constants.CHROME_DRIVER_PATH); - WebDriverManager.chromedriver().setup(); + String browser = System.getProperty("browser", "chrome").toLowerCase(); + boolean headless = Boolean.parseBoolean(System.getProperty("headless", "true")); + if ("firefox".equals(browser)) { + driver = createFirefoxDriver(headless); + } else { + driver = createChromeDriver(headless); + } + driver.manage().window().maximize(); + driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); + WebDriverContext.setDriver(driver); + } + + /** + * Creates a Chrome driver. A driver binary provided by the environment + * (webdriver.chrome.driver or CHROMEWEBDRIVER) is preferred so that no + * download is needed; WebDriverManager is used as the fallback. + * + * @param headless whether to run without a visible browser window + * @return the driver + */ + private WebDriver createChromeDriver(boolean headless) { + if (!resolveDriverBinary("webdriver.chrome.driver", "CHROMEWEBDRIVER", "chromedriver")) { + WebDriverManager.chromedriver().setup(); + } ChromeOptions ops = new ChromeOptions(); ops.addArguments("disable-infobars"); - ops.addArguments("--headless"); + if (headless) { + ops.addArguments("--headless=new"); + } ops.addArguments("--no-sandbox"); ops.addArguments("--disable-dev-shm-usage"); - driver = new ChromeDriver(ops); - driver.manage().window().maximize(); - driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); - WebDriverContext.setDriver(driver); + ops.addArguments("--window-size=1920,1080"); + return new ChromeDriver(ops); + } + + /** + * Creates a Firefox driver. + * + * @param headless whether to run without a visible browser window + * @return the driver + */ + private WebDriver createFirefoxDriver(boolean headless) { + if (!resolveDriverBinary("webdriver.gecko.driver", "GECKOWEBDRIVER", "geckodriver")) { + WebDriverManager.firefoxdriver().setup(); + } + FirefoxOptions ops = new FirefoxOptions(); + if (headless) { + ops.addArguments("-headless"); + } + ops.addArguments("--width=1920"); + ops.addArguments("--height=1080"); + return new FirefoxDriver(ops); + } + + /** + * Points the given webdriver system property at a driver binary already + * present on the machine, if one can be located. + * + * @param systemProperty the webdriver system property + * @param directoryVariable the environment variable holding the driver directory + * @param binaryName the driver executable name + * @return true when the system property is set to an existing binary + */ + private boolean resolveDriverBinary(String systemProperty, String directoryVariable, String binaryName) { + String configured = System.getProperty(systemProperty); + if (configured != null && new File(configured).isFile()) { + return true; + } + String directory = System.getenv(directoryVariable); + if (directory != null) { + File binary = new File(directory, binaryName); + if (binary.isFile()) { + System.setProperty(systemProperty, binary.getAbsolutePath()); + return true; + } + } + return false; } /** @@ -84,7 +153,8 @@ protected void setup() { @AfterClass public void wrapUp() { if (driver != null) { - driver.close(); + // quit() closes every window and ends the session; calling close() + // first ends the session already on single-window browsers. driver.quit(); } } From dbf555c27d7cde36b46cccf7b3b71e3c0c5e697c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:31:28 +0000 Subject: [PATCH 2/4] Pin trivy-action to an existing tag --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27c8775..82ef414 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -197,7 +197,7 @@ jobs: run: mvn -ntp -B dependency:tree -DoutputFile=target/dependency-tree.txt - name: Scan dependencies with Trivy - uses: aquasecurity/trivy-action@0.28.0 + uses: aquasecurity/trivy-action@v0.36.0 with: scan-type: fs scan-ref: . From d9547a8cd686e8ee74c714cfce47327808514e66 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:33:38 +0000 Subject: [PATCH 3/4] Make dependency review non-blocking until dependency graph is enabled --- .github/workflows/ci.yml | 4 ++++ README.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82ef414..c161a8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,7 +233,11 @@ jobs: pull-requests: write steps: - uses: actions/checkout@v4 + # Requires the repository's Dependency graph to be enabled (Settings > + # Code security). Non-blocking so the workflow still passes while it is + # off; Trivy remains the gating-capable scanner. - uses: actions/dependency-review-action@v4 + continue-on-error: true with: fail-on-severity: critical comment-summary-in-pr: on-failure diff --git a/README.md b/README.md index 2ae2cd7..43781ca 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Jobs: 1. **Compile check** - `mvn -ntp -B clean test-compile` on Temurin 17 with the Maven cache. 2. **UI suite** - runs the TestNG suite headlessly on a `chrome`/`firefox` matrix, publishes the ExtentReports HTML, `logfile.log`, Surefire/TestNG results (screenshots are embedded in the Extent report as base64) and writes a pass/fail table into the job summary. 3. **Dependency vulnerability scan** - Trivy filesystem scan uploaded as SARIF, plus the resolved dependency tree as an artifact. -4. **Dependency review** - `actions/dependency-review-action` on pull requests. +4. **Dependency review** - `actions/dependency-review-action` on pull requests. It needs the repository's Dependency graph enabled (Settings > Code security) and is non-blocking until then. The suite job is deliberately non-blocking (`continue-on-error` plus `-Dmaven.test.failure.ignore=true`): the demo tests drive public websites From f3573b1633209e0f5802854b767faeedf41484dd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 22:35:23 +0000 Subject: [PATCH 4/4] Check out repo in CD run-suite job and correct summary wording --- .github/scripts/testng_summary.py | 2 +- .github/workflows/cd.yml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/scripts/testng_summary.py b/.github/scripts/testng_summary.py index 543bb00..d0de0bb 100644 --- a/.github/scripts/testng_summary.py +++ b/.github/scripts/testng_summary.py @@ -59,7 +59,7 @@ def main(): lines.append("") lines.append( "> The demo suite drives public websites and `FaceBookLoginTest` " - "asserts false by design, so failures here do not fail the workflow." + "fails by design, so failures here do not fail the workflow." ) else: lines.append("All tests passed.") diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3e32210..a28bbf3 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -137,6 +137,8 @@ jobs: environment: name: ${{ matrix.environment }} steps: + - uses: actions/checkout@v4 + - name: Configure AWS credentials via OIDC (real run only) if: needs.guard.outputs.dry_run == 'false' uses: aws-actions/configure-aws-credentials@v4 @@ -182,6 +184,9 @@ jobs: run: | destination="s3://$REPORTS_BUCKET/$TARGET_ENV/${GITHUB_RUN_ID}/" if [ "${{ needs.guard.outputs.dry_run }}" = "false" ]; then + # A real run would pull the reports the ECS task wrote to its own + # volume first; the demo has no such volume. + mkdir -p ExtentReports aws s3 cp ExtentReports/ "$destination" --recursive else {