Skip to content

Automate 3270 regression tests in GitHub Actions

3270Connect runs IBM 3270 workflows headless on a CI runner. Record a business flow in 3270Web, add explicit screen checks, then run the JSON on each change. This walkthrough first checks a bundled sample host: no mainframe account or AI provider is needed.

Before you start

Use a Linux runner with Docker. Set the repository variable TN3270_CONNECT_IMAGE to a reviewed, pinned image reference such as ghcr.io/3270io/3270connect@sha256:<your-verified-digest>. Copy a real published digest into that variable; the placeholder is not runnable. The current published image targets linux/amd64.

The sample host is separate from the runner container, so the workflow connects to its Docker network name rather than 127.0.0.1. The latter would address the workflow container itself.

1. Commit a screen assertion

Save this as tests/3270/workflow.json:

{
  "Host": "sample-host",
  "Port": 3270,
  "OutputFilePath": "screens.html",
  "WaitForField": { "Delay": 1, "Retries": 10 },
  "Steps": [
    { "Type": "Connect" },
    {
      "Type": "CheckValue",
      "Coordinates": { "Row": 1, "Column": 29, "Length": 24 },
      "Text": "3270 Example Application"
    },
    { "Type": "AsciiScreenGrab" },
    { "Type": "Disconnect" }
  ]
}

This uses the first bundled 3270Connect sample application and the same title assertion as the repository's sample workflow. A real regression test should also check the business result, not merely that a login page appeared. See Workflow Actions for field coordinates and assertions.

2. Add the workflow

Save as .github/workflows/3270-regression.yml:

name: 3270 regression
on: [push, pull_request, workflow_dispatch]
permissions:
  contents: read

jobs:
  sample-regression:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    env:
      CONNECT_IMAGE: ${{ vars.TN3270_CONNECT_IMAGE }}
    steps:
      - uses: actions/checkout@v4
      - name: Run the sample workflow and assert the result
        shell: bash
        run: |
          set -euo pipefail
          : "${CONNECT_IMAGE:?Set TN3270_CONNECT_IMAGE to a pinned image digest}"
          work="$RUNNER_TEMP/3270-regression"
          network="tn3270-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
          host="${network}-host"
          mkdir -p "$work"
          cp tests/3270/workflow.json "$work/workflow.json"
          docker network create "$network"
          trap 'docker rm -f "$host" >/dev/null 2>&1 || true; docker network rm "$network" >/dev/null 2>&1 || true' EXIT
          docker run -d --name "$host" --network "$network" \
            --network-alias sample-host "$CONNECT_IMAGE" \
            -runApp 1 -runApp-port 3270
          # Wait for the TCP listener without publishing it on the runner host.
          ready=false
          for attempt in {1..30}; do
            if docker run --rm --network "$network" --entrypoint bash \
              "$CONNECT_IMAGE" -c 'echo >/dev/tcp/sample-host/3270' 2>/dev/null; then
              ready=true
              break
            fi
            sleep 1
          done
          if [ "$ready" != true ]; then
            docker logs "$host"
            exit 1
          fi
          docker run --rm --network "$network" \
            --user "$(id -u):$(id -g)" -v "$work:/data" \
            "$CONNECT_IMAGE" -config workflow.json -headless \
            -showConnectionErrors -workflowTimeout 60 -verboseFailures
          python3 - "$work" <<'PY'
          import pathlib, re, sys
          summaries = list(pathlib.Path(sys.argv[1]).glob("logs/summary_*.txt"))
          if len(summaries) != 1:
              raise SystemExit("Expected exactly one saved run summary")
          report = summaries[0].read_text()
          print(report)
          for label, expected in [("Started", 1), ("Completed", 1), ("Failed", 0)]:
              match = re.search(rf"^Total Workflows {label}: (\d+)$", report, re.M)
              if not match or int(match[1]) != expected:
                  raise SystemExit(f"Unexpected workflow total: {label}")
          PY
      - name: Retain sample diagnostics
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: 3270-sample-result
          path: ${{ runner.temp }}/3270-regression/
          retention-days: 7

The saved summary gate matters: the current CLI can finish a failed workflow without returning a failing process exit status. A missing summary, no completed workflow, or a nonzero failure count must fail the check. The parser deliberately fails if the report format changes; review it when upgrading the pinned image.

3. Prove the check can fail

Run the sample successfully, then temporarily change the expected title to an incorrect value. The job must fail and the saved report should identify the failed check. Restore the expected value and confirm it passes again. This is a stronger CI check than treating a completed process as a completed transaction.

Use your test mainframe

Replace the sample host with a reachable test-system address and use a reviewed recording. Choose a self-hosted runner with an approved route to private hosts; a hosted runner does not automatically reach your internal TN3270 service. Keep host TLS validation enabled and configure the terminal model/code page to match the test system. See Basic Usage.

Keep credentials outside committed recordings. Use reviewed runtime field injection or the documented one-time token mechanism where appropriate. Restrict host-test jobs to trusted branches or manual runs; do not give untrusted pull requests host credentials. Screen captures and logs from a real host may contain business data: restrict or omit artifact upload rather than copying this sample's retention policy blindly.

Use one workflow as a regression gate. For controlled performance exercises, continue with Load Testing.