From ca1028c2600d88fb017c71ad0a3e06ebda82dd41 Mon Sep 17 00:00:00 2001 From: Timur Isaev Date: Thu, 27 Aug 2026 15:55:27 -0700 Subject: [PATCH] Post E2E results to pull requests Discover the same-repository pull request associated with an E2E candidate and post a compact, updatable results summary. Keep candidate execution read-only and perform pull request updates in a separate trusted reporting job. Recheck the pull request head before posting, validate successful-run evidence, and map untrusted artifact values to fixed labels before including them in the comment. Continue running E2E normally when no matching pull request exists. --- .github/workflows/e2e.yml | 313 ++++++++++++++++++- script/e2e/post-pr-summary.sh | 490 ++++++++++++++++++++++++++++++ script/e2e/scenarios/lifecycle.sh | 5 - 3 files changed, 798 insertions(+), 10 deletions(-) create mode 100644 script/e2e/post-pr-summary.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 35202ff..8567291 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -8,8 +8,9 @@ on: required: true type: string -permissions: - contents: read +# Permissions are granted per job. Candidate-controlled code must not receive a +# token capable of modifying pull requests. +permissions: {} # Only one gh-elm E2E workflow may use the shared migration environment at a # time. A new run waits instead of cancelling a migration already in progress. @@ -22,9 +23,14 @@ jobs: name: Resolve candidate revision runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + outputs: sha: ${{ steps.candidate.outputs.sha }} harness_present: ${{ steps.candidate.outputs.harness_present }} + pull_request_number: ${{ steps.pull-request.outputs.number }} steps: - name: Validate candidate input @@ -126,6 +132,85 @@ jobs: fi } >>"$GITHUB_STEP_SUMMARY" + - name: Find pull request for candidate + id: pull-request + env: + GH_TOKEN: ${{ github.token }} + CANDIDATE_SHA: ${{ steps.candidate.outputs.sha }} + shell: bash + run: | + set -euo pipefail + + if [[ ! "$CANDIDATE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Candidate output is not a full commit SHA." + exit 1 + fi + + associated_pull_requests="$( + gh api \ + "repos/$GITHUB_REPOSITORY/commits/$CANDIDATE_SHA/pulls?per_page=100" + )" + + mapfile -t pull_request_numbers < <( + jq -r \ + --arg candidate_sha "$CANDIDATE_SHA" \ + --arg repository "$GITHUB_REPOSITORY" ' + .[] + | select( + .state == "open" and + .head.sha == $candidate_sha and + .head.repo.full_name == $repository + ) + | .number + ' <<<"$associated_pull_requests" + ) + + case "${#pull_request_numbers[@]}" in + 0) + pull_request_number="" + + echo "::notice::No matching open pull request has candidate commit $CANDIDATE_SHA as its current head; E2E will run without posting a PR comment." + ;; + 1) + pull_request_number="${pull_request_numbers[0]}" + ;; + *) + echo "::error::Multiple matching open pull requests have candidate commit $CANDIDATE_SHA as their current head." + printf 'Matching pull requests: #%s\n' \ + "${pull_request_numbers[@]}" + exit 1 + ;; + esac + + if [[ -n "$pull_request_number" && + ! "$pull_request_number" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::GitHub returned an invalid pull request number." + exit 1 + fi + + echo "number=$pull_request_number" >>"$GITHUB_OUTPUT" + + if [[ -n "$pull_request_number" ]]; then + { + echo + echo "### Pull request" + echo + echo "- Automatically identified PR: \`#$pull_request_number\`" + echo "- Matching head commit: \`$CANDIDATE_SHA\`" + } >>"$GITHUB_STEP_SUMMARY" + + echo "Candidate commit $CANDIDATE_SHA is the current head of PR #$pull_request_number." + else + { + echo + echo "### Pull request" + echo + echo "No matching open pull request was found." + echo + echo "E2E will run without posting a PR comment." + } >>"$GITHUB_STEP_SUMMARY" + fi + no-harness: name: E2E harness not present needs: resolve @@ -154,8 +239,13 @@ jobs: if: needs.resolve.outputs.harness_present == 'true' runs-on: ubuntu-latest - # This is the only job referencing the protected environment. One approval - # therefore covers candidate setup and both scenarios. + # Candidate-controlled code receives only read access to repository + # contents. It cannot modify pull requests. + permissions: + contents: read + + # This is the only secret-bearing job. Environment approval authorizes the + # immutable candidate commit to run against the migration test environment. environment: name: migration-e2e @@ -176,7 +266,8 @@ jobs: TARGET_ORG: ${{ vars.TARGET_ORG }} TARGET_VISIBILITY: ${{ vars.TARGET_VISIBILITY }} - # GitHub CLI requires GH_TOKEN for extension installation and invocation. + # Candidate code needs GitHub CLI authentication for extension + # installation and invocation. This job's token is read-only. GH_TOKEN: ${{ github.token }} steps: @@ -205,6 +296,18 @@ jobs: echo "Testing immutable commit: $actual_sha" + # Ensure that artifact upload has at least one file even when candidate + # setup or scenario execution fails before producing scenario evidence. + - name: Prepare E2E results directory + env: + CANDIDATE_SHA: ${{ needs.resolve.outputs.sha }} + shell: bash + run: | + set -euo pipefail + + mkdir -p elm-results + printf '%s\n' "$CANDIDATE_SHA" >elm-results/candidate-sha.txt + - name: Validate required environment configuration shell: bash run: | @@ -398,3 +501,203 @@ jobs: path: elm-results/ if-no-files-found: warn retention-days: 14 + + report: + name: Report E2E results + needs: + - resolve + - e2e + + # Run after success or failure, but only when E2E was started and a matching + # pull request was identified. + if: >- + always() && + needs.resolve.result == 'success' && + needs.resolve.outputs.harness_present == 'true' && + needs.resolve.outputs.pull_request_number != '' && + needs.e2e.result != 'skipped' + + runs-on: ubuntu-latest + + # This environment contains no secrets and requires no approval. Its + # deployment branch policy must allow only the default branch, ensuring that + # this write-capable job cannot run from a modified workflow on another ref. + environment: + name: migration-e2e-reporting + + # Only this trusted job may modify pull requests. + permissions: + actions: read + contents: read + pull-requests: write + + steps: + # The reporting environment restricts this job to the default branch, so + # github.sha identifies the immutable trusted workflow revision. Never + # load the reporter from the candidate selected through candidate_ref. + - name: Check out trusted reporter + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + persist-credentials: false + path: trusted + + - name: Validate trusted reporter + shell: bash + run: | + set -euo pipefail + + reporter="trusted/script/e2e/post-pr-summary.sh" + + if [[ ! -f "$reporter" ]]; then + echo "::error file=$reporter::The trusted workflow revision does not contain the PR summary reporter." + exit 1 + fi + + if ! bash -n "$reporter"; then + echo "::error file=$reporter::Bash syntax validation failed." + exit 1 + fi + + echo "The trusted PR summary reporter passed Bash syntax validation." + + - name: Download E2E evidence + env: + GH_TOKEN: ${{ github.token }} + ARTIFACT_NAME: gh-elm-e2e-${{ github.run_id }}-${{ github.run_attempt }} + RESULTS_DIR: ${{ runner.temp }}/elm-results + E2E_RESULT: ${{ needs.e2e.result }} + shell: bash + run: | + set -euo pipefail + + mkdir -p \ + "$RESULTS_DIR/control-plane" \ + "$RESULTS_DIR/lifecycle" + + if gh run download "$GITHUB_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "$ARTIFACT_NAME" \ + --dir "$RESULTS_DIR"; then + echo "Downloaded E2E evidence artifact: $ARTIFACT_NAME" + else + if [[ "$E2E_RESULT" == "success" ]]; then + echo "::error::The E2E job passed, but its results artifact could not be downloaded." + exit 1 + fi + + echo "::warning::No results artifact is available for the unsuccessful E2E job. Scenario results will be shown as not run." + fi + + - name: Validate successful E2E results + if: needs.e2e.result == 'success' + env: + CANDIDATE_SHA: ${{ needs.resolve.outputs.sha }} + RESULTS_DIR: ${{ runner.temp }}/elm-results + shell: bash + run: | + set -euo pipefail + + candidate_file="$RESULTS_DIR/candidate-sha.txt" + control_plane_results="$RESULTS_DIR/control-plane/results.tsv" + lifecycle_results="$RESULTS_DIR/lifecycle/results.tsv" + + if [[ ! -f "$candidate_file" ]]; then + echo "::error::The successful E2E artifact does not contain candidate-sha.txt." + exit 1 + fi + + artifact_candidate_sha="$(<"$candidate_file")" + + if [[ "$artifact_candidate_sha" != "$CANDIDATE_SHA" ]]; then + echo "::error::The E2E artifact does not match the resolved candidate." + exit 1 + fi + + for results_file in \ + "$control_plane_results" \ + "$lifecycle_results"; do + if [[ ! -s "$results_file" ]]; then + echo "::error::The successful E2E artifact is missing a required non-empty scenario results file." + exit 1 + fi + done + + echo "The successful E2E artifact contains results for both scenarios." + + # The PR may advance while E2E waits for environment approval or executes. + # Do not replace the current sticky comment with results for an older head. + - name: Verify pull request still matches candidate + id: pull-request + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ needs.resolve.outputs.pull_request_number }} + CANDIDATE_SHA: ${{ needs.resolve.outputs.sha }} + shell: bash + run: | + set -euo pipefail + + if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + echo "::error::Resolved pull request number is invalid." + exit 1 + fi + + if [[ ! "$CANDIDATE_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::Resolved candidate SHA is invalid." + exit 1 + fi + + pr_details="$( + gh api \ + "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" + )" + + current_state="$( + jq -er '.state' <<<"$pr_details" + )" + + current_head="$( + jq -er '.head.sha' <<<"$pr_details" + )" + + current_head_repository="$( + jq -er '.head.repo.full_name' <<<"$pr_details" + )" + + if [[ "$current_state" != "open" ]]; then + echo "::notice::PR #$PR_NUMBER is no longer open; skipping the E2E comment." + echo "should_post=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "$current_head_repository" != "$GITHUB_REPOSITORY" ]]; then + echo "::notice::PR #$PR_NUMBER no longer has a head branch in this repository; skipping the E2E comment." + echo "should_post=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + if [[ "$current_head" != "$CANDIDATE_SHA" ]]; then + echo "::notice::PR #$PR_NUMBER has advanced from $CANDIDATE_SHA to $current_head; skipping the stale E2E comment." + echo "should_post=false" >>"$GITHUB_OUTPUT" + exit 0 + fi + + echo "should_post=true" >>"$GITHUB_OUTPUT" + echo "PR #$PR_NUMBER still points to candidate commit $CANDIDATE_SHA." + + - name: Post E2E results to pull request + if: steps.pull-request.outputs.should_post == 'true' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ needs.resolve.outputs.pull_request_number }} + CANDIDATE_SHA: ${{ needs.resolve.outputs.sha }} + JOB_STATUS: ${{ needs.e2e.result }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + CONTROL_PLANE_RESULTS: ${{ runner.temp }}/elm-results/control-plane/results.tsv + LIFECYCLE_RESULTS: ${{ runner.temp }}/elm-results/lifecycle/results.tsv + shell: bash + run: | + set -euo pipefail + + bash trusted/script/e2e/post-pr-summary.sh \ + --output "$RUNNER_TEMP/gh-elm-e2e-pr-summary.md" diff --git a/script/e2e/post-pr-summary.sh b/script/e2e/post-pr-summary.sh new file mode 100644 index 0000000..52225cb --- /dev/null +++ b/script/e2e/post-pr-summary.sh @@ -0,0 +1,490 @@ +#!/usr/bin/env bash +# +# Render gh-elm E2E scenario results as a compact Markdown report and create +# or update a sticky pull request comment. +# +# Scenario result artifacts are treated as untrusted input. Check identifiers +# and statuses are mapped to fixed labels before publication. Notes from +# results.tsv are intentionally ignored because they can contain internal +# hosts, repository names, migration IDs, and other detailed evidence. +# +# Required environment variables: +# +# PR_NUMBER +# CANDIDATE_SHA +# JOB_STATUS +# WORKFLOW_URL +# CONTROL_PLANE_RESULTS +# LIFECYCLE_RESULTS +# +# Posting additionally requires: +# +# GITHUB_REPOSITORY +# GH_TOKEN +# +# Usage: +# +# post-pr-summary.sh [--render-only] [--output PATH] +# +# --render-only renders the comment without making a GitHub API request. + +set -Eeuo pipefail + +readonly COMMENT_MARKER='' + +RENDER_ONLY=0 +OUTPUT_FILE="${RUNNER_TEMP:-/tmp}/gh-elm-e2e-pr-summary.md" +PAYLOAD_FILE="" + +cleanup() { + if [[ -n "${PAYLOAD_FILE:-}" ]]; then + rm -f -- "$PAYLOAD_FILE" + PAYLOAD_FILE="" + fi + + return 0 +} + +trap cleanup EXIT + +usage() { + cat <<'EOF' +Usage: post-pr-summary.sh [--render-only] [--output PATH] + +Render E2E results and create or update a pull request comment. + +Options: + --render-only Render the Markdown file without posting it. + --output PATH Write the rendered Markdown to PATH. + -h, --help Show this help text. +EOF +} + +die() { + printf 'ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_variable() { + local name="$1" + + if [[ -z "${!name:-}" ]]; then + die "Required environment variable $name is missing." + fi +} + +require_command() { + local name="$1" + + if ! command -v "$name" >/dev/null 2>&1; then + die "Required command $name was not found on PATH." + fi +} + +parse_arguments() { + while (($# > 0)); do + case "$1" in + --render-only) + RENDER_ONLY=1 + shift + ;; + --output) + if (($# < 2)); then + die "--output requires a path." + fi + + OUTPUT_FILE="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + die "Unknown argument: $1" + ;; + esac + done +} + +validate_inputs() { + require_variable PR_NUMBER + require_variable CANDIDATE_SHA + require_variable JOB_STATUS + require_variable WORKFLOW_URL + require_variable CONTROL_PLANE_RESULTS + require_variable LIFECYCLE_RESULTS + + if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then + die "PR_NUMBER must be a positive integer." + fi + + if [[ ! "$CANDIDATE_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then + die "CANDIDATE_SHA must be a full 40-character commit SHA." + fi + + if [[ "$WORKFLOW_URL" != https://* ]]; then + die "WORKFLOW_URL must be an HTTPS URL." + fi + + if [[ "$WORKFLOW_URL" =~ [[:space:]\)\(\<\>] ]]; then + die "WORKFLOW_URL contains unsupported characters." + fi + + if [[ -z "$OUTPUT_FILE" ]]; then + die "The output path cannot be empty." + fi + + if ((RENDER_ONLY == 0)); then + require_variable GITHUB_REPOSITORY + require_variable GH_TOKEN + + require_command gh + require_command jq + + if [[ ! "$GITHUB_REPOSITORY" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + die "GITHUB_REPOSITORY must be in owner/repository format." + fi + fi +} + +trusted_check_label() { + local scenario_id="$1" + local check_identifier="$2" + + # The result artifact is produced by candidate-controlled code. Never return + # check_identifier directly. Every published label must be a fixed string + # selected by an exact scenario-specific match. + case "$scenario_id:$check_identifier" in + # Checks shared by both scenarios. + "control-plane:Configuration" | "lifecycle:Configuration") + printf 'Configuration' + ;; + "control-plane:Dependencies" | "lifecycle:Dependencies") + printf 'Dependencies' + ;; + "control-plane:Harness" | "lifecycle:Harness") + printf 'Harness' + ;; + "control-plane:Evidence" | "lifecycle:Evidence") + printf 'Evidence' + ;; + "control-plane:Migration ownership" | "lifecycle:Migration ownership") + printf 'Migration ownership' + ;; + "control-plane:Preflight" | "lifecycle:Preflight") + printf 'Preflight' + ;; + "control-plane:Cleanup" | "lifecycle:Cleanup") + printf 'Cleanup' + ;; + "control-plane:Overall result" | "lifecycle:Overall result") + printf 'Overall result' + ;; + + # Control-plane checks. + "control-plane:Create primary migration") + printf 'Create primary migration' + ;; + "control-plane:Migration status") + printf 'Migration status' + ;; + "control-plane:Create pagination migration") + printf 'Create pagination migration' + ;; + "control-plane:List pagination") + printf 'List pagination' + ;; + "control-plane:Cancel primary migration") + printf 'Cancel primary migration' + ;; + + # Lifecycle checks. + "lifecycle:Create lifecycle migration") + printf 'Create lifecycle migration' + ;; + "lifecycle:Initial migration status") + printf 'Initial migration status' + ;; + "lifecycle:Start migration") + printf 'Start migration' + ;; + "lifecycle:Target migration ID") + printf 'Target migration ID' + ;; + "lifecycle:Target migration ID validation") + printf 'Target migration ID validation' + ;; + "lifecycle:Wait for cutover readiness") + printf 'Wait for cutover readiness' + ;; + "lifecycle:Target resources") + printf 'Target resources' + ;; + "lifecycle:Initiate cutover") + printf 'Initiate cutover' + ;; + "lifecycle:Wait for cutover completion") + printf 'Wait for cutover completion' + ;; + "lifecycle:Revert cutover") + printf 'Revert cutover' + ;; + "lifecycle:Verify reverted state") + printf 'Verify reverted state' + ;; + *) + return 1 + ;; + esac +} + +trusted_status_label() { + local status="$1" + + # Never publish an artifact-provided status directly. + case "$status" in + "✅ pass" | "pass" | "success") + printf '✅ Passed' + ;; + "❌ fail" | "fail" | "failure") + printf '❌ Failed' + ;; + "⏭️ skip" | "skip" | "skipped") + printf '⏭️ Skipped' + ;; + *) + return 1 + ;; + esac +} + +overall_status() { + # JOB_STATUS is provided by GitHub Actions rather than by the downloaded + # candidate artifact. + case "$JOB_STATUS" in + success) + printf '✅ Passed' + ;; + failure) + printf '❌ Failed' + ;; + cancelled) + printf '⏹️ Cancelled' + ;; + skipped) + printf '⏭️ Skipped' + ;; + *) + printf '⚠️ Unknown' + ;; + esac +} + +append_scenario_results() { + local scenario_id="$1" + local scenario_name="$2" + local results_file="$3" + local check_identifier + local status + local ignored_note + local trusted_label + local displayed_status + local rendered_rows=0 + local line_number=0 + + { + printf '\n' + printf '### %s scenario\n' "$scenario_name" + printf '\n' + printf '| Check | Status |\n' + printf '| --- | --- |\n' + } >>"$OUTPUT_FILE" + + if [[ ! -s "$results_file" ]]; then + printf '| Scenario | ⏭️ Not run |\n' >>"$OUTPUT_FILE" + return 0 + fi + + # results.tsv has the following format: + # + # checkstatusnote + # + # All fields are untrusted. The check identifier and status are used only for + # exact matching against fixed values. The note is never rendered or logged. + while IFS=$'\t' read -r check_identifier status ignored_note; do + line_number=$((line_number + 1)) + + if [[ -z "$check_identifier" || -z "$status" ]]; then + die "Malformed row in $scenario_id results at line $line_number." + fi + + if ! trusted_label="$( + trusted_check_label "$scenario_id" "$check_identifier" + )"; then + # Do not print the rejected identifier because it may contain a secret. + die "Unexpected check identifier in $scenario_id results at line $line_number." + fi + + if ! displayed_status="$( + trusted_status_label "$status" + )"; then + # Do not print the rejected status because it may contain a secret. + die "Unexpected status in $scenario_id results at line $line_number." + fi + + printf '| %s | %s |\n' \ + "$trusted_label" \ + "$displayed_status" \ + >>"$OUTPUT_FILE" + + rendered_rows=$((rendered_rows + 1)) + done <"$results_file" + + if ((rendered_rows == 0)); then + printf '| Scenario | ⏭️ Not run |\n' >>"$OUTPUT_FILE" + fi +} + +render_comment() { + local output_directory + local temporary_output + local final_output + local result + + output_directory="$(dirname "$OUTPUT_FILE")" + + if [[ ! -d "$output_directory" ]]; then + die "Output directory does not exist: $output_directory" + fi + + temporary_output="${OUTPUT_FILE}.tmp" + result="$(overall_status)" + + rm -f -- "$temporary_output" + + { + printf '%s\n' "$COMMENT_MARKER" + printf '## gh-elm E2E results\n' + printf '\n' + printf '**Overall result:** %s\n' "$result" + printf '\n' + printf -- '- Candidate: `%s`\n' "$CANDIDATE_SHA" + printf -- '- Workflow run: [View logs](%s)\n' "$WORKFLOW_URL" + } >"$temporary_output" + + # Point the scenario renderer at the temporary file so the complete report + # can be moved into place atomically. + final_output="$OUTPUT_FILE" + OUTPUT_FILE="$temporary_output" + + append_scenario_results \ + "control-plane" \ + "Control-plane" \ + "$CONTROL_PLANE_RESULTS" + + append_scenario_results \ + "lifecycle" \ + "Lifecycle" \ + "$LIFECYCLE_RESULTS" + + OUTPUT_FILE="$final_output" + + if ! mv -- "$temporary_output" "$OUTPUT_FILE"; then + rm -f -- "$temporary_output" + die "Failed to finalize the rendered comment." + fi + + printf 'Rendered E2E pull request summary: %s\n' "$OUTPUT_FILE" +} + +find_existing_comment_id() { + local comment_ids + local comment_id + + # Pull request comments use the issues comments API. Match both the Actions + # bot identity and the hidden marker so unrelated bot comments are ignored. + if ! comment_ids="$( + gh api \ + --paginate \ + "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --jq '.[] | + select(.user.login == "github-actions[bot]") | + select(.body | contains("")) | + .id' + )"; then + die "Failed to retrieve comments for pull request #$PR_NUMBER." + fi + + # Use the first matching comment if an earlier implementation accidentally + # created more than one. + comment_id="${comment_ids%%$'\n'*}" + + if [[ -n "$comment_id" && ! "$comment_id" =~ ^[1-9][0-9]*$ ]]; then + die "GitHub returned an invalid existing comment ID." + fi + + printf '%s' "$comment_id" +} + +write_request_payload() { + local payload_file="$1" + + if ! jq -n \ + --rawfile body "$OUTPUT_FILE" \ + '{body: $body}' >"$payload_file"; then + die "Failed to construct the GitHub comment request." + fi +} + +post_comment() { + local comment_id + + PAYLOAD_FILE="$(mktemp)" + + write_request_payload "$PAYLOAD_FILE" + comment_id="$(find_existing_comment_id)" + + if [[ -n "$comment_id" ]]; then + if ! gh api \ + --method PATCH \ + "repos/$GITHUB_REPOSITORY/issues/comments/$comment_id" \ + --input "$PAYLOAD_FILE" \ + >/dev/null; then + die "Failed to update E2E comment $comment_id on pull request #$PR_NUMBER." + fi + + printf 'Updated E2E summary comment %s on pull request #%s.\n' \ + "$comment_id" \ + "$PR_NUMBER" + else + if ! gh api \ + --method POST \ + "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --input "$PAYLOAD_FILE" \ + >/dev/null; then + die "Failed to create an E2E comment on pull request #$PR_NUMBER." + fi + + printf 'Created E2E summary comment on pull request #%s.\n' \ + "$PR_NUMBER" + fi + + rm -f -- "$PAYLOAD_FILE" + PAYLOAD_FILE="" +} + +main() { + parse_arguments "$@" + validate_inputs + render_comment + + if ((RENDER_ONLY == 1)); then + printf 'Render-only mode enabled; no pull request comment was posted.\n' + return 0 + fi + + post_comment +} + +main "$@" diff --git a/script/e2e/scenarios/lifecycle.sh b/script/e2e/scenarios/lifecycle.sh index 2960a2a..1eb743e 100644 --- a/script/e2e/scenarios/lifecycle.sh +++ b/script/e2e/scenarios/lifecycle.sh @@ -48,11 +48,6 @@ run_scenario() { "$target_migration_id" \ "Target migration ID validation" - record_result \ - "Pause and resume" \ - "⏭️ skip" \ - "Not exercised because the GHES sandbox uses the database-backed work scheduler." - wait_for_cutover_readiness \ "$migration_id" \ "$E2E_CUTOVER_TIMEOUT_SECONDS" \