Skip to content

nightly-e2e

nightly-e2e #65

Workflow file for this run

name: nightly-e2e
# Standalone TTY/UX + CLI-contract regression suite. Runs the heavy PTY tests the
# cdk-terrain per-PR CI deliberately skips, against published preview releases.
on:
schedule:
- cron: "0 3 * * *" # 03:00 UTC nightly
workflow_dispatch:
inputs:
cli_ids:
description: 'Space-separated CLI_IDs to test (e.g. "cdktn-next cdktn-latest cdktf-prefork")'
required: false
default: "cdktn-next cdktn-latest"
force:
description: "Run even if the preview version is unchanged"
type: boolean
required: false
default: true
concurrency:
group: nightly-e2e
cancel-in-progress: false
# Least privilege at the workflow level; record-state elevates to contents: write.
permissions:
contents: read
jobs:
# Cron diff-detection: skip the matrix when cdktn-cli@next hasn't moved.
decide:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
next_version: ${{ steps.check.outputs.next_version }}
cli_ids: ${{ steps.ids.outputs.cli_ids }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- id: check
env:
FORCE: ${{ (github.event_name == 'workflow_dispatch' && inputs.force) && '1' || '0' }}
run: node scripts/check-new-preview.mjs
- id: ids
# Pass the dispatch input via env (not direct ${{ }} interpolation into the
# run script) so an operator-supplied value can't inject shell — zizmor
# template-injection. Identical output for normal inputs.
env:
CLI_IDS: ${{ inputs.cli_ids || 'cdktn-next cdktn-latest' }}
run: echo "cli_ids=$CLI_IDS" >> "$GITHUB_OUTPUT"
e2e:
needs: decide
if: needs.decide.outputs.should_run == 'true'
strategy:
fail-fast: false
matrix:
# node-pty 1.1 drives a real PTY on all three: Unix PTY on linux/macOS,
# Windows ConPTY on win (Win10 1809+). conpty validates the inquirer/
# cli-spinners/cli-table3 rendering on Windows terminals too.
os: [ubuntu-latest, macos-latest, windows-latest]
cli_id: [cdktn-next, cdktn-latest] # extend with cdktf-prefork for cross-fork UX deltas
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- run: pnpm install --frozen-lockfile
# node-pty needs a real terraform on PATH for synth/deploy to do anything.
- uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: "1.7.5"
terraform_wrapper: false
- name: Provision CLI under test
run: node scripts/provision.mjs ${{ matrix.cli_id }}
- name: Run e2e
env:
CLI_ID: ${{ matrix.cli_id }}
run: pnpm test
- name: Step summary
if: always()
env:
CLI_ID: ${{ matrix.cli_id }}
run: node scripts/step-summary.mjs reports/ci-report.json
- name: Upload test report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: report-${{ matrix.os }}-${{ matrix.cli_id }}
path: reports/ci-report.json
if-no-files-found: warn
- name: Upload failure screenshots
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: svg-${{ matrix.os }}-${{ matrix.cli_id }}
path: artifacts/
if-no-files-found: ignore
# Publish "last run results" to GitHub Pages via the Actions build mode (NOT a
# legacy branch build). Each matrix leg gets its OWN self-contained report at
# _site/<leg>/index.html (no cross-leg data aggregation — build-report.mjs is
# single-run); a generated _site/index.html landing page links to each leg with a
# pass/fail summary (build-index.mjs). Each nightly cron or manual re-trigger
# overwrites the whole site, so Pages always reflects the latest run.
#
# One-time setup: repo Settings → Pages → Source = "GitHub Actions". Do NOT POST to
# the pages/builds API (that forces a legacy Jekyll build of the branch root).
pages:
needs: [decide, e2e]
if: always() && (needs.e2e.result == 'success' || needs.e2e.result == 'failure')
runs-on: ubuntu-latest
permissions:
contents: read
pages: write # deploy to GitHub Pages
id-token: write # OIDC token the deploy-pages action verifies
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
concurrency:
group: pages-deploy
cancel-in-progress: false
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- name: Download this run's reports + screenshots
env:
GH_TOKEN: ${{ github.token }}
run: |
gh run download ${{ github.run_id }} --pattern 'report-*' --dir _dl || echo "no reports"
gh run download ${{ github.run_id }} --pattern 'svg-*' --dir _dl || echo "no screenshots"
- name: Build a self-contained report per leg + the landing index
env:
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
mkdir -p _site
# One self-contained report per matrix leg → _site/<leg>/index.html.
# Each leg's report is built independently (no cross-leg data aggregation);
# svg-<leg> exists only when that leg had failures (else screenshots omitted).
for d in _dl/report-*/; do
[ -d "$d" ] || continue
leg=$(basename "$d"); leg=${leg#report-}
rep="$d/ci-report.json"
[ -f "$rep" ] || continue
art="_dl/svg-$leg"
[ -d "$art" ] || art="_dl/__no_artifacts__"
echo "Building report for leg: $leg"
node scripts/build-report.mjs --report "$rep" --artifacts "$art" --run-url "$RUN_URL" --out "_site/$leg/index.html"
done
# Landing page linking to each ./<leg>/ with a per-leg pass/fail summary.
node scripts/build-index.mjs --reports-dir _dl --site _site --run-url "$RUN_URL" --out _site/index.html
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: _site
- id: deploy
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
# Surface failures as a dedup'd GitHub issue (the run-once policy means the cron
# does NOT re-run failures; flakes are re-triggered manually via workflow_dispatch
# force=true). One issue per cli_id, aggregated across OS legs. Recovery auto-closes.
report:
needs: [decide, e2e]
if: always() && (needs.e2e.result == 'success' || needs.e2e.result == 'failure')
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
- name: Download per-OS reports
env:
GH_TOKEN: ${{ github.token }}
run: gh run download ${{ github.run_id }} --pattern 'report-*' --dir _reports || echo "no reports to download"
- name: Open/update/close regression issues
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
for id in cdktn-next cdktn-latest; do
files=()
for f in _reports/report-*-"$id"/ci-report.json; do
[ -e "$f" ] && files+=("$f")
done
if [ ${#files[@]} -gt 0 ]; then
node scripts/report-issue.mjs --cli-id "$id" --run-url "$RUN_URL" "${files[@]}"
else
echo "no report for $id"
fi
done
# Record the tested preview version so the next cron can diff against it.
# Run-once policy: advance the marker whether tests PASSED or FAILED (so long as
# the suite actually ran) — the cron must not auto-retry. Skipped/cancelled → skip.
record-state:
needs: [decide, e2e]
if: always() && needs.decide.outputs.next_version != '' && (needs.e2e.result == 'success' || needs.e2e.result == 'failure')
runs-on: ubuntu-latest
permissions:
contents: write # commits state/last-tested.json back to the repo
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: true # required: this job pushes the marker commit
- name: Update last-tested marker
run: |
node -e '
const fs=require("fs");const p="state/last-tested.json";
const s=JSON.parse(fs.readFileSync(p,"utf8"));
s["cdktn-cli@next"]=process.env.V;
fs.writeFileSync(p, JSON.stringify(s,null,2)+"\n");
'
env:
V: ${{ needs.decide.outputs.next_version }}
- name: Commit
# next_version via env (not ${{ }} in the run script) so it can't inject
# shell through the commit message — zizmor template-injection.
env:
V: ${{ needs.decide.outputs.next_version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add state/last-tested.json
git commit -m "chore: record tested preview $V" || echo "no change"
git push